article.js 리팩토링 가이드 - 유지보수 가능한 JavaScript 구조

“코드 리팩토링은 단순히 코드를 정리하는 것이 아니라, 미래의 나와 팀원들에게 보내는 친절한 편지입니다.”

왜 이 가이드를 읽어야 할까요?

이런 순간을 경험해보신 적 있나요?

오후 3시, 새 기능 추가 작업:

// reactions.js (6개월 전에 작성한 파일)
let issueNumber = null;  // 🤔 이게 어디서 쓰이더라?
let updateInterval = null;  // 🤔 이건 뭐였지?

function init() {
  // ... 200줄의 코드 ...
  // 🤔 테이블 기능을 여기에 추가해야 하나?
  // 🤔 아니면 새 파일을 만들어야 하나?
  // 🤔 이 함수 수정하면 다른 곳이 깨지지 않을까?
}

30분이 지났지만, 아직도 “어디에 코드를 추가해야 할지” 결정하지 못했습니다.

저도 똑같은 경험을 했습니다

제 블로그에 테이블 반응형 처리를 추가하려다가 이런 상황을 마주했습니다.

  1. 파일명은 reactions.js인데, 테이블 기능을 추가해야 함
  2. 전역 변수 3개가 어디서 변경되는지 추적이 안 됨
  3. 10개의 전역 함수가 모두 같은 레벨에 나열되어 있음
  4. “이 함수를 수정하면 안전할까?” 하는 불안감

그때 깨달았습니다.

“내가 작성한 코드인데, 왜 이렇게 무섭지?”

이 가이드로 얻을 수 있는 것

리팩토링 후, 제 코드는 이렇게 바뀌었습니다.

Before:

// 이 변수가 어디서 변경되는지 찾으려면 전체 파일을 읽어야 함
let issueNumber = null;

// 이 함수가 무엇을 하는지 이름만으로는 알 수 없음
async function f() { /* ... */ }

After):

// 명확한 구조
const reactions = {
  state: {
    issueNumber: null  // ← 상태는 여기서만 관리
  },

  async findIssueNumber() {  // ← 이름만 봐도 무엇을 하는지 명확
    // ...
  }
};

이 가이드는 실제 프로젝트에서 적용한 7가지 리팩토링 패턴을 다룹니다.

  1. 설정 객체 패턴 - 흩어진 설정을 한 곳으로
  2. 네임스페이스 패턴 - 전역 오염 방지
  3. 상태 캡슐화 - 예측 가능한 상태 관리
  4. 유틸리티 객체 - 공통 로직 재사용
  5. 모듈 분리 - 기능별 독립성 확보
  6. 시각적 구분 - 코드의 ‘목차’ 만들기
  7. 안전한 초기화 - DOM 준비 상태 체크

이 패턴들은 어떤 JavaScript 프로젝트에도 적용할 수 있습니다.

리팩토링 후, 저는:

  • 새 기능 추가 시간이 절반으로 단축되었습니다
  • 버그 수정 시 영향 범위를 즉시 파악할 수 있게 되었습니다
  • 코드를 다시 열었을 때 불안감이 사라졌습니다

여러분도 같은 결과를 얻을 수 있습니다.

목차

  1. 리팩토링 전후 비교
  2. 핵심 리팩토링 패턴 7가지
  3. 패턴별 상세 설명
  4. 실전 적용 가이드
  5. 함정과 주의사항
  6. 성능과 번들 사이즈
  7. 참고 자료

리팩토링 전후 비교

Before: reactions.js

/**
 * GitHub Issues Reactions 통합
 */
(function() {
  'use strict';

  const REPO_OWNER = 'lledellebell';
  const REPO_NAME = 'learn-cs';
  const GITHUB_API_BASE = 'https://api.github.com';
  const EMOJI_OPTIONS = { /* ... */ };

  let issueNumber = null;     // ❌ 전역 상태
  let issueUrl = null;        // ❌ 전역 상태
  let updateInterval = null;  // ❌ 전역 상태

  async function findIssueNumber() { /* ... */ }
  async function fetchReactions() { /* ... */ }
  function countReactions() { /* ... */ }
  function updateUI() { /* ... */ }
  function handleReactionClick() { /* ... */ }
  // ... 10개의 전역 함수들

  async function init() {
    // ❌ 테이블 반응형 처리는 여기에 없음 (다른 파일에 분산)
    // ❌ reactions만 처리하는데 파일명과 일치하지 않음
  }

  init();
})();

문제점:

  • ❌ 파일명이 reactions.js인데 나중에 테이블 처리 기능이 필요함
  • ❌ 전역 변수 3개 (issueNumber, issueUrl, updateInterval)
  • ❌ 10개의 전역 함수들이 모두 같은 레벨에 존재
  • ❌ 설정값과 비즈니스 로직이 섞여 있음
  • ❌ 디버깅 어려움 (어느 함수가 state를 변경하는지 불명확)

After: article.js (구조화된 코드)

/**
 * deep - Article Scripts
 */
(function() {
  'use strict';

  // =========================================
  // 설정
  // =========================================
  const CONFIG = {
    DEBUG: false,
    REPO_OWNER: 'lledellebell',
    REPO_NAME: 'learn-cs',
    REACTION_UPDATE_INTERVAL: 30000,
    EMOJI_OPTIONS: { /* ... */ }
  };

  // =========================================
  // 유틸리티 함수
  // =========================================
  const utils = {
    log: (...args) => { /* ... */ },
    exists: (selector) => document.querySelector(selector) !== null
  };

  // =========================================
  // 테이블 반응형 처리
  // =========================================
  const responsiveTables = {
    addDataLabels() { /* ... */ },
    init() { this.addDataLabels(); }
  };

  // =========================================
  // GitHub 반응 통합
  // =========================================
  const reactions = {
    state: {  // ✅ 상태 캡슐화
      issueNumber: null,
      issueUrl: null,
      updateInterval: null
    },

    async findIssueNumber() { /* ... */ },
    async fetchReactions() { /* ... */ },
    countReactions() { /* ... */ },
    updateUI() { /* ... */ },
    handleReactionClick() { /* ... */ },
    // ... 모든 메서드가 객체 안에 정리됨

    async init() { /* ... */ }
  };

  // =========================================
  // 메인 초기화
  // =========================================
  const init = () => {
    responsiveTables.init();
    reactions.init();
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

개선점:

  • ✅ 파일명이 article.js로 변경되어 역할 명확화
  • ✅ 모든 설정이 CONFIG 객체로 통합
  • ✅ 상태가 reactions.state로 캡슐화
  • ✅ 기능별로 모듈화 (utils, responsiveTables, reactions)
  • ✅ 명확한 시각적 구분 (주석 섹션)
  • ✅ 새 기능 추가가 쉬움 (새 객체만 추가하면 됨)

핵심 리팩토링 패턴 7가지

이 리팩토링에서 적용한 핵심 패턴들입니다.

1. 설정 객체 패턴 (Configuration Object Pattern)

모든 설정값을 하나의 객체로 통합

// ❌ Before: 흩어진 상수들
const REPO_OWNER = 'lledellebell';
const REPO_NAME = 'learn-cs';
const UPDATE_INTERVAL_MS = 30000;

// ✅ After: 통합된 설정 객체
const CONFIG = {
  DEBUG: false,
  REPO_OWNER: 'lledellebell',
  REPO_NAME: 'learn-cs',
  REACTION_UPDATE_INTERVAL: 30000,
  EMOJI_OPTIONS: { /* ... */ }
};

장점:

  • 설정값 관리 용이
  • 환경별 설정 교체 가능
  • 타입스크립트 변환 시 유리

2. 네임스페이스 패턴 (Namespace Pattern)

관련 기능들을 객체로 그룹화

// ❌ Before: 전역 함수들
function addDataLabels() { /* ... */ }
function initTables() { /* ... */ }

// ✅ After: 네임스페이스로 그룹화
const responsiveTables = {
  addDataLabels() { /* ... */ },
  init() { /* ... */ }
};

3. 상태 캡슐화 패턴 (State Encapsulation Pattern)

전역 변수를 객체의 state 프로퍼티로 캡슐화

// ❌ Before: 전역 변수
let issueNumber = null;
let issueUrl = null;
let updateInterval = null;

// ✅ After: 상태 캡슐화
const reactions = {
  state: {
    issueNumber: null,
    issueUrl: null,
    updateInterval: null
  },

  async updateReactions() {
    // this.state로 접근
    if (!this.state.issueNumber) { /* ... */ }
  }
};

4. 유틸리티 객체 패턴 (Utility Object Pattern)

공통 유틸리티 함수들을 별도 객체로 분리

const utils = {
  log: (...args) => {
    if (CONFIG.DEBUG) {
      console.log('[Article]', ...args);
    }
  },

  exists: (selector) => document.querySelector(selector) !== null
};

// 사용 예
utils.log('Reactions initialized');
if (utils.exists('#reactionsSection')) { /* ... */ }

5. 모듈 분리 패턴 (Module Separation Pattern)

기능별로 독립적인 모듈 생성

// ✅ 각 모듈은 독립적으로 동작
const responsiveTables = { /* ... */ };
const reactions = { /* ... */ };

// 메인 초기화에서 조합
const init = () => {
  responsiveTables.init();
  reactions.init();
};

6. 시각적 구분 패턴 (Visual Separation Pattern)

주석으로 코드 섹션을 명확하게 구분

// =========================================
// 설정
// =========================================
const CONFIG = { /* ... */ };

// =========================================
// 유틸리티 함수
// =========================================
const utils = { /* ... */ };

// =========================================
// 테이블 반응형 처리
// =========================================
const responsiveTables = { /* ... */ };

7. 안전한 초기화 패턴 (Safe Initialization Pattern)

DOM 준비 상태를 확인하여 안전하게 초기화

// ❌ Before: init() 함수 내부에서 체크
async function init() {
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
    return;
  }
  // ...
}
init();

// ✅ After: 진입점에서 체크
const init = () => { /* ... */ };

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}

패턴별 상세 설명

패턴 1: 설정 객체 패턴

왜 필요할까요?

설정값이 코드 곳곳에 흩어져 있으면:

  • 값을 변경할 때 여러 곳을 수정해야 함
  • 환경별 설정(개발/운영)을 관리하기 어려움
  • 어떤 값이 설정인지 파악하기 어려움

상세 구현

// =========================================
// 설정
// =========================================
const CONFIG = {
  // 디버그 모드 (개발 시 true로 설정)
  DEBUG: false,

  // GitHub 저장소 정보
  REPO_OWNER: 'lledellebell',
  REPO_NAME: 'learn-cs',
  GITHUB_API_BASE: 'https://api.github.com',

  // 업데이트 간격 (밀리초)
  REACTION_UPDATE_INTERVAL: 30000, // 30초

  // 이모지 옵션 (GitHub API reaction 타입과 매핑)
  EMOJI_OPTIONS: {
    '+1': { emoji: '👍', label: '좋아요' },
    'heart': { emoji: '❤️', label: '사랑해요' },
    'laugh': { emoji: '😊', label: '웃겨요' },
    'hooray': { emoji: '🎉', label: '축하해요' },
    'rocket': { emoji: '🚀', label: '대단해요' },
    'eyes': { emoji: '👀', label: '눈여겨봐요' }
  }
};

환경별 설정 적용하기

설정 객체 패턴의 강력한 점은 환경별 설정을 쉽게 교체할 수 있다는 것입니다.

// 기본 설정
const DEFAULT_CONFIG = {
  DEBUG: false,
  REPO_OWNER: 'lledellebell',
  REPO_NAME: 'learn-cs',
  REACTION_UPDATE_INTERVAL: 30000
};

// 개발 환경 설정
const DEV_CONFIG = {
  ...DEFAULT_CONFIG,
  DEBUG: true,
  REACTION_UPDATE_INTERVAL: 5000 // 개발 시 5초마다 갱신
};

// 현재 환경에 따라 설정 선택
const CONFIG = window.location.hostname === 'localhost'
  ? DEV_CONFIG
  : DEFAULT_CONFIG;

실전 활용: 설정 검증

const CONFIG = {
  DEBUG: false,
  REPO_OWNER: 'lledellebell',
  REPO_NAME: 'learn-cs',
  REACTION_UPDATE_INTERVAL: 30000,
  EMOJI_OPTIONS: { /* ... */ }
};

// ✅ 설정 검증 함수
function validateConfig(config) {
  if (!config.REPO_OWNER || !config.REPO_NAME) {
    throw new Error('REPO_OWNER와 REPO_NAME은 필수입니다.');
  }

  if (config.REACTION_UPDATE_INTERVAL < 1000) {
    console.warn('업데이트 간격이 너무 짧습니다. 최소 1초를 권장합니다.');
  }

  return true;
}

// 초기화 시 검증
validateConfig(CONFIG);

Object.freeze로 설정 보호하기

// ✅ 설정 객체를 읽기 전용으로 만들기
const CONFIG = Object.freeze({
  DEBUG: false,
  REPO_OWNER: 'lledellebell',
  REPO_NAME: 'learn-cs',
  REACTION_UPDATE_INTERVAL: 30000,
  EMOJI_OPTIONS: Object.freeze({
    '+1': Object.freeze({ emoji: '👍', label: '좋아요' }),
    'heart': Object.freeze({ emoji: '❤️', label: '사랑해요' })
  })
});

// ❌ 실수로 변경 시도해도 에러 발생 (strict mode)
CONFIG.DEBUG = true; // TypeError (strict mode)

패턴 2: 네임스페이스 패턴

왜 필요할까요?

JavaScript는 전역 스코프 오염이 쉽게 발생합니다. 특히:

  • 여러 스크립트 파일에서 같은 이름의 함수 사용 시 충돌
  • 코드 양이 늘어날수록 어떤 함수가 어디에 속하는지 불명확
  • 리팩토링 시 의존성 파악이 어려움

Before/After 비교

// ❌ Before: 전역 함수들
function addDataLabels() {
  const tables = document.querySelectorAll('.article-body table');
  tables.forEach(table => {
    // 테이블 처리 로직
  });
}

function findIssueNumber() {
  // GitHub issue 찾기 로직
}

function fetchReactions() {
  // reactions 가져오기 로직
}

// 어떤 함수가 어느 기능에 속하는지 불명확!
// ✅ After: 네임스페이스로 그룹화
const responsiveTables = {
  addDataLabels() {
    const tables = document.querySelectorAll('.article-body table');
    tables.forEach(table => {
      // 테이블 처리 로직
    });
  },

  init() {
    this.addDataLabels();
  }
};

const reactions = {
  async findIssueNumber() {
    // GitHub issue 찾기 로직
  },

  async fetchReactions(issueNum) {
    // reactions 가져오기 로직
  },

  async init() {
    const issueInfo = await this.findIssueNumber();
    const data = await this.fetchReactions(issueInfo.number);
  }
};

// ✅ 명확한 구분!
responsiveTables.init();
reactions.init();

실전 활용: 체계적인 구조

// ✅ 복잡한 애플리케이션의 네임스페이스 구조
const app = {
  // 설정
  config: {
    apiUrl: 'https://api.example.com',
    timeout: 5000
  },

  // API 통신
  api: {
    async get(endpoint) {
      const response = await fetch(`${app.config.apiUrl}${endpoint}`);
      return response.json();
    },

    async post(endpoint, data) {
      // POST 로직
    }
  },

  // UI 관련
  ui: {
    showLoading() { /* ... */ },
    hideLoading() { /* ... */ },
    showError(message) { /* ... */ }
  },

  // 이벤트 핸들러
  handlers: {
    onButtonClick(event) { /* ... */ },
    onFormSubmit(event) { /* ... */ }
  },

  // 초기화
  init() {
    this.ui.showLoading();
    // ...
  }
};

app.init();

충돌 방지 예제

// ❌ Before: 전역 충돌 가능성
// file1.js
function init() {
  console.log('File 1 init');
}

// file2.js
function init() {  // ❌ 충돌!
  console.log('File 2 init');
}

// ✅ After: 네임스페이스로 충돌 방지
// file1.js
const module1 = {
  init() {
    console.log('Module 1 init');
  }
};

// file2.js
const module2 = {
  init() {
    console.log('Module 2 init');
  }
};

// 사용
module1.init(); // "Module 1 init"
module2.init(); // "Module 2 init"

패턴 3: 상태 캡슐화 패턴

왜 필요할까요?

전역 변수는 디버깅의 악몽입니다.

// ❌ Before: 전역 변수들
let issueNumber = null;
let issueUrl = null;
let updateInterval = null;

async function findIssueNumber() {
  // issueNumber를 변경
  issueNumber = 123;
}

function startAutoUpdate() {
  // updateInterval을 변경
  updateInterval = setInterval(/* ... */, 30000);
}

// 문제:
// 1. 어느 함수가 어떤 변수를 변경하는지 추적 어려움
// 2. 여러 함수에서 동시에 접근 시 예측 불가능한 동작
// 3. 테스트하기 어려움 (상태 초기화가 복잡)

상태 캡슐화로 해결

// ✅ After: 상태 캡슐화
const reactions = {
  state: {
    issueNumber: null,
    issueUrl: null,
    updateInterval: null
  },

  async findIssueNumber() {
    const pathname = window.location.pathname;
    // ... 로직 ...

    // ✅ 명확: reactions.state를 통해서만 변경
    this.state.issueNumber = issueInfo.number;
    this.state.issueUrl = issueInfo.url;
  },

  startAutoUpdate() {
    // ✅ 상태 관리가 reactions 객체 내부로 제한됨
    this.state.updateInterval = setInterval(
      async () => await this.updateReactions(),
      CONFIG.REACTION_UPDATE_INTERVAL
    );
  },

  stopAutoUpdate() {
    if (this.state.updateInterval) {
      clearInterval(this.state.updateInterval);
      this.state.updateInterval = null;
    }
  }
};

장점 시각화

❌ Before (전역 변수):

[전역 스코프]
├─ issueNumber ─┬─ findIssueNumber()에서 변경
│               ├─ updateReactions()에서 읽기
│               └─ init()에서 읽기
│
├─ updateInterval ─┬─ startAutoUpdate()에서 생성
│                  └─ stopAutoUpdate()에서 제거
│
└─ [어디서든 접근 가능 → 추적 어려움]


✅ After (캡슐화):

[reactions 객체]
└─ state
   ├─ issueNumber ─┬─ findIssueNumber()에서만 변경
   │               └─ updateReactions()에서 읽기
   │
   └─ updateInterval ─┬─ startAutoUpdate()에서만 생성
                      └─ stopAutoUpdate()에서만 제거

[명확한 경계 → 디버깅 쉬움]

실전: Getter/Setter 패턴

더 엄격한 캡슐화가 필요하다면 Getter/Setter를 사용할 수 있습니다.

const reactions = {
  // Private state (직접 접근 불가)
  _state: {
    issueNumber: null,
    issueUrl: null,
    updateInterval: null
  },

  // ✅ Getter: 읽기 전용 접근
  get issueNumber() {
    return this._state.issueNumber;
  },

  get issueUrl() {
    return this._state.issueUrl;
  },

  // ✅ Setter: 검증 로직 추가 가능
  setIssue(number, url) {
    if (typeof number !== 'number' || number <= 0) {
      throw new Error('Invalid issue number');
    }

    this._state.issueNumber = number;
    this._state.issueUrl = url;

    utils.log('Issue set:', number, url);
  },

  async findIssueNumber() {
    const issueInfo = await this._fetchFromAPI();

    // ✅ setter를 통해 설정 (검증 포함)
    this.setIssue(issueInfo.number, issueInfo.url);
  }
};

// 사용
console.log(reactions.issueNumber); // ✅ 읽기 가능
reactions._state.issueNumber = 999;  // ❌ 권장하지 않음 (컨벤션)
reactions.setIssue(123, 'https://...');  // ✅ setter 사용

실전: 상태 변경 추적

const reactions = {
  state: {
    issueNumber: null,
    issueUrl: null,
    updateInterval: null
  },

  // ✅ 상태 변경 히스토리 추적 (디버깅용)
  _stateHistory: [],

  _logStateChange(key, oldValue, newValue) {
    if (CONFIG.DEBUG) {
      this._stateHistory.push({
        timestamp: Date.now(),
        key,
        oldValue,
        newValue,
        stack: new Error().stack
      });

      utils.log(`State changed: ${key}`, {
        from: oldValue,
        to: newValue
      });
    }
  },

  setIssue(number, url) {
    const oldNumber = this.state.issueNumber;
    const oldUrl = this.state.issueUrl;

    this.state.issueNumber = number;
    this.state.issueUrl = url;

    this._logStateChange('issueNumber', oldNumber, number);
    this._logStateChange('issueUrl', oldUrl, url);
  }
};

패턴 4: 유틸리티 객체 패턴

왜 필요할까요?

프로젝트가 커지면서 반복되는 코드 패턴이 생깁니다.

// ❌ Before: 곳곳에 흩어진 반복 코드
async function findIssueNumber() {
  console.log('[Reactions]', 'Finding issue number...');
  // ...
}

async function fetchReactions() {
  console.log('[Reactions]', 'Fetching reactions...');
  // ...
}

function renderButtons() {
  if (document.querySelector('#reactionsContainer') === null) {
    return;
  }
  // ...
}

function updateUI() {
  if (document.querySelector('#reactionsSection') === null) {
    return;
  }
  // ...
}

유틸리티로 추출

// ✅ After: 공통 로직을 유틸리티로 추출
const utils = {
  /**
   * 디버그 로그 출력 (DEBUG 모드일 때만)
   */
  log: (...args) => {
    if (CONFIG.DEBUG) {
      console.log('[Article]', ...args);
    }
  },

  /**
   * 요소가 존재하는지 확인
   */
  exists: (selector) => {
    return document.querySelector(selector) !== null;
  },

  /**
   * 요소를 안전하게 가져오기 (없으면 null 반환)
   */
  getElement: (selector) => {
    const el = document.querySelector(selector);
    if (!el) {
      utils.log(`Element not found: ${selector}`);
    }
    return el;
  },

  /**
   * 여러 요소를 안전하게 가져오기
   */
  getElements: (selector) => {
    const elements = document.querySelectorAll(selector);
    utils.log(`Found ${elements.length} elements for: ${selector}`);
    return elements;
  }
};

// ✅ 사용 예
async function findIssueNumber() {
  utils.log('Finding issue number...');
  // ...
}

function renderButtons() {
  if (!utils.exists('#reactionsContainer')) {
    return;
  }
  // ...
}

실전: 확장 가능한 유틸리티

const utils = {
  // 로깅
  log: (...args) => {
    if (CONFIG.DEBUG) {
      console.log('[Article]', ...args);
    }
  },

  error: (...args) => {
    console.error('[Article Error]', ...args);
  },

  warn: (...args) => {
    console.warn('[Article Warning]', ...args);
  },

  // DOM 조작
  exists: (selector) => document.querySelector(selector) !== null,

  getElement: (selector) => document.querySelector(selector),

  getElements: (selector) => document.querySelectorAll(selector),

  createElement: (tag, attrs = {}, children = []) => {
    const el = document.createElement(tag);

    Object.entries(attrs).forEach(([key, value]) => {
      if (key === 'className') {
        el.className = value;
      } else if (key === 'dataset') {
        Object.entries(value).forEach(([dataKey, dataValue]) => {
          el.dataset[dataKey] = dataValue;
        });
      } else {
        el.setAttribute(key, value);
      }
    });

    children.forEach(child => {
      if (typeof child === 'string') {
        el.appendChild(document.createTextNode(child));
      } else {
        el.appendChild(child);
      }
    });

    return el;
  },

  // 비동기 유틸리티
  sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),

  retry: async (fn, maxRetries = 3, delay = 1000) => {
    for (let i = 0; i < maxRetries; i++) {
      try {
        return await fn();
      } catch (error) {
        if (i === maxRetries - 1) {
          throw error;
        }
        utils.warn(`Retry ${i + 1}/${maxRetries} after error:`, error);
        await utils.sleep(delay * Math.pow(2, i)); // Exponential backoff
      }
    }
  },

  // 데이터 변환
  debounce: (fn, delay) => {
    let timeoutId;
    return function(...args) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => fn.apply(this, args), delay);
    };
  },

  throttle: (fn, limit) => {
    let inThrottle;
    return function(...args) {
      if (!inThrottle) {
        fn.apply(this, args);
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      }
    };
  }
};

// ✅ 사용 예
const debouncedSearch = utils.debounce((query) => {
  utils.log('Searching for:', query);
  // 검색 로직
}, 300);

// Retry 패턴
const data = await utils.retry(
  async () => await fetchReactions(issueNumber),
  3,  // 최대 3번 재시도
  1000 // 1초 대기
);

프로젝트 전반에서 재사용

// ✅ 다른 모듈에서도 utils 사용
const responsiveTables = {
  addDataLabels() {
    const tables = utils.getElements('.article-body table');

    tables.forEach(table => {
      const headers = [];
      const headerCells = table.querySelectorAll('thead th');

      headerCells.forEach(th => {
        headers.push(th.textContent.trim());
      });

      const rows = table.querySelectorAll('tbody tr');
      rows.forEach(row => {
        const cells = row.querySelectorAll('td');
        cells.forEach((td, index) => {
          if (headers[index]) {
            td.setAttribute('data-label', headers[index]);
          }
        });
      });
    });

    utils.log('Responsive tables initialized for', tables.length, 'tables');
  },

  init() {
    this.addDataLabels();
  }
};

패턴 5: 모듈 분리 패턴

왜 필요할까요?

하나의 파일에 여러 기능이 섞이면:

  • 코드 찾기 어려움
  • 테스트하기 어려움
  • 재사용하기 어려움
  • 새 기능 추가 시 영향 범위 파악이 어려움

독립적인 모듈 설계

// ✅ 모듈 1: 테이블 반응형 처리
const responsiveTables = {
  addDataLabels() {
    const tables = document.querySelectorAll('.article-body table');

    tables.forEach(table => {
      const headers = [];
      const headerCells = table.querySelectorAll('thead th');

      headerCells.forEach(th => {
        headers.push(th.textContent.trim());
      });

      const rows = table.querySelectorAll('tbody tr');
      rows.forEach(row => {
        const cells = row.querySelectorAll('td');
        cells.forEach((td, index) => {
          if (headers[index]) {
            td.setAttribute('data-label', headers[index]);
          }
        });
      });
    });

    utils.log('Responsive tables initialized for', tables.length, 'tables');
  },

  init() {
    this.addDataLabels();
  }
};

// ✅ 모듈 2: GitHub 반응 통합
const reactions = {
  state: {
    issueNumber: null,
    issueUrl: null,
    updateInterval: null
  },

  async findIssueNumber() { /* ... */ },
  async fetchReactions(issueNum) { /* ... */ },
  countReactions(reactions) { /* ... */ },
  updateUI(reactionCounts) { /* ... */ },
  handleReactionClick(event) { /* ... */ },
  renderReactionButtons() { /* ... */ },
  addExplanation() { /* ... */ },
  async updateReactions() { /* ... */ },
  startAutoUpdate() { /* ... */ },
  stopAutoUpdate() { /* ... */ },
  handleVisibilityChange() { /* ... */ },

  async init() { /* ... */ }
};

// ✅ 메인: 모듈들을 조합
const init = () => {
  if (CONFIG.DEBUG) {
    console.log('🚀 Article 스크립트 초기화 시작...');
  }

  // 순서대로 초기화 (의존성 없음)
  responsiveTables.init();
  reactions.init();

  if (CONFIG.DEBUG) {
    console.log('✅ Article 스크립트 초기화 완료');
  }
};

모듈 간 통신

모듈이 독립적이어야 하지만, 때로는 모듈 간 통신이 필요합니다.

// ✅ 방법 1: 이벤트 기반 통신
const eventBus = {
  events: {},

  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  },

  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
};

// 모듈 1: 이벤트 발생
const reactions = {
  async updateReactions() {
    const reactions = await this.fetchReactions(this.state.issueNumber);
    const counts = this.countReactions(reactions);

    // ✅ 이벤트 발생
    eventBus.emit('reactions:updated', counts);
  }
};

// 모듈 2: 이벤트 수신
const analytics = {
  init() {
    // ✅ reactions 업데이트를 감지하여 분석
    eventBus.on('reactions:updated', (counts) => {
      this.trackReactionCounts(counts);
    });
  },

  trackReactionCounts(counts) {
    utils.log('Analytics: Reaction counts updated', counts);
    // 분석 로직
  }
};
// ✅ 방법 2: 중재자 패턴 (Mediator Pattern)
const mediator = {
  modules: {},

  register(name, module) {
    this.modules[name] = module;
  },

  get(name) {
    return this.modules[name];
  },

  notify(sender, event, data) {
    utils.log(`Mediator: ${sender} -> ${event}`, data);

    // 이벤트에 따라 다른 모듈에 전달
    if (event === 'reactions:updated') {
      const analytics = this.get('analytics');
      analytics?.trackReactionCounts(data);
    }
  }
};

// 모듈 등록
mediator.register('reactions', reactions);
mediator.register('analytics', analytics);

// 모듈에서 사용
const reactions = {
  async updateReactions() {
    const counts = await this.fetchAndCountReactions();

    // ✅ 중재자를 통해 알림
    mediator.notify('reactions', 'reactions:updated', counts);
  }
};

실전: 새 기능 추가하기

모듈 분리 패턴의 장점은 새 기능 추가가 쉽다는 것입니다.

// ✅ 새로운 모듈: 코드 하이라이팅
const codeHighlighting = {
  highlightBlocks() {
    const codeBlocks = document.querySelectorAll('pre code');

    codeBlocks.forEach(block => {
      // 하이라이팅 로직
      hljs.highlightBlock(block);
    });

    utils.log('Code blocks highlighted:', codeBlocks.length);
  },

  init() {
    this.highlightBlocks();
  }
};

// ✅ 메인 초기화에 한 줄만 추가
const init = () => {
  responsiveTables.init();
  reactions.init();
  codeHighlighting.init();  // ✅ 새 모듈 추가
};

의존성 관리

// ✅ 모듈 간 의존성을 명시적으로 관리
const init = () => {
  // 1단계: 기본 기능 초기화
  responsiveTables.init();
  codeHighlighting.init();

  // 2단계: API 기반 기능 초기화 (네트워크 필요)
  reactions.init();

  // 3단계: 분석 기능 초기화 (다른 모듈이 준비된 후)
  analytics.init();
};

패턴 6: 시각적 구분 패턴

왜 필요할까요?

코드가 길어지면 어디서부터 어디까지가 한 섹션인지 파악하기 어렵습니다. 시각적 구분은 코드의 “목차” 역할을 합니다.

효과적인 주석 스타일

/* =========================================
   deep - Article Scripts
   ========================================= */

(function() {
  'use strict';

  // =========================================
  // 설정
  // =========================================
  const CONFIG = {
    DEBUG: false,
    REPO_OWNER: 'lledellebell',
    REPO_NAME: 'learn-cs',
    GITHUB_API_BASE: 'https://api.github.com',
    REACTION_UPDATE_INTERVAL: 30000,
    EMOJI_OPTIONS: {
      '+1': { emoji: '👍', label: '좋아요' },
      'heart': { emoji: '❤️', label: '사랑해요' },
      'laugh': { emoji: '😊', label: '웃겨요' },
      'hooray': { emoji: '🎉', label: '축하해요' },
      'rocket': { emoji: '🚀', label: '대단해요' },
      'eyes': { emoji: '👀', label: '눈여겨봐요' }
    }
  };

  // =========================================
  // 유틸리티 함수
  // =========================================
  const utils = {
    log: (...args) => {
      if (CONFIG.DEBUG) {
        console.log('[Article]', ...args);
      }
    },

    exists: (selector) => document.querySelector(selector) !== null
  };

  // =========================================
  // 테이블 반응형 처리
  // =========================================
  const responsiveTables = {
    addDataLabels() {
      // ...
    },

    init() {
      this.addDataLabels();
    }
  };

  // =========================================
  // GitHub 반응 통합
  // =========================================
  const reactions = {
    state: {
      issueNumber: null,
      issueUrl: null,
      updateInterval: null
    },

    // ... 메서드들 ...
  };

  // =========================================
  // 메인 초기화
  // =========================================
  const init = () => {
    if (CONFIG.DEBUG) {
      console.log('🚀 Article 스크립트 초기화 시작...');
    }

    responsiveTables.init();
    reactions.init();

    if (CONFIG.DEBUG) {
      console.log('✅ Article 스크립트 초기화 완료');
    }
  };

  // DOM 준비 완료 시 실행
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

시각적 구분의 효과

파일을 처음 열었을 때:

/* =========================================
   deep - Article Scripts
   ========================================= */

(function() {
  'use strict';

  // =========================================  ← 눈에 확 들어옴
  // 설정
  // =========================================
  const CONFIG = { ... };

  // =========================================  ← 새 섹션 시작
  // 유틸리티 함수
  // =========================================
  const utils = { ... };

  // =========================================  ← 또 다른 섹션
  // 테이블 반응형 처리
  // =========================================
  const responsiveTables = { ... };

  // =========================================  ← 주요 기능
  // GitHub 반응 통합
  // =========================================
  const reactions = { ... };

  // =========================================  ← 진입점
  // 메인 초기화
  // =========================================
  const init = () => { ... };
})();

스크롤 없이 파악 가능:

  • 설정 섹션
  • 유틸리티 섹션
  • 테이블 처리 섹션
  • GitHub 반응 섹션
  • 초기화 섹션

JSDoc과 함께 사용하기

// =========================================
// GitHub 반응 통합
// =========================================
const reactions = {
  state: {
    issueNumber: null,
    issueUrl: null,
    updateInterval: null
  },

  /**
   * GitHub Issues API에서 issue 번호 찾기
   * @returns {Promise<{number: number, url: string}|null>} Issue 정보 또는 null
   */
  async findIssueNumber() {
    const pathname = window.location.pathname;

    try {
      const searchQuery = encodeURIComponent(
        `repo:${CONFIG.REPO_OWNER}/${CONFIG.REPO_NAME} "${pathname}" in:title label:"💬 comment"`
      );
      const searchUrl = `${CONFIG.GITHUB_API_BASE}/search/issues?q=${searchQuery}`;

      const response = await fetch(searchUrl);
      if (!response.ok) {
        throw new Error('Issue 검색 실패');
      }

      const data = await response.json();

      if (data.items && data.items.length > 0) {
        const issue = data.items.find(item =>
          item.title === pathname || item.body?.includes(pathname)
        );

        if (issue) {
          return {
            number: issue.number,
            url: issue.html_url
          };
        }
      }

      return null;
    } catch (error) {
      console.error('Issue 찾기 오류:', error);
      return null;
    }
  },

  /**
   * GitHub API에서 reactions 가져오기
   * @param {number} issueNum - GitHub issue 번호
   * @returns {Promise<Array>} Reactions 배열
   */
  async fetchReactions(issueNum) {
    try {
      const url = `${CONFIG.GITHUB_API_BASE}/repos/${CONFIG.REPO_OWNER}/${CONFIG.REPO_NAME}/issues/${issueNum}/reactions`;
      const response = await fetch(url, {
        headers: {
          'Accept': 'application/vnd.github.squirrel-girl-preview+json'
        }
      });

      if (!response.ok) {
        throw new Error('Reactions 가져오기 실패');
      }

      const reactions = await response.json();
      return reactions;
    } catch (error) {
      console.error('Reactions 가져오기 오류:', error);
      return [];
    }
  }
};

패턴 7: 안전한 초기화 패턴

왜 필요할까요?

DOM이 준비되지 않은 상태에서 JavaScript를 실행하면 오류가 발생합니다.

// ❌ 문제 상황
document.querySelector('#myButton').addEventListener('click', () => {
  // ...
});

// TypeError: Cannot read property 'addEventListener' of null
// (DOM이 아직 로드되지 않았을 때)

Before: 재귀 호출 방식

// ❌ Before: 복잡한 재귀 패턴
async function init() {
  // DOM 준비 대기
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);  // ← 자기 자신 호출
    return;
  }

  const reactionsSection = document.getElementById('reactionsSection');
  if (!reactionsSection) {
    return;
  }

  // 초기화 로직
  // ...
}

init();  // ← 첫 호출

문제점:

  • init() 함수가 두 가지 역할을 함 (DOM 체크 + 초기화)
  • 재귀 호출로 인한 혼란
  • 함수 내부에서 return으로 조기 종료

After: 진입점 분리 방식

// ✅ After: 명확한 진입점
const init = () => {
  if (CONFIG.DEBUG) {
    console.log('🚀 Article 스크립트 초기화 시작...');
  }

  // 순서대로 초기화
  responsiveTables.init();
  reactions.init();

  if (CONFIG.DEBUG) {
    console.log('✅ Article 스크립트 초기화 완료');
  }
};

// ✅ 진입점: DOM 상태에 따라 실행 시점 결정
if (document.readyState === 'loading') {
  // DOM이 아직 로딩 중 → 이벤트 리스너 등록
  document.addEventListener('DOMContentLoaded', init);
} else {
  // DOM이 이미 준비됨 → 즉시 실행
  init();
}

장점:

  • init() 함수는 순수하게 초기화 로직만 담당
  • 진입점 코드가 DOM 상태를 체크
  • 더 읽기 쉬움

실전: 모듈별 안전한 초기화

const responsiveTables = {
  addDataLabels() {
    const tables = document.querySelectorAll('.article-body table');

    tables.forEach(table => {
      // ... 로직 ...
    });

    utils.log('Responsive tables initialized for', tables.length, 'tables');
  },

  init() {
    // ✅ 각 모듈의 init()은 요소 존재 여부만 체크
    const articleBody = document.querySelector('.article-body');
    if (!articleBody) {
      utils.log('Article body not found, skipping table initialization');
      return;
    }

    this.addDataLabels();
  }
};

const reactions = {
  state: { /* ... */ },

  // ... 메서드들 ...

  async init() {
    // ✅ 필수 요소 체크
    const reactionsSection = document.getElementById('reactionsSection');
    if (!reactionsSection) {
      utils.log('Reactions section not found, skipping initialization');
      return;
    }

    // 초기 로딩 상태
    reactionsSection.style.opacity = '0.5';

    // UI 요소 추가
    this.addExplanation();
    this.renderReactionButtons();

    // Issue 찾기 시도
    const issueInfo = await this.findIssueNumber();

    if (issueInfo) {
      this.state.issueNumber = issueInfo.number;
      this.state.issueUrl = issueInfo.url;

      // Reactions 가져오기 및 표시
      const reactions = await this.fetchReactions(issueInfo.number);
      const reactionCounts = this.countReactions(reactions);
      this.updateUI(reactionCounts);

      // 자동 업데이트 시작
      this.startAutoUpdate();
    } else {
      // 아직 issue가 없음
      this.updateUI({});

      // Issue가 생성될 수 있으니 계속 시도
      this.startAutoUpdate();
    }

    // 페이지 가시성 변경 감지
    document.addEventListener('visibilitychange', () => this.handleVisibilityChange());

    // 페이지 언로드 시 정리
    window.addEventListener('beforeunload', () => this.stopAutoUpdate());

    utils.log('Reactions initialized');
  }
};

// ✅ 메인 초기화: 각 모듈의 init() 호출
const init = () => {
  if (CONFIG.DEBUG) {
    console.log('🚀 Article 스크립트 초기화 시작...');
  }

  // 순서대로 초기화
  responsiveTables.init();
  reactions.init();

  if (CONFIG.DEBUG) {
    console.log('✅ Article 스크립트 초기화 완료');
  }
};

// ✅ 진입점
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}

더 안전한 초기화: Promise 기반

// ✅ Promise로 DOM 준비 대기
function waitForDOM() {
  return new Promise((resolve) => {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', resolve);
    } else {
      resolve();
    }
  });
}

// ✅ async/await으로 깔끔하게 초기화
async function main() {
  await waitForDOM();

  if (CONFIG.DEBUG) {
    console.log('🚀 Article 스크립트 초기화 시작...');
  }

  // 순서대로 초기화
  responsiveTables.init();
  await reactions.init(); // reactions.init()이 async이므로 await

  if (CONFIG.DEBUG) {
    console.log('✅ Article 스크립트 초기화 완료');
  }
}

main().catch(error => {
  console.error('초기화 중 오류 발생:', error);
});

특정 요소가 나타날 때까지 대기

// ✅ 특정 요소가 DOM에 추가될 때까지 대기 (MutationObserver 사용)
function waitForElement(selector, timeout = 5000) {
  return new Promise((resolve, reject) => {
    // 이미 존재하면 즉시 반환
    const element = document.querySelector(selector);
    if (element) {
      resolve(element);
      return;
    }

    // MutationObserver로 감지
    const observer = new MutationObserver((mutations) => {
      const element = document.querySelector(selector);
      if (element) {
        observer.disconnect();
        resolve(element);
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });

    // 타임아웃
    setTimeout(() => {
      observer.disconnect();
      reject(new Error(`Element ${selector} not found within ${timeout}ms`));
    }, timeout);
  });
}

// 사용 예
async function init() {
  try {
    // 특정 요소가 나타날 때까지 최대 5초 대기
    const reactionsSection = await waitForElement('#reactionsSection', 5000);

    // 요소를 찾았으면 초기화 진행
    reactions.init();
  } catch (error) {
    utils.warn('Reactions section not loaded:', error.message);
  }
}

실전 적용 가이드

1단계: 현재 코드 평가하기

리팩토링을 시작하기 전에 현재 코드의 문제점을 파악하세요:

체크리스트

## 설정 관리
- [ ] 설정값이 코드 곳곳에 흩어져 있나요?
- [ ] 환경별(개발/운영) 설정 관리가 어렵나요?
- [ ] 설정값을 변경하려면 여러 곳을 수정해야 하나요?

## 전역 네임스페이스
- [ ] 전역 변수가 3개 이상 있나요?
- [ ] 전역 함수가 5개 이상 있나요?
- [ ] 다른 스크립트와 이름 충돌 가능성이 있나요?

## 상태 관리
- [ ] 어느 함수가 어떤 변수를 변경하는지 추적하기 어렵나요?
- [ ] 상태 변경을 디버깅하기 어렵나요?
- [ ] 테스트하기 위해 상태를 초기화하기 복잡한가요?

## 코드 구조
- [ ] 파일이 200줄 이상인가요?
- [ ] 하나의 파일에 여러 기능이 섞여 있나요?
- [ ] 새 기능을 추가할 때 어디에 넣어야 할지 고민되나요?

## 가독성
- [ ] 코드 섹션이 명확하게 구분되지 않나요?
- [ ] 함수들이 모두 같은 레벨에 있나요?
- [ ] 주석이 부족하거나 오래된 주석이 있나요?

5개 이상 체크되면 리팩토링을 권장합니다.

2단계: 점진적 리팩토링 계획

// ✅ 권장: 점진적 접근 (한 번에 하나씩)

// Week 1: 설정 객체 패턴 적용
const CONFIG = {
  // 기존 상수들을 여기로 이동
};

// Week 2: 유틸리티 분리
const utils = {
  log: (...args) => { /* ... */ },
  exists: (selector) => { /* ... */ }
};

// Week 3: 첫 번째 기능 모듈화 (간단한 것부터)
const responsiveTables = {
  // 테이블 관련 함수들을 여기로 이동
};

// Week 4: 두 번째 기능 모듈화
const reactions = {
  state: { /* ... */ },
  // reactions 관련 함수들을 여기로 이동
};

// Week 5: 초기화 로직 정리
const init = () => {
  responsiveTables.init();
  reactions.init();
};

한 번에 모든 것을 바꾸려고 하지 마세요!

3단계: 테스트하면서 진행

// ✅ 각 단계마다 테스트

// 1. 설정 객체 적용 후
console.log('CONFIG:', CONFIG);
// ✅ 모든 설정값이 잘 들어있는지 확인

// 2. 유틸리티 분리 후
utils.log('Test log');
console.log('Element exists:', utils.exists('#myElement'));
// ✅ 유틸리티 함수들이 정상 동작하는지 확인

// 3. 모듈 분리 후
responsiveTables.init();
// ✅ 테이블 반응형 처리가 정상 동작하는지 확인

reactions.init();
// ✅ reactions 기능이 정상 동작하는지 확인

4단계: 문서화

// =========================================
// deep - Article Scripts
// =========================================
/**
 * 이 파일은 article 페이지의 다음 기능을 제공합니다.
 *
 * 1. 테이블 반응형 처리 (responsiveTables)
 *    - 테이블의 thead를 파싱하여 각 td에 data-label 속성 추가
 *    - 모바일에서 카드 형태로 표시되도록 지원
 *
 * 2. GitHub 반응 통합 (reactions)
 *    - GitHub Issues API를 통해 reactions 가져오기
 *    - 실시간 업데이트 (30초마다)
 *    - 사용자 클릭 시 GitHub issue로 이동
 *
 * @version 2.0.0
 * @date 2025-01-15
 */
(function() {
  'use strict';

  // ... 코드 ...
})();

5단계: 버전 관리

# ✅ Git으로 단계별 커밋

# 1단계 커밋
git add assets/js/article.js
git commit -m "refactor: 설정 객체 패턴 적용 (CONFIG)"

# 2단계 커밋
git commit -m "refactor: 유틸리티 함수 분리 (utils)"

# 3단계 커밋
git commit -m "refactor: 테이블 반응형 처리 모듈화 (responsiveTables)"

# 4단계 커밋
git commit -m "refactor: GitHub 반응 모듈화 (reactions)"

# 5단계 커밋
git commit -m "refactor: 초기화 로직 개선"

작은 단위로 커밋하면:

  • 문제가 생겼을 때 되돌리기 쉬움
  • 리뷰하기 쉬움
  • 변경 이력을 추적하기 쉬움

함정과 주의사항

함정 1: this 바인딩 문제

❌ 문제 상황

const reactions = {
  state: {
    issueNumber: null
  },

  async findIssueNumber() {
    this.state.issueNumber = 123; // ✅ 정상 동작
  },

  handleVisibilityChange() {
    if (document.hidden) {
      this.stopAutoUpdate(); // ✅ 정상 동작
    } else {
      this.updateReactions(); // ✅ 정상 동작
      this.startAutoUpdate(); // ✅ 정상 동작
    }
  },

  async init() {
    // ❌ 이벤트 리스너에서 this가 reactions를 가리키지 않음!
    document.addEventListener('visibilitychange', this.handleVisibilityChange);
    // this.handleVisibilityChange 실행 시 this는 document를 가리킴
  }
};

✅ 해결책 1: Arrow Function

const reactions = {
  state: { /* ... */ },

  handleVisibilityChange() {
    if (document.hidden) {
      this.stopAutoUpdate();
    } else {
      this.updateReactions();
      this.startAutoUpdate();
    }
  },

  async init() {
    // ✅ Arrow function으로 this 바인딩
    document.addEventListener('visibilitychange', () => this.handleVisibilityChange());

    // 또는
    window.addEventListener('beforeunload', () => this.stopAutoUpdate());
  }
};

✅ 해결책 2: bind() 사용

const reactions = {
  async init() {
    // ✅ bind()로 this 고정
    document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
    window.addEventListener('beforeunload', this.stopAutoUpdate.bind(this));
  }
};

권장: Arrow function이 더 간결하고 읽기 쉬움


함정 2: 메모리 누수

❌ 문제 상황

const reactions = {
  state: {
    updateInterval: null
  },

  startAutoUpdate() {
    // ❌ 기존 interval을 제거하지 않고 새로 생성
    this.state.updateInterval = setInterval(
      async () => await this.updateReactions(),
      CONFIG.REACTION_UPDATE_INTERVAL
    );
  },

  async init() {
    this.startAutoUpdate();

    // 페이지 가시성 변경 시
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        // interval 정리 안 함 ❌
      } else {
        this.startAutoUpdate(); // ❌ 새 interval 생성 (이전 것은 계속 실행 중)
      }
    });
  }
};

결과:

  • 탭을 여러 번 전환하면 interval이 계속 쌓임
  • 메모리 누수 발생
  • 불필요한 API 호출 증가

✅ 해결책: 정리(cleanup) 로직 추가

const reactions = {
  state: {
    updateInterval: null
  },

  startAutoUpdate() {
    // ✅ 1. 기존 interval이 있으면 먼저 제거
    if (this.state.updateInterval) {
      clearInterval(this.state.updateInterval);
      this.state.updateInterval = null;
    }

    // ✅ 2. 새 interval 설정
    this.state.updateInterval = setInterval(
      async () => await this.updateReactions(),
      CONFIG.REACTION_UPDATE_INTERVAL
    );

    utils.log('Auto-update started:', CONFIG.REACTION_UPDATE_INTERVAL / 1000, 'seconds');
  },

  stopAutoUpdate() {
    // ✅ interval 정리
    if (this.state.updateInterval) {
      clearInterval(this.state.updateInterval);
      this.state.updateInterval = null;
      utils.log('Auto-update stopped');
    }
  },

  handleVisibilityChange() {
    if (document.hidden) {
      // ✅ 페이지가 숨겨지면 업데이트 중지
      this.stopAutoUpdate();
    } else {
      // ✅ 페이지가 보이면 업데이트 재개
      this.updateReactions();
      this.startAutoUpdate();
    }
  },

  async init() {
    // ...

    // 페이지 가시성 변경 감지
    document.addEventListener('visibilitychange', () => this.handleVisibilityChange());

    // ✅ 페이지 언로드 시 정리
    window.addEventListener('beforeunload', () => this.stopAutoUpdate());

    utils.log('Reactions initialized');
  }
};

함정 3: 모듈 간 순환 참조

❌ 문제 상황

// ❌ 모듈 A가 모듈 B를 참조
const moduleA = {
  doSomething() {
    moduleB.doOtherThing(); // B 참조
  }
};

// ❌ 모듈 B가 모듈 A를 참조
const moduleB = {
  doOtherThing() {
    moduleA.doSomething(); // A 참조 → 순환!
  }
};

✅ 해결책 1: 이벤트 기반 통신

// ✅ 이벤트 버스로 분리
const eventBus = {
  events: {},

  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  },

  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
};

// 모듈 A: 이벤트 발생
const moduleA = {
  doSomething() {
    // ✅ 직접 참조 대신 이벤트 발생
    eventBus.emit('something-done', { data: 'example' });
  }
};

// 모듈 B: 이벤트 수신
const moduleB = {
  init() {
    // ✅ 이벤트 리스너 등록
    eventBus.on('something-done', (data) => {
      this.doOtherThing(data);
    });
  },

  doOtherThing(data) {
    console.log('Received:', data);
  }
};

✅ 해결책 2: 공통 로직 추출

// ✅ 공통 로직을 별도 모듈로 추출
const sharedLogic = {
  processData(data) {
    // 공통 처리 로직
    return data.toUpperCase();
  }
};

// 모듈 A
const moduleA = {
  doSomething(data) {
    const processed = sharedLogic.processData(data);
    console.log('A:', processed);
  }
};

// 모듈 B
const moduleB = {
  doOtherThing(data) {
    const processed = sharedLogic.processData(data);
    console.log('B:', processed);
  }
};

함정 4: 과도한 모듈화

❌ 문제 상황

// ❌ 너무 세분화된 모듈
const buttonUtils = {
  createButton() { /* ... */ }
};

const iconUtils = {
  createIcon() { /* ... */ }
};

const textUtils = {
  createText() { /* ... */ }
};

const containerUtils = {
  createContainer() { /* ... */ }
};

// ❌ 사용할 때 너무 복잡
const button = buttonUtils.createButton();
const icon = iconUtils.createIcon();
const text = textUtils.createText();
const container = containerUtils.createContainer();

✅ 해결책: 적절한 수준의 그룹화

// ✅ 관련 기능을 적절히 그룹화
const domUtils = {
  createButton(text, onClick) {
    const button = document.createElement('button');
    button.textContent = text;
    button.addEventListener('click', onClick);
    return button;
  },

  createIcon(type) {
    const icon = document.createElement('span');
    icon.className = `icon icon-${type}`;
    return icon;
  },

  createContainer(className) {
    const container = document.createElement('div');
    container.className = className;
    return container;
  }
};

// ✅ 사용하기 쉬움
const button = domUtils.createButton('Click me', handleClick);
const icon = domUtils.createIcon('star');

가이드라인:

  • 모듈은 하나의 명확한 책임을 가져야 함
  • 너무 작으면 관리 포인트만 늘어남
  • 5-10개의 메서드를 가진 모듈이 적당함

함정 5: 전역 상태 직접 수정

❌ 문제 상황

const reactions = {
  state: {
    issueNumber: null,
    issueUrl: null
  },

  async findIssueNumber() {
    // ...
    // ❌ state를 직접 수정 (여러 곳에서)
    this.state.issueNumber = issueInfo.number;
    this.state.issueUrl = issueInfo.url;
  },

  async updateReactions() {
    if (!this.state.issueNumber) {
      const issueInfo = await this.findIssueNumber();
      if (issueInfo) {
        // ❌ 또 다른 곳에서 직접 수정
        this.state.issueNumber = issueInfo.number;
        this.state.issueUrl = issueInfo.url;
      }
    }
    // ...
  }
};

✅ 해결책: Setter 메서드 사용

const reactions = {
  state: {
    issueNumber: null,
    issueUrl: null,
    updateInterval: null
  },

  // ✅ Setter 메서드로 상태 변경을 한 곳으로 통일
  setIssue(number, url) {
    if (CONFIG.DEBUG) {
      utils.log('Setting issue:', { number, url });
    }

    this.state.issueNumber = number;
    this.state.issueUrl = url;
  },

  clearIssue() {
    if (CONFIG.DEBUG) {
      utils.log('Clearing issue');
    }

    this.state.issueNumber = null;
    this.state.issueUrl = null;
  },

  async findIssueNumber() {
    // ...

    if (issue) {
      // ✅ setter 사용
      this.setIssue(issue.number, issue.html_url);
      return {
        number: issue.number,
        url: issue.html_url
      };
    }

    return null;
  },

  async updateReactions() {
    if (!this.state.issueNumber) {
      const issueInfo = await this.findIssueNumber();
      // ✅ findIssueNumber()가 이미 setIssue()를 호출함
    }
    // ...
  }
};

장점:

  • 상태 변경을 추적하기 쉬움
  • 검증 로직을 한 곳에 모을 수 있음
  • 디버깅 로그를 추가하기 쉬움

함정 6: 비동기 함수 에러 처리 누락

❌ 문제 상황

const reactions = {
  async fetchReactions(issueNum) {
    // ❌ try-catch 없음
    const url = `${CONFIG.GITHUB_API_BASE}/repos/${CONFIG.REPO_OWNER}/${CONFIG.REPO_NAME}/issues/${issueNum}/reactions`;
    const response = await fetch(url, {
      headers: {
        'Accept': 'application/vnd.github.squirrel-girl-preview+json'
      }
    });

    const reactions = await response.json();
    return reactions;

    // 네트워크 오류 시 전체 스크립트 중단!
  }
};

✅ 해결책: 적절한 에러 처리

const reactions = {
  async fetchReactions(issueNum) {
    try {
      const url = `${CONFIG.GITHUB_API_BASE}/repos/${CONFIG.REPO_OWNER}/${CONFIG.REPO_NAME}/issues/${issueNum}/reactions`;
      const response = await fetch(url, {
        headers: {
          'Accept': 'application/vnd.github.squirrel-girl-preview+json'
        }
      });

      // ✅ HTTP 에러 체크
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const reactions = await response.json();
      return reactions;
    } catch (error) {
      // ✅ 에러를 로깅하고 빈 배열 반환 (기능 계속 동작)
      console.error('Reactions 가져오기 오류:', error);
      utils.error('Failed to fetch reactions:', error.message);

      // ✅ fallback 값 반환
      return [];
    }
  },

  async updateReactions() {
    if (!this.state.issueNumber) {
      const issueInfo = await this.findIssueNumber();
      if (issueInfo) {
        this.state.issueNumber = issueInfo.number;
        this.state.issueUrl = issueInfo.url;
      } else {
        // ✅ issue가 없어도 계속 진행
        return;
      }
    }

    // ✅ fetchReactions가 실패해도 빈 배열 반환하므로 안전
    const reactions = await this.fetchReactions(this.state.issueNumber);
    const reactionCounts = this.countReactions(reactions);
    this.updateUI(reactionCounts);
  }
};

성능과 번들 사이즈

코드가 “무겁다”는 것을 어떻게 판단할까요?

코드의 무게는 다음 기준으로 측정합니다.

1. 번들 크기 (가장 중요)

실제 사용자가 다운로드하는 크기는 Gzipped 크기입니다.

실용적 기준:
✅ Gzipped 10KB 미만    → 걱정 안 해도 됨 (매우 가벼움)
⚠️ Gzipped 10-50KB     → 일반적인 라이브러리 수준
❌ Gzipped 50KB 이상    → 최적화 고려 필요

이 프로젝트: Gzipped 2.8KB → 매우 가벼움 ✅
참고: jQuery는 약 30KB (gzipped)

2. 파싱 & 실행 시간

JavaScript 엔진이 코드를 해석하고 실행하는 시간:

실용적 기준:
✅ 10ms 미만    → 체감 불가능
⚠️ 10-50ms     → 일반적인 수준
❌ 50ms 이상    → 사용자가 느낄 수 있음

이 프로젝트: 3.5ms → 체감 불가능 ✅

3. 메모리 사용량

런타임에 차지하는 메모리:

이 프로젝트의 메모리 사용:
- 객체 3개 (CONFIG, utils, reactions, responsiveTables)
- 상태 변수 몇 개
- setInterval 1개

→ 무시할 수준 (1KB 미만) ✅

언제 최적화가 필요한가?

최적화가 필요한 경우:
❌ 모바일에서 페이지 로드가 3초 이상 걸림
❌ Lighthouse 성능 점수가 50점 미만
❌ JavaScript 번들이 100KB (gzipped) 이상
❌ 사용자가 "느리다"고 느낌

최적화가 불필요한 경우:
✅ 페이지 로드 1초 미만
✅ 사용자 체감 성능 좋음
✅ 번들 크기가 작음 (이 프로젝트처럼)

→ 이 프로젝트는 최적화 불필요

실전: 직접 측정하는 방법

// 1. 번들 크기 확인
// 파일을 gzip으로 압축하여 실제 크기 확인
// Linux/Mac: gzip -c article.js | wc -c
// 또는 브라우저 개발자 도구 Network 탭에서 확인

// 2. 초기화 시간 측정
const init = () => {
  const start = performance.now();

  responsiveTables.init();
  reactions.init();

  const end = performance.now();
  console.log(`초기화 시간: ${(end - start).toFixed(2)}ms`);
};

// 3. Chrome DevTools Lighthouse 실행
// - 개발자 도구 열기 (F12)
// - Lighthouse 탭
// - "Generate report" 클릭
// - Performance 점수 확인

// 4. 메모리 사용량 확인
// - Chrome DevTools > Performance > Memory
// - 프로파일링 시작
// - 페이지 로드 후 스냅샷 확인

“코드가 늘어나면 나빠지는 거 아닌가요?”

리팩토링 후 파일 크기가 늘어나는 것을 보고 걱정할 수 있습니다. 하지만 좋은 코드의 목표는 짧은 줄이 아니라 명확성입니다. 때로는 명확성을 위해 더 많은 줄이 필요할 수 있습니다.

리팩토링으로 얻은 것

품질 지표:
- ✅ 전역 변수: 3개 → 0개
- ✅ 전역 함수: 10개 → 0개 (모듈화)
- ✅ 명확한 구조: 없음 → 3개 모듈
- ✅ 에러 처리: 부분적 → 포괄적
- ✅ 상태 관리: 분산됨 → 캡슐화
- ✅ 확장성: 어려움 → 쉬움

파일 크기:
- 파일 크기: 8.2 KB → 12.5 KB
- Gzipped: 2.1 KB → 2.8 KB (+0.7KB)

왜 코드가 늘어났을까요?

명확한 코드를 작성하면 때로 줄이 늘어날 수 있습니다.

// ❌ Before: 짧지만 이해하기 어려운 코드
let n = null;
async function f() { /* ... */ }

// ✅ After: 명확하고 구조화된 코드
const reactions = {
  state: {
    issueNumber: null  // GitHub issue 번호
  },

  /**
   * GitHub Issues API에서 issue 번호 찾기
   * @returns {Promise<{number: number, url: string}|null>}
   */
  async findIssueNumber() {
    try {
      // ... 명확한 에러 처리
    } catch (error) {
      console.error('Issue 찾기 오류:', error);
      return null;
    }
  }
};

늘어난 이유:

  1. 주석과 JSDoc - 코드의 의도를 설명 (복잡한 로직에만)
  2. 에러 처리 - try-catch 블록 추가 (안정성 향상)
  3. 명확한 구조 - 시각적 구분 주석 (큰 파일에만)
  4. 가독성 - 공백과 포맷팅 (읽기 쉬운 코드)
  5. 타입 힌트 - JSDoc으로 타입 정보 제공

중요: 무조건 많은 주석이 좋은 것은 아닙니다. 코드 자체가 명확하면 주석은 최소화하는 것이 좋습니다.

성능 영향

// ✅ 성능 측정
const init = () => {
  const startTime = performance.now();

  if (CONFIG.DEBUG) {
    console.log('🚀 Article 스크립트 초기화 시작...');
  }

  responsiveTables.init();
  reactions.init();

  const endTime = performance.now();

  if (CONFIG.DEBUG) {
    console.log('✅ Article 스크립트 초기화 완료');
    console.log(`⏱️ 초기화 시간: ${(endTime - startTime).toFixed(2)}ms`);
  }
};

// 결과:
// Before: 평균 3.2ms
// After: 평균 3.5ms (+0.3ms, 무시할 수준)

정말 가치 있는 투자인가요?

0.7KB의 추가 용량으로 얻은 것들을 돌아보면:

6개월 후 이 코드를 다시 열었을 때:

  • ❌ Before: “이 변수가 어디서 변경되지…?” (30분 소요)
  • ✅ After: “아, reactions.state에 있구나!” (1분 소요)

새 기능을 추가할 때:

  • ❌ Before: 전역 함수 추가하고 다른 코드 깨지지 않기를 기도
  • ✅ After: 새 모듈 추가하고 init()에 한 줄만 추가

팀원이 이 코드를 처음 볼 때:

  • ❌ Before: “이 코드 누가 짰어요…?” 😱
  • ✅ After: “구조가 깔끔하네요!” 😊

결론: Gzipped 0.7KB는 이미지 하나보다 작습니다. 하지만 개발자 경험은 비교할 수 없이 개선되었습니다.

코드 길이의 균형

좋은 코드는 “짧은 코드”도 “긴 코드”도 아닙니다. 명확하면서도 간결한 코드입니다.

// ❌ 과도하게 압축된 코드 (나쁨)
const r={s:{n:null},f:async()=>{/*...*/}};

// ❌ 과도하게 장황한 코드 (나쁨)
// =========================================
// Reactions 모듈
// =========================================
const reactions = {
  // =========================================
  // State 관리 영역
  // =========================================
  state: {
    // GitHub issue의 번호를 저장하는 변수입니다.
    // 이 변수는 null로 초기화되며, findIssueNumber() 메서드에서 설정됩니다.
    issueNumber: null
  }
};

// ✅ 명확하면서도 간결한 코드 (좋음)
const reactions = {
  state: {
    issueNumber: null
  },

  async findIssueNumber() {
    // 복잡한 로직만 주석으로 설명
  }
};

가이드라인:

  • 코드 자체가 설명이 되도록 작성 (명확한 변수명, 함수명)
  • 복잡한 로직에만 주석 추가
  • 불필요한 주석은 제거
  • 구조화와 압축 사이의 균형 찾기

번들 크기 최적화 팁

// ✅ 1. 프로덕션에서는 DEBUG를 false로
const CONFIG = {
  DEBUG: false,  // ← 프로덕션 빌드 시 false
  // ...
};

// ✅ 2. Minifier가 제거할 수 있도록 코드 작성
if (CONFIG.DEBUG) {
  console.log('디버그 로그');
  // 이 블록은 DEBUG=false일 때 minifier가 제거함
}

// ✅ 3. 사용하지 않는 유틸리티 제거
const utils = {
  log: (...args) => {
    if (CONFIG.DEBUG) {
      console.log('[Article]', ...args);
    }
  },

  // ❌ 사용하지 않는 메서드는 제거
  // unusedMethod() { /* ... */ }
};

참고 자료

관련 문서

실제 프로젝트

  • Before (reactions.js): /assets/js/reactions.js (삭제됨)
  • After (article.js): /assets/js/article.js (현재 파일)

디자인 패턴

  1. Module Pattern: 전역 네임스페이스 오염 방지
  2. Revealing Module Pattern: 공개 API 명시
  3. Observer Pattern: 이벤트 기반 통신 (eventBus)
  4. Mediator Pattern: 모듈 간 통신 중재

추천 도구

  • ESLint: JavaScript 코드 품질 체크
  • Prettier: 코드 포맷팅
  • JSDoc: 타입 힌트와 문서 생성
  • TypeScript: 타입 안전성 (선택적)

마무리

핵심 요약

이 가이드에서 다룬 7가지 리팩토링 패턴:

  1. 설정 객체 패턴: 모든 설정을 CONFIG로 통합
  2. 네임스페이스 패턴: 관련 함수를 객체로 그룹화
  3. 상태 캡슐화 패턴: 전역 변수를 state 프로퍼티로
  4. 유틸리티 객체 패턴: 공통 로직을 utils
  5. 모듈 분리 패턴: 기능별 독립 모듈 생성
  6. 시각적 구분 패턴: 주석으로 섹션 구분
  7. 안전한 초기화 패턴: DOM 준비 상태 확인

실전 체크리스트

리팩토링을 마쳤다면 다음을 확인하세요:

## 코드 품질
- [x] 전역 변수가 없거나 최소화되었나요?
- [x] 각 모듈이 단일 책임을 가지나요?
- [x] 함수/메서드 이름이 명확한가요?
- [x] 주석이 적절히 추가되었나요?

## 유지보수성
- [x] 새 기능을 추가하기 쉬운가요?
- [x] 코드를 처음 보는 사람도 이해하기 쉬운가요?
- [x] 설정값을 쉽게 변경할 수 있나요?
- [x] 디버깅이 쉬운가요?

## 성능
- [x] 메모리 누수가 없나요? (interval, 이벤트 리스너 정리)
- [x] 불필요한 DOM 조작이 없나요?
- [x] API 호출이 효율적인가요?

## 테스트
- [x] 각 기능이 독립적으로 동작하나요?
- [x] 모든 브라우저에서 테스트했나요?
- [x] 에러 처리가 적절한가요?

다음 단계

  1. TypeScript로 변환 (선택적)
    • 타입 안전성 확보
    • IDE 자동완성 개선
    • 리팩토링이 더 안전해짐
  2. 테스트 추가
    • Jest, Vitest 등으로 단위 테스트
    • 각 모듈을 독립적으로 테스트
  3. 번들러 도입 (프로젝트 규모가 커지면)
    • Webpack, Rollup, Vite 등
    • ES Modules로 파일 분리
  4. CI/CD 통합
    • ESLint 자동 체크
    • 코드 리뷰 자동화

마지막으로

저의 실수에서 배운 교훈

처음 reactions.js를 작성할 때는 이렇게 생각했습니다.

“일단 동작하게 만들자. 나중에 정리하면 되지 뭐.”

하지만 6개월 후, 테이블 반응형 기능을 추가하려고 그 파일을 다시 열었을 때…

제가 작성한 코드인데도 이해하기 어려웠습니다. 😅

  • “이 전역 변수는 어디서 쓰이지?”
  • “이 함수를 수정하면 다른 곳이 깨지지 않을까?”
  • “새 기능을 어디에 추가해야 하지?”

그때 깨달았습니다.

“미래의 나는 남이다.”

리팩토링은 여정입니다

이 가이드에서 소개한 7가지 패턴을 한 번에 모두 적용할 필요는 없습니다.

오늘은 설정 객체만 만들어도 충분합니다. 다음 주에는 한 가지 기능을 모듈로 분리해보세요. 그렇게 조금씩 개선하다 보면, 어느새 훨씬 나은 코드베이스를 갖게 될 것입니다.

리팩토링은 한 번에 끝나는 작업이 아니라, 지속적인 개선 과정입니다.

이 가이드가 여러분의 JavaScript 코드를 더 깨끗하고, 유지보수하기 쉽게 만드는 데 도움이 되길 바랍니다.

Happy Refactoring! 🚀

댓글