16. 파일 입출력과 데이터 형식

🎯 이 장의 목표
  • openwith로 파일을 안전하게 읽고 쓴다.
  • 읽기/쓰기/추가 모드와 줄 단위 처리를 안다.
  • CSV 파일을 csv 모듈로 다룬다.
  • JSON으로 데이터를 저장하고 불러온다.

먼저: 파일 입출력이 왜 필요한가

지금까지 만든 프로그램은 종료되면 모든 데이터가 사라졌습니다. 변수는 메모리에만 있으니까요. 파일에 저장하면 프로그램을 껐다 켜도 데이터가 남습니다. 이를 영속성(persistence)이라 하고, 그 첫걸음이 파일 입출력입니다.

with로 파일 열기

파일을 다루는 기본 도구는 open() 함수입니다. 그런데 파일은 다 쓰면 반드시 닫아야 합니다(close()). 닫지 않으면 데이터가 제대로 저장 안 되거나 자원이 새어 나갈 수 있습니다.

with 구문을 쓰면 블록이 끝날 때 파일이 자동으로 닫힙니다. 그래서 항상 with로 파일을 엽니다.

PYTHON
with open("memo.txt", "w", encoding="utf-8") as f:
    f.write("첫째 줄\n")
    f.write("둘째 줄\n")
# 여기서 자동으로 close() 됨

open()의 인자를 봅시다.

  • 첫째: 파일 이름(경로)
  • 둘째: 모드("w" 쓰기, "r" 읽기, "a" 추가)
  • encoding="utf-8": 한글이 깨지지 않게 하는 인코딩. 한글을 다룰 땐 항상 지정하세요.

with가 실제로 대신해주는 일

with가 왜 편한지는, 그것 없이 직접 닫아야 하는 코드와 비교하면 분명합니다.

PYTHON
# with 없이: 직접 열고 닫기 (번거롭고 위험)
f = open("memo.txt", "w", encoding="utf-8")
try:
    f.write("내용")
finally:
    f.close()          # 에러가 나도 반드시 닫아야 하니 finally에

# with 사용: 위 전체와 동등 (닫기를 자동으로!)
with open("memo.txt", "w", encoding="utf-8") as f:
    f.write("내용")
# 블록을 벗어나면 (에러가 나든 안 나든) 자동으로 close()

위 두 코드는 완전히 같은 일을 합니다. with는 "블록을 벗어날 때 close()를 자동 호출"해주는 장치라서, 우리가 try/finally로 직접 닫을 필요가 없습니다. 닫기를 깜빡할 위험도 사라지죠.

📌 핵심
핵심: 파일은 항상 with open(...) as f:로 연다. 블록을 벗어나면 자동으로 닫혀 안전하다. 한글이 있으면 encoding="utf-8"을 잊지 말 것.
📎 with는 14장에서 본 "자원을 안전하게 정리"하는 구문입니다. 파일 외에도 네트워크 연결, 잠금 등 "열었으면 닫아야 하는" 자원에 두루 쓰입니다. 초급편 예외 처리의 finally가 하던 "마무리"를 더 깔끔하게 해주는 도구죠.

파일 쓰기

쓰기 모드("w")로 열면 파일에 내용을 씁니다.

PYTHON
with open("memo.txt", "w", encoding="utf-8") as f:
    f.write("안녕하세요\n")        # 한 문자열 쓰기 (\n으로 줄바꿈)
    f.write("두 번째 줄\n")
    f.writelines(["셋째\n", "넷째\n"])   # 여러 줄을 리스트로
⚠️ 흔한 실수
흔한 함정 1 — w는 덮어쓴다: "w" 모드는 기존 파일 내용을 전부 지우고 새로 씁니다. 기존 내용에 이어 쓰려면 "a"(append) 모드를 쓰세요.
```python
with open("log.txt", "a", encoding="utf-8") as f: # 기존 내용 뒤에 추가
f.write("새 로그\n")
```
⚠️ 흔한 실수
흔한 함정 2 — 줄바꿈은 직접: write()print()와 달리 줄바꿈을 자동으로 넣지 않습니다. 줄을 나누려면 \n을 직접 붙여야 합니다.
모드의미파일이 없으면기존 내용
"r"읽기에러보존
"w"쓰기새로 생성전부 삭제
"a"추가새로 생성보존(뒤에 덧붙임)

파일 읽기

읽기 모드("r", 생략 가능)로 엽니다. 읽는 방법은 여러 가지입니다.

전체를 한 번에: read()

PYTHON
with open("memo.txt", "r", encoding="utf-8") as f:
    content = f.read()      # 파일 전체를 하나의 문자열로
print(content)

⭐ 줄 단위로 순회 (권장)

파일 객체를 for로 돌면 한 줄씩 나옵니다. 큰 파일도 메모리 효율적입니다(14장의 지연 평가와 같은 원리).

PYTHON
with open("memo.txt", "r", encoding="utf-8") as f:
    for line in f:
        print(line.strip())    # strip()으로 줄 끝 \n 제거
💡 팁
각 줄 끝에는 \n이 붙어 있어, print하면 빈 줄이 끼어 보입니다. line.strip()으로 양끝 공백·줄바꿈을 제거하면 깔끔합니다.

모든 줄을 리스트로: readlines()

PYTHON
with open("memo.txt", encoding="utf-8") as f:
    lines = f.readlines()      # ['첫째 줄\n', '둘째 줄\n', ...]
print(len(lines))              # 줄 개수
방법결과적합한 경우
f.read()전체를 한 문자열로작은 파일, 통째로 처리
for line in f:한 줄씩 (지연)큰 파일, 줄별 처리
f.readlines()모든 줄을 리스트로줄 목록이 필요할 때
⚠️ 흔한 실수
흔한 함정 — 없는 파일 읽기: 존재하지 않는 파일을 "r"로 열면 FileNotFoundError가 납니다. 초급편의 try/except로 대비하세요.
```python
try:
with open("없는파일.txt", encoding="utf-8") as f:
data = f.read()
except FileNotFoundError:
print("파일이 없습니다")
```

CSV: 표 형태 데이터

CSV(Comma-Separated Values)는 값을 쉼표로 구분해 표처럼 저장하는 형식입니다. 엑셀로 열리는 그 형식이죠. csv 모듈로 다룹니다.

CSV 쓰기

PYTHON
import csv

with open("people.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["이름", "나이", "도시"])    # 헤더
    writer.writerow(["민지", 25, "서울"])
    writer.writerow(["현우", 30, "부산"])
⚠️ 흔한 실수
흔한 함정 — newline="": CSV를 쓸 때는 opennewline=""을 넣으세요. 빠뜨리면 (특히 Windows에서) 줄 사이에 빈 줄이 끼는 문제가 생깁니다. CSV 작성의 정해진 관용구입니다.

CSV 읽기

PYTHON
import csv

with open("people.csv", encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)
# ['이름', '나이', '도시']
# ['민지', '25', '서울']
# ['현우', '30', '부산']
⚠️ 흔한 실수
흔한 함정 — 전부 문자열로 읽힌다: CSV에서 읽은 값은 모두 문자열입니다. 25가 아니라 '25'로 옵니다. 숫자로 계산하려면 int()/float()로 변환해야 합니다(초급편 형 변환을 떠올리세요).

DictReader / DictWriter: 헤더를 키로

csv.DictReader는 각 행을 딕셔너리로 읽어, 열 이름(헤더)으로 값에 접근하게 해줍니다. 위치(인덱스)보다 이름이 명확하죠.

PYTHON
import csv

with open("people.csv", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["이름"], row["나이"])   # 헤더 이름으로 접근
# 민지 25
# 현우 30
PYTHON
# DictWriter로 쓰기
with open("out.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "score"])
    writer.writeheader()                         # 헤더 줄 쓰기
    writer.writerow({"name": "Alice", "score": 90})
    writer.writerow({"name": "Bob", "score": 85})

JSON: 구조화된 데이터

JSON(JavaScript Object Notation)은 데이터를 {"키": 값} 형태로 적는, 사람도 컴퓨터도 읽기 좋은 형식입니다. 웹 API·설정 파일에서 표준처럼 쓰입니다. 모양이 Python 딕셔너리와 거의 같아 다루기 쉽습니다.

json 모듈의 핵심은 네 함수입니다.

함수방향대상
json.dump(obj, f)Python → 파일객체를 파일에 저장
json.load(f)파일 → Python파일에서 객체로 불러옴
json.dumps(obj)Python → 문자열객체를 JSON 문자열로
json.loads(s)문자열 → PythonJSON 문자열을 객체로
💡 팁
s가 붙으면 "string". dump/load는 파일 상대, dumps/loads는 문자열 상대입니다.

JSON 저장하기

PYTHON
import json

data = {
    "name": "민지",
    "age": 25,
    "hobbies": ["독서", "코딩"],
    "active": True,
}

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

저장된 파일 내용:

JSON
{
  "name": "민지",
  "age": 25,
  "hobbies": [
    "독서",
    "코딩"
  ],
  "active": true
}
  • ensure_ascii=False: 이게 없으면 한글이 \uBBFC\uC9C0처럼 깨져 보입니다. 한글엔 필수.
  • indent=2: 들여쓰기로 보기 좋게. 없으면 한 줄로 빽빽하게 저장됩니다.
💡 팁
Python의 True가 JSON에서는 소문자 true가 됩니다. Nonenull이 되고요. JSON과 Python의 표기가 조금 다르지만, 변환은 json 모듈이 알아서 해줍니다.

JSON 불러오기

PYTHON
import json

with open("data.json", encoding="utf-8") as f:
    loaded = json.load(f)       # 다시 Python 딕셔너리로

print(loaded["name"])           # 민지
print(loaded["hobbies"])        # ['독서', '코딩']

문자열과의 변환

파일이 아니라 문자열로 주고받을 때는 dumps/loads를 씁니다(웹 통신에서 흔함).

PYTHON
import json

s = json.dumps({"x": 1, "y": 2}, ensure_ascii=False)
print(s)                        # {"x": 1, "y": 2}   (문자열)

obj = json.loads('{"a": 10, "b": [1, 2, 3]}')
print(obj["b"])                 # [1, 2, 3]   (딕셔너리로)

CSV vs JSON, 언제 무엇을?

flowchart TD
    Q{"데이터 모양은?"}:::proc
    Q -->|"표(행과 열, 균일)"| CSV["📊 CSV<br/>스프레드시트·표 데이터<br/>엑셀 호환"]:::result
    Q -->|"중첩·계층 구조"| JSON["🗂️ JSON<br/>설정·API·복잡한 구조<br/>리스트 안에 딕셔너리 등"]:::result

    classDef proc fill:#fff3b0,stroke:#e0a800,color:#5c4500
    classDef result fill:#b8e6c1,stroke:#34a853,color:#14532d
  • CSV: 모든 행이 같은 열을 가진 표 형태. 가계부, 성적표, 통계 데이터.
  • JSON: 중첩·계층이 있는 데이터. 설정 파일, 사용자 프로필(취미 리스트 등 안에 또 구조가 있는 경우).

이 장에서 배운 것

  • 파일은 항상 with open(...) as f:로 연다(자동으로 닫힘). 한글엔 encoding="utf-8".
  • 모드: "r"(읽기)·"w"(덮어쓰기)·"a"(추가). write()는 줄바꿈을 자동으로 안 넣는다.
  • 읽기는 read()(전체)·for line in f(줄 단위, 권장)·readlines()(리스트). 줄 끝 \nstrip()으로 제거.
  • CSVcsv 모듈로 다루며, 쓸 때 newline=""을 넣고, 읽은 값은 전부 문자열이다. DictReader로 헤더 이름 접근.
  • JSONdump/load(파일)·dumps/loads(문자열). 한글엔 ensure_ascii=False, 보기 좋게 indent=2.

🧪 실습 문제

문제 1. diary.txt 파일에 세 줄("월요일", "화요일", "수요일")을 쓰는 코드를 작성하세요. (각 줄이 실제로 나뉘도록)

문제 2. 문제 1의 파일을 읽어, 각 줄 앞에 번호를 붙여 1. 월요일 형태로 출력하세요. (힌트: enumerate, strip)

문제 3. 다음 코드의 문제점은 무엇이고 어떻게 고쳐야 할까요?

PYTHON
with open("data.txt", "w") as f:
    f.write("기존 중요 데이터")
# (나중에)
with open("data.txt", "w") as f:
    f.write("새 데이터")

문제 4. 딕셔너리 config = {"theme": "dark", "volume": 70, "tags": ["a", "b"]}config.json에 한글 깨짐 없이, 들여쓰기 2칸으로 저장하는 코드를 쓰세요.

문제 5. 다음 JSON 문자열을 Python 객체로 변환하고, scores 리스트의 합을 출력하세요.

PYTHON
text = '{"name": "반1", "scores": [80, 90, 75]}'

<details>

<summary>✅ 정답·해설 보기</summary>

1.

PYTHON
with open("diary.txt", "w", encoding="utf-8") as f:
    f.write("월요일\n")
    f.write("화요일\n")
    f.write("수요일\n")
# 또는: f.writelines([f"{d}\n" for d in ["월요일","화요일","수요일"]])

2.

PYTHON
with open("diary.txt", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        print(f"{i}. {line.strip()}")

3. 두 번째 "w" 모드가 기존 파일을 덮어써 "기존 중요 데이터"가 사라집니다. 이어 쓰려면 두 번째를 "a"(추가) 모드로 열어야 합니다.

4.

PYTHON
import json
config = {"theme": "dark", "volume": 70, "tags": ["a", "b"]}
with open("config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, ensure_ascii=False, indent=2)

5.

PYTHON
import json
text = '{"name": "반1", "scores": [80, 90, 75]}'
obj = json.loads(text)
print(sum(obj["scores"]))   # 245

</details>

◀️ 이전 장: 15. 모듈과 패키지 | ▶️ 다음 장: 17. 데코레이터