4.1 함수 선언 방식 4가지
JavaScript에서 함수를 정의하는 방법은 크게 4가지입니다. 각 방식은 호이스팅 동작, this 바인딩, 문법 등에서 차이가 있으므로 상황에 맞게 선택해야 합니다.
1. 함수 선언식 (Function Declaration)
가장 전통적인 함수 정의 방법입니다. function 키워드로 시작하며, 전체 함수가 호이스팅됩니다.
// 호이스팅 덕분에 선언 전에 호출 가능
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}
// 재귀 함수 — 함수 이름으로 자신을 참조
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
호이스팅 메커니즘
// 실제 코드
greet("Alice");
function greet(name) {
console.log(`Hello, ${name}!`);
}
// JS 엔진이 처리하는 방식 (개념적)
function greet(name) { // 함수 전체가 최상단으로 끌어올려짐
console.log(`Hello, ${name}!`);
}
greet("Alice"); // Hello, Alice!
2. 함수 표현식 (Function Expression)
변수에 함수를 할당하는 방식입니다. var로 선언하면 변수 이름만 호이스팅되고, const/let은 TDZ(Temporal Dead Zone)로 인해 선언 전 호출이 불가합니다.
// const로 선언 — TDZ로 인해 선언 전 호출 불가
// console.log(multiply(2, 3)); // ReferenceError!
const multiply = function(a, b) {
return a * b;
};
console.log(multiply(2, 3)); // 6
// 기명 함수 표현식 — 디버깅 시 스택 트레이스에 이름이 표시됨
const divide = function divideNumbers(a, b) {
if (b === 0) throw new Error("0으로 나눌 수 없습니다");
return a / b;
};
console.log(divide(10, 2)); // 5
// divideNumbers는 함수 내부에서만 참조 가능
var와 const 차이
// var: 변수 이름만 호이스팅 (undefined로 초기화)
console.log(typeof varFunc); // "undefined"
var varFunc = function() { return "var"; };
// const: TDZ — 접근 자체가 에러
// console.log(typeof constFunc); // ReferenceError
const constFunc = function() { return "const"; };
3. 화살표 함수 (Arrow Function)
ES6에서 도입된 간결한 함수 문법입니다. this 바인딩이 없고, 항상 선언 시점의 외부 this를 사용합니다.
// 기본 문법
const square = (x) => x * x;
const greet = name => `Hello, ${name}!`; // 매개변수 1개면 괄호 생략 가능
const getZero = () => 0; // 매개변수 없으면 괄호 필수
// 함수 본문이 여러 줄이면 중괄호와 return 필수
const clamp = (value, min, max) => {
if (value < min) return min;
if (value > max) return max;
return value;
};
console.log(square(5)); // 25
console.log(greet("Bob")); // Hello, Bob!
console.log(clamp(15, 0, 10)); // 10
// 객체를 암시적으로 반환할 때는 소괄호로 감쌈
const makeUser = (name, age) => ({ name, age });
console.log(makeUser("Alice", 30)); // { name: 'Alice', age: 30 }
this 바인딩 없음 — 실전 예제
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// 화살표 함수: 외부 this(Timer 인스턴스)를 캡처
setInterval(() => {
this.seconds++;
console.log(`${this.seconds}초 경과`);
}, 1000);
}
}
const timer = new Timer();
timer.start();
// 1초 경과, 2초 경과, 3초 경과...
화살표 함수를 사용하면 안 되는 경우
const obj = {
name: "Object",
// ❌ 화살표 함수: this가 전역(또는 undefined)
badMethod: () => {
console.log(this?.name); // undefined
},
// ✅ 메서드 축약: this가 obj
goodMethod() {
console.log(this.name); // "Object"
},
};
obj.badMethod(); // undefined
obj.goodMethod(); // "Object"
4. 메서드 축약 (Method Shorthand)
ES6에서 도입된 객체/클래스 내 메서드 정의 단축 문법입니다. 내부적으로 함수 선언과 유사하게 동작하지만 super 키워드 사용이 가능합니다.
// 객체 리터럴에서 메서드 축약
const calculator = {
value: 0,
add(n) { // function 키워드 생략
this.value += n;
return this; // 메서드 체이닝
},
subtract(n) {
this.value -= n;
return this;
},
result() {
return this.value;
},
};
console.log(calculator.add(10).subtract(3).result()); // 7
// 클래스에서 메서드 축약
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name}이 소리를 냅니다.`;
}
// 정적 메서드도 동일
static create(name) {
return new Animal(name);
}
}
// super 키워드 — 메서드 축약에서만 가능
class Dog extends Animal {
speak() {
return super.speak() + " 멍멍!";
}
}
const dog = new Dog("바둑이");
console.log(dog.speak()); // "바둑이이 소리를 냅니다. 멍멍!"
실전 예제: 이벤트 시스템 구현
4가지 함수 방식을 모두 활용한 실용적인 예제입니다.
// 함수 선언식: 유틸리티 함수 (호이스팅 활용)
function createEventEmitter() {
// 함수 표현식: 내부 구현 (클로저로 비공개 상태 관리)
const listeners = new Map();
return {
// 메서드 축약: 공개 API
on(event, callback) {
if (!listeners.has(event)) {
listeners.set(event, []);
}
listeners.get(event).push(callback);
return this;
},
off(event, callback) {
if (listeners.has(event)) {
const filtered = listeners.get(event).filter(cb => cb !== callback);
listeners.set(event, filtered);
}
return this;
},
emit(event, ...args) {
if (listeners.has(event)) {
// 화살표 함수: 콜백 내에서 this 걱정 없음
listeners.get(event).forEach(callback => callback(...args));
}
return this;
},
once(event, callback) {
// 함수 표현식: 일회성 핸들러 래퍼
const wrapper = function(...args) {
callback(...args);
this.off(event, wrapper);
}.bind(this);
return this.on(event, wrapper);
},
};
}
const emitter = createEventEmitter();
emitter
.on("data", (msg) => console.log(`수신: ${msg}`))
.on("data", (msg) => console.log(`로그: ${msg}`))
.once("connect", () => console.log("연결됨 (최초 1회)"));
emitter.emit("connect"); // 연결됨 (최초 1회)
emitter.emit("connect"); // 아무 일도 없음
emitter.emit("data", "Hello"); // 수신: Hello \n 로그: Hello
방식별 비교 표
| 특징 | 함수 선언식 | 함수 표현식 | 화살표 함수 | 메서드 축약 |
|---|---|---|---|---|
| 호이스팅 | 전체 호이스팅 | 변수만 호이스팅 | 변수만 호이스팅 | N/A (객체/클래스 내부) |
this 바인딩 | 호출 방식에 따라 | 호출 방식에 따라 | 외부 스코프 캡처 | 호출 방식에 따라 |
arguments 객체 | 있음 | 있음 | 없음 | 있음 |
new 생성자 | 가능 | 가능 | 불가능 | 불가능 |
super 키워드 | 불가 | 불가 | 불가 | 가능 |
| 재귀 시 자기 참조 | 이름으로 가능 | 기명 시 가능 | 외부 변수 사용 | 이름으로 가능 |
| 주요 사용처 | 독립 유틸리티, 재귀 | 변수 할당, 콜백 | 콜백, 메서드 내부 | 객체/클래스 메서드 |
언제 어떤 방식을 사용할까?
함수 선언식을 사용할 때
- 파일 최상위 레벨의 독립적인 유틸리티 함수
- 재귀 함수
- 호이스팅을 의도적으로 활용해야 할 때
함수 표현식을 사용할 때
- 변수에 함수를 조건부로 할당할 때
- 즉시 실행 함수 표현식(IIFE)
- 기명 함수로 디버깅 가시성이 필요할 때
화살표 함수를 사용할 때
- 배열 메서드 콜백 (
map,filter,reduce) - 클래스/객체 메서드 내부에서 정의하는 콜백
- 단순한 변환/반환 함수
메서드 축약을 사용할 때
- 객체 리터럴의 메서드
- 클래스의 인스턴스/정적 메서드
super를 사용해야 하는 오버라이드 메서드
고수 팁
팁 1: IIFE(즉시 실행 함수 표현식)로 스코프 격리
// 모듈이 없던 시절의 스코프 격리 패턴
const counter = (function() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
})();
counter.increment();
counter.increment();
console.log(counter.value()); // 2
팁 2: 함수 오버로딩 시뮬레이션
// JavaScript는 오버로딩 미지원 — 조건 분기로 구현
function process(input) {
if (typeof input === "string") return input.toUpperCase();
if (typeof input === "number") return input * 2;
if (Array.isArray(input)) return input.map(process);
throw new TypeError(`지원하지 않는 타입: ${typeof input}`);
}
console.log(process("hello")); // HELLO
console.log(process(21)); // 42
console.log(process(["a", 1, "b"])); // ["A", 2, "B"]
팁 3: 함수를 반환하는 함수 — 지연 실행
// 설정을 캡처한 함수 팩토리
const createLogger = (prefix, level = "INFO") => {
const timestamp = () => new Date().toISOString();
return (message) => console.log(`[${timestamp()}] [${level}] [${prefix}] ${message}`);
};
const appLog = createLogger("App");
const dbLog = createLogger("DB", "DEBUG");
appLog("서버 시작"); // [2024-01-01T00:00:00.000Z] [INFO] [App] 서버 시작
dbLog("쿼리 실행됨"); // [2024-01-01T00:00:00.000Z] [DEBUG] [DB] 쿼리 실행됨