본문 바로가기
Javascript

클로저(closure)와 즉시실행함수(IIFE)

by jewook3617 2020. 1. 26.

Node.js를 이용해 웹사이트를 만들어보긴 했지만 자바스크립트에 대해 제대로 알고있지 않은 것 같아 최근에 O'REILLY에서 나온 '러닝 자바스크립트' 책을 보면서 공부를 시작했습니다. 클로저라는 말에 대해서 들어본 적은 있었지만 무슨 개념인 알지 못하고 있었습니다. 이번엔 제대로 알게 된 클로저와 재미있는 함수인 즉시실행함수에 대해서 정리해보려고 합니다.

Javascript의 정적 스코프

먼저 javascript의 스코프에 대해서 알 필요가 있습니다. javascript의 스코프는 정적 스코프를 따릅니다. 정적 스코프를 영어로 하면 lexical scope입니다. 즉, 프로그램이 실행되기 전에 코드상태에서 스코프가 정해지기 때문에 함수 선언 시에 함수의 스코프가 정해진다는 뜻입니다. 글로 풀어쓰면 무슨 말인지 감이 잘 안 잡히지만 아래 코드를 보면 바로 이해가 됩니다. 

const x=3;

function f(){
    console.log(x);
    console.log(y);
}

{ //새로운 블록 스코프를 만들기 위한 블록
    const y=2;
    f();
}

변수 y가 블록 안에 선언되어 있기 때문에 해당 블록 안에서만 유효합니다. 함수 f()는 블록 안에서 실행되기 때문에 변수 y에 접근해 3과 2가 출력될 것이라고 예상하기 쉽습니다. 하지만 javascript는 정적 스코프이기 때문에 함수 f()가 선언된 위치를 기반으로 스코프를 결정합니다. 따라서 함수 f() 선언 당시에 y는 존재하지 않는 변수이므로 에러가 발생합니다.

클로저 (closure)

책에서 클로저를 다음처럼 정의합니다.

함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 경우가 많은데 이런 것을 보통 클로저라고 한다.

이게 무슨 말인지 코드를 보면 감히 잡힙니다.

let globalFunc;

{
    let blockVar='a';
    globalFunc=function(){
        console.log(blockVar);
    }
}

globalFunc();     // 출력값 : "a"

globalFunc 변수는 전역으로 선언만 하고 블록 스코프 내에 있는 blockVar 변수에 접근하기 위해 globalFunc() 함수를 블록 내에 선언하였습니다. javascript는 정적 스코프를 따르기 때문에 블록 내에 선언된 globalFunc() 함수는 blockVar 변수에 접근이 가능합니다. 이렇게 되면 전역 스코프에 있는 globalFunc() 함수가 블록 스코프 내에 있는 변수에 접근할 수 있게 되는 것입니다. 

클로저로 인한 효과는 그 뿐만이 아닙니다. 전역 스코프에 있는 globalFunc() 함수가 블록 스코프 내에 있는 blockVar 변수를 참조하고 있기 때문에 blockVar 변수는 계속 살아있어야합니다. 즉, 블록이 끝이 나도 해당 스코프가 계속 유지됩니다. 그래서 블록 밖에서 globalFunc() 함수를 실행했을 때 blockVar 변수가 계속 살아있어 "a"가 출력되는 것입니다.

클로저로 인한 효과를 정리해보면 다음과 같다.

  1. 일반적으로 접근할 수 없는 스코프에 있는 data에 접근할 수 있다.
  2. 스코프를 오래 유지하도록 할 수 있다.

즉시실행함수 (IIFE)

공부를 하다가 즉시실행함수라는 신기한 함수를 발견했습니다. 아래 코드와 같이 즉시실행함수는 함수표현식으로 변수에 선언 및 할당됨과 동시에 실행이 되는 함수입니다. 

let f= (function(){
    console.log("나는 선언과 동시에 실행됨");
    return
})();

f; // 출력값 : "나는 선언과 동시에 실행됨" 

일반적인 함수표현식을    ->    function(){ ... }

괄호로 감싸고    ->    ( function(){ ... } )

함수 실행을 뜻하는 (); 를 뒤에 추가하여    ->    ( function(){ ... } )();

선언과 동시에 실행을 시키는 구조입니다. 즉시실행함수를 이용하면 () 없이 변수명만으로 함수를 실행시킬 수 있습니다.

즉시실행함수와 클로저를 결합하면 재미있는 일이 생깁니다.

const f = (function f1() {
    let count = 0;
    return function f2() {
        console.log(`나는 ${++count}번 호출됨.`);
        return
    }
})();

f();   // 출력값 : "나는 1번 호출됨."
f();   // 출력값 : "나는 2번 호출됨."

f의 리턴값으로 선언된 f2 함수가 count 변수를 참조해 클로저를 만들었기 때문에 f1 함수 내의 스코프가 계속 유지됩니다. 하지만 count 변수의 값이 계속 누적되고 있습니다. 일반적으로 함수 내에 선언되어 있는 지역변수는 함수가 실행될 때 다시 선언 및 할당이 이뤄진다. 즉시실행함수가 아닌 일반적인 함수표현식으로 실행하면 결과는 아래와 같습니다.

const f = function f1() {
    let count = 0;
    return function f2() {
        console.log(`나는 ${++count}번 호출됨.`);
        return
    }
}

f()(); // 출력값 : "나는 1번 호출됨."
f()(); // 출력값 : "나는 1번 호출됨."

f() 함수가 실행될 때마다 count 변수가 0으로 초기화 되기 때문에 f()() 함수를 연속으로 실행해도 결과값은 1로 같습니다.

왜 이런 결과가 나오는지 궁금해서 count 선언부분 밑에 출력값 하나를 추가해봤습니다.

const f = (function f1() {
    let count = 0;
    console.log("ㅋㅋ");
    return function f2() {
        console.log(`나는 ${++count}번 호출됨.`);
        return
    }
})();

f();
f(); 

이 코드를 실행하면 결과는 다음과 같다.

ㅋㅋ
나는 1번 호출됨.
나는 2번 호출됨.

즉시실행함수도 함수표현식이기 때문에 변수 f에는 함수가 실행의 결과값인 f2 함수가 저장됩니다. f1 함수는 선언과 동시에 딱 한번 실행이 되고 이후에 f() 함수를 실행했을 때는 f2 함수만 실행이 되는 것입니다. 하지만 f2 함수가 클로저를 만들어 count 변수가 계속 살아있기 때문에 값이 계속 누적됩니다. 즉, f() 함수만 접근할 수 있는 스코프가 생긴 것입니다.

아래와 같이 블록 스코프를 사용하면 똑같은 기능을 구현할 수 있지만 그래도 알아두면 좋을 것 같습니다.

let f;

{
    let count = 0;
    f = function() {
        console.log(`나는 ${++count}번 호출됨.`);
        return
    }
}

f();    // 출력값 : "나는 1번 호출됨."
f();    // 출력값 : "나는 2번 호출됨."

 

이 글에 쓰인 코드들은 O'REILLY의 '러닝 자바스크립트' 책에 있는 샘플코드 입니다.