6개월 전에 짠 분석 노트북을 다시 열었을 때, 어떤 셀을 어떤 순서로 실행해야 하는지 기억나지 않는 경험은 흔하다. 동료가 그 노트북을 받았을 때는 더 막막하다. 재현 가능한 프로젝트 구조는 "6개월 뒤의 나, 또는 처음 보는 동료가 README만 읽고 바로 실행할 수 있는가"를 기준으로 폴더와 코드를 배치하는 방식이다.
한 줄 정의
재현 가능한 프로젝트 구조는 탐색(notebook)과 재사용 코드(src)의 역할을 분리 하고, 데이터·환경·실행 방법을 requirements.txt 와 README.md 로 명시해 누구나 같은 결과를 다시 만들 수 있게 하는 폴더 배치다.
Jupyter Notebook vs .py 스크립트 — 언제 무엇을
상황 | Jupyter(.ipynb) | Python(.py) |
|---|---|---|
목적 | EDA·탐색, 시각화 중심 분석 | 반복 실행 자동화, 라이브러리 코드 |
실행 방식 | 셀 단위로 한 번씩 확인하며 진행 | 처음부터 끝까지 통째로 실행 |
버전 관리 | diff가 지저분하고 코드 리뷰에 부적합 | Git 히스토리·pytest·코드 리뷰 대상 |
최종 형태 | nbconvert로 보고서·HTML 변환 | CI/CD 파이프라인에 그대로 통합 |
둘 중 하나를 골라야 하는 것이 아니다. 탐색은 노트북에서 자유롭게 하고, 검증이 끝난 함수만 .py 모듈로 옮겨 재사용한다. 이 둘을 섞어서 한 파일에 실험 코드와 재사용 코드를 같이 두면, 나중에 어떤 셀이 최종 로직인지 구분할 수 없게 된다.
프로젝트 폴더 구조
data-project/
├── data/
│ ├── raw/ # 원본 데이터 (절대 수정 금지)
│ ├── processed/ # 전처리 완료 데이터
│ └── external/ # 외부 참조 데이터
├── notebooks/ # EDA·실험용 Jupyter 노트북
│ ├── 01_eda.ipynb
│ └── 02_feature_exploration.ipynb
├── src/
│ └── mypkg/ # 재사용 가능한 Python 패키지
│ ├── __init__.py
│ ├── clean.py # 전처리 함수
│ └── viz.py # 시각화 함수
├── tests/
│ └── test_clean.py # src 모듈에 대한 pytest
├── .github/workflows/
│ └── ci.yml # pytest·ruff 자동 실행
├── output/ # 생성된 리포트·모델 파일
├── .gitignore
├── .env.example
├── pyproject.toml
├── requirements.txt
└── README.md
이 구조에서 성격이 다른 세 가지가 뚜렷하게 나뉜다: 데이터(data/) 는 커밋하지 않는 원자료, 코드(src/, tests/) 는 Git으로 관리하는 재사용 자산, 실험(notebooks/) 은 자유롭게 고쳐도 되는 탐색 공간이다.
모듈화 — 노트북 코드를 재사용 가능한 패키지로
노트북에서 반복해서 쓰는 함수가 보이면 src/ 로 옮긴다. 폴더에 __init__.py 가 있으면 Python이 그 폴더를 패키지로 인식한다.
# src/mypkg/clean.py
def clean_nulls(df, cols=None, strategy="median"):
cols = cols or df.select_dtypes("number").columns.tolist()
if strategy == "median":
return df.fillna(df[cols].median())
elif strategy == "drop":
return df.dropna(subset=cols)
return df
노트북에서 이 함수를 불러오는 방법은 두 가지다. 가장 흔한 방법은 sys.path 에 프로젝트 루트를 직접 추가하는 것이다.
# notebooks/01_eda.ipynb 첫 셀
import sys
sys.path.insert(0, "..")
from src.mypkg.clean import clean_nulls
다만 이 방법은 노트북을 어느 폴더에서 실행하느냐에 따라 경로가 어긋나기 쉽다. 더 안정적인 방법은 pyproject.toml 을 최소한으로 작성하고 패키지를 editable 모드 로 설치하는 것이다 — 한 번 설치해두면 노트북이 어느 위치에 있든, 어디서 실행하든 같은 방식으로 불러온다.
[project]
name = "mypkg"
version = "0.1.0"
requires-python = ">=3.11"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
pip install -e .
# 이제 어디서든 sys.path 조작 없이 바로 import
from mypkg.clean import clean_nulls
print(clean_nulls)
<function clean_nulls at 0x102229e40>
-e (editable) 옵션은 패키지를 복사하지 않고 현재 소스 위치를 그대로 가리키게 설치한다. src/mypkg/clean.py 를 고치면 재설치 없이 바로 반영된다.
README.md — 실행 방법을 코드로 남긴다
README는 프로젝트 목적을 설명하는 글이 아니라, 그대로 복사해서 실행하면 되는 명령어 모음 에 가까워야 한다.
## 프로젝트 개요
지역별 매출 데이터를 정제·분석하고 예측 모델을 만든다.
## 개발 환경 설정
```bash
git clone <url> && cd data-project
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
실행 방법
pytest tests/
python src/run_pipeline.py
테스트 통과 여부·커버리지를 보여주는 배지(badge)를 README 상단에 붙이면, 코드를 열어보지 않고도 현재 상태를 바로 알 수 있다. 노트북은 jupyter nbconvert --to html notebooks/01_eda.ipynb 로 정적 HTML로 변환해 코드 실행 환경이 없는 사람과도 공유할 수 있다.
핵심 원칙 네 가지
원칙 | 내용 |
|---|---|
탐색과 재사용 분리 | 노트북은 실험용, 검증된 함수만 src/로 옮겨 재사용 가능한 모듈로 관리한다. |
재현성 = 신뢰성 | requirements.txt·.env.example·README 실행 가이드 이 셋만 있으면 누구나 같은 결과를 재현할 수 있다. |
데이터는 Git에 올리지 않는다 | data/raw/는 .gitignore에 추가하고, 데이터 출처(URL·수집 스크립트)를 README에 남긴다. |
점진적 개선 | 처음부터 완벽한 구조를 만들 필요는 없다. 분석이 반복되면 함수화하고, 함수가 쌓이면 모듈화한다. |
장점과 한계
구분 | 내용 |
|---|---|
장점 | 코드 리뷰·CI·재사용이 가능해지고, 새 팀원이 프로젝트에 합류하는 데 걸리는 시간이 크게 줄어든다. |
한계 | 구조를 갖추는 데 초기 비용이 든다. 한 번 쓰고 버릴 짧은 탐색 작업까지 매번 이 구조를 다 갖출 필요는 없다 — 반복될 가능성이 보일 때 갖춘다. |
자주 발생하는 문제
노트북 하나에 실험과 최종 로직이 뒤섞여 있다. 시행착오 셀과 실제로 쓰는 함수가 같은 노트북에 있으면, 다음에 열었을 때 어디까지가 최종본인지 알 수 없다. 검증이 끝난 부분은 바로 src/ 로 옮기고 노트북에서는 그 함수를 import 해서 쓴다.
data/ 폴더를 통째로 커밋한다. 원본 CSV·Parquet 파일은 수십~수백 MB를 넘기기 쉽다. 한 번 커밋된 대용량 파일은 히스토리에서 지워도 저장소 크기가 줄지 않는다. 처음부터 data/ 를 .gitignore 에 넣고, 데이터를 받는 방법(다운로드 링크·스크립트)만 README에 남긴다.
# .gitignore
.venv/
.env
__pycache__/
*.pyc
data/raw/
data/processed/
output/
requirements.txt에 버전을 고정하지 않는다. pandas 한 줄만 적어두면, 나중에 새 버전이 설치되면서 동작이 달라질 수 있다(26편에서 본 Pandas 2.x의 Copy-on-Write 변화가 그런 예다). pip freeze > requirements.txt 로 실제 설치된 버전을 그대로 고정해 저장한다.
관련 문서
데이터 파일을 다루는 구체적인 방법은 CSV·JSON·Parquet 파일 읽고 쓰기 를, .env 로 비밀정보를 분리하는 방법은 환경변수와 .env로 비밀정보 관리하기 를 참고한다. 마지막 편에서는 이 구조를 실제 프로젝트 하나에 적용한다.
참고 자료
Python Packaging User Guide — Editable installs (2026-08-10 확인)
nbconvert 공식 문서 (2026-08-10 확인)
댓글 0
댓글을 불러오는 중…