읽은 책들/모던 리액트 DeepDive

[모던 리액트 Deep Dive] 1장. 리액트 개발을 위해 꼭 알아야 할 자바스크립트

today is ? 2024. 11. 4. 16:01

1.5 이벤트 루프와 비동기 통신의 이해

더보기

자바스크립트는 한 번에 하나의 작업만 동기 방식으로 처리

 

동기란?

◾ 직렬 방식으로 작업을 처리하는 것

 이 요청이 시작된 이후에는 무조건 응답을 받은 이후에야 비로소 다른 작업을 처리

 그 동안 다른 모든 작업 대기

 

비동기란?

직렬 방식이 아니라 병렬 방식으로 작업을 처리하는 것

요청을 시작한 후 이 응답이 오건 말건 상관없이 다음 작업이 이루어지며, 한 번에 여러 작업이 실행될 수 있음

 

자바스크립트는 싱글 스레드에서 동기 방식으로 작동, 싱글 스레드 기반의 자바스크립트에서도 비동기 작업이 이루어짐

예) 사용자가 검색어를 입력해 검색을 위한 네트워크 요청이 발생하는 순간에도 사용자는 다른 작업 처리 가능

1.5.1 싱글 스레드 자바스크립트

프로세스

  • 프로그램을 구동해 프로그램의 상태가 메모리상에서 실행되는 작업 단위
  • 하나의 프로그램 실행은 하나의 프로세스를 가지고 그 프로세스 내부에서 모든 작업이 처리되는 것을 의미

스레드

  • 하나의 프로세스에서는 여러 개의 스레드를 만들 수 있고, 스레드끼리는 메모리를 공유할 수 있어 여러 가지 작업을 동시에 수행
  • 프로세스 내부에서 여러 개의 스레드를 활용하면서 동시 다발적인 작업 처리 가능
💡 자바스크립트는 왜 싱글 스레드로 설계됐을까?
◾ 스레드는 하나의 프로세스에서 동시에 서로 같은 자원에 접근할 수 있는데 동시에 여러 작업을 수행하다 보면 같은 자원에 대해 여러 번 수정하는 등 동시성 문제가 발생할 수 있어 이에 대한 처리 필요
◾ 각각 격리돼 있는 프로세스와는 다르게, 하나의 스레드가 문제가 생기면 같은 자원을 공유하는 다른 스레드에도 동시에 문제가 일어날 수 있음
◾ 멀티 스레딩을 지원해서 동시에 여러 스레드가 DOM을 조작할 수 있다면 타이밍 이슈 발생 및 브라우저의 DOM 표시에 큰 문제를 야기

 

자바스크립트의 특징

  • 'Run-to-completion', 자바 스크립트의 모든 코드는 '동기식'으로 한 번에 하나씩 순차적으로 처리

비동기?

  • 동시에 일어나지 않는 것
  • 요청한 즉시 결과가 주어지지 않을 수도 있고, 응답이 언제 올지도 알 수 없음
  • 비동기 함수를 선언할 때 : async
console.log(1)

setTimeout(() => {
	console.log(2)
}, 0)

setTimeout(() => {
	console.log(3)
}, 100)

console.log(4)

 

🔖 해딩 콘솔 출력의 순서 : 1, 4, 2, 3 / 하지만 1, 2 (0.1초 후에) 3, 4로 출력되어야 정상
🔖 동기식으로 작동하는 자바스크립트 세상에서 어떻게 이런 비동기 코드를 처리할 수 있는 것일까? 이벤트 루프

 

1.5.2 이벤트 루프란?

  • 자바스크립트 런타임 외부에서 자바스크립트의 비동기 실행을 돕기 위해 만들어진 장치

호출 스택과 이벤트 루프

  • 호출 스택 : 자바스크립트에서 수행해야 할 코드나 함수를 순차적으로 담아두는 스택
function bar() {
	console.log('bar')
}

function baz() {
	console.log('bax')
}

function foo() {
	console.log('foo')
    bar()
    bax()
}

foo()
1. foo()가 호출 스택에 먼저 들어간다.
2. foo() 내부에 console.log가 존재하므로 호출 스택에 들어간다.
3. 2의 실행이 완료된 이후에 다음 코드로 넘어간다. (아직 foo()는 존재)
4. bar()가 호출 스택에 들어간다.
5. bar() 내부에 console.log가 존재하므로 호출 스택에 들어간다.
6. 5의 실행이 완료된 이후에 다음 코드로 넘어간다.(아직 foo(), bar()는 존재)
7. 더 이상 bar()에 남은 것이 없으므로 호출 스택에서 제거된다. (아직 foo()는 존재)
8. baz()가 호출 스택에 들어간다.
9. baz() 내부에 console.log가 존재하므로 호출 스택에 들어간다.
10. 9의 실행이 완료된 이후에 다음 코드로 넘어간다. (아직 foo(), baz()는 존재)
11. 더 이상 baz()에 남은 것이 없으므로 호출 스택에서 제거된다. (아직 foo()는 존재)
12. 더 이상 foo()에 남은 것이 없으므로 호출 스택에서 제거된다.
13. 이제 호출 스택이 완전히 비워졌다.
  • 호출 스택이 비어 있는지 여부를 확인하는 것이 이벤트 루프
  • 이벤트 루프는 단순히 이벤트 루프만의 단일 스레드 내부에서 이 호출 스택 내부에 수행해야 할 작업이 있는지 확인하고, 수행해야 할 코드가 있다면 자바스크립트 엔진을 이용해 실행
  • '코드를 실행하는 것'과 '호출 스택이 비어있는지 확인하는 것' 모두가 단일 스레드에서 일어난다는 점

비동기 작업은 어떻게 실행?

function bar() {
	console.log('bar')
}

function baz() {
	console.log('baz')
}

function foo() {
	console.log('foo')
    setTimeout(bar(), 0) // setTimeout만 추가했다.
    baz()
}

foo()
1. foo() 가 호출 스택에 먼저 들어간다.
2. foo() 내부에 console.log가 존재하므로 호출 스택에 들어간다.
3. 2의 실행이 완료된 이후에 다음 코드로 넘어간다.(아직 foo()는 존재)
4. setTimeout(bar(), 0)이 호출 스택에 들어간다.
5. 4번의 대해 타이머 이벤트가 실행되어 태스크 큐로 들어가고, 그 대신 바로 스택에서 제거된다.
6. baz()가 호출 스택에 들어간다.
7. baz() 내부에 console.log가 존재하므로 호출 스택에 들어간다.
8. 7의 실행이 완료된 이후에 다음 코드로 넘어간다. (아직 foo(), baz()는 존재)
9. 더 이상 baz()에 남은 것이 없으므로 호출 스택에서 제거된다. (아직 foo()는 존재)
10. 더 이상 foo()에 남은 것이 없으므로 호출 스택에서 제거된다.
11. 이제 호출 스택이 완전히 비워졌다.
12. 이벤트 루프가 호출 스택이 비워져 있다는 것을 확인했다. 그리고 태스크 큐를 확인하니 4번에 들어갔던 내용이 있어 bar()를 호출 스택에 들여보낸다.
13. bar() 내부에 console.log가 존재하므로 호출 스택에 들어간다.
14. 13의 실행이 끝내고, 다음 코드로 넘어간다. (아직 bar() 존재)
15. 더 이상 bar()에 남은 것이 없으므로 호출 스택에서 제거된다.

🔖 setTimeout(() => {}), 0)이 정확하게 0초 뒤에 실행된다는 것을 보장하지 못함

 

태스크 큐

  • 실행해야 할 태스크의 집합
  • 이벤트 루프는 태스크 큐를 한 개 이상 가짐
  • 자료 구조의 큐가 아니고 set 형태를 띠고 있음
    • 선택된 큐 중에서 실행 가능한 가장 오래된 태스크를 가져와야 하기 때문
    • 큐는 무조건 앞에 있는 것을 FIFO 형식으로 꺼내와야 하지만 태스크 큐는 그렇지 않음
  • '실행해야 할 태스크'라는 것은 비동기 함수의 콜백 함수나 이벤트 핸들러 등을 의미

이벤트 루프의 역할

  • 호출 스택에 실행 중인 코드가 있는지, 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인하는 역할
  • 호출 스택이 비었다면 태스크 큐에 대기 중인 작업이 있는지 확인하고, 이 작업을 실행 가능한 오래된 것부터 순차적으로 꺼내와서 실행 (태스크 큐가 빌 때까지)
💡 비동기 함수는 누가 수행? / n초 뒤에 setTimeout을 요청하는 작업은 누가 처리? / fetch를 기반으로 실행되는 네트워크 요청은 누가 보내고 응답을 받을 것인가?
  • 모두 자바스킯트 코드가 동기식으로 실행되는 메인 스레드가 아닌 태스크 큐가 할당되는 별도의 스레드에서 수행
  • 별도의 스레드에서 태스크 큐에 작업을 할당해 처리하는 것은 브라우저나 Node.js의 역할
  • 즉, 자바스크립트 코드 실행은 싱글 스레드에서 이루어지지만 외부 Web API등은 모두 자바스크립트 코드 외부에서 실행되고 콜백이 태스크 큐로 들어가는 것

1.5.3 태스크 큐와 마이크로 태스크 큐

  • 이벤트 루프는 하나의 마이크로 태스크 큐를 갖고 있는데, 기존의 태스크 큐와는 다른 태스크를 처리
  • 대표적 : Promise
  • 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 가짐
    • 마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐의 실행은 뒤로 미루어짐
💡 렌더링은 언제 실행될까?
◾ 마이크로 태스크 큐를 실행하고, 이 마이크로 태스크 큐를 실행한 뒤에 렌더링이 일어남
◾ 마이크로 태스크 큐 작업이 끝날 때마다 한 번씩 렌더링할 기회를 얻게 됨
◾ 브라우저에 렌더링하는 작업은 마이크로 태스크 큐와 태스크 큐 사이에서 일어남
◾ 특정 렌더링이 자바스크립트 내 무거운 작업과 연관이 있다면 이를 어떤 식으로 분리해서 사용자에게 좋은 애플리케이션 경험을 제공해 줄지 고민해 보아야 함

 

1.5.4 정리

  • 자바스크립트 코드를 실행하는 것 자체는 싱글 스레드로 이루어져서 비동기를 처리하기 어렵지만 자바스크립트 코드를 실행하는 것 이외에 태스크 큐, 이벤트 루프, 마이크로 태스크 큐, 브라우저/Node.js API 등이 적절한 생태계를 이루고 있기 때문에 싱글 스레드로는 불가능한 비동기 이벤트 처리가 가능해진 것