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

코드 흐름:

  1. count 변수를 전역에 선언하여 모든 함수에서 접근 가능
  2. 각 기능을 개별 함수로 분리 (increment, decrement, updateDisplay, reset)
  3. 페이지 로드 시 updateDisplay()로 초기값 표시
  4. DOM 요소에 이벤트 리스너를 직접 연결

특징

✅ 장점:

  1. 이해하기 쉬움: 순차적으로 읽히며 초보자에게 직관적입니다. “변수 선언 → 함수 정의 → 실행” 순서가 명확합니다.

  2. 호이스팅: 함수 선언이 끌어올려져 어디서든 호출 가능합니다.
    // 함수 선언 전에 호출해도 작동
    greet();  // "Hello!" 출력
    
    function greet() {
        console.log('Hello!');
    }
    
  3. 디버깅 용이: 함수명이 스택 트레이스에 명확히 표시되어 에러 추적이 쉽습니다.
    // 에러 발생 시 스택 트레이스:
    // at increment (script.js:4)
    // at HTMLButtonElement.<anonymous> (script.js:20)
    
  4. 브라우저 호환성: 구형 브라우저(IE6+)에서도 작동하며, 트랜스파일러 없이 바로 사용 가능합니다.

❌ 단점:

  1. 전역 스코프 오염: count, increment 등이 전역 변수로 노출됩니다.
    console.log(window.count);  // 0 (전역 객체에 접근 가능)
    count = 999;  // 외부에서 직접 수정 가능 (위험!)
    
  2. 이름 충돌 위험: 다른 라이브러리나 스크립트와 변수명이 겹칠 수 있습니다.
    // 내 코드
    var data = [1, 2, 3];
    
    // 다른 라이브러리가 로드되면서
    var data = [4, 5, 6];  // 덮어씌워짐!
    
    console.log(data);  // [4, 5, 6] (예상과 다름)
    
  3. 캡슐화 부족: 내부 상태를 외부에서 직접 수정할 수 있어 예상치 못한 버그 발생 가능합니다.
    // 개발자 콘솔에서 누구나 수정 가능
    count = -100;  // 음수로 변경
    count = "문자열";  // 타입까지 변경 가능
    
  4. 테스트 어려움: 전역 상태에 의존하여 격리된 단위 테스트가 어렵습니다.
    // 테스트 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씩 증가!

문제점:

  1. 두 팀이 같은 변수명 count를 사용
  2. 나중에 로드된 analytics.js의 함수가 우리 함수를 덮어씀
  3. 디버깅하기 전까지 문제를 발견하기 어려움
  4. 코드베이스가 커질수록 이런 충돌 확률 증가

이런 문제를 해결하기 위해 모듈 패턴스코프 격리가 필요합니다.


패턴 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();
    }
})();

코드 구조 해설:

  1. IIFE (() => { })() 로 전체를 감쌈: 모든 변수와 함수가 외부로 노출되지 않음
  2. ‘use strict’: 엄격 모드로 실수 방지 (실수로 전역 변수 생성 차단)
  3. 섹션별 구분:
    • 상수 정의 (CONFIG, SELECTORS)
    • 내부 상태 (count)
    • 유틸리티 함수 (clamp)
    • DOM 업데이트 (updateDisplay)
    • 이벤트 핸들러 (handle*)
    • 초기화 (init, attachEventListeners)
  4. 명확한 초기화 흐름: init() 함수에서 모든 설정을 한 곳에서 관리

특징

✅ 장점:

  1. 스코프 격리: 모든 변수가 IIFE 내부에 캡슐화되어 외부에서 접근 불가능합니다.
    (() => {
        let count = 0;
    })();
    
    console.log(count);  // ReferenceError: count is not defined
    console.log(window.count);  // undefined (전역 오염 없음)
    
  2. 이름 충돌 방지: 여러 모듈이 같은 변수명을 사용해도 충돌하지 않습니다.
    // 모듈 A
    (() => {
        let count = 0;  // A의 count
    })();
    
    // 모듈 B
    (() => {
        let count = 100;  // B의 count (독립적)
    })();
    
    // 두 count는 서로 영향을 주지 않음!
    
  3. 명확한 구조: 상수, 상태, 함수를 명확히 구분하여 코드 가독성이 높습니다.
    // 설정 변경이 필요할 때 CONFIG만 수정
    const CONFIG = {
        MIN: 0,
        MAX: 100,  // 최대값 변경 시 여기만 수정
    };
    
  4. 유지보수 용이: 설정값이 CONFIG 객체에 집중되어 변경이 쉽습니다.
    • 매직 넘버가 없음
    • 변경 영향 범위가 명확함
    • 테스트 시 설정을 쉽게 조정 가능
  5. this 바인딩 문제 없음: 화살표 함수는 렉시컬 this를 사용하여 예측 가능합니다.
    const obj = {
        count: 0,
        start() {
            setInterval(() => {
                this.count++;  // ✅ 화살표 함수는 외부 this 사용
                console.log(this.count);  // 정상 작동
            }, 1000);
        }
    };
    

❌ 단점:

  1. 코드 길이 증가: 보일러플레이트 코드가 더 많이 필요합니다.
    • 전통적: ~20줄
    • 현대적: ~100줄 (하지만 구조화되고 명확함)
  2. 학습 곡선: 초보자에게 복잡하게 느껴질 수 있습니다.
    • IIFE 개념 이해 필요
    • 화살표 함수 문법 익히기
    • 모듈 패턴 이해
  3. 호이스팅 없음: 함수 표현식은 선언 전에 호출할 수 없습니다.
    (() => {
        greet();  // ❌ ReferenceError
    
        const greet = () => {
            console.log('Hi');
        };
    })();
    
  4. 디버깅 시 주의: 익명 함수는 스택 트레이스에서 식별이 어려울 수 있습니다 (하지만 변수명을 지정하면 해결됨).
    // 나쁜 예
    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 바인딩이 올바른가?

마치며

두 패턴 모두 상황에 따라 적절히 사용되어야 합니다.

핵심 원칙:

  1. 간단한 스크립트: 전통적 방식으로 빠르게 작성
  2. 프로젝트 규모 증가: 현대적 패턴으로 마이그레이션
  3. 팀 협업: 일관된 패턴 선택
  4. 성능보다 유지보수성: 대부분의 경우 코드 구조가 더 중요

처음에는 “왜 이렇게 복잡하게 작성하지?”라고 생각했지만, 프로젝트가 커지면서 모듈 패턴의 가치를 깨닫게 됩니다.

작은 것부터 시작하세요:

  1. 먼저 IIFE로 감싸기
  2. 화살표 함수 익숙해지기
  3. 상수 분리하기
  4. 구조화된 패턴 적용하기

하나씩 적용하다 보면, 어느새 깔끔하고 유지보수하기 좋은 코드를 작성하게 될 겁니다!


참고 자료

JavaScript 공식 문서

디자인 패턴

스타일 가이드


관련 문서

댓글