🚢 titanic

[python] 항해일지 03. 선실 정리함 채우기: utils와 ModuleNotFoundError

Floaty 2026. 6. 25. 10:00

필수 사항은 아닌 선택 사항이지만 프로젝트를 하면서 코드의 질을 높이기 위해서, 모두의 편-안함을 위해서 해야할 것들에 대해서 작성해볼까 합니다. 타이타닉 데이터 분석을 할 때, `matplotlib` 관련 유틸 함수들을 `utils` 폴더에 모아두고 여러 파일에서 재사용하려고 했습니다. 그런데 막상 `import` 하려니 고려할 것들도 많고 그래서 정리를 한 번 해볼까 해요.

`matplotlib`로 돌아와야 했지만 `matplotlib`에 이 내용을 녹이기에는 쉽지 않을 것 같아서 이렇게 새로운 글로 찾아왔습니다.

 

 

유틸함수란?

본격적으로 시작하기 전에 유틸함수에 대해서 알아보고 갈게요. 유틸 함수(Utility Function)는 여러 곳에서 반복적으로 쓰이는 공통 기능을 모아둔 함수입니다. 그래프를 그릴 때마다 매번 폰트 설정, 색상 지정, 저장 경로를 만드는 코드를 작성한다고 생각해볼까요? 파일이 10개면 동일한 코드를 10번 적어야 합니다. 이건 너무 비효율적이죠. 그래서 이런 반복되는 코드를 따로 함수로 만들어두고 필요할 때마다 가져다 쓰는겁니다. 코드도 깔끔해지고 수정할 일이 생겼을 때 한 곳만 고치면 됩니다. 그리고 이러한 코드들을 보통 `utils`라는 폴더를 만들어서 그곳에 둡니다. 이번 프로젝트에서도 `utils` 폴더를 마늘고 그 안에 유틸 함수들을 모아두는 방식으로 관리할 거예요.

 

 

 

📂 utils 폴더 채우기

utils
├── __init__.py
├── font.py  # 한글 폰트 설정
├── save.py  # 그래프 저장
└── theme.py # 시각화 테마 설정

utils 폴더 안에 총 4개의 파일을 만들거예요. 순서대로 하나씩 설명해볼까 합니다.

 

`__init__.py`

이 파일은 만들기는 하지만 내용은 작성하지 않을 겁니다.

파이썬은 파일 자체가 모듈이라서 파일만 있으면 `import`할 수 있어요. 하지만 폴더를 패키지로 인식하기 위해서는 `__init__.py`라는 파일이 존재해야 합니다. 그래서 과연 이 파일이 하는 건 뭐지? 싶을 겁니다. 이 아이는 '이 폴더가 패키지임!'을 파이썬에게 알립니다. 이게 필수적인 기능이고, 선택적인 기능으로는 패키지 초기화 코드를 작성할 수 있습니다. 우리는 표식 역할만 필요하기 때문에 파일만 만들어두고 비워두도록 할게요.

 

 

`font.py`

import platform
import matplotlib.pyplot as plt

def set_korean_font():
    font_map = {
        "Darwin": "AppleGothic",     # macOS
        "Windows": "Malgun Gothic",  # Windows
    }

    # 현재 OS에 맞는 폰트명 조회, Linux/Colab을 기본값으로 처리
    font_name = font_map.get(platform.system(), "NanumGothic")

    # 한글 폰트 적용
    plt.rcParams["font.family"] = font_name
    # 음수 기호(-) 깨짐 방지
    plt.rcParams["axes.unicode_minus"] = False

    return font_name

 

시각화를 할 때 그래프 제목이나 축 이름에 한글을 쓰면  □□□ 이렇게 깨져서 나오는 경우가 있어요. 파이썬 기본 환경이 한글 폰트를 포함하고 있지 않기 때문입니다. `set_korean_font()`는 현재 운영체제를 감지해서 그에 맞는 한글 폰트를 자동으로 설정해주는 역할을 합니다. macOS는 애플고딕, Windows는 맑은 고딕, Linux나 Google Colab은 나눔고딕을 사용해요. Colab을 사용하신다면 먼저 `!apt-get install -y fonts-nanum`을 실행하고 런타임을 재시작한 뒤 사용하시면 됩니다.

 

 

`save.py`

from pathlib import Path

# 최상위 폴더 고정
FIGURES_DIR = Path("figures")

# 허용된 하위 폴더 목록
VALID_FOLDERS = {"analysis", "tutorial"}


def save_fig(fig, filename, folder, dpi=160):
    # 허용되지 않은 폴더명일 경우 에러 발생
    if folder not in VALID_FOLDERS:
        raise ValueError(f"folder는 {VALID_FOLDERS} 중 하나여야 합니다.")

    # 최종 저장 경로 조합 ex. figures/tutorial
    folder_path = FIGURES_DIR / folder
    # 폴더 없으면 생성, 있어도 에러 없이 통과
    folder_path.mkdir(parents=True, exist_ok=True)

    # .png 확장자 자동 추가
    file_path = folder_path / f"{filename}.png"
    # bbox_inches: tight, 여백 자동 조정
    fig.savefig(file_path, dpi=dpi, bbox_inches="tight")
    print(f"save: {file_path}")
    return file_path

 

`save_fig`는 그래프를 `figures/analysis`, `figures/tutorial` 폴더에 png 파일로 저장해주는 함수입니다. 파일 이름만 넘겨주면 .png 확장자는 자동으로 붙여주고, 폴더가 없으면 알아서 만들어줍니다.

 

 

`theme.py`

import matplotlib.pyplot as plt
import seaborn as sns

BASE_COLOR = "#334155"
SUB_COLOR = "#94a3b8"
GRID_COLOR = "#e2e8f0"


def set_plot_theme():
    sns.set_theme(style="whitegrid")
    # 그림 전체 배경색
    plt.rcParams["figure.facecolor"] = "white"
    # 플롯 영역 배경색, 테두리색
    plt.rcParams["axes.facecolor"] = "white"
    plt.rcParams["axes.edgecolor"] = GRID_COLOR
    # x/y축 레이블 색상
    plt.rcParams["axes.labelcolor"] = BASE_COLOR
    # 전체 텍스트 색상
    plt.rcParams["text.color"] = BASE_COLOR
    # x, y축 눈금 색상
    plt.rcParams["xtick.color"] = BASE_COLOR
    plt.rcParams["ytick.color"] = BASE_COLOR
    # 그리드 선 색상
    plt.rcParams["grid.color"] = GRID_COLOR


def get_palette(n=2):
    """색상 구분이 꼭 필요할 때만 사용할 차분한 팔레트를 반환합니다."""
    palette = [BASE_COLOR, SUB_COLOR, "#64748b", "#cbd5e1"]
    return palette[:n]

두 가지 함수가 있어요. `set_plot_theme()`은 그래프 전체의 배경색, 글자색, 레이블색 등을 한 번에 통일된 스타일로 세팅해주는 함수입니다. 그래프를 그리기 전에 한 번만 호출해두면 이후 모든 그래프에 자동으로 적용됩니다. `get_palette()`는 막대 그래프처럼 여러 항목을 색으로 구분해야 할 때 사용할 색상 목록을 반환해요. 최대 4가지 색상을 진한 것부터 연한 슬레이트 계열로 구성해뒀고, `n`으로 몇 가지 색이 필요한지 지정하면 됩니다.

 

 

일단 이렇게 4개의 파일을 만들어서 추가하겠습니다. 다음 글에서 요긴하게 사용할 예정이니 다들 해줬으면 합니다.

 

 

 

ModuleNotFoundError 발생

보통 `__init__.py`를 만들면 아무 일이 발생하지 않지만 에러가 발생합니다. `ModuleNotFoundError: No module names 'utils'` 이런 에러가 발생했어요. 원인은 간단하지만 해결 방법이 다양해요. 그래서 이렇게 블로그 글로 찾아왔습니다.

 

파이썬은 실행한 파일이 있는 위치를 기준으로 모듈을 탐색하기 때문에 `tutorial/03-matplotlib/`에서 `utils`를 찾습니다. 하지만 `utils`는 `tutorial`과 같은 위치에 존재해요. 그렇게 때문에 `tutorial/03-matplotlib/` 안에서 발견할 수 없는거죠.

 

그래서 여러 시도를 해봤습니다.

 

`root`에서 실행

(venv) python tutorial/03-matplotlib/01-bar.py

루트에서 실행을 하면 `utils`를 발견할 수 있지 않을까? 싶었어요. 그래서 시도해봤는데 같은 오류가 발생합니다. 

 

`sys.path` 추가

import sys
from pathlib import Path

sys.path.append(str(Path(__file__).parents[2]))  # root 경로를 탐색 경로에 추가

이건 성공했어요. 하지만 함수를 만든 이유가 여러 곳에서 여러 번 사용하기 위해서인데 모든 파일마다 이렇게 코드를 넣어야 한다? 이건 너무 비효율의 끝판왕이죠. 이럴거면 `utils`를 안 쓰는게 나은 거 아닐까요? 그래도 성공은 했으니 기록은 해두겠습니다.

 

`pyproject.toml`로 패키지 등록하기

먼저 `toml`에 대해서 알아봐야 할 것 같아요. `toml`은 파일 형식 중 하나입니다. JSON이나 YAML처럼 설정 값을 저장하는 형식인데 사람이 읽고 쓰기 편하도록 만들어진 게 특징이에요. [섹션이름]으로 구역을 나누고 그 아래 `key = value` 형태로 값을 적는 방식이라 직관적입니다. 

 

`pyproject.toml`은 파이썬 프로젝트의 설정 파일이에요. 프로젝트 이름, 버전, 빌드 방식 같은 메타 정보를 담아두는 곳인데, 핵심은 이 프로젝트가 어떤 패키지를 포함하고 있는지를 파이썬 환경에게 알려줄 수 있어요. 그런데 여기서 말하는 패키지는 우리가 환경 세팅을 하면서 만들었던 `requirements.txt`처럼 라이브러리를 기억하도록 하는 게 아니라 `utils` 폴더를 설치 가능한 패키지로 등록해버리는 거예요. 한 번 등록하면 어디서든 `from utils.theme import get_palette`를 사용할 수 있게 됩니다.

 

그래서 `root`폴더 아래에 `pyproject.toml`을 만듭니다.

[build-system]
requires = ["setuptools"]

build-backend = "setuptools.build_meta"

[project]
name = "titanic-data-journey"
version = "0.1.0"

[tool.setuptools.packages.find]
where = ["."]
include = ["utils*"]

하나씩 해석해보자면, 

`[build-system]`은 패키지를 빌드할 때 어떤 도구를 쓸지 정의합니다. `setuptools`는 파이썬에서 가장 널리 쓰이는 빌드 도구인데 `numpy`나 `pandas`처럼 `pip install`로 설치할 수 있는 패키지들도 이런 빌드 도구를 통해서 만들어집니다.

`[project]`는 패키지의 이름과 버전을 정의합니다. `pip install`시 이 이름으로 등록됩니다.

`[tool.setuptools.packages.find]`는 `setuptools`가 패키지를 어디서 어떤걸 찾을지 지정하는데 `where=["."]`는 `root`에서 탐색하고, `incude=["utils*"]`는 `utils`로 시작하는 폴더만 패키지로 등록하겠다는 뜻이에요. `tutorial` 같은 다른 폴더는 패키지로 등록되지 않습니다.

 

(venv) pip install -e

그래서 최종적으로 `root` 경로에서 이 명령어를 실행해주면 됩니다. `-e`는 editable 모드로 파일을 수정해도 재설치 없이 바로 반영된다는 뜻입니다. 개발 중에는 항상 `-e`를 붙이는 게 좋아요.

 

 

이제 어디서 실행하든 깔끔하게 import됩니다.

from utils.theme import get_palette
from utils.font import set_korean_font