article.js 리팩토링 가이드 - 유지보수 가능한 JavaScript 구조
“코드 리팩토링은 단순히 코드를 정리하는 것이 아니라, 미래의 나와 팀원들에게 보내는 친절한 편지입니다.”
왜 이 가이드를 읽어야 할까요?
이런 순간을 경험해보신 적 있나요?
오후 3시, 새 기능 추가 작업:
// reactions.js (6개월 전에 작성한 파일)
let issueNumber = null; // 🤔 이게 어디서 쓰이더라?
let updateInterval = null; // 🤔 이건 뭐였지?
function init() {
// ... 200줄의 코드 ...
// 🤔 테이블 기능을 여기에 추가해야 하나?
// 🤔 아니면 새 파일을 만들어야 하나?
// 🤔 이 함수 수정하면 다른 곳이 깨지지 않을까?
}
30분이 지났지만, 아직도 “어디에 코드를 추가해야 할지” 결정하지 못했습니다.
저도 똑같은 경험을 했습니다
제 블로그에 테이블 반응형 처리를 추가하려다가 이런 상황을 마주했습니다.
- 파일명은
reactions.js인데, 테이블 기능을 추가해야 함 - 전역 변수 3개가 어디서 변경되는지 추적이 안 됨
- 10개의 전역 함수가 모두 같은 레벨에 나열되어 있음
- “이 함수를 수정하면 안전할까?” 하는 불안감
그때 깨달았습니다.
“내가 작성한 코드인데, 왜 이렇게 무섭지?”
이 가이드로 얻을 수 있는 것
리팩토링 후, 제 코드는 이렇게 바뀌었습니다.
Before:
// 이 변수가 어디서 변경되는지 찾으려면 전체 파일을 읽어야 함
let issueNumber = null;
// 이 함수가 무엇을 하는지 이름만으로는 알 수 없음
async function f() { /* ... */ }
After):
// 명확한 구조
const reactions = {
state: {
issueNumber: null // ← 상태는 여기서만 관리
},
async findIssueNumber() { // ← 이름만 봐도 무엇을 하는지 명확
// ...
}
};
이 가이드는 실제 프로젝트에서 적용한 7가지 리팩토링 패턴을 다룹니다.
- ✅ 설정 객체 패턴 - 흩어진 설정을 한 곳으로
- ✅ 네임스페이스 패턴 - 전역 오염 방지
- ✅ 상태 캡슐화 - 예측 가능한 상태 관리
- ✅ 유틸리티 객체 - 공통 로직 재사용
- ✅ 모듈 분리 - 기능별 독립성 확보
- ✅ 시각적 구분 - 코드의 ‘목차’ 만들기
- ✅ 안전한 초기화 - DOM 준비 상태 체크
이 패턴들은 어떤 JavaScript 프로젝트에도 적용할 수 있습니다.
리팩토링 후, 저는:
- 새 기능 추가 시간이 절반으로 단축되었습니다
- 버그 수정 시 영향 범위를 즉시 파악할 수 있게 되었습니다
- 코드를 다시 열었을 때 불안감이 사라졌습니다
여러분도 같은 결과를 얻을 수 있습니다.
목차
리팩토링 전후 비교
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;
}
}
};
늘어난 이유:
- 주석과 JSDoc - 코드의 의도를 설명 (복잡한 로직에만)
- 에러 처리 - try-catch 블록 추가 (안정성 향상)
- 명확한 구조 - 시각적 구분 주석 (큰 파일에만)
- 가독성 - 공백과 포맷팅 (읽기 쉬운 코드)
- 타입 힌트 - 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(현재 파일)
디자인 패턴
- Module Pattern: 전역 네임스페이스 오염 방지
- Revealing Module Pattern: 공개 API 명시
- Observer Pattern: 이벤트 기반 통신 (eventBus)
- Mediator Pattern: 모듈 간 통신 중재
추천 도구
- ESLint: JavaScript 코드 품질 체크
- Prettier: 코드 포맷팅
- JSDoc: 타입 힌트와 문서 생성
- TypeScript: 타입 안전성 (선택적)
마무리
핵심 요약
이 가이드에서 다룬 7가지 리팩토링 패턴:
- 설정 객체 패턴: 모든 설정을
CONFIG로 통합 - 네임스페이스 패턴: 관련 함수를 객체로 그룹화
- 상태 캡슐화 패턴: 전역 변수를
state프로퍼티로 - 유틸리티 객체 패턴: 공통 로직을
utils로 - 모듈 분리 패턴: 기능별 독립 모듈 생성
- 시각적 구분 패턴: 주석으로 섹션 구분
- 안전한 초기화 패턴: DOM 준비 상태 확인
실전 체크리스트
리팩토링을 마쳤다면 다음을 확인하세요:
## 코드 품질
- [x] 전역 변수가 없거나 최소화되었나요?
- [x] 각 모듈이 단일 책임을 가지나요?
- [x] 함수/메서드 이름이 명확한가요?
- [x] 주석이 적절히 추가되었나요?
## 유지보수성
- [x] 새 기능을 추가하기 쉬운가요?
- [x] 코드를 처음 보는 사람도 이해하기 쉬운가요?
- [x] 설정값을 쉽게 변경할 수 있나요?
- [x] 디버깅이 쉬운가요?
## 성능
- [x] 메모리 누수가 없나요? (interval, 이벤트 리스너 정리)
- [x] 불필요한 DOM 조작이 없나요?
- [x] API 호출이 효율적인가요?
## 테스트
- [x] 각 기능이 독립적으로 동작하나요?
- [x] 모든 브라우저에서 테스트했나요?
- [x] 에러 처리가 적절한가요?
다음 단계
- TypeScript로 변환 (선택적)
- 타입 안전성 확보
- IDE 자동완성 개선
- 리팩토링이 더 안전해짐
- 테스트 추가
- Jest, Vitest 등으로 단위 테스트
- 각 모듈을 독립적으로 테스트
- 번들러 도입 (프로젝트 규모가 커지면)
- Webpack, Rollup, Vite 등
- ES Modules로 파일 분리
- CI/CD 통합
- ESLint 자동 체크
- 코드 리뷰 자동화
마지막으로
저의 실수에서 배운 교훈
처음 reactions.js를 작성할 때는 이렇게 생각했습니다.
“일단 동작하게 만들자. 나중에 정리하면 되지 뭐.”
하지만 6개월 후, 테이블 반응형 기능을 추가하려고 그 파일을 다시 열었을 때…
제가 작성한 코드인데도 이해하기 어려웠습니다. 😅
- “이 전역 변수는 어디서 쓰이지?”
- “이 함수를 수정하면 다른 곳이 깨지지 않을까?”
- “새 기능을 어디에 추가해야 하지?”
그때 깨달았습니다.
“미래의 나는 남이다.”
리팩토링은 여정입니다
이 가이드에서 소개한 7가지 패턴을 한 번에 모두 적용할 필요는 없습니다.
오늘은 설정 객체만 만들어도 충분합니다. 다음 주에는 한 가지 기능을 모듈로 분리해보세요. 그렇게 조금씩 개선하다 보면, 어느새 훨씬 나은 코드베이스를 갖게 될 것입니다.
리팩토링은 한 번에 끝나는 작업이 아니라, 지속적인 개선 과정입니다.
이 가이드가 여러분의 JavaScript 코드를 더 깨끗하고, 유지보수하기 쉽게 만드는 데 도움이 되길 바랍니다.
Happy Refactoring! 🚀
댓글