IIFE(즉시 실행 함수 표현)
(function () { console.log('Hello!!') })(); // Hello!!
JS 코드를 보다보면 위와 같은 형식의 코드를 볼 수 있다. 위와 같은 문법은 IIFE(Immediately Invoked Function Expression)이라고 하며, 괄호로 둘러싸인 익명함수와 함수를 실행하는 괄호를 연결하여 함수가 정의되자마자 실행되게 한다. 즉시 실행 함수 표현이라고도 한다. 일단 함수 선언식과 표현식에 대해 알아보자.
1. 함수 선언문과 표현식
//함수 선언문
function fn1() {
console.log("Hello!");
}
//함수 표현식 - 익명
const fn2 = function() {
console.log("Hello!");
}
//함수 표현식 - 기명
const fn3 = function fn3() {
console.log("Hello!");
}
함수를 작성하는 방법은 크게 함수 선언문과 표현식으로 나뉜다. 선언문은 일반적인 프로그래밍 언어와 유사한 형태이며, 표현식은 변수에 함수를 할당한다는 형태이다. 둘은 비슷해보이지만 동작의 차이가 있는 경우가 있다.
//함수 선언문
fn1(); // Hello!
function fn1() {
console.log("Hello!");
}
//함수 표현식 - 익명 + const 할당
fn2(); // Error: Cannot access fn2 before initialization
const fn2 = function() {
console.log("Hello!");
}
함수를 선언하기 전에 해당 함수를 호출했을 때 두 함수는 차이를 보인다. 호이스팅에 대해 알고 있다면, 이 현상이 호이스팅과 관련이 있을 것이라는 추측을 할 수 있다. (let, const 그리고 var) 함수 선언식과 표현식에 어떤 일이 발생한 것인지 분석해보자.
function 키워드는 호이스팅에 의해 함수 선언문은 통채로(선언 + 초기화 + 할당) 스코프의 최상단으로 끌어올려진(것처럼 동작한)다( = 실행 컨텍스트에 이미 로딩되어있다). 따라서 선언식인 f1은 선언 전에 호출해도 올바른 결과가 출력된다. 그런데 함수 표현식의 경우 변수에 할당되는 부분이 호이스팅 되지 않기 때문에 코드가 할당부를 읽은 후에야 변수에 할당된 함수를 호출할 수 있는 것이다.
//함수 표현식 - 기명 + var 할당
console.log(fn3); // undefined
fn3(); // Error: fn3 is not a function
ff(); // Error: ff not defined
var fn3 = function ff() {
console.log("Hello!");
}
위 코드의 결과는 어떨까? 먼저 var로 선언된 fn3는 선언과 초기화가 동시에 이루어지므로, 호이스팅 현상으로 fn3의 값은 undefined로 실행 컨텍스트에 등록되어있다. 따라서 fn3는 undefined가 출력된다.
fn3는 function ff를 할당하였지만 할당은 호이스팅의 영향을 받지 않으므로 변수 할당문 이전에는 fn3는 여전히 undfined이다. 따라서 fn3()를 실행 시 fn3 is not a function이라는 에러가 발생한다.
함수의 이름인 ff를 호출하면 어떨까? 사실 기명 함수 표현식은 외부에서 그 이름을 사용할 수 없다. 간단한 예시를 더보기로 만들어 놓았으니 참고하자.
const x = function ff() {
console.log('Hello');
}
x(); // Hello
ff(); // Error! ff is not defined
위와 같이 제대로된 순서로 함수를 호출했더라도 호이스팅과 관련없이 ff라는 함수를 외부에서 호출할 수 없다. 기명 함수 표현에 대한 자세한 설명은 링크를 참고하자.
2. IIFE
const f1 = function () { console.log('Hello') }; // f1에 할당
(f1)(); // Hello
( function () { console.log('Hello??') })(); // Hello??
function () { console.log('Hello??'); }(); // Error: require a function name
JavaScript는 function 키워드에 대해 보통 함수 선언문으로 간주한다. IIFE는 함수에 괄호를 감싸므로서 괄호 내의 함수를 함수 표현식으로 파싱된다. 이게 왜 중요할까? 프로그램에서 문(Statement)은 명령을 의미한다. 따라서 문을 실행하면 어떤 동작이 발생한다. 반면 식은 어떠한 값으로 평가되는 요소이다. 즉, 표현식은 하나의 값이되며, 문은 식을 포함하고 있다.
문 혹은 문을 포함한 정의(declaration)는 명령이므로 실행이 되고 끝나버려 값으로 남지 않는다. 그 의미는 함수 선언문만으로는 실행시킬 수없다는 의미이다(아무 것도 없는 것을 실행시킬 수는 없다). 따라서 위 코드의 마지막 문장 처럼 선언문에 ( )를 넣는다고 함수를 바로 실행시킬 수 없다는 말이다. 반면, 함수를 괄호로 감싼다면 이는 식이 되어 값이 남게 된다. 남은 값이 함수 이므로 이에 ( )를 추가하여 문으로 만들어 실행시킬 수 있는 것이다.
//IFFE Variation
//Inner Bracket
(function() { console.log("Hello"); }());
//Arrow Function
(() => console.log("Hello"))();
//Argument
(function(str) { console.log("Hello!", str); })('IHL');
//Name
(function f1() { console.log('Hello!!') })();
//Assignment
let f2;
(f2 = function () { console.log('Hello!!') })();
f2();
IIFE는 위와 같이 다양한 형태로 작성 가능하다.
// 일반 함수
function init() { console.log('초기화'); }
init();
// IIFE
(function init() { console.log('초기화'); })();
IIFE를 쓰는 이유가 무엇일까? 우선 IIFE는 이름 없는 함수를 바로 실행시킨다. 따라서 이름 충돌이 발생할 확률이 없다. 또한 위의 init과 같이 단 한번만 호출되어야 하는 함수의 경우 IIFE로 만들어 실수로 다시 호출하는 것을 방지할 수 있다. 그 외에도 IIFE는 클로저와 함께 사용되기도 한다. (이 부분은 더 자세히 공부 후 추후 추가하도록 하겠다) 과거 let, const, import가 없었던 시절에는 변수의 스코프를 제한하고 모듈화를 하기 위해 사용하기도 하였다.
문과 식: https://poiemaweb.com/js-syntax-basics
과거 IIFE 사용예시: https://zzossig.io/posts/javascript/what_is_iife/