성장, 그리고 노력

부족하더라도 어제보다 더 잘해지자. 노력은 절대 배신하지 않는다.

Javascript

1부 - 자바스크립트 함수 표현식, IIFE 그리고 비동기

제이콥(JACOB) 2020. 1. 27. 08:27

함수 표현식

함수 표현식(function expression)은 값이 될 수 있고, 함수 역시 값이 될 수 있다. 이는 함수를 선언하는 한 가지 방법일 뿐이며, 그 함수가 익명이 될 수도 있을 뿐이다. 함수 표현식은 식별자에 할당할 수도 있고 즉시 호출(IIFE)할 수도 있다.

 

함수 표현식은 함수 이름을 생략할 수 있다는 점을 제외하면 함수 선언과 문법적으로 완전히 같다.

const f = function() {
  // ...
};

위 예제는 결과적으로 함수 선언과 동등하다. 식별자 f가 이 함수를 가리키며, 일반적인 함수 선언과 마찬가지로 f()로 이 함수를 호출할 수 있다. 차이점은 먼저 함수 표현식으로 익명 함수를 만들고 그 함수를 변수에 할당했다는 거다.

 

 익명 함수는 어디든지 쓸 수 있다. 다른 함수나 메서드의 매개변수로 넘길 수도 있고, 객체의 함수 프로퍼티가 될 수도 있다. 

 

즉시 호출 함수 표현식(IIFE)

(function() {
  // IIFE body
})();

 IIFE(Immediately Invoked Function Expression)는 함수 표현식으로 익명 함수를 만들고 그 함수를 즉시 호출한다. IIFE의 장점은 내부에 있는 것들이 모두 자신만의 스코프를 가지지만, IIFE 자체는 함수이므로 그 스코프 밖으로 무언가를 내보낼 수 있다는 거다.

 변수 secret은 IIFE의 스코프 안에서 안전하게 보호되며 외부에서 접근할 수 없다. IIFE는 함수이므로 무엇이든 반환할 수 있으며, 배열이나 객체, 함수를 반환할 수도 있다. 그리고 아래와 같이도 사용할 수 있다.

 

 변수 count는 IIFE 안에 안전하게 보관되어 있으므로 손댈 방법이 없다. countFn은 자신이 몇 번 호출됐는지 항상 정확하게 알고 있다. 

 

 물론 ES6에서 블록 스코프 변수를 도입하면서 IIFE가 필요한 경우가 줄어들긴 했지만, 여전히 널리 쓰인다. 클로저를 만들고 클로저에서 무언가 반환받을 때에는 유용하게 쓸 수 있다.


함수 스코프와 호이스팅

둘리도 아는 호이스팅, 얼마나 대중적이면 '스팅'을 생략했을까?

 이 글에 메인인 IIFE와 비동기적 코드에 대해 알아보기 전에 함수 스코프(function scope)호이스팅(hoisting)에 대해 먼저 간단히 짚고 넘어가자.

 

ES6에서 let을 도입하기 전에는 var를 써서 변수를 선언했고 이렇게 선언된 변수들은 함수 스코프라 불리는 스코프를 가졌다(var로 선언한 전역 변수는 명시적인 함수 안에 있지는 않지만 함수 스코프와 똑같이 동작한다).

 

let vs var

 let으로 변수를 선언하면 그 변수를 선언하기 전에는 존재하지 않는다. 

 var로 선언한 변수는 현재 스코프 안이라면 어디서든 사용할 수 있으며 심지어 선언하기 전에도 사용할 수 있다. 

a; // undefined
var a = 1;
a; // 1

 let과 달리 에러를 일으키지 않는다. 왜냐하면 var로 선언한 변수는 끌어올린다는 뜻의 호이스팅(hoisting)이라는 메커니즘을 따른다. 자바스크립트는 함수나 전역 스코프 전체를 살펴보고 var로 선언한 변수를 맨 위로 끌어올린다. 여기서 중요한 점은 선언만 끌어올려진다는 것이며, 할당은 끌어올려지지 않는다. 

 

 바로 위에 있는 예제를 자바스크립트는 아래와 같이 해석한다.

var a; // 선언만 끌어올린다.
a; // undefined
a = 1;
a; //1

 

조금 더 복잡한 예제로 살펴보자.

 var를 통해 같은 변수를 여러 번 정의하더라도 무시한다. 이 예제에서 블록 안에서 두 번째 var문을 썼지만 변수 x는 하나뿐이다(이런 혼란스러운 현상을 막기 위해 let이 생긴 거다).

 

 여기서 우리는 한 가지를 더 생각해야 한다. 이러한 호이스팅은 변수뿐만 아니라, 함수 선언 역시 끌어올린다는 점이다.

f(); // 끌어 올려져라!
function f() {
  console.log("끌어 올려져라!");
}

 

물론 변수에 할당된 함수 표현식은 끌어올려지지 않는다(변수 스코프 규칙과 같다). 

f(); // ReferenceError: f is not defined
let f = function() {
  console.log("끌어 올려져라!");
};

IIFE와 비동기적 코드

 이제 위에서 기초 지식이 습득되었으니, 먼저 IIFE로 비동기적 코드를 처리하는 예제부터 보자.

var i;
for (i = 5; i >= 0; i--) {
  setTimeout(function() {
    console.log(i === 0 ? "go" : i);
  }, (5 - i) * 1000);
}

 5초에서 시작하고 카운트다운이 끝나면 "go"를 표시해 주는 타이머 예제이다. 여기서 let 대신 var를 쓴 이유는 IIFE가 중요하던 시점으로 돌아가서 왜 중요했는지 이해하기 위해서이다. 

 

 위 예제를 실행하면 콘솔에는 어떻게 출력이 될까?

만약 5, 4, 3, 2, 1, go가 출력될 거라 예상했다면, 틀렸다. 정답은 -1만 여섯 번 출력된다.

출처: https://programmingsoup.com/article/asynchronous-javascript

 왜 이런 결과가 나왔을까? setTimeout에 전달된 함수가 루프 안에서 실행되지 않고 루프가 종료된 뒤에 실행됐기 때문이다. 따라서 루프의 i는 5에서 시작해 -1로 끝나기 전까지 콜백 함수는 전혀 호출되지 않으며 i가 -1이 되는 시점에서 콜백 함수가 실행된다.

cf) setTimeout은 왜 마지막에 실행될까?

 자바스크립트에 정의되있지 않는 setTimeout은 WebAPIs에 속한다. WebAPIs는 setTimeout, ajax(XMLHttpRequest, Dom(document) 등을 지원한다. 

 WebAPI 결과는 콜백큐(Callback Queue)에 쌓인다. 그리고 이벤트 루프(Event Loop)는 콜스택(Call Stack)과 콜백큐를 관찰하는 역할을 하며, 콜스택이 비어있으면 콜백 큐의 첫번째 콜백을 콜스택에 쌓는다.

 그리고 사실 setTimeout은 동작하는 최소 시간을 보장하는 것이지, 얼마 후에 동작하겠다는 의미는 아니다. 개발자가 아무리 5초로 지정해 놨더라도, 5초 후에 콜스택이 비어있다면 setTimeout의 콜백이 실행된다.

 그래서 setTimeout(callback, 0)을 사용하더라도 즉시 실행이 되지 않고 콜스택이 비어있을 때까지 기다린 후 호출된다.

 물론 let을 사용해 블록 스코프를 만들면 이 문제는 해결되지만, 비동기적 프로그래밍에 익숙하지 않다면 이 예제를 정확히 이해해야 한다. 위 문제를 예전 방식부터 let을 사용한 방식까지 적용하여 차근차근 해결해 보자.

 

방법 1: 함수 하나 더 쓰기

function loopBody(x) {
  setTimeout(function() {
    console.log(x === 0 ? "go" : x);
  }, (5 - x) * 1000);
}

var i;
for (i = 5; i >= 0; i--) {
  loopBody(i);
}

 루프의 각 단계에서 loopBody 함수가 호출된다. 그러면 스코프는 일곱 개가 만들어졌고 변수도 일곱 개 만들어졌다(하나는 외부 스코프, 나머지 여섯 개는 loopBody를 호출할 때마다). 하지만 이 방법은 일회용 함수에 이름을 붙여줘야 하는 번거로움이 있다. 방법 2를 살펴보자.

 

방법 2: IIFE 적용

var i;
for (i = 5; i >= 0; i--) {
  (function(x) {
    setTimeout(function() {
      console.log(x === 0 ? "go" : x);
    }, (5 - x) * 1000);
  })(i);
}

괄호가 많아진 게 흠이지만, 익명 함수와 IIFE를 이용하여 코드를 변경하였다. 하지만 사실 방법 1의 코드에서 이름 있는 함수를 익명 함수로 교체한 거 외에 스코프 때문에 함수를 똑같이 새로 만들고 있다. 그렇다면 마지막 방법 3을 살펴보자.

 

방법 3: 블록 스코프 변수 사용(let)

for (let i = 5; i >= 0; i--) {
  setTimeout(function() {
    console.log(i === 0 ? "go" : i);
  }, (5 - i) * 1000);
}

for 루프 안에 let 키워드를 사용하면, 자바스크립트는 루프의 단계마다 변수 i의 복사본을 새로 만들며 setTimeout에 전달한 함수가 실행될 때는 독립 스코프에서 변수를 받게 된다.

 


See Also

 

2부 - 자바스크립트 비동기적 프로그래밍(콜백, 프라미스)

동기적 프로그래밍 동기적 프로그래밍 모델에서는 일(task)은 한 번에 하나씩 일어난다. 그러다 보니 오랫동안 실행되는 동작을 수행하는 함수를 호출했다면, 그 함수가 종료될 때까지 그 프로그램은 중지되게 된..

code-masterjung.tistory.com

 

반응형