📚 엔지니어 · 세션 · 프로젝트 이해 (수정중) · L01

L01 — Django 프로젝트 구조와 settings 환경 분리 (수정중)

작성일자 2026-06-08  ·  수정일자 2026-06-08  ·  강의차수 L01  ·  예상소요 30분

목표 standarda 프로젝트의 폴더 구조(앱들 + 루트 설정 앱)를 읽고, settings 가 base / local / production 으로 갈리는 이유와, 요청이 들어올 때 어떤 설정이 로드되는지 설명할 수 있다.

1. 파일 주고받기 — SFTP / Cyberduck

로컬 컴퓨터와 서버 사이에 파일을 주고받는 방법부터.

  • FTP(File Transfer Protocol) / SFTP(Secure FTP) = 파일을 주고받을 때 컴퓨터끼리 지키는 약속(프로토콜)이다.
  • Cyberduck 같은 클라이언트(연결 프로그램)로 SFTP 연결을 하면, 로컬 → 서버로 파일을 올리고(업로드) 서버 → 로컬로 파일을 받을(다운로드) 수 있다.
  • 예전에는 업로드가 안 돼서 Cyberduck 을 꼭 써야 했지만, 지금은 VS Code(원격 연결)로 다운로드·업로드가 다 된다. 그래서 요즘은 Cyberduck 을 잘 안 쓴다.
  • FTP 는 코드 안에서도 서버 → 다른 서버나 다른 IP 로 파일을 보낼 때 쓸 수 있다. "이런 프로토콜이 있다" 정도만 알아두면 된다.

2. 서버에 있는 standarda-template / standarda-core

  • 서버 홈 디렉토리(ubuntu)에 standarda-template, standarda-core 폴더가 실제로 있다.
  • cookiecutter(프로젝트 뼈대를 찍어내는 도구)로 새 프로젝트를 만들 때는 보통 GitHub 의 standarda-template 저장소(레포)를 당겨온다. (서버 로컬에 있는 것도 당길 수 있지만, 헷갈릴까 봐 로컬 사본은 지워뒀다.)
  • standarda-template 업데이트 방법: 서버에서 standarda-template 코드를 수정한 뒤(스킬·백로그 등 추가) push 한다. 변화가 생기면 자동으로 push 되게 해뒀기 때문에 항상 최신 버전이 유지된다.

3. Django 프로젝트 구조 — 앱들 + 루트(설정) 앱

템플릿 구조 = 우리 프로젝트 구조다. (.claude/, GitHub 설정, 그리고 cookiecutter 의 {project_slug} 폴더)

  • Django 는 백엔드(서버 쪽) 프레임워크다 — 웹 서버를 만들 때 쓰는 뼈대 틀이다. 그 안에서 폴더 구조가 "앱(app)" 단위로 나뉜다.
  • 예를 들어 accounts, export_web, emails, profiles, projects, sms — 이게 전부 Django 앱이다.
  • 프로젝트를 처음 셋업하면 프로젝트 이름과 똑같은 이름의 앱 폴더가 하나 생긴다(예: export_web).
  • 이 루트 앱은 다른 앱들과 달리 한 가지가 더 있다 — settings 폴더를 가지고 있다.
{프로젝트명}/            ← 루트 앱 (프로젝트 이름과 동일)
├── settings/           ← ★ 이 앱에만 있다
├── urls.py
├── wsgi.py
└── ...
accounts/  profiles/  projects/  emails/  sms/  export_web/   ← 일반 Django 앱들

4. settings 는 파일이 아니라 "패키지"

  • 원래 Django 의 settings 는 settings.py 파일 하나다.
  • 우리는 이걸 파이썬 패키지(폴더 + __init__.py)로 만들었다. 폴더 안에는:
settings/
├── __init__.py     # 환경에 따라 무엇을 로드할지 고르는 곳
├── base.py         # 공통 설정
├── local.py        # 개발 환경
└── production.py    # 운영 환경

5. __init__.py — 환경에 따라 무엇을 로드하나

# {프로젝트명}/settings/__init__.py
if os.environ.get('{프로젝트명대문자}_PRODUCTION') == 'True':
    load_dotenv(_BASE_DIR / '.env.production', override=True)
else:
    load_dotenv(_BASE_DIR / '.env', override=True)

from .base import *           # 공통을 먼저 다 가져옴

if os.environ.get('{프로젝트명대문자}_PRODUCTION') == 'True':
    from .production import *  # 운영이면 production 으로 덮어씀
else:
    from .local import *       # 아니면 local
  • 로직은 단순하다: PRODUCTION 이면 base + production, 아니면 base + local.
  • _BASE_DIR: 파이썬 pathlib 의 Path 로 src/(루트) 폴더를 가리킨다. Path(__file__) 에서 부모를 세 번 거슬러 올라가면 src/ 다. src/ 가 항상 프로젝트의 루트(베이스) 폴더다.
  • from .base import * 의 * 는 와일드카드 — 해당 모듈에 있는 모든 값을 다 가져온다는 뜻이다.

6. .env / .env.example

  • .env.example: 템플릿을 당기면 같이 생성된다. "이 형식대로 채워 넣어라" 는 견본이다.
  • 이걸 복사해서 .env 를 만들고, API 키 · ALLOWED_HOSTS · 비밀번호 등을 쫙 채워서 쓴다.
  • .env.production.example 도 있다. 나중에 배포(deploy) 할 때 이걸 복사해 .env.production 을 만든다. (지금은 비어 있다.)

비밀값(키·비번)은 settings 코드가 아니라 .env(개발) / .env.production(운영) 에 둔다.


7. 왜 dev / prod 를 나누나

.env 와 production 을 구분하는 이유는, 개발 환경과 운영 환경이 실제로 다르기 때문이다. 연습할 때 쓰는 책상과 실제 손님을 받는 책상이 다른 것과 같다 — 같은 코드라도 어디에 붙이느냐에 따라 연결할 데가 달라진다.

  • 서버 IP 가 다르다: 개발 서버는 43.200.38.93, 앞으로 Production 서버는 52.79.132.206 에서 관리한다.
  • 그래서 ALLOWED_HOSTS 주소부터 달라진다.
  • DB 가 다르다: Django 프로젝트는 기본적으로 PostgreSQL 과 연결돼 있다. 개발 서버에는 개발용(로컬) PostgreSQL 이, Production 서버에는 운영용 PostgreSQL 이 깔려 있고, 각각 연결해야 한다.
  • API 키가 다르다: 예를 들어 LLM API 키도 개발용 키는 우리 키, 운영용 키는 고객사 키가 된다.

→ 그래서 환경마다 달라지는 값들을 따로따로 관리한다.


8. 요청이 들어오면 무슨 일이 — settings 로딩 흐름

브라우저 (도메인 입력)
  → DNS 가 도메인 → IP 조회        (예: meetings.popupstudio.ai → 43.200.38.93)
  → 그 서버의 포트로 요청 도착       (현재 dev 서버에 배포됨, 예: :8024 에서 Django 가 대기)
  → Django 가 settings/__init__.py 를 "제일 먼저" 읽음
       → os.environ 에서 {프로젝트명대문자}_PRODUCTION 확인
       → 없으면 else → .env + base + local
       → 있으면(True)        → .env.production + base + production

os.environ.get(...) 은 시스템(OS) 환경변수를 읽는다. 환경변수는 프로그램이 실행될 때 참고하라고 OS 쪽에 미리 적어둔 설정값이다. 그러면 그 값은 어떻게 넣어두나? 넣는 방법은 크게 세 가지다.

방법 ① OS 의 /etc/environment 에 박아두기

우분투 OS 전역 환경변수 파일이다. production 서버 OS 에는 박아두고, dev 서버 OS 에는 비워둔다. 그러면 서버 단위로 dev / production 이 갈린다 — 서버에 한 번 박아두면 그 서버에서 뜨는 모든 프로세스가 자동으로 그 값을 갖는다. (dev·prod 가 다른 서버로 나뉘어 있을 때 쓸 수 있는 방식)

# [production 서버] /etc/environment 에 한 줄 추가
{프로젝트명대문자}_PRODUCTION=True

# [dev 서버] /etc/environment 에는 이 줄이 없다 → 그래서 자동으로 local 로 뜸

/etc/environment 는 export 없이 KEY=VALUE 형식이다. 수정 후 새 셸/재로그인부터 적용된다.

방법 ② 명령줄(command line)에서 주입하기

프로세스를 띄우는 명령 앞에 환경변수를 붙여 그 프로세스에만 넣는다. OS 전역에 박지 않으므로 일회성 실행·테스트나, 같은 서버에서 프로세스마다 다른 값을 주고 싶을 때 쓴다.

# 그 명령 한 번에만 적용 (이 프로세스 한정)
{프로젝트명대문자}_PRODUCTION=True gunicorn {프로젝트명}.wsgi:application

# 또는 현재 셸 세션에 export 한 뒤 실행 (그 셸에서 띄우는 프로세스에 적용)
export {프로젝트명대문자}_PRODUCTION=True
python manage.py runserver

방법 ③ .env 파일에 적어두기

코드 옆 .env 파일에 KEY=VALUE 로 적고 python-dotenv 의 load_dotenv() 로 읽어 os.environ 에 채운다(§5·§6). 파일이라 .gitignore 로 빼서 비밀값(API 키·비밀번호) 을 담기 좋다.

# .env  (git 에 안 올린다 — .gitignore)
SECRET_KEY=dev-secret-...
DB_PASSWORD=...
SENDGRID_API_KEY=...
# settings/__init__.py — .env 를 읽어 os.environ 에 채운 뒤 settings 가 그 값을 씀
from dotenv import load_dotenv
load_dotenv(_BASE_DIR / '.env')                 # 개발
# load_dotenv(_BASE_DIR / '.env.production')    # 운영 (PRODUCTION 분기일 때)

그래서 우리 프로젝트는 실제로 어떻게 하나 — ② + ③

위 세 가지 중 우리 프로젝트(popax, popup_home_renewal 등)가 실제로 쓰는 조합은 ② 로 스위치 + ③ 으로 값이다. ①(/etc/environment)은 쓰지 않는다.

  • 스위치는 systemd 서비스 유닛(리눅스가 프로그램을 자동으로 띄우고 관리하는 장치)이 박는다 (②의 실전판 — 명령줄 대신 서비스가 주입). prod 로 돌리는 gunicorn/huey 서비스에: ini # /etc/systemd/system/popax-prod.service Environment="POPAX_PRODUCTION=True" ExecStart=/home/ubuntu/popax/bin/gunicorn popax.wsgi:application --bind 127.0.0.1:8024 ...
  • 값은 .env / .env.production 두 파일로 나눠 담는다 (③). 스위치가 고른 환경의 파일을 load_dotenv 가 읽는다.

💡 왜 ①(/etc/environment)이 아니라 ②(systemd)인가? popax 는 dev 와 prod 가 같은 서버(dev 박스)에서 함께 돈다. 서버가 하나면 OS 전역 파일(/etc/environment)로는 두 환경을 가를 수 없다 — 그래서 프로세스(서비스) 단위로 주입하는 ② 를 쓴다. (/etc/environment 에는 POPAX_PRODUCTION 이 없다.)

  • 결과적으로 코드는 dev/prod 양쪽에 똑같은 한 벌이 올라가고, 갈림은 프로세스를 띄울 때 그 환경변수가 박혀 있느냐가 만든다 — dev 의 runserver 에는 없어서 local, prod 의 systemd 서비스엔 {프로젝트명대문자}_PRODUCTION=True 가 박혀 있어서 production 으로 뜬다.
  • 결과적으로 settings 의 구성은:
  • 개발: base.py 의 모든 내용 + local.py 의 모든 내용
  • 운영: base.py 의 모든 내용 + production.py 의 모든 내용

9. base.py 에 들어가는 "공통" 설정

두 환경이 똑같이 쓰는 것은 전부 base.py 에 한 번만 넣는다. 강의에서 짚은 항목:

  • Django 기본 앱: admin, auth, sessions, messages, staticfiles
  • 설치한 라이브러리: rest_framework(DRF), django_extensions 등
  • 미들웨어: CSRF, security, 요청/응답·HTTP 세션 관리 등
  • ROOT_URLCONF(루트 URL), 템플릿 엔진, WSGI 애플리케이션
  • 패스워드 검증기, LANGUAGE_CODE, TIME_ZONE, USE_TZ
  • static / media 파일 저장 위치, 사용할 User 모델
  • SendGrid API 키, Google OAuth client id, 로깅 설정

10. local 과 production 은 실제로 뭐가 다른가

반대로 환경마다 달라져야 하는 것만 local.py / production.py 로 갈린다.

항목 local.py (개발) production.py (운영)
SECRET_KEY 개발용 운영용 (달라야 함 — 지금 같은 건 나중에 수정 예정)
DEBUG True (에러가 나면 메시지를 브라우저에 표시) False (운영에선 에러 메시지를 띄우면 안 됨)
ALLOWED_HOSTS localhost + dev 서버 IP 43.200.38.93 + dev 도메인 운영 도메인
CSRF dev 주소 기준 운영 주소 기준 (주소에 따라 체크가 달라짐)
DATABASES dev 서버의 PostgreSQL (포트 5432) Production 서버의 PostgreSQL

Django 앱은 자기를 서빙하는 IP/도메인으로 들어오는 요청만 처리한다. 그래서 ALLOWED_HOSTS 에 그 주소들이 들어가 있어야 한다.


오늘 정리 + 다음

  • 정리: 요청이 들어오면 Django 는 (앞단의 WSGI, 즉 웹서버와 Django 를 잇는 다리를 지난 다음에) URL 을 처리하는데, 그 전에 settings 가 조립된다. settings 에서 공통이 아닌 — 환경별로 갈려야 하는 값(DB · 도메인 · IP 등) — 을 local / production 으로 분리해두고, 처음 settings 가 로드될 때 환경변수(PRODUCTION 여부)에 따라 둘 중 하나를 불러온다.
  • 개발 서버에서 개발할 때: base + local
  • 사용자가 운영 URL 로 들어오면: base + production
  • 흔한 함정: "분명히 저장했는데 DB에 없다" → 십중팔구 로컬 DB에 저장됐는데 운영 DB를 보고 있는 것. (요즘은 거의 없는 일이긴 하다.)
  • 다음 시간: settings 관련해서 좀 더 디테일하게 설명할 예정.
  • 자습 권장: 시간 날 때 Django settings 원리를 직접 한번 찾아보기.
이 강의를 학습하셨나요?