# 파이썬 JSON: 파일 읽고 쓰기, CSV, pathlib, UTF-8 인코딩

URL: https://skillaria.top/ko/guides/python/python-json

파이썬에서 JSON은 내장 모듈 json으로 다룹니다. json.load는 열린 파일에서, json.loads는 문자열에서 데이터를 읽고, json.dump와 json.dumps는 반대로 씁니다. 파일은 with open(path, encoding="utf-8")으로 열고, 경로는 pathlib.Path로 만들며, 표 형태의 데이터는 csv 모듈로 읽습니다.

파이썬에서 JSON을 다루는 데는 외부 라이브러리가 필요 없습니다. 표준 라이브러리의 `json` 모듈이 딕셔너리와 리스트를 텍스트로 바꾸고, 텍스트를 다시 객체로 바꿉니다. 함께 쓰이는 것은 파일을 여는 `open`과 `with` 구문, 경로를 다루는 `pathlib`, 표 데이터를 다루는 `csv` 모듈입니다. 이 글에서는 실제 코드와 자주 걸리는 함정을 순서대로 정리합니다.

## 파이썬 open 함수: 모드와 인코딩

텍스트 파일은 내장 함수 `open(path, mode, encoding=...)`으로 엽니다. 파일 객체를 돌려주며, 이 객체에는 `read`, `readline`, `write` 메서드가 있고, 반복문으로 한 줄씩 순회할 수 있습니다.

| 모드 | 하는 일 |
| --- | --- |

| `"r"` | 읽기 (기본값). 파일이 없으면 `FileNotFoundError` |

| `"w"` | 쓰기. 파일을 만들거나, **열자마자 내용을 비웁니다** |

| `"a"` | 끝에 덧붙이기. 기존 내용은 그대로 둡니다 |

| `"x"` | 새 파일 만들기. 이미 있으면 `FileExistsError` |

| `"b"` | 바이너리 모드 추가 (`"rb"`, `"wb"`) |

### with를 쓰는 이유

파일은 반드시 닫아야 합니다. 닫지 않으면 데이터가 디스크에 제대로 기록되지 않을 수 있고, 운영체제가 파일 핸들을 계속 붙잡고 있습니다. `with` 구문은 블록 안에서 오류가 나더라도 파일을 자동으로 닫습니다.

```
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("첫 번째 줄\n")
    f.write("두 번째 줄\n")

with open("notes.txt", encoding="utf-8") as f:
    for line in f:
        print(line.rstrip("\n"))
```

파일을 반복문으로 순회하면 한 번에 한 줄씩만 읽으므로, 기가바이트 단위의 로그도 처리할 수 있습니다. `f.read()`는 전체 내용을 문자열 하나로, `f.readlines()`는 줄의 리스트로 돌려줍니다.

### 기존 내용을 지우지 않고 한 줄 추가하기

초보자가 가장 많이 하는 실수는 파일에 한 줄을 '추가'하려고 `"w"` 모드로 여는 것입니다. `"w"`는 `write`를 호출하기 전, 열 때 파일을 비웁니다. 끝에 덧붙이려면 `"a"`를, 이미 있는 파일을 실수로 덮어쓰지 않으려면 `"x"`를 쓰세요.

```
with open("log.txt", "a", encoding="utf-8") as f:
    f.write("새 기록\n")
```

`write`는 줄바꿈을 자동으로 넣지 않으므로 `"\n"`을 직접 써야 합니다. `print(..., file=f)`를 쓰면 줄바꿈이 자동으로 붙습니다.

## UTF-8 인코딩: 글자가 깨지는 이유

Python 3.15 이전에는 `encoding`을 주지 않으면 `open()`이 시스템 기본 인코딩을 씁니다. Linux와 macOS는 보통 UTF-8이지만, 한국어 Windows는 CP949를 씁니다. 서버에서는 잘 돌던 스크립트가 다른 컴퓨터에서 글자가 깨지거나 `UnicodeDecodeError`를 내기도 했습니다.

Python 3.15는 PEP 686에 따라 UTF-8 모드를 기본으로 켭니다. `encoding`을 생략해도 모든 운영체제에서 UTF-8로 읽고 씁니다. 이전 동작으로 되돌리려면 환경 변수 `PYTHONUTF8=0`이나 옵션 `-X utf8=0`을 쓰고, 시스템 기본 인코딩을 명시적으로 쓰려면 `encoding="locale"`을 지정합니다.

```
import sys
print(sys.flags.utf8_mode)   # Python 3.15에서는 1

open("legacy.txt", encoding="locale")     # 시스템 인코딩
open("old_excel.csv", encoding="cp949")   # CP949로 저장된 파일
```

반대쪽 문제도 있습니다. 예전 Windows 스크립트나 엑셀이 CP949로 저장한 CSV는 이제 UTF-8로 읽히므로 `UnicodeDecodeError`가 날 수 있습니다. 이때는 `encoding="cp949"`로 읽은 뒤 UTF-8로 다시 저장하세요. 가장 좋은 습관은 인코딩을 항상 명시하는 것입니다. 그러면 파이썬 버전과 상관없이 같은 결과가 나옵니다.

## pathlib: 경로를 객체로 다루기

경로를 문자열로 이어 붙이면(`"data" + "/" + name`) 불편하고 운영체제에 따라 깨집니다. `pathlib` 모듈의 `Path` 클래스는 `/` 연산자로 경로를 잇고, 여러 속성을 제공합니다.

```
from pathlib import Path

data = Path("data")
csv_path = data / "expenses.csv"

print(csv_path.name)    # expenses.csv
print(csv_path.stem)    # expenses
print(csv_path.suffix)  # .csv
print(csv_path.parent)  # data
print(csv_path.exists())

data.mkdir(parents=True, exist_ok=True)
(data / "hello.txt").write_text("안녕하세요", encoding="utf-8")
print((data / "hello.txt").read_text(encoding="utf-8"))

for p in data.glob("*.csv"):
    print(p)
```

작은 파일이라면 `read_text`와 `write_text`가 `open`과 `with`를 대신합니다. `Path` 객체는 `open`, `json`, `csv` 등 어디든 그대로 넘길 수 있습니다. 현재 작업 폴더가 아니라 스크립트 파일 기준의 경로가 필요하다면 `Path(__file__).parent / "data"`를 쓰세요.

Python 3.14에서는 `Path`에 `copy()`와 `move()` 메서드가 추가되어, 간단한 복사에는 `shutil`이 필요 없어졌습니다. 같은 버전에서는 Zstandard 압축을 위한 `compression.zstd` 모듈도 표준 라이브러리에 들어왔습니다(PEP 784).

## 파이썬 JSON: load, dump, loads, dumps

JSON은 데이터를 주고받을 때 쓰는 텍스트 형식입니다. API가 JSON을 응답하고, 설정이나 보고서를 JSON으로 저장합니다. `json` 모듈은 두 가지 작업을 합니다. 직렬화(파이썬 객체 → JSON 텍스트)와 역직렬화(텍스트 → 파이썬 객체)입니다. 함수는 네 개인데, 헷갈리기 쉽습니다.

| 함수 | 하는 일 |
| --- | --- |

| `json.dumps(obj)` | 객체 → JSON 문자열 |

| `json.loads(s)` | JSON 문자열 → 객체 |

| `json.dump(obj, f)` | 객체 → 열린 파일에 쓰기 |

| `json.load(f)` | 열린 파일에서 JSON 읽기 |

기억하기 쉬운 규칙이 있습니다. 이름 끝의 `s`는 string, 즉 문자열을 다룬다는 뜻입니다. `s`가 없으면 파일을 다룹니다.

### 타입 대응표

| 파이썬 | JSON |
| --- | --- |

| `dict` | 객체 `{}` |

| `list`, `tuple` | 배열 `[]` |

| `str` | 문자열 |

| `int`, `float` | 숫자 |

| `True` / `False` | `true` / `false` |

| `None` | `null` |

튜플은 다시 읽으면 리스트가 됩니다. 집합, 날짜, `Decimal`은 모듈이 모르므로 `TypeError: Object of type ... is not JSON serializable`이 납니다.

### JSON 파일로 저장하기

```
import json
from pathlib import Path

report = {
    "month": "2026-10",
    "total": 15230.5,
    "categories": {"식비": 8200, "교통": 2100},
    "tags": ["집", "회사"],
}

path = Path("data/report.json")
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
    json.dump(report, f, ensure_ascii=False, indent=2)
```

`ensure_ascii=False`를 주면 한글이 그대로 저장됩니다. 이 옵션이 없으면 '식비'가 `"식비"` 같은 형태로 저장됩니다. 올바른 JSON이지만 사람이 읽기는 어렵습니다. `indent=2`는 들여쓰기를 넣어 보기 좋게 만듭니다.

### JSON 파일 읽기

```
with open("data/report.json", encoding="utf-8") as f:
    data = json.load(f)

print(data["total"])               # 15230.5
print(data["categories"]["식비"])   # 8200
```

결과는 평범한 딕셔너리나 리스트이므로 익숙한 방법으로 다루면 됩니다. 파일이 JSON이 아니거나 잘렸다면 `json.JSONDecodeError`가 발생하고, 오류 위치의 줄과 열을 알려 줍니다. 데이터가 외부에서 들어오는 경우에는 이 예외를 잡아 두세요.

### JSON 문자열 파싱과 예쁘게 출력하기

```
import json

raw = '{"name": "안나", "skills": ["python", "sql"], "active": true}'
user = json.loads(raw)
print(user["skills"][0])   # python

print(json.dumps(user, ensure_ascii=False, indent=2, sort_keys=True))
```

`sort_keys=True`는 출력할 때 키를 정렬합니다. git에서 파일 두 개를 비교할 때 편합니다. 터미널에서 파일을 보기 좋게 정리하려면 내장 도구 `python -m json.tool data/report.json`을 쓸 수 있습니다.

HTTP API의 응답도 JSON 문자열입니다. `requests` 라이브러리의 `response.json()`은 내부적으로 `json.loads`를 호출합니다.

### 함정: JSON의 키는 항상 문자열

```
d = {1: "1월", 2: "2월"}
back = json.loads(json.dumps(d))
print(back == d)   # False
print(back)        # {'1': '1월', '2': '2월'}
# back[1] -> KeyError, back["1"]을 써야 합니다

fixed = {int(k): v for k, v in back.items()}
```

JSON 객체의 키는 문자열만 가능합니다. 그래서 숫자 키는 저장과 복원을 거치면 문자열이 됩니다. 읽은 뒤에 필요한 타입으로 바꿔 주세요.

### 직접 만든 타입: 날짜와 Decimal

```
import json
from datetime import date

def default(obj):
    if isinstance(obj, date):
        return obj.isoformat()
    raise TypeError(f"직렬화할 수 없는 타입: {type(obj).__name__}")

print(json.dumps({"day": date(2026, 10, 1)}, default=default))
# {"day": "2026-10-01"}
```

`default` 인자로 넘긴 함수는 모듈이 모르는 객체를 만날 때마다 호출됩니다. 더 복잡한 경우에는 `json.JSONEncoder`를 상속하고 `default` 메서드를 재정의하세요.

### JSON 배열에 항목 추가하기

JSON은 텍스트 로그처럼 끝에 덧붙일 수 없습니다. 파일을 읽고, 바꾸고, 전체를 다시 써야 합니다.

```
path = Path("data/items.json")
items = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
items.append({"id": len(items) + 1, "title": "새 항목"})
path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
```

기록이 많고 계속 늘어난다면 JSON Lines 형식이 좋습니다. 한 줄에 JSON 객체 하나를 쓰고, 파일은 `"a"` 모드로 엽니다.

## 파이썬 CSV: csv 모듈

CSV는 줄이 레코드, 쉼표로 구분된 값이 필드인 텍스트 표입니다. `csv` 모듈은 따옴표, 값 안의 쉼표, 줄바꿈을 올바르게 처리합니다. 손으로 `split(",")`을 쓰면 이런 경우에 틀립니다.

```
import csv

with open("data/expenses.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    total = 0.0
    for row in reader:
        total += float(row["amount"])
        print(row["date"], row["category"], row["amount"])
print("합계:", total)
```

`csv.DictReader`는 헤더 행을 기준으로 각 줄을 딕셔너리로 바꿉니다. 모든 값은 문자열이므로 숫자와 날짜는 직접 변환하세요(`float`, `int`, `date.fromisoformat`). 세미콜론(;)으로 구분된 파일(한국어 엑셀이 저장하는 형식)은 `delimiter=";"`로 읽습니다.

```
rows = [
    {"date": "2026-10-01", "category": "식비", "amount": 450},
    {"date": "2026-10-02", "category": "교통", "amount": 60},
]
with open("data/out.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["date", "category", "amount"])
    writer.writeheader()
    writer.writerows(rows)
```

쓸 때는 `newline=""`를 반드시 지정하세요. 없으면 Windows에서 줄 사이에 빈 줄이 생깁니다. 파일을 엑셀로 열 예정이라면 `encoding="utf-8-sig"`를 쓰세요. BOM 표식이 있으면 엑셀이 UTF-8로 인식합니다.

## CSV에서 JSON으로: 전체 예제

```
import csv
import json
from collections import defaultdict
from pathlib import Path

src = Path("data/expenses.csv")
dst = Path("data/report.json")

by_category = defaultdict(float)
with src.open(newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        by_category[row["category"]] += float(row["amount"])

report = {"total": sum(by_category.values()), "categories": dict(by_category)}
dst.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(dst.read_text(encoding="utf-8"))
```

이것이 보고서 스크립트의 기본 구조입니다. CSV에서 읽고, 딕셔너리에서 집계하고, JSON으로 저장합니다.

## 자주 하는 실수

- `FileNotFoundError` — 상대 경로는 스크립트 위치가 아니라 실행한 폴더를 기준으로 합니다. `Path(__file__).parent`를 쓰세요.
- 파일 내용이 사라짐 — `"w"` 대신 `"a"`를 써야 했습니다.
- `UnicodeDecodeError` — 파일의 인코딩이 다릅니다. `cp949`나 `utf-8-sig`를 명시하세요.
- `TypeError: Object of type set is not JSON serializable` — 리스트로 바꾸거나 `default` 함수를 넘기세요.
- JSON을 읽은 뒤 `KeyError` — 숫자 키가 문자열로 바뀌었습니다.
- 내 파일 이름이 `json.py`나 `csv.py`인 경우 — 표준 모듈보다 먼저 가져와서 `import json`이 내 파일을 가져옵니다.

## 정리

파이썬에서 JSON은 표준 라이브러리로 충분합니다. 파일에는 `json.load`와 `json.dump`를, 문자열에는 `loads`와 `dumps`를 씁니다. 한글을 보존하려면 `ensure_ascii=False`를, 읽기 좋게 만들려면 `indent`를 씁니다. 파일은 `with`로 열고 인코딩을 명시하며, 경로는 `pathlib`로 다루고, 표는 `csv.DictReader`로 읽습니다. 이 주제는 과정 강의에서 실습합니다. 지출 관리기가 CSV의 거래 내역을 읽고 JSON 보고서를 저장합니다.

## FAQ

**json.load와 json.loads는 무엇이 다른가요?**

json.load는 열린 파일 객체에서 읽고, json.loads는 문자열을 분석합니다. 마찬가지로 json.dump는 파일에 쓰고, json.dumps는 문자열을 돌려줍니다.

**JSON 파일에 한글을 \u 이스케이프 없이 저장하려면?**

json.dump나 json.dumps에 ensure_ascii=False를 주고, 파일은 encoding="utf-8"로 여세요.

**JSON을 다루려고 라이브러리를 따로 설치해야 하나요?**

아닙니다. json 모듈은 표준 라이브러리에 포함되어 있어서 import json만 하면 됩니다.

**JSON에 날짜나 집합을 저장할 수 있나요?**

그대로는 안 됩니다. 날짜는 isoformat으로 문자열로 바꾸고, 집합은 리스트로 바꾸거나, default 함수를 넘기세요.

**파일이 메모리에 들어가지 않을 만큼 큰 JSON은 어떻게 읽나요?**

json.load는 파일 전체를 읽습니다. 대용량이라면 JSON Lines 형식으로 저장하고 한 줄씩 json.loads로 처리하세요.


---
Source: Skillaria.Top (https://skillaria.top)
