[{"data":1,"prerenderedAt":1496},["ShallowReactive",2],{"blog-post-\u002Fko\u002Fjavascript-event-loop":3,"blog-related-source":1222},{"id":4,"title":5,"body":6,"description":1209,"extension":1210,"meta":1211,"navigation":56,"path":1217,"seo":1218,"sitemap":1219,"stem":1220,"__hash__":1221},"ko\u002Fko\u002Fjavascript-event-loop.md","자바스크립트 이벤트 루프 완전 정복: 마이크로태스크와 매크로태스크의 실행 순서",{"type":7,"value":8,"toc":1193},"minimark",[9,14,18,148,156,164,179,182,186,192,199,202,288,290,294,297,325,328,336,339,371,373,377,382,476,482,485,489,500,601,607,626,628,632,635,693,796,802,834,836,840,844,851,872,879,1037,1044,1048,1062,1108,1112,1118,1120,1124,1182,1189],[10,11,13],"h2",{"id":12},"왜-이-코드는-1-4-3-2-순서로-찍힐까","왜 이 코드는 1 → 4 → 3 → 2 순서로 찍힐까?",[15,16,17],"p",{},"먼저 퀴즈부터 하나.",[19,20,25],"pre",{"className":21,"code":22,"language":23,"meta":24,"style":24},"language-js shiki shiki-themes github-light github-dark","console.log(\"1\");\n\nsetTimeout(() => console.log(\"2\"), 0);\n\nPromise.resolve().then(() => console.log(\"3\"));\n\nconsole.log(\"4\");\n","js","",[26,27,28,51,58,90,95,129,134],"code",{"__ignoreMap":24},[29,30,33,37,41,44,48],"span",{"class":31,"line":32},"line",1,[29,34,36],{"class":35},"sVt8B","console.",[29,38,40],{"class":39},"sScJk","log",[29,42,43],{"class":35},"(",[29,45,47],{"class":46},"sZZnC","\"1\"",[29,49,50],{"class":35},");\n",[29,52,54],{"class":31,"line":53},2,[29,55,57],{"emptyLinePlaceholder":56},true,"\n",[29,59,61,64,67,71,74,76,78,81,84,88],{"class":31,"line":60},3,[29,62,63],{"class":39},"setTimeout",[29,65,66],{"class":35},"(() ",[29,68,70],{"class":69},"szBVR","=>",[29,72,73],{"class":35}," console.",[29,75,40],{"class":39},[29,77,43],{"class":35},[29,79,80],{"class":46},"\"2\"",[29,82,83],{"class":35},"), ",[29,85,87],{"class":86},"sj4cs","0",[29,89,50],{"class":35},[29,91,93],{"class":31,"line":92},4,[29,94,57],{"emptyLinePlaceholder":56},[29,96,98,101,104,107,110,113,115,117,119,121,123,126],{"class":31,"line":97},5,[29,99,100],{"class":86},"Promise",[29,102,103],{"class":35},".",[29,105,106],{"class":39},"resolve",[29,108,109],{"class":35},"().",[29,111,112],{"class":39},"then",[29,114,66],{"class":35},[29,116,70],{"class":69},[29,118,73],{"class":35},[29,120,40],{"class":39},[29,122,43],{"class":35},[29,124,125],{"class":46},"\"3\"",[29,127,128],{"class":35},"));\n",[29,130,132],{"class":31,"line":131},6,[29,133,57],{"emptyLinePlaceholder":56},[29,135,137,139,141,143,146],{"class":31,"line":136},7,[29,138,36],{"class":35},[29,140,40],{"class":39},[29,142,43],{"class":35},[29,144,145],{"class":46},"\"4\"",[29,147,50],{"class":35},[15,149,150,152,153,155],{},[26,151,63],{},"의 지연 시간이 ",[26,154,87],{},"이니 바로 실행될 것 같지만, 실제 출력은 이렇다.",[19,157,162],{"className":158,"code":160,"language":161},[159],"language-text","1\n4\n3\n2\n","text",[26,163,160],{"__ignoreMap":24},[15,165,166,169,170,173,174,178],{},[26,167,168],{},"Promise.then","이 ",[26,171,172],{},"setTimeout(0)","보다 먼저 실행된다. 이 순서를 이해하려면 ",[175,176,177],"strong",{},"이벤트 루프(Event Loop)"," 가 어떻게 돌아가는지 알아야 한다.",[180,181],"hr",{},[10,183,185],{"id":184},"이벤트-루프란","이벤트 루프란?",[187,188,189],"blockquote",{},[15,190,191],{},"이벤트 루프는 싱글 스레드(Single Thread)인 자바스크립트가 비동기(Asynchronous) 작업을 처리할 수 있도록, \"지금 실행할 코드\"와 \"나중에 실행할 콜백(Callback)\"의 순서를 관리하는 장치다.",[15,193,194,195,198],{},"자바스크립트 엔진은 한 번에 하나의 코드만 실행한다. 그런데도 타이머, 네트워크 요청, 클릭 이벤트를 동시에 다루는 것처럼 보이는 건, 기다리는 일은 브라우저(또는 Node.js)에 맡기고 끝난 작업의 콜백만 ",[175,196,197],{},"큐에 줄 세워"," 하나씩 꺼내 실행하기 때문이다.",[15,200,201],{},"이 과정에 등장하는 구성 요소는 다음과 같다.",[203,204,205,221],"table",{},[206,207,208],"thead",{},[209,210,211,215,218],"tr",{},[212,213,214],"th",{},"구성 요소",[212,216,217],{},"역할",[212,219,220],{},"예시",[222,223,224,236,251,268],"tbody",{},[209,225,226,230,233],{},[227,228,229],"td",{},"콜 스택 (Call Stack)",[227,231,232],{},"지금 실행 중인 함수들이 쌓이는 곳",[227,234,235],{},"일반 함수 호출",[209,237,238,241,244],{},[227,239,240],{},"Web API \u002F 런타임 API (Runtime API)",[227,242,243],{},"시간이 걸리는 작업을 엔진 밖에서 처리",[227,245,246,247,250],{},"타이머, ",[26,248,249],{},"fetch",", DOM 이벤트",[209,252,253,256,259],{},[227,254,255],{},"태스크 큐 (Task Queue) = 매크로태스크 (Macrotask)",[227,257,258],{},"완료된 비동기 작업의 콜백이 기다리는 곳",[227,260,261,263,264,267],{},[26,262,63],{},", ",[26,265,266],{},"setInterval",", 이벤트 핸들러",[209,269,270,273,276],{},[227,271,272],{},"마이크로태스크 큐 (Microtask Queue)",[227,274,275],{},"우선순위가 더 높은 콜백이 기다리는 곳",[227,277,278,263,281,284,285],{},[26,279,280],{},"Promise.then\u002Fcatch\u002Ffinally",[26,282,283],{},"await"," 이후 코드, ",[26,286,287],{},"queueMicrotask",[180,289],{},[10,291,293],{"id":292},"실행-순서의-핵심-규칙","실행 순서의 핵심 규칙",[15,295,296],{},"이벤트 루프는 다음 사이클을 계속 반복한다.",[298,299,300,308,315,322],"ol",{},[301,302,303,304,307],"li",{},"태스크 큐에서 ",[175,305,306],{},"태스크 하나","를 꺼내 실행한다 (처음엔 스크립트 전체가 하나의 태스크).",[301,309,310,311,314],{},"콜 스택이 비면 ",[175,312,313],{},"마이크로태스크 큐를 완전히 비울 때까지"," 전부 실행한다.",[301,316,317,318,321],{},"필요하면 화면을 다시 그린다. 이를 렌더링(Rendering)이라 하고, ",[26,319,320],{},"requestAnimationFrame"," 콜백도 이 단계에서 실행된다.",[301,323,324],{},"1번으로 돌아간다.",[15,326,327],{},"핵심은 2번이다.",[329,330,331],"tip",{},[187,332,333],{},[15,334,335],{},"태스크는 한 번에 하나씩, 마이크로태스크는 큐가 빌 때까지 한꺼번에 실행된다.",[15,337,338],{},"이제 처음 퀴즈를 다시 보면 흐름이 이렇게 된다.",[340,341,342,359,365],"ul",{},[301,343,344,345,348,349,351,352,354,355,358],{},"스크립트 실행(태스크) → ",[26,346,347],{},"1"," 출력 → ",[26,350,63],{}," 콜백은 태스크 큐로 → ",[26,353,112],{}," 콜백은 마이크로태스크 큐로 → ",[26,356,357],{},"4"," 출력",[301,360,361,362,358],{},"스크립트 종료, 콜 스택이 빔 → 마이크로태스크 큐 비우기 → ",[26,363,364],{},"3",[301,366,367,368,358],{},"다음 태스크 → ",[26,369,370],{},"2",[180,372],{},[10,374,376],{"id":375},"예제로-연습하기","예제로 연습하기",[378,379,381],"h3",{"id":380},"_1-태스크-안에서-생긴-마이크로태스크","1. 태스크 안에서 생긴 마이크로태스크",[19,383,385],{"className":21,"code":384,"language":23,"meta":24,"style":24},"setTimeout(() => {\n  console.log(\"timeout 1\");\n  Promise.resolve().then(() => console.log(\"micro in timeout 1\"));\n}, 0);\n\nsetTimeout(() => console.log(\"timeout 2\"), 0);\n",[26,386,387,398,412,440,449,453],{"__ignoreMap":24},[29,388,389,391,393,395],{"class":31,"line":32},[29,390,63],{"class":39},[29,392,66],{"class":35},[29,394,70],{"class":69},[29,396,397],{"class":35}," {\n",[29,399,400,403,405,407,410],{"class":31,"line":53},[29,401,402],{"class":35},"  console.",[29,404,40],{"class":39},[29,406,43],{"class":35},[29,408,409],{"class":46},"\"timeout 1\"",[29,411,50],{"class":35},[29,413,414,417,419,421,423,425,427,429,431,433,435,438],{"class":31,"line":60},[29,415,416],{"class":86},"  Promise",[29,418,103],{"class":35},[29,420,106],{"class":39},[29,422,109],{"class":35},[29,424,112],{"class":39},[29,426,66],{"class":35},[29,428,70],{"class":69},[29,430,73],{"class":35},[29,432,40],{"class":39},[29,434,43],{"class":35},[29,436,437],{"class":46},"\"micro in timeout 1\"",[29,439,128],{"class":35},[29,441,442,445,447],{"class":31,"line":92},[29,443,444],{"class":35},"}, ",[29,446,87],{"class":86},[29,448,50],{"class":35},[29,450,451],{"class":31,"line":97},[29,452,57],{"emptyLinePlaceholder":56},[29,454,455,457,459,461,463,465,467,470,472,474],{"class":31,"line":131},[29,456,63],{"class":39},[29,458,66],{"class":35},[29,460,70],{"class":69},[29,462,73],{"class":35},[29,464,40],{"class":39},[29,466,43],{"class":35},[29,468,469],{"class":46},"\"timeout 2\"",[29,471,83],{"class":35},[29,473,87],{"class":86},[29,475,50],{"class":35},[19,477,480],{"className":478,"code":479,"language":161},[159],"timeout 1\nmicro in timeout 1\ntimeout 2\n",[26,481,479],{"__ignoreMap":24},[15,483,484],{},"두 타이머가 같은 시점에 큐에 들어가 있어도, 첫 번째 태스크가 끝난 직후 그 안에서 생긴 마이크로태스크가 먼저 처리된다. 그다음에야 두 번째 태스크로 넘어간다.",[378,486,488],{"id":487},"_2-async-await","2. async \u002F await",[15,490,491,493,494,496,497],{},[26,492,283],{}," 뒤의 코드는 ",[26,495,112],{}," 콜백과 같다. 즉 ",[175,498,499],{},"마이크로태스크로 예약된다.",[19,501,503],{"className":21,"code":502,"language":23,"meta":24,"style":24},"async function foo() {\n  console.log(\"foo 시작\");\n  await null;\n  console.log(\"foo 끝\");\n}\n\nconsole.log(\"스크립트 시작\");\nfoo();\nconsole.log(\"스크립트 끝\");\n",[26,504,505,519,532,543,556,561,565,578,587],{"__ignoreMap":24},[29,506,507,510,513,516],{"class":31,"line":32},[29,508,509],{"class":69},"async",[29,511,512],{"class":69}," function",[29,514,515],{"class":39}," foo",[29,517,518],{"class":35},"() {\n",[29,520,521,523,525,527,530],{"class":31,"line":53},[29,522,402],{"class":35},[29,524,40],{"class":39},[29,526,43],{"class":35},[29,528,529],{"class":46},"\"foo 시작\"",[29,531,50],{"class":35},[29,533,534,537,540],{"class":31,"line":60},[29,535,536],{"class":69},"  await",[29,538,539],{"class":86}," null",[29,541,542],{"class":35},";\n",[29,544,545,547,549,551,554],{"class":31,"line":92},[29,546,402],{"class":35},[29,548,40],{"class":39},[29,550,43],{"class":35},[29,552,553],{"class":46},"\"foo 끝\"",[29,555,50],{"class":35},[29,557,558],{"class":31,"line":97},[29,559,560],{"class":35},"}\n",[29,562,563],{"class":31,"line":131},[29,564,57],{"emptyLinePlaceholder":56},[29,566,567,569,571,573,576],{"class":31,"line":136},[29,568,36],{"class":35},[29,570,40],{"class":39},[29,572,43],{"class":35},[29,574,575],{"class":46},"\"스크립트 시작\"",[29,577,50],{"class":35},[29,579,581,584],{"class":31,"line":580},8,[29,582,583],{"class":39},"foo",[29,585,586],{"class":35},"();\n",[29,588,590,592,594,596,599],{"class":31,"line":589},9,[29,591,36],{"class":35},[29,593,40],{"class":39},[29,595,43],{"class":35},[29,597,598],{"class":46},"\"스크립트 끝\"",[29,600,50],{"class":35},[19,602,605],{"className":603,"code":604,"language":161},[159],"스크립트 시작\nfoo 시작\n스크립트 끝\nfoo 끝\n",[26,606,604],{"__ignoreMap":24},[15,608,609,611,612,614,615,618,619,169,622,625],{},[26,610,509],{}," 함수는 첫 번째 ",[26,613,283],{},"을 만나기 전까지는 ",[175,616,617],{},"동기적으로"," 실행된다. 그래서 ",[26,620,621],{},"foo 시작",[26,623,624],{},"스크립트 끝","보다 먼저 찍힌다. \"async 함수는 통째로 비동기\"라고 오해하기 쉬운 부분이다.",[180,627],{},[10,629,631],{"id":630},"브라우저-vs-nodejs","브라우저 vs Node.js",[15,633,634],{},"Node.js도 이벤트 루프를 쓰지만 libuv 기반이라 단계(Phase)가 더 세분화되어 있고, 브라우저에는 없는 API가 있다.",[203,636,637,650],{},[206,638,639],{},[209,640,641,644,647],{},[212,642,643],{},"항목",[212,645,646],{},"브라우저",[212,648,649],{},"Node.js",[222,651,652,666,680],{},[209,653,654,657,663],{},[227,655,656],{},"렌더링 단계",[227,658,659,660,662],{},"있음 (",[26,661,320],{},")",[227,664,665],{},"없음",[209,667,668,671,674],{},[227,669,670],{},"추가 큐",[227,672,673],{},"-",[227,675,676,679],{},[26,677,678],{},"process.nextTick"," 큐 (Promise보다 먼저)",[209,681,682,685,687],{},[227,683,684],{},"추가 API",[227,686,673],{},[227,688,689,692],{},[26,690,691],{},"setImmediate"," (check 단계에서 실행)",[19,694,696],{"className":21,"code":695,"language":23,"meta":24,"style":24},"\u002F\u002F Node.js, CommonJS 모듈 기준\nsetTimeout(() => console.log(\"timeout\"), 0);\nsetImmediate(() => console.log(\"immediate\"));\nprocess.nextTick(() => console.log(\"nextTick\"));\nPromise.resolve().then(() => console.log(\"promise\"));\n",[26,697,698,704,727,746,769],{"__ignoreMap":24},[29,699,700],{"class":31,"line":32},[29,701,703],{"class":702},"sJ8bj","\u002F\u002F Node.js, CommonJS 모듈 기준\n",[29,705,706,708,710,712,714,716,718,721,723,725],{"class":31,"line":53},[29,707,63],{"class":39},[29,709,66],{"class":35},[29,711,70],{"class":69},[29,713,73],{"class":35},[29,715,40],{"class":39},[29,717,43],{"class":35},[29,719,720],{"class":46},"\"timeout\"",[29,722,83],{"class":35},[29,724,87],{"class":86},[29,726,50],{"class":35},[29,728,729,731,733,735,737,739,741,744],{"class":31,"line":60},[29,730,691],{"class":39},[29,732,66],{"class":35},[29,734,70],{"class":69},[29,736,73],{"class":35},[29,738,40],{"class":39},[29,740,43],{"class":35},[29,742,743],{"class":46},"\"immediate\"",[29,745,128],{"class":35},[29,747,748,751,754,756,758,760,762,764,767],{"class":31,"line":92},[29,749,750],{"class":35},"process.",[29,752,753],{"class":39},"nextTick",[29,755,66],{"class":35},[29,757,70],{"class":69},[29,759,73],{"class":35},[29,761,40],{"class":39},[29,763,43],{"class":35},[29,765,766],{"class":46},"\"nextTick\"",[29,768,128],{"class":35},[29,770,771,773,775,777,779,781,783,785,787,789,791,794],{"class":31,"line":97},[29,772,100],{"class":86},[29,774,103],{"class":35},[29,776,106],{"class":39},[29,778,109],{"class":35},[29,780,112],{"class":39},[29,782,66],{"class":35},[29,784,70],{"class":69},[29,786,73],{"class":35},[29,788,40],{"class":39},[29,790,43],{"class":35},[29,792,793],{"class":46},"\"promise\"",[29,795,128],{"class":35},[19,797,800],{"className":798,"code":799,"language":161},[159],"nextTick\npromise\ntimeout \u002F immediate (둘의 순서는 실행할 때마다 달라질 수 있음)\n",[26,801,799],{"__ignoreMap":24},[803,804,805],"warning",{},[187,806,807],{},[15,808,809,810,812,813,815,816,819,820,822,823,826,827,830,831,833],{},"메인 모듈에서 ",[26,811,172],{},"과 ",[26,814,691],{},"의 순서는 보장되지 않는다. 다만 I\u002FO(입출력) 콜백(예: ",[26,817,818],{},"fs.readFile","의 콜백) 안에서 호출하면 항상 ",[26,821,691],{},"가 먼저 실행된다. 또 ES 모듈(",[26,824,825],{},".mjs",")에서는 최상위 코드가 이미 마이크로태스크 흐름 안에서 실행되기 때문에 ",[26,828,829],{},"promise","가 ",[26,832,753],{},"보다 먼저 찍힐 수 있다.",[180,835],{},[10,837,839],{"id":838},"실무에서-자주-겪는-문제","실무에서 자주 겪는 문제",[378,841,843],{"id":842},"_1-무거운-동기-작업이-화면을-멈춘다","1. 무거운 동기 작업이 화면을 멈춘다",[15,845,846,847,850],{},"렌더링은 태스크와 태스크 ",[175,848,849],{},"사이","에서만 일어난다. 그래서 하나의 태스크가 오래 걸리면 그동안 클릭도, 스크롤도, 화면 갱신도 모두 멈춘다.",[19,852,854],{"className":21,"code":853,"language":23,"meta":24,"style":24},"\u002F\u002F ❌ 10만 개를 한 번에 처리하면 그동안 UI가 굳는다\nitems.forEach(processItem);\n",[26,855,856,861],{"__ignoreMap":24},[29,857,858],{"class":31,"line":32},[29,859,860],{"class":702},"\u002F\u002F ❌ 10만 개를 한 번에 처리하면 그동안 UI가 굳는다\n",[29,862,863,866,869],{"class":31,"line":53},[29,864,865],{"class":35},"items.",[29,867,868],{"class":39},"forEach",[29,870,871],{"class":35},"(processItem);\n",[15,873,874,875,878],{},"작업을 잘게 나누고, 중간중간 ",[175,876,877],{},"태스크 경계","를 만들어 브라우저에 숨 쉴 틈을 준다. 이렇게 메인 스레드(Main Thread)를 잠깐 내어주는 것을 양보(Yield)라고 한다.",[19,880,882],{"className":21,"code":881,"language":23,"meta":24,"style":24},"\u002F\u002F ✅ 일정 개수마다 다음 태스크로 넘겨서 렌더링과 입력 처리가 끼어들 수 있게 한다\nconst yieldToMain = () => new Promise((resolve) => setTimeout(resolve, 0));\n\nasync function processAll(items) {\n  for (let i = 0; i \u003C items.length; i++) {\n    processItem(items[i]);\n    if (i % 500 === 0) await yieldToMain();\n  }\n}\n",[26,883,884,889,932,936,953,993,1001,1028,1033],{"__ignoreMap":24},[29,885,886],{"class":31,"line":32},[29,887,888],{"class":702},"\u002F\u002F ✅ 일정 개수마다 다음 태스크로 넘겨서 렌더링과 입력 처리가 끼어들 수 있게 한다\n",[29,890,891,894,897,900,903,905,908,911,914,917,920,922,925,928,930],{"class":31,"line":53},[29,892,893],{"class":69},"const",[29,895,896],{"class":39}," yieldToMain",[29,898,899],{"class":69}," =",[29,901,902],{"class":35}," () ",[29,904,70],{"class":69},[29,906,907],{"class":69}," new",[29,909,910],{"class":86}," Promise",[29,912,913],{"class":35},"((",[29,915,106],{"class":916},"s4XuR",[29,918,919],{"class":35},") ",[29,921,70],{"class":69},[29,923,924],{"class":39}," setTimeout",[29,926,927],{"class":35},"(resolve, ",[29,929,87],{"class":86},[29,931,128],{"class":35},[29,933,934],{"class":31,"line":60},[29,935,57],{"emptyLinePlaceholder":56},[29,937,938,940,942,945,947,950],{"class":31,"line":92},[29,939,509],{"class":69},[29,941,512],{"class":69},[29,943,944],{"class":39}," processAll",[29,946,43],{"class":35},[29,948,949],{"class":916},"items",[29,951,952],{"class":35},") {\n",[29,954,955,958,961,964,967,970,973,976,979,982,985,988,991],{"class":31,"line":97},[29,956,957],{"class":69},"  for",[29,959,960],{"class":35}," (",[29,962,963],{"class":69},"let",[29,965,966],{"class":35}," i ",[29,968,969],{"class":69},"=",[29,971,972],{"class":86}," 0",[29,974,975],{"class":35},"; i ",[29,977,978],{"class":69},"\u003C",[29,980,981],{"class":35}," items.",[29,983,984],{"class":86},"length",[29,986,987],{"class":35},"; i",[29,989,990],{"class":69},"++",[29,992,952],{"class":35},[29,994,995,998],{"class":31,"line":131},[29,996,997],{"class":39},"    processItem",[29,999,1000],{"class":35},"(items[i]);\n",[29,1002,1003,1006,1009,1012,1015,1018,1020,1022,1024,1026],{"class":31,"line":136},[29,1004,1005],{"class":69},"    if",[29,1007,1008],{"class":35}," (i ",[29,1010,1011],{"class":69},"%",[29,1013,1014],{"class":86}," 500",[29,1016,1017],{"class":69}," ===",[29,1019,972],{"class":86},[29,1021,919],{"class":35},[29,1023,283],{"class":69},[29,1025,896],{"class":39},[29,1027,586],{"class":35},[29,1029,1030],{"class":31,"line":580},[29,1031,1032],{"class":35},"  }\n",[29,1034,1035],{"class":31,"line":589},[29,1036,560],{"class":35},[15,1038,1039,1040,1043],{},"최신 Chromium 계열 브라우저는 이 용도로 ",[26,1041,1042],{},"scheduler.yield()","를 제공하므로, 지원 여부를 확인하고 쓸 수 있으면 쓰는 것이 좋다.",[378,1045,1047],{"id":1046},"_2-마이크로태스크로는-양보가-안-된다","2. 마이크로태스크로는 양보가 안 된다",[15,1049,1050,1051,1053,1054,1057,1058,1061],{},"위 코드에서 ",[26,1052,63],{}," 대신 ",[26,1055,1056],{},"Promise.resolve()","로 쉬어가면 효과가 없다. 마이크로태스크는 큐가 빌 때까지 이어서 실행되므로 렌더링이 끼어들 틈이 없기 때문이다. 마이크로태스크가 계속 새 마이크로태스크를 만들면 ",[26,1059,1060],{},"while (true)","처럼 페이지가 완전히 멈춘다.",[19,1063,1065],{"className":21,"code":1064,"language":23,"meta":24,"style":24},"\u002F\u002F ❌ 렌더링이 영원히 일어나지 않는다\nfunction loop() {\n  Promise.resolve().then(loop);\n}\nloop();\n",[26,1066,1067,1072,1082,1097,1101],{"__ignoreMap":24},[29,1068,1069],{"class":31,"line":32},[29,1070,1071],{"class":702},"\u002F\u002F ❌ 렌더링이 영원히 일어나지 않는다\n",[29,1073,1074,1077,1080],{"class":31,"line":53},[29,1075,1076],{"class":69},"function",[29,1078,1079],{"class":39}," loop",[29,1081,518],{"class":35},[29,1083,1084,1086,1088,1090,1092,1094],{"class":31,"line":60},[29,1085,416],{"class":86},[29,1087,103],{"class":35},[29,1089,106],{"class":39},[29,1091,109],{"class":35},[29,1093,112],{"class":39},[29,1095,1096],{"class":35},"(loop);\n",[29,1098,1099],{"class":31,"line":92},[29,1100,560],{"class":35},[29,1102,1103,1106],{"class":31,"line":97},[29,1104,1105],{"class":39},"loop",[29,1107,586],{"class":35},[378,1109,1111],{"id":1110},"_3-0ms는-즉시가-아니다","3. \"0ms\"는 \"즉시\"가 아니다",[15,1113,1114,1117],{},[26,1115,1116],{},"setTimeout(fn, 0)","은 \"최소 0ms 뒤에 태스크 큐에 넣어달라\"는 뜻이다. 앞에 쌓인 태스크와 마이크로태스크가 모두 끝나야 실행되고, 브라우저는 중첩된 타이머의 최소 지연을 4ms로 늘리기도 한다. 정확한 타이밍이 필요한 코드에 기대면 안 된다.",[180,1119],{},[10,1121,1123],{"id":1122},"정리-왜-알아두면-좋은가","정리: 왜 알아두면 좋은가",[203,1125,1126,1136],{},[206,1127,1128],{},[209,1129,1130,1133],{},[212,1131,1132],{},"상황",[212,1134,1135],{},"이벤트 루프 관점의 해석",[222,1137,1138,1151,1164,1172],{},[209,1139,1140,1148],{},[227,1141,1142,1144,1145,1147],{},[26,1143,112],{}," 콜백이 ",[26,1146,172],{},"보다 먼저 실행됨",[227,1149,1150],{},"마이크로태스크가 다음 태스크보다 우선",[209,1152,1153,1158],{},[227,1154,1155,1157],{},[26,1156,509],{}," 함수 앞부분이 바로 실행됨",[227,1159,1160,1161,1163],{},"첫 ",[26,1162,283],{}," 전까지는 동기 실행",[209,1165,1166,1169],{},[227,1167,1168],{},"큰 반복문 중 화면이 멈춤",[227,1170,1171],{},"태스크 하나가 렌더링 기회를 막음",[209,1173,1174,1179],{},[227,1175,1176,1178],{},[26,1177,100],{},"로 쪼갰는데도 멈춤",[227,1180,1181],{},"마이크로태스크는 렌더링 전에 전부 실행됨",[15,1183,1184,1185,1188],{},"이벤트 루프를 이해하면 \"왜 이 로그가 이 순서로 찍히지?\" 같은 디버깅 시간이 크게 줄고, 로딩 스피너가 안 돌거나 입력이 버벅이는 문제의 원인도 바로 짚을 수 있다. 비동기 코드가 예상과 다르게 움직일 때는 ",[175,1186,1187],{},"\"이 콜백은 태스크인가, 마이크로태스크인가?\""," 부터 따져보자.",[1190,1191,1192],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":24,"searchDepth":53,"depth":53,"links":1194},[1195,1196,1197,1198,1202,1203,1208],{"id":12,"depth":53,"text":13},{"id":184,"depth":53,"text":185},{"id":292,"depth":53,"text":293},{"id":375,"depth":53,"text":376,"children":1199},[1200,1201],{"id":380,"depth":60,"text":381},{"id":487,"depth":60,"text":488},{"id":630,"depth":53,"text":631},{"id":838,"depth":53,"text":839,"children":1204},[1205,1206,1207],{"id":842,"depth":60,"text":843},{"id":1046,"depth":60,"text":1047},{"id":1110,"depth":60,"text":1111},{"id":1122,"depth":53,"text":1123},"setTimeout(0)보다 Promise.then이 먼저 실행되는 이유는? 콜 스택, 태스크 큐, 마이크로태스크 큐가 어떤 순서로 돌아가는지 예제로 정리하고, 브라우저와 Node.js의 차이, 실무에서 UI 멈춤을 피하는 방법까지 알아보자.","md",{"date":1212,"field":1213,"tags":1214},"2026.10.08","concepts",[1215,1216],"javascript","nodejs","\u002Fko\u002Fjavascript-event-loop",{"title":5,"description":1209},{"loc":1217},"ko\u002Fjavascript-event-loop","0jRVu6v40fTdDkMihdasKuhxxZXMvutuXb5_zm3Tonk",[1223,1234,1244,1252,1259,1266,1276,1284,1292,1304,1312,1321,1327,1333,1340,1347,1350,1357,1363,1372,1381,1388,1394,1401,1408,1416,1423,1430,1439,1446,1453,1460,1468,1475,1483,1489],{"path":1224,"title":1225,"description":1226,"meta":1227},"\u002Fko\u002Fadsense-ads-txt-and-seo-indexing-bug","애드센스 ads.txt 경고를 받고 파헤치다가 찾은 진짜 색인 버그","구글 애드센스가 ads.txt 문제를 알려왔는데, 확인해보니 파일은 멀쩡했다. 대신 서치콘솔 색인 수가 적었던 진짜 원인을 Nuxt Content 슬러그 불일치 버그에서 찾은 과정을 정리한다.",{"date":1228,"field":1229,"tags":1230},"2026.09.17","blog",[1231,1232,1233],"seo","adsense","nuxt-content",{"path":1235,"title":1236,"description":1237,"meta":1238},"\u002Fko\u002Fcss-layouts","CSS Flexbox 완전 정복 가이드","Flexbox를 활용한 반응형 레이아웃 구현 방법과 실전 예제. 수직 정렬, 네비게이션 바, 카드 레이아웃까지 완벽 정리!",{"date":1239,"image":1240,"field":1241,"tags":1242},"2025.01.25","css.jpg","frontend",[1243],"css",{"path":1245,"title":1246,"description":1247,"meta":1248},"\u002Fko\u002Fcss-login-form","로그인","로그인 UI를 그려보자",{"date":1249,"field":1241,"tags":1250},"2025.03.29",[1251,1243],"html",{"path":1253,"title":1254,"description":1255,"meta":1256},"\u002Fko\u002Fcsv-parser-cleaning","복잡한 CSV 데이터를 안전하게 파싱하는 방법","JavaScript에서 큰따옴표, 줄바꿈, 쉼표 등 복잡한 CSV 데이터를 안전하게 객체로 변환하는 전체 과정 설명",{"date":1257,"field":1241,"tags":1258},"2025.06.19",[1215],{"path":1260,"title":1261,"description":1262,"meta":1263},"\u002Fko\u002Fdebounce-guide","debounce와 throttle이란??","반복되는 함수 호출을 제어하는 기술인 디바운스(debounce)와 쓰로틀(throttle)을 알아보자.",{"date":1264,"field":1241,"tags":1265},"2025.03.26",[1215],{"path":1267,"title":1268,"description":1269,"meta":1270},"\u002Fko\u002Fdeclarative-vs-procedural-programming","선언적 프로그래밍 vs 절차형 프로그래밍","선언적 프로그래밍과 절차형 프로그래밍은 서로 대비되는 대표적인 패러다임이다. 두 패러다임의 차이를 알아보고, 각 패러다임에 해당하는 언어와 Vue\u002FReact의 특징을 살펴보자.",{"date":1271,"field":1213,"tags":1272},"2025.05.24",[1273,1274,1275],"paradigm","vue","react",{"path":1277,"title":1278,"description":1279,"meta":1280},"\u002Fko\u002Fdesign-guide","Design-Guide","A comprehensive guide on design principles and best practices for developers.",{"date":1281,"field":1241,"tags":1282},"2025.01.19",[1283],"design-system",{"path":1285,"title":1286,"description":1287,"meta":1288},"\u002Fko\u002Fdikw-pyramid","DIKW 피라미드 정리: 데이터가 지혜가 되기까지","Data, Information, Knowledge, Wisdom 4단계로 구성된 DIKW 피라미드 개념을 로그\u002F분석 데이터 같은 실제 개발 사례에 대입해서 정리한다.",{"date":1289,"field":1213,"tags":1290},"2026.09.18",[1291],"data",{"path":1293,"title":1294,"description":1295,"meta":1296},"\u002Fko\u002Fdisplaying-commits-with-github-actions","깃허브 액션으로 커밋 로그를 블로그에 띄우기: Nuxt와 Vercel 활용법","깃허브 액션을 통해 커밋 데이터를 자동으로 수집하고, Nuxt와 Vercel로 UI에 노출하는 방법을 소개합니다.",{"date":1297,"image":1298,"field":1299,"tags":1300},"2025.02.28","github-commits.jpg","infra",[1301,1302,1303],"github-actions","vercel","nuxt",{"path":1305,"title":1306,"description":1307,"meta":1308},"\u002Fko\u002Fdocker-auto-build-run","로컬에서 커밋 시 도커 빌드 후 컨테이너 시작하기","로컬에서 커밋 시 도커 빌드 후 컨테이너 시작하기.",{"date":1309,"field":1299,"tags":1310},"2025.03.10",[1311],"docker",{"path":1313,"title":1314,"description":1315,"meta":1316},"\u002Fko\u002Fhow-to-mount-storage","라즈베리파이4에 저장장치 마운트 시키기","라즈베리파이에 저장장치를 연결하고 파티션 생성부터 포맷, 마운트까지의 전체 과정을 정리한 기록입니다.",{"date":1317,"field":1299,"tags":1318},"2025.07.11",[1319,1320],"raspberry-pi","linux",{"path":1322,"title":1323,"description":1323,"meta":1324},"\u002Fko\u002Fhtml","HTML이란 무엇인가",{"date":1239,"image":1325,"field":1241,"tags":1326},"html.jpg",[1251],{"path":1328,"title":1329,"description":1330,"meta":1331},"\u002Fko\u002Fhtml-features-without-javascript","자바스크립트 없이 HTML만으로 되는 것들: 아직도 div로 다 만들고 있다면","아코디언, 팝오버, 모달, 자동완성, 폼 검증, 이미지 지연 로딩까지. 라이브러리 없이 HTML 태그와 속성만으로 해결되는 기능을 예제와 함께 정리했다. 코드는 줄고 접근성은 덤으로 따라온다.",{"date":1212,"field":1241,"tags":1332},[1251],{"path":1334,"title":1335,"description":1336,"meta":1337},"\u002Fko\u002Fhtml2","미치광이 카드 컴포넌트","미치광이 카드 컴포넌트는 어떤 느낌일까",{"date":1338,"field":1241,"tags":1339},"2025.03.28",[1251,1243],{"path":1341,"title":1342,"description":1342,"meta":1343},"\u002Fko\u002Fhttps-guide","Let's Encrypt를 이용해서 HTTPS 적용하기",{"date":1344,"field":1299,"tags":1345},"2025.03.11",[1346],"https",{"path":1217,"title":5,"description":1209,"meta":1348},{"date":1212,"field":1213,"tags":1349},[1215,1216],{"path":1351,"title":1352,"description":1353,"meta":1354},"\u002Fko\u002Fjs-data-01","JavaScript 데이터01","JavaScript 데이터 다루는 문제를 풀어보자",{"date":1355,"field":1241,"tags":1356},"2025.03.08",[1215],{"path":1358,"title":1359,"description":1360,"meta":1361},"\u002Fko\u002Fjs-data-02","JavaScript 데이터02","복잡한 데이터 구조를 다루는 실전 문제를 해결해보자.",{"date":1355,"field":1241,"tags":1362},[1215],{"path":1364,"title":1365,"description":1366,"meta":1367},"\u002Fko\u002Fmarkdown-guid","마크다운 문법","마크다운 문법을 알아보자",{"date":1368,"image":1369,"field":1229,"tags":1370},"2025.03.02","markdown.webp",[1371],"markdown",{"path":1373,"title":1374,"description":1375,"meta":1376},"\u002Fko\u002Fminio-guide","MinIO 완벽 가이드: 설치부터 Node.js 연동까지","MinIO는 오픈소스 기반의 고성능 객체 스토리지 서버입니다. AWS S3와 호환되는 API를 제공하여 클라우드 네이티브 애플리케이션을 위한 완벽한 스토리지 솔루션으로 자리잡고 있습니다.",{"date":1377,"field":1378,"tags":1379},"2025.11.23","backend",[1380,1216],"minio",{"path":1382,"title":1383,"description":1384,"meta":1385},"\u002Fko\u002Fnode-simple-server","Node.js 간단한 서버 만들기","Node.js로 간단한 HTTP 서버를 만드는 방법",{"date":1386,"field":1378,"tags":1387},"2025.01.30",[1216],{"path":1389,"title":1390,"description":1391,"meta":1392},"\u002Fko\u002Fnuxt-colormode","Nuxt3 다크모드 적용하기","Nuxt3에 다크모드 적용하여 관리하기",{"date":1249,"field":1241,"tags":1393},[1303],{"path":1395,"title":1396,"description":1397,"meta":1398},"\u002Fko\u002Fnuxt-content-vercel-empty-query-sqlite","배포만 하면 블로그 글이 하나도 안 보이던 버그: Nuxt Content와 Vercel의 \u002Ftmp 문제","로컬에서는 멀쩡한데 Vercel에 올리면 queryCollection()이 빈 배열만 반환하던 버그를 추적한 과정. 원인은 @nuxt\u002Fcontent가 Vercel의 읽기 전용 파일시스템을 지원하기 전 버전에 고정돼 있었던 것.",{"date":1399,"field":1229,"tags":1400},"2026.10.07",[1233,1302],{"path":1402,"title":1403,"description":1404,"meta":1405},"\u002Fko\u002Fnuxt-runtimeconfig-appconfig","Nuxt 설정 runtimeConfig vs app.config 완전 정복","Nuxt3에서 전역 설정을 어떻게 관리하고, 어떤 기준으로 나누어야 할까? 다크모드 예시부터 실무 기준 설정 전략까지 총정리",{"date":1406,"field":1241,"tags":1407},"2025.06.16",[1303],{"path":1409,"title":1410,"description":1411,"meta":1412},"\u002Fko\u002Fnuxt-useasyncdata-cache-error","useAsyncData 캐싱 문제와 Key 동적 설정으로 해결하기","이전 포스트의 데이터가 계속 출력되는 캐싱 관련 문제를 해결하는 방법",{"date":1413,"field":1241,"tags":1414,"image":1415},"2025.01.24",[1303],"cache.jpg",{"path":1417,"title":1418,"description":1419,"meta":1420},"\u002Fko\u002Fnuxt-vercel-sql-error","Nuxt 3에서 Vercel 배포 시 발생한 md 파일 로딩 문제 해결기","Nuxt 3 웹 어플리케이션에서 Vercel 배포 환경에서 발생한 md 파일 로딩 문제를 분석하고 해결한 방법에 대해 공유합니다.",{"date":1413,"image":1421,"field":1241,"tags":1422},"ssg.jpg",[1303,1233,1302],{"path":1424,"title":1425,"description":1426,"meta":1427},"\u002Fko\u002Fnuxt3-pinia-guide","nuxt3 Pinia 적용하기","Nuxt3 프로젝트에서 Pinia를 적용해보자 @pinia\u002Fnuxt",{"date":1249,"field":1241,"tags":1428},[1303,1429],"pinia",{"path":1431,"title":1432,"description":1433,"meta":1434},"\u002Fko\u002Fnuxt3-unit-test","Nuxt 프로젝트에 유닛 테스트 코드 추가하기","Nuxt 3 프로젝트에서 Vitest를 활용한 테스트 환경 설정과 유틸리티 함수 테스트 방법",{"date":1435,"image":1436,"field":1241,"tags":1437},"2025.03.07","unit-test.jpg",[1303,1438],"vitest",{"path":1440,"title":1441,"description":1442,"meta":1443},"\u002Fko\u002Fpm2-guide","프로세스 매니저 PM2","배포환경에서 유용하게 사용할 수 있는 프로세스 매니저를 알아보자.",{"date":1309,"field":1299,"tags":1444},[1445,1216],"pm2",{"path":1447,"title":1448,"description":1449,"meta":1450},"\u002Fko\u002Freact-createportal","React createPortal 완벽 가이드","React의 `createPortal`을 활용한 UI 구현법, 활용 사례, 장단점 및 코드 예제를 소개합니다.",{"date":1451,"field":1241,"tags":1452},"2025.01.27",[1275],{"path":1454,"title":1455,"description":1456,"meta":1457},"\u002Fko\u002Frouting","Routing","Nuxt file-system routing creates a route for every file in the pages\u002F directory.",{"date":1458,"field":1241,"tags":1459},"2025.01.22",[1303],{"path":1461,"title":1462,"description":1463,"meta":1464},"\u002Fko\u002Ftanstack-query-guide","TanStack Query란 무엇인가?","TanStack Query가 웹 애플리케이션에서 데이터 가져오기, 캐싱, 상태 관리를 어떻게 개선하는지 알아본다.",{"date":1465,"field":1241,"tags":1466},"2025.04.15",[1275,1467],"tanstack-query",{"path":1469,"title":1470,"description":1471,"meta":1472},"\u002Fko\u002Fuse-context","리엑트 상태관리 useContext()","React에서 `useContext`를 사용하면 컴포넌트 트리에 깊이 중첩된 컴포넌트에서도 prop drilling 없이 상태를 공유할 수 있습니다. 이 글에서는 `useContext`의 개념, 사용 방법, 장단점, 그리고 실전 예제를 함께 알아보겠습니다.",{"date":1473,"field":1241,"tags":1474},"2025.02.03",[1275],{"path":1476,"title":1477,"description":1478,"meta":1479},"\u002Fko\u002Fvite-plugin-style-inject","리엑티 컴포넌트를 패키지 빌드 시 CSS 같이 적용시키기","dist에 css에 파일이 생성되었으나, 적용 안 되는 이유를 알아보고 에러를 처리하자",{"date":1480,"field":1241,"tags":1481},"2025.04.06",[1482,1275],"vite",{"path":1484,"title":1485,"description":1486,"meta":1487},"\u002Fko\u002Fvue-component-guide","Vue의 \u003Ccomponent :is \u002F> 완벽 정리: 동적 렌더링의 모든 것","Vue에서 \u003Ccomponent :is \u002F>를 활용한 동적 컴포넌트 렌더링 방법과 VNode, 컴포넌트 객체의 차이를 실전 예제로 이해하기",{"date":1239,"field":1241,"tags":1488},[1274],{"path":1490,"title":1491,"description":1492,"meta":1493},"\u002Fko\u002Fvue-query-ref-value-trap-nuxt3","Vue Query에서 데이터가 안 보이는 이유? Nuxt 3와 Ref 구조의 함정","Nuxt 3 + vue-query를 사용할 때 API 데이터가 `v-if` 조건문에서 제대로 표시되지 않는 현상을 해결한 경험을 바탕으로, Ref 구조와 template 디렉티브에서의 차이를 자세히 설명합니다.",{"date":1494,"field":1241,"tags":1495},"2025.06.08",[1303,1274,1467],1791446619223]