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, \n과 strip(), 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를 만들고 다음 요구사항을 조립하세요.
- 실행하면 지난 메모들을 번호를 매겨 먼저 보여 준다. (파일이 없으면
첫 메모를 시작하세요출력) - 새 메모를 입력받아 파일에 덧붙인다.
- 메모 앞에 날짜를 자동으로 붙인다. 힌트:
import datetime
today = datetime.date.today().isoformat() # "2026-09-09" 형태
- 저장 형식은 한 줄에
날짜 | 메모로 한다. - 프로그램을 세 번 이상 실행해 누적되는 것을 확인한다.
심화(선택): 메뉴를 만들어 "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"이 필요한 이유를 설명할 수 있다 - [ ]
\n과strip()의 역할을 안다 - [ ] CSV를
split으로 읽고 JSON을json.load로 복원할 수 있다 - [ ]
os.path.exists안전 패턴과enumerate번호 매기기를 쓸 수 있다 - [ ] 미션(누적 메모장)을 완성하고 세 번 실행해 누적을 확인했다
6. 흔한 실수와 해결
벽 1. 한글이 깨진다
증상: 저장하거나 읽을 때 한글이 깨진 문자로 나옵니다.
원인: encoding="utf-8"을 빼먹은 것입니다. 명시하지 않으면 운영체제의 기본 코드표가 쓰이고, 그 코드표와 파일의 코드표가 어긋나면 깨집니다.
해결: 모든 open에 encoding="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 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.