JavaScript 함수 선언 방식 비교: function vs 화살표 함수
“화살표 함수를 쓰면 뭐가 좋은가요?”
코드 리뷰에서 이런 피드백을 받았습니다.
// 내가 작성한 코드
function handleClick() {
console.log('Clicked');
}
// 리뷰어: "화살표 함수로 바꿔주세요"
const handleClick = () => {
console.log('Clicked');
};
“둘 다 작동하는데 왜 바꿔야 하죠?”
이 글에서는 function 선언과 화살표 함수의 실질적인 차이와 언제 어떤 걸 써야 하는지를 알아봅니다.
패턴 1: 전통적 방식
기본 예제: 간단한 카운터
전통적 방식은 JavaScript 초기부터 사용되어 온 가장 기본적인 코드 작성 방법입니다. 변수와 함수를 그대로 선언하며, 별도의 스코프 관리 없이 직관적으로 작성할 수 있습니다.
// 전역 변수
var count = 0;
// 함수 선언
function increment() {
count++;
updateDisplay();
}
function decrement() {
count--;
updateDisplay();
}
function updateDisplay() {
document.getElementById('counter').textContent = count;
}
function reset() {
count = 0;
updateDisplay();
}
// 초기화
updateDisplay();
// 이벤트 리스너
document.getElementById('btn-inc').addEventListener('click', increment);
document.getElementById('btn-dec').addEventListener('click', decrement);
document.getElementById('btn-reset').addEventListener('click', reset);
코드 흐름:
count변수를 전역에 선언하여 모든 함수에서 접근 가능- 각 기능을 개별 함수로 분리 (
increment,decrement,updateDisplay,reset) - 페이지 로드 시
updateDisplay()로 초기값 표시 - DOM 요소에 이벤트 리스너를 직접 연결
특징
✅ 장점:
-
이해하기 쉬움: 순차적으로 읽히며 초보자에게 직관적입니다. “변수 선언 → 함수 정의 → 실행” 순서가 명확합니다.
- 호이스팅: 함수 선언이 끌어올려져 어디서든 호출 가능합니다.
// 함수 선언 전에 호출해도 작동 greet(); // "Hello!" 출력 function greet() { console.log('Hello!'); } - 디버깅 용이: 함수명이 스택 트레이스에 명확히 표시되어 에러 추적이 쉽습니다.
// 에러 발생 시 스택 트레이스: // at increment (script.js:4) // at HTMLButtonElement.<anonymous> (script.js:20) - 브라우저 호환성: 구형 브라우저(IE6+)에서도 작동하며, 트랜스파일러 없이 바로 사용 가능합니다.
❌ 단점:
- 전역 스코프 오염:
count,increment등이 전역 변수로 노출됩니다.console.log(window.count); // 0 (전역 객체에 접근 가능) count = 999; // 외부에서 직접 수정 가능 (위험!) - 이름 충돌 위험: 다른 라이브러리나 스크립트와 변수명이 겹칠 수 있습니다.
// 내 코드 var data = [1, 2, 3]; // 다른 라이브러리가 로드되면서 var data = [4, 5, 6]; // 덮어씌워짐! console.log(data); // [4, 5, 6] (예상과 다름) - 캡슐화 부족: 내부 상태를 외부에서 직접 수정할 수 있어 예상치 못한 버그 발생 가능합니다.
// 개발자 콘솔에서 누구나 수정 가능 count = -100; // 음수로 변경 count = "문자열"; // 타입까지 변경 가능 - 테스트 어려움: 전역 상태에 의존하여 격리된 단위 테스트가 어렵습니다.
// 테스트 1이 count를 50으로 변경 // 테스트 2가 실행될 때 count가 0이 아닌 50에서 시작 // → 테스트 간 의존성 발생
실제 문제 사례
실제 프로젝트에서 자주 발생하는 문제를 살펴봅시다.
// counter.js (우리 팀이 작성)
var count = 0;
function increment() {
count++;
console.log('Counter:', count);
}
// analytics.js (다른 팀이 작성한 분석 도구)
var count = 0; // ⚠️ 충돌! 같은 이름 사용
function increment() {
count += 5; // ⚠️ 우리 함수를 덮어씌움!
sendAnalytics(count);
}
// 사용자가 버튼 클릭
document.getElementById('btn').addEventListener('click', increment);
// 결과: 예상치 못한 동작
// - counter.js의 increment()가 호출될 줄 알았지만
// - analytics.js의 increment()가 호출됨 (나중에 로드된 것이 덮어씀)
// - 카운터가 1씩이 아니라 5씩 증가!
문제점:
- 두 팀이 같은 변수명
count를 사용 - 나중에 로드된
analytics.js의 함수가 우리 함수를 덮어씀 - 디버깅하기 전까지 문제를 발견하기 어려움
- 코드베이스가 커질수록 이런 충돌 확률 증가
이런 문제를 해결하기 위해 모듈 패턴과 스코프 격리가 필요합니다.
패턴 2: 현대적 방식 (IIFE + 화살표 함수)
기본 예제: 같은 카운터를 현대적으로
현대적 방식은 ES6+ 기능과 모듈 패턴을 활용하여 스코프 격리, 코드 구조화, 유지보수성을 개선합니다.
핵심 개념:
- IIFE (Immediately Invoked Function Expression): 즉시 실행 함수로 독립적인 스코프 생성
- 화살표 함수: 간결한 문법과 렉시컬 this 바인딩
- const/let: 블록 스코프 변수 선언
- 모듈 패턴: 상수, 상태, 함수를 논리적으로 구분
/**
* 카운터 모듈
* IIFE와 화살표 함수를 사용한 모듈 패턴
*/
(() => {
'use strict';
// ==================== 상수 정의 ====================
const SELECTORS = {
counter: '#counter',
btnInc: '#btn-inc',
btnDec: '#btn-dec',
btnReset: '#btn-reset',
};
const CONFIG = {
initialValue: 0,
min: 0,
max: 100,
};
// ==================== 내부 상태 ====================
let count = CONFIG.initialValue;
// ==================== 유틸리티 함수 ====================
/**
* 카운트 값 검증
*/
const clamp = (value) =>
Math.max(CONFIG.min, Math.min(CONFIG.max, value));
// ==================== DOM 업데이트 ====================
/**
* 화면에 카운트 표시
*/
const updateDisplay = () => {
const counterEl = document.querySelector(SELECTORS.counter);
if (counterEl) {
counterEl.textContent = count;
}
};
// ==================== 이벤트 핸들러 ====================
/**
* 카운트 증가
*/
const handleIncrement = () => {
count = clamp(count + 1);
updateDisplay();
};
/**
* 카운트 감소
*/
const handleDecrement = () => {
count = clamp(count - 1);
updateDisplay();
};
/**
* 카운트 초기화
*/
const handleReset = () => {
count = CONFIG.initialValue;
updateDisplay();
};
// ==================== 초기화 ====================
/**
* 이벤트 리스너 연결
*/
const attachEventListeners = () => {
document.querySelector(SELECTORS.btnInc)
?.addEventListener('click', handleIncrement);
document.querySelector(SELECTORS.btnDec)
?.addEventListener('click', handleDecrement);
document.querySelector(SELECTORS.btnReset)
?.addEventListener('click', handleReset);
};
/**
* 앱 초기화
*/
const init = () => {
updateDisplay();
attachEventListeners();
console.log('✅ Counter initialized');
};
// DOM 로드 완료 후 초기화
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
코드 구조 해설:
- IIFE
(() => { })()로 전체를 감쌈: 모든 변수와 함수가 외부로 노출되지 않음 - ‘use strict’: 엄격 모드로 실수 방지 (실수로 전역 변수 생성 차단)
- 섹션별 구분:
- 상수 정의 (CONFIG, SELECTORS)
- 내부 상태 (count)
- 유틸리티 함수 (clamp)
- DOM 업데이트 (updateDisplay)
- 이벤트 핸들러 (handle*)
- 초기화 (init, attachEventListeners)
- 명확한 초기화 흐름:
init()함수에서 모든 설정을 한 곳에서 관리
특징
✅ 장점:
- 스코프 격리: 모든 변수가 IIFE 내부에 캡슐화되어 외부에서 접근 불가능합니다.
(() => { let count = 0; })(); console.log(count); // ReferenceError: count is not defined console.log(window.count); // undefined (전역 오염 없음) - 이름 충돌 방지: 여러 모듈이 같은 변수명을 사용해도 충돌하지 않습니다.
// 모듈 A (() => { let count = 0; // A의 count })(); // 모듈 B (() => { let count = 100; // B의 count (독립적) })(); // 두 count는 서로 영향을 주지 않음! - 명확한 구조: 상수, 상태, 함수를 명확히 구분하여 코드 가독성이 높습니다.
// 설정 변경이 필요할 때 CONFIG만 수정 const CONFIG = { MIN: 0, MAX: 100, // 최대값 변경 시 여기만 수정 }; - 유지보수 용이: 설정값이 CONFIG 객체에 집중되어 변경이 쉽습니다.
- 매직 넘버가 없음
- 변경 영향 범위가 명확함
- 테스트 시 설정을 쉽게 조정 가능
- this 바인딩 문제 없음: 화살표 함수는 렉시컬 this를 사용하여 예측 가능합니다.
const obj = { count: 0, start() { setInterval(() => { this.count++; // ✅ 화살표 함수는 외부 this 사용 console.log(this.count); // 정상 작동 }, 1000); } };
❌ 단점:
- 코드 길이 증가: 보일러플레이트 코드가 더 많이 필요합니다.
- 전통적: ~20줄
- 현대적: ~100줄 (하지만 구조화되고 명확함)
- 학습 곡선: 초보자에게 복잡하게 느껴질 수 있습니다.
- IIFE 개념 이해 필요
- 화살표 함수 문법 익히기
- 모듈 패턴 이해
- 호이스팅 없음: 함수 표현식은 선언 전에 호출할 수 없습니다.
(() => { greet(); // ❌ ReferenceError const greet = () => { console.log('Hi'); }; })(); - 디버깅 시 주의: 익명 함수는 스택 트레이스에서 식별이 어려울 수 있습니다 (하지만 변수명을 지정하면 해결됨).
// 나쁜 예 setTimeout(() => { throw new Error('Oops'); // Stack trace: at <anonymous> }, 1000); // 좋은 예 const handleTimeout = () => { throw new Error('Oops'); // Stack trace: at handleTimeout }; setTimeout(handleTimeout, 1000);
핵심 차이점 비교
두 패턴의 실질적인 차이를 구체적인 예제로 살펴봅시다.
1. 스코프 관리
스코프는 변수가 접근 가능한 범위를 결정합니다. 전역 스코프는 모든 곳에서 접근 가능하지만, 이는 양날의 검입니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 전통적: 전역 스코프
var count = 0;
console.log(window.count); // 0 (전역 객체에 노출)
console.log(count); // 0 (어디서든 접근 가능)
// 개발자 콘솔에서도 접근 가능
count = 999; // 외부에서 마음대로 변경 가능!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 현대적: 지역 스코프 (IIFE로 캡슐화)
(() => {
let count = 0;
console.log(window.count); // undefined (전역 오염 없음)
console.log(count); // 0 (IIFE 내부에서만 접근)
})();
// 개발자 콘솔에서 접근 불가
console.log(count); // ReferenceError: count is not defined
왜 중요한가?
- 전역 변수는 브라우저 확장 프로그램, 다른 스크립트 등에서 실수로 수정할 수 있음
- 대규모 프로젝트에서는 변수명 충돌 가능성이 높아짐
- 보안: 민감한 데이터를 외부에 노출시키지 않음
2. 함수 호이스팅
호이스팅(Hoisting)은 변수와 함수 선언이 스코프의 최상단으로 끌어올려지는 JavaScript의 독특한 동작입니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 전통적: 함수 선언은 호이스팅됨
greet(); // ✅ 작동 - "Hello!" 출력
function greet() {
console.log('Hello!');
}
// 실제로는 이렇게 동작함 (JavaScript 엔진이 자동 변환)
function greet() {
console.log('Hello!');
}
greet(); // 함수가 먼저 선언되고 호출됨
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 현대적: 함수 표현식은 호이스팅 안 됨
greet(); // ❌ ReferenceError: Cannot access 'greet' before initialization
const greet = () => {
console.log('Hello!');
};
// 올바른 사용법: 선언 후 호출
const greet = () => {
console.log('Hello!');
};
greet(); // ✅ 정상 작동
장단점:
- 전통적 (호이스팅 O): 함수를 파일 하단에 작성하고 상단에서 호출 가능 (편리하지만 혼란스러울 수 있음)
- 현대적 (호이스팅 X): 선언 전에 사용할 수 없어 코드 순서가 명확함 (더 예측 가능)
3. this 바인딩
this는 JavaScript에서 가장 혼란스러운 개념 중 하나입니다. 전통적 함수는 호출 방식에 따라 this가 달라지지만, 화살표 함수는 정의된 위치의 this를 사용합니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 전통적: 동적 this (호출 방식에 따라 변함)
const obj = {
count: 0,
increment: function() {
console.log('outer this:', this); // obj
setTimeout(function() {
console.log('inner this:', this); // window (또는 undefined in strict mode)
this.count++; // ❌ window.count를 증가시킴
console.log(this.count); // NaN (undefined + 1)
}, 1000);
}
};
obj.increment();
// 왜 이런 일이 발생할까?
// setTimeout의 콜백은 전역 컨텍스트에서 호출되므로
// this가 window를 가리키게 됨
// 해결책 1: self 변수 (옛날 방식)
const obj = {
count: 0,
increment: function() {
const self = this; // this를 변수에 저장
setTimeout(function() {
self.count++; // ✅ 저장된 this 사용
console.log(self.count); // 1
}, 1000);
}
};
// 해결책 2: bind 사용
const obj = {
count: 0,
increment: function() {
setTimeout(function() {
this.count++;
console.log(this.count);
}.bind(this), 1000); // this를 명시적으로 바인딩
}
};
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 현대적: 렉시컬 this (정의된 위치의 this 사용)
const obj = {
count: 0,
increment: function() {
console.log('outer this:', this); // obj
setTimeout(() => {
console.log('inner this:', this); // obj (외부 this를 그대로 사용)
this.count++; // ✅ obj.count를 증가
console.log(this.count); // 1
}, 1000);
}
};
obj.increment();
// 화살표 함수는 자신만의 this를 가지지 않고
// 외부 스코프의 this를 그대로 사용함 (렉시컬 바인딩)
언제 어떤 걸 쓸까?
- 메서드 정의: 일반 함수 또는 메서드 단축 구문
const obj = { name: 'Alice', // ✅ 좋음: 메서드 단축 구문 greet() { console.log(`Hello, ${this.name}`); }, // ❌ 나쁨: 화살표 함수 (this가 외부를 가리킴) greet: () => { console.log(`Hello, ${this.name}`); // undefined } }; - 콜백 함수: 화살표 함수 (외부 this를 유지해야 할 때)
✅ 화살표 함수 사용 setTimeout(() => { this.doSomething(); }, 1000);
4. 설정 관리
매직 넘버(코드에 직접 쓰인 숫자)는 유지보수를 어렵게 만듭니다. 설정값을 한 곳에 모으면 변경이 훨씬 쉬워집니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 전통적: 매직 넘버 분산
function animate() {
setInterval(update, 1000); // 1000은 뭐지? 밀리초? 초?
}
function validate() {
if (count > 100) { // 100은 어디서 온 값?
alert('Too many!');
}
}
function resetTimer() {
setTimeout(reset, 3000); // 또 다른 3000...
}
// 문제점:
// 1. 1000ms를 500ms로 변경하려면? 모든 코드를 찾아야 함
// 2. 같은 값인지 다른 값인지 알 수 없음
// 3. 값의 의미를 주석으로 설명해야 함
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 현대적: 중앙 집중식 관리
const CONFIG = {
UPDATE_INTERVAL: 1000, // 업데이트 간격 (ms)
MAX_COUNT: 100, // 최대 카운트
RESET_DELAY: 3000, // 리셋 지연 (ms)
ANIMATION_DURATION: 300, // 애니메이션 시간 (ms)
};
const animate = () => {
setInterval(update, CONFIG.UPDATE_INTERVAL); // 명확함!
};
const validate = () => {
if (count > CONFIG.MAX_COUNT) { // 이해하기 쉬움
alert('Too many!');
}
};
const resetTimer = () => {
setTimeout(reset, CONFIG.RESET_DELAY);
};
// 장점:
// 1. 값 변경이 한 곳에서만 발생
// 2. 의미가 명확함 (변수명이 설명)
// 3. 테스트 시 설정을 쉽게 조정 가능
실전 예시: 기획 변경 대응
// ❌ 전통적 방식: 여러 곳을 수정해야 함
// file1.js
setInterval(update, 1000);
// file2.js
setTimeout(refresh, 1000);
// file3.js
if (elapsed > 1000) { /* ... */ }
// 기획: "업데이트 간격을 500ms로 줄여주세요"
// → 3개 파일에서 1000을 모두 찾아서 500으로 변경해야 함
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ✅ 현대적 방식: 한 곳만 수정
const CONFIG = {
UPDATE_INTERVAL: 500, // 1000 → 500으로 변경 (끝!)
};
setInterval(update, CONFIG.UPDATE_INTERVAL);
setTimeout(refresh, CONFIG.UPDATE_INTERVAL);
if (elapsed > CONFIG.UPDATE_INTERVAL) { /* ... */ }
// 모든 곳에 자동으로 반영됨
실전 비교: 타이머 애플리케이션
전통적 방식
var seconds = 0;
var intervalId = null;
function startTimer() {
if (intervalId) return;
intervalId = setInterval(function() {
seconds++;
updateTimerDisplay();
}, 1000);
}
function stopTimer() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
function resetTimer() {
stopTimer();
seconds = 0;
updateTimerDisplay();
}
function updateTimerDisplay() {
var minutes = Math.floor(seconds / 60);
var secs = seconds % 60;
var display = pad(minutes) + ':' + pad(secs);
document.getElementById('timer').textContent = display;
}
function pad(num) {
return num < 10 ? '0' + num : num;
}
// 초기화
updateTimerDisplay();
document.getElementById('start').onclick = startTimer;
document.getElementById('stop').onclick = stopTimer;
document.getElementById('reset').onclick = resetTimer;
문제점:
seconds,intervalId등이 전역에 노출- 다른 타이머가 있으면 충돌 가능
- 설정값(1000ms)이 하드코딩됨
현대적 방식
/**
* 타이머 모듈
* IIFE와 화살표 함수를 사용한 캡슐화
*/
(() => {
'use strict';
// ==================== 상수 ====================
const CONFIG = {
TICK_INTERVAL: 1000, // 1초
};
const SELECTORS = {
timer: '#timer',
btnStart: '#start',
btnStop: '#stop',
btnReset: '#reset',
};
// ==================== 상태 ====================
let seconds = 0;
let intervalId = null;
// ==================== 유틸리티 ====================
/**
* 숫자를 2자리로 패딩
*/
const pad = (num) => (num < 10 ? '0' : '') + num;
/**
* 초를 MM:SS 형식으로 변환
*/
const formatTime = (totalSeconds) => {
const minutes = Math.floor(totalSeconds / 60);
const secs = totalSeconds % 60;
return `${pad(minutes)}:${pad(secs)}`;
};
// ==================== DOM 업데이트 ====================
/**
* 타이머 표시 업데이트
*/
const updateDisplay = () => {
const timerEl = document.querySelector(SELECTORS.timer);
if (timerEl) {
timerEl.textContent = formatTime(seconds);
}
};
// ==================== 타이머 제어 ====================
/**
* 타이머 시작
*/
const startTimer = () => {
if (intervalId) return; // 이미 실행 중이면 무시
intervalId = setInterval(() => {
seconds++;
updateDisplay();
}, CONFIG.TICK_INTERVAL);
console.log('▶ Timer started');
};
/**
* 타이머 정지
*/
const stopTimer = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
console.log('⏸ Timer stopped');
}
};
/**
* 타이머 초기화
*/
const resetTimer = () => {
stopTimer();
seconds = 0;
updateDisplay();
console.log('🔄 Timer reset');
};
// ==================== 초기화 ====================
/**
* 이벤트 리스너 연결
*/
const attachEventListeners = () => {
document.querySelector(SELECTORS.btnStart)
?.addEventListener('click', startTimer);
document.querySelector(SELECTORS.btnStop)
?.addEventListener('click', stopTimer);
document.querySelector(SELECTORS.btnReset)
?.addEventListener('click', resetTimer);
};
/**
* 초기화
*/
const init = () => {
updateDisplay();
attachEventListeners();
console.log('✅ Timer initialized');
};
// DOM 로드 완료 후 실행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
개선점:
- ✅
seconds,intervalId가 모듈 내부에 캡슐화 - ✅ 설정값이 CONFIG 객체에 집중
- ✅ 함수 역할이 명확히 구분됨
- ✅ 여러 타이머를 독립적으로 실행 가능
성능 비교
메모리 사용
// 전통적: 전역 변수 → 페이지 로드 시 메모리에 계속 상주
var data = new Array(1000000); // 전역 스코프에 계속 유지
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 현대적: IIFE → 필요 없어지면 가비지 컬렉션 가능
(() => {
let data = new Array(1000000); // IIFE 종료 후 메모리 해제 가능
processData(data);
})();
실행 속도
// 벤치마크 테스트 (1,000,000회 호출)
// 전통적 function 선언
function add(a, b) {
return a + b;
}
// 평균: ~8ms
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 화살표 함수
const add = (a, b) => a + b;
// 평균: ~8ms
결론: 실행 속도는 거의 동일 (미세한 차이는 측정 오차 범위)
핵심: 성능 차이는 거의 없습니다. 선택은 코드 구조와 유지보수성에 기반해야 합니다.
언제 어떤 패턴을 사용할까?
전통적 방식이 적합한 경우
✅ 간단한 스크립트
// 5줄 이하의 간단한 유틸리티
function toggleMenu() {
document.getElementById('menu').classList.toggle('open');
}
✅ 학습 자료
// 초보자를 위한 튜토리얼
function calculateArea(width, height) {
return width * height;
}
✅ 구형 브라우저 지원
// IE11 이하 지원이 필수인 경우
var data = getData();
function processData() {
// ...
}
현대적 방식이 적합한 경우
✅ 중대형 애플리케이션
// 여러 모듈이 협력하는 복잡한 앱
(() => {
// 모듈 A
})();
(() => {
// 모듈 B (독립적)
})();
✅ 라이브러리/플러그인 개발
// 다른 코드와 충돌 방지가 중요
(() => {
const MyLibrary = {
// API
};
window.MyLibrary = MyLibrary;
})();
✅ 팀 프로젝트
// 여러 개발자가 협업하는 경우
(() => {
'use strict';
// 명확한 구조와 네이밍 규칙
})();
✅ 테스트 가능한 코드
// 단위 테스트가 필요한 경우
const createCounter = () => {
let count = 0;
return {
increment: () => ++count,
getValue: () => count,
};
};
// 테스트에서
const counter = createCounter();
counter.increment();
assert.equal(counter.getValue(), 1);
마이그레이션 가이드
Step 1: IIFE로 감싸기
// Before
var count = 0;
function increment() {
count++;
}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// After
(() => {
var count = 0;
function increment() {
count++;
}
})();
Step 2: var → let/const
(() => {
let count = 0; // 재할당 필요
const MAX = 100; // 상수
})();
Step 3: function → 화살표 함수
(() => {
let count = 0;
const increment = () => {
count++;
};
})();
Step 4: 상수 분리
(() => {
const CONFIG = {
INITIAL_VALUE: 0,
MAX: 100,
};
let count = CONFIG.INITIAL_VALUE;
const increment = () => {
if (count < CONFIG.MAX) {
count++;
}
};
})();
Step 5: 구조화
(() => {
'use strict';
// 상수
const CONFIG = { /* ... */ };
// 상태
let count = 0;
// 함수
const increment = () => { /* ... */ };
// 초기화
const init = () => { /* ... */ };
init();
})();
흔한 실수와 해결책
실수 1: 화살표 함수의 this
❌ 잘못된 사용:
const obj = {
count: 0,
increment: () => {
this.count++; // ❌ this는 전역 객체
}
};
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 올바른 사용:
const obj = {
count: 0,
increment() {
this.count++; // ✅ 메서드 단축 구문 사용
}
};
실수 2: 호이스팅 오해
❌ 작동하지 않음:
greet(); // ReferenceError
const greet = () => {
console.log('Hi');
};
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 해결책:
const greet = () => {
console.log('Hi');
};
greet(); // 선언 후 호출
실수 3: IIFE 없이 let 사용
❌ 여전히 전역 스코프:
let count = 0; // 전역 변수 (window.count는 아니지만)
const increment = () => {
count++;
};
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ IIFE로 캡슐화:
(() => {
let count = 0; // 완전히 격리됨
const increment = () => {
count++;
};
})();
체크리스트
코드 리뷰 시 확인사항
전통적 방식:
[ ] 전역 변수가 최소화되어 있는가?
[ ] 함수명이 다른 코드와 충돌하지 않는가?
[ ] 코드가 충분히 단순한가?
[ ] 구형 브라우저 지원이 필요한가?
현대적 방식:
[ ] 모든 코드가 IIFE로 감싸져 있는가?
[ ] 'use strict'가 선언되어 있는가?
[ ] 상수가 CONFIG 객체에 정리되어 있는가?
[ ] 함수들이 명확한 역할로 구분되어 있는가?
[ ] 초기화 로직이 init() 함수에 집중되어 있는가?
[ ] this 바인딩이 올바른가?
마치며
두 패턴 모두 상황에 따라 적절히 사용되어야 합니다.
핵심 원칙:
- 간단한 스크립트: 전통적 방식으로 빠르게 작성
- 프로젝트 규모 증가: 현대적 패턴으로 마이그레이션
- 팀 협업: 일관된 패턴 선택
- 성능보다 유지보수성: 대부분의 경우 코드 구조가 더 중요
처음에는 “왜 이렇게 복잡하게 작성하지?”라고 생각했지만, 프로젝트가 커지면서 모듈 패턴의 가치를 깨닫게 됩니다.
작은 것부터 시작하세요:
- 먼저 IIFE로 감싸기
- 화살표 함수 익숙해지기
- 상수 분리하기
- 구조화된 패턴 적용하기
하나씩 적용하다 보면, 어느새 깔끔하고 유지보수하기 좋은 코드를 작성하게 될 겁니다!
참고 자료
JavaScript 공식 문서
- MDN - IIFE (Immediately Invoked Function Expression)
- MDN - Arrow Functions
- MDN - Function Hoisting
- MDN - this
디자인 패턴
- Module Pattern - JavaScript Design Patterns
- JavaScript Module Systems
스타일 가이드
- Airbnb JavaScript Style Guide - 화살표 함수 사용 권장
- Google JavaScript Style Guide
관련 문서
- 📖 JavaScript ES6+ 완전 가이드 - 최신 JavaScript 기능
- 🔧 모듈 시스템 (ES Modules vs CommonJS) - import/export 사용법
- 🏗 JavaScript 디자인 패턴 - 실전 패턴 모음
- ⚡ 성능 최적화 기법 - JavaScript 성능 개선
댓글