고전 문제를 통해 알아보는 자바스크립트 원리
포스트
취소

고전 문제를 통해 알아보는 자바스크립트 원리

시작하기 전에

“해당 포스팅은 자바스크립트의 원리를 재밌게 알아보기 위해 제가 생각한 설명이 포함되어 틀릴 수 있다는 점 알려드립니다. 나중에 댓글 기능이 생기면 댓글로 알려주셔도 정말 좋겠지만, 지금은 메일로 알려주시면 정말 감사하겠습니다.”

시작 지점

얼마 전 친구가 자바스크립트 원리에 대해 아냐고 물어본 적이 있다. 클로저, 호이스팅을 물어봤는데 저는 대답은 안하고 그거는 기본이라고 ‘자바스크립트의 3대 원리인 호이스팅, 클로저, 이벤트 루프가 있다.’라고만 대답했던 적이 있습니다.

그 친구는 자바(자바와 자바스크립트는 햄과 햄스터같이 정말 다릅니다!)를 쓰기 때문에 제가 설명 할 자신이 없어서 그랬던 것이기도 한데, 이참에 ‘자바스크립트에 대해 모르는 사람에게도 해당 문제를 쉽게 알려 줄 수 있는 방법이 없을까?’라는 생각에 인터넷을 서칭하던 중 옛날에 보았던 자바스크립트 고전 클로저 문제(?)를 다시 만나게 되었고, 이 문제를 설명하면서 자연스럽게 제가 생각하는 3대 원리를 접목시키는 방식으로 가는 것이 좋겠다는 생각이 들어 이 글을 작성합니다.

주의 해야할 점

이 게시글은 원리를 정석적으로 깊게 설명하는 것이 아니기 때문에 클로저, 호이스팅, 이벤트 루프의 원리를 수박 겉핥기로만 설명합니다.

문제 알아보기

먼저, 인터넷에 떠도는 자바스크립트의 망령인 고전 클로저 문제에 대해 한 번 보도록 하겠습니다.

1
2
3
4
5
6
7
8
9
function func() {
  for (var i = 0; i < 10; i++) {
    setTimeout(function () {
      console.log(i);
    }, 500);
  }
}

func();

저는 0.5초 뒤에 0 ~ 9까지 출력하는 함수를 만들고 싶었습니다.
그래서 위와 같이 코드를 작성하고 실행했더니 이게 왠걸? 10이 10번 출력되는 것이 아니겠습니까?
아니 이게 무슨 일까요? 무슨 문제가 생겨서 이렇게 동작하는 걸까요? 이는 자바스크립트의 원리에 대해 알지 쉽게 해결하지 못할 수도 있습니다.

왜 제가 생각했던대로 동작하지 않는지에 대해 먼저 말씀드리자면, 변수(var)의 스코프 문제와 비동기(setTimeout)가 합쳐져 환장의 조합이 탄생했기 때문입니다.

var로 선언한 변수의 특성과 비동기를 잠깐 짚고 넘어가자면 var로 선언했을 경우 재할당, 재선언이 가능해지고, 변수의 범위는 함수 스코프를 가지게됩니다.
비동기는 말이 어려울 수도 있기 때문에 풀어서 써보자면, “나 다른 일 하고 올테니깐 다음 라인 실행시켜 줘.”라고 할 수 있겠습니다.

제가 설명한 것 중에 굵은 글씨를 다시 봐주시기 바랍니다.
왜 이러한 현상이 발생했는지 또 이 문제를 어떻게 해결해야하는지는 굵은 글씨에 달려있습니다. 차근차근 가봅시다.

함수스코프

재선언은 말 그대로 다시 선언 할 수 있다는 것이고, 함수 스코프는 변수를 관리(편의상 관리라고 하겠습니다 너른 양해 부탁드립니다.)하는 곳이 함수가 되는 것입니다.

그런데 자바스크립트만 알고 있다면 ‘이게 왜?’라고 생각하실 수 있겠지만 제가 알고있는 언어 중 파이썬을 제외한 거의 대부분의 언어가 재선언이 불가능하고, 블록 스코프를 가지고 있습니다. 이렇기 때문에 우리가 해결하고자 하는 문제는 자바스크립트의 인기가 올라감에 따라 조롱의 대상이 되기도 했습니다.

함수스코프를 간단하게 코드로 봐봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
function func() {
  {
    var number = 1;
    console.log(number); // 1 출력
  }
  {
    console.log(number); // 1 출력
  }
  console.log(number); // 1 출력
}

func();

여기서는 설명의 편의상 스코프를 세세하게 나누어보았습니다.
한눈에 봐도 뭔가 이상함이 느껴지시나요? 바로 함수 스코프의 원리를 모른다면 이게 어떻게 동작하는지 이해가 안되실 수도 있습니다.
앞에서 설명드린대로 var의 경우는 함수에서 변수를 관리하기 때문에 {}이라는 블록 안에 선언을 하더라도 func함수에서 관리를 하게 되는 것입니다.
그래서 조금 이상해 보일지 몰라도 동작하게 되는 것이죠.

하지만 es2015(es6)부터 let과 const의 등장으로 블록스코프를 지원합니다. let과 const를 사용합시다.

비동기

비동기는 말씀드렸다시피 “나 대신 이것 좀 실행시켜 줘”입니다.
비동기는 깊게 들어가면 책 몇권이 나올정도로 심오한 기술이지만 우리는 초심자의 관점에서 아주 쉽게 알아봅시다.

1
2
3
4
5
console.log(1);
setTimeout(() => {
  console.log(2);
}, 100);
console.log(3);

setTimeout함수 또한 비동기 함수의 일종으로 일정 시간이 지난 후에 전달 받은 함수를 실행해달라는 의미입니다.
그럼 위의 코드는 어떻게 출력될 것 같나요? 1, 3, 2순서로 출력이 되겠죠. 정말 쉽죠?
그런데 이것만 봐서는 비동기의 원리를 반 쪽만 이해했다고 할 수 있습니다.

1
2
3
4
5
console.log(1);
setTimeout(() => {
  console.log(2);
}, 0);
console.log(3);

그렇다면 위와 같이 setTimeout의 호출 시간이 0인 경우에는 어떻게 실행될까요?
출력해보면 1, 3, 2가 똑같이 출력되는 것을 알 수 있습니다.
사실 자바스크립트의 비동기는 한 가지 원리가 더 있습니다. 바로 현재 실행이 가능 하더라도 실행 순서가 마지막으로 밀려난다는 것입니다.
정확하게 이렇게 동작하는 것은 아니지만,
너무 깊게 알면 어렵기 때문에 깊게 알고 싶으신 분들은 실행 컨텍스트와 이벤트 루프에 대해 알아보시기 바랍니다.

setTimout 이미지 추가예정

문제 해결 과정

자 이제 문제 해결을 위한 자바스크립트의 원리는 대충 알아봤습니다.
그럼 이 원리를 바탕으로 어떻게 하면 문제를 해결할 수 있을 지 생각해봅시다.
다시 코드를 봐보도록 하겠습니다.

1
2
3
4
5
6
7
8
9
function func() {
  for (var i = 0; i < 10; i++) {
    setTimeout(function () {
      console.log(i);
    }, 500);
  }
}

func();

우리는 분명 0 ~ 9를 출력하고 싶었는데 “왜 자바스크립트는 10을 출력했을까?”에 대한 답을 앞서 배운 2 가지의 특성을 통해 한 번 알아봅시다.

앞서 비동기를 사용하는 함수는 호출하게 되면 순위가 뒤로 밀린다고 했죠?
그렇기 때문에 setTimeout에 의해 호출 된 함수 즉, i를 출력하는 함수는 후 순위로 밀려나게 됩니다.
또한 i는 var로 선언된 변수이기 때문에 함수 스코프를 가집니다.
반복문을 풀어서 보면 더 쉽기 때문에 한 번 풀어봅시다.(편의를 위해 풀어쓴 것이지 실제로는 이렇게 동작하지 않습니다.)
반복문의 정확한 동작과정을 알고 싶으시면 반복문 동작 과정을 찾아보시기 바랍니다.

1
2
3
4
5
6
7
8
9
10
11
12
function func() {
  var i = 0;
  if (i < 10) {
    // 길이를 위해 화살표 함수로 변경 한 점 양해 바랍니다.
    setTimeout(() => console.log(i), 500);
  }
  if (i < 10) {
    var i = ++i;
    setTimeout(() => console.log(i), 500);
  }
  ...
}

이 코드를 작성하던 도중 한 가지 흥미로운 사실을 발견했습니다.
비동기 코드를 작성할 때(8번 째 줄) 전위 연산자와 후위 연산자에 따라 비동기 요청 값이 바뀌는 것을 확인했습니다.
해당 사항은 조금 찾아봐야겠지만 이 포스팅과는 크게 연관이 없기 때문에 궁금하신 분들은 찾아보시는 것도 재밌을 것 같습니다.

위의 코드를 실행하게 되면, console.log는 후 순위로 밀려나게 되고, i를 먼저 더합니다.
더한 뒤 또 setTimout을 만나 console.log는 후 순위로 밀려나가는 과정이 10번 반복됩니다.
이렇게 console.log는 10번 후 순위로 밀려나게 되고, 그 동안 i의 값은 10이 되어 10이 10번 출력되는 것입니다.

대충 비동기 이미지 추가 예정

문제 해결

이러한 원리를 알았다면, 이제 내가 원하는대로 0 ~ 9까지 출력할 수도 있겠죠?

첫 번째 방법

첫 번째 방법은 즉시실행함수(IIFE)를 이용하는 것이다.
IIFE를 이용하게 되면, 값을 함수에 전달함으로써 값을 기억하고 있게 만들 수 있다.

대충 스코프 이미지 추가 예정

1
2
3
4
5
6
7
8
9
function func() {
  for (var i = 0; i < 10; i++) {
    (function (i) {
      setTimeout(() => console.log(i), 500);
    })(i);
  }
}

func();

두 번째 방법

두 번째 방법으로는 let을 사용하여 블록 스코프를 사용하는 것입니다. var는 함수 스코프이기 때문에 생성된 비동기 함수가 전체적으로 영향을 받았다면,
let을 사용함으로써 블록 스코프로 만들어 내가 원하는 비동기 함수만 영향이 가게 만들 수 있습니다.

1
2
3
4
5
6
7
function func() {
  for (let i = 0; i < 10; i++) {
    setTimeout(() => console.log(i), 500);
  }
}

func();

마무리

이렇게 고전 클로저 문제를 해결했습니다. 자바스크립트와 한 발자국 더 가까워진 것 같네요.
그런데 함수 스코프를 블록 스코프로 잘 조작하는 것만으로도 해결이 가능했습니다.
그렇기 때문에 제 짧은 지식으로는 해당 문제가 왜 클로저 문제인지 정확하게 모르겠습니다.
찾아보지 않았기 때문에 몇 가지 추측을 해볼 수 있을 것 같은데요.

  1. 클로저는 내가 호출된 시점에 스코프를 기억하는 것이다.
    그렇기 때문에 당연히 for문을 돌 때마다 기억하고 있을 줄 알았던 i를 기억하지 못했다(원리에 대한 이해 미숙).
  2. 해당 문제는 let이 나오기 이전에 발생했던 고전 문제로 알고있다(최소 10년).
    그렇기 때문에 당연히 IIFE를 통해 해결할 수 밖에 없었고, 이는 스코프를 한 단계 더 생성한 클로저 환경을 만들었기 때문에 클로저 문제라고 할 수 있다(해당 문제는 더 이상 IIFE로 해결하지 않음).

제 개인적인 의견으로는 이제는 충분히 블록 스코프로 만들어 해결이 가능하기 때문에 더 이상 해당 문제는 클로저 문제라고 보기는 어려워 보입니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.