Step 45. 파일 입출력 — 기억하는 프로그램 만들기

Step 45. 파일 입출력 — 기억하는 프로그램 만들기

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★☆☆☆ | 예상 소요 시간 3시간

전제: Step 41~44를 마쳤다. 변수, 조건, 반복, 함수와 모듈을 쓸 수 있다.

  • 준비물: Step 41의 작업대(파이썬 + VSCode). 오늘 만든 파일들은 프로그램과 같은 폴더에 생깁니다.
  • 주의: 오늘 실습은 100% 안전합니다. 다만 "w" 모드는 기존 파일 내용을 전부 지우고 시작하므로, 실습 파일 외의 중요한 파일을 w로 열지 마세요.

메모리(변수)는 휘발성입니다. 프로그램이 꺼지는 순간, 아무리 열심히 모은 데이터도 증발합니다. 그래서 남기고 싶은 것은 디스크, 즉 파일(file)에 적습니다. 파일 입출력은 지루한 기초처럼 보이지만, 실은 이 책에서 가장 재사용되는 기술입니다. 스캔 결과를 저장할 때, 로그를 읽어 분석할 때, 문제 데이터를 불러올 때 — 전부 오늘의 open 한 줄로 시작합니다. 오늘 배우는 세 가지 형식(txt, CSV, JSON)은 데이터 교환의 기본 양식입니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • with open()으로 파일을 만들고, 쓰고, 읽는다
  • 모드 r/w/a의 차이와 w의 위험성을 설명한다
  • encoding="utf-8"이 필요한 이유를 안다
  • CSV를 split으로 읽고, JSON을 json.dump/json.load로 저장·복원한다
  • 읽기 전에 os.path.exists로 존재를 확인하는 안전 패턴을 쓴다
  • 실행할 때마다 기록이 쌓이는 누적 메모장 프로그램을 완성한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3.12 이상, VSCode (Step 41의 작업대)
오늘의 명령어·문법 open(경로, 모드, encoding="utf-8"), with ~ as f:, f.write()/f.read(), 모드 r/w/a, \nstrip(), json.dump/json.load, os.path.exists, enumerate
필요한 개념 파일 모드, 인코딩(UTF-8), txt/CSV/JSON 세 저장 형식, 상대 경로

2-1. 파일 열기 — 세 가지 자세(모드)

파이썬에서 파일은 open()으로 엽니다. 열 때 모드(mode)를 정해야 합니다.

  • "r" (read): 읽기 전용. 파일이 없으면 오류.
  • "w" (write): 새로 쓰기. 기존 내용을 전부 지우고 백지부터 시작합니다.
  • "a" (append): 덧붙이기. 기존 내용 뒤에 이어 씁니다.

이 중 "w"가 가장 위험합니다. 실수로 중요한 파일을 w로 열면 내용이 증발합니다. 덧붙이고 싶었다면 반드시 "a"입니다.

2-2. with 문 — 파일 닫기 자동화

파일은 열었으면 닫아야 합니다. 안 닫으면 저장이 누락되거나 파일이 잠길 수 있습니다. 사람은 까먹는 동물이라, 파이썬에는 자동으로 닫아 주는 문법이 있습니다.

with open("memo.txt", "w", encoding="utf-8") as f:
    f.write("첫 줄\n")

with 블록(들여쓰기 구역)이 끝나는 순간 파이썬이 알아서 파일을 닫습니다. 이것이 현대 파이썬의 표준 자세이고, 이 책에서는 항상 이 방식을 씁니다.

2-3. 인코딩 — 글자를 숫자로 바꾸는 코드표

컴퓨터는 글자를 숫자로 저장하는데, 그때 쓰는 코드표가 인코딩(encoding)입니다. 한글이 깨지는 사고의 대부분은 "쓸 때의 코드표"와 "읽을 때의 코드표"가 달라서입니다. 세계 표준 코드표가 UTF-8이고, 우리는 항상 encoding="utf-8"을 명시합니다. 이 한 습관이 미래의 수많은 한글 깨짐을 예방합니다.

2-4. 세 가지 형식 — 목적에 따른 저장 양식

  • 텍스트(txt): 그냥 글줄. 메모, 로그. 사람이 읽기 가장 쉽습니다.
  • CSV(Comma-Separated Values): 쉼표로 칸을 나눈 표. 이름,점수 한 줄이 한 행이고, 엑셀에서 바로 열립니다.
  • JSON(JavaScript Object Notation): 파이썬의 딕셔너리·리스트와 생긴 것이 거의 같은 텍스트 형식. 중첩 구조를 통째로 저장할 때 표준처럼 씁니다.

3. 따라 하기

3-1. 첫 파일 — 쓰고 확인하기

file1.py 파일을 만듭니다.

with open("memo.txt", "w", encoding="utf-8") as f:
    f.write("오늘 파이썬 파일 입출력을 배웠다.\n")
    f.write("파일은 프로그램이 꺼져도 남는다.\n")
print("저장 완료!")
저장 완료!

(2026-09-09 실측. 실행 후 프로그램과 같은 폴더에 memo.txt가 실제로 생성되었습니다. 더블클릭해 내용을 확인하세요.)

읽는 법: \n은 "줄 바꿈"이라는 특수 문자입니다. write는 친절하게 줄을 바꿔 주지 않으므로, 줄 바꿈이 필요하면 직접 적어 넣습니다.

3-2. 읽기 — 남긴 것 불러오기

file2.py:

with open("memo.txt", "r", encoding="utf-8") as f:
    content = f.read()
print(content)
오늘 파이썬 파일 입출력을 배웠다.
파일은 프로그램이 꺼져도 남는다.

(2026-09-09 실측. 마지막 \n 때문에 빈 줄이 하나 더 보입니다.)

읽는 법: .read()는 파일 전체를 하나의 긴 문자열로 가져옵니다. 프로그램을 껐다 켜도 내용이 그대로인 것 — 이것이 파일의 존재 이유입니다.

줄 단위로 하나씩 처리하고 싶으면 이렇게 합니다 (같은 실측에서 이어서 실행한 출력):

with open("memo.txt", "r", encoding="utf-8") as f:
    for line in f:
        print("읽은 줄:", line.strip())
읽은 줄: 오늘 파이썬 파일 입출력을 배웠다.
읽은 줄: 파일은 프로그램이 꺼져도 남는다.

(2026-09-09 실측.)

읽는 법: 파일을 for로 돌리면 한 줄씩 꺼내 줍니다. .strip()은 줄 끝의 \n을 벗겨 내는 기술입니다. 없이 출력하면 줄이 두 번 바뀌어 보이니, 직접 비교해 보세요.

3-3. 덧붙이기 — 일기장 모드

file3.py:

memo = input("오늘의 메모: ")
with open("diary.txt", "a", encoding="utf-8") as f:
    f.write(memo + "\n")
print("기록되었습니다.")

이 프로그램을 세 번 실행해 서로 다른 메모를 넣습니다. 실측에서 아침에 일찍 일어났다, 점심은 김밥, 파이썬 재미있다를 차례로 입력한 뒤 diary.txt의 내용입니다 (2026-09-09 실측):

아침에 일찍 일어났다
점심은 김밥
파이썬 재미있다

읽는 법: "a" 모드라서 기존 내용이 지워지지 않고 뒤에 붙었습니다. 그럼 "w"였다면? 실측으로 확인한 결과 — 같은 파일에 w로 두 번 쓰면 마지막 것만 남습니다 (2026-09-09 실측: "첫 번째 실행"을 쓴 뒤 w로 "두 번째 실행"을 쓰니 파일 내용이 두 번째 실행 하나뿐이었습니다).

직접 해 보기: 3-3의 코드에서 "a""w"로 바꿔 세 번 실행하고 diary.txt를 확인해 보세요. 이 실험이 오늘 가장 중요한 안전 수칙을 몸에 새깁니다.

3-4. CSV — 표 만들기

file4.py:

with open("scores.csv", "w", encoding="utf-8") as f:
    f.write("이름,점수\n")
    f.write("민수,85\n")
    f.write("지영,92\n")
    f.write("철수,78\n")
print("CSV 저장 완료")

실행 후 scores.csv를 메모장이나 엑셀로 열어 보세요. 쉼표가 칸의 경계입니다. 읽을 때는 Step 42의 split이 그대로 쓰입니다 (이어서 같은 파일에):

with open("scores.csv", "r", encoding="utf-8") as f:
    for line in f:
        name, score = line.strip().split(",")
        print(f"{name}: {score}점")
CSV 저장 완료
이름: 점수점
민수: 85점
지영: 92점
철수: 78점

(2026-09-09 실측.)

읽는 법: line.strip().split(",")은 "줄 끝을 벗기고 쉼표로 쪼갠다"는 뜻입니다. 결과가 두 조각이니 변수 두 개로 받습니다. 실측 출력의 첫 줄 이름: 점수점을 보세요 — 제목 줄까지 데이터처럼 읽힌 것입니다. 제목 줄 건너뛰기는 3-8에서 해결합니다.

3-5. JSON — 구조 통째로 저장하기

file5.py:

import json

host = {"ip": "192.168.0.23", "ports": [22, 80], "os": "ubuntu"}

with open("host.json", "w", encoding="utf-8") as f:
    json.dump(host, f, ensure_ascii=False)

with open("host.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded)
print(loaded["ports"][1])
{'ip': '192.168.0.23', 'ports': [22, 80], 'os': 'ubuntu'}
80

(2026-09-09 실측.)

읽는 법: json.dump(바구니, 파일)은 바구니를 JSON 글자로 변환해 저장하고, json.load(파일)은 읽어서 다시 진짜 딕셔너리로 복원합니다. 복원되었으니 loaded["ports"][1]처럼 바로 꺼내 쓸 수 있습니다. ensure_ascii=False는 "한글을 깨지 않게 그대로 저장해 줘"라는 옵션입니다. CSV는 평평한 표만 되지만, JSON은 중첩 바구니(딕셔너리 안의 리스트)도 통째로 저장됩니다.

3-6. 파일이 없을 때 — 읽기 전 확인

없는 파일을 r로 열면 이런 오류가 납니다 (2026-09-09 실측):

Traceback (most recent call last):
  File "<string>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'no_such_file.txt'

그래서 열기 전에 묻는 것이 안전벨트입니다. file6.py:

import os

filename = "diary.txt"
if os.path.exists(filename):
    with open(filename, "r", encoding="utf-8") as f:
        print(f.read())
else:
    print("아직 일기장이 없습니다. 첫 메모부터 시작하세요.")

읽는 법: os.path.exists(경로)는 "그 파일이 존재하니?"라는 참/거짓 질문이고, os는 운영체제와 대화하는 기본 모듈입니다. 실측에서는 3-3에서 만든 diary.txt가 있어서 그 내용이 출력되었습니다 (2026-09-09 실측). 참고로 os.getcwd()는 "지금 프로그램이 서 있는 폴더"를 알려 줍니다 — 벽 3에서 씁니다.

3-7. enumerate — 줄 번호를 자동으로 매기기

파일을 줄 단위로 읽을 때 번호가 필요한 경우가 많습니다. 손으로 세지 말고 도구를 씁니다.

with open("diary.txt", "r", encoding="utf-8") as f:
    for num, line in enumerate(f, start=1):
        print(f"{num}번째 기록: {line.strip()}")
1번째 기록: 아침에 일찍 일어났다
2번째 기록: 점심은 김밥
3번째 기록: 파이썬 재미있다

(2026-09-09 실측.)

읽는 법: enumerate(바구니, start=1)는 꺼낼 때마다 번호표를 함께 붙여 줍니다. start를 안 쓰면 0부터 셉니다. "몇 번째 줄에서 문제가?"라는 질문은 로그 분석의 단골이고, enumerate가 그 답을 줍니다.

3-8. 형식 변환 실습 — CSV를 JSON으로

두 형식을 오가는 변환기를 만듭니다. 오늘의 종합 체조입니다. convert.py:

import json

students = []
with open("scores.csv", "r", encoding="utf-8") as f:
    first = True
    for line in f:
        if first:          # 첫 줄(제목 줄)은 건너뛰기
            first = False
            continue
        name, score = line.strip().split(",")
        students.append({"name": name, "score": int(score)})

with open("scores.json", "w", encoding="utf-8") as f:
    json.dump(students, f, ensure_ascii=False, indent=2)

print("변환 완료! scores.json을 열어 보세요.")
변환 완료! scores.json을 열어 보세요.

생성된 scores.json의 실제 내용 (2026-09-09 실측):

[
  {
    "name": "민수",
    "score": 85
  },
  {
    "name": "지영",
    "score": 92
  },
  {
    "name": "철수",
    "score": 78
  }
]

읽는 법: CSV를 한 줄씩 읽어 딕셔너리로 만들고, 리스트에 담아 JSON으로 저장합니다. 첫 줄은 제목이라 first 깃발로 건너뜁니다 — 3-4에서 이름: 점수점이 출력되던 문제의 해결책입니다. indent=2는 JSON을 사람이 읽기 좋게 들여써 주는 옵션입니다. "읽어서 → 바구니에 담아서 → 다른 형식으로 저장"하는 변환기는 실무에서 가장 흔한 스크립트 중 하나입니다.


4. 미션과 연습문제

미션 — 누적 메모장

notepad.py를 만들고 다음 요구사항을 조립하세요.

  1. 실행하면 지난 메모들을 번호를 매겨 먼저 보여 준다. (파일이 없으면 첫 메모를 시작하세요 출력)
  2. 새 메모를 입력받아 파일에 덧붙인다.
  3. 메모 앞에 날짜를 자동으로 붙인다. 힌트:
import datetime
today = datetime.date.today().isoformat()   # "2026-09-09" 형태
  1. 저장 형식은 한 줄에 날짜 | 메모로 한다.
  2. 프로그램을 세 번 이상 실행해 누적되는 것을 확인한다.

심화(선택): 메뉴를 만들어 "1. 보기 2. 쓰기 3. 전체 삭제"를 고르게 하고, 3번은 정말 지울 건지 한 번 더 묻게 해 보세요. 삭제는 "w"로 빈 문자열을 쓰면 됩니다.

연습문제

문제 1. 모드 r, w, a의 차이를 각각 한 문장으로 설명하고, 셋 중 데이터를 잃어버릴 위험이 있는 것이 어느 것인지 답해 보세요.

문제 2. encoding="utf-8"을 빼먹으면 어떤 일이 생길 수 있으며, 왜 쓸 때와 읽을 때 둘 다 붙여야 하나요?

문제 3. f.write("첫 줄")f.write("둘째 줄")을 연속으로 실행하면 파일 내용이 어떻게 되나요? 원하는 모양이 두 줄이라면 어떻게 고쳐야 하나요?

문제 4. CSV와 JSON의 역할 차이를 설명하고, "서버 목록인데 각 서버마다 열린 포트 목록이 함께 있다"는 데이터에는 어느 쪽이 맞는지 이유와 함께 답해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

import os
import datetime

filename = "notes.txt"

# 1. 지난 메모 보여 주기
if os.path.exists(filename):
    print("=== 지난 메모 ===")
    with open(filename, "r", encoding="utf-8") as f:
        for num, line in enumerate(f, start=1):
            print(f"{num}. {line.strip()}")
else:
    print("첫 메모를 시작하세요")

# 2~4. 새 메모 덧붙이기
memo = input("새 메모: ")
today = datetime.date.today().isoformat()
with open(filename, "a", encoding="utf-8") as f:
    f.write(f"{today} | {memo}\n")
print("기록되었습니다.")

실행 예시 (세 번 실행 후):

=== 지난 메모 ===
1. 2026-09-09 | 파이썬 파일 입출력 복습
2. 2026-09-09 | with 문이 파일을 닫아 준다
새 메모: enumerate로 번호를 단다
기록되었습니다.

해설: ① os.path.exists로 "없으면 안내, 있으면 출력" 갈림길을 만듭니다. ② 읽기(r) 블록과 쓰기(a) 블록을 분리한 것이 중요합니다 — 같은 with 안에서 쓰기 직후 읽으려 하면 빈 결과가 나옵니다(벽 4). ③ enumerate(f, start=1)가 번호를 붙이고, f"{today} | {memo}\n"이 저장 형식을 만듭니다.

검증하는 법: ① 첫 실행에서 "첫 메모를 시작하세요"가 나오는지 봅니다 (notes.txt를 지우고 시험). ② 세 번 실행해 번호가 1, 2, 3으로 누적되는지 봅니다. ③ notes.txt를 메모장으로 열어 날짜 | 메모 형식인지 확인합니다.

연습문제 해답

문제 1 해답. r은 읽기 전용(없으면 오류), w는 기존 내용을 전부 지우고 새로 쓰기, a는 기존 내용 뒤에 덧붙이기입니다. 위험한 것은 w입니다 — 실측에서도 w로 두 번 쓰니 첫 번째 내용이 사라지고 마지막 것만 남았습니다 (2026-09-09 실측). 덧붙이려면 반드시 a입니다.

문제 2 해답. 인코딩을 명시하지 않으면 운영체제의 기본 코드표가 쓰이는데, 그것이 파일의 코드표와 어긋나면 한글이 깨집니다. 쓸 때와 읽을 때의 코드표가 같아야 글자가 복원되므로, 둘 다 encoding="utf-8"로 고정하는 것이 안전합니다.

문제 3 해답. 줄 바꿈 없이 첫 줄둘째 줄로 이어 붙습니다. write는 자동으로 줄을 바꿔 주지 않기 때문입니다. f.write("첫 줄\n")처럼 \n을 직접 넣어야 두 줄이 됩니다.

문제 4 해답. CSV는 "쉼표로 나눈 평평한 표"라 행과 열이 고정된 데이터에 맞고, JSON은 딕셔너리·리스트 구조를 통째로 저장하므로 중첩된 데이터에 맞습니다. "서버마다 포트 목록"은 서버 안에 또 목록이 있는 중첩 구조이므로 JSON이 맞습니다 — 3-8 실측에서 딕셔너리의 리스트가 JSON으로 그대로 저장된 것을 확인했습니다.

완료 기준 체크리스트

  • [ ] with open()으로 파일을 쓰고 읽을 수 있다
  • [ ] r/w/a 세 모드의 차이와 w의 위험성을 실험으로 확인했다
  • [ ] encoding="utf-8"이 필요한 이유를 설명할 수 있다
  • [ ] \nstrip()의 역할을 안다
  • [ ] CSV를 split으로 읽고 JSON을 json.load로 복원할 수 있다
  • [ ] os.path.exists 안전 패턴과 enumerate 번호 매기기를 쓸 수 있다
  • [ ] 미션(누적 메모장)을 완성하고 세 번 실행해 누적을 확인했다

6. 흔한 실수와 해결

벽 1. 한글이 깨진다

증상: 저장하거나 읽을 때 한글이 깨진 문자로 나옵니다.
원인: encoding="utf-8"을 빼먹은 것입니다. 명시하지 않으면 운영체제의 기본 코드표가 쓰이고, 그 코드표와 파일의 코드표가 어긋나면 깨집니다.
해결: 모든 openencoding="utf-8"을 붙이는 것을 손가락 습관으로 만드세요. 쓸 때와 읽을 때 둘 다 붙여야 합니다.

벽 2. "w"로 열었다가 내용이 다 지워졌다

증상: 덧붙이려 했는데 옛날 내용이 증발했습니다.
원인: w는 무조건 백지부터입니다 (2026-09-09 실측: 같은 파일에 w로 두 번 쓰니 마지막 내용만 남았습니다). 덧붙이기는 a입니다.
해결: 쓰기 전에 스스로 묻는 습관 — "지우고 새로? (w) 이어서? (a)". 중요한 파일은 작업 전에 복사본을 만들어 두세요.

벽 3. FileNotFoundError가 난다

증상 (2026-09-09 실측):

FileNotFoundError: [Errno 2] No such file or directory: 'no_such_file.txt'

원인: 파일이 진짜 없거나, 프로그램이 다른 폴더에서 실행 중인 경우입니다. 상대 경로("memo.txt")는 "지금 프로그램이 돌고 있는 폴더" 기준입니다.
해결: import os; print(os.getcwd())로 지금 서 있는 폴더를 확인하세요 (실측 환경에서는 스크립트가 있는 폴더 경로가 출력되었습니다). 그리고 3-6의 exists 패턴으로 "있으면 읽고, 없으면 안내"하는 구조를 씁니다.

벽 4. 읽었는데 빈 문자열만 나온다

증상: read() 결과가 비어 있습니다.
원인: 같은 with 안에서 쓰기 직후 읽으려 한 경우, 또는 파일을 r이 아니라 w로 열어 방금 지운 경우입니다.
해결: 쓰기와 읽기는 별도의 with 블록으로 나누세요. 쓰기 블록이 끝나(파일이 닫혀)야 저장이 완료되고, 그 후에 읽기 블록을 엽니다. 미션 모범 답안이 이 구조입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
파일 모드 r(읽기) / w(지우고 쓰기) / a(덧붙이기)
with 문 블록이 끝나면 파일을 자동으로 닫는 표준 자세
인코딩 글자를 숫자로 바꾸는 코드표 — 항상 UTF-8 명시
CSV 쉼표로 칸을 나눈 평평한 표 형식
JSON 딕셔너리·리스트 구조를 통째로 저장하는 형식
상대 경로 "프로그램이 돌고 있는 폴더" 기준의 경로

오늘의 문법

문법 하는 일
with open(경로, 모드, encoding="utf-8") as f: 파일을 열고 자동으로 닫기
f.write(문자열) / f.read() 쓰기 / 전체 읽기
for line in f: 파일을 한 줄씩 반복
\n / strip() 줄 바꿈 넣기 / 줄 끝 벗기기
json.dump(바구니, f) / json.load(f) 바구니를 JSON으로 저장 / 복원
os.path.exists(경로) 파일 존재 여부 묻기
enumerate(바구니, start=1) 번호표를 붙여 하나씩 꺼내기

문법보다 중요한 감각

오늘로 여러분의 프로그램은 기억을 갖게 되었습니다. 꺼도 잊지 않는 프로그램. 저장은 기록이고, 기록은 책임입니다. 도구를 만들기 시작하면 스캔 결과·분석 로그 같은 결과물을 파일로 남기게 되는데, 그 파일은 "내가 무엇을 했는가"의 기록이기도 합니다. 모든 공격 실습은 내 랩·합법 플랫폼에서만. 실서비스 무단 공격은 범죄입니다. 내 랩의 결과물은 포트폴리오가 되지만, 무단 대상의 결과물은 증거가 됩니다.

마지막으로 하나. JSON은 인터넷의 공용어입니다. 나중에 웹 요청을 배우면 서버가 돌려주는 응답의 대부분이 JSON인데, 오늘 json.load로 딕셔너리를 복원한 그 한 줄이 그날에도 그대로 쓰입니다. 파일에서 읽느냐, 네트워크에서 받느냐만 다를 뿐입니다.


전부 체크되면 Step 45 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.