Python 모듈과 패키지 | import, pip, 가상환경 완벽 정리

Python 모듈과 패키지 | import, pip, 가상환경 완벽 정리

이 글의 핵심

Python 모듈과 패키지에 대한 실전 가이드입니다. import, pip, 가상환경 완벽 정리 등을 예제와 함께 상세히 설명합니다.

들어가며

”코드를 모듈로 나누기”

모듈과 패키지는 코드를 구조화하고 재사용하기 위한 핵심 개념입니다.


1. 모듈 (Module)이란?

모듈의 정의

모듈(Module)은 Python 코드를 담은 .py 파일입니다. 함수, 클래스, 변수를 모듈로 만들어 다른 파일에서 재사용할 수 있습니다.

왜 모듈을 사용할까?

  • 코드 재사용: 같은 코드를 여러 곳에서 사용
  • 네임스페이스 관리: 이름 충돌 방지
  • 유지보수 용이: 기능별로 파일 분리

모듈 만들기

예제: 수학 유틸리티 모듈

math_utils.py처럼 파일 하나를 모듈로 두면, 자주 쓰는 도구를 정리해 둔 공구함과 같습니다. 다른 스크립트에서는 import math_utils로 공구함만 가져와 함수 이름으로 꺼내 쓰면 됩니다.

# math_utils.py (모듈 파일)
"""
수학 관련 유틸리티 함수 모음
"""

def add(a, b):
    """두 수를 더합니다."""
    return a + b

def multiply(a, b):
    """두 수를 곱합니다."""
    return a * b

def power(base, exponent):
    """base의 exponent 제곱을 계산합니다."""
    return base ** exponent

# 모듈 레벨 상수
PI = 3.14159
E = 2.71828

# 모듈이 직접 실행될 때만 실행되는 코드
if __name__ == '__main__':
    print("math_utils 모듈 테스트")
    print(f"2 + 3 = {add(2, 3)}")
    print(f"2 * 3 = {multiply(2, 3)}")

모듈 사용하기

# main.py (모듈을 사용하는 파일)
import math_utils

# 모듈의 함수 호출
result1 = math_utils.add(3, 5)
print(result1)  # 8

result2 = math_utils.multiply(4, 7)
print(result2)  # 28

# 모듈의 상수 사용
print(math_utils.PI)  # 3.14159

# 원의 넓이 계산
radius = 5
area = math_utils.PI * math_utils.power(radius, 2)
print(f"반지름 {radius}인 원의 넓이: {area}")  # 78.53975

__name__ 변수의 동작

# math_utils.py
print(f"__name__ = {__name__}")

if __name__ == '__main__':
    print("이 모듈이 직접 실행되었습니다")
else:
    print("이 모듈이 import 되었습니다")

# 실행 결과:
# 1. python math_utils.py 실행 시:
#    __name__ = __main__
#    이 모듈이 직접 실행되었습니다

# 2. import math_utils 실행 시:
#    __name__ = math_utils
#    이 모듈이 import 되었습니다

모듈 검색 경로

Python은 모듈을 다음 순서로 검색합니다:

import sys

# 모듈 검색 경로 확인
for path in sys.path:
    print(path)

# 출력 예시:
# 1. 현재 디렉토리 (스크립트가 있는 위치)
# 2. PYTHONPATH 환경 변수
# 3. 표준 라이브러리 경로
# 4. site-packages (pip로 설치한 패키지)

# 경로 추가 (런타임)
sys.path.append('/custom/path')

2. import 문법

import의 4가지 방법

Python에서 모듈을 가져오는 방법은 크게 4가지입니다. 각각의 장단점을 이해하고 상황에 맞게 사용해야 합니다.

방법 1: 모듈 전체 import

import math

print(math.sqrt(16))  # 4.0
print(math.pi)  # 3.141592653589793
print(math.factorial(5))  # 120

# 장점:
# - 어디서 온 함수인지 명확 (math.sqrt → math 모듈의 sqrt)
# - 이름 충돌 방지

# 단점:
# - 매번 모듈명을 붙여야 함 (타이핑 많음)

언제 사용?

  • 여러 함수를 사용할 때
  • 코드의 출처를 명확히 하고 싶을 때

방법 2: 특정 요소만 import

from math import sqrt, pi, factorial

print(sqrt(16))  # 4.0 (math. 없이 사용)
print(pi)  # 3.141592653589793
print(factorial(5))  # 120

# 장점:
# - 간결한 코드 (모듈명 생략)
# - 필요한 것만 가져옴

# 단점:
# - 출처가 불명확 (sqrt가 어디서 왔는지 알기 어려움)
# - 이름 충돌 가능

이름 충돌 예시:

# 문제 상황
from math import sqrt
from numpy import sqrt  # math.sqrt를 덮어씀!

print(sqrt(16))  # numpy.sqrt 실행 (의도와 다를 수 있음)

# 해결 방법: 별칭 사용
from math import sqrt as math_sqrt
from numpy import sqrt as np_sqrt

print(math_sqrt(16))  # 4.0
print(np_sqrt(16))  # 4.0

언제 사용?

  • 특정 함수를 자주 사용할 때
  • 코드가 짧고 출처가 명확할 때

방법 3: 별칭(Alias) 사용

import math as m
import numpy as np
import pandas as pd

print(m.sqrt(16))  # 4.0
print(np.array([1, 2, 3]))  # [1 2 3]
print(pd.DataFrame({'a': [1, 2]}))

# 장점:
# - 긴 모듈명을 짧게 사용
# - 출처 명확 + 타이핑 간결
# - 관습적 별칭 (np, pd) 사용 시 가독성 향상

# 단점:
# - 비표준 별칭 사용 시 혼란 가능

관습적 별칭:

import numpy as np          # ✅ 표준
import pandas as pd         # ✅ 표준
import matplotlib.pyplot as plt  # ✅ 표준
import tensorflow as tf     # ✅ 표준

import numpy as n           # ❌ 비표준 (혼란 유발)
import pandas as p          # ❌ 비표준

언제 사용?

  • 긴 모듈명을 반복 사용할 때
  • 관습적 별칭이 있는 라이브러리 (numpy → np)

방법 4: 모든 것 import (비추천)

from math import *

print(sqrt(16))  # 4.0
print(pi)  # 3.141592653589793

# 장점:
# - 모든 함수를 바로 사용 가능

# 단점:
# - 어디서 온 함수인지 알 수 없음
# - 이름 충돌 위험 높음
# - 코드 리뷰 어려움
# - PEP 8 스타일 가이드 위반

문제 상황:

from math import *
from statistics import *

# mean 함수가 두 모듈에 모두 있음!
# 어느 mean이 사용되는지 알 수 없음
result = mean([1, 2, 3, 4, 5])

언제 사용?

  • 거의 사용하지 않음 (대화형 셸에서만 가끔)
  • 프로덕션 코드에서는 절대 사용 금지

import 방법 비교표

방법문법사용 예시장점단점추천 상황
모듈 전체import mathmath.sqrt(16)출처 명확타이핑 많음여러 함수 사용
특정 요소from math import sqrtsqrt(16)간결출처 불명확특정 함수만 사용
별칭import math as mm.sqrt(16)출처 명확 + 간결별칭 기억 필요긴 모듈명
전체 importfrom math import *sqrt(16)매우 간결충돌 위험 높음사용 금지

상대 import vs 절대 import

절대 import: 프로젝트 루트부터 전체 경로 지정

# 프로젝트 구조:
# myproject/
# ├── main.py
# └── mypackage/
#     ├── __init__.py
#     ├── utils.py
#     └── core/
#         └── engine.py

# mypackage/core/engine.py에서
from mypackage.utils import helper  # 절대 import

상대 import: 현재 위치 기준으로 경로 지정

# mypackage/core/engine.py에서
from ..utils import helper  # 상대 import (상위 디렉토리)
from . import config        # 상대 import (같은 디렉토리)

# . : 현재 디렉토리
# .. : 상위 디렉토리
# ... : 상위의 상위 디렉토리

비교:

# 절대 import
# ✅ 장점: 명확, 어디서든 동일
# ❌ 단점: 패키지명 변경 시 모든 import 수정 필요

# 상대 import
# ✅ 장점: 패키지 이동/이름 변경 시 유리
# ❌ 단점: 스크립트를 직접 실행할 수 없음 (python engine.py → 에러)

권장 사항: 절대 import를 기본으로 사용하고, 패키지 내부에서만 상대 import 사용


3. 패키지 (Package)

패키지란?

패키지(Package)모듈들을 담은 디렉토리입니다. __init__.py 파일이 있으면 Python이 해당 디렉토리를 패키지로 인식합니다.

왜 패키지를 사용할까?

  • 계층적 구조: 관련 모듈을 그룹화
  • 네임스페이스: 모듈 이름 충돌 방지
  • 배포 용이: 패키지 단위로 배포

패키지 구조 예시

myproject/
├── main.py                 # 실행 파일
├── requirements.txt        # 의존성
└── mypackage/              # 패키지 디렉토리
    ├── __init__.py         # 패키지 초기화 (필수*)
    ├── module1.py          # 모듈 1
    ├── module2.py          # 모듈 2
    └── subpackage/         # 하위 패키지
        ├── __init__.py
        └── module3.py

# *Python 3.3+에서는 __init__.py가 선택사항이지만,
# 명시적으로 만드는 것이 권장됩니다.

패키지 만들기

1단계: 디렉토리와 파일 생성

# mypackage/math_ops.py
def add(a, b):
    """덧셈"""
    return a + b

def subtract(a, b):
    """뺄셈"""
    return a - b

# mypackage/string_ops.py
def reverse(text):
    """문자열 뒤집기"""
    return text[::-1]

def capitalize_words(text):
    """각 단어의 첫 글자 대문자"""
    return ' '.join(word.capitalize() for word in text.split())

2단계: __init__.py 작성

# mypackage/__init__.py
"""
mypackage: 유틸리티 함수 모음
"""

# 하위 모듈에서 함수 가져오기
from .math_ops import add, subtract
from .string_ops import reverse, capitalize_words

# 패키지 버전
__version__ = '1.0.0'

# __all__: from mypackage import *에서 가져올 항목
__all__ = ['add', 'subtract', 'reverse', 'capitalize_words']

# 패키지 초기화 코드 (선택)
print(f"mypackage v{__version__} 로드됨")

3단계: 패키지 사용

# main.py
import mypackage

# __init__.py에서 가져온 함수 사용
print(mypackage.add(3, 5))  # 8
print(mypackage.reverse("Hello"))  # olleH

# 또는 직접 모듈 import
from mypackage.math_ops import add
from mypackage.string_ops import reverse

print(add(10, 20))  # 30
print(reverse("Python"))  # nohtyP

# 또는 __init__.py에서 정의한 함수 직접 import
from mypackage import add, reverse
print(add(1, 2))  # 3

__init__.py의 역할

# 1. 빈 __init__.py: 디렉토리를 패키지로 표시만
# mypackage/__init__.py
# (내용 없음)

# 2. 함수 재export: 편리한 import 경로 제공
# mypackage/__init__.py
from .math_ops import add, subtract
# 이제 from mypackage import add 가능
# from mypackage.math_ops import add 대신

# 3. 패키지 초기화: 설정, 로깅 등
# mypackage/__init__.py
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("패키지 초기화")

# 4. __all__ 정의: 공개 API 명시
# mypackage/__init__.py
__all__ = ['add', 'subtract']  # 공개 함수만

하위 패키지 (Subpackage)

복잡한 프로젝트는 패키지 안에 패키지를 만들 수 있습니다.

myproject/
├── main.py
└── mypackage/
    ├── __init__.py
    ├── math_ops.py
    ├── string_ops.py
    └── advanced/           # 하위 패키지
        ├── __init__.py
        ├── statistics.py
        └── algorithms.py

하위 패키지 사용:

# mypackage/advanced/statistics.py
def mean(numbers):
    """평균 계산"""
    return sum(numbers) / len(numbers)

def median(numbers):
    """중앙값 계산"""
    sorted_nums = sorted(numbers)
    n = len(sorted_nums)
    mid = n // 2
    return sorted_nums[mid] if n % 2 == 1 else (sorted_nums[mid-1] + sorted_nums[mid]) / 2

# main.py에서 사용
from mypackage.advanced.statistics import mean, median

data = [1, 2, 3, 4, 5]
print(f"평균: {mean(data)}")  # 3.0
print(f"중앙값: {median(data)}")  # 3

4. 표준 라이브러리 (Standard Library)

표준 라이브러리란?

Python 설치 시 기본 제공되는 모듈 모음입니다. 별도 설치 없이 import만으로 사용 가능합니다.

자주 사용하는 표준 라이브러리

1. os - 운영체제 기능

import os

# 현재 작업 디렉토리
print(os.getcwd())  # C:\Users\JB\workspace\pkglog.com

# 디렉토리 변경
os.chdir('/path/to/directory')

# 디렉토리 생성
os.mkdir("new_folder")  # 단일 디렉토리
os.makedirs("parent/child/grandchild")  # 중첩 디렉토리

# 파일/디렉토리 존재 확인
print(os.path.exists('file.txt'))  # True/False
print(os.path.isfile('file.txt'))  # 파일인지
print(os.path.isdir('folder'))  # 디렉토리인지

# 경로 조합 (OS 독립적)
path = os.path.join('folder', 'subfolder', 'file.txt')
print(path)  # Windows: folder\subfolder\file.txt
             # Linux/Mac: folder/subfolder/file.txt

# 환경 변수
print(os.environ.get('PATH'))  # PATH 환경 변수
os.environ['MY_VAR'] = 'value'  # 환경 변수 설정

# 파일 목록
files = os.listdir('.')  # 현재 디렉토리의 모든 파일/폴더
print(files)

2. datetime - 날짜와 시간

from datetime import datetime, timedelta, date, time

# 현재 날짜/시간
now = datetime.now()
print(now)  # 2026-03-29 14:30:45.123456

# 포맷팅
formatted = now.strftime("%Y-%m-%d %H:%M:%S")
print(formatted)  # 2026-03-29 14:30:45

# 날짜 계산
tomorrow = now + timedelta(days=1)
next_week = now + timedelta(weeks=1)
two_hours_ago = now - timedelta(hours=2)

print(f"내일: {tomorrow.strftime('%Y-%m-%d')}")
print(f"다음 주: {next_week.strftime('%Y-%m-%d')}")

# 문자열 → datetime
date_str = "2026-12-31 23:59:59"
parsed = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
print(parsed)  # 2026-12-31 23:59:59

# 날짜만
today = date.today()
print(today)  # 2026-03-29

# 시간만
current_time = time(14, 30, 0)  # 14:30:00
print(current_time)

3. random - 난수 생성

import random

# 정수 난수
print(random.randint(1, 10))  # 1~10 (양 끝 포함)
print(random.randrange(0, 10, 2))  # 0, 2, 4, 6, 8 중 하나

# 실수 난수
print(random.random())  # 0.0 ~ 1.0
print(random.uniform(1.0, 10.0))  # 1.0 ~ 10.0

# 리스트에서 선택
colors = ['빨강', '파랑', '초록', '노랑']
print(random.choice(colors))  # 하나 선택
print(random.choices(colors, k=3))  # 중복 허용 3개
print(random.sample(colors, k=2))  # 중복 없이 2개

# 리스트 섞기 (in-place)
numbers = [1, 2, 3, 4, 5]
random.shuffle(numbers)
print(numbers)  # [3, 1, 5, 2, 4] (무작위 순서)

# 시드 설정 (재현 가능한 난수)
random.seed(42)
print(random.randint(1, 100))  # 항상 같은 값

4. json - JSON 처리

import json

# Python 객체 → JSON 문자열
data = {"name": "철수", "age": 25, "hobbies": ["독서", "영화"]}
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)
# {
#   "name": "철수",
#   "age": 25,
#   "hobbies": ["독서", "영화"]
# }

# JSON 문자열 → Python 객체
parsed = json.loads(json_str)
print(parsed['name'])  # 철수
print(type(parsed))  # <class 'dict'>

# 파일로 저장
with open('data.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

# 파일에서 읽기
with open('data.json', 'r', encoding='utf-8') as f:
    loaded = json.load(f)
    print(loaded)

5. sys - 시스템 관련

import sys

# Python 버전
print(sys.version)  # 3.11.0 (main, Oct 24 2022, ...)

# 명령줄 인자
# python script.py arg1 arg2
print(sys.argv)  # ['script.py', 'arg1', 'arg2']

# 모듈 검색 경로
print(sys.path)

# 프로그램 종료
# sys.exit(0)  # 정상 종료
# sys.exit(1)  # 에러 종료

6. pathlib - 경로 처리 (현대적 방법)

from pathlib import Path

# 현재 디렉토리
current = Path.cwd()
print(current)  # C:\Users\JB\workspace\pkglog.com

# 경로 조합
file_path = Path('folder') / 'subfolder' / 'file.txt'
print(file_path)  # folder\subfolder\file.txt

# 파일 존재 확인
if file_path.exists():
    print("파일 존재")

# 파일 읽기/쓰기
file_path.write_text("Hello, World!", encoding='utf-8')
content = file_path.read_text(encoding='utf-8')
print(content)  # Hello, World!

# 파일 정보
print(file_path.name)  # file.txt
print(file_path.stem)  # file
print(file_path.suffix)  # .txt
print(file_path.parent)  # folder\subfolder

# 디렉토리 내 파일 목록
for file in Path('.').glob('*.py'):
    print(file)

5. pip 패키지 관리

pip란?

pip는 Python 패키지를 설치하고 관리하는 패키지 관리자입니다. PyPI(Python Package Index)에서 패키지를 다운로드합니다.

pip 기본 명령어

# 패키지 설치
pip install requests

# 설치 과정:
# 1. PyPI에서 requests 패키지 검색
# 2. 최신 버전 다운로드
# 3. 의존성 패키지도 자동 설치
# 4. site-packages에 설치

# 특정 버전 설치
pip install requests==2.28.0  # 정확한 버전
pip install "requests>=2.28.0"  # 2.28.0 이상
pip install "requests>=2.28.0,<3.0.0"  # 범위 지정

# 패키지 업그레이드
pip install --upgrade requests
pip install -U requests  # 단축 명령

# 패키지 정보 확인
pip show requests
# Name: requests
# Version: 2.28.0
# Summary: Python HTTP for Humans.
# Location: C:\Python311\Lib\site-packages
# Requires: charset-normalizer, idna, urllib3, certifi

# 패키지 삭제
pip uninstall requests
pip uninstall -y requests  # 확인 없이 삭제

# 설치된 패키지 목록
pip list
# Package    Version
# ---------- -------
# pip        23.0.1
# requests   2.28.0
# setuptools 65.5.0

# 오래된 패키지 확인
pip list --outdated
# Package  Version  Latest  Type
# -------- -------- ------- -----
# requests 2.28.0   2.31.0  wheel

# requirements.txt 생성
pip freeze > requirements.txt

# requirements.txt로 설치
pip install -r requirements.txt

requirements.txt 작성법

# requirements.txt

# 1. 정확한 버전 (프로덕션 권장)
requests==2.28.0
flask==2.3.0
pandas==2.0.0

# 2. 최소 버전
numpy>=1.24.0

# 3. 범위 지정
django>=4.0.0,<5.0.0

# 4. 주석
# 웹 프레임워크
flask==2.3.0

# 데이터 처리
pandas==2.0.0
numpy==1.24.0

# 5. 개발 의존성 분리 (선택)
# requirements-dev.txt
pytest==7.3.0
black==23.3.0
flake8==6.0.0

pip 고급 사용법

# 1. 로컬 패키지 설치 (개발 모드)
pip install -e .
# -e: editable mode (소스 코드 수정 시 재설치 불필요)

# 2. GitHub에서 직접 설치
pip install git+https://github.com/user/repo.git

# 3. 특정 브랜치/태그 설치
pip install git+https://github.com/user/repo.git@branch_name

# 4. 로컬 .whl 파일 설치
pip install package-1.0.0-py3-none-any.whl

# 5. 여러 패키지 동시 설치
pip install requests flask pandas

# 6. 캐시 무시하고 설치
pip install --no-cache-dir requests

# 7. 사용자 디렉토리에 설치 (권한 없을 때)
pip install --user requests

pip 트러블슈팅

# 문제 1: pip 버전이 오래됨
pip install --upgrade pip

# 문제 2: SSL 인증서 오류
pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org requests

# 문제 3: 권한 오류 (Windows)
# 관리자 권한으로 실행 또는
pip install --user requests

# 문제 4: 패키지 충돌
pip install --force-reinstall requests

# 문제 5: 의존성 확인
pip check  # 의존성 문제 확인

6. 가상환경 (Virtual Environment)

venv 사용

# 가상환경 생성
python -m venv myenv

# 가상환경 활성화
# Windows
myenv\Scripts\activate

# Mac/Linux
source myenv/bin/activate

# 패키지 설치 (가상환경 내)
pip install requests

# 가상환경 비활성화
deactivate

7. 가상환경 심화

venv 상세 설명

# 가상환경 생성
python -m venv myenv
# myenv: 가상환경 이름 (관례: venv, .venv, env)

# 디렉토리 구조:
# myenv/
# ├── Scripts/       # Windows (실행 파일)
# ├── bin/           # Mac/Linux (실행 파일)
# ├── Lib/           # 설치된 패키지
# └── pyvenv.cfg     # 설정 파일

# 가상환경 활성화
# Windows (PowerShell)
myenv\Scripts\Activate.ps1

# Windows (CMD)
myenv\Scripts\activate.bat

# Mac/Linux
source myenv/bin/activate

# 활성화 확인:
# (myenv) PS C:\Users\JB\workspace\pkglog.com>
# 프롬프트 앞에 (myenv) 표시

# Python 경로 확인
which python  # Mac/Linux
where python  # Windows
# 출력: myenv/bin/python (가상환경의 Python)

# 패키지 설치 (가상환경 내)
pip install requests
pip install flask pandas

# 설치된 패키지 확인
pip list

# requirements.txt 생성
pip freeze > requirements.txt

# 가상환경 비활성화
deactivate
# 프롬프트에서 (myenv) 사라짐

가상환경 실전 워크플로우

# 새 프로젝트 시작
mkdir myproject
cd myproject

# 가상환경 생성 및 활성화
python -m venv venv
venv\Scripts\activate  # Windows

# 패키지 설치
pip install flask sqlalchemy pytest

# 개발 작업...

# requirements.txt 생성 (배포용)
pip freeze > requirements.txt

# Git에 커밋 (venv는 제외)
echo "venv/" >> .gitignore
git add .
git commit -m "Initial commit"

# 다른 환경에서 복원
git clone <repo>
cd myproject
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt

가상환경 vs 전역 환경

구분전역 환경가상환경
설치 위치Python 설치 디렉토리프로젝트 디렉토리
패키지 격리❌ 모든 프로젝트 공유✅ 프로젝트별 독립
버전 충돌⚠️ 발생 가능✅ 없음
권장 사용시스템 도구만모든 프로젝트

8. 실전 예제

예제 1: 완전한 프로젝트 구조

myproject/
├── venv/                   # 가상환경 (Git 제외)
├── src/                    # 소스 코드
│   ├── __init__.py
│   ├── main.py             # 진입점
│   ├── config.py           # 설정
│   ├── utils/              # 유틸리티 패키지
│   │   ├── __init__.py
│   │   ├── file_utils.py   # 파일 처리
│   │   └── string_utils.py # 문자열 처리
│   └── models/             # 데이터 모델
│       ├── __init__.py
│       └── user.py
├── tests/                  # 테스트
│   ├── __init__.py
│   ├── test_utils.py
│   └── test_models.py
├── docs/                   # 문서
│   └── README.md
├── .gitignore              # Git 제외 파일
├── requirements.txt        # 의존성
├── requirements-dev.txt    # 개발 의존성
├── setup.py                # 패키지 설정 (배포용)
└── README.md               # 프로젝트 설명

예제 2: 간단한 웹 스크래퍼 프로젝트

프로젝트 구조:

web_scraper/
├── venv/
├── scraper/
│   ├── __init__.py
│   ├── main.py
│   ├── parser.py
│   └── storage.py
├── requirements.txt
└── README.md

코드 구현:

# scraper/parser.py
import requests
from bs4 import BeautifulSoup

def fetch_page(url):
    """웹 페이지 가져오기"""
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response.text

def parse_titles(html):
    """HTML에서 제목 추출"""
    soup = BeautifulSoup(html, 'html.parser')
    titles = [h2.text.strip() for h2 in soup.find_all('h2')]
    return titles

# scraper/storage.py
import json
from pathlib import Path

def save_to_json(data, filename):
    """데이터를 JSON 파일로 저장"""
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

def load_from_json(filename):
    """JSON 파일에서 데이터 로드"""
    if not Path(filename).exists():
        return None
    with open(filename, 'r', encoding='utf-8') as f:
        return json.load(f)

# scraper/__init__.py
"""
웹 스크래퍼 패키지
"""
from .parser import fetch_page, parse_titles
from .storage import save_to_json, load_from_json

__version__ = '1.0.0'
__all__ = ['fetch_page', 'parse_titles', 'save_to_json', 'load_from_json']

# scraper/main.py
from scraper import fetch_page, parse_titles, save_to_json

def main():
    url = 'https://example.com'
    
    print(f"페이지 가져오는 중: {url}")
    html = fetch_page(url)
    
    print("제목 파싱 중...")
    titles = parse_titles(html)
    
    save_to_json({'titles': titles}, 'output.json')
    
    print(f"✅ {len(titles)}개 제목 저장 완료")

if __name__ == '__main__':
    main()

requirements.txt:

requests==2.31.0
beautifulsoup4==4.12.0

실행:

# 가상환경 설정
python -m venv venv
venv\Scripts\activate

# 의존성 설치
pip install -r requirements.txt

# 실행
python -m scraper.main

9. 모듈 import 문제 해결

문제 1: ModuleNotFoundError

# 에러: ModuleNotFoundError: No module named 'mymodule'

# 원인 1: 모듈이 설치되지 않음
# 해결: pip install mymodule

# 원인 2: 모듈이 sys.path에 없음
import sys
print(sys.path)  # 모듈 검색 경로 확인

# 해결: 경로 추가
sys.path.append('/path/to/module')

# 원인 3: 가상환경이 활성화되지 않음
# 해결: 가상환경 활성화 확인
import sys
print(sys.prefix)  # 가상환경 경로 확인

문제 2: 순환 import (Circular Import)

# module_a.py
from module_b import func_b

def func_a():
    return func_b()

# module_b.py
from module_a import func_a  # 순환 import!

def func_b():
    return func_a()

# 실행 시: ImportError: cannot import name 'func_b'

# 해결 1: import를 함수 내부로 이동 (지연 import)
# module_a.py
def func_a():
    from module_b import func_b  # 함수 호출 시에만 import
    return func_b()

# 해결 2: 공통 모듈로 분리
# common.py
def shared_func():
    pass

# module_a.py
from common import shared_func

# module_b.py
from common import shared_func

# 해결 3: 구조 재설계 (권장)
# 순환 의존성이 생기지 않도록 모듈 구조 개선

문제 3: 상대 import 에러

# 에러: ImportError: attempted relative import with no known parent package

# 원인: 스크립트를 직접 실행
# python mypackage/module.py  # ❌

# 해결: 패키지를 모듈로 실행
# python -m mypackage.module  # ✅

# 또는 절대 import 사용
# from mypackage.utils import helper  # ✅

문제 4: ImportError vs ModuleNotFoundError

# ModuleNotFoundError: 모듈 자체를 찾을 수 없음
# import nonexistent_module

# ImportError: 모듈은 있지만 특정 항목을 찾을 수 없음
# from math import nonexistent_function

# 해결: 모듈 내용 확인
import math
print(dir(math))  # 사용 가능한 모든 항목 출력

10. import 순서와 선택적 의존성

한 파일 안에서 import표준 라이브러리 → 서드파티 → 내 프로젝트 순으로 두면, 의존 관계가 한눈에 들어옵니다(PEP 8 권장). NumPy처럼 있으면 쓰고 없으면 다른 경로로 가는 코드를 쓸 때는 try/except ImportError로 선택적 import를 나눕니다.

# 1. 표준 라이브러리
import os
import sys
from datetime import datetime

# 빈 줄

# 2. 서드파티 라이브러리
import requests
import numpy as np
from flask import Flask

# 빈 줄

# 3. 로컬 모듈
from mypackage import utils
from mypackage.models import User

# 각 그룹 내에서는 알파벳 순으로 정렬

2. 조건부 import

# 선택적 의존성 (패키지가 없어도 동작)
try:
    import numpy as np
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False
    print("NumPy가 설치되지 않았습니다. 기본 기능만 사용합니다.")

def process_data(data):
    if HAS_NUMPY:
        return np.array(data).mean()
    else:
        return sum(data) / len(data)

# 사용
result = process_data([1, 2, 3, 4, 5])
print(result)  # NumPy 있으면 np.mean(), 없으면 순수 Python

3. 지연 import (Lazy Import)

# 무거운 모듈은 필요할 때만 import
def process_image(image_path):
    # PIL은 함수 호출 시에만 import (프로그램 시작 시간 단축)
    from PIL import Image
    
    img = Image.open(image_path)
    return img.resize((100, 100))

# 장점: 프로그램 시작 시간 단축
# 단점: 첫 호출 시 약간 느림

4. __all__ 사용

# mymodule.py
def public_func():
    """공개 함수"""
    pass

def _private_func():
    """비공개 함수 (관례: _ 접두사)"""
    pass

def __internal_func():
    """내부 함수 (강한 비공개: __ 접두사)"""
    pass

# 공개 API 명시
__all__ = ['public_func']

# 다른 파일에서
from mymodule import *
# public_func만 import됨 (_private_func, __internal_func는 제외)

# 명시적 import는 여전히 가능
from mymodule import _private_func  # 가능하지만 권장하지 않음

5. 모듈 리로드 (개발 중)

# 모듈 수정 후 재import가 필요할 때
import importlib
import mymodule

# 모듈 리로드
importlib.reload(mymodule)

# 주의: 프로덕션 코드에서는 사용하지 말 것
# 개발/디버깅 용도로만 사용

11. 연습 문제

문제 1: 간단한 유틸리티 패키지 만들기

다음 구조의 패키지를 만들고 사용하세요:

utils/
├── __init__.py
├── math_utils.py  # add, multiply 함수
└── text_utils.py  # reverse, count_words 함수

문제 2: requirements.txt 작성

다음 패키지를 포함하는 requirements.txt를 작성하세요:

  • requests (2.31.0)
  • flask (2.3.0 이상, 3.0.0 미만)
  • pandas (최신 버전)

문제 3: 표준 라이브러리 활용

datetimerandom을 사용하여 다음 기능을 구현하세요:

  • 오늘부터 7일 후 날짜 출력
  • 1~100 중 랜덤 숫자 10개 생성 (중복 없이)
정답 보기

문제 1:

# utils/math_utils.py
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

# utils/text_utils.py
def reverse(text):
    return text[::-1]

def count_words(text):
    return len(text.split())

# utils/__init__.py
from .math_utils import add, multiply
from .text_utils import reverse, count_words

__all__ = ['add', 'multiply', 'reverse', 'count_words']

# main.py
from utils import add, reverse

print(add(3, 5))  # 8
print(reverse("Hello"))  # olleH

문제 2:

requests==2.31.0
flask>=2.3.0,<3.0.0
pandas

문제 3:

from datetime import datetime, timedelta
import random

# 7일 후
today = datetime.now()
after_7_days = today + timedelta(days=7)
print(after_7_days.strftime("%Y-%m-%d"))

# 랜덤 숫자 10개 (중복 없이)
random_numbers = random.sample(range(1, 101), k=10)
print(random_numbers)

정리

핵심 요약

  1. 모듈: .py 파일, import로 재사용
  2. 패키지: 모듈들의 디렉토리, __init__.py로 초기화
  3. import: 4가지 방법 (전체, 특정, 별칭, *)
  4. 표준 라이브러리: os, datetime, random, json, sys, pathlib
  5. pip: PyPI에서 패키지 설치/관리
  6. 가상환경: 프로젝트별 독립 환경 (venv)
  7. requirements.txt: 의존성 관리

모듈 시스템을 마스터하면

  • 코드 재사용이 쉬워집니다
  • 프로젝트 구조가 깔끔해집니다
  • 협업이 원활해집니다
  • 배포가 간단해집니다

다음 단계

  • 클래스와 객체 - 객체지향 프로그래밍
  • 예외 처리 - 에러 처리
  • 파일 입출력 - 파일 읽기/쓰기

관련 글

  • Python 환경 설정 | Windows/Mac에서 Python 설치하고 시작하기
  • Python 기본 문법 | 변수, 연산자, 조건문, 반복문 완벽 가이드