파이썬으로 만든 스크립트가 잘 돌아가도, 주변 사람에게 공유하려면 항상 이런 문제가 생깁니다.
- “파이썬 설치해야 해?”
- “pip로 뭐 설치해야 해?”
- “실행했더니 검은 창만 뜨고 꺼지는데?”
그래서 결국 많이 선택하는 방법이 EXE로 패키징하는 겁니다.
이번 글은 제가 실제로 가장 많이 쓰는 도구인 PyInstaller 기준으로, “초보도 따라 할 수 있게” 정리했습니다.
1) EXE 만들기 전에 꼭 알아야 할 것
✅ EXE로 만들면 좋은 점
- 파이썬이 설치되지 않은 PC에서도 실행 가능(대부분의 경우)
- 배포/공유가 편해짐
- 파일 1개(또는 폴더 1개)로 정리 가능
⚠️ 현실적으로 생기는 문제
- 용량이 커짐(수십~수백 MB도 흔함)
- 백신 오탐(특히 새로 만든 exe)
- 경로 문제(파일 저장/설정 파일 위치)
- GUI(Tkinter 등)에서는 괜찮은데, 콘솔 출력은 안 보이기도 함
이걸 미리 알고 가면 삽질이 크게 줄어요.
2) 준비물: 파이썬/폴더 구조 추천
권장 폴더 구조 예시
my_app/
main.py
core/
assets/
data/
requirements.txt
- assets/ : 이미지, 아이콘 등 리소스
- data/ : 설정 파일, 저장 파일(앱 실행 중 생성되는 데이터)
- requirements.txt : 의존성 목록
3) PyInstaller 설치
가장 먼저 가상환경을 추천합니다. (빌드가 깔끔해져요)
(선택) 가상환경 만들기
cd my_app
python -m venv .venv
.venv\Scripts\activate
PyInstaller 설치
python -m pip install --upgrade pip
pip install pyinstaller
설치 확인:
pyinstaller --version
4) 가장 기본 빌드: onefile로 EXE 만들기
콘솔 프로그램(터미널 창 필요)
pyinstaller --onefile main.py
GUI 프로그램(창만 띄우고 콘솔 숨김)
pyinstaller --onefile --noconsole main.py
빌드가 끝나면 보통 아래 폴더가 생깁니다.
dist/: 실제 배포 파일(exe)build/: 빌드 캐시main.spec: 빌드 설정 파일
결과 EXE는 dist/main.exe 에서 확인합니다.
5) 아이콘 넣기(윈도우 exe 느낌 살리기)
아이콘은 .ico 파일이 필요합니다. (png 그대로는 안됨)
pyinstaller --onefile --noconsole --icon=assets/app.ico main.py
6) 이미지/설정파일 등 “외부 파일” 포함하기
여기서 많이 막힙니다.
파이썬에서는 상대경로로 잘 읽히는데, exe로 묶이면 경로가 달라져요.
6-1) 빌드 옵션으로 파일 포함
예: assets/ 폴더를 함께 포함
pyinstaller --onefile --noconsole ^
--add-data "assets;assets" ^
main.py
윈도우에서
--add-data "원본;대상"형태로 세미콜론(;)을 사용합니다.
6-2) exe 환경에서 경로 처리(핵심)
파이썬 코드에서 “실행 위치”를 잡는 함수를 넣어두면 안정적입니다.
import os, sysdef resource_path(relative_path: str) -> str:
# PyInstaller로 묶이면 _MEIPASS 경로에 리소스가 풀림
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, relative_path)
사용 예:
icon_path = resource_path("assets/app.ico")
7) “exe 실행하면 저장이 안돼요/초기화돼요” 문제 해결
이 현상은 거의 대부분 저장 위치가 잘못 잡혀서 생깁니다.
- 개발 중: 프로젝트 폴더에 저장
- exe 실행: 사용자 PC의 다른 위치에서 실행 → 상대경로가 달라짐
- 또는 Program Files 아래라면 쓰기 권한 문제로 저장 실패
추천: 사용자 폴더(AppData) 아래에 저장 폴더 만들기
import osdef get_app_data_dir(app_name="MyApp"):
base = os.getenv("APPDATA") # C:\Users\...\AppData\Roaming
path = os.path.join(base, app_name)
os.makedirs(path, exist_ok=True)
return path
저장 예:
cfg_path = os.path.join(get_app_data_dir("HStockGuard"), "config.json")
이렇게 하면 exe로 배포해도 설정/저장 데이터가 유지됩니다.
8) 자주 겪는 에러/해결 체크리스트
✅ 실행하자마자 창이 꺼짐
- 콘솔 모드로 빌드해서 에러 확인
--noconsole빼고 실행해보기
✅ 모듈 못 찾는다고 나옴
- 누락된 모듈을 hidden-import로 추가
pyinstaller --onefile --hidden-import=패키지명 main.py
✅ 백신이 위험하다고 막음
- 새 exe는 오탐이 흔함
- 코드서명까지는 현실적으로 어렵고, 최소한 배포 파일을 자주 바꾸지 않는 게 도움
- 가능하면
onedir배포도 고려(오탐이 덜 나는 경우가 있음)
✅ 용량이 너무 큼
- 가상환경을 깨끗하게 유지
- 불필요한 패키지 제거
--exclude-module사용 고려(상황에 따라)
9) 배포 방식 추천: onefile vs onedir
onefile
- 장점: 파일 1개로 배포 끝
- 단점: 실행 시 임시폴더에 풀리는 과정 때문에 첫 실행이 느릴 수 있음
onedir
pyinstaller --noconsole main.py
- 장점: 실행 빠르고 문제 추적이 쉬움
- 단점: 폴더째로 배포해야 함
개인적으로는 처음엔 onedir로 안정화 → 마지막에 onefile로 마무리하는 경우가 많았습니다.
FAQ
Q1. PyInstaller 말고 다른 방법도 있나요?
A. 네. cx_Freeze, Nuitka 같은 대안도 있습니다. 다만 가장 대중적이고 자료가 많은 건 PyInstaller입니다.
Q2. 32비트/64비트는 어떻게 맞추나요?
A. 빌드에 사용하는 파이썬이 64비트면 보통 64비트 exe가 됩니다. 배포 대상 PC 환경과 맞추세요.
Q3. exe로 만들면 소스가 완전히 숨겨지나요?
A. 완전한 보안은 아닙니다. “배포 편의” 목적에 가깝다고 보는 게 현실적입니다.
부록) 그대로 복사해서 쓰는 샘플 코드(저장 유지 + 리소스 경로)
core/paths.py
import os
import sysdef resource_path(relative_path: str) -> str:
"""
PyInstaller(onefile)에서 리소스 경로를 안전하게 가져온다.
"""
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, relative_path)def appdata_dir(app_name: str = "MyApp") -> str:
"""
AppData\Roaming 아래에 앱 폴더를 생성해 저장 경로로 사용한다.
"""
base = os.getenv("APPDATA") or os.path.expanduser("~")
path = os.path.join(base, app_name)
os.makedirs(path, exist_ok=True)
return path
core/config.py
import json
import os
from .paths import appdata_dirdef load_config(app_name: str = "MyApp", filename: str = "config.json") -> dict:
path = os.path.join(appdata_dir(app_name), filename)
if not os.path.exists(path):
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)def save_config(cfg: dict, app_name: str = "MyApp", filename: str = "config.json") -> str:
path = os.path.join(appdata_dir(app_name), filename)
with open(path, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
return path
main.py (테스트용)
from core.config import load_config, save_configAPP_NAME = "HowinfoPyExeDemo"def main():
cfg = load_config(APP_NAME)
run_count = int(cfg.get("run_count", 0)) + 1
cfg["run_count"] = run_count saved_path = save_config(cfg, APP_NAME)
print(f"[OK] 실행 횟수: {run_count}")
print(f"[OK] 설정 저장 위치: {saved_path}")if __name__ == "__main__":
main()
빌드 명령어(윈도우)
pyinstaller --onefile main.py
GUI + 아이콘 + assets 포함 예시:
pyinstaller --onefile --noconsole ^
--icon=assets/app.ico ^
--add-data "assets;assets" ^
main.py