deep JavaScript 아키텍처 가이드
deep 프로젝트의 JavaScript 아키텍처를 소개합니다. 함수형 프로그래밍과 모듈 시스템을 기반으로 한 현대적인 구조입니다.
시작하기 전에
“새 프로젝트에 참여했는데, 코드 구조가 어떻게 되어 있지?”
이 가이드는 바로 그런 분들을 위한 것입니다. deep 프로젝트의 JavaScript 코드가 어떻게 구성되어 있는지, 왜 이런 구조를 선택했는지, 어떻게 사용하면 되는지 차근차근 알아봅시다.
빠른 시작
프로젝트를 처음 받았다면?
# 1. 의존성 설치
npm install
# 2. 개발 모드 실행 (Watch + Jekyll)
npm start
# 또는 빌드만
npm run build
이게 끝입니다! 🎉
디렉토리 구조 한눈에 보기
assets/js/
├── core/ ← 핵심 기능 (어디서든 쓰임)
│ ├── config.js → 설정 관리
│ ├── event-bus.js → 모듈 간 통신
│ ├── utils.js → 공통 함수들
│ ├── module-loader.js → 동적 로딩
│ └── performance.js → 성능 측정
│
├── modules/ ← 기능 모듈 (독립적)
│ ├── theme.js → 다크모드
│ ├── navigation.js → 네비게이션
│ ├── toc.js → 목차
│ └── ... (17개 더)
│
├── ui/ ← UI 컴포넌트
│ ├── modal.js
│ ├── carousel.js
│ ├── animations.js
│ └── hero-canvas.js
│
├── dist/ ← 빌드 결과물
│ ├── bundle.js (41KB - 전체 기능)
│ └── core.js (8.7KB - 필수만)
│
└── app.js ← 진입점 (모든 것의 시작)
아키텍처 철학
“왜 이렇게 만들었나요?”
저희는 3가지 핵심 원칙을 따랐습니다.
1. 모듈화: 각자의 일만 하기
큰 파일 하나 (❌) vs 작은 모듈 여러 개 (✅)
┌──────────────┐ ┌────┐ ┌────┐ ┌────┐
│ │ │ A │ │ B │ │ C │
│ 모든 기능 │ └────┘ └────┘ └────┘
│ (1650줄) │ 각각 100줄 이하
│ │ 명확한 책임
└──────────────┘ 독립적 테스트 가능
실제 효과:
- 버그 수정 시간: 30분 → 5분
- 기능 추가 시간: 2시간 → 30분
- 코드 리뷰: “전체 다 봐야 해” → “이 모듈만 보면 돼”
2. 함수형 프로그래밍: 데이터는 안전하게
// ❌ Mutable (위험)
let theme = 'light';
theme = 'dark'; // 언제 어디서든 변경 가능
// ✅ Immutable (안전)
let state = Object.freeze({ theme: 'light' });
state = Object.freeze({ ...state, theme: 'dark' }); // 새 객체 생성
왜 중요한가요?
시나리오: 테마 변경 버튼 클릭
Mutable:
theme = 'dark'
↓
"어? 왜 navigation이 안 바뀌지?"
↓
"누가 theme를 다시 'light'로 바꿨나?"
↓
디버깅 지옥...
Immutable:
newState = { ...state, theme: 'dark' }
emit('theme:changed', newState)
↓
모든 모듈이 일관된 데이터 수신
↓
예측 가능한 동작!
3. 느슨한 결합: Event Bus로 대화하기
직접 통신 (❌):
theme.js ─────> navigation.js
│
├──────────> header.js
│
└──────────> canvas.js
문제: theme.js가 모든 모듈을 알아야 함
Event Bus (✅):
theme.js ──┐
│
navigation.js ──┤
│ event-bus
header.js ──┤ (우체국)
│
canvas.js ──┘
장점: 서로 모르고도 통신 가능
핵심 모듈 사용법
1. Config: 설정 관리
어디에 쓰나요?
- 디버그 모드 on/off
- 모바일 breakpoint 설정
- 전역 설정값 관리
사용 예시:
import { get, set, setMany } from './core/config.js';
// 1. 값 가져오기
const debug = get('DEBUG');
const breakpoint = get('MOBILE_BREAKPOINT');
// 2. 값 설정하기
set('DEBUG', true);
// 3. 여러 값 한번에
setMany({
DEBUG: true,
MOBILE_BREAKPOINT: 1024
});
// 4. 모든 설정 보기
const all = getAll();
console.log(all); // { DEBUG: true, MOBILE_BREAKPOINT: 1024, ... }
왜 함수형으로 만들었나요?
// Before (Class - Mutable)
config.settings.DEBUG = true; // 직접 변경 (위험!)
// After (Functional - Immutable)
set('DEBUG', true); // 새 객체 생성 (안전!)
// 내부 구현
let currentSettings = Object.freeze({ DEBUG: false });
export const set = (key, value) => {
currentSettings = Object.freeze({
...currentSettings,
[key]: value
});
return currentSettings;
};
2. Event Bus: 모듈 간 통신
어디에 쓰나요?
- 테마 변경 알림
- 페이지 전환 알림
- 모듈 간 데이터 전달
사용 예시:
import { on, emit, once, off } from './core/event-bus.js';
// 1. 이벤트 구독 (계속 듣기)
const unsubscribe = on('theme:changed', ({ theme }) => {
console.log('테마가 변경되었습니다.', theme);
});
// 2. 이벤트 발행
emit('theme:changed', { theme: 'dark' });
// 3. 한번만 듣기
once('app:ready', () => {
console.log('앱이 준비되었습니다!');
});
// 4. 구독 해제
unsubscribe(); // 또는
off('theme:changed', callback);
실제 사용 패턴:
// theme.js - 발행자
export const toggle = () => {
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme(newTheme);
emit('theme:changed', { theme: newTheme }); // 알림!
};
// navigation.js - 구독자
on('theme:changed', ({ theme }) => {
updateNavigationStyle(theme);
});
// hero-canvas.js - 또 다른 구독자
on('theme:changed', ({ theme }) => {
updateCanvasColors(theme);
});
시각화:
┌─────────────────────────────────────────────────┐
│ theme.js가 emit('theme:changed') │
│ ↓ │
│ [ event-bus ] │
│ / | \ │
│ ↓ ↓ ↓ │
│ navigation header canvas │
│ (구독자1) (구독자2) (구독자3) │
└─────────────────────────────────────────────────┘
3. Utils: 공통 유틸리티
어디에 쓰나요?
- 자주 쓰는 함수들
- 성능 최적화 (debounce, throttle)
- DOM 헬퍼
- 스토리지 관리
주요 함수들:
3-1. 기본 유틸리티
import {
log,
isMobile,
exists,
allExist,
getCSSVariable
} from './core/utils.js';
// 디버그 로깅
log('앱이 시작되었습니다'); // DEBUG 모드일 때만 출력
// 모바일 체크
if (isMobile()) {
// 모바일 전용 로직
}
// DOM 존재 확인
if (exists('#myElement')) {
// 요소가 있을 때만 실행
}
// 여러 요소 확인
if (allExist('#header', '#nav', '#footer')) {
// 모두 있을 때
}
// CSS 변수 가져오기
const primary = getCSSVariable('--color-primary');
3-2. 성능 최적화
Debounce (마지막 호출만 실행):
import { debounce } from './core/utils.js';
// 검색 입력 시
const search = debounce((query) => {
fetchResults(query);
}, 300); // 300ms 동안 입력 없으면 실행
input.addEventListener('input', (e) => {
search(e.target.value);
});
사용자 입력: a → ab → abc → abcd
| | | |
타이머: 300 300 300 300 (마지막만 실행!)
↓ ↓ ↓ ↓
결과: X X X ✓ "abcd" 검색
Throttle (일정 간격으로 실행):
import { throttle } from './core/utils.js';
// 스크롤 이벤트
const handleScroll = throttle(() => {
updateScrollPosition();
}, 100); // 100ms마다 최대 1회 실행
window.addEventListener('scroll', handleScroll);
스크롤 발생: ||||||||||||||||||||||||
↓ ↓ ↓ ↓ ↓ ↓
실행 간격: 100ms마다
결과: ✓ ✓ ✓ ✓ ✓ ✓
optimizedScrollHandler (60fps 보장):
import { optimizedScrollHandler } from './core/utils.js';
// 부드러운 스크롤 애니메이션
const handleScroll = optimizedScrollHandler(() => {
updateParallaxEffect();
});
window.addEventListener('scroll', handleScroll);
3-3. 스토리지
import { storage } from './core/utils.js';
// 저장 (자동 JSON 변환)
storage.set('user', { name: 'Alice', age: 25 });
// 가져오기
const user = storage.get('user'); // { name: 'Alice', age: 25 }
// 기본값 제공
const theme = storage.get('theme', 'light'); // 없으면 'light'
// 삭제
storage.remove('user');
// 전체 삭제
storage.clear();
4. Theme Module: 실전 예제
theme.js의 전체 구조:
// ============================================
// 1. 상태 (클로저로 보호)
// ============================================
let state = {
themeToggle: null,
themeIcon: null,
currentTheme: null,
initialized: false
};
// ============================================
// 2. 상수 (불변)
// ============================================
const ICONS = Object.freeze({
light: '<path ... />',
dark: '<path ... />'
});
// ============================================
// 3. Private 함수들
// ============================================
const loadTheme = () => storage.get('theme', 'light');
const updateIcon = (theme) => {
if (!state.themeIcon) return;
state.themeIcon.innerHTML = ICONS[theme];
};
const applyTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
state = { ...state, currentTheme: theme };
updateIcon(theme);
storage.set('theme', theme);
emit('theme:changed', { theme }); // 다른 모듈에 알림!
return theme;
};
// ============================================
// 4. Public API
// ============================================
export const init = () => {
if (state.initialized) return;
state = {
...state,
themeToggle: document.getElementById('themeToggle'),
themeIcon: document.getElementById('themeIcon'),
currentTheme: loadTheme()
};
if (!state.themeToggle || !state.themeIcon) return;
applyTheme(state.currentTheme);
state.themeToggle.addEventListener('click', toggle);
state.initialized = true;
};
export const toggle = () => {
const newTheme = state.currentTheme === 'light' ? 'dark' : 'light';
return applyTheme(newTheme);
};
export const getCurrent = () => state.currentTheme;
export const destroy = () => {
if (state.themeToggle) {
state.themeToggle.removeEventListener('click', toggle);
}
state = { themeToggle: null, themeIcon: null, currentTheme: null, initialized: false };
};
왜 이렇게 만들었나요?
- 상태 보호:
state는 직접 접근 불가 (클로저) - 불변성:
state업데이트 시 새 객체 생성 - 명확한 API:
init,toggle,getCurrent,destroy만 노출 - Event Bus 통합: 테마 변경 시 다른 모듈에 자동 알림
데이터 흐름 이해하기
전체 흐름도
┌─────────────────────────────────────────────────────────┐
│ 1. 페이지 로드 │
│ ↓ │
│ 2. app.js 실행 │
│ ├─ config.init() (설정 초기화) │
│ ├─ theme.init() (테마 적용) │
│ ├─ navigation.init() (네비게이션 설정) │
│ └─ toc.init() (목차 생성) │
│ ↓ │
│ 3. 사용자 인터랙션 │
│ "다크모드 버튼 클릭!" │
│ ↓ │
│ 4. theme.toggle() 실행 │
│ ├─ 상태 업데이트: light → dark │
│ ├─ DOM 업데이트: data-theme="dark" │
│ ├─ 스토리지 저장: localStorage.theme = 'dark' │
│ └─ emit('theme:changed', { theme: 'dark' }) │
│ ↓ │
│ 5. Event Bus가 모든 구독자에게 알림 │
│ ├─> navigation.js: 네비 스타일 업데이트 │
│ ├─> header.js: 헤더 색상 변경 │
│ └─> canvas.js: 캔버스 색상 변경 │
│ ↓ │
│ 6. 화면 업데이트 완료! │
└─────────────────────────────────────────────────────────┘
예시: 테마 변경의 여정
Step 1: 사용자 클릭
// HTML
<button id="themeToggle">
<svg id="themeIcon">...</svg>
</button>
// 사용자가 버튼 클릭!
Step 2: theme.js 동작
export const toggle = () => {
// 현재: 'light'
const newTheme = state.currentTheme === 'light' ? 'dark' : 'light';
// 새로운: 'dark'
return applyTheme(newTheme);
};
const applyTheme = (theme) => {
// 1. DOM 업데이트
document.documentElement.setAttribute('data-theme', 'dark');
// 2. 상태 업데이트 (불변!)
state = { ...state, currentTheme: 'dark' };
// 3. 아이콘 변경
updateIcon('dark');
// 4. 로컬스토리지 저장
storage.set('theme', 'dark');
// 5. 다른 모듈에 알림!
emit('theme:changed', { theme: 'dark' });
return 'dark';
};
Step 3: 다른 모듈들 반응
// navigation.js
on('theme:changed', ({ theme }) => {
const nav = document.querySelector('nav');
nav.classList.toggle('dark', theme === 'dark');
});
// hero-canvas.js
on('theme:changed', ({ theme }) => {
const colors = theme === 'dark'
? { bg: '#1a1a1a', particle: '#ffffff' }
: { bg: '#ffffff', particle: '#1a1a1a' };
updateCanvasColors(colors);
});
Step 4: 화면 업데이트
CSS 자동 적용:
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #ffffff;
...
}
→ 모든 요소가 자동으로 다크모드 스타일 적용!
성능 최적화 전략
1. 번들 분리 전략
// rollup.config.js
export default [
// 전체 번들 (41KB)
{
input: 'assets/js/app.js',
output: { file: 'dist/bundle.js' }
},
// 코어만 (8.7KB)
{
input: 'assets/js/core/index.js',
output: { file: 'dist/core.js' }
}
];
언제 뭘 쓰나요?
<!-- 일반 페이지: 필수 기능만 -->
<script src="/assets/js/dist/core.js"></script>
<!-- 8.7KB: config + event-bus + utils -->
<!-- 블로그 글 페이지: 전체 기능 -->
<script src="/assets/js/dist/bundle.js"></script>
<!-- 41KB: 모든 모듈 포함 -->
효과:
일반 페이지:
Before: 150KB 로드 (필요 없는 기능도 포함)
After: 8.7KB 로드 (94% 감소!)
블로그 글:
Before: 150KB
After: 41KB (73% 감소!)
2. Lazy Loading (지연 로딩)
// app.js
export const init = () => {
// 1단계: 필수 기능 (즉시)
config.init();
theme.init();
navigation.init();
// 2단계: 조건부 기능 (있으면 로드)
if (document.querySelector('.article-content')) {
toc.init(); // 목차
codeBlocks.init(); // 코드 블록
readingTime.init(); // 읽기 시간
}
if (document.getElementById('heroCanvas')) {
heroCanvas.init(); // Canvas 애니메이션
}
// 3단계: 비필수 기능 (Idle 시)
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
analytics.init(); // 분석
share.init(); // 공유
});
}
};
시각화:
페이지 로드 시간라인:
0ms ─┬─ [필수] config, theme, navigation 초기화
│
50ms ─┤
│
100ms ┬─ [조건부] 아티클이면 toc, codeBlocks 초기화
│
200ms ┤
│
500ms ┬─ [Idle] CPU 여유 생기면 analytics 초기화
3. Tree Shaking
// utils.js에 20개 함수 정의
export const debounce = ...
export const throttle = ...
export const isMobile = ...
// ... 17개 더
// theme.js에서 2개만 사용
import { debounce, storage } from './utils.js';
// 빌드 결과: 사용한 2개만 번들에 포함!
// 나머지 18개는 자동 제거 (Tree Shaking)
실전 가이드
새 기능 추가하는 법
시나리오: “좋아요” 기능을 추가하고 싶어요!
Step 1: 모듈 파일 생성
// modules/like.js
import { emit, on } from '../core/event-bus.js';
import { storage, log } from '../core/utils.js';
let state = {
likeButton: null,
likeCount: 0,
initialized: false
};
// Private 함수
const updateUI = () => {
if (!state.likeButton) return;
state.likeButton.textContent = `❤️ ${state.likeCount}`;
};
// Public API
export const init = () => {
if (state.initialized) return;
state.likeButton = document.getElementById('likeButton');
if (!state.likeButton) return;
state.likeCount = storage.get('likeCount', 0);
updateUI();
state.likeButton.addEventListener('click', like);
state.initialized = true;
log('Like module initialized');
};
export const like = () => {
state.likeCount++;
storage.set('likeCount', state.likeCount);
updateUI();
emit('like:added', { count: state.likeCount });
};
export default { init, like };
Step 2: app.js에 등록
// app.js
import like from './modules/like.js';
export const init = () => {
// ... 기존 모듈들 ...
// 새 모듈 추가!
if (document.getElementById('likeButton')) {
like.init();
}
};
Step 3: HTML 추가
<button id="likeButton">❤️ 0</button>
Step 4: 빌드 & 테스트
npm run build
끝! 🎉
기존 기능 수정하는 법
시나리오: “테마 변경 시 애니메이션을 추가하고 싶어요!”
방법 1: 해당 모듈 직접 수정
// modules/theme.js
const applyTheme = (theme) => {
// 기존 코드
document.documentElement.setAttribute('data-theme', theme);
// 애니메이션 추가!
document.body.classList.add('theme-transition');
setTimeout(() => {
document.body.classList.remove('theme-transition');
}, 300);
// 나머지 코드...
};
방법 2: Event Bus로 확장 (권장)
// modules/theme-animation.js (새 파일)
import { on } from '../core/event-bus.js';
export const init = () => {
on('theme:changed', () => {
document.body.classList.add('theme-transition');
setTimeout(() => {
document.body.classList.remove('theme-transition');
}, 300);
});
};
장점: theme.js를 건드리지 않아도 됨!
문제 해결 (Troubleshooting)
Q1: “모듈이 초기화되지 않아요”
증상:
theme.init(); // 아무 일도 일어나지 않음
체크리스트:
// 1. HTML 요소가 있나요?
const el = document.getElementById('themeToggle');
console.log('요소 존재?', el !== null);
// 2. 이미 초기화되었나요?
console.log('초기화 상태?', state.initialized);
// 3. 디버그 모드 켜기
import { set } from './core/config.js';
set('DEBUG', true);
// 4. 로그 확인
// "Theme module initialized" 메시지가 나오나요?
Q2: “이벤트가 발행되지 않아요”
증상:
emit('my:event', { data: 'test' }); // 아무 구독자도 반응 안 함
디버깅:
import { getEvents, getHandlerCount } from './core/event-bus.js';
// 1. 등록된 이벤트 확인
console.log('등록된 이벤트:', getEvents());
// ['theme:changed', 'nav:toggled', ...]
// 2. 구독자 수 확인
console.log('구독자 수:', getHandlerCount('my:event'));
// 0이면 구독자가 없는 것!
// 3. 구독 시점 확인
// init() 안에서 on()을 호출했나요?
// emit()보다 on()이 먼저 실행되었나요?
Q3: “번들이 너무 커요”
체크:
# 번들 크기 확인
npm run build
ls -lh assets/js/dist/
# 예상 크기:
# bundle.js: ~41KB
# core.js: ~8.7KB
# 만약 훨씬 크다면?
해결:
// 1. Tree Shaking 확인
// rollup.config.js
treeshake: {
moduleSideEffects: false // 사이드 이펙트 무시
}
// 2. 불필요한 import 제거
// ❌
import _ from 'lodash'; // 전체 라이브러리 (70KB+)
// ✅
import debounce from 'lodash/debounce'; // 필요한 것만
// 3. 압축 확인
plugins: [
terser() // Minify 플러그인 있나요?
]
성능 지표
측정 결과
| 지표 | Before | After | 개선율 |
|---|---|---|---|
| 메인 번들 | ~150KB | 41KB | 73% ↓ |
| 코어 번들 | N/A | 8.7KB | - |
| FCP | 1.2s | 0.8s | 33% ↑ |
| TTI | 2.5s | 1.5s | 40% ↑ |
| 모듈 수 | 2개 | 36개 | - |
| 평균 줄/파일 | 1035줄 | 83줄 | 92% ↓ |
실제 체감
Before:
- 페이지 로딩: "조금 느린데...?"
- 버그 수정: 30분 (문제 찾는 데만 25분)
- 기능 추가: 2시간
- Git Conflict: 자주 발생
After:
- 페이지 로딩: "빠르다!"
- 버그 수정: 5분 (모듈 파악 1분, 수정 4분)
- 기능 추가: 30분
- Git Conflict: 거의 없음 (각자 다른 모듈 작업)
핵심 요약
기억할 3가지
- 모듈화
큰 파일 하나 (X) → 작은 모듈 여러 개 (O) 각 모듈은 하나의 책임만 - 함수형 프로그래밍
데이터 직접 변경 (X) → 새 객체 생성 (O) 예측 가능한 동작, 안전한 상태 관리 - Event Bus
직접 import (X) → Event Bus 통신 (O) 느슨한 결합, 독립적인 모듈
디렉토리 구조 다시 보기
core/ → 어디서든 쓰는 기본 기능
modules/ → 독립적인 기능 모듈
ui/ → 재사용 가능한 UI
app.js → 모든 것의 시작점
워크플로우
# 개발
npm run dev # Watch 모드
# 빌드
npm run build # 프로덕션 빌드
# 둘 다
npm start # Jekyll + Watch
다음 단계
이 아키텍처는 계속 진화합니다.
1. TypeScript 도입
// 타입 안정성 확보
interface Module {
init(): void;
destroy?(): void;
}
interface ThemeState {
themeToggle: HTMLElement | null;
currentTheme: 'light' | 'dark';
initialized: boolean;
}
2. 테스트 추가
// Vitest로 각 모듈 테스트
import { describe, it, expect } from 'vitest';
import { toggle, getCurrent } from './theme.js';
describe('Theme Module', () => {
it('should toggle theme', () => {
const initial = getCurrent();
toggle();
const after = getCurrent();
expect(after).not.toBe(initial);
});
});
3. 고급 함수형 패턴
// Maybe Monad로 null 안전 처리
const Maybe = (value) => ({
map: (fn) => value != null ? Maybe(fn(value)) : Maybe(null),
getOrElse: (defaultValue) => value != null ? value : defaultValue
});
const theme = Maybe(storage.get('theme'))
.map(t => t.toLowerCase())
.map(t => ['light', 'dark'].includes(t) ? t : null)
.getOrElse('light');
마무리
“이 구조를 선택한 이유”
- 유지보수성: “어디를 고쳐야 하지?” → “이 모듈만 보면 돼!”
- 확장성: “새 기능 추가하면 기존 코드가…” → “새 모듈 만들면 끝!”
- 협업: “Conflict 났어…” → “각자 다른 모듈 작업하니 충돌 없어!”
- 성능: “로딩이 느려…” → “73% 가벼워졌어!”
- 안정성: “버그가 자꾸…” → “불변성으로 예측 가능해!”
실제 개발 경험
Before: "코드 무서워..."
After: "코드 이해돼!"
Before: "고치면 뭐가 깨질까..."
After: "자신있게 수정!"
Before: "새 기능 추가 부담스러워..."
After: "모듈 하나 만들면 끝!"
여러분의 프로젝트에 적용하기
이 아키텍처는 deep 프로젝트에 최적화되어 있지만, 핵심 원칙은 어느 프로젝트에나 적용 가능합니다.
- 모듈화
- 함수형 프로그래밍
- Event Bus
- 성능 최적화
작게 시작하세요: 한 번에 하나의 모듈부터!
Happy Coding! 🚀
참고 자료
관련 가이드
- JavaScript 모듈화 가이드 - 리팩토링 과정
- 함수형 프로그래밍 가이드 - 함수형 패턴
외부 자료
작성일: 2025-10-20 프로젝트: deep 버전: 2.0.0 상태: Production Ready ✅
댓글