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 };
};

왜 이렇게 만들었나요?

  1. 상태 보호: state는 직접 접근 불가 (클로저)
  2. 불변성: state 업데이트 시 새 객체 생성
  3. 명확한 API: init, toggle, getCurrent, destroy만 노출
  4. 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가지

  1. 모듈화
    큰 파일 하나 (X) → 작은 모듈 여러 개 (O)
    각 모듈은 하나의 책임만
    
  2. 함수형 프로그래밍
    데이터 직접 변경 (X) → 새 객체 생성 (O)
    예측 가능한 동작, 안전한 상태 관리
    
  3. 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');

마무리

“이 구조를 선택한 이유”

  1. 유지보수성: “어디를 고쳐야 하지?” → “이 모듈만 보면 돼!”
  2. 확장성: “새 기능 추가하면 기존 코드가…” → “새 모듈 만들면 끝!”
  3. 협업: “Conflict 났어…” → “각자 다른 모듈 작업하니 충돌 없어!”
  4. 성능: “로딩이 느려…” → “73% 가벼워졌어!”
  5. 안정성: “버그가 자꾸…” → “불변성으로 예측 가능해!”

실제 개발 경험

Before: "코드 무서워..."
After:  "코드 이해돼!"

Before: "고치면 뭐가 깨질까..."
After:  "자신있게 수정!"

Before: "새 기능 추가 부담스러워..."
After:  "모듈 하나 만들면 끝!"

여러분의 프로젝트에 적용하기

이 아키텍처는 deep 프로젝트에 최적화되어 있지만, 핵심 원칙은 어느 프로젝트에나 적용 가능합니다.

  • 모듈화
  • 함수형 프로그래밍
  • Event Bus
  • 성능 최적화

작게 시작하세요: 한 번에 하나의 모듈부터!


Happy Coding! 🚀


참고 자료

관련 가이드

외부 자료


작성일: 2025-10-20 프로젝트: deep 버전: 2.0.0 상태: Production Ready ✅

댓글