자바스크립트를 쓰다 보면, 어느 순간 이런 코드를 작성하게 됩니다.

javascript
greet(); // "Hello World!"

function greet() {
  console.log("Hello World!");
}

선언 전에 호출했는데 잘 됩니다. 그런데 이렇게 바꾸면?

javascript
greet(); // TypeError: greet is not a function

var greet = function () {
  console.log("Hello World!");
};

에러가 납니다. console.log로 10분 디버깅하다가 에러 메시지를 읽었습니다. TypeError: greet is not a function. 함수인데 함수가 아니라고요.

자바스크립트가 저한테만 그러는 건 아니었더라고요. “왜 같은 함수인데 다르게 동작하지?” — 다들 한 번쯤 이런 생각 해보셨죠?

어두운 모니터에 표시된 코드 화면 - 매일 쓰는 함수도 동작 원리를 알면 다르게 보입니다
어두운 모니터에 표시된 코드 화면 - 매일 쓰는 함수도 동작 원리를 알면 다르게 보입니다
사진: Joan Gamell / Unsplash

함수 선언식과 함수 표현식은 생긴 건 비슷해도 동작 방식이 다릅니다. 그 차이의 핵심에 호이스팅(Hoisting) 이 있어요. 오늘은 이 개념을 실무 예제와 함께 제대로 정리해볼게요.

함수 선언식(Function Declaration)

function 키워드로 시작하는 가장 기본적인 함수 형태입니다.

javascript
function add(a, b) {
  return a + b;
}

이름이 필수예요. function () {} 처럼 이름 없이는 쓸 수 없습니다.

함수 표현식(Function Expression)

변수에 함수를 값처럼 할당하는 방식입니다.

javascript
const add = function (a, b) {
  return a + b;
};

여기서 오른쪽 function (a, b) { ... } 부분이 익명 함수 표현식입니다. 이름을 붙일 수도 있어요.

javascript
const add = function addNumbers(a, b) {
  return a + b;
};

이름을 붙이면 스택 트레이스에서 함수명이 표시되어 디버깅할 때 도움이 됩니다. 다만 addNumbers는 함수 내부에서만 참조할 수 있고, 외부에서는 add로만 호출해야 해요.

코드 블록이 위로 끌어올려지는 과정 - 호이스팅은 선언이 스코프 맨 위로 올라가는 동작을 말합니다
코드 블록이 위로 끌어올려지는 과정 - 호이스팅은 선언이 스코프 맨 위로 올라가는 동작을 말합니다

핵심 차이: 호이스팅

이 둘의 가장 중요한 차이가 바로 호이스팅 동작입니다.

호이스팅이란 자바스크립트 엔진이 코드를 실행하기 전에 변수와 함수 선언을 스코프의 맨 위로 끌어올리는 동작을 말합니다. 실제로 코드가 이동하는 건 아니고, 엔진이 실행 컨텍스트를 준비하는 과정에서 일어나는 일이에요.

함수 선언식은 완전히 호이스팅됩니다

javascript
// 선언 전에 호출해도 잘 동작합니다
console.log(add(2, 3)); // 5

function add(a, b) {
  return a + b;
}

자바스크립트 엔진은 코드를 실행하기 전에 함수 선언식 전체(이름 + 본문)를 미리 등록합니다. 그래서 선언 전에 호출해도 문제가 없어요.

내부적으로는 이렇게 해석됩니다.

javascript
// 엔진이 실제로 처리하는 순서 (개념적)
function add(a, b) {
  return a + b;
}

console.log(add(2, 3)); // 5

함수 표현식은 변수 선언만 호이스팅됩니다

javascript
console.log(add); // undefined (var) 또는 ReferenceError (let/const)
console.log(add(2, 3)); // TypeError: add is not a function

var add = function (a, b) {
  return a + b;
};

var로 선언하면 변수 자체는 호이스팅되지만, 함수가 할당되는 건 코드 실행 시점입니다. 그래서 할당 전에 호출하면 undefined()를 실행하려다가 오류가 나는 거예요.

let이나 const는 더 엄격합니다. 호이스팅은 되지만 TDZ(Temporal Dead Zone) 때문에 선언 전에 접근하면 바로 ReferenceError가 납니다.

javascript
console.log(add); // ReferenceError: Cannot access 'add' before initialization

const add = function (a, b) {
  return a + b;
};

한눈에 비교

데이터 표
구분함수 선언식함수 표현식 (var)함수 표현식 (let/const)
호이스팅완전히 (이름 + 본문)변수명만 (값은 undefined)변수명만 (TDZ)
선언 전 호출✅ 가능❌ TypeError❌ ReferenceError
이름 필수 여부필수선택선택

표를 보고 “그럼 그냥 항상 함수 선언식 쓰면 되겠다"고 생각하셨나요? 저도 잠깐 그랬는데, 화살표 함수가 조용히 기다리고 있었습니다.

화살표 함수는 어디에?

화살표 함수(=>)는 함수 표현식의 일종입니다. 호이스팅 동작도 동일해요.

javascript
// 이렇게 쓰면
const add = (a, b) => a + b;

// 이것과 동작이 같습니다
const add = function (a, b) {
  return a + b;
};

다만 화살표 함수는 일반 함수와 중요한 차이가 있어요.

this 바인딩이 다릅니다. 화살표 함수는 자신만의 this를 갖지 않고, 선언된 위치의 외부 스코프 this를 그대로 씁니다.

javascript
const counter = {
  count: 0,
  // 일반 함수 표현식: this는 counter 객체
  increment: function () {
    this.count++;
  },
  // 화살표 함수: this는 외부 스코프 (여기선 전역 또는 undefined)
  incrementArrow: () => {
    this.count++; // 의도한 대로 동작하지 않을 수 있습니다
  },
};

그래서 객체 메서드나 이벤트 핸들러에서 this가 필요한 경우에는 일반 함수 표현식을 써야 합니다.

arguments 객체가 없습니다. 화살표 함수는 arguments 객체를 갖지 않아요. 가변 인자를 처리할 때는 rest 파라미터(...args)를 사용하세요.

javascript
// 일반 함수: arguments 사용 가능
function sum() {
  return Array.from(arguments).reduce((acc, val) => acc + val, 0);
}

// 화살표 함수: rest 파라미터 사용
const sum = (...args) => args.reduce((acc, val) => acc + val, 0);

실무에서 어떻게 쓸까?

이론은 알겠는데, 실제로 어떤 상황에서 뭘 쓸지 헷갈리죠. 몇 가지 기준을 공유해드릴게요.

콜백과 짧은 함수는 화살표 함수

map, filter, forEach 같은 배열 메서드의 콜백에는 화살표 함수가 자연스럽습니다. 코드가 짧고 읽기 좋아요.

javascript
const numbers = [1, 2, 3, 4, 5];

// 화살표 함수가 훨씬 간결합니다
const doubled = numbers.map((n) => n * 2);
const evens = numbers.filter((n) => n % 2 === 0);

모듈 레벨 함수는 취향껏, 단 일관성 있게

최근 프론트엔드 코드에서는 const로 선언하는 함수 표현식을 많이 씁니다. 특히 React 컴포넌트나 유틸 함수를 작성할 때 자주 보게 되죠.

javascript
// 많은 프로젝트에서 볼 수 있는 스타일
const formatDate = (date) => {
  return new Intl.DateTimeFormat("ko-KR").format(date);
};

export default function UserCard({ name }) {
  return <div>{name}</div>;
}

어느 쪽이 더 좋다기보다는, 팀의 컨벤션을 따르고 일관성을 유지하는 게 중요합니다.

호이스팅에 의존하는 코드는 피하세요

함수 선언식이 호이스팅된다는 사실을 알고, 선언보다 위에서 호출하는 코드를 일부러 작성하는 경우가 있습니다. 하지만 이건 코드를 읽는 사람 입장에서 혼란스러울 수 있어요.

javascript
// ❌ 가능은 하지만, 읽는 사람이 아래로 스크롤해야 합니다
initApp();

function initApp() {
  // ...
}

// ✅ 선언 후 호출하는 게 의도가 더 명확합니다
function initApp() {
  // ...
}

initApp();

물론 파일 구조상 초기화 코드를 위에 두고 헬퍼 함수들을 아래에 두는 방식도 하나의 스타일입니다. 중요한 건 팀원 모두가 이해하고 합의한 방식을 따르는 것이에요.

정리

  • 함수 선언식: 완전 호이스팅 → 선언 전 호출 가능. 이름 필수.
  • 함수 표현식: 변수만 호이스팅 → 선언 전 호출 불가. 이름 선택.
  • 화살표 함수: 함수 표현식의 일종. thisarguments가 없음.
  • 실무 팁: 호이스팅에 의존하는 코드보다는 선언 후 사용하는 방식이 읽기 좋습니다.

매일 쓰는 함수지만, 동작 원리를 알면 디버깅도 훨씬 빨라집니다. 다음에 TypeError: xxx is not a function을 만났을 때 바로 호이스팅을 확인해보세요.