함수 선언식 vs 함수 표현식
JavaScript 함수 선언식과 함수 표현식의 호이스팅 차이, 스코프 동작, 기명 함수 표현식의 재귀 참조, 조건부 함수 정의 패턴을 깊이 다룹니다.
지난 글에서 템플릿 리터럴과 태그드 템플릿을 살펴봤습니다. 이번에는 JavaScript 함수를 정의하는 두 가지 핵심 방법인 함수 선언식과 함수 표현식의 차이를 정확히 이해합니다. 같은 함수를 만들지만 호이스팅 동작, 스코프, 이름 바인딩이 다르기 때문에 어떤 상황에서 무엇을 선택해야 하는지 알아야 합니다.
문법 형태
두 방식 모두 function 키워드를 쓰지만 위치가 다릅니다.
// 함수 선언식 — function이 문장의 시작
function add(a, b) {
return a + b;
}
// 익명 함수 표현식 — 값으로서 변수에 할당
const add = function(a, b) {
return a + b;
};
// 기명 함수 표현식 — 이름이 있지만 외부에서는 접근 불가
const add = function addNumbers(a, b) {
return a + b;
};
파서가 function을 문장의 첫 토큰으로 만나면 선언식, 표현식 위치(할당 우변, 인수, 조건문 등)에서 만나면 표현식으로 처리합니다.
핵심 차이: 호이스팅
함수 선언식은 전체 함수 본문이 스코프 최상단으로 끌어올려집니다. 따라서 선언 전에 호출해도 정상 동작합니다.
// 선언 전 호출 — 정상 작동
result = greet('Kim'); // '안녕, Kim!'
function greet(name) {
return `안녕, ${name}!`;
}
함수 표현식은 변수 선언(const/let/var)의 규칙을 따릅니다. const·let은 TDZ로 인해 선언 전 접근이 ReferenceError이고, var는 선언만 올라가고 값이 undefined로 남습니다.
// const 함수 표현식 — TDZ
fn(); // ReferenceError
const fn = function() {};
// var 함수 표현식 — undefined 호출 시도
fn(); // TypeError: fn is not a function
var fn = function() {};
호이스팅 내부 동작
JavaScript 엔진은 코드를 두 단계로 처리합니다.
- 파싱/컴파일 단계 — 함수 선언식을 스코프에 등록(전체 본문 포함).
var는 선언만 등록(undefined).const/let은 TDZ에 넣음. - 실행 단계 — 코드를 순서대로 실행. 함수 표현식 할당문에 도달하면 그때 변수에 함수가 바인딩됨.
// 실제 실행 순서 (엔진 관점)
function a() { return 1; } // 1. 선언식 전체 호이스팅
var b; // 2. var 선언만 (undefined)
// TDZ: c는 접근 불가
console.log(a()); // 1
console.log(b); // undefined
// console.log(c) // ReferenceError
b = function() {};
const c = function() {};
블록 안의 함수 선언식
if, for 같은 블록 내부의 함수 선언식 동작은 비엄격 모드에서 브라우저마다 다릅니다. strict mode에서는 블록 스코프로 제한되어 일관성 있게 동작합니다.
'use strict';
if (true) {
function blockFn() { return 1; }
}
typeof blockFn; // 'undefined' — 블록 밖에서 접근 불가
// 조건부 함수 정의는 표현식으로 해야 안전
const handler = condition
? function() { /* A */ }
: function() { /* B */ };
기명 함수 표현식의 장점
함수 표현식에 이름을 붙이면 두 가지 이점이 있습니다.
스택 트레이스 가독성: 익명 함수는 오류 스택에 anonymous로 표시되지만 기명 함수는 이름이 나타납니다.
내부 자기 참조: 이름은 함수 본문 내에서만 접근 가능하므로 재귀에 안전합니다. 외부 변수명이 바뀌어도 재귀가 깨지지 않습니다.
const fac = function factorial(n) {
return n <= 1 ? 1 : n * factorial(n - 1);
};
// 변수명 변경해도 재귀 동작
const f = fac;
fac = null;
f(5); // 120 — factorial 내부 참조는 유지됨
.name 프로퍼티
ES2015부터 함수 추론 이름(inferred name)을 지원합니다. 변수에 익명 함수를 할당하면 변수명이 함수 이름으로 추론됩니다.
const fn = function() {};
fn.name; // 'fn' (추론됨)
const obj = {
method: function() {}
};
obj.method.name; // 'method'
function declared() {}
declared.name; // 'declared'
언제 무엇을 선택하나
실제 코드베이스에서 권장되는 가이드라인입니다.
- 모듈의 공개 함수·유틸리티 → 함수 선언식. 파일 어디서든 사용 가능하고 의도가 명확.
- 콜백·핸들러·즉시 실행 값 → 함수 표현식(또는 화살표 함수).
- 조건부 함수 생성 → 반드시 함수 표현식.
- 재귀 함수 표현식 → 기명 함수 표현식으로 안전한 자기 참조.
// 권장: 유틸리티는 선언식
function formatDate(date) { /* ... */ }
// 권장: 콜백은 표현식
array.map(function(item) { return item * 2; });
// 또는 화살표 함수
array.map(item => item * 2);
지난 글: 템플릿 리터럴 완전 정복
다음 글: 화살표 함수와 this
읽어주셔서 감사합니다. 😊