JS의 scope 차이에 관한 재미있는 문제 해결

adultlee·2023년 7월 25일
50

이 글을 함께 활동한 부스트캠퍼이신 대현님, 경미님, 우찬님, 동혁님께 바칩니다!!

😂 어쩌다 작성하게 된거야?!

: (대현님이 공유해주신 문제를 보고)
: 대현님 이거 문제 너무 좋은거 같아요! 혹시 이벤트루프 관련 문제 맞죠?!
(갓)대현님 : 아 네, 성인님!! 감사합니다 ㅎㅎ 맞아요! 이벤트루프에 관한 문제에요!!
: 역시 대단하시네요! 혹시 해설을 부탁드려도 될까요?
(갓)대현님 : 제가 해설을 해봐도 좋지만, 성인님께서 한번 정리해 공유해주시면 어떨까요?!😚
: (?!!??!!)

살짝의 각색이 들어갔지만... 아무튼 이런 느낌으로 제가 해설을 작성해보게 되었습니다😂

😆 잠시 멈추고 문제를 읽어봐요!

단순하게 보일 수 있으나, for문안에 setTimeout이 들어가는것을 보니 조금은 다르다고 생각이 듭니다!

setTimeout을 생각해보면 입력받은 두번째 인자만큼의 시간이 지났을때, 첫번째 인자의 함수를 실행시키는 것인데...
그렇다면, 과연 for문을 다 기다린 후, 해당 동작이 발생하는 걸까요?

정답은.....


어라라...?

제가 예상한 정답이 나오지는 않았던것 같습니다.
첫번째 for문과 두번째 for문 모두 동일한 정답이 나올것이라 생각했거든요!
물론 1초씩 지나가면 출력이 될꺼라는 생각은 안했지만... 다 똑같이 3이 나와버리다니?
조금 놀라운 일이었습니다.

🤔 문제는 뭐였을까?

이 문제에서 다루면 좋은 주제는 크게 두가지가 있습니다.
1. setTimeout이 JS의 webapi에 속하여 비동기 처리가 된다.
2. setTimeout의 콜백함수가 실행될 때, 이미 i는 3이었어!
2-1. 그렇다면 순서대로 (1,2,3) 이 출력되기 위해서는 어떻게 코드를 수정할 수 있을까?

그렇다면 문제를 찾았으니 하나씩 알아보도록 하겠습니다!

1. setTimeout이 브라우저의 webapi에 속하여 비동기 처리가 된다.

JS는 싱글스레드 언어 입니다.
싱글 스레드 언어라는 말은, 이해하기 쉽게 말해보자면

한순간에 하나의 일만 수행한다. (두 가지 일을 동시에 수행하지 못한다.)

한 순간에 하나의 일만 수행할 수 있다는 말은 어떤 의미를 가지고 있을까요?

setTimeout(() => {
  console.log("1초가 지났습니다"); // 절대 출력되지 않음
}, 1000);

while (true) {
  console.log("무한 반복중인 세상"); 
}

console.log("세상이 끝났어!!"); // 마찬가지로 절대 출력되지 않습니다. 세상은 절대 끝나지 않지.... 


사진에서 보실 수 있듯, 무한하게 반복중인 세상만이 출력될 뿐, setTimeout에 있는 console 은 더 이상 출력되 지 않는것을 알 수 있습니다.
이로서, JS가 멀티스레드가 아닌, 한 순간에 하나의 일만 진행하고 있는것을 알 수 있습니다. (시간이 지나 출력해주는 일을 할 스레드가 없음)
그렇다면 setTimeout 저 친구는 어디서 뭐하고 있을까요?!

사진에서 확인할 수 있듯, JS runtime engine 내부의 stack에서 webapi에 보관되고, 테스크 큐(callback queue)에 저장되는 것을 확인할 수 있습니다.

eventLoop가 중간에 있는데, event Loop는 JS runtime engine의 stack이 비워져야만
테스크 큐에 저장된 콜백함수를 stack으로 넘겨주는 역할을 수행합니다.

여기서 중요한건 stack이 비워져야 한다는것인데요,
바로 위의 코드에서는 while문으로 인해 stack이 절대 비워지지 않으니
setTimeout으로 넘겨주는 콜백함수는 영원히 실행될 수 없습니다.

-> 알게 된것

setTimeout 함수는 브라우저의 webApis에 속하여 선언될 당시,
콜백함수를 넘겨주고 JS 내부의 stack이 비워져야만! 실행이 된다는것입니다

2. setTimeout의 콜백함수가 실행될 때, 이미 i는 3이었어!

// 까먹을 수도 잇으니 다시 가져온 우리의 문제!
for(var i=0; i<3; ++i){
  setTimeout(() => console.log(i), 1000);
}

첫 번째 반복문이 실행되고 setTimeout 함수가 세 번 호출됩니다.
하지만 각 콜백 함수는 1초(1000밀리초) 후에 실행되도록 예약되므로 반복문이 끝난 후에 콜백 함수가 실행됩니다.

반복문은 매우 빠르게 실행되므로 1초 후에 첫 번째 반복문과 두 번째 반복문이 모두 이미 종료되었습니다.

setTimeout 함수의 콜백 함수들이 실행될 때 i의 값은 이미 3이 됩니다.

2-1. 그렇다면 순서대로 (1,2,3) 이 출력되기 위해서는 어떻게 코드를 수정할 수 있을까? (with Closure)

for (var i = 0; i < 3; ++i) {
  (function (index) { // 즉시 실행 함수(IIFE)를 사용하여 클로저 생성
    setTimeout(() => console.log(index), 1000); // 1초(1000ms) 후에 콜백 함수 실행
  })(i); // 현재 반복문의 i 값을 즉시 실행 함수에 전달하여 클로저 생성
}

for (var i = 0; i < 3; ++i) {
  (function (index) {
    setTimeout(() => console.log(index), 1000);
  })(i);
}

이 문제를 해결하기 위해서는 각 콜백 함수가 반복될 때마다 i의 값이 캡처되도록 하여 클로저를 생성해야 합니다!
예를 들어 let 키워드를 사용하여 블록 스코프 변수를 만들거나, 함수를 사용하여 클로저를 생성할 수 있습니다.

반복문 안에서 즉시 실행 함수 (IIFE)를 사용함으로써 새로운 스코프를 생성하고, 이 안에서 index라는 매개변수를 통해 현재 반복문의 i 값을 전달합니다. index는 IIFE의 매개변수로서 해당 반복에서만 유효한 지역 변수가 되고, 클로저를 형성하게 됩니다.

setTimeout 함수는 각 반복에서 index의 값을 1초(1000ms) 후에 출력하는 콜백 함수를 예약합니다. 따라서 첫 번째 반복문에서는 i 값이 0, 1, 2가 되어 순서대로 출력되며, 두 번째 반복문도 마찬가지로 0, 1, 2가 순서대로 출력됩니다.

결과적으로, 이러한 클로저를 사용하여 각 반복에서 올바른 i 값을 캡처하게 되므로, 기대한 대로 0, 1, 2가 2번 출력되는 것을 확인할 수 있습니다.

하지만 이게 좋은 방법일까... 더 좋은 방법은 있을꺼야...

2-1. 그렇다면 순서대로 (1,2,3) 이 출력되기 위해서는 어떻게 코드를 수정할 수 있을까? (with Block Scope)

우린 잊고 있었던지도 몰라요!
사실 우리가 원하는 출력(0, 1, 2) 를 만들어 내는 방법은 바로 let을 사용하여 블록 스코프(block scope)를 사용하면 된다는 것이죠!!

이 코드에서는 변수 i를 let 키워드로 선언하여 for 루프의 범위를 블록 스코프(block scope)로 만들었습니다. let으로 선언된 변수는 블록 범위를 갖기 때문에, for 루프의 각 반복마다 변수 i가 새로 생성됩니다. 이로 인해 setTimeout 콜백 함수는 각각 다른 블록 범위의 i를 가지고 실행됩니다.

따라서 각각의 setTimeout 콜백 함수에서는 해당 블록 범위의 i 값이 출력됩니다. for 루프가 0부터 2까지 순회하면서 setTimeout 함수가 실행되므로, 1초 간격으로 0, 1, 2가 출력됩니다.

🔥결론 (Var 는 왜! 않되)

var 변수는 함수 스코프(function scope)를 가집니다.
따라서 내가 사용할 위치 보다 더 높은 위치의 var 변수는 해당 스크립트 파일 전체에서 접근이 가능합니다.

즉, 함수 외부에서 선언한 var 변수는 해당 스크립트 파일의 최상위 영역인 전역 변수 취급을 받습니다. 따라서 우리의 문제 에서는 콜백함수가 돌아올 시점에 값의 변화가 모두 이루어져 있었으며, 해당 값을 반환했기 때문이죠.

그렇지만! ES6 이후 부터는 let과 const 키워드가 도입되었습니다. let과 const는 블록 스코프(block scope)를 가지며, 호이스팅 현상을 최소화하여 변수의 범위를 명확하게 관리할 수 있게 되었습니다. 따라서 let과 const를 사용하여 변수를 선언하는 것이 좋습니다.

let은 재할당 가능한 변수를, const는 재할당이 불가능한 상수를 선언할 때 사용합니다. 이렇게 변수 범위를 더 잘 제어하면 코드의 가독성과 유지보수성이 향상됩니다.

3줄 요약

  1. 문제의 요지는 var와 let의 scope차이로 인해서 발생함!
  2. setTimeout을 통해서 반환되는 시점은 call stack이 모두 비워져 있는 순간이다. (event loop 가 관리)
  3. 클로져를 통해서 문제 해결이 가능했었지만, ES6문법의 let , const를 사용하는 것이 코드 가독성과 유지보수에 유리하다.

이런 공부는 어떨까요?

아래는 제가 추천하는 이 글을 읽고 공부해보면 좋을 주제입니다. 제 바통을 받아 누군가가 작성해주시면 또 좋을것 같아요!

  1. callback 함수들의 우선순위 차이(태스크 큐와 마이크로 테스크 큐)에 따른 문제에 대해서 풀어보기
  2. ES6로 인해서 바뀌게 된 큰 변환점에 대해서 이해하기 (아마 대부분 ES6 문법 이후를 공부했지만, 오히려 그전 문법에 대해서 알아 보는건 어떨까요?)

15개의 댓글

comment-user-thumbnail
2023년 7월 25일

잘 읽었습니다. 좋은 정보 감사드립니다.

1개의 답글
comment-user-thumbnail
2023년 7월 27일

반복문에서 맨 처음 문제에 변수 선언문(var let)과 해설에서의 변수 선언문(var var)이 다른것 같아요

2개의 답글
comment-user-thumbnail
2023년 7월 27일

이 문제 재밋네요! 흥미로운게 for of문을 사용하면 var / let, const 별로 결과가 다릅니다.

for (var index of [0,1,2]) {
  setTimeout(() => console.log(index,'var'), 1000);
}

for (let index of [0,1,2]) {
  setTimeout(() => console.log(index,'let'), 1000);
}

for (const index of [0,1,2]) {
  setTimeout(() => console.log(index,'const'), 1000);
}

2"var" (x3)
0"let"
1"let"
2"let"
0"const"
1"const"
2"const"

var와 다르게 let, const는 블록 범위를 가져서 그런건가? 싶네요.
좋은 글 감사합니다!

2개의 답글
comment-user-thumbnail
2023년 7월 31일

To be honest, I don't think I'm suitable for such questions, so I can't recommend anything specific. The only way I can help you is to recommend reading an article about what is the difference between https://www.milople.com/blogs/. I think you need to read it if you want to choose a platform that would suit your e-commerce business.

1개의 답글
comment-user-thumbnail
2023년 8월 1일

잘봤습니다.

답글 달기
comment-user-thumbnail
2023년 8월 1일

잘 읽고 갑니다!

답글 달기
comment-user-thumbnail
2023년 8월 1일

var 와 let 의 차의점은 범위였네.
var는 함수 범위이므로 정의된 전체 함수에서 사용할 수 있고.
let은 블록 범위이므로 정의된 블록, 문 또는 식 내에서만 사용할 수 있으니깐

좋은 공부가 되었습니다.
감사합니다.

답글 달기
comment-user-thumbnail
2023년 8월 5일

dfdfddf1ㅂㅈ234

답글 달기