IIFE — 즉시 실행 함수 표현식 완전 정복

IIFE(Immediately Invoked Function Expression)의 문법·목적·역사적 배경, 클로저를 활용한 모듈 패턴, 비동기 IIFE, 그리고 ES 모듈 시대의 현재 역할을 다룹니다.

· 6 min read · PALDYN Team

지난 글에서 arguments 객체와 그 현대적 대체 방법을 살펴봤습니다. 이번에는 ES 모듈이 없던 시절 JavaScript 코드 구조화의 핵심이었던 **IIFE(Immediately Invoked Function Expression, 즉시 실행 함수 표현식)**를 다룹니다. 레거시 코드에서 빈번하게 등장하고, 오늘날에도 특정 상황에서 유용한 패턴입니다.

IIFE란

함수를 정의하자마자 즉시 호출하는 표현식입니다. 핵심은 함수를 표현식으로 만들어 즉시 호출하는 것입니다.

// 기본 형태 — 함수 전체를 괄호로 감싸기 (크록포드 스타일)
(function() {
  console.log('즉시 실행!');
}());

// 호출 괄호를 바깥에 두기 (더글라스 스타일)
(function() {
  console.log('즉시 실행!');
})();

// 화살표 함수 IIFE
(() => {
  console.log('즉시 실행!');
})();

괄호가 필요한 이유는 JavaScript 파서가 function으로 시작하는 문장을 선언식으로 해석하기 때문입니다. 괄호로 감싸거나 void, !, + 같은 단항 연산자를 앞에 붙이면 표현식 위치로 해석되어 즉시 호출이 가능해집니다.

void function() { console.log('실행!'); }();
!function() { console.log('실행!'); }();
+function() { console.log('실행!'); }();

왜 IIFE가 필요했나

ES2015 이전에는 var 변수가 함수 스코프만 가졌고, 블록 스코프가 없었습니다. ES 모듈도 없었습니다. 이런 환경에서 코드를 모듈화하고 전역 네임스페이스 오염을 막는 유일한 방법이 IIFE였습니다.

// ES2015 이전 — 전역 오염 없는 초기화
var MyApp = (function() {
  var _private = 'secret'; // 외부에서 접근 불가

  function init() {
    console.log('앱 시작');
  }

  return {
    init: init,
    version: '1.0'
  };
}());

MyApp.init();          // 정상 동작
console.log(MyApp._private); // undefined — 접근 불가

IIFE 문법 형태

클로저 모듈 패턴

IIFE와 클로저를 결합하면 프라이빗 상태를 가진 모듈을 만들 수 있습니다.

const counter = (function() {
  let _count = 0; // 프라이빗

  return {
    increment() { _count++; },
    decrement() { _count--; },
    getCount()  { return _count; },
    reset()     { _count = 0; },
  };
}());

counter.increment();
counter.increment();
counter.getCount(); // 2
counter._count;     // undefined — 직접 접근 불가

이 패턴은 revealing module pattern의 기반이었으며, ES2022 클래스 프라이빗 필드(#)가 등장하기 전까지 캡슐화의 표준 방식이었습니다.

IIFE 모듈 패턴

초기화 값 전달

IIFE에 인수를 넘겨 외부 값을 안전하게 캡처할 수 있습니다. 미니파이어가 파라미터 이름을 줄여도 내부 동작은 보장됩니다.

const app = (function(window, document, undefined) {
  // undefined를 명시적으로 파라미터에 올려 외부 변조 방지 (ES5 관용구)
  // window, document는 지역 변수로 미니파이 가능

  function onReady() {
    document.title = 'App Ready';
  }

  window.addEventListener('DOMContentLoaded', onReady);
  return { version: '1.0' };
}(window, document));

비동기 IIFE

Top-level await가 없던 시절, 모듈 최상단에서 async/await를 사용하려면 비동기 IIFE가 필요했습니다.

(async function() {
  try {
    const config = await fetch('/api/config').then(r => r.json());
    initApp(config);
  } catch (err) {
    console.error('초기화 실패:', err);
  }
}());

// 화살표 버전
(async () => {
  const data = await loadData();
  render(data);
})();

오늘날 ES 모듈에서는 top-level await를 직접 사용할 수 있어 이 패턴이 불필요해졌습니다.

// 현대 ES 모듈에서
const data = await loadData(); // 바로 사용 가능
render(data);

for 루프 var 클로저 문제 해결 (역사적 패턴)

var와 루프의 클로저 문제를 IIFE로 해결하던 고전 패턴입니다.

// 문제: 모든 핸들러가 i=5를 출력
for (var i = 0; i < 5; i++) {
  setTimeout(function() { console.log(i); }, i * 100);
}

// 해결: IIFE로 i를 캡처
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() { console.log(j); }, j * 100);
  }(i));
}

// 현대 해결책: let 사용 (블록 스코프)
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), i * 100);
}

현대 코드에서 IIFE

ES2015+ 환경에서 IIFE의 전통적 역할 대부분은 다른 기능이 대체했습니다.

  • 스코프 격리 → ES 모듈 (각 파일이 독립 스코프)
  • 프라이빗 상태 → 클래스 프라이빗 필드(#) 또는 클로저
  • 블록 스코프let/const
  • 비동기 초기화 → Top-level await

그럼에도 IIFE가 여전히 유용한 경우가 있습니다.

// 복잡한 초기화 값을 const에 할당
const DATA = (() => {
  const raw = [/* 복잡한 처리 */];
  return Object.freeze(raw.map(transform));
})();

// 블록 스코프가 필요하지만 이름 없는 로직
{
  const temp = computeExpensive();
  useOnce(temp);
} // temp는 여기서 GC 대상

// 위와 동일한 효과를 IIFE로
(() => {
  const temp = computeExpensive();
  useOnce(temp);
})();

지난 글: arguments 객체 완전 정복

다음 글: 고차 함수 완전 정복


읽어주셔서 감사합니다. 😊