비동기와 리액트...(2)
이전 글: https://velog.io/@minkwan/TILReact-20241209
이전 글에서 React.lazy()와 Suspense의 개념에 대해 얕게 다룬 바 있다. 두 개념에 대해 알아가는 과정에서 code splitting이라는 키워드를 마주하게 되었고, code splitting은 결국 번들링으로 이어졌다.
"무엇을 번들링 하는가?"라는 질문이 시작일 수밖에 없다. 결론부터 말하자면 Module을 번들링 한다.
(이 글에서는 구체적인 툴에 대한 사용법을 다루기보다는, Javascript와 React에 관한 거시적인 흐름을 설명하고자 한다.)
초기 Javascript 파일 관리 방식에는 문제가 많았다. Javascript가 초기에 파일을 관리하는 방식은 다음과 같았다.
하나의 파일에 모두 작성한다.<script> 태그를 사용하여 여러 JavaScript 파일을 로드한다. 이 두 방식은 다음 세가지 문제점으로 이어졌다.
의존성 관리의 문제: 어떤 코드가 먼저 수행되어야 하는지를 수동으로 지정해야 했다.Global Namespace 오염 문제: 모든 코드가 전역에서 실행되어 변수 충돌이 발생했다.성능 저하 문제: 파일이 많을 때 브라우저가 일일이 요청하여 로딩 시간이 길어졌다.
위와 같은 문제를 해결하기 위해 모듈화 개념이 도입되었다. 모듈은 재사용 가능한 독립적인 코드 조각을 의미하며, JavaScript에서 모듈은 파일 단위로 구성되기에 파일 자체가 모듈로 간주된다.
JavaScript는 1995년 브라우저에서 실행되는 클라이언트 측 스크립트 언어로 개발되었다. 초기에는 UI 조작과 이벤트 처리를 중심으로 프론트엔드 개발에 사용되었다.
2009년, Node.js가 등장하면서 JavaScript가 브라우저를 벗어나 서버 환경에서도 실행될 수 있게 되었다. 이를 통해 JavaScript로 백엔드 개발이 가능해졌다.
그러나 JavaScript는 본래 브라우저에서 동작하도록 설계된 언어였기 때문에, 파일 시스템, 네트워크, 모듈 관리와 같은 서버 개발에 필요한 API가 존재하지 않았다. 이를 해결하기 위해 CommonJS라는 표준이 등장했으며, 이 표준은 모듈 관리 체계를 포함하여 서버 개발에 필요한 여러 기능을 정의했다.
Node.js는 CommonJS 표준을 기반으로 구현되었으며, CommonJS에서 제안한 모듈 관리 문법이 바로 require다. 하지만 require는 브라우저 환경에서 지원되지 않으므로, 프론트엔드에서 사용하면 오류가 발생한다.
JavaScript는 ECMAScript 표준을 기반으로 발전해왔으며, 2015년 ES6(ECMAScript 6)라는 대규모 업데이트를 통해 새로운 기능들이 도입되었다.
특히 모듈 관리 체계가 추가되면서, 프론트엔드에서도 import/export 문법을 사용해 모듈을 관리할 수 있게 되었다. 이는 기존의 CommonJS(require)와는 별개로, JavaScript의 공식적인 모듈 표준으로 자리 잡았다.
💡 그런데 CommonJS의 require 문법을 왜 ES6에 통합하지 않고 별도로 import 방식을 만들었을까?
알아보니 두 가지 이유 때문이었다.
결과적으로 ES6의 도입을 통해 프론트엔드/백엔드 모두에서 import/export를 통한 모듈 관리가 가능해졌다. 잠깐 정리하고 넘어가자.
초기 JavaScript는 파일 관리 체계가 부재하여 여러 문제점들이 발생했다. 대표적으로 의존성 관리의 어려움, 전역 네임스페이스 오염, 성능 저하 등이 있었다. 이를 해결하기 위해 모듈화 개념이 도입되었고, JavaScript는 서버와 클라이언트 측에서 모두 사용할 수 있게 되었다. Node.js는 서버 측에서 JavaScript를 사용할 수 있도록 만들어졌고, 이를 위해 CommonJS 표준이 도입되었다. 이후, 2015년에 발표된 ES6에서는 import/export 방식으로 클라이언트 측 모듈 관리 체계가 정립되었다.
번들링과 번들러에 대한 논의는 정확히 지금 시점에서 다루는 게 맞지만, 정말 훌륭한 글을 발견했다. 조급히 정리하기보다는 이 시리즈를 천천히 읽고 나서 정리하는 것이 더 깊이 있는 학습과 더 나은 글을 작성하는 데 도움이 될 것 같아 잠시 멈추기로 한다.
기똥찬 글 시리즈:
1) https://deemmun.tistory.com/86
2) https://deemmun.tistory.com/87
3) https://deemmun.tistory.com/88
하단의 이미지를 기반으로 번들러 역사를 톺아볼 예정

Task Queue는 Web API가 수행한 비동기 함수를 넘겨받아 Event Loop가 해당 함수를 Call Stack에 넘겨줄 때까지 비동기 함수를 쌓아놓는, 예민한 고양이 대기실이라고 이전 글에서 다룬 바 있다.
? -> 그런데 공부하던 중 Task Queue말고도 다른 Queue가 존재한다는 것을 알아버렸다. 미치겠다. 진짜 아무도 날 말릴 수 없으셈.

그런데 개별 요소들을 알아보기 전에 질문이 먼저 나와야 한다.
Queue가 여러 개인건 알겠어. 그런데 왜? => 훌륭한 학생이다.
브라우저에 존재하는 여러 비동기 처리에 우선순위를 부여하고 싶고, 그 우선순위에 따라 Queue가 나뉘게 된 것이다. 결론부터 얘기하면 우선 순위는 다음과 같다.
1순위: MicroTask Queue -> 2순위: Animation Frames -> 3순위: MacroTask Queue 순이다.
일반적으로 Task Queue라고 하면, MacroTask Queue를 의미한다.
MacroTask Queue에서는 setTimeout(), setInterval(), setImmediate()와 같은 task를 넘겨받는다.

MicroTask Queue는 Promise나 async/await 등과 관련된 비동기 호출을 넘겨받게 되는 Queue다. 이벤트 루프에서 1순위로 처리하려 하는 대상이기도 하다.
// 1. 실행
console.log('script start')
// 2. task queue로 전달
setTimeout(function() {
// 8. task 실행
console.log('setTimeout')
}, 0)
// 3. microtask queue로 전달
Promise.resolve()
.then(function() {
// 5. microtask 실행
console.log('promise1')
// 6. microtask queue로 전달
})
.then(function() {
// 7. microtask 실행
console.log('promise2')
})
// 4. 실행
console.log('script end')
위 코드에 대한 콘솔은 다음과 같은 순서로 입력될 것이다.
script start -> script end -> promise1 -> promise2 -> setTimeout
1번

2번

3번

4번

5번

6번

1번

2번

3번

4번

5번

async/await를 사용할 때에는 Promise와 조금 다른 flow를 보인다.
myFunc 함수 내부의 두번째 줄이 실행될 때, one 함수는 콜 스택에서 pop되어 promise를 반환한다.
promise가 반환되며 마주하는 것이 await인데, 이 경우 async 함수의 실행이 미루어진다.
async 함수의 나머지 부분들은 MicroTask Queue로 전달된다.
Animation Frames는 requestAnimationFrame()과 같이 브라우저 렌더링과 관련된 task를 넘겨받는 Queue이다. 예시 코드와 그 실행 순서를 확인해보자.
/ 1. 실행
console.log("script start");
// 2. task queue로 전달
setTimeout(function () {
// 10. task 실행
console.log("setTimeout");
}, 0);
//3. microtask queue로 전달
Promise.resolve()
.then(function () {
// 6. microtask 실행
console.log("promise1");
}) // 7. microtask queue로 전달
.then(function () {
// 8. microtask 실행
console.log("promise2");
});
//4. AnimationFrame으로 전달
requestAnimationFrame(function () {
//9. animation frame 실행
console.log("animation");
});
//5. 실행
console.log("script end");
script start
script end
promise1
promise2
animation
setTimeout
requestAnimationFrame()은 딱 우선순위가 있다고만 알고 넘어가도록 한다. 왜냐하면 결국 DOM과 렌더링, 즉 브라우저 동작 방식에 대해 깊은 연구가 필요한데, 한 번에 다 다룰 수는 없다.
+ 향후 과제
1. 번들러의 역사를 정리해야 될 듯
2. DOM과 렌더링 => 브라우저 동작 원리에 대한 시리즈를 릴리즈 할 필요가 있음
3. 에디터 최적화 관련해서 useMemo와 useCallback을 정리해야 할 필요가 있음
4. 할 일이 산더미라는 뜻
회고 🌿
구슬이 서 말이라도 꿰어야 보배라고 했던가. 딥러닝 시간에 본인이 유일하게 존경하는 교수님께서는, 코드를 띄워놓고 코드에 대해 설명하는 건 전체 수업 시간의 10분도 채 안 됐고, 해당 코드가 등장하기까지의 흐름을 설명하는 데에만 거의 2시간을 할애하셨다. 교수님의 주장은 다음과 같았다.
"코딩 실력은 즐겁게 놀이처럼 하다 보면 자연스럽게 향상된다. 그 놀이는 내 영역이 아니다. 그런데 놀이를 하려면 규칙을 제대로 이해해야 한다. 즐겁고 행복하기 위해서는 지금 이 얘기가 꼭 필요하다."
이번 시리즈를 정리하면서 작업이 더 재밌어졌다. 일단은 뭐라도 동작하게 만들어왔는데, 규칙을 명확히 파악하려는 시도와 함께 놀이를 하다 보니, 이해의 수준이 이전과는 비교하지 못할 정도로 깊어지고 있음을 느낀다. 다음에는 어떤 게임을 할까, 그 게임의 규칙은 무엇일까. 역시 인생은 즐겁고 봐야 된다.