[{"data":1,"prerenderedAt":17984},["ShallowReactive",2],{"blog":3,"blog-search-sections":17539,"blog-tags-subjects":17934},[4,851,1510,1861,2863,2996,3270,7538,7677,8471,8605,9313,9479,12326,12527,13056,17476],{"id":5,"title":6,"body":7,"created":837,"description":838,"extension":839,"filename":840,"meta":841,"navigation":842,"path":843,"seo":844,"sitemap":845,"stem":846,"subject":847,"tags":848,"updated":837,"volume":849,"__hash__":850},"blog\u002Fblog\u002F250724-interact-text-overflow-white-space-width.md","말줄임 시 text-overflow 와 white-space, width의 관계",{"type":8,"value":9,"toc":828},"minimark",[10,15,19,44,48,73,77,134,141,494,497,507,510,517,528,531,535,628,631,725,732,745,749,752,755,758,766,771,806,809,812,818,824],[11,12,14],"h1",{"id":13},"box-model","Box model",[16,17,18],"p",{},"![Untitled](blog\u002Fimg\u002Finteract-text-overflow-white-space-width\u002Fimage copy.png)",[20,21,22,35,38,41],"ul",{},[23,24,25,26,30,31,34],"li",{},"Content: 실제 내용이 들어가는 영역. ",[27,28,29],"code",{},"width","와 ",[27,32,33],{},"height","가 기본적으로 지정하는 영역.",[23,36,37],{},"Padding: 콘텐츠와 테두리 사이의 여백인데 배경색은 이 영역까지 적용됨.",[23,39,40],{},"Border: 패딩을 감싸는 선.",[23,42,43],{},"Margin: 테두리 바깥의 공간으로, 다른 요소와의 간격을 벌릴 때 사용. 배경색이 적용되지 않는 투명한 영역",[45,46,47],"h2",{"id":47},"box-sizing",[20,49,50,65],{},[23,51,52,53],{},"content-box : 컨텐츠 영역만 width 로 잡겠다.\n",[20,54,55],{},[23,56,57,60,61,64],{},[27,58,59],{},"width: 100px",", ",[27,62,63],{},"padding: 20px","인 경우 : 전체 너비는 140px이 된다.",[23,66,67,68],{},"border-box : 테두리까지 포함한 크기가 width 이다. (margin 은 아님)\n",[20,69,70],{},[23,71,72],{},"전체 너비가 100px 이고 컨텐츠 영역이 60px 로 줄어든다.",[11,74,76],{"id":75},"_1줄-말줄임","1줄 말줄임",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-css shiki shiki-themes github-light github-dark",".line-clamp-1-normal {\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n","css","",[27,85,86,99,115,128],{"__ignoreMap":83},[87,88,91,95],"span",{"class":89,"line":90},"line",1,[87,92,94],{"class":93},"sScJk",".line-clamp-1-normal",[87,96,98],{"class":97},"sVt8B"," {\n",[87,100,102,106,109,112],{"class":89,"line":101},2,[87,103,105],{"class":104},"sj4cs","  overflow",[87,107,108],{"class":97},": ",[87,110,111],{"class":104},"hidden",[87,113,114],{"class":97},";\n",[87,116,118,121,123,126],{"class":89,"line":117},3,[87,119,120],{"class":104},"  text-overflow",[87,122,108],{"class":97},[87,124,125],{"class":104},"ellipsis",[87,127,114],{"class":97},[87,129,131],{"class":89,"line":130},4,[87,132,133],{"class":97},"}\n",[16,135,136,137,140],{},"부모의 width 가 결정되어 있는 경우에는 ",[27,138,139],{},"white-space: nowrap"," 이 없어도 말줄임이 발생한다.",[78,142,146],{"className":143,"code":144,"language":145,"meta":83,"style":83},"language-html shiki shiki-themes github-light github-dark","\u003Cdiv class=\"w-80 overflow-hidden\">\n    \u003Cdiv class=\"flex items-center\">\n        \u003Cdiv class=\"w-6 h-6\">1\u003C\u002Fdiv>\n        \u003Cdiv class=\"flex-1 min-w-0\">\n            \u003Cdiv class=\"text-overflow-ellipsis overflow-hidden\">white-space: nowrap 없어서 줄바꿈되고 말줄임도 안일어남\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv>2024.01.01\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n    \u003Cdiv class=\"flex items-center\">\n        \u003Cdiv class=\"w-6 h-6\">2\u003C\u002Fdiv>\n        \u003Cdiv class=\"flex-1 min-w-0\">\n            \u003Cdiv class=\"truncate\">의도한대로 줄바꿈이 일어난 경우입니다람쥐\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv>2024.01.02\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n    \u003Cdiv class=\"flex items-center\">\n        \u003Cdiv class=\"w-6 h-6\">3\u003C\u002Fdiv>\n        \u003Cdiv class=\"flex-1\">\n            \u003Cdiv class=\"truncate\">min-width: 0 없어서 줄어들지 않음\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv>2024.01.03\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n","html",[27,147,148,170,186,207,222,244,254,268,278,293,313,328,349,358,372,381,396,416,432,452,461,475,484],{"__ignoreMap":83},[87,149,150,153,157,160,163,167],{"class":89,"line":90},[87,151,152],{"class":97},"\u003C",[87,154,156],{"class":155},"s9eBZ","div",[87,158,159],{"class":93}," class",[87,161,162],{"class":97},"=",[87,164,166],{"class":165},"sZZnC","\"w-80 overflow-hidden\"",[87,168,169],{"class":97},">\n",[87,171,172,175,177,179,181,184],{"class":89,"line":101},[87,173,174],{"class":97},"    \u003C",[87,176,156],{"class":155},[87,178,159],{"class":93},[87,180,162],{"class":97},[87,182,183],{"class":165},"\"flex items-center\"",[87,185,169],{"class":97},[87,187,188,191,193,195,197,200,203,205],{"class":89,"line":117},[87,189,190],{"class":97},"        \u003C",[87,192,156],{"class":155},[87,194,159],{"class":93},[87,196,162],{"class":97},[87,198,199],{"class":165},"\"w-6 h-6\"",[87,201,202],{"class":97},">1\u003C\u002F",[87,204,156],{"class":155},[87,206,169],{"class":97},[87,208,209,211,213,215,217,220],{"class":89,"line":130},[87,210,190],{"class":97},[87,212,156],{"class":155},[87,214,159],{"class":93},[87,216,162],{"class":97},[87,218,219],{"class":165},"\"flex-1 min-w-0\"",[87,221,169],{"class":97},[87,223,225,228,230,232,234,237,240,242],{"class":89,"line":224},5,[87,226,227],{"class":97},"            \u003C",[87,229,156],{"class":155},[87,231,159],{"class":93},[87,233,162],{"class":97},[87,235,236],{"class":165},"\"text-overflow-ellipsis overflow-hidden\"",[87,238,239],{"class":97},">white-space: nowrap 없어서 줄바꿈되고 말줄임도 안일어남\u003C\u002F",[87,241,156],{"class":155},[87,243,169],{"class":97},[87,245,247,250,252],{"class":89,"line":246},6,[87,248,249],{"class":97},"        \u003C\u002F",[87,251,156],{"class":155},[87,253,169],{"class":97},[87,255,257,259,261,264,266],{"class":89,"line":256},7,[87,258,190],{"class":97},[87,260,156],{"class":155},[87,262,263],{"class":97},">2024.01.01\u003C\u002F",[87,265,156],{"class":155},[87,267,169],{"class":97},[87,269,271,274,276],{"class":89,"line":270},8,[87,272,273],{"class":97},"    \u003C\u002F",[87,275,156],{"class":155},[87,277,169],{"class":97},[87,279,281,283,285,287,289,291],{"class":89,"line":280},9,[87,282,174],{"class":97},[87,284,156],{"class":155},[87,286,159],{"class":93},[87,288,162],{"class":97},[87,290,183],{"class":165},[87,292,169],{"class":97},[87,294,296,298,300,302,304,306,309,311],{"class":89,"line":295},10,[87,297,190],{"class":97},[87,299,156],{"class":155},[87,301,159],{"class":93},[87,303,162],{"class":97},[87,305,199],{"class":165},[87,307,308],{"class":97},">2\u003C\u002F",[87,310,156],{"class":155},[87,312,169],{"class":97},[87,314,316,318,320,322,324,326],{"class":89,"line":315},11,[87,317,190],{"class":97},[87,319,156],{"class":155},[87,321,159],{"class":93},[87,323,162],{"class":97},[87,325,219],{"class":165},[87,327,169],{"class":97},[87,329,331,333,335,337,339,342,345,347],{"class":89,"line":330},12,[87,332,227],{"class":97},[87,334,156],{"class":155},[87,336,159],{"class":93},[87,338,162],{"class":97},[87,340,341],{"class":165},"\"truncate\"",[87,343,344],{"class":97},">의도한대로 줄바꿈이 일어난 경우입니다람쥐\u003C\u002F",[87,346,156],{"class":155},[87,348,169],{"class":97},[87,350,352,354,356],{"class":89,"line":351},13,[87,353,249],{"class":97},[87,355,156],{"class":155},[87,357,169],{"class":97},[87,359,361,363,365,368,370],{"class":89,"line":360},14,[87,362,190],{"class":97},[87,364,156],{"class":155},[87,366,367],{"class":97},">2024.01.02\u003C\u002F",[87,369,156],{"class":155},[87,371,169],{"class":97},[87,373,375,377,379],{"class":89,"line":374},15,[87,376,273],{"class":97},[87,378,156],{"class":155},[87,380,169],{"class":97},[87,382,384,386,388,390,392,394],{"class":89,"line":383},16,[87,385,174],{"class":97},[87,387,156],{"class":155},[87,389,159],{"class":93},[87,391,162],{"class":97},[87,393,183],{"class":165},[87,395,169],{"class":97},[87,397,399,401,403,405,407,409,412,414],{"class":89,"line":398},17,[87,400,190],{"class":97},[87,402,156],{"class":155},[87,404,159],{"class":93},[87,406,162],{"class":97},[87,408,199],{"class":165},[87,410,411],{"class":97},">3\u003C\u002F",[87,413,156],{"class":155},[87,415,169],{"class":97},[87,417,419,421,423,425,427,430],{"class":89,"line":418},18,[87,420,190],{"class":97},[87,422,156],{"class":155},[87,424,159],{"class":93},[87,426,162],{"class":97},[87,428,429],{"class":165},"\"flex-1\"",[87,431,169],{"class":97},[87,433,435,437,439,441,443,445,448,450],{"class":89,"line":434},19,[87,436,227],{"class":97},[87,438,156],{"class":155},[87,440,159],{"class":93},[87,442,162],{"class":97},[87,444,341],{"class":165},[87,446,447],{"class":97},">min-width: 0 없어서 줄어들지 않음\u003C\u002F",[87,449,156],{"class":155},[87,451,169],{"class":97},[87,453,455,457,459],{"class":89,"line":454},20,[87,456,249],{"class":97},[87,458,156],{"class":155},[87,460,169],{"class":97},[87,462,464,466,468,471,473],{"class":89,"line":463},21,[87,465,190],{"class":97},[87,467,156],{"class":155},[87,469,470],{"class":97},">2024.01.03\u003C\u002F",[87,472,156],{"class":155},[87,474,169],{"class":97},[87,476,478,480,482],{"class":89,"line":477},22,[87,479,273],{"class":97},[87,481,156],{"class":155},[87,483,169],{"class":97},[87,485,487,490,492],{"class":89,"line":486},23,[87,488,489],{"class":97},"\u003C\u002F",[87,491,156],{"class":155},[87,493,169],{"class":97},[16,495,496],{},"![Untitled](blog\u002Fimg\u002Finteract-text-overflow-white-space-width\u002Fimage copy2.png)",[16,498,499,500,502,503,506],{},"flexbox 내부에서는 자식 컴포넌트의 말줄임을 할 땐, 너비에 맞춰서 자동으로 줄바꿈이 되기 때문에 ",[27,501,139],{}," 속성이 필요해진다.\nflexbox 안의 다른 요소와 함께 너비가 결정되는 경우 ",[27,504,505],{},"min-width: 0px;"," 가 필요하기도 하다.",[45,508,509],{"id":509},"min-w-0",[16,511,512],{},[513,514],"img",{"alt":515,"src":516},"Untitled","blog\u002Fimg\u002Finteract-text-overflow-white-space-width\u002Fimage.png",[16,518,519,520,523,524,527],{},"기본적으로 ",[27,521,522],{},"min-width : auto;"," 로 작동한다. 이는 최소한의 너비를 보장하라는 뜻이다. 보통을 글자의 경우 자동 줄바꿈이 되는데, ",[27,525,526],{},"whitespace-nowrap"," 과 만나게 되면 자신의 너비를 유지하기 위해서 overflow 가 발생한다. 부모 컨테이너의 너비가 한정되어 있어도 줄바꿈을 절대 하지 않기 때문에 벗어나면서 컨텐츠 영역이 보장되기 때문에 overflow 가 발생하지 않는다.",[16,529,530],{},"min-width: 0px; 를 주게되면. 0px 까지 줄어들 수 있어 라는 뜻이 된다. 그래서 최소 너비를 보장하지 않아도, 줄어들 수 있고, nowrap 과 만나도, 줄어들 수 있기 때문에 text overflow 가 발생할 수 있게 된다.",[45,532,534],{"id":533},"_2줄-말줄임","2줄 말줄임",[78,536,538],{"className":80,"code":537,"language":82,"meta":83,"style":83},".\btext-over {\n    max-width: 253px;\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n",[27,539,540,550,566,578,590,602,613,624],{"__ignoreMap":83},[87,541,542,545,548],{"class":89,"line":90},[87,543,544],{"class":97},".\b",[87,546,547],{"class":155},"text-over",[87,549,98],{"class":97},[87,551,552,555,557,560,564],{"class":89,"line":101},[87,553,554],{"class":104},"    max-width",[87,556,108],{"class":97},[87,558,559],{"class":104},"253",[87,561,563],{"class":562},"szBVR","px",[87,565,114],{"class":97},[87,567,568,571,573,576],{"class":89,"line":117},[87,569,570],{"class":104},"    display",[87,572,108],{"class":97},[87,574,575],{"class":104},"-webkit-box",[87,577,114],{"class":97},[87,579,580,583,585,588],{"class":89,"line":130},[87,581,582],{"class":104},"    -webkit-line-clamp",[87,584,108],{"class":97},[87,586,587],{"class":104},"2",[87,589,114],{"class":97},[87,591,592,595,597,600],{"class":89,"line":224},[87,593,594],{"class":104},"    -webkit-box-orient",[87,596,108],{"class":97},[87,598,599],{"class":104},"vertical",[87,601,114],{"class":97},[87,603,604,607,609,611],{"class":89,"line":246},[87,605,606],{"class":104},"    overflow",[87,608,108],{"class":97},[87,610,111],{"class":104},[87,612,114],{"class":97},[87,614,615,618,620,622],{"class":89,"line":256},[87,616,617],{"class":104},"    text-overflow",[87,619,108],{"class":97},[87,621,125],{"class":104},[87,623,114],{"class":97},[87,625,626],{"class":89,"line":270},[87,627,133],{"class":97},[16,629,630],{},"이렇게만 했을 때는 분명 2줄 말줄임이었는데 1줄 말줄임으로 표시되었다. css 만 봐서는 문제가 없어보였다.",[78,632,634],{"className":80,"code":633,"language":82,"meta":83,"style":83},".text-over {\n    max-width: 253px;\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: normal; \u002F\u002F 이게 들어가야 함\n}\n",[27,635,636,643,655,665,675,685,695,705,721],{"__ignoreMap":83},[87,637,638,641],{"class":89,"line":90},[87,639,640],{"class":93},".text-over",[87,642,98],{"class":97},[87,644,645,647,649,651,653],{"class":89,"line":101},[87,646,554],{"class":104},[87,648,108],{"class":97},[87,650,559],{"class":104},[87,652,563],{"class":562},[87,654,114],{"class":97},[87,656,657,659,661,663],{"class":89,"line":117},[87,658,570],{"class":104},[87,660,108],{"class":97},[87,662,575],{"class":104},[87,664,114],{"class":97},[87,666,667,669,671,673],{"class":89,"line":130},[87,668,582],{"class":104},[87,670,108],{"class":97},[87,672,587],{"class":104},[87,674,114],{"class":97},[87,676,677,679,681,683],{"class":89,"line":224},[87,678,594],{"class":104},[87,680,108],{"class":97},[87,682,599],{"class":104},[87,684,114],{"class":97},[87,686,687,689,691,693],{"class":89,"line":246},[87,688,606],{"class":104},[87,690,108],{"class":97},[87,692,111],{"class":104},[87,694,114],{"class":97},[87,696,697,699,701,703],{"class":89,"line":256},[87,698,617],{"class":104},[87,700,108],{"class":97},[87,702,125],{"class":104},[87,704,114],{"class":97},[87,706,707,710,712,715,718],{"class":89,"line":270},[87,708,709],{"class":104},"    white-space",[87,711,108],{"class":97},[87,713,714],{"class":104},"normal",[87,716,717],{"class":97},";",[87,719,720],{"class":97}," \u002F\u002F 이게 들어가야 함\n",[87,722,723],{"class":89,"line":280},[87,724,133],{"class":97},[16,726,727,728,731],{},"그런데 나도 모르게 상속된 ",[27,729,730],{},"white-space"," 속성이 줄바꿈을 방지해서 1줄까지만 표시되고 말줄임이 일어났던 것이다.",[16,733,734,737,738,740,741,744],{},[27,735,736],{},"-webkit-line-clamp"," 가 2이상 작동하려면 일단 줄바꿈이 일어나야 한다.  ",[27,739,730],{}," 를 지정해주어야 정해진 줄 수에서 말줄임이 일어난다. ",[27,742,743],{},"white-space: nowrap;"," 인 경우는 애초에 줄바꿈이 일어나지 않기 때문에 2줄이 되지 않는다.",[45,746,748],{"id":747},"알아두면-좋을-다른-auto-속성들","알아두면 좋을 다른 auto 속성들",[16,750,751],{},"다른 auto 속성이 어떻게 작동하는지 몇개 찾아봤다.",[753,754,29],"h3",{"id":29},[16,756,757],{},"width 는 element 가 block 이냐 inline 이냐에 따라서 다르다.",[20,759,760,763],{},[23,761,762],{},"block : 가로 전체 100%",[23,764,765],{},"inline : 컨텐츠의 너비만큼",[767,768,770],"h4",{"id":769},"_100-와의-차이점","100% 와의 차이점",[20,772,773,784],{},[23,774,775,776],{},"width: 100% : border, padding 제외 컨텐츠 영역만 100%\n",[20,777,778],{},[23,779,780,783],{},[27,781,782],{},"box-sizing: border-box;"," 를 주면 auto 와 마찬가지로 작동함",[23,785,786,787,791,792,795,796,795,799,795,802,805],{},"width: auto : 해당 요소의 ",[788,789,790],"strong",{},"전체 너비","(",[27,793,794],{},"margin"," + ",[27,797,798],{},"border",[27,800,801],{},"padding",[27,803,804],{},"content",")가 부모 요소의 콘텐츠 영역(content area)에 맞춰진다. padding 값을 추가하면 컨텐츠 영역은 줄어든다는 것.",[753,807,808],{"id":808},"table",[767,810,811],{"id":811},"table-layout",[16,813,814,817],{},[27,815,816],{},"\u003Ctable>"," 의 열 너비값을 어떻게 정할지 정한다. auto 가 기본이라서 가장 넓은 콘텐츠에 맞춰서 자동으로 지정된다. -> 컨텐츠의 크기를 모두 게산하고 나서야 열의 너비를 알고 렌더링하기 때문에 속도가 조금 느릴 수 있음.",[16,819,820,823],{},[27,821,822],{},"fixed"," 를 사용하면 첫 행의 너비만으로 결정한다.",[825,826,827],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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}",{"title":83,"searchDepth":101,"depth":101,"links":829},[830,831,832,833],{"id":47,"depth":101,"text":47},{"id":509,"depth":101,"text":509},{"id":533,"depth":101,"text":534},{"id":747,"depth":101,"text":748,"children":834},[835,836],{"id":29,"depth":117,"text":29},{"id":808,"depth":117,"text":808},"2025-07-24","텍스트의 말줄임을 구현할 때 text-overflow 와 white-space 사이의 상호작용을 알아봄. 몇 줄에서 줄이느냐에 따라서도 달라진다. flex 박스 모델에서 자식 요소가 빠져나가는 현상과 min-width-0 의 역할 알아보기","md","interact-text-overflow-white-space-width",{},true,"\u002Fblog\u002F250724-interact-text-overflow-white-space-width",{"title":6,"description":838},{"loc":843},"blog\u002F250724-interact-text-overflow-white-space-width","CSS",[82],"medium","xyv5zlxX3xfR2dVTlDKsAyh-IniB8dSIfgRwQdfL4uw",{"id":852,"title":853,"body":854,"created":1497,"description":1498,"extension":839,"filename":1499,"meta":1500,"navigation":842,"path":1501,"seo":1502,"sitemap":1503,"stem":1504,"subject":1505,"tags":1506,"updated":1497,"volume":849,"__hash__":1509},"blog\u002Fblog\u002F250819-exif.md","EXIF(Exchangeable Image File Format) 데이터",{"type":8,"value":855,"toc":1491},[856,859,939,942,946,953,964,981,994,1090,1098,1103,1107,1115,1118,1125,1129,1132,1135,1141,1144,1148,1300,1306,1310,1318,1321,1330,1485,1488],[16,857,858],{},"**EXIF(Exchangeable Image File Format)**는 사진 이미지 데이터 자체 외에, 그 사진이 어떻게, 언제, 어디서, 무엇으로 찍혔는지에 대한 구체적인 **메타데이터(데이터에 대한 데이터)**를 담고 있다.",[20,860,861,881,919],{},[23,862,863,866,867],{},[788,864,865],{},"카메라 정보",": 어떤 장비로 사진을 찍었는지 알려준다.\n",[20,868,869,875],{},[23,870,871,874],{},[788,872,873],{},"카메라 제조사 및 모델",": 예) SAMSUNG, Apple, SM-S928N, iPhone 15 Pro",[23,876,877,880],{},[788,878,879],{},"렌즈 정보",": 사용된 렌즈의 종류, 초점 거리 등",[23,882,883,886,887],{},[788,884,885],{},"촬영 설정값",": 사진을 어떤 설정으로 찍었는지에 대한 기술적인 정보.\n",[20,888,889,895,901,907],{},[23,890,891,894],{},[788,892,893],{},"ISO 감도",": 빛에 얼마나 민감하게 반응했는지",[23,896,897,900],{},[788,898,899],{},"조리개 값 (F-stop)",": 렌즈를 얼마나 열었는지 (심도 표현과 관련)",[23,902,903,906],{},[788,904,905],{},"셔터 속도",": 얼마나 빠른 속도로 빛을 받아들였는지 (움직임 표현과 관련)",[23,908,909,60,912,60,915,918],{},[788,910,911],{},"노출 보정",[788,913,914],{},"화이트 밸런스",[788,916,917],{},"플래시 사용 여부"," 등",[23,920,921,924,925],{},[788,922,923],{},"시간 및 위치 정보",": 사진 찍은 시간, 위경도\n",[20,926,927,933],{},[23,928,929,932],{},[788,930,931],{},"촬영 날짜 및 시간",": 사진을 찍은 정확한 날짜와 시간.",[23,934,935,938],{},[788,936,937],{},"GPS 좌표 (위도, 경도)",": 카메라의 위치 정보 기록 기능이 켜져 있어야 함.",[16,940,941],{},"내가 필요한 건 시간 및 위치 정보가 남기를 바란다.",[11,943,945],{"id":944},"webview-태그를-통해서-업로드-시","webview 태그를 통해서 업로드 시",[16,947,948,949,952],{},"브라우저에서 ",[27,950,951],{},"\u003Cinput type=\"file\">"," 를 통해 파일을 선택하면, 안드로이드의 파일 선택기가 열리고, 브라우저는 파일의 복사본을 만들어둔다. 이 과정에서 브라우저는 개인정보 유출을 막기 위해서 EXIF 데이터를 의도적으로 삭제한다.",[16,954,955,956,959,960,963],{},"웹뷰의 경우 ",[27,957,958],{},"onShowFileChooser"," 에서 개발자가 EXIF 정보가 보존되게 할 수 있다. ",[27,961,962],{},"ACCESS_MEDIA_LOCATION"," 권한을 사용자에게 허가받으면 파일의 위치 정보를 보존할 수 있다",[78,965,969],{"className":966,"code":967,"language":968,"meta":83,"style":83},"language-xml shiki shiki-themes github-light github-dark","\u003Cuses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" \u002F>\n\u003Cuses-permission android:name=\"android.permission.ACCESS_MEDIA_LOCATION\" \u002F>\n","xml",[27,970,971,976],{"__ignoreMap":83},[87,972,973],{"class":89,"line":90},[87,974,975],{},"\u003Cuses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" \u002F>\n",[87,977,978],{"class":89,"line":101},[87,979,980],{},"\u003Cuses-permission android:name=\"android.permission.ACCESS_MEDIA_LOCATION\" \u002F>\n",[16,982,983,985,986,989,990,993],{},[27,984,958],{}," 에서 파일 선택 결과를 ",[27,987,988],{},"ActivityResultLauncher"," 로 받는데, 이때 Uri 를 가공하지 않고 그대로 ",[27,991,992],{},"filePathCallback.onReceiveValue()"," 에 전달하면 된다.",[78,995,999],{"className":996,"code":997,"language":998,"meta":83,"style":83},"language-kotlin shiki shiki-themes github-light github-dark","\u002F\u002F EXIF 정보 디버그 출력  \ntry {  \n    val uri = data?.data  \n    if (uri != null) {  \n        activity.contentResolver.openInputStream(uri)?.use { inputStream ->  \n            val exif = ExifInterface(inputStream)  \n            Log.d(TAG, \"Single File EXIF Info:\")  \n            Log.d(TAG, \"  DateTime: ${exif.getAttribute(ExifInterface.TAG_DATETIME)}\")  \n            Log.d(TAG, \"  GPS Latitude: ${exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)}\")  \n            Log.d(TAG, \"  GPS Longitude: ${exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)}\")  \n            Log.d(TAG, \"  Make: ${exif.getAttribute(ExifInterface.TAG_MAKE)}\")  \n            Log.d(TAG, \"  Model: ${exif.getAttribute(ExifInterface.TAG_MODEL)}\")  \n            Log.d(TAG, \"  Orientation: ${exif.getAttribute(ExifInterface.TAG_ORIENTATION)}\")  \n        }  \n    }  \n} catch (e: Exception) {  \n    Log.e(TAG, \"EXIF 정보 읽기 실패\", e)  \n}\n","kotlin",[27,1000,1001,1006,1011,1016,1021,1026,1031,1036,1041,1046,1051,1056,1061,1066,1071,1076,1081,1086],{"__ignoreMap":83},[87,1002,1003],{"class":89,"line":90},[87,1004,1005],{},"\u002F\u002F EXIF 정보 디버그 출력  \n",[87,1007,1008],{"class":89,"line":101},[87,1009,1010],{},"try {  \n",[87,1012,1013],{"class":89,"line":117},[87,1014,1015],{},"    val uri = data?.data  \n",[87,1017,1018],{"class":89,"line":130},[87,1019,1020],{},"    if (uri != null) {  \n",[87,1022,1023],{"class":89,"line":224},[87,1024,1025],{},"        activity.contentResolver.openInputStream(uri)?.use { inputStream ->  \n",[87,1027,1028],{"class":89,"line":246},[87,1029,1030],{},"            val exif = ExifInterface(inputStream)  \n",[87,1032,1033],{"class":89,"line":256},[87,1034,1035],{},"            Log.d(TAG, \"Single File EXIF Info:\")  \n",[87,1037,1038],{"class":89,"line":270},[87,1039,1040],{},"            Log.d(TAG, \"  DateTime: ${exif.getAttribute(ExifInterface.TAG_DATETIME)}\")  \n",[87,1042,1043],{"class":89,"line":280},[87,1044,1045],{},"            Log.d(TAG, \"  GPS Latitude: ${exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)}\")  \n",[87,1047,1048],{"class":89,"line":295},[87,1049,1050],{},"            Log.d(TAG, \"  GPS Longitude: ${exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)}\")  \n",[87,1052,1053],{"class":89,"line":315},[87,1054,1055],{},"            Log.d(TAG, \"  Make: ${exif.getAttribute(ExifInterface.TAG_MAKE)}\")  \n",[87,1057,1058],{"class":89,"line":330},[87,1059,1060],{},"            Log.d(TAG, \"  Model: ${exif.getAttribute(ExifInterface.TAG_MODEL)}\")  \n",[87,1062,1063],{"class":89,"line":351},[87,1064,1065],{},"            Log.d(TAG, \"  Orientation: ${exif.getAttribute(ExifInterface.TAG_ORIENTATION)}\")  \n",[87,1067,1068],{"class":89,"line":360},[87,1069,1070],{},"        }  \n",[87,1072,1073],{"class":89,"line":374},[87,1074,1075],{},"    }  \n",[87,1077,1078],{"class":89,"line":383},[87,1079,1080],{},"} catch (e: Exception) {  \n",[87,1082,1083],{"class":89,"line":398},[87,1084,1085],{},"    Log.e(TAG, \"EXIF 정보 읽기 실패\", e)  \n",[87,1087,1088],{"class":89,"line":418},[87,1089,133],{},[78,1091,1096],{"className":1092,"code":1094,"language":1095},[1093],"language-text","Single File EXIF Info:\n   DateTime: 2025:07:18 08:43:28\n   GPS Latitude: 37\u002F1,0\u002F1,0\u002F100000\n   GPS Longitude: 127\u002F1,0\u002F1,0\u002F00000\n   Make: samsung\n   Model: SM-N960N\n   Orientation: 6\n","text",[27,1097,1094],{"__ignoreMap":83},[16,1099,1100,1102],{},[27,1101,962],{}," 권한을 부여하면 GPS 정보가 정상적으로 기록된다. 그렇지 않으면 0\u002F1과 같은 형태로 기록된다. 따라서 사용자에게 권한을 명시적으로 받아야 한다.",[11,1104,1106],{"id":1105},"웹에서-exif-정보-가져오기","웹에서 exif 정보 가져오기",[1108,1109],"cardlink",{"description":1110,"host":1111,"title":1112,"url":1113,"favicon":1114},"온라인 메타데이터 및 EXIF는 동영상, 오디오, 이미지, 아카이브 및 문서 파일에서 비밀 메타데이터 및 EXIF 정보를 무료로 추출하는 온라인 도구입니다. 다양한 파일 및 메타데이터 형식을 지원합니다.","online-metadata.com","온라인 메타데이터 및 EXIF 무료 뷰어","https:\u002F\u002Fonline-metadata.com\u002Fko","\u002Ffavicon.ico",[16,1116,1117],{},"웹에서 파일 업로드를 했을 때 EXIF 정보가 누락되는 현상이 있다. 하지만 위 사이트에서는 정상적으로 작동한다. 무슨 차이가 있을까?",[16,1119,1120,1121,1124],{},"위 페이지는 파일을 ",[27,1122,1123],{},"\u003Cform>"," 태그를 통해 그대로 서버로 전달하기 때문에 EXIF 데이터의 손실 없이 전달되어 분석이 가능한 것이다. 반면 브라우저 JavaScript에서 파일 데이터에 직접 접근하려고 하면 브라우저 보안 정책상 일부 정보(EXIF 등)가 차단될 수 있다.",[45,1126,1128],{"id":1127},"웹뷰에서-native-로직을-통해-전달된-경우","웹뷰에서 native 로직을 통해 전달된 경우",[16,1130,1131],{},"앱이 OS 권한을 얻어서 브라우저 기본 정책이 아닌 앱의 권한으로 데이터를 직접 전달한다. 따라서 앱을 통해 파일을 선택한 경우 웹에서도 EXIF 데이터를 조회하고 다룰 수 있게 된다.",[16,1133,1134],{},"다만 EXIF 도 개인정보로 취급할 수 있기 때문에 저장과 노출에 주의를 해야한다. 서버에 저장할 경우 정말 필요한 만큼만 특정되지 않을 정도의 정보만 취급하거나, 저장하지 않는 게 좋다.",[1108,1136],{"description":1137,"host":1138,"title":1139,"url":1140},"정부혁신 보다나은정부","www.innovation.go.kr","혁신24 - 정부혁신 홈페이지","https:\u002F\u002Fwww.innovation.go.kr\u002Fucms\u002Fbbs\u002FB0000060\u002Fview.do?nttId=17555&menuNo=300240&searchType=4&pageIndex=1",[16,1142,1143],{},"정부 페이지에서 EXIF 데이터의 위험성과 대응법도 홍보하고 있다.",[753,1145,1147],{"id":1146},"데이터-형식","데이터 형식",[78,1149,1153],{"className":1150,"code":1151,"language":1152,"meta":83,"style":83},"language-ts shiki shiki-themes github-light github-dark","fileList.forEach((file) => {\n    exifr.parse(file)\n        .then((metadata: any) => {\n            console.log(`[EXIF] ${file.name}`, metadata);\n        })\n        .catch((error: any) => {\n            console.error(`[EXIF] Failed to parse EXIF for ${file.name}`, error);\n        });\n});\n","ts",[27,1154,1155,1178,1189,1214,1241,1246,1268,1290,1295],{"__ignoreMap":83},[87,1156,1157,1160,1163,1166,1170,1173,1176],{"class":89,"line":90},[87,1158,1159],{"class":97},"fileList.",[87,1161,1162],{"class":93},"forEach",[87,1164,1165],{"class":97},"((",[87,1167,1169],{"class":1168},"s4XuR","file",[87,1171,1172],{"class":97},") ",[87,1174,1175],{"class":562},"=>",[87,1177,98],{"class":97},[87,1179,1180,1183,1186],{"class":89,"line":101},[87,1181,1182],{"class":97},"    exifr.",[87,1184,1185],{"class":93},"parse",[87,1187,1188],{"class":97},"(file)\n",[87,1190,1191,1194,1197,1199,1202,1205,1208,1210,1212],{"class":89,"line":117},[87,1192,1193],{"class":97},"        .",[87,1195,1196],{"class":93},"then",[87,1198,1165],{"class":97},[87,1200,1201],{"class":1168},"metadata",[87,1203,1204],{"class":562},":",[87,1206,1207],{"class":104}," any",[87,1209,1172],{"class":97},[87,1211,1175],{"class":562},[87,1213,98],{"class":97},[87,1215,1216,1219,1222,1224,1227,1229,1232,1235,1238],{"class":89,"line":130},[87,1217,1218],{"class":97},"            console.",[87,1220,1221],{"class":93},"log",[87,1223,791],{"class":97},[87,1225,1226],{"class":165},"`[EXIF] ${",[87,1228,1169],{"class":97},[87,1230,1231],{"class":165},".",[87,1233,1234],{"class":97},"name",[87,1236,1237],{"class":165},"}`",[87,1239,1240],{"class":97},", metadata);\n",[87,1242,1243],{"class":89,"line":224},[87,1244,1245],{"class":97},"        })\n",[87,1247,1248,1250,1253,1255,1258,1260,1262,1264,1266],{"class":89,"line":246},[87,1249,1193],{"class":97},[87,1251,1252],{"class":93},"catch",[87,1254,1165],{"class":97},[87,1256,1257],{"class":1168},"error",[87,1259,1204],{"class":562},[87,1261,1207],{"class":104},[87,1263,1172],{"class":97},[87,1265,1175],{"class":562},[87,1267,98],{"class":97},[87,1269,1270,1272,1274,1276,1279,1281,1283,1285,1287],{"class":89,"line":256},[87,1271,1218],{"class":97},[87,1273,1257],{"class":93},[87,1275,791],{"class":97},[87,1277,1278],{"class":165},"`[EXIF] Failed to parse EXIF for ${",[87,1280,1169],{"class":97},[87,1282,1231],{"class":165},[87,1284,1234],{"class":97},[87,1286,1237],{"class":165},[87,1288,1289],{"class":97},", error);\n",[87,1291,1292],{"class":89,"line":270},[87,1293,1294],{"class":97},"        });\n",[87,1296,1297],{"class":89,"line":280},[87,1298,1299],{"class":97},"});\n",[16,1301,1302,1305],{},[27,1303,1304],{},"exifr"," 라이브러리를 활용해서 EXIF 데이터를 분석했다. parse 결과값에서 원하는 정보를 활용하면 된다.",[45,1307,1309],{"id":1308},"exif-데이터-삽입하기-piexif-라이브러리","exif 데이터 삽입하기 (piexif 라이브러리)",[1108,1311],{"description":1312,"host":1313,"title":1314,"url":1315,"favicon":1316,"image":1317},"Read and modify exif in client-side or server-side JavaScript. - hMatoba\u002Fpiexifjs","github.com","GitHub - hMatoba\u002Fpiexifjs: Read and modify exif in client-side or server-side JavaScript.","https:\u002F\u002Fgithub.com\u002FhMatoba\u002Fpiexifjs","https:\u002F\u002Fgithub.githubassets.com\u002Ffavicons\u002Ffavicon.svg","https:\u002F\u002Fopengraph.githubassets.com\u002F4d891948fb9214037064aa86f3e5d24d9852af2a18436ffb78fadfae5d6d1977\u002FhMatoba\u002Fpiexifjs",[16,1319,1320],{},"이미지를 후처리하다가 EXIF 데이터가 소실되는 경우도 있다. 예를 들어 캔버스(Canvas)에 이미지를 그리고 추가적인 처리를 한 뒤 다시 이미지로 추출했을 때는 EXIF 데이터가 없는 상태가 된다. 이러한 경우에는 미리 가지고 있던 EXIF 데이터를 저장해두었다가 결과 이미지에 주입하는 방법을 사용할 수 있다.",[16,1322,1323,1325,1326,1329],{},[27,1324,1304],{}," 라이브러리는 읽기 전용 라이브러리이므로, EXIF 데이터를 파일에 쓰는 기능은 없다. ",[27,1327,1328],{},"piexif"," 라이브러리를 사용해보자.",[78,1331,1333],{"className":1150,"code":1332,"language":1152,"meta":83,"style":83},"\u002F\u002F EXIF 저장\nlet exifStr: string | null = null;\ntry {\n    const originalDataURL = await fileToDataURL(origFile);\n    const exifObj = piexif.load(originalDataURL);\n    exifStr = piexif.dump(exifObj);\n} catch (e) { ... }\n\n\u002F\u002F EXIF 삽입\ntry {\n    finalDataURL = piexif.insert(exifStr, resizeResult.dataURL);\n} catch (e) { ... }\n",[27,1334,1335,1341,1367,1374,1393,1411,1426,1442,1447,1452,1458,1473],{"__ignoreMap":83},[87,1336,1337],{"class":89,"line":90},[87,1338,1340],{"class":1339},"sJ8bj","\u002F\u002F EXIF 저장\n",[87,1342,1343,1346,1349,1351,1354,1357,1360,1363,1365],{"class":89,"line":101},[87,1344,1345],{"class":562},"let",[87,1347,1348],{"class":97}," exifStr",[87,1350,1204],{"class":562},[87,1352,1353],{"class":104}," string",[87,1355,1356],{"class":562}," |",[87,1358,1359],{"class":104}," null",[87,1361,1362],{"class":562}," =",[87,1364,1359],{"class":104},[87,1366,114],{"class":97},[87,1368,1369,1372],{"class":89,"line":117},[87,1370,1371],{"class":562},"try",[87,1373,98],{"class":97},[87,1375,1376,1379,1382,1384,1387,1390],{"class":89,"line":130},[87,1377,1378],{"class":562},"    const",[87,1380,1381],{"class":104}," originalDataURL",[87,1383,1362],{"class":562},[87,1385,1386],{"class":562}," await",[87,1388,1389],{"class":93}," fileToDataURL",[87,1391,1392],{"class":97},"(origFile);\n",[87,1394,1395,1397,1400,1402,1405,1408],{"class":89,"line":224},[87,1396,1378],{"class":562},[87,1398,1399],{"class":104}," exifObj",[87,1401,1362],{"class":562},[87,1403,1404],{"class":97}," piexif.",[87,1406,1407],{"class":93},"load",[87,1409,1410],{"class":97},"(originalDataURL);\n",[87,1412,1413,1416,1418,1420,1423],{"class":89,"line":246},[87,1414,1415],{"class":97},"    exifStr ",[87,1417,162],{"class":562},[87,1419,1404],{"class":97},[87,1421,1422],{"class":93},"dump",[87,1424,1425],{"class":97},"(exifObj);\n",[87,1427,1428,1431,1433,1436,1439],{"class":89,"line":256},[87,1429,1430],{"class":97},"} ",[87,1432,1252],{"class":562},[87,1434,1435],{"class":97}," (e) { ",[87,1437,1438],{"class":562},"...",[87,1440,1441],{"class":97}," }\n",[87,1443,1444],{"class":89,"line":270},[87,1445,1446],{"emptyLinePlaceholder":842},"\n",[87,1448,1449],{"class":89,"line":280},[87,1450,1451],{"class":1339},"\u002F\u002F EXIF 삽입\n",[87,1453,1454,1456],{"class":89,"line":295},[87,1455,1371],{"class":562},[87,1457,98],{"class":97},[87,1459,1460,1463,1465,1467,1470],{"class":89,"line":315},[87,1461,1462],{"class":97},"    finalDataURL ",[87,1464,162],{"class":562},[87,1466,1404],{"class":97},[87,1468,1469],{"class":93},"insert",[87,1471,1472],{"class":97},"(exifStr, resizeResult.dataURL);\n",[87,1474,1475,1477,1479,1481,1483],{"class":89,"line":330},[87,1476,1430],{"class":97},[87,1478,1252],{"class":562},[87,1480,1435],{"class":97},[87,1482,1438],{"class":562},[87,1484,1441],{"class":97},[16,1486,1487],{},"원본의 exif 가 그대로 주입된다.",[825,1489,1490],{},"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 .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":83,"searchDepth":101,"depth":101,"links":1492},[1493,1496],{"id":1127,"depth":101,"text":1128,"children":1494},[1495],{"id":1146,"depth":117,"text":1147},{"id":1308,"depth":101,"text":1309},"2025-08-19","웹과 안드로이드에서 사진의 EXIF 데이터를 다루는 방법. 브라우저의 보안 특성 상 웹에서는 EXIF 를 의도적으로 누락시킴.","exif",{},"\u002Fblog\u002F250819-exif",{"title":853,"description":1498},{"loc":1501},"blog\u002F250819-exif","System",[1507,1508],"android","mobile","oGkBVFAB5TzWv1pYwp9DKP4IgKktT29epNzkFnU-_Qc",{"id":1511,"title":1512,"body":1513,"created":1850,"description":1851,"extension":839,"filename":1852,"meta":1853,"navigation":842,"path":1854,"seo":1855,"sitemap":1856,"stem":1857,"subject":847,"tags":1858,"updated":1850,"volume":1859,"__hash__":1860},"blog\u002Fblog\u002F251211-max-height-transition-delay.md","max-height transition 딜레이",{"type":8,"value":1514,"toc":1848},[1515,1699,1704,1707,1714,1717,1741,1842,1845],[78,1516,1518],{"className":80,"code":1517,"language":82,"meta":83,"style":83},".memo-text {\n  word-break: break-word;\n  max-height: 48px;\n  transition: max-height 0.5s cubic-bezier(0, 1, 0, 1);\n  overflow: hidden;\n  position: relative;\n  will-change: max-height;\n  \n  &.memo-text-expanded {\n    max-height: 1000px;\n    transition: max-height 1s ease-in-out;\n    \n    &::after {\n      opacity: 0;\n    }\n  }\n}\n",[27,1519,1520,1527,1539,1553,1591,1601,1613,1621,1626,1636,1650,1666,1671,1676,1685,1690,1695],{"__ignoreMap":83},[87,1521,1522,1525],{"class":89,"line":90},[87,1523,1524],{"class":93},".memo-text",[87,1526,98],{"class":97},[87,1528,1529,1532,1534,1537],{"class":89,"line":101},[87,1530,1531],{"class":104},"  word-break",[87,1533,108],{"class":97},[87,1535,1536],{"class":104},"break-word",[87,1538,114],{"class":97},[87,1540,1541,1544,1546,1549,1551],{"class":89,"line":117},[87,1542,1543],{"class":104},"  max-height",[87,1545,108],{"class":97},[87,1547,1548],{"class":104},"48",[87,1550,563],{"class":562},[87,1552,114],{"class":97},[87,1554,1555,1558,1561,1564,1567,1570,1572,1575,1577,1580,1582,1584,1586,1588],{"class":89,"line":130},[87,1556,1557],{"class":104},"  transition",[87,1559,1560],{"class":97},": max-height ",[87,1562,1563],{"class":104},"0.5",[87,1565,1566],{"class":562},"s",[87,1568,1569],{"class":104}," cubic-bezier",[87,1571,791],{"class":97},[87,1573,1574],{"class":104},"0",[87,1576,60],{"class":97},[87,1578,1579],{"class":104},"1",[87,1581,60],{"class":97},[87,1583,1574],{"class":104},[87,1585,60],{"class":97},[87,1587,1579],{"class":104},[87,1589,1590],{"class":97},");\n",[87,1592,1593,1595,1597,1599],{"class":89,"line":224},[87,1594,105],{"class":104},[87,1596,108],{"class":97},[87,1598,111],{"class":104},[87,1600,114],{"class":97},[87,1602,1603,1606,1608,1611],{"class":89,"line":246},[87,1604,1605],{"class":104},"  position",[87,1607,108],{"class":97},[87,1609,1610],{"class":104},"relative",[87,1612,114],{"class":97},[87,1614,1615,1618],{"class":89,"line":256},[87,1616,1617],{"class":104},"  will-change",[87,1619,1620],{"class":97},": max-height;\n",[87,1622,1623],{"class":89,"line":270},[87,1624,1625],{"class":97},"  \n",[87,1627,1628,1631,1634],{"class":89,"line":280},[87,1629,1630],{"class":97},"  &.",[87,1632,1633],{"class":104},"memo-text-expanded",[87,1635,98],{"class":97},[87,1637,1638,1641,1643,1646,1648],{"class":89,"line":295},[87,1639,1640],{"class":104},"    max-height",[87,1642,108],{"class":97},[87,1644,1645],{"class":104},"1000",[87,1647,563],{"class":562},[87,1649,114],{"class":97},[87,1651,1652,1655,1657,1659,1661,1664],{"class":89,"line":315},[87,1653,1654],{"class":104},"    transition",[87,1656,1560],{"class":97},[87,1658,1579],{"class":104},[87,1660,1566],{"class":562},[87,1662,1663],{"class":104}," ease-in-out",[87,1665,114],{"class":97},[87,1667,1668],{"class":89,"line":330},[87,1669,1670],{"class":97},"    \n",[87,1672,1673],{"class":89,"line":351},[87,1674,1675],{"class":97},"    &::after {\n",[87,1677,1678,1681,1683],{"class":89,"line":360},[87,1679,1680],{"class":97},"      opacity: ",[87,1682,1574],{"class":104},[87,1684,114],{"class":97},[87,1686,1687],{"class":89,"line":374},[87,1688,1689],{"class":97},"    }\n",[87,1691,1692],{"class":89,"line":383},[87,1693,1694],{"class":97},"  }\n",[87,1696,1697],{"class":89,"line":398},[87,1698,133],{"class":97},[16,1700,1701],{},[513,1702],{"alt":1512,"src":1703},"blog\u002Fimg\u002F251211-max-height-transition-delay\u002Fimage.png",[16,1705,1706],{},"이렇게 2줄 이상일 때 아래 그라데이션이 생기고 (::after 부분) 접고 펼 수 있는 기능이다. max-height 를 조절해서 구현했는데, 펼칠 때는 너무 빠르고, 닫을 때는 너무 느린 현상이 발생했다.",[1108,1708],{"description":1709,"host":1710,"image":1711,"title":1712,"url":1713},"I'm having an issue with a transition I'm using to slide a panel in and out.Please take a look at the following jsbin http:\u002F\u002Fjsbin.com\u002Fuvejuj\u002F1\u002F Notice that when i click the toggle button the f...","stackoverflow.com","https:\u002F\u002Fstackoverflow.com\u002FContent\u002FSites\u002Fstackoverflow\u002FImg\u002Fapple-touch-icon@2.png?v=73d79a89bded","How to Remove Delay on Css3 Slide out transition which uses max-height transition","https:\u002F\u002Fstackoverflow.com\u002Fa\u002F39103903",[16,1715,1716],{},"여기에서 처럼 cubic-bezier 를 조절했다.",[1718,1719,1720],"blockquote",{},[16,1721,1722,1725,1726,1728,1729,1732,1733,1736,1737,1740],{},[87,1723,1724],{},"!hint","\nIt's because you're animating between ",[27,1727,1574],{}," and ",[27,1730,1731],{},"1000px"," ",[27,1734,1735],{},"max-height",", but your content is only about ",[27,1738,1739],{},"120px"," high. The delay is the animation happening on the 880 pixels you can't see.\nmax-height 가 1000px 에서 0으로 주는 동안 120px 짜리의 컨텐츠는 아무 동작을 안하는 것처럼 보이는 것이다.",[78,1742,1744],{"className":80,"code":1743,"language":82,"meta":83,"style":83},".text {\n  overflow: hidden;\n  max-height: 0;\n  transition: max-height 0.5s cubic-bezier(0, 1, 0, 1);\n  &.full {\n    max-height: 1000px;\n    transition: max-height 1s ease-in-out;\n}\n",[27,1745,1746,1753,1763,1773,1803,1812,1824,1838],{"__ignoreMap":83},[87,1747,1748,1751],{"class":89,"line":90},[87,1749,1750],{"class":93},".text",[87,1752,98],{"class":97},[87,1754,1755,1757,1759,1761],{"class":89,"line":101},[87,1756,105],{"class":104},[87,1758,108],{"class":97},[87,1760,111],{"class":104},[87,1762,114],{"class":97},[87,1764,1765,1767,1769,1771],{"class":89,"line":117},[87,1766,1543],{"class":104},[87,1768,108],{"class":97},[87,1770,1574],{"class":104},[87,1772,114],{"class":97},[87,1774,1775,1777,1779,1781,1783,1785,1787,1789,1791,1793,1795,1797,1799,1801],{"class":89,"line":130},[87,1776,1557],{"class":104},[87,1778,1560],{"class":97},[87,1780,1563],{"class":104},[87,1782,1566],{"class":562},[87,1784,1569],{"class":104},[87,1786,791],{"class":97},[87,1788,1574],{"class":104},[87,1790,60],{"class":97},[87,1792,1579],{"class":104},[87,1794,60],{"class":97},[87,1796,1574],{"class":104},[87,1798,60],{"class":97},[87,1800,1579],{"class":104},[87,1802,1590],{"class":97},[87,1804,1805,1807,1810],{"class":89,"line":224},[87,1806,1630],{"class":97},[87,1808,1809],{"class":104},"full",[87,1811,98],{"class":97},[87,1813,1814,1816,1818,1820,1822],{"class":89,"line":246},[87,1815,1640],{"class":104},[87,1817,108],{"class":97},[87,1819,1645],{"class":104},[87,1821,563],{"class":562},[87,1823,114],{"class":97},[87,1825,1826,1828,1830,1832,1834,1836],{"class":89,"line":256},[87,1827,1654],{"class":104},[87,1829,1560],{"class":97},[87,1831,1579],{"class":104},[87,1833,1566],{"class":562},[87,1835,1663],{"class":104},[87,1837,114],{"class":97},[87,1839,1840],{"class":89,"line":270},[87,1841,133],{"class":97},[16,1843,1844],{},"그래서 이렇게 극단적으로 애니메이션 동작 지점을 단축시키면서 해결한듯 하다.",[825,1846,1847],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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);}",{"title":83,"searchDepth":101,"depth":101,"links":1849},[],"2025-12-11","accordion 을 만들며 height 를 조정해 접고 펼침을 구현할 때, 애니메이션의 딜레이가 발생하는 이유와 헤결, 경험을 공유.","max-height-transition-delay",{},"\u002Fblog\u002F251211-max-height-transition-delay",{"title":1512,"description":1851},{"loc":1854},"blog\u002F251211-max-height-transition-delay",[82],"short","zBPT9wx08AVhifikizLNFB3PuPk1WU0CpQvOOZUTWRg",{"id":1862,"title":1863,"body":1864,"created":2850,"description":2851,"extension":839,"filename":2852,"meta":2853,"navigation":842,"path":2854,"seo":2855,"sitemap":2856,"stem":2857,"subject":2858,"tags":2859,"updated":2850,"volume":1859,"__hash__":2862},"blog\u002Fblog\u002F260119-tracking-parent-the-time.md","부모 컴포넌트의 초기화 완료 시점 감지하기 (Promise.withResolvers)",{"type":8,"value":1865,"toc":2847},[1866,1873,1895,1898,1904,1910,1914,1928,2152,2166,2289,2295,2299,2315,2319,2329,2371,2376,2468,2476,2480,2486,2698,2710,2841,2844],[16,1867,1868,1869,1872],{},"일반적으로 Vue에서는 부모가 자식에게 값을 전달하고, 자식은 이벤트를 통해 부모에게 데이터를 전달하는 ",[788,1870,1871],{},"Props Down, Events Up"," 방식을 따른다. 부모가 자식의 내부 함수를 직접 호출하는 방식은 컴포넌트 간 결합도를 높이므로 지양하는 편이다. 하지만 부모 컴포넌트의 특정 비동기 작업이 완료된 시점을 자식 컴포넌트가 정확히 알아야 할 때가 있다. 예를 들어, 부모가 비동기로 초기화 데이터를 모두 가져온 후에만 자식 컴포넌트가 로직을 수행해야 하는 경우다.",[20,1874,1875,1882,1889],{},[23,1876,1877,1878,1881],{},"부모의 isInitialized 변수를 자식에게 전달하고, 자식은 ",[27,1879,1880],{},"watch"," 로 감지하기.",[23,1883,1884,1885,1888],{},"자식의 parentInitialized() 함수를 ",[27,1886,1887],{},"defineExpose"," 로 export 한 후 template ref 로 호출하기.",[23,1890,1891,1892,1894],{},"promise 를 전달해서 적절한 시점에 자식이 ",[27,1893,1196],{}," 블럭에서 기능 수행하기.",[16,1896,1897],{},"이 중 3번째 방법을 고안해냈다.",[16,1899,1900,1901,1903],{},"첫 번째 방법(",[27,1902,1880],{},")은 의도하지 않은 시점에 변수가 변경되어 사이드 이펙트가 발생할 가능성이 있다. 단순한 값이 아닌 복잡한 초기화 흐름을 제어하고 싶을 때, 예기치 않은 타이밍에 감지가 일어나 로직이 꼬일 수 있다. 또한 자식 컴포넌트가 마운트되기 전에 이미 부모의 초기화가 끝나버렸다면 감지가 불가능할 수도 있다.",[16,1905,1906,1907,1909],{},"두 번째 방법(",[27,1908,1887],{},")은 부모가 자식의 구현 상세(메서드명 등)를 알아야 하므로 결합도가 높아진다.",[11,1911,1913],{"id":1912},"promise-전달해서-자식도-초기화-시점-감지하기","Promise 전달해서 자식도 초기화 시점 감지하기",[16,1915,1916,1917,1920,1921,1923,1924,1927],{},"ECMAScript 2024 에 추가된 ",[27,1918,1919],{},"Promise.withResolvers","를 활용하면 완료 시점을 깔끔하게 싱크할 수 있다. 일반적인 비동기 함수는 실행과 동시에 Promise를 반환한다. 하지만 ",[27,1922,1919],{},"를 사용하면 Promise 객체와 이를 해결할 수 있는 ",[27,1925,1926],{},"resolve"," 함수를 분리하여 선언할 수 있다. 덕분에 부모는 원하는 복잡한 로직을 모두 수행한 뒤, 아주 정밀한 타이밍에 자식에게 신호를 보낸다.",[78,1929,1933],{"className":1930,"code":1931,"language":1932,"meta":83,"style":83},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup lang=\"ts\">\n\u002F\u002F Promise와 resolve 함수를 외부로 추출하여 선언\nconst initReady = Promise.withResolvers\u003Cvoid>();\n\nasync function runInit() {\n  try {\n    \u002F\u002F API 호출, 로컬 스토리지 확인, 권한 체크 등 복잡한 초기화 수행\n    await doSomethingComplex();\n    \n    \u002F\u002F 모든 작업이 끝난 원하는 시점에 약속 이행\n    initReady.resolve();\n  } catch (error) {\n    initReady.reject(error);\n  }\n}\n\nonMounted(() => {\n  runInit();\n});\n\u003C\u002Fscript>\n\u003Ctemplate>\n  \u003CChild :init-ready=\"initReady.promise\" \u002F>\n\u003C\u002Ftemplate>\n","vue",[27,1934,1935,1955,1960,1986,1990,2004,2011,2016,2027,2031,2036,2045,2055,2065,2069,2073,2077,2089,2096,2100,2108,2117,2144],{"__ignoreMap":83},[87,1936,1937,1939,1942,1945,1948,1950,1953],{"class":89,"line":90},[87,1938,152],{"class":97},[87,1940,1941],{"class":155},"script",[87,1943,1944],{"class":93}," setup",[87,1946,1947],{"class":93}," lang",[87,1949,162],{"class":97},[87,1951,1952],{"class":165},"\"ts\"",[87,1954,169],{"class":97},[87,1956,1957],{"class":89,"line":101},[87,1958,1959],{"class":1339},"\u002F\u002F Promise와 resolve 함수를 외부로 추출하여 선언\n",[87,1961,1962,1965,1968,1970,1973,1975,1978,1980,1983],{"class":89,"line":117},[87,1963,1964],{"class":562},"const",[87,1966,1967],{"class":104}," initReady",[87,1969,1362],{"class":562},[87,1971,1972],{"class":104}," Promise",[87,1974,1231],{"class":97},[87,1976,1977],{"class":93},"withResolvers",[87,1979,152],{"class":97},[87,1981,1982],{"class":104},"void",[87,1984,1985],{"class":97},">();\n",[87,1987,1988],{"class":89,"line":130},[87,1989,1446],{"emptyLinePlaceholder":842},[87,1991,1992,1995,1998,2001],{"class":89,"line":224},[87,1993,1994],{"class":562},"async",[87,1996,1997],{"class":562}," function",[87,1999,2000],{"class":93}," runInit",[87,2002,2003],{"class":97},"() {\n",[87,2005,2006,2009],{"class":89,"line":246},[87,2007,2008],{"class":562},"  try",[87,2010,98],{"class":97},[87,2012,2013],{"class":89,"line":256},[87,2014,2015],{"class":1339},"    \u002F\u002F API 호출, 로컬 스토리지 확인, 권한 체크 등 복잡한 초기화 수행\n",[87,2017,2018,2021,2024],{"class":89,"line":270},[87,2019,2020],{"class":562},"    await",[87,2022,2023],{"class":93}," doSomethingComplex",[87,2025,2026],{"class":97},"();\n",[87,2028,2029],{"class":89,"line":280},[87,2030,1670],{"class":97},[87,2032,2033],{"class":89,"line":295},[87,2034,2035],{"class":1339},"    \u002F\u002F 모든 작업이 끝난 원하는 시점에 약속 이행\n",[87,2037,2038,2041,2043],{"class":89,"line":315},[87,2039,2040],{"class":97},"    initReady.",[87,2042,1926],{"class":93},[87,2044,2026],{"class":97},[87,2046,2047,2050,2052],{"class":89,"line":330},[87,2048,2049],{"class":97},"  } ",[87,2051,1252],{"class":562},[87,2053,2054],{"class":97}," (error) {\n",[87,2056,2057,2059,2062],{"class":89,"line":351},[87,2058,2040],{"class":97},[87,2060,2061],{"class":93},"reject",[87,2063,2064],{"class":97},"(error);\n",[87,2066,2067],{"class":89,"line":360},[87,2068,1694],{"class":97},[87,2070,2071],{"class":89,"line":374},[87,2072,133],{"class":97},[87,2074,2075],{"class":89,"line":383},[87,2076,1446],{"emptyLinePlaceholder":842},[87,2078,2079,2082,2085,2087],{"class":89,"line":398},[87,2080,2081],{"class":93},"onMounted",[87,2083,2084],{"class":97},"(() ",[87,2086,1175],{"class":562},[87,2088,98],{"class":97},[87,2090,2091,2094],{"class":89,"line":418},[87,2092,2093],{"class":93},"  runInit",[87,2095,2026],{"class":97},[87,2097,2098],{"class":89,"line":434},[87,2099,1299],{"class":97},[87,2101,2102,2104,2106],{"class":89,"line":454},[87,2103,489],{"class":97},[87,2105,1941],{"class":155},[87,2107,169],{"class":97},[87,2109,2110,2112,2115],{"class":89,"line":463},[87,2111,152],{"class":97},[87,2113,2114],{"class":155},"template",[87,2116,169],{"class":97},[87,2118,2119,2122,2125,2128,2131,2133,2136,2139,2141],{"class":89,"line":477},[87,2120,2121],{"class":97},"  \u003C",[87,2123,2124],{"class":155},"Child",[87,2126,2127],{"class":97}," :",[87,2129,2130],{"class":93},"init-ready",[87,2132,162],{"class":97},[87,2134,2135],{"class":165},"\"",[87,2137,2138],{"class":97},"initReady.promise",[87,2140,2135],{"class":165},[87,2142,2143],{"class":97}," \u002F>\n",[87,2145,2146,2148,2150],{"class":89,"line":486},[87,2147,489],{"class":97},[87,2149,2114],{"class":155},[87,2151,169],{"class":97},[16,2153,2154,2155,2158,2159,2162,2163,2165],{},"부모는 ",[27,2156,2157],{},"initReady","라는 통신 창구를 만들어둔다. ",[27,2160,2161],{},"initReady.resolve()","가 호출되는 순간 대기하고 있던 자식 컴포넌트들에게 신호가 전달된다. 자식에게는 ",[27,2164,2138],{}," 객체만 Props로 전달하면 된다.",[78,2167,2169],{"className":1930,"code":2168,"language":1932,"meta":83,"style":83},"\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  initReady: Promise\u003Cvoid>\n}>();\n\nonMounted(async () => {\n  \u002F\u002F 부모의 초기화가 끝날 때까지 대기\n  await props.initReady;\n  \n  \u002F\u002F 이후 로직 실행\n  console.log(\"부모의 준비가 끝나 자식 로직을 시작합니다.\");\n});\n\u003C\u002Fscript>\n",[27,2170,2171,2187,2202,2217,2222,2226,2241,2246,2254,2258,2263,2277,2281],{"__ignoreMap":83},[87,2172,2173,2175,2177,2179,2181,2183,2185],{"class":89,"line":90},[87,2174,152],{"class":97},[87,2176,1941],{"class":155},[87,2178,1944],{"class":93},[87,2180,1947],{"class":93},[87,2182,162],{"class":97},[87,2184,1952],{"class":165},[87,2186,169],{"class":97},[87,2188,2189,2191,2194,2196,2199],{"class":89,"line":101},[87,2190,1964],{"class":562},[87,2192,2193],{"class":104}," props",[87,2195,1362],{"class":562},[87,2197,2198],{"class":93}," defineProps",[87,2200,2201],{"class":97},"\u003C{\n",[87,2203,2204,2207,2209,2211,2213,2215],{"class":89,"line":117},[87,2205,2206],{"class":1168},"  initReady",[87,2208,1204],{"class":562},[87,2210,1972],{"class":93},[87,2212,152],{"class":97},[87,2214,1982],{"class":104},[87,2216,169],{"class":97},[87,2218,2219],{"class":89,"line":130},[87,2220,2221],{"class":97},"}>();\n",[87,2223,2224],{"class":89,"line":224},[87,2225,1446],{"emptyLinePlaceholder":842},[87,2227,2228,2230,2232,2234,2237,2239],{"class":89,"line":246},[87,2229,2081],{"class":93},[87,2231,791],{"class":97},[87,2233,1994],{"class":562},[87,2235,2236],{"class":97}," () ",[87,2238,1175],{"class":562},[87,2240,98],{"class":97},[87,2242,2243],{"class":89,"line":256},[87,2244,2245],{"class":1339},"  \u002F\u002F 부모의 초기화가 끝날 때까지 대기\n",[87,2247,2248,2251],{"class":89,"line":270},[87,2249,2250],{"class":562},"  await",[87,2252,2253],{"class":97}," props.initReady;\n",[87,2255,2256],{"class":89,"line":280},[87,2257,1625],{"class":97},[87,2259,2260],{"class":89,"line":295},[87,2261,2262],{"class":1339},"  \u002F\u002F 이후 로직 실행\n",[87,2264,2265,2268,2270,2272,2275],{"class":89,"line":315},[87,2266,2267],{"class":97},"  console.",[87,2269,1221],{"class":93},[87,2271,791],{"class":97},[87,2273,2274],{"class":165},"\"부모의 준비가 끝나 자식 로직을 시작합니다.\"",[87,2276,1590],{"class":97},[87,2278,2279],{"class":89,"line":330},[87,2280,1299],{"class":97},[87,2282,2283,2285,2287],{"class":89,"line":351},[87,2284,489],{"class":97},[87,2286,1941],{"class":155},[87,2288,169],{"class":97},[16,2290,2291,2294],{},[27,2292,2293],{},"await props.initReady","를 통해 부모의 초기화가 완료된 이후에만 로직이 실행되도록 보장할 수 있다.",[11,2296,2298],{"id":2297},"자식이-v-if-로-마운트의-on-off-를-반복할-때","자식이 v-if 로 마운트의 on off 를 반복할 때",[16,2300,2301,2302,2305,2306,2308,2309,2311,2312,2314],{},"자식 컴포넌트가 ",[27,2303,2304],{},"v-if","에 의해 언마운트되었다가 다시 마운트되는 경우, ",[27,2307,2157],{}," 신호를 놓치지 않을까 걱정할 수 있다. 하지만 Promise는 한 번 ",[27,2310,1926],{}," 되면 그 상태(Fulfilled)가 유지된다. 따라서 자식 컴포넌트가 다시 생성되어 ",[27,2313,2293],{},"를 실행하더라도, 이미 완료된 Promise이므로 즉시 다음 로직이 실행된다. 매번 상태를 다시 확인할 필요 없이 안정적인 동작을 보장한다.",[11,2316,2318],{"id":2317},"provide-inject-로-깊은-여러-컴포넌트에-전달","provide, inject 로 깊은 여러 컴포넌트에 전달",[16,2320,2321,2322,30,2325,2328],{},"컴포넌트 계층이 깊거나(Prop Drilling 문제), 여러 자식 컴포넌트가 동시에 신호를 받아야 하는 경우 ",[27,2323,2324],{},"provide",[27,2326,2327],{},"inject","를 활용할 수 있다.",[78,2330,2332],{"className":1150,"code":2331,"language":1152,"meta":83,"style":83},"const initReady = Promise.withResolvers\u003Cvoid>();\n\u002F\u002F promise 를 provide\nprovide('initReady', initReady.promise);\n",[27,2333,2334,2354,2359],{"__ignoreMap":83},[87,2335,2336,2338,2340,2342,2344,2346,2348,2350,2352],{"class":89,"line":90},[87,2337,1964],{"class":562},[87,2339,1967],{"class":104},[87,2341,1362],{"class":562},[87,2343,1972],{"class":104},[87,2345,1231],{"class":97},[87,2347,1977],{"class":93},[87,2349,152],{"class":97},[87,2351,1982],{"class":104},[87,2353,1985],{"class":97},[87,2355,2356],{"class":89,"line":101},[87,2357,2358],{"class":1339},"\u002F\u002F promise 를 provide\n",[87,2360,2361,2363,2365,2368],{"class":89,"line":117},[87,2362,2324],{"class":93},[87,2364,791],{"class":97},[87,2366,2367],{"class":165},"'initReady'",[87,2369,2370],{"class":97},", initReady.promise);\n",[16,2372,2154,2373,2375],{},[27,2374,2324],{},"를 통해 Promise 객체를 하위 컴포넌트들에 공급한다.",[78,2377,2379],{"className":1150,"code":2378,"language":1152,"meta":83,"style":83},"\u002F\u002F promise 주입받기\nconst initReady = inject\u003CPromise\u003Cvoid>>('initReady');\n\nonMounted(async () => {\n  if (initReady) {\n    await initReady;\n    console.log(\"초기화 완료 후 자식 로직 실행\");\n  }\n});\n",[27,2380,2381,2386,2413,2417,2431,2439,2446,2460,2464],{"__ignoreMap":83},[87,2382,2383],{"class":89,"line":90},[87,2384,2385],{"class":1339},"\u002F\u002F promise 주입받기\n",[87,2387,2388,2390,2392,2394,2397,2399,2402,2404,2406,2409,2411],{"class":89,"line":101},[87,2389,1964],{"class":562},[87,2391,1967],{"class":104},[87,2393,1362],{"class":562},[87,2395,2396],{"class":93}," inject",[87,2398,152],{"class":97},[87,2400,2401],{"class":93},"Promise",[87,2403,152],{"class":97},[87,2405,1982],{"class":104},[87,2407,2408],{"class":97},">>(",[87,2410,2367],{"class":165},[87,2412,1590],{"class":97},[87,2414,2415],{"class":89,"line":117},[87,2416,1446],{"emptyLinePlaceholder":842},[87,2418,2419,2421,2423,2425,2427,2429],{"class":89,"line":130},[87,2420,2081],{"class":93},[87,2422,791],{"class":97},[87,2424,1994],{"class":562},[87,2426,2236],{"class":97},[87,2428,1175],{"class":562},[87,2430,98],{"class":97},[87,2432,2433,2436],{"class":89,"line":224},[87,2434,2435],{"class":562},"  if",[87,2437,2438],{"class":97}," (initReady) {\n",[87,2440,2441,2443],{"class":89,"line":246},[87,2442,2020],{"class":562},[87,2444,2445],{"class":97}," initReady;\n",[87,2447,2448,2451,2453,2455,2458],{"class":89,"line":256},[87,2449,2450],{"class":97},"    console.",[87,2452,1221],{"class":93},[87,2454,791],{"class":97},[87,2456,2457],{"class":165},"\"초기화 완료 후 자식 로직 실행\"",[87,2459,1590],{"class":97},[87,2461,2462],{"class":89,"line":270},[87,2463,1694],{"class":97},[87,2465,2466],{"class":89,"line":280},[87,2467,1299],{"class":97},[16,2469,2470,2472,2473,2475],{},[27,2471,2327],{},"를 통해 받아와서 사용하면 된다. 하지만 단순히 문자열 키(",[27,2474,2367],{},")로 주입받으면 타입 추론이 안 되거나, 데이터의 출처를 명확히 알기 어려워 유지보수가 힘들어질 수 있다.",[45,2477,2479],{"id":2478},"composable-를-활용해-추적을-용이하게-하기","composable 를 활용해 추적을 용이하게 하기",[16,2481,2482,2485],{},[27,2483,2484],{},"provide\u002Finject"," 패턴은 키 관리나 타입 정의가 번거로울 수 있으므로, Composable 함수(Hook)로 캡슐화하여 사용하는 것을 선호한다.",[78,2487,2489],{"className":1150,"code":2488,"language":1152,"meta":83,"style":83},"import { provide, inject, InjectionKey } from 'vue';\n\u002F\u002F 심볼을 사용하여 고유한 키 정의 \nconst InitReadyKey: InjectionKey\u003CPromise\u003Cvoid>> = Symbol('InitReady');\n\n\u002F**\n * 부모에서 호출할 provide 함수\n *\u002F\nexport function provideInit(promise: Promise\u003Cvoid>) {\n  provide(InitReadyKey, promise);\n}\n\n\u002F** \n * 자식에서 사용할 composable\n *\u002F\nexport function useInit() {\n  const initReady = inject(InitReadyKey);\n  \u002F\u002F provide 없으면 에러\n  if (!initReady) {\n    throw new Error('useInit()은 provideInit() 범위 내에서 사용해야 합니다.');\n  }\n  return initReady;\n}\n",[27,2490,2491,2507,2512,2547,2551,2556,2561,2566,2592,2600,2604,2608,2613,2618,2622,2633,2647,2652,2665,2683,2687,2694],{"__ignoreMap":83},[87,2492,2493,2496,2499,2502,2505],{"class":89,"line":90},[87,2494,2495],{"class":562},"import",[87,2497,2498],{"class":97}," { provide, inject, InjectionKey } ",[87,2500,2501],{"class":562},"from",[87,2503,2504],{"class":165}," 'vue'",[87,2506,114],{"class":97},[87,2508,2509],{"class":89,"line":101},[87,2510,2511],{"class":1339},"\u002F\u002F 심볼을 사용하여 고유한 키 정의 \n",[87,2513,2514,2516,2519,2521,2524,2526,2528,2530,2532,2535,2537,2540,2542,2545],{"class":89,"line":117},[87,2515,1964],{"class":562},[87,2517,2518],{"class":104}," InitReadyKey",[87,2520,1204],{"class":562},[87,2522,2523],{"class":93}," InjectionKey",[87,2525,152],{"class":97},[87,2527,2401],{"class":93},[87,2529,152],{"class":97},[87,2531,1982],{"class":104},[87,2533,2534],{"class":97},">> ",[87,2536,162],{"class":562},[87,2538,2539],{"class":93}," Symbol",[87,2541,791],{"class":97},[87,2543,2544],{"class":165},"'InitReady'",[87,2546,1590],{"class":97},[87,2548,2549],{"class":89,"line":130},[87,2550,1446],{"emptyLinePlaceholder":842},[87,2552,2553],{"class":89,"line":224},[87,2554,2555],{"class":1339},"\u002F**\n",[87,2557,2558],{"class":89,"line":246},[87,2559,2560],{"class":1339}," * 부모에서 호출할 provide 함수\n",[87,2562,2563],{"class":89,"line":256},[87,2564,2565],{"class":1339}," *\u002F\n",[87,2567,2568,2571,2573,2576,2578,2581,2583,2585,2587,2589],{"class":89,"line":270},[87,2569,2570],{"class":562},"export",[87,2572,1997],{"class":562},[87,2574,2575],{"class":93}," provideInit",[87,2577,791],{"class":97},[87,2579,2580],{"class":1168},"promise",[87,2582,1204],{"class":562},[87,2584,1972],{"class":93},[87,2586,152],{"class":97},[87,2588,1982],{"class":104},[87,2590,2591],{"class":97},">) {\n",[87,2593,2594,2597],{"class":89,"line":280},[87,2595,2596],{"class":93},"  provide",[87,2598,2599],{"class":97},"(InitReadyKey, promise);\n",[87,2601,2602],{"class":89,"line":295},[87,2603,133],{"class":97},[87,2605,2606],{"class":89,"line":315},[87,2607,1446],{"emptyLinePlaceholder":842},[87,2609,2610],{"class":89,"line":330},[87,2611,2612],{"class":1339},"\u002F** \n",[87,2614,2615],{"class":89,"line":351},[87,2616,2617],{"class":1339}," * 자식에서 사용할 composable\n",[87,2619,2620],{"class":89,"line":360},[87,2621,2565],{"class":1339},[87,2623,2624,2626,2628,2631],{"class":89,"line":374},[87,2625,2570],{"class":562},[87,2627,1997],{"class":562},[87,2629,2630],{"class":93}," useInit",[87,2632,2003],{"class":97},[87,2634,2635,2638,2640,2642,2644],{"class":89,"line":383},[87,2636,2637],{"class":562},"  const",[87,2639,1967],{"class":104},[87,2641,1362],{"class":562},[87,2643,2396],{"class":93},[87,2645,2646],{"class":97},"(InitReadyKey);\n",[87,2648,2649],{"class":89,"line":398},[87,2650,2651],{"class":1339},"  \u002F\u002F provide 없으면 에러\n",[87,2653,2654,2656,2659,2662],{"class":89,"line":418},[87,2655,2435],{"class":562},[87,2657,2658],{"class":97}," (",[87,2660,2661],{"class":562},"!",[87,2663,2664],{"class":97},"initReady) {\n",[87,2666,2667,2670,2673,2676,2678,2681],{"class":89,"line":434},[87,2668,2669],{"class":562},"    throw",[87,2671,2672],{"class":562}," new",[87,2674,2675],{"class":93}," Error",[87,2677,791],{"class":97},[87,2679,2680],{"class":165},"'useInit()은 provideInit() 범위 내에서 사용해야 합니다.'",[87,2682,1590],{"class":97},[87,2684,2685],{"class":89,"line":454},[87,2686,1694],{"class":97},[87,2688,2689,2692],{"class":89,"line":463},[87,2690,2691],{"class":562},"  return",[87,2693,2445],{"class":97},[87,2695,2696],{"class":89,"line":477},[87,2697,133],{"class":97},[16,2699,2700,2701,30,2703,2705,2706,2709],{},"구조는 간단하다. ",[27,2702,2324],{},[27,2704,2327],{}," 로직을 하나의 파일에서 관리하여 키(",[27,2707,2708],{},"InjectionKey",")와 타입의 일관성을 유지한다.",[78,2711,2713],{"className":1930,"code":2712,"language":1932,"meta":83,"style":83},"\u003Cscript setup lang=\"ts\">\nimport { provideInit } from '.\u002FuseInit';\nconst initReady = Promise.withResolvers\u003Cvoid>();\nprovideInit(initReady.promise); \u002F\u002F 추상화된 함수 호출\n\u003C\u002Fscript>\n\n\u003Cscript setup lang=\"ts\">\nimport { useInit } from '.\u002FuseInit';\nconst initReady = useInit(); \u002F\u002F 내부 로직을 몰라도 안전하게 주입받음\n\u003C\u002Fscript>\n",[27,2714,2715,2731,2745,2765,2776,2784,2788,2804,2817,2833],{"__ignoreMap":83},[87,2716,2717,2719,2721,2723,2725,2727,2729],{"class":89,"line":90},[87,2718,152],{"class":97},[87,2720,1941],{"class":155},[87,2722,1944],{"class":93},[87,2724,1947],{"class":93},[87,2726,162],{"class":97},[87,2728,1952],{"class":165},[87,2730,169],{"class":97},[87,2732,2733,2735,2738,2740,2743],{"class":89,"line":101},[87,2734,2495],{"class":562},[87,2736,2737],{"class":97}," { provideInit } ",[87,2739,2501],{"class":562},[87,2741,2742],{"class":165}," '.\u002FuseInit'",[87,2744,114],{"class":97},[87,2746,2747,2749,2751,2753,2755,2757,2759,2761,2763],{"class":89,"line":117},[87,2748,1964],{"class":562},[87,2750,1967],{"class":104},[87,2752,1362],{"class":562},[87,2754,1972],{"class":104},[87,2756,1231],{"class":97},[87,2758,1977],{"class":93},[87,2760,152],{"class":97},[87,2762,1982],{"class":104},[87,2764,1985],{"class":97},[87,2766,2767,2770,2773],{"class":89,"line":130},[87,2768,2769],{"class":93},"provideInit",[87,2771,2772],{"class":97},"(initReady.promise); ",[87,2774,2775],{"class":1339},"\u002F\u002F 추상화된 함수 호출\n",[87,2777,2778,2780,2782],{"class":89,"line":224},[87,2779,489],{"class":97},[87,2781,1941],{"class":155},[87,2783,169],{"class":97},[87,2785,2786],{"class":89,"line":246},[87,2787,1446],{"emptyLinePlaceholder":842},[87,2789,2790,2792,2794,2796,2798,2800,2802],{"class":89,"line":256},[87,2791,152],{"class":97},[87,2793,1941],{"class":155},[87,2795,1944],{"class":93},[87,2797,1947],{"class":93},[87,2799,162],{"class":97},[87,2801,1952],{"class":165},[87,2803,169],{"class":97},[87,2805,2806,2808,2811,2813,2815],{"class":89,"line":270},[87,2807,2495],{"class":562},[87,2809,2810],{"class":97}," { useInit } ",[87,2812,2501],{"class":562},[87,2814,2742],{"class":165},[87,2816,114],{"class":97},[87,2818,2819,2821,2823,2825,2827,2830],{"class":89,"line":280},[87,2820,1964],{"class":562},[87,2822,1967],{"class":104},[87,2824,1362],{"class":562},[87,2826,2630],{"class":93},[87,2828,2829],{"class":97},"(); ",[87,2831,2832],{"class":1339},"\u002F\u002F 내부 로직을 몰라도 안전하게 주입받음\n",[87,2834,2835,2837,2839],{"class":89,"line":295},[87,2836,489],{"class":97},[87,2838,1941],{"class":155},[87,2840,169],{"class":97},[16,2842,2843],{},"타입 추론도 제공하고, 단일 파일에서 파악이 가능하기 때문에 가독성도 높고 유지보수하기 좋다.",[825,2845,2846],{},"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 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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":83,"searchDepth":101,"depth":101,"links":2848},[2849],{"id":2478,"depth":101,"text":2479},"2026-01-19","Vue에서 부모 컴포넌트의 비동기 초기화 작업이 완료된 시점을 자식 컴포넌트에게 안전하게 전달하는 방법을 알아봅니다. Promise.withResolvers를 활용한 패턴을 소개","tracking-parent-the-time",{},"\u002Fblog\u002F260119-tracking-parent-the-time",{"title":1863,"description":2851},{"loc":2854},"blog\u002F260119-tracking-parent-the-time","Vue",[1932,2860,2861],"typescript","javascript","8KL_V2k8mH4dzeTyx-sxghE88wTg9pUCRpyR58NuAe0",{"id":2864,"title":2865,"body":2866,"created":2984,"description":2985,"extension":839,"filename":2986,"meta":2987,"navigation":842,"path":2988,"seo":2989,"sitemap":2990,"stem":2991,"subject":2992,"tags":2993,"updated":2984,"volume":1859,"__hash__":2995},"blog\u002Fblog\u002Fandroid-noti-bundle.md","안드로이드 노티피케이션 번들 데이터 최신화 Android Notification Bundle Data",{"type":8,"value":2867,"toc":2982},[2868,2926,2937,2940,2949,2952,2967,2976,2979],[78,2869,2871],{"className":996,"code":2870,"language":998,"meta":83,"style":83},"fun sendNoti(title: String, message: String, data: Map\u003CString, String>) {  \n    val intent = Intent(this, MainActivity::class.java).apply {  \n        flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP\n        data.get(\"key\")?.let {value -> \n            this.putExtra(\"key\", value)\n        }\n    }\n    val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_MUTABLE) \n    val notificationBuilder = NotificationCompat.Builder(this, channelId)\n        .setContentIntent(pendingIntent)  \n}\n",[27,2872,2873,2878,2883,2888,2893,2898,2903,2907,2912,2917,2922],{"__ignoreMap":83},[87,2874,2875],{"class":89,"line":90},[87,2876,2877],{},"fun sendNoti(title: String, message: String, data: Map\u003CString, String>) {  \n",[87,2879,2880],{"class":89,"line":101},[87,2881,2882],{},"    val intent = Intent(this, MainActivity::class.java).apply {  \n",[87,2884,2885],{"class":89,"line":117},[87,2886,2887],{},"        flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP\n",[87,2889,2890],{"class":89,"line":130},[87,2891,2892],{},"        data.get(\"key\")?.let {value -> \n",[87,2894,2895],{"class":89,"line":224},[87,2896,2897],{},"            this.putExtra(\"key\", value)\n",[87,2899,2900],{"class":89,"line":246},[87,2901,2902],{},"        }\n",[87,2904,2905],{"class":89,"line":256},[87,2906,1689],{},[87,2908,2909],{"class":89,"line":270},[87,2910,2911],{},"    val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_MUTABLE) \n",[87,2913,2914],{"class":89,"line":280},[87,2915,2916],{},"    val notificationBuilder = NotificationCompat.Builder(this, channelId)\n",[87,2918,2919],{"class":89,"line":295},[87,2920,2921],{},"        .setContentIntent(pendingIntent)  \n",[87,2923,2924],{"class":89,"line":315},[87,2925,133],{},[16,2927,2928,2929,2932,2933,2936],{},"이렇게 넣어주었는데, 노티를 띄울 때는\n",[27,2930,2931],{},"intent Bundle[{key=newvalue}]","\n로 표시되고 onCreate 에서 노티 데이터를 받았을 때는\n",[27,2934,2935],{},"bundle Bundle[{key=oldvalue}]"," 로 표시된다.",[16,2938,2939],{},"가장 최근으로 띄운 노티의 데이터가 아닌, 이젠 노티의 데이터를 사용하는 듯 하다.",[16,2941,2942],{},[2943,2944,2948],"a",{"href":2945,"rel":2946},"https:\u002F\u002Fstackoverflow.com\u002Fa\u002F3140497",[2947],"nofollow","Android keeps caching my intents Extras, how to declare a pending intent that keeps fresh extras? - Stack Overflow",[16,2950,2951],{},"가장 최신의 노티 데이터를 항상 덮어 쓰게끔 하면 된다!",[2953,2954,2955,2961],"ol",{},[23,2956,2957,2960],{},[27,2958,2959],{},"FLAG_UPDATE_CURRENT"," 플래그를 사용하고",[23,2962,2963,2966],{},[27,2964,2965],{},"requestCode"," 를 0 고정 값이 아니라 유니크한 값으로 지정해주면 된다.",[78,2968,2970],{"className":996,"code":2969,"language":998,"meta":83,"style":83},"val pendingIntent = PendingIntent.getActivity(this, System.currentTimeMillis().toInt(), intent, FLAG_MUTABLE)\n",[27,2971,2972],{"__ignoreMap":83},[87,2973,2974],{"class":89,"line":90},[87,2975,2969],{},[16,2977,2978],{},"우선은 별다른 규칙 없이 현재 타임스탬프를 지정해주니까, 내가 넣어준 마지막 값으로 잘 들어온다.",[825,2980,2981],{},"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);}",{"title":83,"searchDepth":101,"depth":101,"links":2983},[],"2023-09-07","안드로이드 노티피케이션을 터치해 액티비티를 진입할 때 onCreate 에서 전달받는 데이터가 원치 않는 값으로 들어온다. 해결 방법을 찾아보자.","android-noti-bundle",{},"\u002Fblog\u002Fandroid-noti-bundle",{"title":2865,"description":2985},{"loc":2988},"blog\u002Fandroid-noti-bundle","Android",[2992,2994],"Android Notification","rMS4JxoK_Jc_DbpL5OUnDNKi-QRWJUcJWD-9iEkyLHY",{"id":2997,"title":2998,"body":2999,"created":3259,"description":3260,"extension":839,"filename":3261,"meta":3262,"navigation":842,"path":3263,"seo":3264,"sitemap":3265,"stem":3266,"subject":847,"tags":3267,"updated":3259,"volume":1859,"__hash__":3269},"blog\u002Fblog\u002Fcolor-mix-hash-value-transparency.md","css variables 에 투명도 조절 (color-mix 함수)",{"type":8,"value":3000,"toc":3255},[3001,3015,3018,3056,3059,3079,3082,3085,3093,3111,3114,3122,3125,3131,3136,3139,3143,3151,3246,3252],[78,3002,3004],{"className":80,"code":3003,"language":82,"meta":83,"style":83},"--color-base-20: #ddd;\n",[27,3005,3006],{"__ignoreMap":83},[87,3007,3008,3011],{"class":89,"line":90},[87,3009,3010],{"class":97},"--color-base-20: ",[87,3012,3014],{"class":3013},"s7hpK","#ddd;\n",[16,3016,3017],{},"이런식으로 정의한 컬러 코드 값의 변수로 배경에 투명도를 섞어서 사용할 일이 생겼다.",[78,3019,3021],{"className":80,"code":3020,"language":82,"meta":83,"style":83},"background-color: rgba(--color-base-20, 0.5);\nbackground-color: rgba(var(--color-base-20), 0.5);\nbackground: var(--color-base-20) rgba(0, 0, 0, 0.5);\n",[27,3022,3023,3036,3047],{"__ignoreMap":83},[87,3024,3025,3028,3031,3034],{"class":89,"line":90},[87,3026,3027],{"class":155},"background-color",[87,3029,3030],{"class":97},": rgba(--color-base-20, 0",[87,3032,3033],{"class":93},".5",[87,3035,1590],{"class":97},[87,3037,3038,3040,3043,3045],{"class":89,"line":101},[87,3039,3027],{"class":155},[87,3041,3042],{"class":97},": rgba(var(--color-base-20), 0",[87,3044,3033],{"class":93},[87,3046,1590],{"class":97},[87,3048,3049,3052,3054],{"class":89,"line":117},[87,3050,3051],{"class":97},"background: var(--color-base-20) rgba(0, 0, 0, 0",[87,3053,3033],{"class":93},[87,3055,1590],{"class":97},[16,3057,3058],{},"등 다양한 형태로 사용해봤지만, 모두 작동하지 않는다. hex 형태의 컬러코드는 rgba 에서 사용이 불가능하다.",[78,3060,3062],{"className":80,"code":3061,"language":82,"meta":83,"style":83},"--color-base-20: 240, 240, 240;\nbackground-color: rgba(var(--color-base-20), 0.5);\n",[27,3063,3064,3069],{"__ignoreMap":83},[87,3065,3066],{"class":89,"line":90},[87,3067,3068],{"class":97},"--color-base-20: 240, 240, 240;\n",[87,3070,3071,3073,3075,3077],{"class":89,"line":101},[87,3072,3027],{"class":155},[87,3074,3042],{"class":97},[87,3076,3033],{"class":93},[87,3078,1590],{"class":97},[16,3080,3081],{},"변수를 rgb 값을 찢어놓는 방법도 있다. 그런데 기존 변수들은 건들고 싶지 않았다.",[45,3083,3084],{"id":3084},"color-mix",[1108,3086],{"favicon":3087,"host":3088,"title":3089,"url":3090,"description":3091,"image":3092},"https:\u002F\u002Fdeveloper.mozilla.org\u002Ffavicon-48x48.bc390275e955dacb2e65.png","developer.mozilla.org","color-mix() - CSS: Cascading Style Sheets | MDN","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002Fcolor_value\u002Fcolor-mix","The color-mix() functional notation takes two \u003Ccolor> values and returns the result of mixing them in a given colorspace by a given amount.","https:\u002F\u002Fdeveloper.mozilla.org\u002Fmdn-social-share.d893525a4fb5fb1f67a2.png",[78,3094,3096],{"className":80,"code":3095,"language":82,"meta":83,"style":83},"background-color: color-mix(in srgb, var(--color-base-20), #0000 50%);\n",[27,3097,3098],{"__ignoreMap":83},[87,3099,3100,3102,3105,3108],{"class":89,"line":90},[87,3101,3027],{"class":155},[87,3103,3104],{"class":97},": color-mix(in srgb, var(--color-base-20), ",[87,3106,3107],{"class":3013},"#0000",[87,3109,3110],{"class":97}," 50%);\n",[16,3112,3113],{},"이렇게 색상을 섞을 수 있는 mixin 이 있다.",[2953,3115,3116,3119],{},[23,3117,3118],{},"타입 : hsl, lch, srgb, lab, custom-color-space",[23,3120,3121],{},"색상 : 색상 %",[16,3123,3124],{},"투명도만 주고 싶은 경우이기 때문에, 첫번째 색상은 원본 색상을 주고, 두번째는 완전 투명한 색상에 투명도를 50%로 주어서 섞었다. 그럼 rgba(--var, 0.5) 와 동일한 효과가 된다.",[1108,3126],{"favicon":3127,"host":3128,"title":3129,"url":3130},"https:\u002F\u002Fcaniuse.com\u002Fimg\u002Ffavicon-128.png","caniuse.com","\"color-mix\" | Can I use... Support tables for HTML5, CSS3, etc","https:\u002F\u002Fcaniuse.com\u002F?search=color-mix",[16,3132,3133],{},[513,3134],{"alt":515,"src":3135},"blog\u002Fimg\u002Fcolor-mix-hash-value-transparency\u002Fimage.png",[16,3137,3138],{},"웬만하면 다 쓸 수 있는듯~",[45,3140,3142],{"id":3141},"tailwindcss-를-사용하면-alpha-를-사용할-수-있다","tailwindcss 를 사용하면 --alpha() 를 사용할 수 있다",[1108,3144],{"favicon":3145,"host":3146,"title":3147,"url":3148,"description":3149,"image":3150},"https:\u002F\u002Ftailwindcss.com\u002Ffavicons\u002Ffavicon-32x32.png?v=4","tailwindcss.com","Functions and directives - Core concepts","https:\u002F\u002Ftailwindcss.com\u002Fdocs\u002Ffunctions-and-directives#alpha-function","A reference for the custom functions and directives Tailwind exposes to your CSS.","https:\u002F\u002Ftailwindcss.com\u002Fapi\u002Fog?path=\u002Fdocs\u002Ffunctions-and-directives",[78,3152,3154],{"className":80,"code":3153,"language":82,"meta":83,"style":83},".my-element {\n  color: --alpha(var(--color-lime-300) \u002F 50%);\n}\n\u002F* compiled css *\u002F\n.my-element {\n  color: color-mix(in oklab, var(--color-lime-300) 50%, transparent);\n}\n",[27,3155,3156,3163,3190,3194,3199,3205,3242],{"__ignoreMap":83},[87,3157,3158,3161],{"class":89,"line":90},[87,3159,3160],{"class":93},".my-element",[87,3162,98],{"class":97},[87,3164,3165,3168,3171,3174,3176,3179,3182,3185,3188],{"class":89,"line":101},[87,3166,3167],{"class":104},"  color",[87,3169,3170],{"class":97},": --alpha(",[87,3172,3173],{"class":104},"var",[87,3175,791],{"class":97},[87,3177,3178],{"class":1168},"--color-lime-300",[87,3180,3181],{"class":97},") \u002F ",[87,3183,3184],{"class":104},"50",[87,3186,3187],{"class":562},"%",[87,3189,1590],{"class":97},[87,3191,3192],{"class":89,"line":117},[87,3193,133],{"class":97},[87,3195,3196],{"class":89,"line":130},[87,3197,3198],{"class":1339},"\u002F* compiled css *\u002F\n",[87,3200,3201,3203],{"class":89,"line":224},[87,3202,3160],{"class":93},[87,3204,98],{"class":97},[87,3206,3207,3209,3211,3213,3215,3218,3221,3223,3225,3227,3229,3231,3233,3235,3237,3240],{"class":89,"line":246},[87,3208,3167],{"class":104},[87,3210,108],{"class":97},[87,3212,3084],{"class":104},[87,3214,791],{"class":97},[87,3216,3217],{"class":1168},"in",[87,3219,3220],{"class":1168}," oklab",[87,3222,60],{"class":97},[87,3224,3173],{"class":104},[87,3226,791],{"class":97},[87,3228,3178],{"class":1168},[87,3230,1172],{"class":97},[87,3232,3184],{"class":104},[87,3234,3187],{"class":562},[87,3236,60],{"class":97},[87,3238,3239],{"class":104},"transparent",[87,3241,1590],{"class":97},[87,3243,3244],{"class":89,"line":256},[87,3245,133],{"class":97},[16,3247,3248,3249,3251],{},"결국 css 로 컴파일되면 ",[27,3250,3084],{}," 이긴 하다.",[825,3253,3254],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":83,"searchDepth":101,"depth":101,"links":3256},[3257,3258],{"id":3084,"depth":101,"text":3084},{"id":3141,"depth":101,"text":3142},"2025-03-26","CSS 변수의 투명도를 조절하는 방법을 자세히 다루고, color-mix 함수를 통해 배경에 효과적인 투명도를 적용하는 기법을 소개한다. 다양한 컬러 코드 및 변수를 활용하여 디자인에 깊이를 더하는 방법을 배울 수 있다","color-mix-hash-value-transparency",{},"\u002Fblog\u002Fcolor-mix-hash-value-transparency",{"title":2998,"description":3260},{"loc":3263},"blog\u002Fcolor-mix-hash-value-transparency",[82,3268],"tailwindcss","I5l0huVsTwvCp3HUY9wukXk7QoGCYIb-aYfd_lreL7Y",{"id":3271,"title":3272,"body":3273,"created":7520,"description":7521,"extension":839,"filename":7522,"meta":7523,"navigation":842,"path":7524,"seo":7525,"sitemap":7526,"stem":7527,"subject":2858,"tags":7528,"updated":7535,"volume":7536,"__hash__":7537},"blog\u002Fblog\u002Fmaking-blog-githubio-vue3-1.md","Github.io + Vue3 로 블로그 만들기",{"type":8,"value":3274,"toc":7501},[3275,3278,3281,3288,3291,3295,3300,3307,3312,3315,3319,3324,3327,3330,3341,3344,3347,3360,3363,3372,3377,3418,3429,3433,3436,3712,3723,3728,3742,3747,3754,3759,3762,3767,3770,3782,3787,3790,3794,3797,3993,4000,4579,4582,4596,4649,4656,4800,4803,4808,4811,4815,4818,4839,4845,4851,5000,5012,5108,5111,5115,5118,5180,5183,5191,5194,5197,5208,5211,5454,5467,5477,5851,5854,5859,5862,5866,5892,5899,6044,6047,6163,6170,6174,6181,6184,6189,6192,6197,6280,6283,6288,6291,6296,6299,6304,6311,6316,6319,6323,6326,6851,6871,6875,6878,6882,6889,6894,6897,6902,6905,6909,6920,7122,7132,7137,7140,7208,7219,7223,7243,7257,7430,7436,7493,7498],[16,3276,3277],{},"개발 블로그를 작성하기 위해서 github.io 를 사용하기로 했다. velog나 tistory, medium 같은 것들도 있지만, 내가 완전히 커스터마이징 가능한 페이지를 갖고 싶었다. 그렇다고 서버를 돌리거나, AWS를 통해서 구축하는 수고까지는 하지 않으려 했다. 지금 와서 생각해보면 거의 비슷한 공수가 들었다. Notion도 고려했는데, 내 개인 노트로 아주 잘 활용하고 있으나 블로그로 사용하기에는 너무 에디터다. 이렇게 쓰니까 깃헙을 고른 이유는 그냥 해보고싶어서에 가까워 보인다.",[16,3279,3280],{},"페이지를 구축할 언어로는 내가 지금 사용하고 있는 Vue 프레임워크를 사용할 것이다. 단순히 정말 지금 내가 익숙한 것이기 때문에 골랐다. 그렇다고 블로그에 대단한 기능을 넣을 것은 아니다. 나중에 게시글 뿐 아니라 포트폴리오의 역할도 해줄 수 있을 것 같아서, 주력 프레임워크를 선택해야겠다고 다짐했다.",[16,3282,3283],{},[2943,3284,3287],{"href":3285,"rel":3286},"https:\u002F\u002Fyemsu.github.io\u002F",[2947],"ENJOY DEV",[16,3289,3290],{},"참고한 블로그는 위 블로그이다. 내가 하려던 것을 똑같이 이미 구현해서 사용하고 계셨기 때문에 참고하기에 너무 훌륭한 블로그였다. 직접 블로그에 포스팅도 해주셨는데, 중간중간 소스도 염탐했다.",[45,3292,3294],{"id":3293},"githubio-페이지-만들기","Github.io 페이지 만들기",[16,3296,3297],{},[513,3298],{"alt":515,"src":3299},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled.png",[16,3301,3302,3303,3306],{},"당연히 Github 계정이 있어야한다. ",[27,3304,3305],{},"[유저명.github.io](http:\u002F\u002F유저명.github.io)"," 라고 프로젝트를 하나 만든다. 이렇게 프로젝트를 만들면 자동으로 Github Pages 를 사용하겠다고 알리는 격이다. 필수는 아니고, 나중에 Github Pages 를 사용하겠다고 따로 설정할 수도 있다.",[16,3308,3309],{},[513,3310],{"alt":515,"src":3311},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled1.png",[16,3313,3314],{},"배포 대상 브랜치를 선택하고, 폴더를 선택하여 저장하면 끝이다. 하지만 이름을 짓기도 귀찮고, 굳이 따로 설정할 필요가 없으니 규칙을 따라서 진행한다.",[45,3316,3318],{"id":3317},"github-actions-로-github-pages-배포","Github Actions 로 Github Pages 배포",[16,3320,3321],{},[513,3322],{"alt":515,"src":3323},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled2.png",[16,3325,3326],{},"Github Pages 로 사용되는 레포는 대부분 결과물을 관리하는 레포이다. 레포에 푸시가 되면 Github 봇이 감지하여, 자동으로 Github Pages 배포를 실행한다.",[16,3328,3329],{},"나는 vue 앱을 빌드하고 결과물을 배포하는 것까지 한번에 관리를 하고 싶다. 방법은 세 가지가 있다.",[2953,3331,3332,3335,3338],{},[23,3333,3334],{},"gh-pages 활용",[23,3336,3337],{},"수동 빌드",[23,3339,3340],{},"github actions 등록",[16,3342,3343],{},"내가 선택한 방법은 3번이고, 1번은 시도도 안해봤다. 하지만 gh-pages 를 많이들 사용한다. 간단하게 설명을 하자면,",[753,3345,3346],{"id":3346},"gh-pages",[16,3348,3349,3351,3352,3355,3356,3359],{},[788,3350,3346],{}," 라는 라이브러리를 가지고, 빌드 결과물을 ",[27,3353,3354],{},"gh-pages 브랜치"," 에 publish 하는 것이다. 기본적으로는 package.json 파일의 ",[27,3357,3358],{},"homepage"," 속성에 해당하는 url 로 배포를 하게 된다.",[753,3361,3337],{"id":3362},"수동-빌드",[16,3364,3365,3367,3368,3371],{},[788,3366,3337],{}," 는 Github Pages 설정 페이지에서 호스팅할 폴더를 고를 수 있다. 기본적으로 root 폴더이며, ",[27,3369,3370],{},"\u002Fdocs"," 폴더를 추가로 지정할 수 있다. 해당 레포 브랜치의 docs 폴더를 호스팅하겠다는 것이다.",[16,3373,3374],{},[513,3375],{"alt":515,"src":3376},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled3.png",[78,3378,3382],{"className":3379,"code":3380,"language":3381,"meta":83,"style":83},"language-jsx shiki shiki-themes github-light github-dark","\u002F\u002F vue.config.js\nmodule.exports = {\n  outputDir: 'docs',\n}\n","jsx",[27,3383,3384,3389,3403,3414],{"__ignoreMap":83},[87,3385,3386],{"class":89,"line":90},[87,3387,3388],{"class":1339},"\u002F\u002F vue.config.js\n",[87,3390,3391,3394,3396,3399,3401],{"class":89,"line":101},[87,3392,3393],{"class":104},"module",[87,3395,1231],{"class":97},[87,3397,3398],{"class":104},"exports",[87,3400,1362],{"class":562},[87,3402,98],{"class":97},[87,3404,3405,3408,3411],{"class":89,"line":117},[87,3406,3407],{"class":97},"  outputDir: ",[87,3409,3410],{"class":165},"'docs'",[87,3412,3413],{"class":97},",\n",[87,3415,3416],{"class":89,"line":130},[87,3417,133],{"class":97},[16,3419,3420,3421,3424,3425,3428],{},"빌드의 결과물을 docs 폴더에 생성되게 하고, 레포에 올려주기면 하면 된다. vue 의 경우 ",[27,3422,3423],{},"outputDir"," 속성을 docs 로 변경해주면 간단하게 결과 폴더가 바뀐다. 추가로 ",[27,3426,3427],{},".gitignore"," 에 docs 폴더가 있다면 항목을 제거해서 원격 레포에 푸시해주면 자동으로 봇이 실행되면서 페이지를 배포한다.",[753,3430,3432],{"id":3431},"github-actions","Github Actions",[16,3434,3435],{},"Github Actions 는 간단하게 설명하면, CICD 툴이다. 젠킨스나, aws pipeline, circleCI 같은 역할을 한다. Gitlab 에도 비슷한 기능이 있는데, 이쪽은 아직 써보지 못했다. Github Actions 는 프로젝트에 포함된 yaml 파일을 기반으로 명령을 실행한다.",[78,3437,3441],{"className":3438,"code":3439,"language":3440,"meta":83,"style":83},"language-yaml shiki shiki-themes github-light github-dark","name: Deployment to Github Pages\non:\n  push:\n    branches:\n      - deploy\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@master\n      - name: Set up Node.js\n        uses: actions\u002Fsetup-node@master\n        with:\n          node-version: 16.x\n      \n      - name: Install dependencies\n        run: npm install\n\n      - name: Build\n        run: npm run build\n        env:\n          NODE_ENV: production\n      \n      - name: Deploy\n        uses: peaceiris\u002Factions-gh-pages@v2.5.0\n        env:\n          PERSONAL_TOKEN: $\\{\\{secrets.GH_TOKEN\\}\\}\n          PUBLISH_BRANCH: master\n          PUBLISH_DIR: .\u002Fdist\n          SCRIPT_MODE: true\n","yaml",[27,3442,3443,3452,3460,3467,3474,3482,3486,3493,3500,3510,3517,3529,3540,3550,3557,3567,3572,3583,3593,3597,3608,3617,3624,3634,3639,3651,3661,3668,3679,3690,3701],{"__ignoreMap":83},[87,3444,3445,3447,3449],{"class":89,"line":90},[87,3446,1234],{"class":155},[87,3448,108],{"class":97},[87,3450,3451],{"class":165},"Deployment to Github Pages\n",[87,3453,3454,3457],{"class":89,"line":101},[87,3455,3456],{"class":104},"on",[87,3458,3459],{"class":97},":\n",[87,3461,3462,3465],{"class":89,"line":117},[87,3463,3464],{"class":155},"  push",[87,3466,3459],{"class":97},[87,3468,3469,3472],{"class":89,"line":130},[87,3470,3471],{"class":155},"    branches",[87,3473,3459],{"class":97},[87,3475,3476,3479],{"class":89,"line":224},[87,3477,3478],{"class":97},"      - ",[87,3480,3481],{"class":165},"deploy\n",[87,3483,3484],{"class":89,"line":246},[87,3485,1446],{"emptyLinePlaceholder":842},[87,3487,3488,3491],{"class":89,"line":256},[87,3489,3490],{"class":155},"jobs",[87,3492,3459],{"class":97},[87,3494,3495,3498],{"class":89,"line":270},[87,3496,3497],{"class":155},"  deploy",[87,3499,3459],{"class":97},[87,3501,3502,3505,3507],{"class":89,"line":280},[87,3503,3504],{"class":155},"    runs-on",[87,3506,108],{"class":97},[87,3508,3509],{"class":165},"ubuntu-latest\n",[87,3511,3512,3515],{"class":89,"line":295},[87,3513,3514],{"class":155},"    steps",[87,3516,3459],{"class":97},[87,3518,3519,3521,3524,3526],{"class":89,"line":315},[87,3520,3478],{"class":97},[87,3522,3523],{"class":155},"uses",[87,3525,108],{"class":97},[87,3527,3528],{"class":165},"actions\u002Fcheckout@master\n",[87,3530,3531,3533,3535,3537],{"class":89,"line":330},[87,3532,3478],{"class":97},[87,3534,1234],{"class":155},[87,3536,108],{"class":97},[87,3538,3539],{"class":165},"Set up Node.js\n",[87,3541,3542,3545,3547],{"class":89,"line":351},[87,3543,3544],{"class":155},"        uses",[87,3546,108],{"class":97},[87,3548,3549],{"class":165},"actions\u002Fsetup-node@master\n",[87,3551,3552,3555],{"class":89,"line":360},[87,3553,3554],{"class":155},"        with",[87,3556,3459],{"class":97},[87,3558,3559,3562,3564],{"class":89,"line":374},[87,3560,3561],{"class":155},"          node-version",[87,3563,108],{"class":97},[87,3565,3566],{"class":165},"16.x\n",[87,3568,3569],{"class":89,"line":383},[87,3570,3571],{"class":97},"      \n",[87,3573,3574,3576,3578,3580],{"class":89,"line":398},[87,3575,3478],{"class":97},[87,3577,1234],{"class":155},[87,3579,108],{"class":97},[87,3581,3582],{"class":165},"Install dependencies\n",[87,3584,3585,3588,3590],{"class":89,"line":418},[87,3586,3587],{"class":155},"        run",[87,3589,108],{"class":97},[87,3591,3592],{"class":165},"npm install\n",[87,3594,3595],{"class":89,"line":434},[87,3596,1446],{"emptyLinePlaceholder":842},[87,3598,3599,3601,3603,3605],{"class":89,"line":454},[87,3600,3478],{"class":97},[87,3602,1234],{"class":155},[87,3604,108],{"class":97},[87,3606,3607],{"class":165},"Build\n",[87,3609,3610,3612,3614],{"class":89,"line":463},[87,3611,3587],{"class":155},[87,3613,108],{"class":97},[87,3615,3616],{"class":165},"npm run build\n",[87,3618,3619,3622],{"class":89,"line":477},[87,3620,3621],{"class":155},"        env",[87,3623,3459],{"class":97},[87,3625,3626,3629,3631],{"class":89,"line":486},[87,3627,3628],{"class":155},"          NODE_ENV",[87,3630,108],{"class":97},[87,3632,3633],{"class":165},"production\n",[87,3635,3637],{"class":89,"line":3636},24,[87,3638,3571],{"class":97},[87,3640,3642,3644,3646,3648],{"class":89,"line":3641},25,[87,3643,3478],{"class":97},[87,3645,1234],{"class":155},[87,3647,108],{"class":97},[87,3649,3650],{"class":165},"Deploy\n",[87,3652,3654,3656,3658],{"class":89,"line":3653},26,[87,3655,3544],{"class":155},[87,3657,108],{"class":97},[87,3659,3660],{"class":165},"peaceiris\u002Factions-gh-pages@v2.5.0\n",[87,3662,3664,3666],{"class":89,"line":3663},27,[87,3665,3621],{"class":155},[87,3667,3459],{"class":97},[87,3669,3671,3674,3676],{"class":89,"line":3670},28,[87,3672,3673],{"class":155},"          PERSONAL_TOKEN",[87,3675,108],{"class":97},[87,3677,3678],{"class":165},"$\\{\\{secrets.GH_TOKEN\\}\\}\n",[87,3680,3682,3685,3687],{"class":89,"line":3681},29,[87,3683,3684],{"class":155},"          PUBLISH_BRANCH",[87,3686,108],{"class":97},[87,3688,3689],{"class":165},"master\n",[87,3691,3693,3696,3698],{"class":89,"line":3692},30,[87,3694,3695],{"class":155},"          PUBLISH_DIR",[87,3697,108],{"class":97},[87,3699,3700],{"class":165},".\u002Fdist\n",[87,3702,3704,3707,3709],{"class":89,"line":3703},31,[87,3705,3706],{"class":155},"          SCRIPT_MODE",[87,3708,108],{"class":97},[87,3710,3711],{"class":104},"true\n",[16,3713,3714,3716,3717,3722],{},[27,3715,3456],{}," 은 트리거로 Github 대부분의 훅을 활용할 수 있다. 가장 기본적으로 푸시가 있으며, 라벨, 이슈 관리, 풀 리퀘스트, 스케줄 등이 있다. ",[2943,3718,3721],{"href":3719,"rel":3720},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions\u002Fusing-workflows\u002Fworkflow-syntax-for-github-actions",[2947],"자세한 건 Github Docs 에 자세히 작성","되었다.",[16,3724,3725,3727],{},[27,3726,3490],{}," 는 말 그대로 작업이며, 적힌대로 쭉 작업을 진행한다.",[16,3729,3730,3731,3734,3735,3738,3739,3741],{},"위 코드를 간략히 보자면, “deploy 브랜치의 푸시가 감지되면, 코드를 체크아웃한 후에 ",[27,3732,3733],{},"npm install"," 로 종속성을 모두 다운로드 받고, ",[27,3736,3737],{},"npm run build"," 로 빌드를 한 후, ",[27,3740,3346],{}," 아티팩트를 활용해서 master 브랜치에 dist 폴더에 담긴 결과물을 배포” 이다.",[16,3743,3744],{},[513,3745],{"alt":515,"src":3746},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled4.png",[16,3748,3749,3750,3753],{},"이 때 활용하게 되는 것이 Github Secrets 이다. Actions 실행할 때 필요한 암호키나 레포에 노출되지 말아야 하는 중요한 데이터의 경우 레포 설정에 저장해놓고 사용함으로써 외부로 노출시키지 않아도 된다. 간단하게 이름-값 으로 이루어져 있어서 대충봐도 키를 등록할 수 있을 것이다. 데이터를 사용할 때는 ",[27,3751,3752],{},"PERSONAL_TOKEN: $\\{\\{secrets.GH_TOKEN\\}\\}"," 이런 식으로 사용한다.",[16,3755,3756],{},[513,3757],{"alt":515,"src":3758},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled5.png",[16,3760,3761],{},"master 브랜치에 결과물이 배포되기 때문에 Pages 설정을 알맞게 바꿔준다.",[16,3763,3764],{},[513,3765],{"alt":515,"src":3766},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled6.png",[16,3768,3769],{},"Github Workflow 는 두 번 돌게 된다.",[2953,3771,3772,3779],{},[23,3773,3774,3775,3778],{},"deploy 브랜치에 배포를 하면 Github Actions 가 실행되어 빌드하고, ",[27,3776,3777],{},"master"," 브랜치에 결과물을 푸시한다.",[23,3780,3781],{},"그럼 Gihub 봇이 알아채고 페이지에 배포를 시작한다.",[16,3783,3784],{},[513,3785],{"alt":515,"src":3786},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled7.png",[16,3788,3789],{},"봇이 돌린 Workflow 에서 배포가 완료된 우리의 페이지도 확인할 수 있다. 아래 Artifacts 는 Jekyll 이 빌드한 결과물이다. 실제로 보면 npm build 결과를 그대로 래핑하는 것과 매한가지다.",[45,3791,3793],{"id":3792},"블로그-포스트-목록-관리하기","블로그 포스트 목록 관리하기",[16,3795,3796],{},"따로 서버를 관리할 것이 아니라면 레포에 글 목록도 관리를 하는 게 맞다고 판단했다. 클라에서 모든 것을 해결하는 것은 어렵지 않다. 데이터를 직접 작성해서 갖고 있으면 된다.",[78,3798,3800],{"className":3379,"code":3799,"language":3381,"meta":83,"style":83},"\u002F\u002F public\u002Fposts\u002Fpostlist.json\n[\n  {\n    \"url\": \"d5c653f34310b85f731a497e64970059921fba363647f5cc1e39ead9ea9cf76f\",\n    \"fileName\": \"first-post\",\n    \"title\": \"블로그를 Github.io 로 시작하는 첫 포스트\",\n        \"description\": \"요약을 써줘요\",\n    \"createdAt\": \"2022-06-24\",\n    \"updatedAt\": \"2022-06-25\",\n    \"tags\": [\"tag1\"]\n  },\n  {\n    \"url\": \"8689a6f9a9428af5a3570a0236abdb9abf62409077138a3d02a676b3a3f757f4\",\n    \"fileName\": \"second-post\",\n    \"title\": \"두 번째 포스트\",\n        \"description\": \"요약을 써줘요\",\n    \"createdAt\": \"2022-06-24\",\n    \"updatedAt\": \"2022-06-25\",\n    \"tags\": [\"tag1\"]\n  }\n]\n",[27,3801,3802,3807,3812,3817,3829,3841,3853,3865,3877,3889,3903,3908,3912,3923,3934,3945,3955,3965,3975,3985,3989],{"__ignoreMap":83},[87,3803,3804],{"class":89,"line":90},[87,3805,3806],{"class":1339},"\u002F\u002F public\u002Fposts\u002Fpostlist.json\n",[87,3808,3809],{"class":89,"line":101},[87,3810,3811],{"class":97},"[\n",[87,3813,3814],{"class":89,"line":117},[87,3815,3816],{"class":97},"  {\n",[87,3818,3819,3822,3824,3827],{"class":89,"line":130},[87,3820,3821],{"class":165},"    \"url\"",[87,3823,108],{"class":97},[87,3825,3826],{"class":165},"\"d5c653f34310b85f731a497e64970059921fba363647f5cc1e39ead9ea9cf76f\"",[87,3828,3413],{"class":97},[87,3830,3831,3834,3836,3839],{"class":89,"line":224},[87,3832,3833],{"class":165},"    \"fileName\"",[87,3835,108],{"class":97},[87,3837,3838],{"class":165},"\"first-post\"",[87,3840,3413],{"class":97},[87,3842,3843,3846,3848,3851],{"class":89,"line":246},[87,3844,3845],{"class":165},"    \"title\"",[87,3847,108],{"class":97},[87,3849,3850],{"class":165},"\"블로그를 Github.io 로 시작하는 첫 포스트\"",[87,3852,3413],{"class":97},[87,3854,3855,3858,3860,3863],{"class":89,"line":256},[87,3856,3857],{"class":165},"        \"description\"",[87,3859,108],{"class":97},[87,3861,3862],{"class":165},"\"요약을 써줘요\"",[87,3864,3413],{"class":97},[87,3866,3867,3870,3872,3875],{"class":89,"line":270},[87,3868,3869],{"class":165},"    \"createdAt\"",[87,3871,108],{"class":97},[87,3873,3874],{"class":165},"\"2022-06-24\"",[87,3876,3413],{"class":97},[87,3878,3879,3882,3884,3887],{"class":89,"line":280},[87,3880,3881],{"class":165},"    \"updatedAt\"",[87,3883,108],{"class":97},[87,3885,3886],{"class":165},"\"2022-06-25\"",[87,3888,3413],{"class":97},[87,3890,3891,3894,3897,3900],{"class":89,"line":295},[87,3892,3893],{"class":165},"    \"tags\"",[87,3895,3896],{"class":97},": [",[87,3898,3899],{"class":165},"\"tag1\"",[87,3901,3902],{"class":97},"]\n",[87,3904,3905],{"class":89,"line":315},[87,3906,3907],{"class":97},"  },\n",[87,3909,3910],{"class":89,"line":330},[87,3911,3816],{"class":97},[87,3913,3914,3916,3918,3921],{"class":89,"line":351},[87,3915,3821],{"class":165},[87,3917,108],{"class":97},[87,3919,3920],{"class":165},"\"8689a6f9a9428af5a3570a0236abdb9abf62409077138a3d02a676b3a3f757f4\"",[87,3922,3413],{"class":97},[87,3924,3925,3927,3929,3932],{"class":89,"line":360},[87,3926,3833],{"class":165},[87,3928,108],{"class":97},[87,3930,3931],{"class":165},"\"second-post\"",[87,3933,3413],{"class":97},[87,3935,3936,3938,3940,3943],{"class":89,"line":374},[87,3937,3845],{"class":165},[87,3939,108],{"class":97},[87,3941,3942],{"class":165},"\"두 번째 포스트\"",[87,3944,3413],{"class":97},[87,3946,3947,3949,3951,3953],{"class":89,"line":383},[87,3948,3857],{"class":165},[87,3950,108],{"class":97},[87,3952,3862],{"class":165},[87,3954,3413],{"class":97},[87,3956,3957,3959,3961,3963],{"class":89,"line":398},[87,3958,3869],{"class":165},[87,3960,108],{"class":97},[87,3962,3874],{"class":165},[87,3964,3413],{"class":97},[87,3966,3967,3969,3971,3973],{"class":89,"line":418},[87,3968,3881],{"class":165},[87,3970,108],{"class":97},[87,3972,3886],{"class":165},[87,3974,3413],{"class":97},[87,3976,3977,3979,3981,3983],{"class":89,"line":434},[87,3978,3893],{"class":165},[87,3980,3896],{"class":97},[87,3982,3899],{"class":165},[87,3984,3902],{"class":97},[87,3986,3987],{"class":89,"line":454},[87,3988,1694],{"class":97},[87,3990,3991],{"class":89,"line":463},[87,3992,3902],{"class":97},[16,3994,3995,3996,3999],{},"static 하게 직접 관리할 대상이기 때문에, ",[27,3997,3998],{},"public"," 폴더에 두었다. Vue 빌드 시 public 폴더 안의 내용물은 그대로 결과폴더에 복사되기 때문에, 깃헙에서 바로 읽어올 수 있다.",[78,4001,4005],{"className":4002,"code":4003,"language":4004,"meta":83,"style":83},"language-tsx shiki shiki-themes github-light github-dark","import { VuexModule, Module, Mutation, Action } from \"vuex-module-decorators\";\nimport axios from \"axios\";\n\nexport interface Post {\n  url: string;\n  fileName: string;\n    description: string;\n  title: string;\n  tags: string[];\n  data: string;\n}\n\n@Module({ namespaced: true, })\nclass Posts extends VuexModule {\n  public postList:Post[] = [];\n  public currentUrl = '';\n\n  get currentPost(): Post | undefined {\n    if(this.currentUrl == null || this.currentUrl == '' || this.postList.length \u003C= 0){\n      return undefined;\n    }\n    else {\n      return this.postList.find(post => {\n        return post.url === this.currentUrl;\n      })\n    }\n  }\n  \n  @Mutation\n  public setPostList(postList: Post[]) {\n    this.postList = postList;\n  }\n  @Mutation\n  public setCurrentUrl(currentUrl: string) {\n    this.currentUrl = currentUrl;\n  }\n\n  @Action\n  public requestGetPostList() {\n    axios.get(`\u002Fposts\u002Fpostlist.json`).then(res => {\n      this.context.commit(\"setPostList\", res.data);\n    });\n  }\n\n  @Action\n  public moveCurrentUrl(url: string) {\n    this.context.commit(\"setCurrentUrl\", url);\n  }\n}\n\nexport default Posts;\n","tsx",[27,4006,4007,4021,4035,4039,4051,4062,4073,4084,4095,4107,4118,4122,4126,4143,4159,4180,4194,4198,4220,4269,4278,4282,4289,4310,4326,4331,4335,4339,4343,4348,4367,4380,4385,4390,4410,4422,4427,4432,4438,4448,4476,4496,4502,4507,4512,4517,4536,4553,4558,4563,4568],{"__ignoreMap":83},[87,4008,4009,4011,4014,4016,4019],{"class":89,"line":90},[87,4010,2495],{"class":562},[87,4012,4013],{"class":97}," { VuexModule, Module, Mutation, Action } ",[87,4015,2501],{"class":562},[87,4017,4018],{"class":165}," \"vuex-module-decorators\"",[87,4020,114],{"class":97},[87,4022,4023,4025,4028,4030,4033],{"class":89,"line":101},[87,4024,2495],{"class":562},[87,4026,4027],{"class":97}," axios ",[87,4029,2501],{"class":562},[87,4031,4032],{"class":165}," \"axios\"",[87,4034,114],{"class":97},[87,4036,4037],{"class":89,"line":117},[87,4038,1446],{"emptyLinePlaceholder":842},[87,4040,4041,4043,4046,4049],{"class":89,"line":130},[87,4042,2570],{"class":562},[87,4044,4045],{"class":562}," interface",[87,4047,4048],{"class":93}," Post",[87,4050,98],{"class":97},[87,4052,4053,4056,4058,4060],{"class":89,"line":224},[87,4054,4055],{"class":1168},"  url",[87,4057,1204],{"class":562},[87,4059,1353],{"class":104},[87,4061,114],{"class":97},[87,4063,4064,4067,4069,4071],{"class":89,"line":246},[87,4065,4066],{"class":1168},"  fileName",[87,4068,1204],{"class":562},[87,4070,1353],{"class":104},[87,4072,114],{"class":97},[87,4074,4075,4078,4080,4082],{"class":89,"line":256},[87,4076,4077],{"class":1168},"    description",[87,4079,1204],{"class":562},[87,4081,1353],{"class":104},[87,4083,114],{"class":97},[87,4085,4086,4089,4091,4093],{"class":89,"line":270},[87,4087,4088],{"class":1168},"  title",[87,4090,1204],{"class":562},[87,4092,1353],{"class":104},[87,4094,114],{"class":97},[87,4096,4097,4100,4102,4104],{"class":89,"line":280},[87,4098,4099],{"class":1168},"  tags",[87,4101,1204],{"class":562},[87,4103,1353],{"class":104},[87,4105,4106],{"class":97},"[];\n",[87,4108,4109,4112,4114,4116],{"class":89,"line":295},[87,4110,4111],{"class":1168},"  data",[87,4113,1204],{"class":562},[87,4115,1353],{"class":104},[87,4117,114],{"class":97},[87,4119,4120],{"class":89,"line":315},[87,4121,133],{"class":97},[87,4123,4124],{"class":89,"line":330},[87,4125,1446],{"emptyLinePlaceholder":842},[87,4127,4128,4131,4134,4137,4140],{"class":89,"line":351},[87,4129,4130],{"class":97},"@",[87,4132,4133],{"class":93},"Module",[87,4135,4136],{"class":97},"({ namespaced: ",[87,4138,4139],{"class":104},"true",[87,4141,4142],{"class":97},", })\n",[87,4144,4145,4148,4151,4154,4157],{"class":89,"line":360},[87,4146,4147],{"class":562},"class",[87,4149,4150],{"class":93}," Posts",[87,4152,4153],{"class":562}," extends",[87,4155,4156],{"class":93}," VuexModule",[87,4158,98],{"class":97},[87,4160,4161,4164,4167,4169,4172,4175,4177],{"class":89,"line":374},[87,4162,4163],{"class":562},"  public",[87,4165,4166],{"class":1168}," postList",[87,4168,1204],{"class":562},[87,4170,4171],{"class":93},"Post",[87,4173,4174],{"class":97},"[] ",[87,4176,162],{"class":562},[87,4178,4179],{"class":97}," [];\n",[87,4181,4182,4184,4187,4189,4192],{"class":89,"line":383},[87,4183,4163],{"class":562},[87,4185,4186],{"class":1168}," currentUrl",[87,4188,1362],{"class":562},[87,4190,4191],{"class":165}," ''",[87,4193,114],{"class":97},[87,4195,4196],{"class":89,"line":398},[87,4197,1446],{"emptyLinePlaceholder":842},[87,4199,4200,4203,4206,4209,4211,4213,4215,4218],{"class":89,"line":418},[87,4201,4202],{"class":562},"  get",[87,4204,4205],{"class":93}," currentPost",[87,4207,4208],{"class":97},"()",[87,4210,1204],{"class":562},[87,4212,4048],{"class":93},[87,4214,1356],{"class":562},[87,4216,4217],{"class":104}," undefined",[87,4219,98],{"class":97},[87,4221,4222,4225,4227,4230,4233,4236,4238,4241,4244,4246,4248,4250,4252,4254,4257,4260,4263,4266],{"class":89,"line":434},[87,4223,4224],{"class":562},"    if",[87,4226,791],{"class":97},[87,4228,4229],{"class":104},"this",[87,4231,4232],{"class":97},".currentUrl ",[87,4234,4235],{"class":562},"==",[87,4237,1359],{"class":104},[87,4239,4240],{"class":562}," ||",[87,4242,4243],{"class":104}," this",[87,4245,4232],{"class":97},[87,4247,4235],{"class":562},[87,4249,4191],{"class":165},[87,4251,4240],{"class":562},[87,4253,4243],{"class":104},[87,4255,4256],{"class":97},".postList.",[87,4258,4259],{"class":104},"length",[87,4261,4262],{"class":562}," \u003C=",[87,4264,4265],{"class":104}," 0",[87,4267,4268],{"class":97},"){\n",[87,4270,4271,4274,4276],{"class":89,"line":454},[87,4272,4273],{"class":562},"      return",[87,4275,4217],{"class":104},[87,4277,114],{"class":97},[87,4279,4280],{"class":89,"line":463},[87,4281,1689],{"class":97},[87,4283,4284,4287],{"class":89,"line":477},[87,4285,4286],{"class":562},"    else",[87,4288,98],{"class":97},[87,4290,4291,4293,4295,4297,4300,4302,4305,4308],{"class":89,"line":486},[87,4292,4273],{"class":562},[87,4294,4243],{"class":104},[87,4296,4256],{"class":97},[87,4298,4299],{"class":93},"find",[87,4301,791],{"class":97},[87,4303,4304],{"class":1168},"post",[87,4306,4307],{"class":562}," =>",[87,4309,98],{"class":97},[87,4311,4312,4315,4318,4321,4323],{"class":89,"line":3636},[87,4313,4314],{"class":562},"        return",[87,4316,4317],{"class":97}," post.url ",[87,4319,4320],{"class":562},"===",[87,4322,4243],{"class":104},[87,4324,4325],{"class":97},".currentUrl;\n",[87,4327,4328],{"class":89,"line":3641},[87,4329,4330],{"class":97},"      })\n",[87,4332,4333],{"class":89,"line":3653},[87,4334,1689],{"class":97},[87,4336,4337],{"class":89,"line":3663},[87,4338,1694],{"class":97},[87,4340,4341],{"class":89,"line":3670},[87,4342,1625],{"class":97},[87,4344,4345],{"class":89,"line":3681},[87,4346,4347],{"class":97},"  @Mutation\n",[87,4349,4350,4352,4355,4357,4360,4362,4364],{"class":89,"line":3692},[87,4351,4163],{"class":562},[87,4353,4354],{"class":93}," setPostList",[87,4356,791],{"class":97},[87,4358,4359],{"class":1168},"postList",[87,4361,1204],{"class":562},[87,4363,4048],{"class":93},[87,4365,4366],{"class":97},"[]) {\n",[87,4368,4369,4372,4375,4377],{"class":89,"line":3703},[87,4370,4371],{"class":104},"    this",[87,4373,4374],{"class":97},".postList ",[87,4376,162],{"class":562},[87,4378,4379],{"class":97}," postList;\n",[87,4381,4383],{"class":89,"line":4382},32,[87,4384,1694],{"class":97},[87,4386,4388],{"class":89,"line":4387},33,[87,4389,4347],{"class":97},[87,4391,4393,4395,4398,4400,4403,4405,4407],{"class":89,"line":4392},34,[87,4394,4163],{"class":562},[87,4396,4397],{"class":93}," setCurrentUrl",[87,4399,791],{"class":97},[87,4401,4402],{"class":1168},"currentUrl",[87,4404,1204],{"class":562},[87,4406,1353],{"class":104},[87,4408,4409],{"class":97},") {\n",[87,4411,4413,4415,4417,4419],{"class":89,"line":4412},35,[87,4414,4371],{"class":104},[87,4416,4232],{"class":97},[87,4418,162],{"class":562},[87,4420,4421],{"class":97}," currentUrl;\n",[87,4423,4425],{"class":89,"line":4424},36,[87,4426,1694],{"class":97},[87,4428,4430],{"class":89,"line":4429},37,[87,4431,1446],{"emptyLinePlaceholder":842},[87,4433,4435],{"class":89,"line":4434},38,[87,4436,4437],{"class":97},"  @Action\n",[87,4439,4441,4443,4446],{"class":89,"line":4440},39,[87,4442,4163],{"class":562},[87,4444,4445],{"class":93}," requestGetPostList",[87,4447,2003],{"class":97},[87,4449,4451,4454,4457,4459,4462,4465,4467,4469,4472,4474],{"class":89,"line":4450},40,[87,4452,4453],{"class":97},"    axios.",[87,4455,4456],{"class":93},"get",[87,4458,791],{"class":97},[87,4460,4461],{"class":165},"`\u002Fposts\u002Fpostlist.json`",[87,4463,4464],{"class":97},").",[87,4466,1196],{"class":93},[87,4468,791],{"class":97},[87,4470,4471],{"class":1168},"res",[87,4473,4307],{"class":562},[87,4475,98],{"class":97},[87,4477,4479,4482,4485,4488,4490,4493],{"class":89,"line":4478},41,[87,4480,4481],{"class":104},"      this",[87,4483,4484],{"class":97},".context.",[87,4486,4487],{"class":93},"commit",[87,4489,791],{"class":97},[87,4491,4492],{"class":165},"\"setPostList\"",[87,4494,4495],{"class":97},", res.data);\n",[87,4497,4499],{"class":89,"line":4498},42,[87,4500,4501],{"class":97},"    });\n",[87,4503,4505],{"class":89,"line":4504},43,[87,4506,1694],{"class":97},[87,4508,4510],{"class":89,"line":4509},44,[87,4511,1446],{"emptyLinePlaceholder":842},[87,4513,4515],{"class":89,"line":4514},45,[87,4516,4437],{"class":97},[87,4518,4520,4522,4525,4527,4530,4532,4534],{"class":89,"line":4519},46,[87,4521,4163],{"class":562},[87,4523,4524],{"class":93}," moveCurrentUrl",[87,4526,791],{"class":97},[87,4528,4529],{"class":1168},"url",[87,4531,1204],{"class":562},[87,4533,1353],{"class":104},[87,4535,4409],{"class":97},[87,4537,4539,4541,4543,4545,4547,4550],{"class":89,"line":4538},47,[87,4540,4371],{"class":104},[87,4542,4484],{"class":97},[87,4544,4487],{"class":93},[87,4546,791],{"class":97},[87,4548,4549],{"class":165},"\"setCurrentUrl\"",[87,4551,4552],{"class":97},", url);\n",[87,4554,4556],{"class":89,"line":4555},48,[87,4557,1694],{"class":97},[87,4559,4561],{"class":89,"line":4560},49,[87,4562,133],{"class":97},[87,4564,4566],{"class":89,"line":4565},50,[87,4567,1446],{"emptyLinePlaceholder":842},[87,4569,4571,4573,4576],{"class":89,"line":4570},51,[87,4572,2570],{"class":562},[87,4574,4575],{"class":562}," default",[87,4577,4578],{"class":97}," Posts;\n",[16,4580,4581],{},"posts store의 기능은 다음과 같다.",[2953,4583,4584,4590],{},[23,4585,4586,4589],{},[788,4587,4588],{},"포스트 목록을 postlist.json 에서 읽어오기(사이트 접속 시 한 번)","\n사이트 접속 시 한 번이라고 판단한 근거는 기술 블로그를 찾아다닐 때는 보통 구글링을 통해서 유입된다. 그렇다고 한번 들어온 사람이 그 블로그의 다른 글까지도 샅샅이 뒤져보는 경우는 거의 없다. 원하는 정보를 확인한 후 바로 나가는 것이 태반이다. 또 대부분 최신의 포스팅 보다는 최소 한 두달 전의 게시글을 확인하기 마련이며, 글 목록을 계속해서 갱신할 필요는 없다고 판단했다.",[23,4591,4592,4595],{},[788,4593,4594],{},"포스트 페이지에서 전달받은 포스트 url 을 갖고 해당하는 포스트를 조회한다.(포스트 페이지 진입 시)","\n이 부분은 갖고 있는 포스팅 목록에서 단순히 조회하는 기능이다. 추후에 글이 엄청 많아져서 조회에 걸리는 데 부하가 걸리면 행복한 고민이니까 나중에 하자.",[78,4597,4599],{"className":4002,"code":4598,"language":4004,"meta":83,"style":83},"\u002F\u002F App.vue\nimport { useStore } from 'vuex';\nconst store = useStore();\nstore.dispatch(\"Posts\u002FrequestGetPostList\");\n",[27,4600,4601,4606,4620,4634],{"__ignoreMap":83},[87,4602,4603],{"class":89,"line":90},[87,4604,4605],{"class":1339},"\u002F\u002F App.vue\n",[87,4607,4608,4610,4613,4615,4618],{"class":89,"line":101},[87,4609,2495],{"class":562},[87,4611,4612],{"class":97}," { useStore } ",[87,4614,2501],{"class":562},[87,4616,4617],{"class":165}," 'vuex'",[87,4619,114],{"class":97},[87,4621,4622,4624,4627,4629,4632],{"class":89,"line":117},[87,4623,1964],{"class":562},[87,4625,4626],{"class":104}," store",[87,4628,1362],{"class":562},[87,4630,4631],{"class":93}," useStore",[87,4633,2026],{"class":97},[87,4635,4636,4639,4642,4644,4647],{"class":89,"line":130},[87,4637,4638],{"class":97},"store.",[87,4640,4641],{"class":93},"dispatch",[87,4643,791],{"class":97},[87,4645,4646],{"class":165},"\"Posts\u002FrequestGetPostList\"",[87,4648,1590],{"class":97},[16,4650,4651,4652,4655],{},"사이트 최초 접속 시에만 포스트 목록을 가져오게끔 ",[27,4653,4654],{},"App.vue"," 의 setup 훅에서 조회하도록 했다.",[78,4657,4659],{"className":4002,"code":4658,"language":4004,"meta":83,"style":83},"\u002F\u002F DetailView.vue\nimport { ref, defineProps, onMounted, computed } from 'vue';\nimport { useRoute } from 'vue-router';\nimport { useStore } from 'vuex';\n\nconst store = useStore();\nconst route = useRoute();\nconst paramId = route.params.id;\n\nstore.dispatch(\"Posts\u002FmoveCurrentUrl\", paramId);\nconst post = computed(() => {\n  return store.getters[\"Posts\u002FcurrentPost\"];\n});\n",[27,4660,4661,4666,4679,4693,4705,4709,4721,4735,4747,4751,4765,4783,4796],{"__ignoreMap":83},[87,4662,4663],{"class":89,"line":90},[87,4664,4665],{"class":1339},"\u002F\u002F DetailView.vue\n",[87,4667,4668,4670,4673,4675,4677],{"class":89,"line":101},[87,4669,2495],{"class":562},[87,4671,4672],{"class":97}," { ref, defineProps, onMounted, computed } ",[87,4674,2501],{"class":562},[87,4676,2504],{"class":165},[87,4678,114],{"class":97},[87,4680,4681,4683,4686,4688,4691],{"class":89,"line":117},[87,4682,2495],{"class":562},[87,4684,4685],{"class":97}," { useRoute } ",[87,4687,2501],{"class":562},[87,4689,4690],{"class":165}," 'vue-router'",[87,4692,114],{"class":97},[87,4694,4695,4697,4699,4701,4703],{"class":89,"line":130},[87,4696,2495],{"class":562},[87,4698,4612],{"class":97},[87,4700,2501],{"class":562},[87,4702,4617],{"class":165},[87,4704,114],{"class":97},[87,4706,4707],{"class":89,"line":224},[87,4708,1446],{"emptyLinePlaceholder":842},[87,4710,4711,4713,4715,4717,4719],{"class":89,"line":246},[87,4712,1964],{"class":562},[87,4714,4626],{"class":104},[87,4716,1362],{"class":562},[87,4718,4631],{"class":93},[87,4720,2026],{"class":97},[87,4722,4723,4725,4728,4730,4733],{"class":89,"line":256},[87,4724,1964],{"class":562},[87,4726,4727],{"class":104}," route",[87,4729,1362],{"class":562},[87,4731,4732],{"class":93}," useRoute",[87,4734,2026],{"class":97},[87,4736,4737,4739,4742,4744],{"class":89,"line":270},[87,4738,1964],{"class":562},[87,4740,4741],{"class":104}," paramId",[87,4743,1362],{"class":562},[87,4745,4746],{"class":97}," route.params.id;\n",[87,4748,4749],{"class":89,"line":280},[87,4750,1446],{"emptyLinePlaceholder":842},[87,4752,4753,4755,4757,4759,4762],{"class":89,"line":295},[87,4754,4638],{"class":97},[87,4756,4641],{"class":93},[87,4758,791],{"class":97},[87,4760,4761],{"class":165},"\"Posts\u002FmoveCurrentUrl\"",[87,4763,4764],{"class":97},", paramId);\n",[87,4766,4767,4769,4772,4774,4777,4779,4781],{"class":89,"line":315},[87,4768,1964],{"class":562},[87,4770,4771],{"class":104}," post",[87,4773,1362],{"class":562},[87,4775,4776],{"class":93}," computed",[87,4778,2084],{"class":97},[87,4780,1175],{"class":562},[87,4782,98],{"class":97},[87,4784,4785,4787,4790,4793],{"class":89,"line":330},[87,4786,2691],{"class":562},[87,4788,4789],{"class":97}," store.getters[",[87,4791,4792],{"class":165},"\"Posts\u002FcurrentPost\"",[87,4794,4795],{"class":97},"];\n",[87,4797,4798],{"class":89,"line":351},[87,4799,1299],{"class":97},[16,4801,4802],{},"DetailView 가 포스트 내용을 보여줄 뷰인데, 포스트 url 을 확인한 후 해당하는 포스트 데이터를 가져오게 했다.",[16,4804,4805],{},[513,4806],{"alt":515,"src":4807},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled8.png",[16,4809,4810],{},"이걸 토대로 글 목록을 보여주니 잘 나온다.",[45,4812,4814],{"id":4813},"markdown-적용하기","Markdown 적용하기",[16,4816,4817],{},"나는 개발 노트를 2014년부터 사용했는데, 에버노트 - Typora - Joplin - 노션 순서로 넘어왔다. Typora 와 Joplin 은 사용기간이 매우 짧아서 에버노트 사용 후 노션으로 넘어왔다고 봐도 무방하다. 그 이유가 마크다운이었던 만큼, 개발 블로그는 무조건 마크다운이다 라고 생각했다.",[78,4819,4821],{"className":4002,"code":4820,"language":4004,"meta":83,"style":83},"npm i showdown\nnpm i showdown-table\n",[27,4822,4823,4828],{"__ignoreMap":83},[87,4824,4825],{"class":89,"line":90},[87,4826,4827],{"class":97},"npm i showdown\n",[87,4829,4830,4833,4836],{"class":89,"line":101},[87,4831,4832],{"class":97},"npm i showdown",[87,4834,4835],{"class":562},"-",[87,4837,4838],{"class":97},"table\n",[16,4840,4841,4844],{},[788,4842,4843],{},"showdown"," 이라는 라이브러리를 사용할 것이다. 참고한 블로그에서 사용했기 때문이다. 왠지 마크다운 변환 라이브러리가 많을 것 같아서 다 알아봐야하는 데 시간 좀 걸리겠다 했는데, 빠르게 정할 수 있었다. 이 라이브러리는 단순하게 md 데이터를 html 로 마크업해주는 역할이다. 단순 스트링을 인자로 넘겨주면 된다.",[16,4846,4847,4850],{},[788,4848,4849],{},"showdown-table"," 은 이름 그대로 table 까지 지원해주는 확장라이브러리이다. 그 말은 즉슨 showdown 라이브러리에서는 기본으로 지워하지 않는다는 뜻이다.",[78,4852,4854],{"className":4002,"code":4853,"language":4004,"meta":83,"style":83},"\u002F\u002F src\u002Fstore\u002Fposts.ts\n@Action\npublic requestGetMarkdoen(postName: string) {\n  return axios.get(`\u002Fposts\u002F${postName}.md`).then(res => {\n    const markdownPost = res.data;\n    **const converter = new showdown.Converter()**\n        converter.setOption('tables', true);\n    **const md2html = converter.makeHtml(markdownPost);\n    return md2html;**\n  });\n}\n",[27,4855,4856,4861,4866,4877,4909,4921,4944,4963,4981,4991,4996],{"__ignoreMap":83},[87,4857,4858],{"class":89,"line":90},[87,4859,4860],{"class":1339},"\u002F\u002F src\u002Fstore\u002Fposts.ts\n",[87,4862,4863],{"class":89,"line":101},[87,4864,4865],{"class":97},"@Action\n",[87,4867,4868,4871,4874],{"class":89,"line":117},[87,4869,4870],{"class":97},"public ",[87,4872,4873],{"class":93},"requestGetMarkdoen",[87,4875,4876],{"class":97},"(postName: string) {\n",[87,4878,4879,4881,4884,4886,4888,4891,4894,4897,4899,4901,4903,4905,4907],{"class":89,"line":130},[87,4880,2691],{"class":562},[87,4882,4883],{"class":97}," axios.",[87,4885,4456],{"class":93},[87,4887,791],{"class":97},[87,4889,4890],{"class":165},"`\u002Fposts\u002F${",[87,4892,4893],{"class":97},"postName",[87,4895,4896],{"class":165},"}.md`",[87,4898,4464],{"class":97},[87,4900,1196],{"class":93},[87,4902,791],{"class":97},[87,4904,4471],{"class":1168},[87,4906,4307],{"class":562},[87,4908,98],{"class":97},[87,4910,4911,4913,4916,4918],{"class":89,"line":224},[87,4912,1378],{"class":562},[87,4914,4915],{"class":104}," markdownPost",[87,4917,1362],{"class":562},[87,4919,4920],{"class":97}," res.data;\n",[87,4922,4923,4926,4929,4931,4933,4936,4939,4941],{"class":89,"line":246},[87,4924,4925],{"class":562},"    **const",[87,4927,4928],{"class":104}," converter",[87,4930,1362],{"class":562},[87,4932,2672],{"class":562},[87,4934,4935],{"class":97}," showdown.",[87,4937,4938],{"class":93},"Converter",[87,4940,4208],{"class":97},[87,4942,4943],{"class":562},"**\n",[87,4945,4946,4949,4952,4954,4957,4959,4961],{"class":89,"line":256},[87,4947,4948],{"class":97},"        converter.",[87,4950,4951],{"class":93},"setOption",[87,4953,791],{"class":97},[87,4955,4956],{"class":165},"'tables'",[87,4958,60],{"class":97},[87,4960,4139],{"class":104},[87,4962,1590],{"class":97},[87,4964,4965,4967,4970,4972,4975,4978],{"class":89,"line":270},[87,4966,4925],{"class":562},[87,4968,4969],{"class":104}," md2html",[87,4971,1362],{"class":562},[87,4973,4974],{"class":97}," converter.",[87,4976,4977],{"class":93},"makeHtml",[87,4979,4980],{"class":97},"(markdownPost);\n",[87,4982,4983,4986,4989],{"class":89,"line":280},[87,4984,4985],{"class":562},"    return",[87,4987,4988],{"class":97}," md2html;",[87,4990,4943],{"class":562},[87,4992,4993],{"class":89,"line":295},[87,4994,4995],{"class":97},"  });\n",[87,4997,4998],{"class":89,"line":315},[87,4999,133],{"class":97},[16,5001,5002,5003,5005,5006,5011],{},"사용 방법도 간단하다. converter 를 할당해주고, ",[27,5004,4977],{}," 함수를 호출하면 끝이다. table 확장을 추가할 때, 공식 Github 문서에서는 ",[788,5007,5008],{},[27,5009,5010],{},"new showdown.Converter({ extensions: ['table'] })"," 로 사용하라고 하지만, md 변환이 전혀 안되는 문제가 있어서 다른 방법을 찾았다.",[78,5013,5015],{"className":4002,"code":5014,"language":4004,"meta":83,"style":83},"\u002F\u002F DetailView.vue\n\u003Cdiv v-html=\"postContents\">\u003C\u002Fdiv>\n\n\u002F\u002F script\nonMounted(() => {\n  store.dispatch(\"Posts\u002FrequestGetMarkdoen\", post.value.fileName).then((res) => {\n    postContents.value = res;\n  })\n})\n",[27,5016,5017,5021,5042,5046,5051,5061,5088,5098,5103],{"__ignoreMap":83},[87,5018,5019],{"class":89,"line":90},[87,5020,4665],{"class":1339},[87,5022,5023,5025,5027,5030,5032,5035,5038,5040],{"class":89,"line":101},[87,5024,152],{"class":97},[87,5026,156],{"class":155},[87,5028,5029],{"class":93}," v-html",[87,5031,162],{"class":562},[87,5033,5034],{"class":165},"\"postContents\"",[87,5036,5037],{"class":97},">\u003C\u002F",[87,5039,156],{"class":155},[87,5041,169],{"class":97},[87,5043,5044],{"class":89,"line":117},[87,5045,1446],{"emptyLinePlaceholder":842},[87,5047,5048],{"class":89,"line":130},[87,5049,5050],{"class":1339},"\u002F\u002F script\n",[87,5052,5053,5055,5057,5059],{"class":89,"line":224},[87,5054,2081],{"class":93},[87,5056,2084],{"class":97},[87,5058,1175],{"class":562},[87,5060,98],{"class":97},[87,5062,5063,5066,5068,5070,5073,5076,5078,5080,5082,5084,5086],{"class":89,"line":246},[87,5064,5065],{"class":97},"  store.",[87,5067,4641],{"class":93},[87,5069,791],{"class":97},[87,5071,5072],{"class":165},"\"Posts\u002FrequestGetMarkdoen\"",[87,5074,5075],{"class":97},", post.value.fileName).",[87,5077,1196],{"class":93},[87,5079,1165],{"class":97},[87,5081,4471],{"class":1168},[87,5083,1172],{"class":97},[87,5085,1175],{"class":562},[87,5087,98],{"class":97},[87,5089,5090,5093,5095],{"class":89,"line":256},[87,5091,5092],{"class":97},"    postContents.value ",[87,5094,162],{"class":562},[87,5096,5097],{"class":97}," res;\n",[87,5099,5100],{"class":89,"line":270},[87,5101,5102],{"class":97},"  })\n",[87,5104,5105],{"class":89,"line":280},[87,5106,5107],{"class":97},"})\n",[16,5109,5110],{},"DOM의 innerValue 로 넣어주면 간단하게 md 로 작성했던 포스팅이 html 이 렌더링된다. 간단-",[45,5112,5114],{"id":5113},"prerendering","Prerendering",[16,5116,5117],{},"Github Pages 특성상 SPA 지원을 안하기 때문에, 홈이 아닌 다른 URL을 접속하려고 하면 404 페이지가 뜬다. 물론 SPA 사이트도 홈에서 라우팅을 한다면 작동은 하겠지만은 누가 블로그를 홈에서부터 여행을 하겠는가. 구글엔진은 SPA도 js를 실행해 크롤링한다고는 하는데, Github Pages는 홈을 제외한 페이지에 아예 접속조차 안되며, SEO도 신경써야하니 Prerendering 은 필수이다.",[78,5119,5121],{"className":4002,"code":5120,"language":4004,"meta":83,"style":83},"npm i -D prerender-spa-plugin -> webpack4\nnpm i -D prerender-spa-wp5-plugin -> webpack5\n",[27,5122,5123,5152],{"__ignoreMap":83},[87,5124,5125,5128,5130,5133,5136,5138,5141,5143,5146,5149],{"class":89,"line":90},[87,5126,5127],{"class":97},"npm i ",[87,5129,4835],{"class":562},[87,5131,5132],{"class":104},"D",[87,5134,5135],{"class":97}," prerender",[87,5137,4835],{"class":562},[87,5139,5140],{"class":97},"spa",[87,5142,4835],{"class":562},[87,5144,5145],{"class":97},"plugin ",[87,5147,5148],{"class":562},"->",[87,5150,5151],{"class":97}," webpack4\n",[87,5153,5154,5156,5158,5160,5162,5164,5166,5168,5171,5173,5175,5177],{"class":89,"line":101},[87,5155,5127],{"class":97},[87,5157,4835],{"class":562},[87,5159,5132],{"class":104},[87,5161,5135],{"class":97},[87,5163,4835],{"class":562},[87,5165,5140],{"class":97},[87,5167,4835],{"class":562},[87,5169,5170],{"class":97},"wp5",[87,5172,4835],{"class":562},[87,5174,5145],{"class":97},[87,5176,5148],{"class":562},[87,5178,5179],{"class":97}," webpack5\n",[16,5181,5182],{},"한참 애를 먹었던 녀석이다. 참고하던 블로그에서는 Vue가 내부적으로 Webpack4 를 사용해서 원본 라이브러리로도 잘 돌아가지만, 내가 만든 프로젝트에서는 계속 에러가 발생했다.",[1718,5184,5185],{},[16,5186,5187,5190],{},[87,5188,5189],{},"prerender-spa-plugin"," Unable to prerender all routes!",[16,5192,5193],{},"바로 이 에러인데, 내용도 모른다. 콜스택은 에러를 뿌려주는 코드의 에러로만 나타나서 어디가 문제인지도 한눈에 알아볼 수 없었다.",[16,5195,5196],{},"개같이 찾아봤지만 확실한 답을 얻지 못하고, 아예 라이브러리 소스를 디버깅해보기로 했다. 저 에러를 콘솔에 찍는 부분부터 역으로 코드를 확인해서 답을 얻었다.",[16,5198,5199,5200,5203,5204,5207],{},"Webpack5 에서의 ",[27,5201,5202],{},"complier.outputFileSystem"," 에는 ",[27,5205,5206],{},"mkdirp"," 가 없는데, 이걸 사용하려고 해서 에러가 발생했다. Webpack5로의 마이그레이션이 전혀 안된 것이다. 그제야 Github에서도 2018년에 업데이트가 끝났다는 걸 확인했다.",[16,5209,5210],{},"소스를 바꿔서 사용해야하나 싶었다.",[78,5212,5214],{"className":4002,"code":5213,"language":4004,"meta":83,"style":83},"\u002F\u002F node_modules\u002Fprerender-spa-plugin\u002Fes6\u002Findex.js\n\n\u002F\u002F 58 line\n\u002F\u002F mkdirp 함수를 대체한다.\nconst mkdirp = function (dir, opts) {\n  return new Promise((resolve, reject) => {\n    console.log('\\ndir', dir, opts, '\\n');\n    try {\n      compilerFS.mkdirp(dir, opts, (err, made) => err === null ? resolve(made) : reject(err))\n    } catch(e) {\n      compilerFS.mkdir(dir, opts, (err, made) => err === null ? resolve(made) : reject(err))\n    }\n  })\n}\n\n\u002F\u002F 124 line\n\u002F\u002F recursive 옵션을 추가해준다.\nreturn mkdirp(path.dirname(processedRoute.outputPath), {recursive: true}) \n",[27,5215,5216,5221,5225,5230,5235,5258,5280,5308,5315,5361,5371,5408,5412,5416,5420,5424,5429,5434],{"__ignoreMap":83},[87,5217,5218],{"class":89,"line":90},[87,5219,5220],{"class":1339},"\u002F\u002F node_modules\u002Fprerender-spa-plugin\u002Fes6\u002Findex.js\n",[87,5222,5223],{"class":89,"line":101},[87,5224,1446],{"emptyLinePlaceholder":842},[87,5226,5227],{"class":89,"line":117},[87,5228,5229],{"class":1339},"\u002F\u002F 58 line\n",[87,5231,5232],{"class":89,"line":130},[87,5233,5234],{"class":1339},"\u002F\u002F mkdirp 함수를 대체한다.\n",[87,5236,5237,5239,5242,5244,5246,5248,5251,5253,5256],{"class":89,"line":224},[87,5238,1964],{"class":562},[87,5240,5241],{"class":93}," mkdirp",[87,5243,1362],{"class":562},[87,5245,1997],{"class":562},[87,5247,2658],{"class":97},[87,5249,5250],{"class":1168},"dir",[87,5252,60],{"class":97},[87,5254,5255],{"class":1168},"opts",[87,5257,4409],{"class":97},[87,5259,5260,5262,5264,5266,5268,5270,5272,5274,5276,5278],{"class":89,"line":246},[87,5261,2691],{"class":562},[87,5263,2672],{"class":562},[87,5265,1972],{"class":104},[87,5267,1165],{"class":97},[87,5269,1926],{"class":1168},[87,5271,60],{"class":97},[87,5273,2061],{"class":1168},[87,5275,1172],{"class":97},[87,5277,1175],{"class":562},[87,5279,98],{"class":97},[87,5281,5282,5284,5286,5288,5291,5294,5297,5300,5302,5304,5306],{"class":89,"line":256},[87,5283,2450],{"class":97},[87,5285,1221],{"class":93},[87,5287,791],{"class":97},[87,5289,5290],{"class":165},"'",[87,5292,5293],{"class":104},"\\n",[87,5295,5296],{"class":165},"dir'",[87,5298,5299],{"class":97},", dir, opts, ",[87,5301,5290],{"class":165},[87,5303,5293],{"class":104},[87,5305,5290],{"class":165},[87,5307,1590],{"class":97},[87,5309,5310,5313],{"class":89,"line":270},[87,5311,5312],{"class":562},"    try",[87,5314,98],{"class":97},[87,5316,5317,5320,5322,5325,5328,5330,5333,5335,5337,5340,5342,5344,5347,5350,5353,5355,5358],{"class":89,"line":280},[87,5318,5319],{"class":97},"      compilerFS.",[87,5321,5206],{"class":93},[87,5323,5324],{"class":97},"(dir, opts, (",[87,5326,5327],{"class":1168},"err",[87,5329,60],{"class":97},[87,5331,5332],{"class":1168},"made",[87,5334,1172],{"class":97},[87,5336,1175],{"class":562},[87,5338,5339],{"class":97}," err ",[87,5341,4320],{"class":562},[87,5343,1359],{"class":104},[87,5345,5346],{"class":562}," ?",[87,5348,5349],{"class":93}," resolve",[87,5351,5352],{"class":97},"(made) ",[87,5354,1204],{"class":562},[87,5356,5357],{"class":93}," reject",[87,5359,5360],{"class":97},"(err))\n",[87,5362,5363,5366,5368],{"class":89,"line":295},[87,5364,5365],{"class":97},"    } ",[87,5367,1252],{"class":562},[87,5369,5370],{"class":97},"(e) {\n",[87,5372,5373,5375,5378,5380,5382,5384,5386,5388,5390,5392,5394,5396,5398,5400,5402,5404,5406],{"class":89,"line":315},[87,5374,5319],{"class":97},[87,5376,5377],{"class":93},"mkdir",[87,5379,5324],{"class":97},[87,5381,5327],{"class":1168},[87,5383,60],{"class":97},[87,5385,5332],{"class":1168},[87,5387,1172],{"class":97},[87,5389,1175],{"class":562},[87,5391,5339],{"class":97},[87,5393,4320],{"class":562},[87,5395,1359],{"class":104},[87,5397,5346],{"class":562},[87,5399,5349],{"class":93},[87,5401,5352],{"class":97},[87,5403,1204],{"class":562},[87,5405,5357],{"class":93},[87,5407,5360],{"class":97},[87,5409,5410],{"class":89,"line":330},[87,5411,1689],{"class":97},[87,5413,5414],{"class":89,"line":351},[87,5415,5102],{"class":97},[87,5417,5418],{"class":89,"line":360},[87,5419,133],{"class":97},[87,5421,5422],{"class":89,"line":374},[87,5423,1446],{"emptyLinePlaceholder":842},[87,5425,5426],{"class":89,"line":383},[87,5427,5428],{"class":1339},"\u002F\u002F 124 line\n",[87,5430,5431],{"class":89,"line":398},[87,5432,5433],{"class":1339},"\u002F\u002F recursive 옵션을 추가해준다.\n",[87,5435,5436,5439,5441,5444,5447,5450,5452],{"class":89,"line":418},[87,5437,5438],{"class":562},"return",[87,5440,5241],{"class":93},[87,5442,5443],{"class":97},"(path.",[87,5445,5446],{"class":93},"dirname",[87,5448,5449],{"class":97},"(processedRoute.outputPath), {recursive: ",[87,5451,4139],{"class":104},[87,5453,5107],{"class":97},[16,5455,5456,5457,5459,5460,5462,5463,5466],{},"소스 수정 항목은 매우 간단하다. ",[27,5458,5377],{}," 함수로 바꾸면 되는데 이러면 또 문제가 생긴다. 포스팅 여러개를 렌더링할 때 ",[27,5461,5377],{}," 을 여러번 호출하는데, 중복이 된 경우에도 예외가 발생해서 렌더링이 안된다. 그래서 ",[27,5464,5465],{},"recursive"," 옵션까지 주어야 완벽하게 호환이 된다.",[16,5468,5469,5470,5472,5473,5476],{},"하지만 문제는 Github Actions 를 사용한다는 것이다. Actions 가 실행될 때는 ",[27,5471,3733],{}," 로 종속성 다운로드 후 바로 빌드를 하기 때문에 바뀐 소스를 적용하지 못한다. 그래서 결국 Webpack5 버전으로 누군가 새로 올려준 라이브러리를 받아서 진행했다. 그게 ",[27,5474,5475],{},"prerender-spa-wp5-plugin"," 이다.",[78,5478,5480],{"className":4002,"code":5479,"language":4004,"meta":83,"style":83},"const path = require('path')\nconst PrerenderSpaPlugin = require('prerender-spa-wp5-plugin')\n\nconst posts = require('.\u002Fpublic\u002Fposts\u002Fpostlist.json')\nconst routes = posts.map(post => `\u002F${post.url}`)\nconst paths = posts.map(post => {\n  return { \n    path: `\u002F${post.name}\u002F`,\n    lastmod: post.lastmod || post.date,\n    changefreq: 'yearly'\n  }\n})\n\nmodule.exports = [\n  new PrerenderSpaPlugin({\n    staticDir: path.join(__dirname, 'dist'),\n    routes: [\"\u002F\", ...routes],\n    renderer: new PrerenderSpaPlugin.PuppeteerRenderer({\n      renderAfterElementExists: '#app',\n    }),\n  }),\n];\n\u002F\u002F vue.config.js\nconst webpackPlugins = require('.\u002Fwebpack.plugin');\nmodule.exports = {\n    configureWebpack: (config) => {\n      if (process.env.NODE_ENV === 'production') {\n        config.plugins.push(...webpackPlugins); \u002F\u002F 상단에서 정의한 postPlugins 내용 삽입\n      }\n    },\n}\n",[27,5481,5482,5502,5520,5524,5542,5576,5597,5604,5623,5634,5642,5646,5650,5654,5667,5677,5694,5709,5725,5735,5740,5745,5749,5753,5771,5783,5800,5819,5837,5842,5847],{"__ignoreMap":83},[87,5483,5484,5486,5489,5491,5494,5496,5499],{"class":89,"line":90},[87,5485,1964],{"class":562},[87,5487,5488],{"class":104}," path",[87,5490,1362],{"class":562},[87,5492,5493],{"class":93}," require",[87,5495,791],{"class":97},[87,5497,5498],{"class":165},"'path'",[87,5500,5501],{"class":97},")\n",[87,5503,5504,5506,5509,5511,5513,5515,5518],{"class":89,"line":101},[87,5505,1964],{"class":562},[87,5507,5508],{"class":104}," PrerenderSpaPlugin",[87,5510,1362],{"class":562},[87,5512,5493],{"class":93},[87,5514,791],{"class":97},[87,5516,5517],{"class":165},"'prerender-spa-wp5-plugin'",[87,5519,5501],{"class":97},[87,5521,5522],{"class":89,"line":117},[87,5523,1446],{"emptyLinePlaceholder":842},[87,5525,5526,5528,5531,5533,5535,5537,5540],{"class":89,"line":130},[87,5527,1964],{"class":562},[87,5529,5530],{"class":104}," posts",[87,5532,1362],{"class":562},[87,5534,5493],{"class":93},[87,5536,791],{"class":97},[87,5538,5539],{"class":165},"'.\u002Fpublic\u002Fposts\u002Fpostlist.json'",[87,5541,5501],{"class":97},[87,5543,5544,5546,5549,5551,5554,5557,5559,5561,5563,5566,5568,5570,5572,5574],{"class":89,"line":224},[87,5545,1964],{"class":562},[87,5547,5548],{"class":104}," routes",[87,5550,1362],{"class":562},[87,5552,5553],{"class":97}," posts.",[87,5555,5556],{"class":93},"map",[87,5558,791],{"class":97},[87,5560,4304],{"class":1168},[87,5562,4307],{"class":562},[87,5564,5565],{"class":165}," `\u002F${",[87,5567,4304],{"class":97},[87,5569,1231],{"class":165},[87,5571,4529],{"class":97},[87,5573,1237],{"class":165},[87,5575,5501],{"class":97},[87,5577,5578,5580,5583,5585,5587,5589,5591,5593,5595],{"class":89,"line":246},[87,5579,1964],{"class":562},[87,5581,5582],{"class":104}," paths",[87,5584,1362],{"class":562},[87,5586,5553],{"class":97},[87,5588,5556],{"class":93},[87,5590,791],{"class":97},[87,5592,4304],{"class":1168},[87,5594,4307],{"class":562},[87,5596,98],{"class":97},[87,5598,5599,5601],{"class":89,"line":256},[87,5600,2691],{"class":562},[87,5602,5603],{"class":97}," { \n",[87,5605,5606,5609,5612,5614,5616,5618,5621],{"class":89,"line":270},[87,5607,5608],{"class":97},"    path: ",[87,5610,5611],{"class":165},"`\u002F${",[87,5613,4304],{"class":97},[87,5615,1231],{"class":165},[87,5617,1234],{"class":97},[87,5619,5620],{"class":165},"}\u002F`",[87,5622,3413],{"class":97},[87,5624,5625,5628,5631],{"class":89,"line":280},[87,5626,5627],{"class":97},"    lastmod: post.lastmod ",[87,5629,5630],{"class":562},"||",[87,5632,5633],{"class":97}," post.date,\n",[87,5635,5636,5639],{"class":89,"line":295},[87,5637,5638],{"class":97},"    changefreq: ",[87,5640,5641],{"class":165},"'yearly'\n",[87,5643,5644],{"class":89,"line":315},[87,5645,1694],{"class":97},[87,5647,5648],{"class":89,"line":330},[87,5649,5107],{"class":97},[87,5651,5652],{"class":89,"line":351},[87,5653,1446],{"emptyLinePlaceholder":842},[87,5655,5656,5658,5660,5662,5664],{"class":89,"line":360},[87,5657,3393],{"class":104},[87,5659,1231],{"class":97},[87,5661,3398],{"class":104},[87,5663,1362],{"class":562},[87,5665,5666],{"class":97}," [\n",[87,5668,5669,5672,5674],{"class":89,"line":374},[87,5670,5671],{"class":562},"  new",[87,5673,5508],{"class":93},[87,5675,5676],{"class":97},"({\n",[87,5678,5679,5682,5685,5688,5691],{"class":89,"line":383},[87,5680,5681],{"class":97},"    staticDir: path.",[87,5683,5684],{"class":93},"join",[87,5686,5687],{"class":97},"(__dirname, ",[87,5689,5690],{"class":165},"'dist'",[87,5692,5693],{"class":97},"),\n",[87,5695,5696,5699,5702,5704,5706],{"class":89,"line":398},[87,5697,5698],{"class":97},"    routes: [",[87,5700,5701],{"class":165},"\"\u002F\"",[87,5703,60],{"class":97},[87,5705,1438],{"class":562},[87,5707,5708],{"class":97},"routes],\n",[87,5710,5711,5714,5717,5720,5723],{"class":89,"line":418},[87,5712,5713],{"class":97},"    renderer: ",[87,5715,5716],{"class":562},"new",[87,5718,5719],{"class":97}," PrerenderSpaPlugin.",[87,5721,5722],{"class":93},"PuppeteerRenderer",[87,5724,5676],{"class":97},[87,5726,5727,5730,5733],{"class":89,"line":434},[87,5728,5729],{"class":97},"      renderAfterElementExists: ",[87,5731,5732],{"class":165},"'#app'",[87,5734,3413],{"class":97},[87,5736,5737],{"class":89,"line":454},[87,5738,5739],{"class":97},"    }),\n",[87,5741,5742],{"class":89,"line":463},[87,5743,5744],{"class":97},"  }),\n",[87,5746,5747],{"class":89,"line":477},[87,5748,4795],{"class":97},[87,5750,5751],{"class":89,"line":486},[87,5752,3388],{"class":1339},[87,5754,5755,5757,5760,5762,5764,5766,5769],{"class":89,"line":3636},[87,5756,1964],{"class":562},[87,5758,5759],{"class":104}," webpackPlugins",[87,5761,1362],{"class":562},[87,5763,5493],{"class":93},[87,5765,791],{"class":97},[87,5767,5768],{"class":165},"'.\u002Fwebpack.plugin'",[87,5770,1590],{"class":97},[87,5772,5773,5775,5777,5779,5781],{"class":89,"line":3641},[87,5774,3393],{"class":104},[87,5776,1231],{"class":97},[87,5778,3398],{"class":104},[87,5780,1362],{"class":562},[87,5782,98],{"class":97},[87,5784,5785,5788,5791,5794,5796,5798],{"class":89,"line":3653},[87,5786,5787],{"class":93},"    configureWebpack",[87,5789,5790],{"class":97},": (",[87,5792,5793],{"class":1168},"config",[87,5795,1172],{"class":97},[87,5797,1175],{"class":562},[87,5799,98],{"class":97},[87,5801,5802,5805,5808,5811,5814,5817],{"class":89,"line":3663},[87,5803,5804],{"class":562},"      if",[87,5806,5807],{"class":97}," (process.env.",[87,5809,5810],{"class":104},"NODE_ENV",[87,5812,5813],{"class":562}," ===",[87,5815,5816],{"class":165}," 'production'",[87,5818,4409],{"class":97},[87,5820,5821,5824,5827,5829,5831,5834],{"class":89,"line":3670},[87,5822,5823],{"class":97},"        config.plugins.",[87,5825,5826],{"class":93},"push",[87,5828,791],{"class":97},[87,5830,1438],{"class":562},[87,5832,5833],{"class":97},"webpackPlugins); ",[87,5835,5836],{"class":1339},"\u002F\u002F 상단에서 정의한 postPlugins 내용 삽입\n",[87,5838,5839],{"class":89,"line":3681},[87,5840,5841],{"class":97},"      }\n",[87,5843,5844],{"class":89,"line":3692},[87,5845,5846],{"class":97},"    },\n",[87,5848,5849],{"class":89,"line":3703},[87,5850,133],{"class":97},[16,5852,5853],{},"위와 같이 Webpack 플러그인을 작성한 후 등록해주면 간단하게 끝난다.",[16,5855,5856],{},[513,5857],{"alt":515,"src":5858},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled9.png",[16,5860,5861],{},"빌드하면 결과물 폴더에, 변환한 URL에 해당하는 index.html 파일들이 우수수 빌드가 된 걸 확인할 수 있다. 그리고 포스팅 URL 로 바로 접속도 가능해졌다.",[45,5863,5865],{"id":5864},"sitemap-자동-생성","Sitemap 자동 생성",[78,5867,5869],{"className":4002,"code":5868,"language":4004,"meta":83,"style":83},"npm i -D sitemap-webpack-plugin\n",[27,5870,5871],{"__ignoreMap":83},[87,5872,5873,5875,5877,5879,5882,5884,5887,5889],{"class":89,"line":90},[87,5874,5127],{"class":97},[87,5876,4835],{"class":562},[87,5878,5132],{"class":104},[87,5880,5881],{"class":97}," sitemap",[87,5883,4835],{"class":562},[87,5885,5886],{"class":97},"webpack",[87,5888,4835],{"class":562},[87,5890,5891],{"class":97},"plugin\n",[16,5893,5894,5895,5898],{},"사이트맵 생성에는 ",[27,5896,5897],{},"sitemap-webpack-plugin"," 을 사용한다. 개발자들의 스승님인 구글 검색 엔진에 블로그가 더 잘 노출이 되고, 크롤링에 도움을 주기위해서 사이트맵은 필요하다. Webpack 이 참 플러그인 생태계가 좋다.",[78,5900,5902],{"className":4002,"code":5901,"language":4004,"meta":83,"style":83},"const SitemapPlugin = require('sitemap-webpack-plugin').default;\nconst paths = posts.map(post => {\n  return { \n        path: `\u002F${post.url}\u002F`,\n    lastmod: post.updatedAt || post.createdAt,\n    changefreq: 'yearly'\n  }\n})\n\nmodule.exports = [\n    ...,\n  new SitemapPlugin({\n    base: process.env.VUE_APP_BASE_URL,\n    paths\n  })\n];\n",[27,5903,5904,5923,5943,5949,5966,5976,5982,5986,5990,5994,6006,6013,6021,6031,6036,6040],{"__ignoreMap":83},[87,5905,5906,5908,5911,5913,5915,5917,5920],{"class":89,"line":90},[87,5907,1964],{"class":562},[87,5909,5910],{"class":104}," SitemapPlugin",[87,5912,1362],{"class":562},[87,5914,5493],{"class":93},[87,5916,791],{"class":97},[87,5918,5919],{"class":165},"'sitemap-webpack-plugin'",[87,5921,5922],{"class":97},").default;\n",[87,5924,5925,5927,5929,5931,5933,5935,5937,5939,5941],{"class":89,"line":101},[87,5926,1964],{"class":562},[87,5928,5582],{"class":104},[87,5930,1362],{"class":562},[87,5932,5553],{"class":97},[87,5934,5556],{"class":93},[87,5936,791],{"class":97},[87,5938,4304],{"class":1168},[87,5940,4307],{"class":562},[87,5942,98],{"class":97},[87,5944,5945,5947],{"class":89,"line":117},[87,5946,2691],{"class":562},[87,5948,5603],{"class":97},[87,5950,5951,5954,5956,5958,5960,5962,5964],{"class":89,"line":130},[87,5952,5953],{"class":97},"        path: ",[87,5955,5611],{"class":165},[87,5957,4304],{"class":97},[87,5959,1231],{"class":165},[87,5961,4529],{"class":97},[87,5963,5620],{"class":165},[87,5965,3413],{"class":97},[87,5967,5968,5971,5973],{"class":89,"line":224},[87,5969,5970],{"class":97},"    lastmod: post.updatedAt ",[87,5972,5630],{"class":562},[87,5974,5975],{"class":97}," post.createdAt,\n",[87,5977,5978,5980],{"class":89,"line":246},[87,5979,5638],{"class":97},[87,5981,5641],{"class":165},[87,5983,5984],{"class":89,"line":256},[87,5985,1694],{"class":97},[87,5987,5988],{"class":89,"line":270},[87,5989,5107],{"class":97},[87,5991,5992],{"class":89,"line":280},[87,5993,1446],{"emptyLinePlaceholder":842},[87,5995,5996,5998,6000,6002,6004],{"class":89,"line":295},[87,5997,3393],{"class":104},[87,5999,1231],{"class":97},[87,6001,3398],{"class":104},[87,6003,1362],{"class":562},[87,6005,5666],{"class":97},[87,6007,6008,6011],{"class":89,"line":315},[87,6009,6010],{"class":562},"    ...",[87,6012,3413],{"class":97},[87,6014,6015,6017,6019],{"class":89,"line":330},[87,6016,5671],{"class":562},[87,6018,5910],{"class":93},[87,6020,5676],{"class":97},[87,6022,6023,6026,6029],{"class":89,"line":351},[87,6024,6025],{"class":97},"    base: process.env.",[87,6027,6028],{"class":104},"VUE_APP_BASE_URL",[87,6030,3413],{"class":97},[87,6032,6033],{"class":89,"line":360},[87,6034,6035],{"class":97},"    paths\n",[87,6037,6038],{"class":89,"line":374},[87,6039,5102],{"class":97},[87,6041,6042],{"class":89,"line":383},[87,6043,4795],{"class":97},[16,6045,6046],{},"또 플러그인을 작성하고, 빌드하면 끝이다.",[78,6048,6050],{"className":4002,"code":6049,"language":4004,"meta":83,"style":83},"\u003Curl>\n    \u003Cloc>https:\u002F\u002Flhs-source.github.io\u002Fd5c653f34310b85f731a497e64970059921fba363647f5cc1e39ead9ea9cf76f\u002F\u003C\u002Floc>\n    \u003Clastmod>2022-06-25T00:00:00.000Z\u003C\u002Flastmod>\n    \u003Cchangefreq>yearly\u003C\u002Fchangefreq>\n\u003C\u002Furl>\n\u003Curl>\n    \u003Cloc>https:\u002F\u002Flhs-source.github.io\u002F8689a6f9a9428af5a3570a0236abdb9abf62409077138a3d02a676b3a3f757f4\u002F\u003C\u002Floc>\n    \u003Clastmod>2022-06-25T00:00:00.000Z\u003C\u002Flastmod>\n    \u003Cchangefreq>yearly\u003C\u002Fchangefreq>\n\u003C\u002Furl>\n",[27,6051,6052,6060,6074,6088,6102,6110,6118,6131,6143,6155],{"__ignoreMap":83},[87,6053,6054,6056,6058],{"class":89,"line":90},[87,6055,152],{"class":97},[87,6057,4529],{"class":155},[87,6059,169],{"class":97},[87,6061,6062,6064,6067,6070,6072],{"class":89,"line":101},[87,6063,174],{"class":97},[87,6065,6066],{"class":155},"loc",[87,6068,6069],{"class":97},">https:\u002F\u002Flhs-source.github.io\u002Fd5c653f34310b85f731a497e64970059921fba363647f5cc1e39ead9ea9cf76f\u002F\u003C\u002F",[87,6071,6066],{"class":155},[87,6073,169],{"class":97},[87,6075,6076,6078,6081,6084,6086],{"class":89,"line":117},[87,6077,174],{"class":97},[87,6079,6080],{"class":155},"lastmod",[87,6082,6083],{"class":97},">2022-06-25T00:00:00.000Z\u003C\u002F",[87,6085,6080],{"class":155},[87,6087,169],{"class":97},[87,6089,6090,6092,6095,6098,6100],{"class":89,"line":130},[87,6091,174],{"class":97},[87,6093,6094],{"class":155},"changefreq",[87,6096,6097],{"class":97},">yearly\u003C\u002F",[87,6099,6094],{"class":155},[87,6101,169],{"class":97},[87,6103,6104,6106,6108],{"class":89,"line":224},[87,6105,489],{"class":97},[87,6107,4529],{"class":155},[87,6109,169],{"class":97},[87,6111,6112,6114,6116],{"class":89,"line":246},[87,6113,152],{"class":97},[87,6115,4529],{"class":155},[87,6117,169],{"class":97},[87,6119,6120,6122,6124,6127,6129],{"class":89,"line":256},[87,6121,174],{"class":97},[87,6123,6066],{"class":155},[87,6125,6126],{"class":97},">https:\u002F\u002Flhs-source.github.io\u002F8689a6f9a9428af5a3570a0236abdb9abf62409077138a3d02a676b3a3f757f4\u002F\u003C\u002F",[87,6128,6066],{"class":155},[87,6130,169],{"class":97},[87,6132,6133,6135,6137,6139,6141],{"class":89,"line":270},[87,6134,174],{"class":97},[87,6136,6080],{"class":155},[87,6138,6083],{"class":97},[87,6140,6080],{"class":155},[87,6142,169],{"class":97},[87,6144,6145,6147,6149,6151,6153],{"class":89,"line":280},[87,6146,174],{"class":97},[87,6148,6094],{"class":155},[87,6150,6097],{"class":97},[87,6152,6094],{"class":155},[87,6154,169],{"class":97},[87,6156,6157,6159,6161],{"class":89,"line":295},[87,6158,489],{"class":97},[87,6160,4529],{"class":155},[87,6162,169],{"class":97},[16,6164,6165,6166,6169],{},"이렇게 구글 엔진에 등록할 ",[27,6167,6168],{},"sitemap.xml"," 이 저절로 만들어진다. 결과물 폴더에 만들어지기 때문에, 브라우저에서도 바로 접속할 수 있다.",[753,6171,6173],{"id":6172},"사이트맵-등록하기","사이트맵 등록하기",[16,6175,6176],{},[2943,6177,6180],{"href":6178,"rel":6179},"https:\u002F\u002Fsearch.google.com\u002Fsearch-console\u002Fabout",[2947],"Google Search Console",[16,6182,6183],{},"구글 검색 콘솔에 접속한다. 우선은 사이트를 생성하고, 그 사이트에 사이트맵을 등록할 것이다.",[16,6185,6186],{},[513,6187],{"alt":515,"src":6188},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled10.png",[16,6190,6191],{},"URL 을 입력하고 다음으로 넘어가자.",[16,6193,6194],{},[513,6195],{"alt":515,"src":6196},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled11.png",[78,6198,6200],{"className":4002,"code":6199,"language":4004,"meta":83,"style":83},"\u003C!DOCTYPE html>\n\u003Chtml lang=\"\">\n  \u003Chead>\n    \u003Cmeta name=\"google-site-verification\" content=\"p4lZFdtKOsDj6_1O2f_PsufQnO068kmB_Sgrs5UlweQ\" \u002F>\n    \u003C\u002Fhead>\n\u003C\u002Fhtml>\n",[27,6201,6202,6215,6230,6239,6264,6272],{"__ignoreMap":83},[87,6203,6204,6207,6210,6213],{"class":89,"line":90},[87,6205,6206],{"class":562},"\u003C!",[87,6208,6209],{"class":104},"DOCTYPE",[87,6211,6212],{"class":97}," html",[87,6214,169],{"class":562},[87,6216,6217,6219,6221,6223,6225,6228],{"class":89,"line":101},[87,6218,152],{"class":97},[87,6220,145],{"class":155},[87,6222,1947],{"class":93},[87,6224,162],{"class":562},[87,6226,6227],{"class":165},"\"\"",[87,6229,169],{"class":97},[87,6231,6232,6234,6237],{"class":89,"line":117},[87,6233,2121],{"class":97},[87,6235,6236],{"class":155},"head",[87,6238,169],{"class":97},[87,6240,6241,6243,6246,6249,6251,6254,6257,6259,6262],{"class":89,"line":130},[87,6242,174],{"class":97},[87,6244,6245],{"class":155},"meta",[87,6247,6248],{"class":93}," name",[87,6250,162],{"class":562},[87,6252,6253],{"class":165},"\"google-site-verification\"",[87,6255,6256],{"class":93}," content",[87,6258,162],{"class":562},[87,6260,6261],{"class":165},"\"p4lZFdtKOsDj6_1O2f_PsufQnO068kmB_Sgrs5UlweQ\"",[87,6263,2143],{"class":97},[87,6265,6266,6268,6270],{"class":89,"line":224},[87,6267,273],{"class":97},[87,6269,6236],{"class":155},[87,6271,169],{"class":97},[87,6273,6274,6276,6278],{"class":89,"line":246},[87,6275,489],{"class":97},[87,6277,145],{"class":155},[87,6279,169],{"class":97},[16,6281,6282],{},"제일 간단한 메타태그 추가를 통해 사이트 인증을 진행한다. 사이트 루트에 메타태그가 있어야하기 때문에, public\u002Findex.html 에 메타태그를 추가해주자.",[16,6284,6285],{},[513,6286],{"alt":515,"src":6287},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled12.png",[16,6289,6290],{},"블로그 루트 페이지에 추가된 것을 확인하고 다시 콘솔로 간다.",[16,6292,6293],{},[513,6294],{"alt":515,"src":6295},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled13.png",[16,6297,6298],{},"소유권을 확인받았다.",[16,6300,6301],{},[513,6302],{"alt":515,"src":6303},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled14.png",[16,6305,6306,6307,6310],{},"사이트맵은 자동생성되었기 때문에, ",[27,6308,6309],{},"\u002Fsitemap.xml"," 를 통해 접속할 수 있다. URL로 한번 접속해보고 데이터가 잘 오는 걸 확인하고 제출하자.",[16,6312,6313],{},[513,6314],{"alt":515,"src":6315},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled15.png",[16,6317,6318],{},"성공!",[45,6320,6322],{"id":6321},"메타태그-작성하기","메타태그 작성하기",[16,6324,6325],{},"블로그 내용에 대해서 잘 알려주기 위해 렌더링하는 포스팅 페이지에 메타태그를 추가하자. 누군가 링크를 퍼가거나, 검색엔진에서 긁어갈 때 메타태그가 아주 중요한 역할을 한다.",[78,6327,6329],{"className":4002,"code":6328,"language":4004,"meta":83,"style":83},"module.exports = [\n  new PrerenderSpaPlugin({\n    postProcess(renderedHtml) {\n            let { html, route } = renderedHtml;\n      console.log('renderedHtml', renderedHtml);\n      const foundPost = posts.find(post => route.includes(post.url))\n      if(foundPost == null) {\n        return renderedHtml;\n      }\n      const { title, description, tags } = foundPost;\n      const titleText = title ? title.replace(\u002F\u003Cbr>\u002Fig, '') : process.env.VUE_APP_TITLE\n      const descriptionText = description || '이현수 개발기'\n      const tagsText = tags || '개발, 프론트엔드, 블로그, github pages, Vue3'\n      const url = `${process.env.VUE_APP_BASE_URL}${route}`\n      const imgUrl = `${process.env.VUE_APP_BASE_URL}\u002Fimages\u002Fthumbnail.jpg`\n\n      const metaData = `\n        \u003Ctitle>${titleText}\u003C\u002Ftitle>\n        \u003Cmeta name=\"title\" content=\"${titleText}\" \u002F>\n        \u003Cmeta name=\"description\" content=\"${descriptionText}\" \u002F>\n        \u003Cmeta name=\"keywords\" content=\"${tagsText}\" \u002F>\n        \u003Cmeta property=\"og:url\" content=\"${url}\" \u002F>\n        \u003Cmeta property=\"og:type\" content=\"article\" \u002F>\n        \u003Cmeta property=\"og:title\" content=\"${titleText}\" \u002F>\n        \u003Cmeta property=\"og:description\" content=\"${descriptionText}\" \u002F>\n        \u003Cmeta property=\"og:image\" content=\"${imgUrl}\" \u002F>\n        \u003Cmeta property=\"twitter:card\" content=\"${imgUrl}\" \u002F>\n        \u003Cmeta property=\"twitter:url\" content=\"${url}\" \u002F>\n        \u003Cmeta property=\"twitter:title\" content=\"${titleText}\" \u002F>\n        \u003Cmeta property=\"twitter:description\" content=\"${descriptionText}\" \u002F>\n        \u003Cmeta property=\"twitter:image\" content=\"${imgUrl}\" \u002F>\n      `;\n      const start = html.indexOf('\u003Chead>') + '\u003Chead>'.length;\n      html = html.slice(0, start) + metaData + html.slice(start);\n      renderedRoute.html = html;\n      return renderedRoute;\n    },\n",[27,6330,6331,6343,6351,6363,6376,6391,6420,6433,6439,6443,6471,6521,6538,6555,6588,6612,6616,6628,6639,6649,6659,6669,6678,6683,6692,6701,6711,6720,6729,6738,6747,6756,6763,6797,6830,6840,6847],{"__ignoreMap":83},[87,6332,6333,6335,6337,6339,6341],{"class":89,"line":90},[87,6334,3393],{"class":104},[87,6336,1231],{"class":97},[87,6338,3398],{"class":104},[87,6340,1362],{"class":562},[87,6342,5666],{"class":97},[87,6344,6345,6347,6349],{"class":89,"line":101},[87,6346,5671],{"class":562},[87,6348,5508],{"class":93},[87,6350,5676],{"class":97},[87,6352,6353,6356,6358,6361],{"class":89,"line":117},[87,6354,6355],{"class":93},"    postProcess",[87,6357,791],{"class":97},[87,6359,6360],{"class":1168},"renderedHtml",[87,6362,4409],{"class":97},[87,6364,6365,6368,6371,6373],{"class":89,"line":130},[87,6366,6367],{"class":562},"            let",[87,6369,6370],{"class":97}," { html, route } ",[87,6372,162],{"class":562},[87,6374,6375],{"class":97}," renderedHtml;\n",[87,6377,6378,6381,6383,6385,6388],{"class":89,"line":224},[87,6379,6380],{"class":97},"      console.",[87,6382,1221],{"class":93},[87,6384,791],{"class":97},[87,6386,6387],{"class":165},"'renderedHtml'",[87,6389,6390],{"class":97},", renderedHtml);\n",[87,6392,6393,6396,6399,6401,6403,6405,6407,6409,6411,6414,6417],{"class":89,"line":246},[87,6394,6395],{"class":562},"      const",[87,6397,6398],{"class":104}," foundPost",[87,6400,1362],{"class":562},[87,6402,5553],{"class":97},[87,6404,4299],{"class":93},[87,6406,791],{"class":97},[87,6408,4304],{"class":1168},[87,6410,4307],{"class":562},[87,6412,6413],{"class":97}," route.",[87,6415,6416],{"class":93},"includes",[87,6418,6419],{"class":97},"(post.url))\n",[87,6421,6422,6424,6427,6429,6431],{"class":89,"line":256},[87,6423,5804],{"class":562},[87,6425,6426],{"class":97},"(foundPost ",[87,6428,4235],{"class":562},[87,6430,1359],{"class":104},[87,6432,4409],{"class":97},[87,6434,6435,6437],{"class":89,"line":270},[87,6436,4314],{"class":562},[87,6438,6375],{"class":97},[87,6440,6441],{"class":89,"line":280},[87,6442,5841],{"class":97},[87,6444,6445,6447,6450,6453,6455,6458,6460,6463,6466,6468],{"class":89,"line":295},[87,6446,6395],{"class":562},[87,6448,6449],{"class":97}," { ",[87,6451,6452],{"class":104},"title",[87,6454,60],{"class":97},[87,6456,6457],{"class":104},"description",[87,6459,60],{"class":97},[87,6461,6462],{"class":104},"tags",[87,6464,6465],{"class":97}," } ",[87,6467,162],{"class":562},[87,6469,6470],{"class":97}," foundPost;\n",[87,6472,6473,6475,6478,6480,6483,6486,6489,6492,6494,6497,6501,6503,6506,6508,6511,6513,6515,6518],{"class":89,"line":315},[87,6474,6395],{"class":562},[87,6476,6477],{"class":104}," titleText",[87,6479,1362],{"class":562},[87,6481,6482],{"class":97}," title ",[87,6484,6485],{"class":562},"?",[87,6487,6488],{"class":97}," title.",[87,6490,6491],{"class":93},"replace",[87,6493,791],{"class":97},[87,6495,6496],{"class":165},"\u002F",[87,6498,6500],{"class":6499},"sA_wV","\u003Cbr>",[87,6502,6496],{"class":165},[87,6504,6505],{"class":562},"ig",[87,6507,60],{"class":97},[87,6509,6510],{"class":165},"''",[87,6512,1172],{"class":97},[87,6514,1204],{"class":562},[87,6516,6517],{"class":97}," process.env.",[87,6519,6520],{"class":104},"VUE_APP_TITLE\n",[87,6522,6523,6525,6528,6530,6533,6535],{"class":89,"line":330},[87,6524,6395],{"class":562},[87,6526,6527],{"class":104}," descriptionText",[87,6529,1362],{"class":562},[87,6531,6532],{"class":97}," description ",[87,6534,5630],{"class":562},[87,6536,6537],{"class":165}," '이현수 개발기'\n",[87,6539,6540,6542,6545,6547,6550,6552],{"class":89,"line":351},[87,6541,6395],{"class":562},[87,6543,6544],{"class":104}," tagsText",[87,6546,1362],{"class":562},[87,6548,6549],{"class":97}," tags ",[87,6551,5630],{"class":562},[87,6553,6554],{"class":165}," '개발, 프론트엔드, 블로그, github pages, Vue3'\n",[87,6556,6557,6559,6562,6564,6567,6570,6572,6575,6577,6579,6582,6585],{"class":89,"line":360},[87,6558,6395],{"class":562},[87,6560,6561],{"class":104}," url",[87,6563,1362],{"class":562},[87,6565,6566],{"class":165}," `${",[87,6568,6569],{"class":97},"process",[87,6571,1231],{"class":165},[87,6573,6574],{"class":97},"env",[87,6576,1231],{"class":165},[87,6578,6028],{"class":104},[87,6580,6581],{"class":165},"}${",[87,6583,6584],{"class":97},"route",[87,6586,6587],{"class":165},"}`\n",[87,6589,6590,6592,6595,6597,6599,6601,6603,6605,6607,6609],{"class":89,"line":374},[87,6591,6395],{"class":562},[87,6593,6594],{"class":104}," imgUrl",[87,6596,1362],{"class":562},[87,6598,6566],{"class":165},[87,6600,6569],{"class":97},[87,6602,1231],{"class":165},[87,6604,6574],{"class":97},[87,6606,1231],{"class":165},[87,6608,6028],{"class":104},[87,6610,6611],{"class":165},"}\u002Fimages\u002Fthumbnail.jpg`\n",[87,6613,6614],{"class":89,"line":383},[87,6615,1446],{"emptyLinePlaceholder":842},[87,6617,6618,6620,6623,6625],{"class":89,"line":398},[87,6619,6395],{"class":562},[87,6621,6622],{"class":104}," metaData",[87,6624,1362],{"class":562},[87,6626,6627],{"class":165}," `\n",[87,6629,6630,6633,6636],{"class":89,"line":418},[87,6631,6632],{"class":165},"        \u003Ctitle>${",[87,6634,6635],{"class":97},"titleText",[87,6637,6638],{"class":165},"}\u003C\u002Ftitle>\n",[87,6640,6641,6644,6646],{"class":89,"line":434},[87,6642,6643],{"class":165},"        \u003Cmeta name=\"title\" content=\"${",[87,6645,6635],{"class":97},[87,6647,6648],{"class":165},"}\" \u002F>\n",[87,6650,6651,6654,6657],{"class":89,"line":454},[87,6652,6653],{"class":165},"        \u003Cmeta name=\"description\" content=\"${",[87,6655,6656],{"class":97},"descriptionText",[87,6658,6648],{"class":165},[87,6660,6661,6664,6667],{"class":89,"line":463},[87,6662,6663],{"class":165},"        \u003Cmeta name=\"keywords\" content=\"${",[87,6665,6666],{"class":97},"tagsText",[87,6668,6648],{"class":165},[87,6670,6671,6674,6676],{"class":89,"line":477},[87,6672,6673],{"class":165},"        \u003Cmeta property=\"og:url\" content=\"${",[87,6675,4529],{"class":97},[87,6677,6648],{"class":165},[87,6679,6680],{"class":89,"line":486},[87,6681,6682],{"class":165},"        \u003Cmeta property=\"og:type\" content=\"article\" \u002F>\n",[87,6684,6685,6688,6690],{"class":89,"line":3636},[87,6686,6687],{"class":165},"        \u003Cmeta property=\"og:title\" content=\"${",[87,6689,6635],{"class":97},[87,6691,6648],{"class":165},[87,6693,6694,6697,6699],{"class":89,"line":3641},[87,6695,6696],{"class":165},"        \u003Cmeta property=\"og:description\" content=\"${",[87,6698,6656],{"class":97},[87,6700,6648],{"class":165},[87,6702,6703,6706,6709],{"class":89,"line":3653},[87,6704,6705],{"class":165},"        \u003Cmeta property=\"og:image\" content=\"${",[87,6707,6708],{"class":97},"imgUrl",[87,6710,6648],{"class":165},[87,6712,6713,6716,6718],{"class":89,"line":3663},[87,6714,6715],{"class":165},"        \u003Cmeta property=\"twitter:card\" content=\"${",[87,6717,6708],{"class":97},[87,6719,6648],{"class":165},[87,6721,6722,6725,6727],{"class":89,"line":3670},[87,6723,6724],{"class":165},"        \u003Cmeta property=\"twitter:url\" content=\"${",[87,6726,4529],{"class":97},[87,6728,6648],{"class":165},[87,6730,6731,6734,6736],{"class":89,"line":3681},[87,6732,6733],{"class":165},"        \u003Cmeta property=\"twitter:title\" content=\"${",[87,6735,6635],{"class":97},[87,6737,6648],{"class":165},[87,6739,6740,6743,6745],{"class":89,"line":3692},[87,6741,6742],{"class":165},"        \u003Cmeta property=\"twitter:description\" content=\"${",[87,6744,6656],{"class":97},[87,6746,6648],{"class":165},[87,6748,6749,6752,6754],{"class":89,"line":3703},[87,6750,6751],{"class":165},"        \u003Cmeta property=\"twitter:image\" content=\"${",[87,6753,6708],{"class":97},[87,6755,6648],{"class":165},[87,6757,6758,6761],{"class":89,"line":4382},[87,6759,6760],{"class":165},"      `",[87,6762,114],{"class":97},[87,6764,6765,6767,6770,6772,6775,6778,6780,6783,6785,6788,6791,6793,6795],{"class":89,"line":4387},[87,6766,6395],{"class":562},[87,6768,6769],{"class":104}," start",[87,6771,1362],{"class":562},[87,6773,6774],{"class":97}," html.",[87,6776,6777],{"class":93},"indexOf",[87,6779,791],{"class":97},[87,6781,6782],{"class":165},"'\u003Chead>'",[87,6784,1172],{"class":97},[87,6786,6787],{"class":562},"+",[87,6789,6790],{"class":165}," '\u003Chead>'",[87,6792,1231],{"class":97},[87,6794,4259],{"class":104},[87,6796,114],{"class":97},[87,6798,6799,6802,6804,6806,6809,6811,6813,6816,6818,6821,6823,6825,6827],{"class":89,"line":4392},[87,6800,6801],{"class":97},"      html ",[87,6803,162],{"class":562},[87,6805,6774],{"class":97},[87,6807,6808],{"class":93},"slice",[87,6810,791],{"class":97},[87,6812,1574],{"class":104},[87,6814,6815],{"class":97},", start) ",[87,6817,6787],{"class":562},[87,6819,6820],{"class":97}," metaData ",[87,6822,6787],{"class":562},[87,6824,6774],{"class":97},[87,6826,6808],{"class":93},[87,6828,6829],{"class":97},"(start);\n",[87,6831,6832,6835,6837],{"class":89,"line":4412},[87,6833,6834],{"class":97},"      renderedRoute.html ",[87,6836,162],{"class":562},[87,6838,6839],{"class":97}," html;\n",[87,6841,6842,6844],{"class":89,"line":4424},[87,6843,4273],{"class":562},[87,6845,6846],{"class":97}," renderedRoute;\n",[87,6848,6849],{"class":89,"line":4429},[87,6850,5846],{"class":97},[16,6852,6853,6854,6857,6858,6861,6862,60,6864,6866,6867,6870],{},"아까 사용했던 ",[27,6855,6856],{},"PrerenderSpaPlugin"," 을 활용한다. 훅 중 하나인 ",[27,6859,6860],{},"postProcess"," 에 메타 태그를 심어주는 코드를 작성한다. 필수로 들어가면 좋을 태그는 ",[27,6863,6452],{},[27,6865,6457],{}," , ",[27,6868,6869],{},"keyword"," 라고 한다.",[45,6872,6874],{"id":6873},"google-analytics-추가","Google Analytics 추가",[16,6876,6877],{},"네이버나 구글 블로그 같이 전용 플랫폼 위에서 돌아가는 블로그가 아니기 때문에 방문자 집계는 직접 해야한다. 그렇기 때문에 Google Analytics 4를 사용해서 방문자와 페이지뷰 수를 확인하는 간단한 기능을 추가할 것이다.",[753,6879,6881],{"id":6880},"ga4-계정-생성하기","GA4 계정 생성하기",[16,6883,6884],{},[2943,6885,6888],{"href":6886,"rel":6887},"https:\u002F\u002Fanalytics.google.com\u002F",[2947],"Redirecting...",[16,6890,6891],{},[513,6892],{"alt":515,"src":6893},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled16.png",[16,6895,6896],{},"GA 콘솔에 들어가서 계정과 속성을 차례대로 만들어준다. 속성 단위로 데이터를 종합하고 집계해서 대시보드를 작성한다고 보면 된다. 속성을 만들 때 시간대는 모두 대한민국으로 맞춰주면 된다.",[16,6898,6899],{},[513,6900],{"alt":515,"src":6901},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled17.png",[16,6903,6904],{},"그 후 데이터스트림을 만들어준다. 스트림 이름은 적당히 알아볼 수 있게 하고, 중요한 것은 “측정 ID” 이다. 이것이 데이터를 전송하는 데 사용하는 키가 된다.",[767,6906,6908],{"id":6907},"로컬-테스트용-데이터스트림-만들기","로컬 테스트용 데이터스트림 만들기",[16,6910,6911,6912,6915,6916,6919],{},"스트림 URL 에는 ",[27,6913,6914],{},"localhost"," 혹은 ",[27,6917,6918],{},"127.0.0.1"," 같은 루프백 주소는 입력할 수 없다. 그렇다고 테스트할 때마다 매번 Github에 배포하는 것도 번거로운 일이다. 적어도 1분에서 3분정도 걸리는데 그 시간이 누적되면 손해가 이만저만이 아니다.",[78,6921,6923],{"className":4002,"code":6922,"language":4004,"meta":83,"style":83},"##\n## Host Database\n#\n## localhost is used to configure the loopback interface\n## when the system is booting.  Do not change this entry.\n##\n127.0.0.1   localhost\n255.255.255.255 broadcasthost\n::1             localhost\n## Added by Docker Desktop\n## To allow the same kube context to work on the host and the container:\n127.0.0.1 kubernetes.docker.internal\n**127.0.0.1 blog.local**\n## End of section\n",[27,6924,6925,6930,6935,6940,6948,6988,6992,7000,7008,7015,7031,7076,7094,7109],{"__ignoreMap":83},[87,6926,6927],{"class":89,"line":90},[87,6928,6929],{"class":97},"##\n",[87,6931,6932],{"class":89,"line":101},[87,6933,6934],{"class":97},"## Host Database\n",[87,6936,6937],{"class":89,"line":117},[87,6938,6939],{"class":97},"#\n",[87,6941,6942,6945],{"class":89,"line":130},[87,6943,6944],{"class":97},"## localhost is used to configure the loopback ",[87,6946,6947],{"class":562},"interface\n",[87,6949,6950,6953,6956,6959,6962,6965,6968,6971,6974,6977,6980,6982,6985],{"class":89,"line":224},[87,6951,6952],{"class":97},"## ",[87,6954,6955],{"class":93},"when",[87,6957,6958],{"class":93}," the",[87,6960,6961],{"class":93}," system",[87,6963,6964],{"class":93}," is",[87,6966,6967],{"class":93}," booting",[87,6969,6970],{"class":97},".  ",[87,6972,6973],{"class":93},"Do",[87,6975,6976],{"class":93}," not",[87,6978,6979],{"class":93}," change",[87,6981,4243],{"class":93},[87,6983,6984],{"class":93}," entry",[87,6986,6987],{"class":97},".\n",[87,6989,6990],{"class":89,"line":246},[87,6991,6929],{"class":97},[87,6993,6994,6997],{"class":89,"line":256},[87,6995,6996],{"class":97},"127.0.0.1   ",[87,6998,6999],{"class":93},"localhost\n",[87,7001,7002,7005],{"class":89,"line":270},[87,7003,7004],{"class":97},"255.255.255.255 ",[87,7006,7007],{"class":93},"broadcasthost\n",[87,7009,7010,7013],{"class":89,"line":280},[87,7011,7012],{"class":97},"::1             ",[87,7014,6999],{"class":93},[87,7016,7017,7019,7022,7025,7028],{"class":89,"line":295},[87,7018,6952],{"class":97},[87,7020,7021],{"class":93},"Added",[87,7023,7024],{"class":93}," by",[87,7026,7027],{"class":93}," Docker",[87,7029,7030],{"class":93}," Desktop\n",[87,7032,7033,7035,7038,7041,7043,7046,7049,7052,7055,7058,7061,7063,7066,7069,7071,7074],{"class":89,"line":315},[87,7034,6952],{"class":97},[87,7036,7037],{"class":93},"To",[87,7039,7040],{"class":93}," allow",[87,7042,6958],{"class":93},[87,7044,7045],{"class":93}," same",[87,7047,7048],{"class":93}," kube",[87,7050,7051],{"class":93}," context",[87,7053,7054],{"class":93}," to",[87,7056,7057],{"class":93}," work",[87,7059,7060],{"class":93}," on",[87,7062,6958],{"class":93},[87,7064,7065],{"class":93}," host",[87,7067,7068],{"class":93}," and",[87,7070,6958],{"class":93},[87,7072,7073],{"class":93}," container",[87,7075,3459],{"class":97},[87,7077,7078,7081,7084,7086,7089,7091],{"class":89,"line":330},[87,7079,7080],{"class":97},"127.0.0.1 ",[87,7082,7083],{"class":93},"kubernetes",[87,7085,1231],{"class":97},[87,7087,7088],{"class":93},"docker",[87,7090,1231],{"class":97},[87,7092,7093],{"class":93},"internal\n",[87,7095,7096,7099,7102,7104,7107],{"class":89,"line":351},[87,7097,7098],{"class":97},"**127.0.0.1 ",[87,7100,7101],{"class":93},"blog",[87,7103,1231],{"class":97},[87,7105,7106],{"class":93},"local",[87,7108,4943],{"class":97},[87,7110,7111,7113,7116,7119],{"class":89,"line":360},[87,7112,6952],{"class":97},[87,7114,7115],{"class":93},"End",[87,7117,7118],{"class":93}," of",[87,7120,7121],{"class":93}," section\n",[16,7123,7124,7127,7128,7131],{},[27,7125,7126],{},"hosts"," 파일에 DNS 정보를 넣어서 가상의 URL을 만들어준 후에, 이 URL로 로컬 테스트를 진행하면 된다. 나는 단순하게 ",[27,7129,7130],{},"blog.local"," 이라고 정했다.",[16,7133,7134],{},[513,7135],{"alt":515,"src":7136},"blog\u002Fimg\u002Fmaking-blog-githubio-vue3-1\u002FUntitled18.png",[16,7138,7139],{},"나의 경우 실제 오픈한 블로그와 로컬 테스트 블로그 각각에서 잡히는 데이터를 분리하고 싶었다. 그래서 아예 속성 자체를 분리했다. 그래야 섞이지 않고 올바르게 데이터를 쌓을 수 있다.",[78,7141,7143],{"className":4002,"code":7142,"language":4004,"meta":83,"style":83},"\u002F\u002F vue.config.js\nmodule.exports = {\n    devServer: {\n        \u002F\u002F webpack4\n        disableHostCheck: true,\n        \u002F\u002F webpack5\n    \u002F\u002F allowedHosts: ['.host.com', 'host2.com'],\n    allowedHosts: 'all',\n  }\n}\n",[27,7144,7145,7149,7161,7166,7171,7180,7185,7190,7200,7204],{"__ignoreMap":83},[87,7146,7147],{"class":89,"line":90},[87,7148,3388],{"class":1339},[87,7150,7151,7153,7155,7157,7159],{"class":89,"line":101},[87,7152,3393],{"class":104},[87,7154,1231],{"class":97},[87,7156,3398],{"class":104},[87,7158,1362],{"class":562},[87,7160,98],{"class":97},[87,7162,7163],{"class":89,"line":117},[87,7164,7165],{"class":97},"    devServer: {\n",[87,7167,7168],{"class":89,"line":130},[87,7169,7170],{"class":1339},"        \u002F\u002F webpack4\n",[87,7172,7173,7176,7178],{"class":89,"line":224},[87,7174,7175],{"class":97},"        disableHostCheck: ",[87,7177,4139],{"class":104},[87,7179,3413],{"class":97},[87,7181,7182],{"class":89,"line":246},[87,7183,7184],{"class":1339},"        \u002F\u002F webpack5\n",[87,7186,7187],{"class":89,"line":256},[87,7188,7189],{"class":1339},"    \u002F\u002F allowedHosts: ['.host.com', 'host2.com'],\n",[87,7191,7192,7195,7198],{"class":89,"line":270},[87,7193,7194],{"class":97},"    allowedHosts: ",[87,7196,7197],{"class":165},"'all'",[87,7199,3413],{"class":97},[87,7201,7202],{"class":89,"line":280},[87,7203,1694],{"class":97},[87,7205,7206],{"class":89,"line":295},[87,7207,133],{"class":97},[16,7209,7210,7211,7214,7215,7218],{},"vue.config.js 에다가 추가로 설정이 필요하다. Webpack4 를 사용 중이라면 ",[27,7212,7213],{},"disableHostCheck"," 속성을, Webpack5 사용 중이라면 ",[27,7216,7217],{},"allowedHosts"," 를 사용한다.",[753,7220,7222],{"id":7221},"vue에서-ga4로-데이터-전송하기","Vue에서 GA4로 데이터 전송하기",[78,7224,7226],{"className":4002,"code":7225,"language":4004,"meta":83,"style":83},"npm install vue-gtag-next\n",[27,7227,7228],{"__ignoreMap":83},[87,7229,7230,7233,7235,7238,7240],{"class":89,"line":90},[87,7231,7232],{"class":97},"npm install vue",[87,7234,4835],{"class":562},[87,7236,7237],{"class":97},"gtag",[87,7239,4835],{"class":562},[87,7241,7242],{"class":97},"next\n",[16,7244,7245,7246,7249,7250,7252,7253,7256],{},"Vue에서는 ",[27,7247,7248],{},"vue-gtag","라는 라이브러리를 사용해서 간단하게 GA 태그를 심을 수 있다. 하지만 ",[27,7251,7248],{}," 는 Vue2 까지 지원하며, Vue3 에서는 ",[27,7254,7255],{},"vue-gtag-next"," 를 사용해야한다. 사용법도 조금은 다르다.",[78,7258,7260],{"className":4002,"code":7259,"language":4004,"meta":83,"style":83},"\u002F\u002F main.ts\nimport VueGTag from \"vue-gtag-next\";\n\nlet GAID = \"G-XXYYZZXXTT\";  \u002F\u002F dev\nif(process.env.NODE_ENV === \"production\") {\n    GAID = \"G-XXYYZZXXEE\";  \u002F\u002F prod\n}\n\nconst app = createApp(App)\n.use(store)\n.use(router)\n**.use(VueGTag, {\n    property: {\n        id: GAID, \u002F\u002F prod\n    }\n})**\n.mount('#app')\n",[27,7261,7262,7267,7281,7285,7303,7320,7335,7339,7343,7358,7368,7377,7389,7394,7406,7410,7417],{"__ignoreMap":83},[87,7263,7264],{"class":89,"line":90},[87,7265,7266],{"class":1339},"\u002F\u002F main.ts\n",[87,7268,7269,7271,7274,7276,7279],{"class":89,"line":101},[87,7270,2495],{"class":562},[87,7272,7273],{"class":97}," VueGTag ",[87,7275,2501],{"class":562},[87,7277,7278],{"class":165}," \"vue-gtag-next\"",[87,7280,114],{"class":97},[87,7282,7283],{"class":89,"line":117},[87,7284,1446],{"emptyLinePlaceholder":842},[87,7286,7287,7289,7292,7294,7297,7300],{"class":89,"line":130},[87,7288,1345],{"class":562},[87,7290,7291],{"class":104}," GAID",[87,7293,1362],{"class":562},[87,7295,7296],{"class":165}," \"G-XXYYZZXXTT\"",[87,7298,7299],{"class":97},";  ",[87,7301,7302],{"class":1339},"\u002F\u002F dev\n",[87,7304,7305,7308,7311,7313,7315,7318],{"class":89,"line":224},[87,7306,7307],{"class":562},"if",[87,7309,7310],{"class":97},"(process.env.",[87,7312,5810],{"class":104},[87,7314,5813],{"class":562},[87,7316,7317],{"class":165}," \"production\"",[87,7319,4409],{"class":97},[87,7321,7322,7325,7327,7330,7332],{"class":89,"line":246},[87,7323,7324],{"class":104},"    GAID",[87,7326,1362],{"class":562},[87,7328,7329],{"class":165}," \"G-XXYYZZXXEE\"",[87,7331,7299],{"class":97},[87,7333,7334],{"class":1339},"\u002F\u002F prod\n",[87,7336,7337],{"class":89,"line":256},[87,7338,133],{"class":97},[87,7340,7341],{"class":89,"line":270},[87,7342,1446],{"emptyLinePlaceholder":842},[87,7344,7345,7347,7350,7352,7355],{"class":89,"line":280},[87,7346,1964],{"class":562},[87,7348,7349],{"class":104}," app",[87,7351,1362],{"class":562},[87,7353,7354],{"class":93}," createApp",[87,7356,7357],{"class":97},"(App)\n",[87,7359,7360,7362,7365],{"class":89,"line":295},[87,7361,1231],{"class":97},[87,7363,7364],{"class":93},"use",[87,7366,7367],{"class":97},"(store)\n",[87,7369,7370,7372,7374],{"class":89,"line":315},[87,7371,1231],{"class":97},[87,7373,7364],{"class":93},[87,7375,7376],{"class":97},"(router)\n",[87,7378,7379,7382,7384,7386],{"class":89,"line":330},[87,7380,7381],{"class":562},"**",[87,7383,1231],{"class":97},[87,7385,7364],{"class":93},[87,7387,7388],{"class":97},"(VueGTag, {\n",[87,7390,7391],{"class":89,"line":351},[87,7392,7393],{"class":97},"    property: {\n",[87,7395,7396,7399,7402,7404],{"class":89,"line":360},[87,7397,7398],{"class":97},"        id: ",[87,7400,7401],{"class":104},"GAID",[87,7403,60],{"class":97},[87,7405,7334],{"class":1339},[87,7407,7408],{"class":89,"line":374},[87,7409,1689],{"class":97},[87,7411,7412,7415],{"class":89,"line":383},[87,7413,7414],{"class":97},"})",[87,7416,4943],{"class":562},[87,7418,7419,7421,7424,7426,7428],{"class":89,"line":398},[87,7420,1231],{"class":97},[87,7422,7423],{"class":93},"mount",[87,7425,791],{"class":97},[87,7427,5732],{"class":165},[87,7429,5501],{"class":97},[16,7431,7432,7433,7435],{},"우선 main.ts 에서 “측정 ID” 를 가지고 GA 초기화를 해야한다. 실서버와 로컬 환경의 키를 분리해서 할당하고는, Vue app의 ",[27,7434,7364],{}," 로 등록해주자.",[78,7437,7439],{"className":4002,"code":7438,"language":4004,"meta":83,"style":83},"\u002F\u002F router\u002Findex.ts\nimport { trackRouter } from 'vue-gtag-next'\nconst router = createRouter({\n    ...\n})\n\ntrackRouter(router);\n",[27,7440,7441,7446,7458,7472,7477,7481,7485],{"__ignoreMap":83},[87,7442,7443],{"class":89,"line":90},[87,7444,7445],{"class":1339},"\u002F\u002F router\u002Findex.ts\n",[87,7447,7448,7450,7453,7455],{"class":89,"line":101},[87,7449,2495],{"class":562},[87,7451,7452],{"class":97}," { trackRouter } ",[87,7454,2501],{"class":562},[87,7456,7457],{"class":165}," 'vue-gtag-next'\n",[87,7459,7460,7462,7465,7467,7470],{"class":89,"line":117},[87,7461,1964],{"class":562},[87,7463,7464],{"class":104}," router",[87,7466,1362],{"class":562},[87,7468,7469],{"class":93}," createRouter",[87,7471,5676],{"class":97},[87,7473,7474],{"class":89,"line":130},[87,7475,7476],{"class":562},"    ...\n",[87,7478,7479],{"class":89,"line":224},[87,7480,5107],{"class":97},[87,7482,7483],{"class":89,"line":246},[87,7484,1446],{"emptyLinePlaceholder":842},[87,7486,7487,7490],{"class":89,"line":256},[87,7488,7489],{"class":93},"trackRouter",[87,7491,7492],{"class":97},"(router);\n",[16,7494,7495,7497],{},[27,7496,7489],{}," 로 간단하게 Vue-Router 와 연동하여 라우팅 페이지 뷰를 자동으로 인식하고 데이터를 전송할 수 있다.",[825,7499,7500],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":83,"searchDepth":101,"depth":101,"links":7502},[7503,7504,7509,7510,7511,7512,7515,7516],{"id":3293,"depth":101,"text":3294},{"id":3317,"depth":101,"text":3318,"children":7505},[7506,7507,7508],{"id":3346,"depth":117,"text":3346},{"id":3362,"depth":117,"text":3337},{"id":3431,"depth":117,"text":3432},{"id":3792,"depth":101,"text":3793},{"id":4813,"depth":101,"text":4814},{"id":5113,"depth":101,"text":5114},{"id":5864,"depth":101,"text":5865,"children":7513},[7514],{"id":6172,"depth":117,"text":6173},{"id":6321,"depth":101,"text":6322},{"id":6873,"depth":101,"text":6874,"children":7517},[7518,7519],{"id":6880,"depth":117,"text":6881},{"id":7221,"depth":117,"text":7222},"2022-06-24","Github.io 와 Vue3 를 활용해서 블로그를 시작. SPA를 prerendering 하여 개별 페이지를 생성하고, 메타태그를 추가. 구글 검색 엔진에 등록할 사이트맵 자동 생성. 마크다운을 html 로 변환. Google Analytics 도입으로 방문자 수 체크.","making-blog-githubio-vue3-1",{},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1",{"title":3272,"description":7521},{"loc":7524},"blog\u002Fmaking-blog-githubio-vue3-1",[2858,7529,7530,7531,7532,7533,7534],"Vue3","Github","Blog","Markdown","SEO","GoogleAnalytics","2022-06-25","long","dX-CbtjMBFSSLAsTRq1Nx91EZts5FYplO23QCxLkbm8",{"id":7539,"title":7540,"body":7541,"created":7662,"description":7663,"extension":839,"filename":7664,"meta":7665,"navigation":842,"path":7666,"seo":7667,"sitemap":7668,"stem":7669,"subject":7670,"tags":7671,"updated":7662,"volume":1859,"__hash__":7676},"blog\u002Fblog\u002Fnpm-rollup-os.md","npm 의존성 설치 문제 애플 실리콘에서의 Rollup 패키지 (npm ERR notsup Unsupported platform for rollup-darwin-arm64)",{"type":8,"value":7542,"toc":7660},[7543,7549,7569,7600,7607,7633,7651,7657],[1108,7544],{"description":7545,"favicon":1316,"host":1313,"image":7546,"title":7547,"url":7548},"Rollup Version 4.8.0 Operating System (or Browser) MacOS M2 Pro Node Version (if applicable) 20.10.0 Link To Reproduction Nuxt 3, npm i Expected Behaviour module should be installed as expected. Ac...","https:\u002F\u002Fopengraph.githubassets.com\u002F4f1a9d9ff77dc018315c5faae0716be8d6e371e7a8f96f7acf8f9e0a847b5069\u002Frollup\u002Frollup\u002Fissues\u002F5295","Unsupported platform · Issue #5295 · rollup\u002Frollup","https:\u002F\u002Fgithub.com\u002Frollup\u002Frollup\u002Fissues\u002F5295#issuecomment-1942985686",[1718,7550,7551,7554,7557,7560,7563,7566],{},[16,7552,7553],{},"npm ERR! code EBADPLATFORM",[16,7555,7556],{},"npm ERR! notsup Unsupported platform for @rollup\u002Frollup-darwin-arm64@4.30.1: wanted {\"os\":\"darwin\",\"cpu\":\"arm64\"} (current: {\"os\":\"win32\",\"cpu\":\"x64\"})",[16,7558,7559],{},"npm ERR! notsup Valid os:   darwin",[16,7561,7562],{},"npm ERR! notsup Actual os:  win32",[16,7564,7565],{},"npm ERR! notsup Valid cpu:  arm64",[16,7567,7568],{},"npm ERR! notsup Actual cpu: x64",[78,7570,7574],{"className":7571,"code":7572,"language":7573,"meta":83,"style":83},"language-json shiki shiki-themes github-light github-dark","\"dependencies\": {\n    \"@rollup\u002Frollup-darwin-arm64\": \"^4.30.1\",\n}\n","json",[27,7575,7576,7584,7596],{"__ignoreMap":83},[87,7577,7578,7581],{"class":89,"line":90},[87,7579,7580],{"class":165},"\"dependencies\"",[87,7582,7583],{"class":97},": {\n",[87,7585,7586,7589,7591,7594],{"class":89,"line":101},[87,7587,7588],{"class":104},"    \"@rollup\u002Frollup-darwin-arm64\"",[87,7590,108],{"class":97},[87,7592,7593],{"class":165},"\"^4.30.1\"",[87,7595,3413],{"class":97},[87,7597,7598],{"class":89,"line":117},[87,7599,133],{"class":97},[16,7601,7602,7603,7606],{},"package.json 파일에서 dependencies 에 ",[27,7604,7605],{},"@rollup\u002Frollup-darwin-arm64"," 를 종속성으로 등록해두었지만, 실행하는 환경이 Windows 혹은 Linux 등 애플실레콘이 아닐 때 일 때 설치할 수 없기 때문에 발생한다. dependencies 나 devDependencies 의 경우 설치에 실패하면 무조건 중단되며 실패로 끝난다.",[78,7608,7610],{"className":7571,"code":7609,"language":7573,"meta":83,"style":83},"\"optionalDependencies\": {\n    \"@rollup\u002Frollup-darwin-arm64\": \"^4.30.1\"\n},\n",[27,7611,7612,7619,7628],{"__ignoreMap":83},[87,7613,7614,7617],{"class":89,"line":90},[87,7615,7616],{"class":165},"\"optionalDependencies\"",[87,7618,7583],{"class":97},[87,7620,7621,7623,7625],{"class":89,"line":101},[87,7622,7588],{"class":104},[87,7624,108],{"class":97},[87,7626,7627],{"class":165},"\"^4.30.1\"\n",[87,7629,7630],{"class":89,"line":117},[87,7631,7632],{"class":97},"},\n",[16,7634,7635,7638,7639,7642,7643,7646,7647,7650],{},[27,7636,7637],{},"optionalDependencies","에 등록한 의존성은 ",[788,7640,7641],{},"설치 실패해도 전체 설치 과정에 영향을 주지 않는"," 의존성이다. 즉, ",[788,7644,7645],{},"설치 가능하면 설치하고",", 불가능하면 ",[788,7648,7649],{},"무시","한다.",[16,7652,7653,7654,7656],{},"플랫폼 환경에 따라서 설치해야하는 의존성이 있는 경우, 특히 이 파트를 사용한다. ",[27,7655,7605],{},"는 애플실리콘에서만 설치되는 패키지이다.",[825,7658,7659],{},"html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}",{"title":83,"searchDepth":101,"depth":101,"links":7661},[],"2025-01-20","npm의 의존성 설치 실패 문제. @rollup\u002Frollup-darwin-arm64 패키지가 애플 실리콘에서만 설치 가능하다. Windows 및 Linux 환경에서의 설치 불가 원인. optionalDependencies의 사용 방법.","npm-rollup-os",{},"\u002Fblog\u002Fnpm-rollup-os",{"title":7540,"description":7663},{"loc":7666},"blog\u002Fnpm-rollup-os","Node",[7672,1257,7673,7674,7675],"rollup","dependencies","node","npm","9ngtBXz8zP9KDaoY46LJmbji5nAVozk7HlaIVwsvkZ4",{"id":7678,"title":7679,"body":7680,"created":7662,"description":8460,"extension":839,"filename":8461,"meta":8462,"navigation":842,"path":8463,"seo":8464,"sitemap":8465,"stem":8466,"subject":8243,"tags":8467,"updated":7662,"volume":849,"__hash__":8470},"blog\u002Fblog\u002Fnuxt-pinia-get-active-pinia.md","Nuxt Pinia 시작하기 + getActivePinia 에러",{"type":8,"value":7681,"toc":8450},[7682,7685,7689,7716,7720,7751,7757,7761,7770,7776,7921,7925,7932,8047,8051,8057,8108,8111,8115,8135,8138,8152,8156,8188,8191,8206,8209,8289,8298,8301,8305,8390,8393,8444,8447],[16,7683,7684],{},"Nuxt 프로젝트에서 Pinia를 사용하는 방법은 Vue3와 다르다.",[45,7686,7688],{"id":7687},"pinia-설치","Pinia 설치",[78,7690,7694],{"className":7691,"code":7692,"language":7693,"meta":83,"style":83},"language-bash shiki shiki-themes github-light github-dark","npm install @pinia\u002Fnuxt\nyarn add @pinia\u002Fnuxt\n","bash",[27,7695,7696,7706],{"__ignoreMap":83},[87,7697,7698,7700,7703],{"class":89,"line":90},[87,7699,7675],{"class":93},[87,7701,7702],{"class":165}," install",[87,7704,7705],{"class":165}," @pinia\u002Fnuxt\n",[87,7707,7708,7711,7714],{"class":89,"line":101},[87,7709,7710],{"class":93},"yarn",[87,7712,7713],{"class":165}," add",[87,7715,7705],{"class":165},[45,7717,7719],{"id":7718},"nuxt-설정-파일에-pinia-추가","Nuxt 설정 파일에 Pinia 추가",[78,7721,7723],{"className":1150,"code":7722,"language":1152,"meta":83,"style":83},"export default defineNuxtConfig({\n  modules: ['@pinia\u002Fnuxt'],\n})\n",[27,7724,7725,7736,7747],{"__ignoreMap":83},[87,7726,7727,7729,7731,7734],{"class":89,"line":90},[87,7728,2570],{"class":562},[87,7730,4575],{"class":562},[87,7732,7733],{"class":93}," defineNuxtConfig",[87,7735,5676],{"class":97},[87,7737,7738,7741,7744],{"class":89,"line":101},[87,7739,7740],{"class":97},"  modules: [",[87,7742,7743],{"class":165},"'@pinia\u002Fnuxt'",[87,7745,7746],{"class":97},"],\n",[87,7748,7749],{"class":89,"line":117},[87,7750,5107],{"class":97},[16,7752,7753,7756],{},[27,7754,7755],{},"nuxt.config.ts"," 파일에 모듈을 추가해야 한다.",[45,7758,7760],{"id":7759},"스토어-생성","스토어 생성",[16,7762,7763,7766,7767,7769],{},[27,7764,7765],{},"stores"," 폴더를 만들고 그 안에 스토어 파일을 작성한다. Nuxt에서 ",[27,7768,7765],{}," 폴더를 자동으로 인식해서 따로 import할 필요 없다.",[16,7771,7772,7773],{},"예시: ",[27,7774,7775],{},"stores\u002Fcounter.ts",[78,7777,7779],{"className":1150,"code":7778,"language":1152,"meta":83,"style":83},"import { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useCounterStore = defineStore('counter', () => {\n  const count = ref(0)\n  const doubleCount = computed(() => count.value * 2)\n\n  function increment() {\n    count.value++\n  }\n\n  return { count, doubleCount, increment }\n})\n",[27,7780,7781,7793,7805,7809,7836,7854,7880,7884,7894,7902,7906,7910,7917],{"__ignoreMap":83},[87,7782,7783,7785,7788,7790],{"class":89,"line":90},[87,7784,2495],{"class":562},[87,7786,7787],{"class":97}," { defineStore } ",[87,7789,2501],{"class":562},[87,7791,7792],{"class":165}," 'pinia'\n",[87,7794,7795,7797,7800,7802],{"class":89,"line":101},[87,7796,2495],{"class":562},[87,7798,7799],{"class":97}," { ref } ",[87,7801,2501],{"class":562},[87,7803,7804],{"class":165}," 'vue'\n",[87,7806,7807],{"class":89,"line":117},[87,7808,1446],{"emptyLinePlaceholder":842},[87,7810,7811,7813,7816,7819,7821,7824,7826,7829,7832,7834],{"class":89,"line":130},[87,7812,2570],{"class":562},[87,7814,7815],{"class":562}," const",[87,7817,7818],{"class":104}," useCounterStore",[87,7820,1362],{"class":562},[87,7822,7823],{"class":93}," defineStore",[87,7825,791],{"class":97},[87,7827,7828],{"class":165},"'counter'",[87,7830,7831],{"class":97},", () ",[87,7833,1175],{"class":562},[87,7835,98],{"class":97},[87,7837,7838,7840,7843,7845,7848,7850,7852],{"class":89,"line":224},[87,7839,2637],{"class":562},[87,7841,7842],{"class":104}," count",[87,7844,1362],{"class":562},[87,7846,7847],{"class":93}," ref",[87,7849,791],{"class":97},[87,7851,1574],{"class":104},[87,7853,5501],{"class":97},[87,7855,7856,7858,7861,7863,7865,7867,7869,7872,7875,7878],{"class":89,"line":246},[87,7857,2637],{"class":562},[87,7859,7860],{"class":104}," doubleCount",[87,7862,1362],{"class":562},[87,7864,4776],{"class":93},[87,7866,2084],{"class":97},[87,7868,1175],{"class":562},[87,7870,7871],{"class":97}," count.value ",[87,7873,7874],{"class":562},"*",[87,7876,7877],{"class":104}," 2",[87,7879,5501],{"class":97},[87,7881,7882],{"class":89,"line":256},[87,7883,1446],{"emptyLinePlaceholder":842},[87,7885,7886,7889,7892],{"class":89,"line":270},[87,7887,7888],{"class":562},"  function",[87,7890,7891],{"class":93}," increment",[87,7893,2003],{"class":97},[87,7895,7896,7899],{"class":89,"line":280},[87,7897,7898],{"class":97},"    count.value",[87,7900,7901],{"class":562},"++\n",[87,7903,7904],{"class":89,"line":295},[87,7905,1694],{"class":97},[87,7907,7908],{"class":89,"line":315},[87,7909,1446],{"emptyLinePlaceholder":842},[87,7911,7912,7914],{"class":89,"line":330},[87,7913,2691],{"class":562},[87,7915,7916],{"class":97}," { count, doubleCount, increment }\n",[87,7918,7919],{"class":89,"line":351},[87,7920,5107],{"class":97},[45,7922,7924],{"id":7923},"컴포넌트에서-사용하기","컴포넌트에서 사용하기",[16,7926,7927,7928,7931],{},"컴포넌트에서 ",[27,7929,7930],{},"useCounterStore","를 불러와서 사용하면 된다.",[78,7933,7935],{"className":1930,"code":7934,"language":1932,"meta":83,"style":83},"\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cp>Count: {{ counter.count }}\u003C\u002Fp>\n    \u003Cbutton @click=\"counter.increment\">증가\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst counter = useCounterStore()\n\u003C\u002Fscript>\n",[27,7936,7937,7945,7953,7966,7988,7997,8005,8009,8025,8039],{"__ignoreMap":83},[87,7938,7939,7941,7943],{"class":89,"line":90},[87,7940,152],{"class":97},[87,7942,2114],{"class":155},[87,7944,169],{"class":97},[87,7946,7947,7949,7951],{"class":89,"line":101},[87,7948,2121],{"class":97},[87,7950,156],{"class":155},[87,7952,169],{"class":97},[87,7954,7955,7957,7959,7962,7964],{"class":89,"line":117},[87,7956,174],{"class":97},[87,7958,16],{"class":155},[87,7960,7961],{"class":97},">Count: {{ counter.count }}\u003C\u002F",[87,7963,16],{"class":155},[87,7965,169],{"class":97},[87,7967,7968,7970,7973,7976,7978,7981,7984,7986],{"class":89,"line":130},[87,7969,174],{"class":97},[87,7971,7972],{"class":155},"button",[87,7974,7975],{"class":93}," @click",[87,7977,162],{"class":97},[87,7979,7980],{"class":165},"\"counter.increment\"",[87,7982,7983],{"class":97},">증가\u003C\u002F",[87,7985,7972],{"class":155},[87,7987,169],{"class":97},[87,7989,7990,7993,7995],{"class":89,"line":224},[87,7991,7992],{"class":97},"  \u003C\u002F",[87,7994,156],{"class":155},[87,7996,169],{"class":97},[87,7998,7999,8001,8003],{"class":89,"line":246},[87,8000,489],{"class":97},[87,8002,2114],{"class":155},[87,8004,169],{"class":97},[87,8006,8007],{"class":89,"line":256},[87,8008,1446],{"emptyLinePlaceholder":842},[87,8010,8011,8013,8015,8017,8019,8021,8023],{"class":89,"line":270},[87,8012,152],{"class":97},[87,8014,1941],{"class":155},[87,8016,1944],{"class":93},[87,8018,1947],{"class":93},[87,8020,162],{"class":97},[87,8022,1952],{"class":165},[87,8024,169],{"class":97},[87,8026,8027,8029,8032,8034,8036],{"class":89,"line":280},[87,8028,1964],{"class":562},[87,8030,8031],{"class":104}," counter",[87,8033,1362],{"class":562},[87,8035,7818],{"class":93},[87,8037,8038],{"class":97},"()\n",[87,8040,8041,8043,8045],{"class":89,"line":295},[87,8042,489],{"class":97},[87,8044,1941],{"class":155},[87,8046,169],{"class":97},[45,8048,8050],{"id":8049},"선택-서버-사이드-렌더링ssr-설정","(선택) 서버 사이드 렌더링(SSR) 설정",[16,8052,8053,8054,8056],{},"Pinia는 SSR도 지원한다. 특별한 설정 없이도 기본적으로 SSR에서 잘 작동하지만, 커스텀이 필요하면 ",[27,8055,7755],{},"에 아래와 같이 옵션을 추가할 수 있다.",[78,8058,8060],{"className":1150,"code":8059,"language":1152,"meta":83,"style":83},"export default defineNuxtConfig({\n  modules: ['@pinia\u002Fnuxt'],\n  pinia: {\n    autoImports: ['defineStore', 'acceptHMRUpdate'],\n  },\n})\n",[27,8061,8062,8072,8080,8085,8100,8104],{"__ignoreMap":83},[87,8063,8064,8066,8068,8070],{"class":89,"line":90},[87,8065,2570],{"class":562},[87,8067,4575],{"class":562},[87,8069,7733],{"class":93},[87,8071,5676],{"class":97},[87,8073,8074,8076,8078],{"class":89,"line":101},[87,8075,7740],{"class":97},[87,8077,7743],{"class":165},[87,8079,7746],{"class":97},[87,8081,8082],{"class":89,"line":117},[87,8083,8084],{"class":97},"  pinia: {\n",[87,8086,8087,8090,8093,8095,8098],{"class":89,"line":130},[87,8088,8089],{"class":97},"    autoImports: [",[87,8091,8092],{"class":165},"'defineStore'",[87,8094,60],{"class":97},[87,8096,8097],{"class":165},"'acceptHMRUpdate'",[87,8099,7746],{"class":97},[87,8101,8102],{"class":89,"line":224},[87,8103,3907],{"class":97},[87,8105,8106],{"class":89,"line":246},[87,8107,5107],{"class":97},[16,8109,8110],{},"이렇게 하면 Pinia를 Nuxt 프로젝트에서 손쉽게 사용할 수 있다.",[11,8112,8114],{"id":8113},"getactivepinia-was-called-but-there-was-no-active-pinia-are-you-trying-to-use-a-store-before-calling-appusepinia","getActivePinia()\" was called but there was no active Pinia. Are you trying to use a store before calling \"app.use(pinia)\"?",[1718,8116,8117,8124,8132],{},[16,8118,8119,8120,8123],{},"[nuxt] error caught during app initialization Error: ",[87,8121,8122],{},"🍍",": \"getActivePinia()\" was called but there was no active Pinia. Are you trying to use a store before calling \"app.use(pinia)\"?",[16,8125,8126,8127,8131],{},"See ",[2943,8128,8129],{"href":8129,"rel":8130},"https:\u002F\u002Fpinia.vuejs.org\u002Fcore-concepts\u002Foutside-component-usage.html",[2947]," for help.",[16,8133,8134],{},"This will fail in production.\nat useStore (pinia.js?v=859d6957:1322:13)",[16,8136,8137],{},"다 설정하고 보니 위 에러가 발생했다.",[2953,8139,8140,8143,8149],{},[23,8141,8142],{},"모듈 누락 확인",[23,8144,8145,8148],{},[27,8146,8147],{},"@pinia\u002Fnuxt"," 추가했는지 확인",[23,8150,8151],{},"비동기 로직 이후 스토어 접근 ㄴㄴ",[753,8153,8155],{"id":8154},"캐시-및-라이브러리-다시-설치","캐시 및 라이브러리 다시 설치",[78,8157,8161],{"className":8158,"code":8159,"language":8160,"meta":83,"style":83},"language-sh shiki shiki-themes github-light github-dark","rm -rf node_modules\u002F.cache\nrm -rf .nuxt\nrm -rf node_modules\nnpm install  # 또는 yarn install\nnpm run dev\n","sh",[27,8162,8163,8168,8173,8178,8183],{"__ignoreMap":83},[87,8164,8165],{"class":89,"line":90},[87,8166,8167],{},"rm -rf node_modules\u002F.cache\n",[87,8169,8170],{"class":89,"line":101},[87,8171,8172],{},"rm -rf .nuxt\n",[87,8174,8175],{"class":89,"line":117},[87,8176,8177],{},"rm -rf node_modules\n",[87,8179,8180],{"class":89,"line":130},[87,8181,8182],{},"npm install  # 또는 yarn install\n",[87,8184,8185],{"class":89,"line":224},[87,8186,8187],{},"npm run dev\n",[16,8189,8190],{},"잔류 캐시나 이전버전 라이브러리 등의 충돌이 있을 수 있으니!",[1718,8192,8193,8198],{},[16,8194,8195,8197],{},[87,8196,1724],{},"\nWARN  Module pinia is disabled due to incompatibility issues:                      오후 9:18:14",[20,8199,8200],{},[23,8201,8202,8205],{},[87,8203,8204],{},"nuxt"," Nuxt version ^2.0.0 || >=3.13.0 is required but currently using 3.12.1",[16,8207,8208],{},"버전도 확인해보자. 호환이 안되는 라이브러리일 수 있다. 나의 경우 nuxt 는 3.12.1 버전인데, pinia 2.3.x 버전을 사용하려면 3.13.0 이상의 버전을 요한다고 경고한다.",[808,8210,8211,8233],{},[8212,8213,8214],"thead",{},[8215,8216,8217,8223,8228],"tr",{},[8218,8219,8220],"th",{},[788,8221,8222],{},"라이브러리",[8218,8224,8225],{},[788,8226,8227],{},"현재 버전",[8218,8229,8230],{},[788,8231,8232],{},"권장 버전",[8234,8235,8236,8255,8273],"tbody",{},[8215,8237,8238,8244,8249],{},[8239,8240,8241],"td",{},[788,8242,8243],{},"Nuxt",[8239,8245,8246],{},[27,8247,8248],{},"3.12.1",[8239,8250,8251,8254],{},[27,8252,8253],{},"3.13.0"," 이상",[8215,8256,8257,8262,8267],{},[8239,8258,8259],{},[788,8260,8261],{},"Pinia",[8239,8263,8264],{},[27,8265,8266],{},"^2.3.0",[8239,8268,8269,8272],{},[27,8270,8271],{},"2.3.x"," (OK)",[8215,8274,8275,8279,8284],{},[8239,8276,8277],{},[788,8278,8147],{},[8239,8280,8281],{},[27,8282,8283],{},"^0.9.0",[8239,8285,8286,8272],{},[27,8287,8288],{},"0.9.x",[78,8290,8292],{"className":8158,"code":8291,"language":8160,"meta":83,"style":83},"npm install nuxt@latest\n",[27,8293,8294],{"__ignoreMap":83},[87,8295,8296],{"class":89,"line":90},[87,8297,8291],{},[16,8299,8300],{},"nuxt 버전을 최신버전으로 업데이트 한다. 그럼 아래 pinia 를 수동등록하지 않아도 위 방법대로 잘 동작한다.",[753,8302,8304],{"id":8303},"pinia-사용을-수동으로-등록","Pinia 사용을 수동으로 등록",[78,8306,8308],{"className":1150,"code":8307,"language":1152,"meta":83,"style":83},"\u002F\u002F plugins\u002Fpinia.ts\nimport { defineNuxtPlugin } from '#app'\nimport { createPinia } from 'pinia'\n\nexport default defineNuxtPlugin((nuxtApp) => {\n  const pinia = createPinia()\n  nuxtApp.vueApp.use(pinia)\n})\n",[27,8309,8310,8315,8327,8338,8342,8362,8376,8386],{"__ignoreMap":83},[87,8311,8312],{"class":89,"line":90},[87,8313,8314],{"class":1339},"\u002F\u002F plugins\u002Fpinia.ts\n",[87,8316,8317,8319,8322,8324],{"class":89,"line":101},[87,8318,2495],{"class":562},[87,8320,8321],{"class":97}," { defineNuxtPlugin } ",[87,8323,2501],{"class":562},[87,8325,8326],{"class":165}," '#app'\n",[87,8328,8329,8331,8334,8336],{"class":89,"line":117},[87,8330,2495],{"class":562},[87,8332,8333],{"class":97}," { createPinia } ",[87,8335,2501],{"class":562},[87,8337,7792],{"class":165},[87,8339,8340],{"class":89,"line":130},[87,8341,1446],{"emptyLinePlaceholder":842},[87,8343,8344,8346,8348,8351,8353,8356,8358,8360],{"class":89,"line":224},[87,8345,2570],{"class":562},[87,8347,4575],{"class":562},[87,8349,8350],{"class":93}," defineNuxtPlugin",[87,8352,1165],{"class":97},[87,8354,8355],{"class":1168},"nuxtApp",[87,8357,1172],{"class":97},[87,8359,1175],{"class":562},[87,8361,98],{"class":97},[87,8363,8364,8366,8369,8371,8374],{"class":89,"line":246},[87,8365,2637],{"class":562},[87,8367,8368],{"class":104}," pinia",[87,8370,1362],{"class":562},[87,8372,8373],{"class":93}," createPinia",[87,8375,8038],{"class":97},[87,8377,8378,8381,8383],{"class":89,"line":256},[87,8379,8380],{"class":97},"  nuxtApp.vueApp.",[87,8382,7364],{"class":93},[87,8384,8385],{"class":97},"(pinia)\n",[87,8387,8388],{"class":89,"line":270},[87,8389,5107],{"class":97},[16,8391,8392],{},"결국 이 방법으로 해결했다. nuxt 를",[78,8394,8396],{"className":7571,"code":8395,"language":7573,"meta":83,"style":83},"\"dependencies\": {\n    \"@pinia\u002Fnuxt\": \"^0.9.0\",\n    \"nuxt\": \"^3.12.1\",\n    \"pinia\": \"^2.3.0\",\n},\n",[27,8397,8398,8404,8416,8428,8440],{"__ignoreMap":83},[87,8399,8400,8402],{"class":89,"line":90},[87,8401,7580],{"class":165},[87,8403,7583],{"class":97},[87,8405,8406,8409,8411,8414],{"class":89,"line":101},[87,8407,8408],{"class":104},"    \"@pinia\u002Fnuxt\"",[87,8410,108],{"class":97},[87,8412,8413],{"class":165},"\"^0.9.0\"",[87,8415,3413],{"class":97},[87,8417,8418,8421,8423,8426],{"class":89,"line":117},[87,8419,8420],{"class":104},"    \"nuxt\"",[87,8422,108],{"class":97},[87,8424,8425],{"class":165},"\"^3.12.1\"",[87,8427,3413],{"class":97},[87,8429,8430,8433,8435,8438],{"class":89,"line":130},[87,8431,8432],{"class":104},"    \"pinia\"",[87,8434,108],{"class":97},[87,8436,8437],{"class":165},"\"^2.3.0\"",[87,8439,3413],{"class":97},[87,8441,8442],{"class":89,"line":224},[87,8443,7632],{"class":97},[16,8445,8446],{},"이 버전들의 조합으로는 수동으로 등록하는 것이 답이었다",[825,8448,8449],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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":83,"searchDepth":101,"depth":101,"links":8451},[8452,8453,8454,8455,8456],{"id":7687,"depth":101,"text":7688},{"id":7718,"depth":101,"text":7719},{"id":7759,"depth":101,"text":7760},{"id":7923,"depth":101,"text":7924},{"id":8049,"depth":101,"text":8050,"children":8457},[8458,8459],{"id":8154,"depth":117,"text":8155},{"id":8303,"depth":117,"text":8304},"Nuxt 프로젝트에서 Pinia를 설정하고 사용하는 방법을 안내. Pinia 설치, Nuxt 설정 파일에 추가하는 방법, 스토어 생성을 포함하여 컴포넌트에서의 사용 예시. 서버 사이드 렌더링(SSR) 설정 및 일반적인 오류 해결 방법.","nuxt-pinia-get-active-pinia",{},"\u002Fblog\u002Fnuxt-pinia-get-active-pinia",{"title":7679,"description":8460},{"loc":8463},"blog\u002Fnuxt-pinia-get-active-pinia",[8204,8468,8469],"vue3","vue3\u002Fpinia","eraPrhs82MmeKcDQnpNr_wFEaLd2SckD0DgEE8FTXeU",{"id":8472,"title":8473,"body":8474,"created":8593,"description":8594,"extension":839,"filename":8595,"meta":8596,"navigation":842,"path":8597,"seo":8598,"sitemap":8599,"stem":8600,"subject":2992,"tags":8601,"updated":8593,"volume":1859,"__hash__":8604},"blog\u002Fblog\u002Fpostman-fcm-test.md","Postman 으로 FCM 알림 테스트하기",{"type":8,"value":8475,"toc":8591},[8476,8482,8485,8490,8493,8501,8511,8585,8588],[78,8477,8480],{"className":8478,"code":8479,"language":1095},[1093],"POST https:\u002F\u002Ffcm.googleapis.com\u002Ffcm\u002Fsend\n",[27,8481,8479],{"__ignoreMap":83},[16,8483,8484],{},"위 경로로 메세지를 보낸다.",[16,8486,8487],{},[513,8488],{"alt":515,"src":8489},"blog\u002Fimg\u002Fpostman-fcm-test\u002Fimg.png",[16,8491,8492],{},"헤더 데이터",[20,8494,8495,8498],{},[23,8496,8497],{},"Authorization : key=AAAXXXXXX",[23,8499,8500],{},"Content-Type : application\u002Fjson",[16,8502,8503,8506,8507,8510],{},[27,8504,8505],{},"Authorization"," 은 ",[27,8508,8509],{},"key=서버키"," 형태로 넣는다.",[78,8512,8514],{"className":7571,"code":8513,"language":7573,"meta":83,"style":83},"{\n    \"to\":\"FCMToken\",\n    \"data\" : {\n      \"title\":\"제목\",\n      \"body\":\"내용\",\n      \"otherKey\":\"asdf\",\n    }\n}\n",[27,8515,8516,8521,8533,8541,8553,8565,8577,8581],{"__ignoreMap":83},[87,8517,8518],{"class":89,"line":90},[87,8519,8520],{"class":97},"{\n",[87,8522,8523,8526,8528,8531],{"class":89,"line":101},[87,8524,8525],{"class":104},"    \"to\"",[87,8527,1204],{"class":97},[87,8529,8530],{"class":165},"\"FCMToken\"",[87,8532,3413],{"class":97},[87,8534,8535,8538],{"class":89,"line":117},[87,8536,8537],{"class":104},"    \"data\"",[87,8539,8540],{"class":97}," : {\n",[87,8542,8543,8546,8548,8551],{"class":89,"line":130},[87,8544,8545],{"class":104},"      \"title\"",[87,8547,1204],{"class":97},[87,8549,8550],{"class":165},"\"제목\"",[87,8552,3413],{"class":97},[87,8554,8555,8558,8560,8563],{"class":89,"line":224},[87,8556,8557],{"class":104},"      \"body\"",[87,8559,1204],{"class":97},[87,8561,8562],{"class":165},"\"내용\"",[87,8564,3413],{"class":97},[87,8566,8567,8570,8572,8575],{"class":89,"line":246},[87,8568,8569],{"class":104},"      \"otherKey\"",[87,8571,1204],{"class":97},[87,8573,8574],{"class":165},"\"asdf\"",[87,8576,3413],{"class":97},[87,8578,8579],{"class":89,"line":256},[87,8580,1689],{"class":97},[87,8582,8583],{"class":89,"line":270},[87,8584,133],{"class":97},[16,8586,8587],{},"body 는 정해진 데이터대로 넣는다.",[825,8589,8590],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}",{"title":83,"searchDepth":101,"depth":101,"links":8592},[],"2023-09-16","FCM 알림을 테스트하는 간단한 방법. Postman 을 비롯한 curl 기반 HTTP 요청 프로그램으로 테스트하기.","postman-fcm-test",{},"\u002Fblog\u002Fpostman-fcm-test",{"title":8473,"description":8594},{"loc":8597},"blog\u002Fpostman-fcm-test",[2992,8602,8603],"FCM","Postman","bUS8C4dVgAcuE6AwbNAJLrjetkBdwE8BgsJSi952544",{"id":8606,"title":8607,"body":8608,"created":9303,"description":9304,"extension":839,"filename":9305,"meta":9306,"navigation":842,"path":9307,"seo":9308,"sitemap":9309,"stem":9310,"subject":847,"tags":9311,"updated":9303,"volume":1859,"__hash__":9312},"blog\u002Fblog\u002Ftailwindcss-color-preset-with-theme.md","tailwindcss color 프리셋 적용하기",{"type":8,"value":8609,"toc":9298},[8610,8613,8655,8658,8662,8668,8711,8718,8722,8725,8728,8734,8779,8803,8813,8818,8821,8827,8830,8925,8940,8964,8973,9017,9028,9178,9195,9199,9204,9286,9295],[16,8611,8612],{},"tailwindcss 에서 미리 기 정의된 color 프리셋을 제공한다. font, background 등등. 다만 기존에 사용하던 색상들을 tailwindcss 체계에 녹이고 싶다.",[808,8614,8615,8629],{},[8212,8616,8617],{},[8215,8618,8619,8624],{},[8218,8620,8621],{},[27,8622,8623],{},"text-black",[8218,8625,8626],{},[27,8627,8628],{},"color: var(--color-black); \u002F* #000 *\u002F",[8234,8630,8631,8643],{},[8215,8632,8633,8638],{},[8239,8634,8635],{},[27,8636,8637],{},"text-white",[8239,8639,8640],{},[27,8641,8642],{},"color: var(--color-white); \u002F* #fff *\u002F",[8215,8644,8645,8650],{},[8239,8646,8647],{},[27,8648,8649],{},"text-red-50",[8239,8651,8652],{},[27,8653,8654],{},"color: var(--color-red-50); \u002F* oklch(0.971 0.013 17.38) *\u002F",[16,8656,8657],{},"비슷한 형태로, 우리의 color 프리셋을 사용해보자.",[45,8659,8661],{"id":8660},"font-color-preset-custom","font color preset custom",[1108,8663],{"host":3146,"title":8664,"url":8665,"description":8666,"image":8667,"favicon":3145},"color - Typography","https:\u002F\u002Ftailwindcss.com\u002Fdocs\u002Fcolor#using-a-custom-value","Utilities for controlling the text color of an element.","https:\u002F\u002Ftailwindcss.com\u002Fapi\u002Fog?path=\u002Fdocs\u002Fcolor",[78,8669,8671],{"className":143,"code":8670,"language":145,"meta":83,"style":83},"\u003Cp class=\"text-[#50d71e] ...\">\u003C\u002Fp>\n\u003Cp class=\"text-(--my-color) ...\">\u003C\u002Fp>\n",[27,8672,8673,8692],{"__ignoreMap":83},[87,8674,8675,8677,8679,8681,8683,8686,8688,8690],{"class":89,"line":90},[87,8676,152],{"class":97},[87,8678,16],{"class":155},[87,8680,159],{"class":93},[87,8682,162],{"class":97},[87,8684,8685],{"class":165},"\"text-[#50d71e] ...\"",[87,8687,5037],{"class":97},[87,8689,16],{"class":155},[87,8691,169],{"class":97},[87,8693,8694,8696,8698,8700,8702,8705,8707,8709],{"class":89,"line":101},[87,8695,152],{"class":97},[87,8697,16],{"class":155},[87,8699,159],{"class":93},[87,8701,162],{"class":97},[87,8703,8704],{"class":165},"\"text-(--my-color) ...\"",[87,8706,5037],{"class":97},[87,8708,16],{"class":155},[87,8710,169],{"class":97},[16,8712,8713,8714,8717],{},"이런식으로 color 코드를 직접 넣거나, variables 를 지정하면 커스텀 컬러를 사용할 수 있다. 하지만 나는 ",[27,8715,8716],{},"text-error-90"," 형식으로 사용하고 싶다.",[45,8719,8721],{"id":8720},"customizing-theme","customizing theme",[16,8723,8724],{},"테마를 수정하는 방식을 제공한다.",[1108,8726],{"host":3146,"title":8664,"url":8727,"description":8666,"image":8667,"favicon":3145},"https:\u002F\u002Ftailwindcss.com\u002Fdocs\u002Fcolor#customizing-your-theme",[1108,8729],{"host":3146,"title":8730,"url":8731,"description":8732,"image":8733,"favicon":3145},"Theme variables - Core concepts","https:\u002F\u002Ftailwindcss.com\u002Fdocs\u002Ftheme","Using utility classes as an API for your design tokens.","https:\u002F\u002Ftailwindcss.com\u002Fapi\u002Fog?path=\u002Fdocs\u002Ftheme",[78,8735,8737],{"className":80,"code":8736,"language":82,"meta":83,"style":83},"@theme {\n  --color-base-10: #FFFFFF;\n  --color-base-20: #F8F8F9;\n  --color-base-30: #ECEDF0;\n  ...\n}\n",[27,8738,8739,8746,8754,8762,8770,8775],{"__ignoreMap":83},[87,8740,8741,8744],{"class":89,"line":90},[87,8742,8743],{"class":562},"@theme",[87,8745,98],{"class":97},[87,8747,8748,8751],{"class":89,"line":101},[87,8749,8750],{"class":97},"  --color-base-10: ",[87,8752,8753],{"class":3013},"#FFFFFF;\n",[87,8755,8756,8759],{"class":89,"line":117},[87,8757,8758],{"class":97},"  --color-base-20: ",[87,8760,8761],{"class":3013},"#F8F8F9;\n",[87,8763,8764,8767],{"class":89,"line":130},[87,8765,8766],{"class":97},"  --color-base-30: ",[87,8768,8769],{"class":3013},"#ECEDF0;\n",[87,8771,8772],{"class":89,"line":224},[87,8773,8774],{"class":97},"  ...\n",[87,8776,8777],{"class":89,"line":246},[87,8778,133],{"class":97},[78,8780,8782],{"className":143,"code":8781,"language":145,"meta":83,"style":83},"\u003Cp class=\"text-base-30\">\u003C\u002Fp>\n",[27,8783,8784],{"__ignoreMap":83},[87,8785,8786,8788,8790,8792,8794,8797,8799,8801],{"class":89,"line":90},[87,8787,152],{"class":97},[87,8789,16],{"class":155},[87,8791,159],{"class":93},[87,8793,162],{"class":97},[87,8795,8796],{"class":165},"\"text-base-30\"",[87,8798,5037],{"class":97},[87,8800,16],{"class":155},[87,8802,169],{"class":97},[16,8804,8805,8808,8809,8812],{},[27,8806,8807],{},"text-base-30"," ",[27,8810,8811],{},"bg-base-30"," 이런식으로 색상을 사용할 수 있게 된다. tailwindcss 가 변수를 읽어 자동으로 utility class 를 생성해준다.",[16,8814,8815],{},[513,8816],{"alt":515,"src":8817},"blog\u002Fimg\u002Ftailwindcss-color-preset-with-theme\u002Fimg.png",[16,8819,8820],{},"오오",[1108,8822],{"host":1710,"title":8823,"url":8824,"description":8825,"image":8826},"How to use custom color themes in TailwindCSS v4","https:\u002F\u002Fstackoverflow.com\u002Fa\u002F79499827","My tailwind.config.js in v3 looks like this, but I can't find a way to use it in v4:theme: {  extend: {    colors: {      lightHover: '#fcf4ff',      darkHover: '#2a004a',      darktheme: '#1...","https:\u002F\u002Fcdn.sstatic.net\u002FSites\u002Fstackoverflow\u002Fimg\u002Fapple-touch-icon@2.png?v=73d79a89bded",[16,8828,8829],{},"다음은 테마를 적용해보자.",[78,8831,8833],{"className":80,"code":8832,"language":82,"meta":83,"style":83},"\n@custom-variant dark (&:where(.dark, .dark *));\n@theme {\n  --color-custom-10: #FFFFFF;\n  --color-custom-20: #F8F8F9;\n  --color-custom-30: #ECEDF0;\n    ...\n}\n@variant dark {\n  --color-custom-10: #0D152A;\n  --color-custom-20: #303445;\n  --color-custom-30: #555C6C;\n    ...\n}\n",[27,8834,8835,8839,8847,8853,8860,8867,8874,8878,8882,8890,8899,8908,8917,8921],{"__ignoreMap":83},[87,8836,8837],{"class":89,"line":90},[87,8838,1446],{"emptyLinePlaceholder":842},[87,8840,8841,8844],{"class":89,"line":101},[87,8842,8843],{"class":562},"@custom-variant",[87,8845,8846],{"class":97}," dark (&:where(.dark, .dark *));\n",[87,8848,8849,8851],{"class":89,"line":117},[87,8850,8743],{"class":562},[87,8852,98],{"class":97},[87,8854,8855,8858],{"class":89,"line":130},[87,8856,8857],{"class":97},"  --color-custom-10: ",[87,8859,8753],{"class":3013},[87,8861,8862,8865],{"class":89,"line":224},[87,8863,8864],{"class":97},"  --color-custom-20: ",[87,8866,8761],{"class":3013},[87,8868,8869,8872],{"class":89,"line":246},[87,8870,8871],{"class":97},"  --color-custom-30: ",[87,8873,8769],{"class":3013},[87,8875,8876],{"class":89,"line":256},[87,8877,7476],{"class":97},[87,8879,8880],{"class":89,"line":270},[87,8881,133],{"class":97},[87,8883,8884,8887],{"class":89,"line":280},[87,8885,8886],{"class":562},"@variant",[87,8888,8889],{"class":97}," dark {\n",[87,8891,8892,8894,8897],{"class":89,"line":295},[87,8893,8857],{"class":97},[87,8895,8896],{"class":3013},"#0D152A",[87,8898,114],{"class":97},[87,8900,8901,8903,8906],{"class":89,"line":315},[87,8902,8864],{"class":97},[87,8904,8905],{"class":3013},"#303445",[87,8907,114],{"class":97},[87,8909,8910,8912,8915],{"class":89,"line":330},[87,8911,8871],{"class":97},[87,8913,8914],{"class":3013},"#555C6C",[87,8916,114],{"class":97},[87,8918,8919],{"class":89,"line":351},[87,8920,7476],{"class":97},[87,8922,8923],{"class":89,"line":360},[87,8924,133],{"class":97},[16,8926,8927,8928,8931,8932,8935,8936,8939],{},"이렇게 ",[27,8929,8930],{},"@variant dark"," 로 컬러를 선언해주면, ",[27,8933,8934],{},"@custom-variant dark"," 에 의해서, ",[27,8937,8938],{},".dark"," 클래스 아래의 요소들은 variables 가 갈아끼워진다.",[78,8941,8943],{"className":143,"code":8942,"language":145,"meta":83,"style":83},"\u003Chtml class=\"dark\">\u003C\u002Fhtml>\n",[27,8944,8945],{"__ignoreMap":83},[87,8946,8947,8949,8951,8953,8955,8958,8960,8962],{"class":89,"line":90},[87,8948,152],{"class":97},[87,8950,145],{"class":155},[87,8952,159],{"class":93},[87,8954,162],{"class":97},[87,8956,8957],{"class":165},"\"dark\"",[87,8959,5037],{"class":97},[87,8961,145],{"class":155},[87,8963,169],{"class":97},[16,8965,8966,8967,8969,8970,8972],{},"위 형태로 ",[27,8968,145],{}," 태그의 클래스에 dark 가 포함되면, ",[27,8971,8930],{}," 의 변수들이 사용되는 구조이다.",[78,8974,8976],{"className":143,"code":8975,"language":145,"meta":83,"style":83},"\u003Cdiv class=\"text-custom-70 dark:text-custom-50\">test color tailwindcss\u003C\u002Fdiv>\n\u003Cdiv class=\"text-custom-70\">test color tailwindcss\u003C\u002Fdiv>\n",[27,8977,8978,8998],{"__ignoreMap":83},[87,8979,8980,8982,8984,8986,8988,8991,8994,8996],{"class":89,"line":90},[87,8981,152],{"class":97},[87,8983,156],{"class":155},[87,8985,159],{"class":93},[87,8987,162],{"class":97},[87,8989,8990],{"class":165},"\"text-custom-70 dark:text-custom-50\"",[87,8992,8993],{"class":97},">test color tailwindcss\u003C\u002F",[87,8995,156],{"class":155},[87,8997,169],{"class":97},[87,8999,9000,9002,9004,9006,9008,9011,9013,9015],{"class":89,"line":101},[87,9001,152],{"class":97},[87,9003,156],{"class":155},[87,9005,159],{"class":93},[87,9007,162],{"class":97},[87,9009,9010],{"class":165},"\"text-custom-70\"",[87,9012,8993],{"class":97},[87,9014,156],{"class":155},[87,9016,169],{"class":97},[16,9018,9019,9020,9023,9024,9027],{},"tailwindcss 에서는 dark mode 일 때는 ",[27,9021,9022],{},"dark:bg-base-10"," 이런식으로 특정 조건일 때 클래스를 적용할 수 있도록 기능을 제공한다.\n",[27,9025,9026],{},"dark:*"," 로 dark 모드일 때 데이터를 지정하여, dark 모드가 되었을 때 70 이 아닌 50을 사용하겠다는 뜻이 된다. 다만, dark 모드로 갈아끼워진 variable 의 50을 사용하게 되는 것을 주의해야 한다.",[78,9029,9031],{"className":1150,"code":9030,"language":1152,"meta":83,"style":83},"\u002F**\n * # 테마 컬러를 변경한다.\n *\u002F\nfunction changeTheme(theme: ColorTheme) {\n    document.documentElement.classList.remove(...[\"dark-mode\", ...]);\n    if (theme === ColorTheme.DARK) {\n        document.documentElement.classList.add(\"dark-mode\");\n    } else if (theme === ColorTheme.SOMETHEME) { ... }\n    localStorage.setItem(STORAGE_KEY, theme);\n    colorTheme.value = theme;\n}\n",[27,9032,9033,9037,9042,9046,9066,9091,9108,9122,9148,9164,9174],{"__ignoreMap":83},[87,9034,9035],{"class":89,"line":90},[87,9036,2555],{"class":1339},[87,9038,9039],{"class":89,"line":101},[87,9040,9041],{"class":1339}," * # 테마 컬러를 변경한다.\n",[87,9043,9044],{"class":89,"line":117},[87,9045,2565],{"class":1339},[87,9047,9048,9051,9054,9056,9059,9061,9064],{"class":89,"line":130},[87,9049,9050],{"class":562},"function",[87,9052,9053],{"class":93}," changeTheme",[87,9055,791],{"class":97},[87,9057,9058],{"class":1168},"theme",[87,9060,1204],{"class":562},[87,9062,9063],{"class":93}," ColorTheme",[87,9065,4409],{"class":97},[87,9067,9068,9071,9074,9076,9078,9081,9084,9086,9088],{"class":89,"line":224},[87,9069,9070],{"class":97},"    document.documentElement.classList.",[87,9072,9073],{"class":93},"remove",[87,9075,791],{"class":97},[87,9077,1438],{"class":562},[87,9079,9080],{"class":97},"[",[87,9082,9083],{"class":165},"\"dark-mode\"",[87,9085,60],{"class":97},[87,9087,1438],{"class":562},[87,9089,9090],{"class":97},"]);\n",[87,9092,9093,9095,9098,9100,9103,9106],{"class":89,"line":246},[87,9094,4224],{"class":562},[87,9096,9097],{"class":97}," (theme ",[87,9099,4320],{"class":562},[87,9101,9102],{"class":97}," ColorTheme.",[87,9104,9105],{"class":104},"DARK",[87,9107,4409],{"class":97},[87,9109,9110,9113,9116,9118,9120],{"class":89,"line":256},[87,9111,9112],{"class":97},"        document.documentElement.classList.",[87,9114,9115],{"class":93},"add",[87,9117,791],{"class":97},[87,9119,9083],{"class":165},[87,9121,1590],{"class":97},[87,9123,9124,9126,9129,9132,9134,9136,9138,9141,9144,9146],{"class":89,"line":270},[87,9125,5365],{"class":97},[87,9127,9128],{"class":562},"else",[87,9130,9131],{"class":562}," if",[87,9133,9097],{"class":97},[87,9135,4320],{"class":562},[87,9137,9102],{"class":97},[87,9139,9140],{"class":104},"SOMETHEME",[87,9142,9143],{"class":97},") { ",[87,9145,1438],{"class":562},[87,9147,1441],{"class":97},[87,9149,9150,9153,9156,9158,9161],{"class":89,"line":280},[87,9151,9152],{"class":97},"    localStorage.",[87,9154,9155],{"class":93},"setItem",[87,9157,791],{"class":97},[87,9159,9160],{"class":104},"STORAGE_KEY",[87,9162,9163],{"class":97},", theme);\n",[87,9165,9166,9169,9171],{"class":89,"line":295},[87,9167,9168],{"class":97},"    colorTheme.value ",[87,9170,162],{"class":562},[87,9172,9173],{"class":97}," theme;\n",[87,9175,9176],{"class":89,"line":315},[87,9177,133],{"class":97},[16,9179,9180,9181,9184,9185,9187,9188,9191,9192,9194],{},"테마 변경을 ",[27,9182,9183],{},"body"," 의 클래스로 활용했었다. ",[27,9186,145],{}," 태그의 클래스를 바꿔주기 위해서 대상을 ",[27,9189,9190],{},"document.documentElement"," 로 변경해주었다. ",[27,9193,8886],{}," 는 html 태그의 클래스를 인식한다.",[45,9196,9198],{"id":9197},"vue3-에서-css-variables-값-가져오기","vue3 에서 css variables 값 가져오기",[1108,9200],{"host":9201,"title":9202,"url":9203},"www.reddit.com","Reddit - The heart of the internet","https:\u002F\u002Fwww.reddit.com\u002Fr\u002Fvuejs\u002Fcomments\u002F13o4z9y\u002Fhow_to_access_css_variable_varbackgrounf_color_in\u002F",[78,9205,9207],{"className":1150,"code":9206,"language":1152,"meta":83,"style":83},"const root = document.querySelector(':root');\nif(root) {\n    return {\n        100: getComputedStyle(root).getPropertyValue(`--color-${p}-100`),\n        ...\n    };\n} \n",[27,9208,9209,9231,9238,9244,9272,9277,9282],{"__ignoreMap":83},[87,9210,9211,9213,9216,9218,9221,9224,9226,9229],{"class":89,"line":90},[87,9212,1964],{"class":562},[87,9214,9215],{"class":104}," root",[87,9217,1362],{"class":562},[87,9219,9220],{"class":97}," document.",[87,9222,9223],{"class":93},"querySelector",[87,9225,791],{"class":97},[87,9227,9228],{"class":165},"':root'",[87,9230,1590],{"class":97},[87,9232,9233,9235],{"class":89,"line":101},[87,9234,7307],{"class":562},[87,9236,9237],{"class":97},"(root) {\n",[87,9239,9240,9242],{"class":89,"line":117},[87,9241,4985],{"class":562},[87,9243,98],{"class":97},[87,9245,9246,9249,9251,9254,9257,9260,9262,9265,9267,9270],{"class":89,"line":130},[87,9247,9248],{"class":104},"        100",[87,9250,108],{"class":97},[87,9252,9253],{"class":93},"getComputedStyle",[87,9255,9256],{"class":97},"(root).",[87,9258,9259],{"class":93},"getPropertyValue",[87,9261,791],{"class":97},[87,9263,9264],{"class":165},"`--color-${",[87,9266,16],{"class":97},[87,9268,9269],{"class":165},"}-100`",[87,9271,5693],{"class":97},[87,9273,9274],{"class":89,"line":224},[87,9275,9276],{"class":562},"        ...\n",[87,9278,9279],{"class":89,"line":246},[87,9280,9281],{"class":97},"    };\n",[87,9283,9284],{"class":89,"line":256},[87,9285,133],{"class":97},[16,9287,9288,9290,9291,9294],{},[27,9289,8886],{}," 로 지정한 테마의 색상 변수는 ",[27,9292,9293],{},":root"," 요소를 찾아야 한다. 계산된 style 을 찾아서 변수의 값을 가져오자.",[825,9296,9297],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}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}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":83,"searchDepth":101,"depth":101,"links":9299},[9300,9301,9302],{"id":8660,"depth":101,"text":8661},{"id":8720,"depth":101,"text":8721},{"id":9197,"depth":101,"text":9198},"2025-03-27","Tailwind CSS의 테마 커스터마이징 방법을 소개하며, 사용자 정의 색상 변수를 설정하여 디자인을 개선하는 기술을 다룬다. 다양한 테마 설정과 CSS 변수 사용 사례를 통해 원하는 스타일을 구현하는 방법을 배울 수 있다.","tailwindcss-color-preset-with-theme",{},"\u002Fblog\u002Ftailwindcss-color-preset-with-theme",{"title":8607,"description":9304},{"loc":9307},"blog\u002Ftailwindcss-color-preset-with-theme",[82,3268],"j4U4fl8xsMe9j8Ndha4iilgKYTj7Hle2Xp05ngUVWiQ",{"id":9314,"title":9315,"body":9316,"created":9466,"description":9467,"extension":839,"filename":9468,"meta":9469,"navigation":842,"path":9470,"seo":9471,"sitemap":9472,"stem":9473,"subject":9474,"tags":9475,"updated":9466,"volume":849,"__hash__":9478},"blog\u002Fblog\u002Ftailwindcss-storybook-version-error.md","tailwindcss 4.0.8 버전 storybook 호환성 오류",{"type":8,"value":9317,"toc":9461},[9318,9322,9325,9328,9342,9348,9351,9357,9363,9366,9374,9379,9386,9390,9396,9399,9410,9414,9420,9431,9456,9459],[45,9319,9321],{"id":9320},"error-enoent-no-such-file-or-directory-stat-cusersprojectpathiframehtml","Error: ENOENT: no such file or directory, stat 'C:\\Users\\PROJECTPATH\\iframe.html'",[16,9323,9324],{},"어느날 갑자기 CICD가 안되어서 내용을 보니 storybook 빌드를 하면서 에러가 발생했었다. 분명 로컬에서는 되는데 원격 환경에서만 에러가 발생하는 것이었다.",[16,9326,9327],{},"사용 중인 버전정보는 다음과 같다,",[20,9329,9330,9333,9336,9339],{},[23,9331,9332],{},"tailwindcss@4.0.8",[23,9334,9335],{},"storybook@8.6.0",[23,9337,9338],{},"vite@6.1.0",[23,9340,9341],{},"vue3@3.5.13",[78,9343,9346],{"className":9344,"code":9345,"language":1095},[1093],"{projectname} build-storybook\nstorybook build\n\n@storybook\u002Fcore v8.6.0\n\ninfo => Cleaning outputDir: storybook-static\ninfo => Loading presets\nWARN The \"@storybook\u002Faddon-mdx-gfm\" addon is meant as a migration assistant for Storybook 8.0; and will likely be removed in a future version.\nWARN It's recommended you read this document:\nWARN https:\u002F\u002Fstorybook.js.org\u002Fdocs\u002Fwriting-docs\u002Fmdx#markdown-tables-arent-rendering-correctly\nWARN\nWARN Once you've made the necessary changes, you can remove the addon from your package.json and storybook config.\ninfo => Building manager..\ninfo => Manager built (236 ms)\ninfo => Building preview..\nmode production\nnpm_package_version 2.12.3\nplugin 'rollup-plugin-html-env' uses deprecated 'transform' option. Use 'handler' option instead.\nvite v6.1.0 building for production...\ntransforming (1) virtual:@storybook\\builder-vite\\vite-app.jsC:\\Users\\PROJECTPATH\\node_modules\\storybook\\bin\\index.cjs:23\nthrow error;\n^\n\nError: ENOENT: no such file or directory, stat 'C:\\Users\\PROJECTPATH\\iframe.html'\nat async Object.stat (node:internal\u002Ffs\u002Fpromises:1036:18)\nat async C.addBuildDependency (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002F@tailwindcss\u002Fvite\u002Fdist\u002Findex.mjs:1:5226) {\nerrno: -4058,\ncode: 'ENOENT',\nsyscall: 'stat',\npath: 'C:\\Users\\PROJECTPATH\\iframe.html'\n}\n",[27,9347,9345],{"__ignoreMap":83},[16,9349,9350],{},"iframe.html 파일을 찾지 못해서 발생한 에러이다. storybook 과 tailwindcss 를 같이 사용할 때, 미리보기 파일이 생성이 안되면서 발생하는 것으로 보인다.",[1108,9352],{"description":9353,"favicon":1316,"host":1313,"image":9354,"title":9355,"url":9356},"What version of Tailwind CSS are you using? v4.0.8 What build tool (or framework if it abstracts the build tool) are you using? Storybook v8.5.8 Vite v5.4.14 What version of Node.js are you using? ...","https:\u002F\u002Fopengraph.githubassets.com\u002Fe7a6f8fe1ba78e023dbfa9f44388e93bcbe3c43841cf0e9034777d276d47677c\u002Ftailwindlabs\u002Ftailwindcss\u002Fissues\u002F16785","4.0.8 + Vite + Storybook = new crash · Issue #16785 · tailwindlabs\u002Ftailwindcss","https:\u002F\u002Fgithub.com\u002Ftailwindlabs\u002Ftailwindcss\u002Fissues\u002F16785",[1108,9358],{"description":9359,"favicon":1316,"host":1313,"image":9360,"title":9361,"url":9362},"Fixes #16732If we can not get the mtime from a file, chances are that the resource is a virtual module. This is perfectly legit and we can fall back to what we did before the changes in 4.0.8 (whi...","https:\u002F\u002Fopengraph.githubassets.com\u002Ff4e77eb4132aa99f9be1cfadae376ec84a4a99df80f95e1692ae31ba1d1ef10a\u002Ftailwindlabs\u002Ftailwindcss\u002Fpull\u002F16780","Vite: Don't crash with virtual module dependencies by philipp-spiess · Pull Request #16780 · tailwindlabs\u002Ftailwindcss","https:\u002F\u002Fgithub.com\u002Ftailwindlabs\u002Ftailwindcss\u002Fpull\u002F16780",[16,9364,9365],{},"헤당 이슈 혹은 비슷한 이슈가 이미 오픈되어 있었다. 이 문제를 발견한 시점으로부터 바로 하루 전이었다.",[1718,9367,9368],{},[16,9369,9370,9373],{},[87,9371,9372],{},"!success","\n여기서 insider 라는 용어를 처음 알았다. 공식적으로 발표되지 않았지만, 사용해볼 수 있는 버전을 뜻하는듯 하다.",[16,9375,9376],{},[513,9377],{"alt":515,"src":9378},"blog\u002Fimg\u002Ftailwindcss-storybook-version-error\u002Ftailwindcss-storybook-version-error.png",[16,9380,9381,9382,9385],{},"내용으로 봐서는 4.0.8 버전에 일부 문제가 있었던 것 같고, 4.0.9 버전에서 픽스가 되어서 바로 사용해볼 수 있었다. ",[27,9383,9384],{},"\"tailwindcss\": \"^4.0.9\""," 으로 세팅해서 lock 파일과 node_modules 폴더를 모두 지우고 다시 설치해서 이 문제는 해결되었다.",[45,9387,9389],{"id":9388},"cannot-be-closed-a","Cannot be closed: a",[78,9391,9394],{"className":9392,"code":9393,"language":1095},[1093],"✗ Build failed in 8.40s\n=> Failed to build the preview\n[vite:build-html] Cannot be closed: a\n    at ParseHTML.onCloseTagEndEvent (.\\node_modules\\vite-plugin-html-env\\lib\\parse\\index.js:313:15)\n    at ParseHTML.parse (.\\node_modules\\vite-plugin-html-env\\lib\\parse\\index.js:189:16)\n    at transform (.\\node_modules\\vite-plugin-html-env\\lib\\index.js:148:19)\n    at applyHtmlTransforms (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Fvite\u002Fdist\u002Fnode\u002Fchunks\u002Fdep-ByPKlqZ5.js:42504:23)\n    at async Object.generateBundle (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Fvite\u002Fdist\u002Fnode\u002Fchunks\u002Fdep-ByPKlqZ5.js:42244:18)\n    at async Bundle.generate (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Frollup\u002Fdist\u002Fes\u002Fshared\u002Fnode-entry.js:20116:9)\n    at async file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Frollup\u002Fdist\u002Fes\u002Fshared\u002Fnode-entry.js:22805:27\n    at async catchUnfinishedHookActions (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Frollup\u002Fdist\u002Fes\u002Fshared\u002Fnode-entry.js:22187:16)\n    at async buildEnvironment (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Fvite\u002Fdist\u002Fnode\u002Fchunks\u002Fdep-ByPKlqZ5.js:51450:16)\n    at async build (.\\node_modules\\@storybook\\builder-vite\\dist\\index.js:80:230)\n",[27,9395,9393],{"__ignoreMap":83},[16,9397,9398],{},"다시 최신 커밋으로 돌아가 tailwindcss 버전만 4.0.9 로 올린다고 성공하진 않았다. storybook 버전은 그대로 사용해야 충돌이 안나는듯 했다. 이 에러는 storybook 의 에러로 보인다. story 파일도 모두 지우고 테스트해봐도 같은 결과인 걸 보면, 작성한 stories 에서 문제가 있던 건 아닌 거로 보인다.",[16,9400,9401,9402,9405,9406,9409],{},"문제가 된 커밋을 찾아보니, 이번엔 storybook 의 버전 문제였다. 원래 되던 것이 안되면 무조건 버전 문제다. ",[27,9403,9404],{},"8.4.7"," -> ",[27,9407,9408],{},"8.6.0"," 으로 올렸던 것이 문제가 되었었다. 이 커밋을 revert 하고 시도하니 해결되었다. 무작정 신버전이 능사는아니다.",[45,9411,9413],{"id":9412},"tailwindcssoxide-linux-x64-gnu","@tailwindcss\u002Foxide-linux-x64-gnu",[78,9415,9418],{"className":9416,"code":9417,"language":1095},[1093],"failed to load config from \u002Fhome\u002Frunner\u002Fwork\u002FUpbox-2.0-Front-Application\u002FUpbox-2.0-Front-Application\u002Fvite.config.ts\n=> Failed to build the preview\nError: Cannot find module '@tailwindcss\u002Foxide-linux-x64-gnu'\nRequire stack:\n- .\u002Fnode_modules\u002F@tailwindcss\u002Foxide\u002Findex.js\n    at Function.\u003Canonymous> (node:internal\u002Fmodules\u002Fcjs\u002Floader:1225:15)\n    at Module._resolveFilename (.\u002Fnode_modules\u002Fesbuild-register\u002Fdist\u002Fnode.js:4794:36)\n    at Function._load (node:internal\u002Fmodules\u002Fcjs\u002Floader:1055:27)\n    at TracingChannel.traceSync (node:diagnostics_channel:322:14)\n    at wrapModuleLoad (node:internal\u002Fmodules\u002Fcjs\u002Floader:220:24)\n    at Module.require (node:internal\u002Fmodules\u002Fcjs\u002Floader:1311:12)\n    at require (node:internal\u002Fmodules\u002Fhelpers:136:16)\n    at Object.\u003Canonymous> (.\u002Fnode_modules\u002F@tailwindcss\u002Foxide\u002Findex.js:190:31)\n    at Module._compile (node:internal\u002Fmodules\u002Fcjs\u002Floader:1554:14)\n    at node:internal\u002Fmodules\u002Fcjs\u002Floader:1706:10\n",[27,9419,9417],{"__ignoreMap":83},[16,9421,9422,9423,9426,9427,9430],{},"github actions 에서 빌드를 하려니 또 이런 에러가 발생한다. ",[27,9424,9425],{},"^4.0.6"," 버전일 때는 에러가 없었는데, ",[27,9428,9429],{},"^4.0.9"," 로 올리면서 에러가 발생한듯 하다. 일단 optionalDependencies 에 추가해주었다.",[78,9432,9434],{"className":7571,"code":9433,"language":7573,"meta":83,"style":83},"\"optionalDependencies\": {\n    \"@tailwindcss\u002Foxide-linux-x64-gnu\": \"^4.0.9\"\n}\n",[27,9435,9436,9442,9452],{"__ignoreMap":83},[87,9437,9438,9440],{"class":89,"line":90},[87,9439,7616],{"class":165},[87,9441,7583],{"class":97},[87,9443,9444,9447,9449],{"class":89,"line":101},[87,9445,9446],{"class":104},"    \"@tailwindcss\u002Foxide-linux-x64-gnu\"",[87,9448,108],{"class":97},[87,9450,9451],{"class":165},"\"^4.0.9\"\n",[87,9453,9454],{"class":89,"line":117},[87,9455,133],{"class":97},[16,9457,9458],{},"버전은 tailwindcss 버전과 맞추면 될듯 하다.",[825,9460,7659],{},{"title":83,"searchDepth":101,"depth":101,"links":9462},[9463,9464,9465],{"id":9320,"depth":101,"text":9321},{"id":9388,"depth":101,"text":9389},{"id":9412,"depth":101,"text":9413},"2025-03-21","tailwindcss 와 storybook 의 호환성 문제 해결 과정. 특정 버전의 문제로 핫픽스 적용.","tailwindcss-storybook-version-error",{},"\u002Fblog\u002Ftailwindcss-storybook-version-error",{"title":9315,"description":9467},{"loc":9470},"blog\u002Ftailwindcss-storybook-version-error","Tailwindcss",[3268,1257,9476,8468,9477],"Vite","storybook","tl7IS5hl8Q7LUZMXFCNHZ6Hdddv0yYP4aCMqI7ASgmE",{"id":9480,"title":9481,"body":9482,"created":12316,"description":12317,"extension":839,"filename":12318,"meta":12319,"navigation":842,"path":12320,"seo":12321,"sitemap":12322,"stem":12323,"subject":2858,"tags":12324,"updated":12316,"volume":7536,"__hash__":12325},"blog\u002Fblog\u002Fvite-vue-and-github-pages.md","Vite vue + Github Pages 페이지 구축",{"type":8,"value":9483,"toc":12293},[9484,9487,9495,9499,9505,9508,9511,9523,9526,9532,9547,9578,9581,9666,9676,9679,9689,9699,9705,9791,9796,9808,9810,9820,9823,9829,9832,9988,9994,9998,10011,10025,10028,10032,10043,10046,10051,10182,10189,10280,10289,10292,10302,10305,10311,10365,10372,10432,10438,10585,10588,10591,10601,10604,10610,10709,10721,10725,10732,10755,10797,10804,10844,10847,10912,10918,10922,10925,10935,10941,10944,10954,10960,10964,10967,11160,11167,11190,11241,11247,11250,11260,11269,11337,11343,11377,11388,11391,11399,11402,11421,11424,11430,11650,11657,11951,11954,12096,12099,12102,12106,12112,12119,12194,12208,12211,12220,12226,12232,12284,12290],[16,9485,9486],{},"기존 vue-cli 를 사용한 vue 프로젝트로 github pages 를 구축했는데, 대세에 따라 vite 로 변경하기로 했다. 말이 변경이지 새로 만들었다.",[16,9488,9489,9490,9494],{},"기존 vue-cli 로 했던 것에서 vite 로 변경하며 필요한 부분만 작성하였고, github actions 등의 기능은 ",[2943,9491,3272],{"href":9492,"rel":9493},"https:\u002F\u002Flhs-source.github.io\u002Fposts\u002Fmaking-blog-githubio-vue3-1#github-actions-%EB%A1%9C-github-pages-%EB%B0%B0%ED%8F%AC",[2947]," 를 참고하면 된다.",[45,9496,9498],{"id":9497},"vite-프로젝트-생성","vite 프로젝트 생성",[78,9500,9503],{"className":9501,"code":9502,"language":1095},[1093],"npm create vite@latest\n",[27,9504,9502],{"__ignoreMap":83},[16,9506,9507],{},"우선 빈 vite 프로젝트를 만들어준다. 우리는 vue3 와 typescript 를 활용해서 개발할 것이다.",[45,9509,9510],{"id":9510},"vite-ssg",[1718,9512,9513],{},[16,9514,9515,9516,9519],{},"Vite SSG",[9517,9518],"br",{},[2943,9520,9521],{"href":9521,"rel":9522},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvite-ssg",[2947],[16,9524,9525],{},"vite 로 static site generating 하는 라이브러리이다. 우리가 만드는 포스트 마다 정적 사이트로 만들어준다",[78,9527,9530],{"className":9528,"code":9529,"language":1095},[1093],"npm i -D vite-ssg vue-router @vueuse\u002Fhead\n",[27,9531,9529],{"__ignoreMap":83},[16,9533,9534,9535,9538,9539,9542,9543,9546],{},"위 커맨드로 ",[27,9536,9537],{},"vue-router","  와 ",[27,9540,9541],{},"@vueuse\u002Fhead","  도 함께 설치해주라고 나와 있는데, 나는 ",[27,9544,9545],{},"@unhead\u002Fvue"," 를 사용했다.",[78,9548,9550],{"className":7571,"code":9549,"language":7573,"meta":83,"style":83},"\u002F\u002F package.json\n\"scripts\": {\n  \"build:ssg\": \"vite-ssg build\"\n}\n",[27,9551,9552,9557,9564,9574],{"__ignoreMap":83},[87,9553,9554],{"class":89,"line":90},[87,9555,9556],{"class":1339},"\u002F\u002F package.json\n",[87,9558,9559,9562],{"class":89,"line":101},[87,9560,9561],{"class":165},"\"scripts\"",[87,9563,7583],{"class":97},[87,9565,9566,9569,9571],{"class":89,"line":117},[87,9567,9568],{"class":104},"  \"build:ssg\"",[87,9570,108],{"class":97},[87,9572,9573],{"class":165},"\"vite-ssg build\"\n",[87,9575,9576],{"class":89,"line":130},[87,9577,133],{"class":97},[16,9579,9580],{},"빌드 커맨드를 대체 혹은 추가해준다.",[78,9582,9585],{"className":1150,"code":9583,"filename":9584,"language":1152,"meta":83,"style":83},"export const createApp = ViteSSG(\n    \u002F\u002F 루트 컴포넌트\n    App,\n    { routes },\n    ({ app, router, routes, isClient, initialState }) => {\n        \u002F\u002F 플러그인 세팅\n    },\n)\n","main.ts",[27,9586,9587,9603,9608,9613,9618,9653,9658,9662],{"__ignoreMap":83},[87,9588,9589,9591,9593,9595,9597,9600],{"class":89,"line":90},[87,9590,2570],{"class":562},[87,9592,7815],{"class":562},[87,9594,7354],{"class":104},[87,9596,1362],{"class":562},[87,9598,9599],{"class":93}," ViteSSG",[87,9601,9602],{"class":97},"(\n",[87,9604,9605],{"class":89,"line":101},[87,9606,9607],{"class":1339},"    \u002F\u002F 루트 컴포넌트\n",[87,9609,9610],{"class":89,"line":117},[87,9611,9612],{"class":97},"    App,\n",[87,9614,9615],{"class":89,"line":130},[87,9616,9617],{"class":97},"    { routes },\n",[87,9619,9620,9623,9626,9628,9631,9633,9636,9638,9641,9643,9646,9649,9651],{"class":89,"line":224},[87,9621,9622],{"class":97},"    ({ ",[87,9624,9625],{"class":1168},"app",[87,9627,60],{"class":97},[87,9629,9630],{"class":1168},"router",[87,9632,60],{"class":97},[87,9634,9635],{"class":1168},"routes",[87,9637,60],{"class":97},[87,9639,9640],{"class":1168},"isClient",[87,9642,60],{"class":97},[87,9644,9645],{"class":1168},"initialState",[87,9647,9648],{"class":97}," }) ",[87,9650,1175],{"class":562},[87,9652,98],{"class":97},[87,9654,9655],{"class":89,"line":246},[87,9656,9657],{"class":1339},"        \u002F\u002F 플러그인 세팅\n",[87,9659,9660],{"class":89,"line":256},[87,9661,5846],{"class":97},[87,9663,9664],{"class":89,"line":270},[87,9665,5501],{"class":97},[16,9667,9668,9671,9672,9675],{},[27,9669,9670],{},"createApp","  을 ",[27,9673,9674],{},"ViteSSG","  로 변경한다. 이러면 빌드했을 때, SPA 앱이 아니라, MPA로 빌드가 된다. 각 라우트마다 html 페이지가 따로 생성된다.",[45,9677,9678],{"id":9678},"vite-plugin-pages-sitemap",[1718,9680,9681],{},[16,9682,9678,9683,9685],{},[9517,9684],{},[2943,9686,9687],{"href":9687,"rel":9688},"https:\u002F\u002Fgithub.com\u002Fjbaubree\u002Fvite-plugin-pages-sitemap",[2947],[16,9690,9691,9692,9694,9695,9698],{},"사이트맵 자동 생성기이다. ",[27,9693,9510],{},"  와 ",[27,9696,9697],{},"vite-plugin-pages","  를 같이 지원한다.",[78,9700,9703],{"className":9701,"code":9702,"language":1095},[1093],"npm install -D vite-plugin-pages-sitemap\n",[27,9704,9702],{"__ignoreMap":83},[78,9706,9708],{"className":1150,"code":9707,"language":1152,"meta":83,"style":83},"import Pages from 'vite-plugin-pages'\nimport generateSitemap from 'vite-plugin-pages-sitemap'\nexport default {\n  plugins: [\n    \u002F\u002F ...\n    Pages({\n      onRoutesGenerated: routes => (generateSitemap({ routes })),\n    }),\n  ],\n}\n\n",[27,9709,9710,9722,9734,9742,9747,9752,9759,9778,9782,9787],{"__ignoreMap":83},[87,9711,9712,9714,9717,9719],{"class":89,"line":90},[87,9713,2495],{"class":562},[87,9715,9716],{"class":97}," Pages ",[87,9718,2501],{"class":562},[87,9720,9721],{"class":165}," 'vite-plugin-pages'\n",[87,9723,9724,9726,9729,9731],{"class":89,"line":101},[87,9725,2495],{"class":562},[87,9727,9728],{"class":97}," generateSitemap ",[87,9730,2501],{"class":562},[87,9732,9733],{"class":165}," 'vite-plugin-pages-sitemap'\n",[87,9735,9736,9738,9740],{"class":89,"line":117},[87,9737,2570],{"class":562},[87,9739,4575],{"class":562},[87,9741,98],{"class":97},[87,9743,9744],{"class":89,"line":130},[87,9745,9746],{"class":97},"  plugins: [\n",[87,9748,9749],{"class":89,"line":224},[87,9750,9751],{"class":1339},"    \u002F\u002F ...\n",[87,9753,9754,9757],{"class":89,"line":246},[87,9755,9756],{"class":93},"    Pages",[87,9758,5676],{"class":97},[87,9760,9761,9764,9766,9768,9770,9772,9775],{"class":89,"line":256},[87,9762,9763],{"class":93},"      onRoutesGenerated",[87,9765,108],{"class":97},[87,9767,9635],{"class":1168},[87,9769,4307],{"class":562},[87,9771,2658],{"class":97},[87,9773,9774],{"class":93},"generateSitemap",[87,9776,9777],{"class":97},"({ routes })),\n",[87,9779,9780],{"class":89,"line":270},[87,9781,5739],{"class":97},[87,9783,9784],{"class":89,"line":280},[87,9785,9786],{"class":97},"  ],\n",[87,9788,9789],{"class":89,"line":295},[87,9790,133],{"class":97},[16,9792,9793],{},[513,9794],{"alt":83,"src":9795},"blog\u002Fimg\u002Fvite-vue-and-github-pages\u002Fgit_blog.png",[16,9797,9798,9800,9801,9803,9804,9807],{},[27,9799,3737],{},"  를 하면 바로 사이트맵과 로보츠 파일이 만들어진다. 기본 사용법대로 사이트맵 파일을 만들면 주소가 ",[27,9802,6914],{},"  로 되는데, ",[27,9805,9806],{},"hostname","  을 변경해서 내 사이트 주소를 설정해주어야 한다.",[45,9809,9697],{"id":9697},[1718,9811,9812],{},[16,9813,9697,9814,9816],{},[9517,9815],{},[2943,9817,9818],{"href":9818,"rel":9819},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvite-plugin-pages",[2947],[16,9821,9822],{},"파일 시스템 기반의 라우트를 생성한다.",[78,9824,9827],{"className":9825,"code":9826,"language":1095},[1093],"npm install -D vite-plugin-pages\n\n",[27,9828,9826],{"__ignoreMap":83},[16,9830,9831],{},"이녀석도 vue-router 가 필요한데, 위에서 설치했기 때문에 생략한다.",[78,9833,9835],{"className":1150,"code":9834,"language":1152,"meta":83,"style":83},"\u002F\u002F vite.config.ts\nimport Pages from 'vite-plugin-pages';\nexport default defineConfig({\n  plugins: [\n    Pages({\n      extensions: ['vue', 'md'],\n      pagesDir: 'src\u002Fpages',\n      \u002F**\n       * 생성된 라우트에 추가 로직을 작성한다.\n       * @param route 생성된 라우트\n       * @returns \n       *\u002F\n      extendRoute(route) {\n        console.log('route', route);\n        return route;\n      }\n    }),\n  ],\n})\n\n",[27,9836,9837,9842,9855,9866,9870,9876,9891,9901,9906,9911,9924,9934,9939,9950,9965,9972,9976,9980,9984],{"__ignoreMap":83},[87,9838,9839],{"class":89,"line":90},[87,9840,9841],{"class":1339},"\u002F\u002F vite.config.ts\n",[87,9843,9844,9846,9848,9850,9853],{"class":89,"line":101},[87,9845,2495],{"class":562},[87,9847,9716],{"class":97},[87,9849,2501],{"class":562},[87,9851,9852],{"class":165}," 'vite-plugin-pages'",[87,9854,114],{"class":97},[87,9856,9857,9859,9861,9864],{"class":89,"line":117},[87,9858,2570],{"class":562},[87,9860,4575],{"class":562},[87,9862,9863],{"class":93}," defineConfig",[87,9865,5676],{"class":97},[87,9867,9868],{"class":89,"line":130},[87,9869,9746],{"class":97},[87,9871,9872,9874],{"class":89,"line":224},[87,9873,9756],{"class":93},[87,9875,5676],{"class":97},[87,9877,9878,9881,9884,9886,9889],{"class":89,"line":246},[87,9879,9880],{"class":97},"      extensions: [",[87,9882,9883],{"class":165},"'vue'",[87,9885,60],{"class":97},[87,9887,9888],{"class":165},"'md'",[87,9890,7746],{"class":97},[87,9892,9893,9896,9899],{"class":89,"line":256},[87,9894,9895],{"class":97},"      pagesDir: ",[87,9897,9898],{"class":165},"'src\u002Fpages'",[87,9900,3413],{"class":97},[87,9902,9903],{"class":89,"line":270},[87,9904,9905],{"class":1339},"      \u002F**\n",[87,9907,9908],{"class":89,"line":280},[87,9909,9910],{"class":1339},"       * 생성된 라우트에 추가 로직을 작성한다.\n",[87,9912,9913,9916,9919,9921],{"class":89,"line":295},[87,9914,9915],{"class":1339},"       * ",[87,9917,9918],{"class":562},"@param",[87,9920,4727],{"class":97},[87,9922,9923],{"class":1339}," 생성된 라우트\n",[87,9925,9926,9928,9931],{"class":89,"line":315},[87,9927,9915],{"class":1339},[87,9929,9930],{"class":562},"@returns",[87,9932,9933],{"class":1339}," \n",[87,9935,9936],{"class":89,"line":330},[87,9937,9938],{"class":1339},"       *\u002F\n",[87,9940,9941,9944,9946,9948],{"class":89,"line":351},[87,9942,9943],{"class":93},"      extendRoute",[87,9945,791],{"class":97},[87,9947,6584],{"class":1168},[87,9949,4409],{"class":97},[87,9951,9952,9955,9957,9959,9962],{"class":89,"line":360},[87,9953,9954],{"class":97},"        console.",[87,9956,1221],{"class":93},[87,9958,791],{"class":97},[87,9960,9961],{"class":165},"'route'",[87,9963,9964],{"class":97},", route);\n",[87,9966,9967,9969],{"class":89,"line":374},[87,9968,4314],{"class":562},[87,9970,9971],{"class":97}," route;\n",[87,9973,9974],{"class":89,"line":383},[87,9975,5841],{"class":97},[87,9977,9978],{"class":89,"line":398},[87,9979,5739],{"class":97},[87,9981,9982],{"class":89,"line":418},[87,9983,9786],{"class":97},[87,9985,9986],{"class":89,"line":434},[87,9987,5107],{"class":97},[16,9989,9990,9993],{},[27,9991,9992],{},"Pages","  플러그인을 설정파일에 추가하자. md 파일이 있는 경로와, 확장자를 추가해주면 된다.",[753,9995,9997],{"id":9996},"파일-추가하기","파일 추가하기",[16,9999,10000,10002,10003,10006,10007,10010],{},[27,10001,9992],{},"  플러그인에 등록해둔 경로에 ",[27,10004,10005],{},".vue","  혹은 ",[27,10008,10009],{},".md","  파일을 만들면 자동으로 라우트로 인식한다. 기본적으로 만들어야 할 파일이 두가지 있다.",[20,10012,10013,10019],{},[23,10014,10015,10018],{},[788,10016,10017],{},"[...all].vue →"," 접속한 페이지에 해당하는 라우트가 없을 때 띄워줄 404 페이지",[23,10020,10021,10024],{},[788,10022,10023],{},"index.vue"," → 루트 페이지",[16,10026,10027],{},"나머지는 파일을 만드는대로 라우트에 등록된다.",[753,10029,10031],{"id":10030},"nested-routes","nested routes",[1718,10033,10034],{},[16,10035,10036,10037,10039],{},"Nested Routes",[9517,10038],{},[2943,10040,10041],{"href":10041,"rel":10042},"https:\u002F\u002Fgithub.com\u002Fhannoeru\u002Fvite-plugin-pages#nested-routes",[2947],[16,10044,10045],{},"메인 페이지와 포스트 페이지의 디자인을 완전히 분리하고 nested routes 로 관리할 것이다.",[16,10047,10048],{},[513,10049],{"alt":83,"src":10050},"blog\u002Fimg\u002Fvite-vue-and-github-pages\u002Fgit_blog%202.png",[78,10052,10054],{"className":1150,"code":10053,"language":1152,"meta":83,"style":83},"route {\n  path: '\u002Fposts',\n  component: '\u002Fsrc\u002Fpages\u002Fposts.vue',\n  customBlock: undefined,\n  children: [\n    {\n      name: 'posts-post2',\n      path: 'post2',\n      component: '\u002Fsrc\u002Fpages\u002Fposts\u002Fpost2.md',\n      customBlock: undefined,\n      props: true\n    },\n    \u002F\u002F ....\n  ],\n  props: true\n}\n\n",[27,10055,10056,10061,10073,10085,10097,10105,10110,10120,10130,10140,10149,10156,10160,10165,10169,10178],{"__ignoreMap":83},[87,10057,10058],{"class":89,"line":90},[87,10059,10060],{"class":97},"route {\n",[87,10062,10063,10066,10068,10071],{"class":89,"line":101},[87,10064,10065],{"class":93},"  path",[87,10067,108],{"class":97},[87,10069,10070],{"class":165},"'\u002Fposts'",[87,10072,3413],{"class":97},[87,10074,10075,10078,10080,10083],{"class":89,"line":117},[87,10076,10077],{"class":93},"  component",[87,10079,108],{"class":97},[87,10081,10082],{"class":165},"'\u002Fsrc\u002Fpages\u002Fposts.vue'",[87,10084,3413],{"class":97},[87,10086,10087,10090,10092,10095],{"class":89,"line":130},[87,10088,10089],{"class":93},"  customBlock",[87,10091,108],{"class":97},[87,10093,10094],{"class":104},"undefined",[87,10096,3413],{"class":97},[87,10098,10099,10102],{"class":89,"line":224},[87,10100,10101],{"class":93},"  children",[87,10103,10104],{"class":97},": [\n",[87,10106,10107],{"class":89,"line":246},[87,10108,10109],{"class":97},"    {\n",[87,10111,10112,10115,10118],{"class":89,"line":256},[87,10113,10114],{"class":97},"      name: ",[87,10116,10117],{"class":165},"'posts-post2'",[87,10119,3413],{"class":97},[87,10121,10122,10125,10128],{"class":89,"line":270},[87,10123,10124],{"class":97},"      path: ",[87,10126,10127],{"class":165},"'post2'",[87,10129,3413],{"class":97},[87,10131,10132,10135,10138],{"class":89,"line":280},[87,10133,10134],{"class":97},"      component: ",[87,10136,10137],{"class":165},"'\u002Fsrc\u002Fpages\u002Fposts\u002Fpost2.md'",[87,10139,3413],{"class":97},[87,10141,10142,10145,10147],{"class":89,"line":295},[87,10143,10144],{"class":97},"      customBlock: ",[87,10146,10094],{"class":104},[87,10148,3413],{"class":97},[87,10150,10151,10154],{"class":89,"line":315},[87,10152,10153],{"class":97},"      props: ",[87,10155,3711],{"class":104},[87,10157,10158],{"class":89,"line":330},[87,10159,5846],{"class":97},[87,10161,10162],{"class":89,"line":351},[87,10163,10164],{"class":1339},"    \u002F\u002F ....\n",[87,10166,10167],{"class":89,"line":360},[87,10168,9786],{"class":97},[87,10170,10171,10174,10176],{"class":89,"line":374},[87,10172,10173],{"class":93},"  props",[87,10175,108],{"class":97},[87,10177,3711],{"class":104},[87,10179,10180],{"class":89,"line":383},[87,10181,133],{"class":97},[16,10183,10184,10185,10188],{},"\u002Fpages 폴더 아레, ",[27,10186,10187],{},"posts","  로 폴더와 vue 파일을 하나씩 만든다. 그럼 자동으로 nested routes 로 인식한다.",[78,10190,10192],{"className":1930,"code":10191,"language":1932,"meta":83,"style":83},"\u003Cscript setup>\u003C\u002Fscript>\n\u003Ctemplate>\n  \u003Cdiv class=\"post-index-wrapper\">\n    \u003Crouter-view>\u003C\u002Frouter-view>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle>\n\u003C\u002Fstyle>\n\n",[27,10193,10194,10208,10216,10231,10244,10252,10260,10264,10272],{"__ignoreMap":83},[87,10195,10196,10198,10200,10202,10204,10206],{"class":89,"line":90},[87,10197,152],{"class":97},[87,10199,1941],{"class":155},[87,10201,1944],{"class":93},[87,10203,5037],{"class":97},[87,10205,1941],{"class":155},[87,10207,169],{"class":97},[87,10209,10210,10212,10214],{"class":89,"line":101},[87,10211,152],{"class":97},[87,10213,2114],{"class":155},[87,10215,169],{"class":97},[87,10217,10218,10220,10222,10224,10226,10229],{"class":89,"line":117},[87,10219,2121],{"class":97},[87,10221,156],{"class":155},[87,10223,159],{"class":93},[87,10225,162],{"class":97},[87,10227,10228],{"class":165},"\"post-index-wrapper\"",[87,10230,169],{"class":97},[87,10232,10233,10235,10238,10240,10242],{"class":89,"line":130},[87,10234,174],{"class":97},[87,10236,10237],{"class":155},"router-view",[87,10239,5037],{"class":97},[87,10241,10237],{"class":155},[87,10243,169],{"class":97},[87,10245,10246,10248,10250],{"class":89,"line":224},[87,10247,7992],{"class":97},[87,10249,156],{"class":155},[87,10251,169],{"class":97},[87,10253,10254,10256,10258],{"class":89,"line":246},[87,10255,489],{"class":97},[87,10257,2114],{"class":155},[87,10259,169],{"class":97},[87,10261,10262],{"class":89,"line":256},[87,10263,1446],{"emptyLinePlaceholder":842},[87,10265,10266,10268,10270],{"class":89,"line":270},[87,10267,152],{"class":97},[87,10269,825],{"class":155},[87,10271,169],{"class":97},[87,10273,10274,10276,10278],{"class":89,"line":280},[87,10275,489],{"class":97},[87,10277,825],{"class":155},[87,10279,169],{"class":97},[16,10281,10282,10285,10286,10288],{},[27,10283,10284],{},"posts.vue","  파일에는 ",[27,10287,10237],{},"  를 사용하면 끝!",[45,10290,10291],{"id":10291},"vite-plugin-vue-layouts",[1718,10293,10294],{},[16,10295,10291,10296,10298],{},[9517,10297],{},[2943,10299,10300],{"href":10300,"rel":10301},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvite-plugin-vue-layouts",[2947],[16,10303,10304],{},"vite-plugin-pages 와 세트이다. main 에서 createApp 을 할 때, 라우트를 인식시키기 위함이다. 무조건 필요한지는 잘 모르겠다. 더 알아봐야 함.",[78,10306,10309],{"className":10307,"code":10308,"language":1095},[1093],"npm install -D vite-plugin-vue-layouts\n",[27,10310,10308],{"__ignoreMap":83},[78,10312,10314],{"className":1150,"code":10313,"language":1152,"meta":83,"style":83},"\u002F\u002F vite.config.ts\nimport Layouts from 'vite-plugin-vue-layouts';\nexport default defineConfig({\n  plugins: [\n    Layouts(),\n  ]\n})\n",[27,10315,10316,10320,10334,10344,10348,10356,10361],{"__ignoreMap":83},[87,10317,10318],{"class":89,"line":90},[87,10319,9841],{"class":1339},[87,10321,10322,10324,10327,10329,10332],{"class":89,"line":101},[87,10323,2495],{"class":562},[87,10325,10326],{"class":97}," Layouts ",[87,10328,2501],{"class":562},[87,10330,10331],{"class":165}," 'vite-plugin-vue-layouts'",[87,10333,114],{"class":97},[87,10335,10336,10338,10340,10342],{"class":89,"line":117},[87,10337,2570],{"class":562},[87,10339,4575],{"class":562},[87,10341,9863],{"class":93},[87,10343,5676],{"class":97},[87,10345,10346],{"class":89,"line":130},[87,10347,9746],{"class":97},[87,10349,10350,10353],{"class":89,"line":224},[87,10351,10352],{"class":93},"    Layouts",[87,10354,10355],{"class":97},"(),\n",[87,10357,10358],{"class":89,"line":246},[87,10359,10360],{"class":97},"  ]\n",[87,10362,10363],{"class":89,"line":256},[87,10364,5107],{"class":97},[16,10366,10367,10368,10371],{},"vite 설정에 ",[27,10369,10370],{},"Layouts","  추가하기",[78,10373,10375],{"className":7571,"code":10374,"language":7573,"meta":83,"style":83},"\u002F\u002F tsconfig.json\n\"compileOptions\": {\n    \"types\": [\n      \"vite\u002Fclient\", \n      \"vite-plugin-pages\u002Fclient\", \n      \"vite-plugin-vue-layouts\u002Fclient\", \n      \"node\"\n    ],\n}\n\n",[27,10376,10377,10382,10389,10396,10404,10411,10418,10423,10428],{"__ignoreMap":83},[87,10378,10379],{"class":89,"line":90},[87,10380,10381],{"class":1339},"\u002F\u002F tsconfig.json\n",[87,10383,10384,10387],{"class":89,"line":101},[87,10385,10386],{"class":165},"\"compileOptions\"",[87,10388,7583],{"class":97},[87,10390,10391,10394],{"class":89,"line":117},[87,10392,10393],{"class":104},"    \"types\"",[87,10395,10104],{"class":97},[87,10397,10398,10401],{"class":89,"line":130},[87,10399,10400],{"class":165},"      \"vite\u002Fclient\"",[87,10402,10403],{"class":97},", \n",[87,10405,10406,10409],{"class":89,"line":224},[87,10407,10408],{"class":165},"      \"vite-plugin-pages\u002Fclient\"",[87,10410,10403],{"class":97},[87,10412,10413,10416],{"class":89,"line":246},[87,10414,10415],{"class":165},"      \"vite-plugin-vue-layouts\u002Fclient\"",[87,10417,10403],{"class":97},[87,10419,10420],{"class":89,"line":256},[87,10421,10422],{"class":165},"      \"node\"\n",[87,10424,10425],{"class":89,"line":270},[87,10426,10427],{"class":97},"    ],\n",[87,10429,10430],{"class":89,"line":280},[87,10431,133],{"class":97},[16,10433,10434,10437],{},[27,10435,10436],{},"virtual","  import 를 인식하기 위해서 types 항목을 추가한다.",[78,10439,10441],{"className":1150,"code":10440,"language":1152,"meta":83,"style":83},"\u002F\u002F main.ts\n\u002F\u002F virtual:generated-layouts\n\u002F\u002F   tsconfig.json-compilerOptions-types\n\u002F\u002F   \"vite-plugin-vue-layouts\u002Fclient\"\nimport { setupLayouts } from 'virtual:generated-layouts'\n\u002F\u002F virtual:generated-pages\n\u002F\u002F   tsconfig.json-compilerOptions-types\n\u002F\u002F   \"vite-plugin-pages\u002Fclient\"\nimport generatedRoutes from 'virtual:generated-pages'\n\nconst routes = setupLayouts(generatedRoutes)\nexport const createApp = ViteSSG(\n    \u002F\u002F 루트 컴포넌트\n    App,\n    { routes },\n    ({ app, router, routes, isClient, initialState }) => {\n        \u002F\u002F 플러그인 세팅\n    },\n)\n",[27,10442,10443,10447,10452,10457,10462,10474,10479,10483,10488,10500,10504,10518,10532,10536,10540,10545,10573,10577,10581],{"__ignoreMap":83},[87,10444,10445],{"class":89,"line":90},[87,10446,7266],{"class":1339},[87,10448,10449],{"class":89,"line":101},[87,10450,10451],{"class":1339},"\u002F\u002F virtual:generated-layouts\n",[87,10453,10454],{"class":89,"line":117},[87,10455,10456],{"class":1339},"\u002F\u002F   tsconfig.json-compilerOptions-types\n",[87,10458,10459],{"class":89,"line":130},[87,10460,10461],{"class":1339},"\u002F\u002F   \"vite-plugin-vue-layouts\u002Fclient\"\n",[87,10463,10464,10466,10469,10471],{"class":89,"line":224},[87,10465,2495],{"class":562},[87,10467,10468],{"class":97}," { setupLayouts } ",[87,10470,2501],{"class":562},[87,10472,10473],{"class":165}," 'virtual:generated-layouts'\n",[87,10475,10476],{"class":89,"line":246},[87,10477,10478],{"class":1339},"\u002F\u002F virtual:generated-pages\n",[87,10480,10481],{"class":89,"line":256},[87,10482,10456],{"class":1339},[87,10484,10485],{"class":89,"line":270},[87,10486,10487],{"class":1339},"\u002F\u002F   \"vite-plugin-pages\u002Fclient\"\n",[87,10489,10490,10492,10495,10497],{"class":89,"line":280},[87,10491,2495],{"class":562},[87,10493,10494],{"class":97}," generatedRoutes ",[87,10496,2501],{"class":562},[87,10498,10499],{"class":165}," 'virtual:generated-pages'\n",[87,10501,10502],{"class":89,"line":295},[87,10503,1446],{"emptyLinePlaceholder":842},[87,10505,10506,10508,10510,10512,10515],{"class":89,"line":315},[87,10507,1964],{"class":562},[87,10509,5548],{"class":104},[87,10511,1362],{"class":562},[87,10513,10514],{"class":93}," setupLayouts",[87,10516,10517],{"class":97},"(generatedRoutes)\n",[87,10519,10520,10522,10524,10526,10528,10530],{"class":89,"line":330},[87,10521,2570],{"class":562},[87,10523,7815],{"class":562},[87,10525,7354],{"class":104},[87,10527,1362],{"class":562},[87,10529,9599],{"class":93},[87,10531,9602],{"class":97},[87,10533,10534],{"class":89,"line":351},[87,10535,9607],{"class":1339},[87,10537,10538],{"class":89,"line":360},[87,10539,9612],{"class":97},[87,10541,10542],{"class":89,"line":374},[87,10543,10544],{"class":97},"    { routes },\n",[87,10546,10547,10549,10551,10553,10555,10557,10559,10561,10563,10565,10567,10569,10571],{"class":89,"line":383},[87,10548,9622],{"class":97},[87,10550,9625],{"class":1168},[87,10552,60],{"class":97},[87,10554,9630],{"class":1168},[87,10556,60],{"class":97},[87,10558,9635],{"class":1168},[87,10560,60],{"class":97},[87,10562,9640],{"class":1168},[87,10564,60],{"class":97},[87,10566,9645],{"class":1168},[87,10568,9648],{"class":97},[87,10570,1175],{"class":562},[87,10572,98],{"class":97},[87,10574,10575],{"class":89,"line":398},[87,10576,9657],{"class":1339},[87,10578,10579],{"class":89,"line":418},[87,10580,5846],{"class":97},[87,10582,10583],{"class":89,"line":434},[87,10584,5501],{"class":97},[16,10586,10587],{},"pages 플러그인이 셍성한 라우트를 main 에서 적용시킨다.",[45,10589,10590],{"id":10590},"vite-plugin-md",[1718,10592,10593],{},[16,10594,10590,10595,10597],{},[9517,10596],{},[2943,10598,10599],{"href":10599,"rel":10600},"https:\u002F\u002Fgithub.com\u002Fantfu\u002Fvite-plugin-md",[2947],[16,10602,10603],{},"마크다운을 뷰 컴포넌트로 만들어준다.",[78,10605,10608],{"className":10606,"code":10607,"language":1095},[1093],"npm i vite-plugin-md -D\n",[27,10609,10607],{"__ignoreMap":83},[78,10611,10613],{"className":1150,"code":10612,"language":1152,"meta":83,"style":83},"import Markdown from 'vite-plugin-md'\n\nexport default defineConfig({\n  plugins: [\n    vue({\n      include: [\u002F\\.vue$\u002F, \u002F\\.md$\u002F], \u002F\u002F 마크다운 파일도 인식하기\n    }),\n    Markdown(),\n  ],\n})\n\n",[27,10614,10615,10627,10631,10641,10645,10652,10690,10694,10701,10705],{"__ignoreMap":83},[87,10616,10617,10619,10622,10624],{"class":89,"line":90},[87,10618,2495],{"class":562},[87,10620,10621],{"class":97}," Markdown ",[87,10623,2501],{"class":562},[87,10625,10626],{"class":165}," 'vite-plugin-md'\n",[87,10628,10629],{"class":89,"line":101},[87,10630,1446],{"emptyLinePlaceholder":842},[87,10632,10633,10635,10637,10639],{"class":89,"line":117},[87,10634,2570],{"class":562},[87,10636,4575],{"class":562},[87,10638,9863],{"class":93},[87,10640,5676],{"class":97},[87,10642,10643],{"class":89,"line":130},[87,10644,9746],{"class":97},[87,10646,10647,10650],{"class":89,"line":224},[87,10648,10649],{"class":93},"    vue",[87,10651,5676],{"class":97},[87,10653,10654,10657,10659,10663,10665,10668,10670,10673,10676,10678,10680,10682,10684,10687],{"class":89,"line":246},[87,10655,10656],{"class":97},"      include: [",[87,10658,6496],{"class":165},[87,10660,10662],{"class":10661},"snhLl","\\.",[87,10664,1932],{"class":6499},[87,10666,10667],{"class":562},"$",[87,10669,6496],{"class":165},[87,10671,10672],{"class":97},",",[87,10674,10675],{"class":165}," \u002F",[87,10677,10662],{"class":10661},[87,10679,839],{"class":6499},[87,10681,10667],{"class":562},[87,10683,6496],{"class":165},[87,10685,10686],{"class":97},"], ",[87,10688,10689],{"class":1339},"\u002F\u002F 마크다운 파일도 인식하기\n",[87,10691,10692],{"class":89,"line":256},[87,10693,5739],{"class":97},[87,10695,10696,10699],{"class":89,"line":270},[87,10697,10698],{"class":93},"    Markdown",[87,10700,10355],{"class":97},[87,10702,10703],{"class":89,"line":280},[87,10704,9786],{"class":97},[87,10706,10707],{"class":89,"line":295},[87,10708,5107],{"class":97},[16,10710,10711,10712,10714,10715,10717,10718,10720],{},"기본적으로 있던 ",[27,10713,1932],{},"  플러그인에 ",[27,10716,839],{},"  파일도 인식할 수 있게 항목을 추가해주고, ",[27,10719,7532],{},"  플로그인을 추가한다.",[753,10722,10724],{"id":10723},"메타태그-추가","메타태그 추가",[16,10726,10727],{},[2943,10728,10731],{"href":10729,"rel":10730},"https:\u002F\u002Fgithub.com\u002Fmdit-vue\u002Fvite-plugin-vue-markdown#document-head-and-meta",[2947],"GitHub - mdit-vue\u002Fvite-plugin-vue-markdown: Compile Markdown to Vue component",[16,10733,10734,10735,10737,10738,10740,10741,10743,10744,10747,10748,10751,10752,10754],{},"페이지별로 메타태그를 추가하고싶은데, ",[27,10736,10009],{}," 파일로는 어떻게 해줘야 할까?\n",[27,10739,9510],{}," 와 ",[27,10742,10590],{}," 에서 이미 ",[27,10745,10746],{},"vueuse\u002Fhead"," 를 사용한다. 그래서 ",[27,10749,10750],{},"unhead\u002Fvue"," 를 사용해보려고 했지만, 충돌 문제가 있어서 ",[27,10753,10746],{}," 의 방식을 따르기로 했다.",[78,10756,10759],{"className":10757,"code":10758,"language":839,"meta":83,"style":83},"language-md shiki shiki-themes github-light github-dark","---\nmeta:\n  - name: My Cool App\n    description: cool things happen to people who use cool apps\n---\n",[27,10760,10761,10766,10772,10784,10793],{"__ignoreMap":83},[87,10762,10763],{"class":89,"line":90},[87,10764,10765],{"class":97},"---\n",[87,10767,10768,10770],{"class":89,"line":101},[87,10769,6245],{"class":155},[87,10771,3459],{"class":97},[87,10773,10774,10777,10779,10781],{"class":89,"line":117},[87,10775,10776],{"class":97},"  - ",[87,10778,1234],{"class":155},[87,10780,108],{"class":97},[87,10782,10783],{"class":165},"My Cool App\n",[87,10785,10786,10788,10790],{"class":89,"line":130},[87,10787,4077],{"class":155},[87,10789,108],{"class":97},[87,10791,10792],{"class":165},"cool things happen to people who use cool apps\n",[87,10794,10795],{"class":89,"line":224},[87,10796,10765],{"class":97},[16,10798,10799,10800,10803],{},"md 파일의 최상단에 ",[27,10801,10802],{},"---"," 로 구분선을 위아래로 넣어주고, 안에 내용을 넣어준다.",[78,10805,10807],{"className":10757,"code":10806,"language":839,"meta":83,"style":83},"---\nmeta:\n  - name: description\n    content: Github.io 와 Vue3 를 활용해서 블로그를 시작. SPA를 prerendering 하여 개별 페이지를 생성하고, 메타태그를 추가. 구글 검색 엔진에 등록할 사이트맵 자동 생성. 마크다운을 html 로 변환. Google Analytics 도입으로 방문자 수 체크.\n---\n",[27,10808,10809,10813,10819,10830,10840],{"__ignoreMap":83},[87,10810,10811],{"class":89,"line":90},[87,10812,10765],{"class":97},[87,10814,10815,10817],{"class":89,"line":101},[87,10816,6245],{"class":155},[87,10818,3459],{"class":97},[87,10820,10821,10823,10825,10827],{"class":89,"line":117},[87,10822,10776],{"class":97},[87,10824,1234],{"class":155},[87,10826,108],{"class":97},[87,10828,10829],{"class":165},"description\n",[87,10831,10832,10835,10837],{"class":89,"line":130},[87,10833,10834],{"class":155},"    content",[87,10836,108],{"class":97},[87,10838,10839],{"class":165},"Github.io 와 Vue3 를 활용해서 블로그를 시작. SPA를 prerendering 하여 개별 페이지를 생성하고, 메타태그를 추가. 구글 검색 엔진에 등록할 사이트맵 자동 생성. 마크다운을 html 로 변환. Google Analytics 도입으로 방문자 수 체크.\n",[87,10841,10842],{"class":89,"line":224},[87,10843,10765],{"class":97},[16,10845,10846],{},"이런 형식도 된다.",[78,10848,10850],{"className":143,"code":10849,"language":145,"meta":83,"style":83},"\u003Ctitle>Github.io + Vue3 로 블로그 만들기\u003C\u002Ftitle>\n\u003Cmeta name=\"description\"\ncontent=\"Github.io 와 Vue3 를 활용해서 블로그를 시작. SPA를 prerendering 하여 개별 페이지를 생성하고, 메타태그를 추가. 구글 검색 엔진에 등록할 사이트맵 자동 생성. 마크다운을 html 로 변환. Google Analytics 도입으로 방문자 수 체크.\">\n\u003Cmeta property=\"og:title\" content=\"Github.io + Vue3 로 블로그 만들기\">\n",[27,10851,10852,10865,10878,10889],{"__ignoreMap":83},[87,10853,10854,10856,10858,10861,10863],{"class":89,"line":90},[87,10855,152],{"class":97},[87,10857,6452],{"class":155},[87,10859,10860],{"class":97},">Github.io + Vue3 로 블로그 만들기\u003C\u002F",[87,10862,6452],{"class":155},[87,10864,169],{"class":97},[87,10866,10867,10869,10871,10873,10875],{"class":89,"line":101},[87,10868,152],{"class":97},[87,10870,6245],{"class":155},[87,10872,6248],{"class":93},[87,10874,162],{"class":97},[87,10876,10877],{"class":165},"\"description\"\n",[87,10879,10880,10882,10884,10887],{"class":89,"line":117},[87,10881,804],{"class":93},[87,10883,162],{"class":97},[87,10885,10886],{"class":165},"\"Github.io 와 Vue3 를 활용해서 블로그를 시작. SPA를 prerendering 하여 개별 페이지를 생성하고, 메타태그를 추가. 구글 검색 엔진에 등록할 사이트맵 자동 생성. 마크다운을 html 로 변환. Google Analytics 도입으로 방문자 수 체크.\"",[87,10888,169],{"class":97},[87,10890,10891,10893,10895,10898,10900,10903,10905,10907,10910],{"class":89,"line":130},[87,10892,152],{"class":97},[87,10894,6245],{"class":155},[87,10896,10897],{"class":93}," property",[87,10899,162],{"class":97},[87,10901,10902],{"class":165},"\"og:title\"",[87,10904,6256],{"class":93},[87,10906,162],{"class":97},[87,10908,10909],{"class":165},"\"Github.io + Vue3 로 블로그 만들기\"",[87,10911,169],{"class":97},[16,10913,10914,10915,10917],{},"html 파일에 자동으로 ",[27,10916,6245],{}," 태그가 추가된다.",[45,10919,10921],{"id":10920},"markdown-it-플러그인","markdown-it 플러그인",[753,10923,10924],{"id":10924},"markdown-it-anchor",[1718,10926,10927],{},[16,10928,10924,10929,10931],{},[9517,10930],{},[2943,10932,10933],{"href":10933,"rel":10934},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmarkdown-it-anchor",[2947],[16,10936,10937,10940],{},[27,10938,10939],{},"H","  태그에 기본적으로 anchor 를 달아서 이동할 수 있게 한다.  한글도 지원한다.",[753,10942,10943],{"id":10943},"markdown-it-table-of-contents",[1718,10945,10946],{},[16,10947,10943,10948,10950],{},[9517,10949],{},[2943,10951,10952],{"href":10952,"rel":10953},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmarkdown-it-table-of-contents",[2947],[16,10955,10956,10959],{},[27,10957,10958],{},"[[toc]]","  를 사용하면 목차를 만들 수 있다.",[767,10961,10963],{"id":10962},"목차-요소를-따로-떼서-다른-곳에-붙이기","목차 요소를 따로 떼서 다른 곳에 붙이기",[16,10965,10966],{},"목차가 md 페이지 컴포넌트의 상단에 고정되어 있어서 원하는 곳에서 사용할 수 없다. 예를 들어, 오른쪽 부분에 고정을 시킨다던지.",[78,10968,10970],{"className":1150,"code":10969,"language":1152,"meta":83,"style":83},"onMounted(() => {\n  \u002F\u002F 목차를 markdown-body 내부에서 제거한 후, \n  \u002F\u002F 옆에 붙인다.\n  const mdBody = document.querySelector(\".markdown-body\");\n  const targetToc = document.querySelector(\".new-table-of-contents\");\n  const toc = document.querySelector(\".markdown-body .table-of-contents\");\n  const main = document.querySelector(\"main\");\n\n  if(mdBody && targetToc && toc && main) {\n    targetToc.innerHTML = toc?.innerHTML!;\n    \u002F\u002F 라우트 변경 시 `.table-of-contents` 의 innerHTML 이 한번 제거되고 undefined 로 뜨기 때문에\n    \u002F\u002F display: none 만 붙여서 숨긴다.\n    (toc as HTMLElement)?.style.setProperty(\"display\", \"none\");\n  }\n})\n",[27,10971,10972,10982,10987,10992,11012,11032,11052,11072,11076,11099,11113,11118,11123,11152,11156],{"__ignoreMap":83},[87,10973,10974,10976,10978,10980],{"class":89,"line":90},[87,10975,2081],{"class":93},[87,10977,2084],{"class":97},[87,10979,1175],{"class":562},[87,10981,98],{"class":97},[87,10983,10984],{"class":89,"line":101},[87,10985,10986],{"class":1339},"  \u002F\u002F 목차를 markdown-body 내부에서 제거한 후, \n",[87,10988,10989],{"class":89,"line":117},[87,10990,10991],{"class":1339},"  \u002F\u002F 옆에 붙인다.\n",[87,10993,10994,10996,10999,11001,11003,11005,11007,11010],{"class":89,"line":130},[87,10995,2637],{"class":562},[87,10997,10998],{"class":104}," mdBody",[87,11000,1362],{"class":562},[87,11002,9220],{"class":97},[87,11004,9223],{"class":93},[87,11006,791],{"class":97},[87,11008,11009],{"class":165},"\".markdown-body\"",[87,11011,1590],{"class":97},[87,11013,11014,11016,11019,11021,11023,11025,11027,11030],{"class":89,"line":224},[87,11015,2637],{"class":562},[87,11017,11018],{"class":104}," targetToc",[87,11020,1362],{"class":562},[87,11022,9220],{"class":97},[87,11024,9223],{"class":93},[87,11026,791],{"class":97},[87,11028,11029],{"class":165},"\".new-table-of-contents\"",[87,11031,1590],{"class":97},[87,11033,11034,11036,11039,11041,11043,11045,11047,11050],{"class":89,"line":246},[87,11035,2637],{"class":562},[87,11037,11038],{"class":104}," toc",[87,11040,1362],{"class":562},[87,11042,9220],{"class":97},[87,11044,9223],{"class":93},[87,11046,791],{"class":97},[87,11048,11049],{"class":165},"\".markdown-body .table-of-contents\"",[87,11051,1590],{"class":97},[87,11053,11054,11056,11059,11061,11063,11065,11067,11070],{"class":89,"line":256},[87,11055,2637],{"class":562},[87,11057,11058],{"class":104}," main",[87,11060,1362],{"class":562},[87,11062,9220],{"class":97},[87,11064,9223],{"class":93},[87,11066,791],{"class":97},[87,11068,11069],{"class":165},"\"main\"",[87,11071,1590],{"class":97},[87,11073,11074],{"class":89,"line":270},[87,11075,1446],{"emptyLinePlaceholder":842},[87,11077,11078,11080,11083,11086,11089,11091,11094,11096],{"class":89,"line":280},[87,11079,2435],{"class":562},[87,11081,11082],{"class":97},"(mdBody ",[87,11084,11085],{"class":562},"&&",[87,11087,11088],{"class":97}," targetToc ",[87,11090,11085],{"class":562},[87,11092,11093],{"class":97}," toc ",[87,11095,11085],{"class":562},[87,11097,11098],{"class":97}," main) {\n",[87,11100,11101,11104,11106,11109,11111],{"class":89,"line":295},[87,11102,11103],{"class":97},"    targetToc.innerHTML ",[87,11105,162],{"class":562},[87,11107,11108],{"class":97}," toc?.innerHTML",[87,11110,2661],{"class":562},[87,11112,114],{"class":97},[87,11114,11115],{"class":89,"line":315},[87,11116,11117],{"class":1339},"    \u002F\u002F 라우트 변경 시 `.table-of-contents` 의 innerHTML 이 한번 제거되고 undefined 로 뜨기 때문에\n",[87,11119,11120],{"class":89,"line":330},[87,11121,11122],{"class":1339},"    \u002F\u002F display: none 만 붙여서 숨긴다.\n",[87,11124,11125,11128,11131,11134,11137,11140,11142,11145,11147,11150],{"class":89,"line":351},[87,11126,11127],{"class":97},"    (toc ",[87,11129,11130],{"class":562},"as",[87,11132,11133],{"class":93}," HTMLElement",[87,11135,11136],{"class":97},")?.style.",[87,11138,11139],{"class":93},"setProperty",[87,11141,791],{"class":97},[87,11143,11144],{"class":165},"\"display\"",[87,11146,60],{"class":97},[87,11148,11149],{"class":165},"\"none\"",[87,11151,1590],{"class":97},[87,11153,11154],{"class":89,"line":360},[87,11155,1694],{"class":97},[87,11157,11158],{"class":89,"line":374},[87,11159,5107],{"class":97},[16,11161,11162,11163,11166],{},"자바스크립트로 간단하게 ",[27,11164,11165],{},"innerHTML","  을 붙이려는 요소에 넣어주고, 해당 요소는 제거하면 된댜.",[1718,11168,11169],{},[16,11170,11171,11174,11175,11178,11179,11182,11183,11185,11186,11189],{},[87,11172,11173],{},"!attention","\n원래는  ",[27,11176,11177],{},"toc"," 를 remove 로 제거했으나, 그러면 라우트를 두번 이상 이동할 때, 이미 제거된 ",[27,11180,11181],{},".table-of-contents"," 에서 내용을 찾아 ",[27,11184,10094],{}," 를 얻게 된다. 라우트 시 toc 요소가 없으면 생성하지 못하는듯 하다.\n그래서 ",[27,11187,11188],{},"display: none;"," 속성만 주어서 보이지만 않게 한다.",[78,11191,11193],{"className":1150,"code":11192,"language":1152,"meta":83,"style":83},"watch(() => route.fullPath, () => {\n    \u002F\u002F SPA 라우트가 바뀔 때마다 toc 변경\n    nextTick(() => {\n        createToc();\n    });\n});\n",[27,11194,11195,11210,11215,11226,11233,11237],{"__ignoreMap":83},[87,11196,11197,11199,11201,11203,11206,11208],{"class":89,"line":90},[87,11198,1880],{"class":93},[87,11200,2084],{"class":97},[87,11202,1175],{"class":562},[87,11204,11205],{"class":97}," route.fullPath, () ",[87,11207,1175],{"class":562},[87,11209,98],{"class":97},[87,11211,11212],{"class":89,"line":101},[87,11213,11214],{"class":1339},"    \u002F\u002F SPA 라우트가 바뀔 때마다 toc 변경\n",[87,11216,11217,11220,11222,11224],{"class":89,"line":117},[87,11218,11219],{"class":93},"    nextTick",[87,11221,2084],{"class":97},[87,11223,1175],{"class":562},[87,11225,98],{"class":97},[87,11227,11228,11231],{"class":89,"line":130},[87,11229,11230],{"class":93},"        createToc",[87,11232,2026],{"class":97},[87,11234,11235],{"class":89,"line":224},[87,11236,4501],{"class":97},[87,11238,11239],{"class":89,"line":246},[87,11240,1299],{"class":97},[16,11242,11243,11244,11246],{},"라우트가 변경될 때마다 TOC를 옮겨주기 위해서 ",[27,11245,1880],{}," 를 추가한다.",[753,11248,11249],{"id":11249},"markdown-it-prism",[1718,11251,11252],{},[16,11253,11249,11254,11256],{},[9517,11255],{},[2943,11257,11258],{"href":11258,"rel":11259},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmarkdown-it-prism",[2947],[16,11261,11262,11265,11266,11268],{},[27,11263,11264],{},"prism","  라이브러를 래핑했다. 마크다운 파일 내의 소스코드를 위한 ",[27,11267,78],{},"  태그를 하이라이팅한다.",[78,11270,11272],{"className":1150,"code":11271,"language":1152,"meta":83,"style":83},"export default defineConfig({\n  plugins: [\n    Markdown({\n      markdownItSetup(md) {\n        \u002F\u002F prism 코드 하이라이터\n        md.use(MDPrism)\n      },\n    }),\n  ],\n})\n",[27,11273,11274,11284,11288,11294,11305,11310,11320,11325,11329,11333],{"__ignoreMap":83},[87,11275,11276,11278,11280,11282],{"class":89,"line":90},[87,11277,2570],{"class":562},[87,11279,4575],{"class":562},[87,11281,9863],{"class":93},[87,11283,5676],{"class":97},[87,11285,11286],{"class":89,"line":101},[87,11287,9746],{"class":97},[87,11289,11290,11292],{"class":89,"line":117},[87,11291,10698],{"class":93},[87,11293,5676],{"class":97},[87,11295,11296,11299,11301,11303],{"class":89,"line":130},[87,11297,11298],{"class":93},"      markdownItSetup",[87,11300,791],{"class":97},[87,11302,839],{"class":1168},[87,11304,4409],{"class":97},[87,11306,11307],{"class":89,"line":224},[87,11308,11309],{"class":1339},"        \u002F\u002F prism 코드 하이라이터\n",[87,11311,11312,11315,11317],{"class":89,"line":246},[87,11313,11314],{"class":97},"        md.",[87,11316,7364],{"class":93},[87,11318,11319],{"class":97},"(MDPrism)\n",[87,11321,11322],{"class":89,"line":256},[87,11323,11324],{"class":97},"      },\n",[87,11326,11327],{"class":89,"line":270},[87,11328,5739],{"class":97},[87,11330,11331],{"class":89,"line":280},[87,11332,9786],{"class":97},[87,11334,11335],{"class":89,"line":295},[87,11336,5107],{"class":97},[16,11338,11339,11342],{},[27,11340,11341],{},"markdownItSetup"," 함수에서 use 로 사용선언하면 자동으로 마크다운 코드 영역은 하이라이팅 된다. 클래스가 부여되기만 해서 css 는 직접 넣어주어야 한다.",[78,11344,11348],{"className":11345,"code":11346,"language":11347,"meta":83,"style":83},"language-scss shiki shiki-themes github-light github-dark","\u003Cstyle>\n@use \"..\u002Fassets\u002Fscss\u002Fprism-vscode.scss\"; \n\u003C\u002Fstyle>\n","scss",[27,11349,11350,11358,11369],{"__ignoreMap":83},[87,11351,11352,11354,11356],{"class":89,"line":90},[87,11353,152],{"class":97},[87,11355,825],{"class":155},[87,11357,169],{"class":97},[87,11359,11360,11363,11366],{"class":89,"line":101},[87,11361,11362],{"class":562},"@use",[87,11364,11365],{"class":165}," \"..\u002Fassets\u002Fscss\u002Fprism-vscode.scss\"",[87,11367,11368],{"class":97},"; \n",[87,11370,11371,11373,11375],{"class":89,"line":117},[87,11372,489],{"class":97},[87,11374,825],{"class":155},[87,11376,169],{"class":97},[16,11378,11379,11380,11383,11384,11387],{},"우리의 경우 포스트가 ",[27,11381,11382],{},"RouterView"," 에서 자동으로 페이지화되어 보여지기 때문에, ",[27,11385,11386],{},"scoped style"," 로는 스타일을 적용할 수 없다. 그래서 전역 css 로 선언해주어야 한다.",[767,11389,11390],{"id":11390},"테마",[16,11392,11393,11398],{},[2943,11394,11397],{"href":11395,"rel":11396},"https:\u002F\u002Fgithub.com\u002FPrismJS\u002Fprism-themes",[2947],"GitHub - PrismJS\u002Fprism-themes: A wider selection of Prism themes","\n여러가지 테마를 모아둔 깃헙이다. 가장 익숙한 vscode 를 사용했다.",[45,11400,11401],{"id":11401},"pinia",[1718,11403,11404],{},[16,11405,11401,11406,11408,11412,11414,11415,11417],{},[9517,11407],{},[2943,11409,11410],{"href":11410,"rel":11411},"https:\u002F\u002Fpinia.vuejs.org\u002Fgetting-started.html#installation",[2947],[9517,11413],{},"\nvite-ssg pinia",[9517,11416],{},[2943,11418,11419],{"href":11419,"rel":11420},"https:\u002F\u002Fgithub.com\u002Fantfu\u002Fvite-ssg#initial-state",[2947],[16,11422,11423],{},"포스트 목록을 관리하기 위해서 pinia 를 설치하자.",[78,11425,11428],{"className":11426,"code":11427,"language":1095},[1093],"npm install pinia\n",[27,11429,11427],{"__ignoreMap":83},[78,11431,11433],{"className":1150,"code":11432,"language":1152,"meta":83,"style":83},"\u002F\u002F main.ts\nexport const createApp = ViteSSG(\n  App,\n  { routes },\n  ({ app, router, initialState }) => {\n    const pinia = createPinia()\n    app.use(pinia)\n\n    if (import.meta.env.SSR)\n      initialState.pinia = pinia.state.value\n    else\n      pinia.state.value = initialState.pinia || {}\n\n    router.beforeEach((to, from, next) => {\n      const store = useRootStore(pinia)\n      if (!store.ready)\n        \u002F\u002F perform the (user-implemented) store action to fill the store's state\n        store.initialize()\n      next()\n    })\n  },\n)\n\n",[27,11434,11435,11439,11453,11458,11463,11484,11496,11505,11509,11529,11539,11544,11559,11563,11591,11604,11615,11620,11630,11637,11642,11646],{"__ignoreMap":83},[87,11436,11437],{"class":89,"line":90},[87,11438,7266],{"class":1339},[87,11440,11441,11443,11445,11447,11449,11451],{"class":89,"line":101},[87,11442,2570],{"class":562},[87,11444,7815],{"class":562},[87,11446,7354],{"class":104},[87,11448,1362],{"class":562},[87,11450,9599],{"class":93},[87,11452,9602],{"class":97},[87,11454,11455],{"class":89,"line":117},[87,11456,11457],{"class":97},"  App,\n",[87,11459,11460],{"class":89,"line":130},[87,11461,11462],{"class":97},"  { routes },\n",[87,11464,11465,11468,11470,11472,11474,11476,11478,11480,11482],{"class":89,"line":224},[87,11466,11467],{"class":97},"  ({ ",[87,11469,9625],{"class":1168},[87,11471,60],{"class":97},[87,11473,9630],{"class":1168},[87,11475,60],{"class":97},[87,11477,9645],{"class":1168},[87,11479,9648],{"class":97},[87,11481,1175],{"class":562},[87,11483,98],{"class":97},[87,11485,11486,11488,11490,11492,11494],{"class":89,"line":246},[87,11487,1378],{"class":562},[87,11489,8368],{"class":104},[87,11491,1362],{"class":562},[87,11493,8373],{"class":93},[87,11495,8038],{"class":97},[87,11497,11498,11501,11503],{"class":89,"line":256},[87,11499,11500],{"class":97},"    app.",[87,11502,7364],{"class":93},[87,11504,8385],{"class":97},[87,11506,11507],{"class":89,"line":270},[87,11508,1446],{"emptyLinePlaceholder":842},[87,11510,11511,11513,11515,11517,11519,11521,11524,11527],{"class":89,"line":280},[87,11512,4224],{"class":562},[87,11514,2658],{"class":97},[87,11516,2495],{"class":562},[87,11518,1231],{"class":97},[87,11520,6245],{"class":104},[87,11522,11523],{"class":97},".env.",[87,11525,11526],{"class":104},"SSR",[87,11528,5501],{"class":97},[87,11530,11531,11534,11536],{"class":89,"line":295},[87,11532,11533],{"class":97},"      initialState.pinia ",[87,11535,162],{"class":562},[87,11537,11538],{"class":97}," pinia.state.value\n",[87,11540,11541],{"class":89,"line":315},[87,11542,11543],{"class":562},"    else\n",[87,11545,11546,11549,11551,11554,11556],{"class":89,"line":330},[87,11547,11548],{"class":97},"      pinia.state.value ",[87,11550,162],{"class":562},[87,11552,11553],{"class":97}," initialState.pinia ",[87,11555,5630],{"class":562},[87,11557,11558],{"class":97}," {}\n",[87,11560,11561],{"class":89,"line":351},[87,11562,1446],{"emptyLinePlaceholder":842},[87,11564,11565,11568,11571,11573,11576,11578,11580,11582,11585,11587,11589],{"class":89,"line":360},[87,11566,11567],{"class":97},"    router.",[87,11569,11570],{"class":93},"beforeEach",[87,11572,1165],{"class":97},[87,11574,11575],{"class":1168},"to",[87,11577,60],{"class":97},[87,11579,2501],{"class":1168},[87,11581,60],{"class":97},[87,11583,11584],{"class":1168},"next",[87,11586,1172],{"class":97},[87,11588,1175],{"class":562},[87,11590,98],{"class":97},[87,11592,11593,11595,11597,11599,11602],{"class":89,"line":374},[87,11594,6395],{"class":562},[87,11596,4626],{"class":104},[87,11598,1362],{"class":562},[87,11600,11601],{"class":93}," useRootStore",[87,11603,8385],{"class":97},[87,11605,11606,11608,11610,11612],{"class":89,"line":383},[87,11607,5804],{"class":562},[87,11609,2658],{"class":97},[87,11611,2661],{"class":562},[87,11613,11614],{"class":97},"store.ready)\n",[87,11616,11617],{"class":89,"line":398},[87,11618,11619],{"class":1339},"        \u002F\u002F perform the (user-implemented) store action to fill the store's state\n",[87,11621,11622,11625,11628],{"class":89,"line":418},[87,11623,11624],{"class":97},"        store.",[87,11626,11627],{"class":93},"initialize",[87,11629,8038],{"class":97},[87,11631,11632,11635],{"class":89,"line":434},[87,11633,11634],{"class":93},"      next",[87,11636,8038],{"class":97},[87,11638,11639],{"class":89,"line":454},[87,11640,11641],{"class":97},"    })\n",[87,11643,11644],{"class":89,"line":463},[87,11645,3907],{"class":97},[87,11647,11648],{"class":89,"line":477},[87,11649,5501],{"class":97},[16,11651,11652,11653,11656],{},"vite-ssg 깃헙에서 나온 설명서인데, 글로벌로 store 를 등록하는 게 아니라면, ",[27,11654,11655],{},"app.use","  까지만 해주어도 된다.",[78,11658,11660],{"className":1150,"code":11659,"language":1152,"meta":83,"style":83},"\u002F\u002F src\u002Fstore\u002Fposts.ts\nimport axios from 'axios';\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue';\n\nexport interface Post {\n  url: string;\n  fileName: string;\n  title: string;\n  description: string;\n  tags: string[];\n  data: string;\n}\n\nexport const usePosts = defineStore('post', () => {\n  const postList = ref\u003CPost[]>([]);\n  const currentUrl = ref\u003Cstring>('');\n\n  async function fetchPostList(\n) {\n    axios.get\u003CPost[]>(`\u002Fpostlist.json`).then(res => {\n      postList.value = res.data;\n      return res.data;\n    });\n  }\n\n  return {\n    postList,\n    currentUrl,\n    fetchPostList,\n  }\n})\n\n",[27,11661,11662,11666,11679,11689,11701,11705,11715,11725,11735,11745,11756,11766,11776,11780,11784,11808,11825,11847,11851,11863,11867,11895,11904,11910,11914,11918,11922,11928,11933,11938,11943,11947],{"__ignoreMap":83},[87,11663,11664],{"class":89,"line":90},[87,11665,4860],{"class":1339},[87,11667,11668,11670,11672,11674,11677],{"class":89,"line":101},[87,11669,2495],{"class":562},[87,11671,4027],{"class":97},[87,11673,2501],{"class":562},[87,11675,11676],{"class":165}," 'axios'",[87,11678,114],{"class":97},[87,11680,11681,11683,11685,11687],{"class":89,"line":117},[87,11682,2495],{"class":562},[87,11684,7787],{"class":97},[87,11686,2501],{"class":562},[87,11688,7792],{"class":165},[87,11690,11691,11693,11695,11697,11699],{"class":89,"line":130},[87,11692,2495],{"class":562},[87,11694,7799],{"class":97},[87,11696,2501],{"class":562},[87,11698,2504],{"class":165},[87,11700,114],{"class":97},[87,11702,11703],{"class":89,"line":224},[87,11704,1446],{"emptyLinePlaceholder":842},[87,11706,11707,11709,11711,11713],{"class":89,"line":246},[87,11708,2570],{"class":562},[87,11710,4045],{"class":562},[87,11712,4048],{"class":93},[87,11714,98],{"class":97},[87,11716,11717,11719,11721,11723],{"class":89,"line":256},[87,11718,4055],{"class":1168},[87,11720,1204],{"class":562},[87,11722,1353],{"class":104},[87,11724,114],{"class":97},[87,11726,11727,11729,11731,11733],{"class":89,"line":270},[87,11728,4066],{"class":1168},[87,11730,1204],{"class":562},[87,11732,1353],{"class":104},[87,11734,114],{"class":97},[87,11736,11737,11739,11741,11743],{"class":89,"line":280},[87,11738,4088],{"class":1168},[87,11740,1204],{"class":562},[87,11742,1353],{"class":104},[87,11744,114],{"class":97},[87,11746,11747,11750,11752,11754],{"class":89,"line":295},[87,11748,11749],{"class":1168},"  description",[87,11751,1204],{"class":562},[87,11753,1353],{"class":104},[87,11755,114],{"class":97},[87,11757,11758,11760,11762,11764],{"class":89,"line":315},[87,11759,4099],{"class":1168},[87,11761,1204],{"class":562},[87,11763,1353],{"class":104},[87,11765,4106],{"class":97},[87,11767,11768,11770,11772,11774],{"class":89,"line":330},[87,11769,4111],{"class":1168},[87,11771,1204],{"class":562},[87,11773,1353],{"class":104},[87,11775,114],{"class":97},[87,11777,11778],{"class":89,"line":351},[87,11779,133],{"class":97},[87,11781,11782],{"class":89,"line":360},[87,11783,1446],{"emptyLinePlaceholder":842},[87,11785,11786,11788,11790,11793,11795,11797,11799,11802,11804,11806],{"class":89,"line":374},[87,11787,2570],{"class":562},[87,11789,7815],{"class":562},[87,11791,11792],{"class":104}," usePosts",[87,11794,1362],{"class":562},[87,11796,7823],{"class":93},[87,11798,791],{"class":97},[87,11800,11801],{"class":165},"'post'",[87,11803,7831],{"class":97},[87,11805,1175],{"class":562},[87,11807,98],{"class":97},[87,11809,11810,11812,11814,11816,11818,11820,11822],{"class":89,"line":383},[87,11811,2637],{"class":562},[87,11813,4166],{"class":104},[87,11815,1362],{"class":562},[87,11817,7847],{"class":93},[87,11819,152],{"class":97},[87,11821,4171],{"class":93},[87,11823,11824],{"class":97},"[]>([]);\n",[87,11826,11827,11829,11831,11833,11835,11837,11840,11843,11845],{"class":89,"line":398},[87,11828,2637],{"class":562},[87,11830,4186],{"class":104},[87,11832,1362],{"class":562},[87,11834,7847],{"class":93},[87,11836,152],{"class":97},[87,11838,11839],{"class":104},"string",[87,11841,11842],{"class":97},">(",[87,11844,6510],{"class":165},[87,11846,1590],{"class":97},[87,11848,11849],{"class":89,"line":418},[87,11850,1446],{"emptyLinePlaceholder":842},[87,11852,11853,11856,11858,11861],{"class":89,"line":434},[87,11854,11855],{"class":562},"  async",[87,11857,1997],{"class":562},[87,11859,11860],{"class":93}," fetchPostList",[87,11862,9602],{"class":97},[87,11864,11865],{"class":89,"line":454},[87,11866,4409],{"class":97},[87,11868,11869,11871,11873,11875,11877,11880,11883,11885,11887,11889,11891,11893],{"class":89,"line":463},[87,11870,4453],{"class":97},[87,11872,4456],{"class":93},[87,11874,152],{"class":97},[87,11876,4171],{"class":93},[87,11878,11879],{"class":97},"[]>(",[87,11881,11882],{"class":165},"`\u002Fpostlist.json`",[87,11884,4464],{"class":97},[87,11886,1196],{"class":93},[87,11888,791],{"class":97},[87,11890,4471],{"class":1168},[87,11892,4307],{"class":562},[87,11894,98],{"class":97},[87,11896,11897,11900,11902],{"class":89,"line":477},[87,11898,11899],{"class":97},"      postList.value ",[87,11901,162],{"class":562},[87,11903,4920],{"class":97},[87,11905,11906,11908],{"class":89,"line":486},[87,11907,4273],{"class":562},[87,11909,4920],{"class":97},[87,11911,11912],{"class":89,"line":3636},[87,11913,4501],{"class":97},[87,11915,11916],{"class":89,"line":3641},[87,11917,1694],{"class":97},[87,11919,11920],{"class":89,"line":3653},[87,11921,1446],{"emptyLinePlaceholder":842},[87,11923,11924,11926],{"class":89,"line":3663},[87,11925,2691],{"class":562},[87,11927,98],{"class":97},[87,11929,11930],{"class":89,"line":3670},[87,11931,11932],{"class":97},"    postList,\n",[87,11934,11935],{"class":89,"line":3681},[87,11936,11937],{"class":97},"    currentUrl,\n",[87,11939,11940],{"class":89,"line":3692},[87,11941,11942],{"class":97},"    fetchPostList,\n",[87,11944,11945],{"class":89,"line":3703},[87,11946,1694],{"class":97},[87,11948,11949],{"class":89,"line":4382},[87,11950,5107],{"class":97},[16,11952,11953],{},"post 목록을 가져오는 store 를 하나 만든다. 포스트를 단순히 json 파일로 저장해두고, 사용할 것이다.",[78,11955,11957],{"className":1150,"code":11956,"language":1152,"meta":83,"style":83},"\u002F\u002F public\u002Fpostlist.json\n[\n  {\n      \"url\": \"Vue-Prefetch\",\n      \"fileName\": \"Vue-Prefetch\",\n      \"title\": \"Vue Code Spliting 간단히 알아보기 (+ Prefetch)\",\n      \"description\": \"Vue 프로젝트의 Code Spliting 이 어떻게 작동하는지와 webpack prefetch 옵션 에 대해서 간략한 설명.\",\n      \"createdAt\": \"2022-08-05\",\n      \"updatedAt\": \"2022-08-05\",\n      \"tags\": [\"Vue\", \"Vue2\", \"Vue3\", \"Code Spliting\", \"코드분산\", \"Prefecth\", \"Webpack\"]\n  },\n  \u002F\u002F ...\n]\n\n",[27,11958,11959,11964,11968,11972,11984,11995,12006,12018,12030,12041,12083,12087,12092],{"__ignoreMap":83},[87,11960,11961],{"class":89,"line":90},[87,11962,11963],{"class":1339},"\u002F\u002F public\u002Fpostlist.json\n",[87,11965,11966],{"class":89,"line":101},[87,11967,3811],{"class":97},[87,11969,11970],{"class":89,"line":117},[87,11971,3816],{"class":97},[87,11973,11974,11977,11979,11982],{"class":89,"line":130},[87,11975,11976],{"class":165},"      \"url\"",[87,11978,108],{"class":97},[87,11980,11981],{"class":165},"\"Vue-Prefetch\"",[87,11983,3413],{"class":97},[87,11985,11986,11989,11991,11993],{"class":89,"line":224},[87,11987,11988],{"class":165},"      \"fileName\"",[87,11990,108],{"class":97},[87,11992,11981],{"class":165},[87,11994,3413],{"class":97},[87,11996,11997,11999,12001,12004],{"class":89,"line":246},[87,11998,8545],{"class":165},[87,12000,108],{"class":97},[87,12002,12003],{"class":165},"\"Vue Code Spliting 간단히 알아보기 (+ Prefetch)\"",[87,12005,3413],{"class":97},[87,12007,12008,12011,12013,12016],{"class":89,"line":256},[87,12009,12010],{"class":165},"      \"description\"",[87,12012,108],{"class":97},[87,12014,12015],{"class":165},"\"Vue 프로젝트의 Code Spliting 이 어떻게 작동하는지와 webpack prefetch 옵션 에 대해서 간략한 설명.\"",[87,12017,3413],{"class":97},[87,12019,12020,12023,12025,12028],{"class":89,"line":270},[87,12021,12022],{"class":165},"      \"createdAt\"",[87,12024,108],{"class":97},[87,12026,12027],{"class":165},"\"2022-08-05\"",[87,12029,3413],{"class":97},[87,12031,12032,12035,12037,12039],{"class":89,"line":280},[87,12033,12034],{"class":165},"      \"updatedAt\"",[87,12036,108],{"class":97},[87,12038,12027],{"class":165},[87,12040,3413],{"class":97},[87,12042,12043,12046,12048,12051,12053,12056,12058,12061,12063,12066,12068,12071,12073,12076,12078,12081],{"class":89,"line":295},[87,12044,12045],{"class":165},"      \"tags\"",[87,12047,3896],{"class":97},[87,12049,12050],{"class":165},"\"Vue\"",[87,12052,60],{"class":97},[87,12054,12055],{"class":165},"\"Vue2\"",[87,12057,60],{"class":97},[87,12059,12060],{"class":165},"\"Vue3\"",[87,12062,60],{"class":97},[87,12064,12065],{"class":165},"\"Code Spliting\"",[87,12067,60],{"class":97},[87,12069,12070],{"class":165},"\"코드분산\"",[87,12072,60],{"class":97},[87,12074,12075],{"class":165},"\"Prefecth\"",[87,12077,60],{"class":97},[87,12079,12080],{"class":165},"\"Webpack\"",[87,12082,3902],{"class":97},[87,12084,12085],{"class":89,"line":315},[87,12086,3907],{"class":97},[87,12088,12089],{"class":89,"line":330},[87,12090,12091],{"class":1339},"  \u002F\u002F ...\n",[87,12093,12094],{"class":89,"line":351},[87,12095,3902],{"class":97},[16,12097,12098],{},"public 폴더에 파일을 두면, 그 항목은 사이트의 루트에 포함되게 된다. 같은 호스트이기 때문에 cors 없이 axios 로 동적으로 조회할 수 있다.",[45,12100,12101],{"id":12101},"문제",[753,12103,12105],{"id":12104},"vite-ssg-gsap","vite-ssg + gsap",[78,12107,12110],{"className":12108,"code":12109,"language":1095},[1093],"[vite-ssg] An internal error occurred.\n[vite-ssg] Please report an issue, if none already exists: https:\u002F\u002Fgithub.com\u002Fantfu\u002Fvite-ssg\u002Fissues\nfile:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002F.vite-ssg-temp\u002Fmain.mjs:6\nimport { ScrollTrigger } from \"gsap\u002FScrollTrigger.js\";\n         ^^^^^^^^^^^^^\nSyntaxError: Named export 'ScrollTrigger' not found. The requested module 'gsap\u002FScrollTrigger.js' is a CommonJS module, which may not support all module.exports as named exports.\nCommonJS modules can always be imported via the default export, for example using:\n\nimport pkg from 'gsap\u002FScrollTrigger.js';\nconst { ScrollTrigger } = pkg;\n\n    at ModuleJob._instantiate (node:internal\u002Fmodules\u002Fesm\u002Fmodule_job:123:21)\n    at async ModuleJob.run (node:internal\u002Fmodules\u002Fesm\u002Fmodule_job:189:5)\n    at async Promise.all (index 0)\n    at async ESMLoader.import (node:internal\u002Fmodules\u002Fesm\u002Floader:530:24)\n    at async build (file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Fvite-ssg\u002Fdist\u002Fshared\u002Fvite-ssg.62550b28.mjs:996:87)\n    at async Object.handler (file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Fvite-ssg\u002Fdist\u002Fnode\u002Fcli.mjs:29:5)\n\nNode.js v18.12.1\n",[27,12111,12109],{"__ignoreMap":83},[16,12113,12114],{},[2943,12115,12118],{"href":12116,"rel":12117},"https:\u002F\u002Fstackoverflow.com\u002Fa\u002F76578317",[2947],"javascript - GSAP ScrollTrigger with Next.js - Stack Overflow",[78,12120,12122],{"className":1150,"code":12121,"language":1152,"meta":83,"style":83},"import MPP from 'gsap\u002FMotionPathPlugin';\nimport ScrollTrigger from 'gsap\u002FScrollTrigger';\n\nconst { MotionPathPlugin } = MPP;\ngsap.registerPlugin(ScrollTrigger);\ngsap.registerPlugin(MotionPathPlugin);\n",[27,12123,12124,12138,12152,12156,12174,12185],{"__ignoreMap":83},[87,12125,12126,12128,12131,12133,12136],{"class":89,"line":90},[87,12127,2495],{"class":562},[87,12129,12130],{"class":97}," MPP ",[87,12132,2501],{"class":562},[87,12134,12135],{"class":165}," 'gsap\u002FMotionPathPlugin'",[87,12137,114],{"class":97},[87,12139,12140,12142,12145,12147,12150],{"class":89,"line":101},[87,12141,2495],{"class":562},[87,12143,12144],{"class":97}," ScrollTrigger ",[87,12146,2501],{"class":562},[87,12148,12149],{"class":165}," 'gsap\u002FScrollTrigger'",[87,12151,114],{"class":97},[87,12153,12154],{"class":89,"line":117},[87,12155,1446],{"emptyLinePlaceholder":842},[87,12157,12158,12160,12162,12165,12167,12169,12172],{"class":89,"line":130},[87,12159,1964],{"class":562},[87,12161,6449],{"class":97},[87,12163,12164],{"class":104},"MotionPathPlugin",[87,12166,6465],{"class":97},[87,12168,162],{"class":562},[87,12170,12171],{"class":104}," MPP",[87,12173,114],{"class":97},[87,12175,12176,12179,12182],{"class":89,"line":224},[87,12177,12178],{"class":97},"gsap.",[87,12180,12181],{"class":93},"registerPlugin",[87,12183,12184],{"class":97},"(ScrollTrigger);\n",[87,12186,12187,12189,12191],{"class":89,"line":246},[87,12188,12178],{"class":97},[87,12190,12181],{"class":93},[87,12192,12193],{"class":97},"(MotionPathPlugin);\n",[16,12195,12196,12197,12199,12200,12203,12204,12207],{},"이런식으로 바꿔주었다. ",[27,12198,9510],{}," 로 사용할 때는 다른 빌드 방식을 사용하나보다. ",[27,12201,12202],{},"esbuild"," 라거나,, commonJS 라거나.. 그래서 ",[27,12205,12206],{},"\u002Fdist"," 폴더 안의 라이브러리를 사용해주었다.",[753,12209,12210],{"id":12210},"axios",[16,12212,12213,12215,12216,12219],{},[27,12214,9510],{}," 는 정적 html 사이트를 생성하기 때문에 ",[27,12217,12218],{},"setup"," 안에 있는 함수를 무조건 실행한다. 그래서 최초에 데이터를 가져오는 axios 함수가 실행되면서 없는 서버로 데이터를 가져오려고 해서 에러가 발생한다.",[78,12221,12224],{"className":12222,"code":12223,"language":1095},[1093],"file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Faxios\u002Flib\u002Fcore\u002FAxiosError.js:89\n  AxiosError.call(axiosError, error.message, code, config, request, response);\n             ^\nAxiosError: connect ECONNREFUSED ::1:80\n    at AxiosError.from (file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Faxios\u002Flib\u002Fcore\u002FAxiosError.js:89:14)\n    at RedirectableRequest.handleRequestError (file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Faxios\u002Flib\u002Fadapters\u002Fhttp.js:591:25)\n    at RedirectableRequest.emit (node:events:513:28)\n    at eventHandlers.\u003Ccomputed> (\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Ffollow-redirects\u002Findex.js:14:24)\n    at ClientRequest.emit (node:events:513:28)\n    at Socket.socketErrorListener (node:_http_client:494:9)\n    at Socket.emit (node:events:513:28)\n    at emitErrorNT (node:internal\u002Fstreams\u002Fdestroy:151:8)\n    at emitErrorCloseNT (node:internal\u002Fstreams\u002Fdestroy:116:3)\n    at process.processTicksAndRejections (node:internal\u002Fprocess\u002Ftask_queues:82:21) {\n",[27,12225,12223],{"__ignoreMap":83},[16,12227,12228,12231],{},[27,12229,12230],{},"::1:80"," 경로로 데이터를 가져오면서 에러가 발생한 것이다.",[78,12233,12235],{"className":1150,"code":12234,"language":1152,"meta":83,"style":83},"const posts = usePosts();\n\nonMounted(() => {\n  animPostList();\n  posts.fetchPostList();\n})\n",[27,12236,12237,12249,12253,12263,12270,12280],{"__ignoreMap":83},[87,12238,12239,12241,12243,12245,12247],{"class":89,"line":90},[87,12240,1964],{"class":562},[87,12242,5530],{"class":104},[87,12244,1362],{"class":562},[87,12246,11792],{"class":93},[87,12248,2026],{"class":97},[87,12250,12251],{"class":89,"line":101},[87,12252,1446],{"emptyLinePlaceholder":842},[87,12254,12255,12257,12259,12261],{"class":89,"line":117},[87,12256,2081],{"class":93},[87,12258,2084],{"class":97},[87,12260,1175],{"class":562},[87,12262,98],{"class":97},[87,12264,12265,12268],{"class":89,"line":130},[87,12266,12267],{"class":93},"  animPostList",[87,12269,2026],{"class":97},[87,12271,12272,12275,12278],{"class":89,"line":224},[87,12273,12274],{"class":97},"  posts.",[87,12276,12277],{"class":93},"fetchPostList",[87,12279,2026],{"class":97},[87,12281,12282],{"class":89,"line":246},[87,12283,5107],{"class":97},[16,12285,12286,12287,12289],{},"onMounted 내부에서 fetch 를 실행하면, ",[27,12288,12218],{}," 훅과 다르게 빌드 시 실행하지 않는다.",[825,12291,12292],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":83,"searchDepth":101,"depth":101,"links":12294},[12295,12296,12297,12298,12302,12303,12306,12311,12312],{"id":9497,"depth":101,"text":9498},{"id":9510,"depth":101,"text":9510},{"id":9678,"depth":101,"text":9678},{"id":9697,"depth":101,"text":9697,"children":12299},[12300,12301],{"id":9996,"depth":117,"text":9997},{"id":10030,"depth":117,"text":10031},{"id":10291,"depth":101,"text":10291},{"id":10590,"depth":101,"text":10590,"children":12304},[12305],{"id":10723,"depth":117,"text":10724},{"id":10920,"depth":101,"text":10921,"children":12307},[12308,12309,12310],{"id":10924,"depth":117,"text":10924},{"id":10943,"depth":117,"text":10943},{"id":11249,"depth":117,"text":11249},{"id":11401,"depth":101,"text":11401},{"id":12101,"depth":101,"text":12101,"children":12313},[12314,12315],{"id":12104,"depth":117,"text":12105},{"id":12210,"depth":117,"text":12210},"2023-09-26","기존 vue-cli 를 사용한 vue 프로젝트로 github pages 를 구축했는데, 대세에 따라 vite 로 변경하기로 했다.","vite-vue-and-github-pages",{},"\u002Fblog\u002Fvite-vue-and-github-pages",{"title":9481,"description":12317},{"loc":12320},"blog\u002Fvite-vue-and-github-pages",[9476,2858,7529,7530,7531,7532],"S1hw5MlK5CzyjHQGrXLLDqumgm3MSjJFb-xM1Nmao9I",{"id":12327,"title":12328,"body":12329,"created":12513,"description":12514,"extension":839,"filename":12515,"meta":12516,"navigation":842,"path":12517,"seo":12518,"sitemap":12519,"stem":12520,"subject":2858,"tags":12521,"updated":12513,"volume":1859,"__hash__":12526},"blog\u002Fblog\u002Fvitest-timer-flushpromise.md","Vitest Fake Timer 로 setInterval 테스트 중 다음 루프가 실행 안되는 경우",{"type":8,"value":12330,"toc":12510},[12331,12338,12341,12422,12426,12500,12507],[16,12332,12333],{},[2943,12334,12337],{"href":12335,"rel":12336},"https:\u002F\u002Fvitest.dev\u002Fapi\u002Fvi#fake-timers",[2947],"vitest 공식 홈페이지 #fake-timers",[16,12339,12340],{},"타이머를 가짜로 돌려서, 원하는 시간만큼 컨트롤할 수 있다. 시간을 기다리지 않고, 임의로 시간을 흐르게 해서 빠른 테스트를 가능하게 한다.",[78,12342,12344],{"className":1150,"code":12343,"language":1152,"meta":83,"style":83},"let i = 0\nsetInterval(() => console.log(++i), 50)\n\nvi.advanceTimersByTime(150)\n\n\u002F\u002F log: 1\n\u002F\u002F log: 2\n\u002F\u002F log: 3\n",[27,12345,12346,12358,12384,12388,12403,12407,12412,12417],{"__ignoreMap":83},[87,12347,12348,12350,12353,12355],{"class":89,"line":90},[87,12349,1345],{"class":562},[87,12351,12352],{"class":97}," i ",[87,12354,162],{"class":562},[87,12356,12357],{"class":104}," 0\n",[87,12359,12360,12363,12365,12367,12370,12372,12374,12377,12380,12382],{"class":89,"line":101},[87,12361,12362],{"class":93},"setInterval",[87,12364,2084],{"class":97},[87,12366,1175],{"class":562},[87,12368,12369],{"class":97}," console.",[87,12371,1221],{"class":93},[87,12373,791],{"class":97},[87,12375,12376],{"class":562},"++",[87,12378,12379],{"class":97},"i), ",[87,12381,3184],{"class":104},[87,12383,5501],{"class":97},[87,12385,12386],{"class":89,"line":117},[87,12387,1446],{"emptyLinePlaceholder":842},[87,12389,12390,12393,12396,12398,12401],{"class":89,"line":130},[87,12391,12392],{"class":97},"vi.",[87,12394,12395],{"class":93},"advanceTimersByTime",[87,12397,791],{"class":97},[87,12399,12400],{"class":104},"150",[87,12402,5501],{"class":97},[87,12404,12405],{"class":89,"line":224},[87,12406,1446],{"emptyLinePlaceholder":842},[87,12408,12409],{"class":89,"line":246},[87,12410,12411],{"class":1339},"\u002F\u002F log: 1\n",[87,12413,12414],{"class":89,"line":256},[87,12415,12416],{"class":1339},"\u002F\u002F log: 2\n",[87,12418,12419],{"class":89,"line":270},[87,12420,12421],{"class":1339},"\u002F\u002F log: 3\n",[45,12423,12425],{"id":12424},"타이머-내부에-async-fetch-코드가-있다면-flushpromise-필수","타이머 내부에 async fetch 코드가 있다면 flushPromise 필수",[78,12427,12429],{"className":1150,"code":12428,"language":1152,"meta":83,"style":83},"async function fetchFunction() {\n  await fetchDetail();\n    \u002F\u002F ...\n}\n\nfunction startTimer() {\n  refreshTimer = setInterval(fetchFunction, 5 * 1000);\n}\n",[27,12430,12431,12442,12451,12455,12459,12463,12472,12496],{"__ignoreMap":83},[87,12432,12433,12435,12437,12440],{"class":89,"line":90},[87,12434,1994],{"class":562},[87,12436,1997],{"class":562},[87,12438,12439],{"class":93}," fetchFunction",[87,12441,2003],{"class":97},[87,12443,12444,12446,12449],{"class":89,"line":101},[87,12445,2250],{"class":562},[87,12447,12448],{"class":93}," fetchDetail",[87,12450,2026],{"class":97},[87,12452,12453],{"class":89,"line":117},[87,12454,9751],{"class":1339},[87,12456,12457],{"class":89,"line":130},[87,12458,133],{"class":97},[87,12460,12461],{"class":89,"line":224},[87,12462,1446],{"emptyLinePlaceholder":842},[87,12464,12465,12467,12470],{"class":89,"line":246},[87,12466,9050],{"class":562},[87,12468,12469],{"class":93}," startTimer",[87,12471,2003],{"class":97},[87,12473,12474,12477,12479,12482,12485,12488,12491,12494],{"class":89,"line":256},[87,12475,12476],{"class":97},"  refreshTimer ",[87,12478,162],{"class":562},[87,12480,12481],{"class":93}," setInterval",[87,12483,12484],{"class":97},"(fetchFunction, ",[87,12486,12487],{"class":104},"5",[87,12489,12490],{"class":562}," *",[87,12492,12493],{"class":104}," 1000",[87,12495,1590],{"class":97},[87,12497,12498],{"class":89,"line":270},[87,12499,133],{"class":97},[16,12501,12502,12503,12506],{},"이런식으로 fetch 함수가 타이머 함수 내부에 실행되는 경우, ",[27,12504,12505],{},"flushPromise"," 없이는 다음 타이머가 동작하지 않는다. vi.advanceTimersByTime()은 시간만 진행시킬 뿐, 비동기 작업의 완료를 보장하지 않는다. 즉, 비동기 작업은 flushPromise 호출 전까지 대기한다.",[825,12508,12509],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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);}",{"title":83,"searchDepth":101,"depth":101,"links":12511},[12512],{"id":12424,"depth":101,"text":12425},"2025-01-14","setInterval 로 돌아가는 로직을 vitest 의 Fake Timer 를 사용해 테스트. fetch 함수가 포함된 interval 로직은 flushPromise 없이 다음 루프가 돌지 않음.","vitest-timer-flushpromise",{},"\u002Fblog\u002Fvitest-timer-flushpromise",{"title":12328,"description":12514},{"loc":12517},"blog\u002Fvitest-timer-flushpromise",[12522,12523,12524,12525],"Vitest","Javscript","Jest","Async","ukR7_qfk8ljY77CWbYUWWDiBSVftkLQlxTaHKJvcZVo",{"id":12528,"title":12529,"body":12530,"created":13042,"description":13043,"extension":839,"filename":13044,"meta":13045,"navigation":842,"path":13046,"seo":13047,"sitemap":13048,"stem":13049,"subject":2858,"tags":13050,"updated":13042,"volume":1859,"__hash__":13055},"blog\u002Fblog\u002Fvue-prefetch.md","Vue Code Spliting 간단히 알아보기 (+ Prefetch)",{"type":8,"value":12531,"toc":13034},[12532,12536,12543,12547,12649,12659,12663,12765,12768,12779,12785,12807,12811,12821,12888,12891,12895,12947,12959,13016,13022,13025,13028,13031],[45,12533,12535],{"id":12534},"code-spliting","Code Spliting",[16,12537,12538,12539,12542],{},"우리가 Vue 프로젝트에서 Code Spliting 을 사용하는 이유는 ",[788,12540,12541],{},"빌드 결과물의 데이터 양을 분산시키기 위해서","다. 기본적으로 SPA 프론트엔드 프로젝트를 번들링할 때, 스크립트 파일은 하나로 뭉쳐진다. 프로젝트 크기에 따라서 이 파일의 크기는 선형으로 증가하며, 초기에 모든 기능을 로드하는 SPA 특성상 초기 렌더링 시간이 급격하게 늘어난다.",[753,12544,12546],{"id":12545},"vue-에서는-어떻게-기능을-사용하나","Vue 에서는 어떻게 기능을 사용하나",[78,12548,12550],{"className":4002,"code":12549,"language":4004,"meta":83,"style":83},"import Home from '..\u002Fviews\u002FHomeView.vue'\n{\n  path: '\u002F',\n  name: 'Home',\n  component: Home\n}, {\n  path: '\u002F:id',\n  name: 'Datail',\n  component: () => import(\u002F* webpackChunkName: \"detail\" *\u002F '..\u002Fviews\u002FDetailView.vue')\n}\n",[27,12551,12552,12564,12568,12579,12591,12598,12603,12613,12623,12645],{"__ignoreMap":83},[87,12553,12554,12556,12559,12561],{"class":89,"line":90},[87,12555,2495],{"class":562},[87,12557,12558],{"class":97}," Home ",[87,12560,2501],{"class":562},[87,12562,12563],{"class":165}," '..\u002Fviews\u002FHomeView.vue'\n",[87,12565,12566],{"class":89,"line":101},[87,12567,8520],{"class":97},[87,12569,12570,12572,12574,12577],{"class":89,"line":117},[87,12571,10065],{"class":93},[87,12573,108],{"class":97},[87,12575,12576],{"class":165},"'\u002F'",[87,12578,3413],{"class":97},[87,12580,12581,12584,12586,12589],{"class":89,"line":130},[87,12582,12583],{"class":93},"  name",[87,12585,108],{"class":97},[87,12587,12588],{"class":165},"'Home'",[87,12590,3413],{"class":97},[87,12592,12593,12595],{"class":89,"line":224},[87,12594,10077],{"class":93},[87,12596,12597],{"class":97},": Home\n",[87,12599,12600],{"class":89,"line":246},[87,12601,12602],{"class":97},"}, {\n",[87,12604,12605,12608,12611],{"class":89,"line":256},[87,12606,12607],{"class":97},"  path: ",[87,12609,12610],{"class":165},"'\u002F:id'",[87,12612,3413],{"class":97},[87,12614,12615,12618,12621],{"class":89,"line":270},[87,12616,12617],{"class":97},"  name: ",[87,12619,12620],{"class":165},"'Datail'",[87,12622,3413],{"class":97},[87,12624,12625,12627,12630,12632,12635,12637,12640,12643],{"class":89,"line":280},[87,12626,10077],{"class":93},[87,12628,12629],{"class":97},": () ",[87,12631,1175],{"class":562},[87,12633,12634],{"class":93}," import",[87,12636,791],{"class":97},[87,12638,12639],{"class":1339},"\u002F* webpackChunkName: \"detail\" *\u002F",[87,12641,12642],{"class":165}," '..\u002Fviews\u002FDetailView.vue'",[87,12644,5501],{"class":97},[87,12646,12647],{"class":89,"line":295},[87,12648,133],{"class":97},[16,12650,12651,12652,12655,12656,12658],{},"vue-router 에서 컴포넌트를 정적으로 로드할 경우 app.js 에 컴포넌트 코드가 함께 번들링된다. 위 코드의 Detail 컴포넌트 처럼 함수를 통해 동적으로 로드할 경우 코드는 분산된다. ",[27,12653,12654],{},"webpackChunkName"," 에 정의된 이름으로 하나의 청크로 묶인다. 서로 다른 두 개의 라우트에서 같은 ",[27,12657,12654],{}," 을 사용하면, 하나의 파일에 두 라우트 컴포넌트가 묶인다.",[767,12660,12662],{"id":12661},"만약-서로-다른-라우트에서-같은-컴포넌트를-사용한다면","만약 서로 다른 라우트에서 같은 컴포넌트를 사용한다면?",[78,12664,12666],{"className":4002,"code":12665,"language":4004,"meta":83,"style":83},"{\n    \u002F\u002F Input 컴포넌트를 사용\n  path: '\u002F',\n  name: 'Home',\n    component: () => import(\u002F* webpackChunkName: \"home\" *\u002F '..\u002Fviews\u002FHomeView.vue')\n}, {\n    \u002F\u002F 마찬가지로 Input 컴포넌트를 사용\n  path: '\u002F:id',\n  name: 'Datail',\n  component: () => import(\u002F* webpackChunkName: \"detail\" *\u002F '..\u002Fviews\u002FDetailView.vue')\n}\n",[27,12667,12668,12672,12677,12687,12697,12718,12722,12727,12735,12743,12761],{"__ignoreMap":83},[87,12669,12670],{"class":89,"line":90},[87,12671,8520],{"class":97},[87,12673,12674],{"class":89,"line":101},[87,12675,12676],{"class":1339},"    \u002F\u002F Input 컴포넌트를 사용\n",[87,12678,12679,12681,12683,12685],{"class":89,"line":117},[87,12680,10065],{"class":93},[87,12682,108],{"class":97},[87,12684,12576],{"class":165},[87,12686,3413],{"class":97},[87,12688,12689,12691,12693,12695],{"class":89,"line":130},[87,12690,12583],{"class":93},[87,12692,108],{"class":97},[87,12694,12588],{"class":165},[87,12696,3413],{"class":97},[87,12698,12699,12702,12704,12706,12708,12710,12713,12716],{"class":89,"line":224},[87,12700,12701],{"class":93},"    component",[87,12703,12629],{"class":97},[87,12705,1175],{"class":562},[87,12707,12634],{"class":93},[87,12709,791],{"class":97},[87,12711,12712],{"class":1339},"\u002F* webpackChunkName: \"home\" *\u002F",[87,12714,12715],{"class":165}," '..\u002Fviews\u002FHomeView.vue'",[87,12717,5501],{"class":97},[87,12719,12720],{"class":89,"line":246},[87,12721,12602],{"class":97},[87,12723,12724],{"class":89,"line":256},[87,12725,12726],{"class":1339},"    \u002F\u002F 마찬가지로 Input 컴포넌트를 사용\n",[87,12728,12729,12731,12733],{"class":89,"line":270},[87,12730,12607],{"class":97},[87,12732,12610],{"class":165},[87,12734,3413],{"class":97},[87,12736,12737,12739,12741],{"class":89,"line":280},[87,12738,12617],{"class":97},[87,12740,12620],{"class":165},[87,12742,3413],{"class":97},[87,12744,12745,12747,12749,12751,12753,12755,12757,12759],{"class":89,"line":295},[87,12746,10077],{"class":93},[87,12748,12629],{"class":97},[87,12750,1175],{"class":562},[87,12752,12634],{"class":93},[87,12754,791],{"class":97},[87,12756,12639],{"class":1339},[87,12758,12642],{"class":165},[87,12760,5501],{"class":97},[87,12762,12763],{"class":89,"line":315},[87,12764,133],{"class":97},[16,12766,12767],{},"만약 Home과 Detail 컴포넌트 모두 Input 컴포넌트를 공통으로 사용하고 있다면, 코드는 어떻게 묶일까.",[20,12769,12770,12773,12776],{},[23,12771,12772],{},"home.js",[23,12774,12775],{},"detail.js",[23,12777,12778],{},"home-detail.js",[16,12780,12781,12782,12784],{},"위처럼 공통된 컴포넌트는 별도의 파일로 묶이며 각 페이지 진입 시에 ",[788,12783,12778],{}," 파일을 똑같이 로드한다.",[20,12786,12787,12798],{},[23,12788,12789,12790,10740,12792,12794,12795,12797],{},"Home 컴포넌트 진입 시 ",[788,12791,12772],{},[788,12793,12778],{}," 로드 + ",[788,12796,12775],{}," 로드 안함",[23,12799,12800,12801,10740,12803,12794,12805,12797],{},"Detail 컴포넌트 진입 시 ",[788,12802,12775],{},[788,12804,12778],{},[788,12806,12772],{},[45,12808,12810],{"id":12809},"prefetch","Prefetch?",[16,12812,12813,12814,12816,12817,12820],{},"Code Spliting 을 통해 번들을 여러 파일로 분산하고, 필요에 따라 브라우저가 동적으로 요청하게끔 하는 것으로 초기 렌더링 시간을 줄일 수 있게 되었다. 그런데 여기서 설명하는 ",[27,12815,12809],{}," 라는 것은 분산 시킨 코드들을 ",[27,12818,12819],{},"index.html"," 에서 미리 로드해 갖고 있는 것이다.",[78,12822,12824],{"className":4002,"code":12823,"language":4004,"meta":83,"style":83},"\u003Chead>\n    \u003Clink href=\"\u002Fjs\u002Fsomepart1.js\" rel=\"prefetch\">\n    \u003Clink href=\"\u002Fjs\u002Fsomepart2.js\" rel=\"prefetch\">\n\u003Chead>\n",[27,12825,12826,12834,12859,12880],{"__ignoreMap":83},[87,12827,12828,12830,12832],{"class":89,"line":90},[87,12829,152],{"class":97},[87,12831,6236],{"class":155},[87,12833,169],{"class":97},[87,12835,12836,12838,12841,12844,12846,12849,12852,12854,12857],{"class":89,"line":101},[87,12837,174],{"class":97},[87,12839,12840],{"class":155},"link",[87,12842,12843],{"class":93}," href",[87,12845,162],{"class":562},[87,12847,12848],{"class":165},"\"\u002Fjs\u002Fsomepart1.js\"",[87,12850,12851],{"class":93}," rel",[87,12853,162],{"class":562},[87,12855,12856],{"class":165},"\"prefetch\"",[87,12858,169],{"class":97},[87,12860,12861,12863,12865,12867,12869,12872,12874,12876,12878],{"class":89,"line":117},[87,12862,174],{"class":97},[87,12864,12840],{"class":155},[87,12866,12843],{"class":93},[87,12868,162],{"class":562},[87,12870,12871],{"class":165},"\"\u002Fjs\u002Fsomepart2.js\"",[87,12873,12851],{"class":93},[87,12875,162],{"class":562},[87,12877,12856],{"class":165},[87,12879,169],{"class":97},[87,12881,12882,12884,12886],{"class":89,"line":130},[87,12883,152],{"class":97},[87,12885,6236],{"class":155},[87,12887,169],{"class":97},[16,12889,12890],{},"이런 식으로 말이다. 이렇게 되면 코드를 분산시켰음에도 처음에 다 로드를 하게 된다. 의도한대로 작동하지 않는다.",[45,12892,12894],{"id":12893},"vue-cli","Vue-Cli",[78,12896,12898],{"className":4002,"code":12897,"language":4004,"meta":83,"style":83},"module.exports = {\n    chainWebpack: config => {\n        config.plugins.delete('prefetch');\n    }\n}\n",[27,12899,12900,12912,12925,12939,12943],{"__ignoreMap":83},[87,12901,12902,12904,12906,12908,12910],{"class":89,"line":90},[87,12903,3393],{"class":104},[87,12905,1231],{"class":97},[87,12907,3398],{"class":104},[87,12909,1362],{"class":562},[87,12911,98],{"class":97},[87,12913,12914,12917,12919,12921,12923],{"class":89,"line":101},[87,12915,12916],{"class":93},"    chainWebpack",[87,12918,108],{"class":97},[87,12920,5793],{"class":1168},[87,12922,4307],{"class":562},[87,12924,98],{"class":97},[87,12926,12927,12929,12932,12934,12937],{"class":89,"line":117},[87,12928,5823],{"class":97},[87,12930,12931],{"class":93},"delete",[87,12933,791],{"class":97},[87,12935,12936],{"class":165},"'prefetch'",[87,12938,1590],{"class":97},[87,12940,12941],{"class":89,"line":130},[87,12942,1689],{"class":97},[87,12944,12945],{"class":89,"line":224},[87,12946,133],{"class":97},[16,12948,12949,12951,12952,12955,12956,12958],{},[788,12950,12893],{}," 에서는 기본적으로 사용하게끔 웹팩 설정이 되어 있다. 하지만 끄게끔 권장한다. ",[27,12953,12954],{},"vue.config.json"," 에다가 ",[27,12957,12809],{}," 플러그인을 제거하면 간단하게 기능을 끌 수 있다.",[78,12960,12962],{"className":4002,"code":12961,"language":4004,"meta":83,"style":83},"{\n    path: '\u002Fpage',\n    name: 'SomePage',\n    component: () => import(\u002F* webpackPrefetch: true *\u002F '@\u002Fsrc\u002FSomePage.vue'),\n}\n",[27,12963,12964,12968,12980,12992,13012],{"__ignoreMap":83},[87,12965,12966],{"class":89,"line":90},[87,12967,8520],{"class":97},[87,12969,12970,12973,12975,12978],{"class":89,"line":101},[87,12971,12972],{"class":93},"    path",[87,12974,108],{"class":97},[87,12976,12977],{"class":165},"'\u002Fpage'",[87,12979,3413],{"class":97},[87,12981,12982,12985,12987,12990],{"class":89,"line":117},[87,12983,12984],{"class":93},"    name",[87,12986,108],{"class":97},[87,12988,12989],{"class":165},"'SomePage'",[87,12991,3413],{"class":97},[87,12993,12994,12996,12998,13000,13002,13004,13007,13010],{"class":89,"line":130},[87,12995,12701],{"class":93},[87,12997,12629],{"class":97},[87,12999,1175],{"class":562},[87,13001,12634],{"class":93},[87,13003,791],{"class":97},[87,13005,13006],{"class":1339},"\u002F* webpackPrefetch: true *\u002F",[87,13008,13009],{"class":165}," '@\u002Fsrc\u002FSomePage.vue'",[87,13011,5693],{"class":97},[87,13013,13014],{"class":89,"line":224},[87,13015,133],{"class":97},[16,13017,13018,13019,13021],{},"단 전략에 따라서 미리 로드해야하는 코드에 대해서는 ",[788,13020,9537],{}," 의 주석으로 prefetch 를 적용할 수 있다.",[45,13023,9476],{"id":13024},"vite",[16,13026,13027],{},"Vite 의 경우 Webpack 이 아닌 Rollup 을 사용한다. 기본적인 웹팩 설정들은 사용할 수 없고, 기본적으로 동적로딩을 하는 경우 모두 항상 코드는 분산되어 빌드된다.",[16,13029,13030],{},"추가로 알아보아야 함!",[825,13032,13033],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":83,"searchDepth":101,"depth":101,"links":13035},[13036,13039,13040,13041],{"id":12534,"depth":101,"text":12535,"children":13037},[13038],{"id":12545,"depth":117,"text":12546},{"id":12809,"depth":101,"text":12810},{"id":12893,"depth":101,"text":12894},{"id":13024,"depth":101,"text":9476},"2022-08-05","Vue 프로젝트의 Code Spliting 이 어떻게 작동하는지와 webpack prefetch 옵션 에 대해서 간략한 설명.","vue-prefetch",{},"\u002Fblog\u002Fvue-prefetch",{"title":12529,"description":13043},{"loc":13046},"blog\u002Fvue-prefetch",[2858,13051,7529,12535,13052,13053,13054],"Vue2","코드분산","Prefetch","Webpack","uNTjHrleVUyAURwW2P8dzKwzO-Nx5LYO0wX3x2tAEkI",{"id":13057,"title":13058,"body":13059,"created":17464,"description":17465,"extension":839,"filename":17466,"meta":17467,"navigation":842,"path":17468,"seo":17469,"sitemap":17470,"stem":17471,"subject":2858,"tags":17472,"updated":17464,"volume":7536,"__hash__":17475},"blog\u002Fblog\u002Fvue2-reactivity.md","Vue2, Vue3 Reactivity 비교 - Vue2 편",{"type":8,"value":13060,"toc":17450},[13061,13065,13068,13071,13074,13078,13083,13096,13100,13103,13110,13683,13703,13746,13751,13754,13781,13785,13962,13982,14348,14351,14362,14374,14393,14402,14641,14650,14660,14890,14900,15532,15538,15559,15571,15591,15594,15604,15852,15862,15866,15874,15878,15921,15932,15961,15967,15971,15992,16053,16056,16060,16063,16071,16151,16154,16168,16193,16199,16203,16303,16306,16329,16332,16337,16340,16345,16348,16353,16363,16498,16501,16505,16696,16699,16862,16865,16871,16877,16883,16986,16996,16999,17003,17010,17139,17153,17157,17160,17312,17323,17444,17447],[45,13062,13064],{"id":13063},"반응성","반응성?",[16,13066,13067],{},"Vue 프레임워크로 컴포넌트를 작업하다보면, 데이터를 바꿨을 뿐인데 화면에 바뀐 데이터를 바로바로 반영하는 것이 그저 익숙할 것이다. 그런데 Vue를 비롯한 프론트엔드 프레임워크를 사용하지 않을 때, 데이터만 바꿨다고 화면에 바로 뿌려줬는가? 아니다. 개발자가 직접 DOM을 수정해서 보여주어야 했다. 아니면 서버에서 바뀐 데이터로 렌더링한 결과물을 새로 받아야 했다.",[16,13069,13070],{},"Vue에서는 컴포넌트의 데이터 정보만 바꾸면 화면에 알아서 적용된다. 이를 반응성이라 한다. 컴포넌트의 데이터를 관찰하고 있다가 변경이 감지되면 Virtual DOM을 다시 컴파일한 후 만들어진 트리를 실제 DOM에 적용시킨다.",[16,13072,13073],{},"개발자는 이러한 내부적 로직은 알 필요 없이 데이터에 집중하는 개발을 하도록 Vue 는 유도하고 있다. 하지만 개발자는 내가 사용하는 도구; 여기서는 프레임워크가 어떻게 작동하는 녀석인지 알아야 입맛대로 작업할 수 있고, 문제 해결을 할 수 있다. 처음엔 쉬워도 언젠가 구조적 문제에 직면하는 게 개발자 삶이다.",[45,13075,13077],{"id":13076},"vue2에서는-반응성을-어떻게-구현했나","Vue2에서는 반응성을 어떻게 구현했나.",[16,13079,13080],{},[513,13081],{"alt":515,"src":13082},".\u002FUntitled.png",[16,13084,13085,13086,10740,13089,13092,13093,13095],{},"Vue의 모든 컴포넌트는 Watcher 를 갖고 있다. 또 data option에 등록한 데이터는 자동으로 ",[27,13087,13088],{},"getter",[27,13090,13091],{},"setter"," 를 주입받는다. 데이터의 ",[27,13094,13091],{}," 가 실행되는 순간, 컴포넌트의 Watcher 가 이를 감지하고, 렌더 함수를 발동시킨다. 그로 인해 Virtual DOM을 다시 구성하게 되고, 이것이 화면에 나타나는 것이다. data 뿐 아니라 computed 도 마찬가지다. data 의 변경 감지 → computed 로직 실행 → computed 변경 감지 → 렌더링의 순서로 이루어진다.",[753,13097,13099],{"id":13098},"gettersetter-주입","Getter\u002FSetter 주입",[16,13101,13102],{},"Vue의 코드를 조금 살펴보면서 반응형을 어떻게 구현했는지 간단하게 알아보자.",[16,13104,13105,13106,13109],{},"Vue의 데이터에 주입하는 getter\u002Fsetter은 ",[27,13107,13108],{},"defineReactive"," 라는 함수에서 주입한다.",[78,13111,13113],{"className":3379,"code":13112,"language":3381,"meta":83,"style":83},"Object.defineProperty(obj, key, {\n  enumerable: true,\n  configurable: true,\n  get: function reactiveGetter() {\n      var value = getter ? getter.call(obj) : val;\n      if (Dep.target) {\n          if (process.env.NODE_ENV !== 'production') {\n              dep.depend({\n                  target: obj,\n                  type: \"get\" \u002F* TrackOpTypes.GET *\u002F,\n                  key: key\n              });\n          }\n          else {\n              dep.depend();\n          }\n          if (childOb) {\n              childOb.dep.depend();\n              if (isArray(value)) {\n                  dependArray(value);\n              }\n          }\n      }\n      return isRef(value) && !shallow ? value.value : value;\n  },\n  set: function reactiveSetter(newVal) {\n      var value = getter ? getter.call(obj) : val;\n      if (!hasChanged(value, newVal)) {\n          return;\n      }\n      if (process.env.NODE_ENV !== 'production' && customSetter) {\n          customSetter();\n      }\n      if (setter) {\n          setter.call(obj, newVal);\n      }\n      else if (getter) {\n          \u002F\u002F #7981: for accessor properties without setter\n          return;\n      }\n      else if (isRef(value) && !isRef(newVal)) {\n          value.value = newVal;\n          return;\n      }\n      else {\n          val = newVal;\n      }\n      childOb = !shallow && observe(newVal, false, mock);\n      if (process.env.NODE_ENV !== 'production') {\n          dep.notify({\n              type: \"set\" \u002F* TriggerOpTypes.SET *\u002F,\n              target: obj,\n              key: key,\n              newValue: newVal,\n              oldValue: value\n          });\n      }\n      else {\n          dep.notify();\n      }\n  }\n});\n",[27,13114,13115,13126,13135,13144,13157,13186,13193,13209,13219,13224,13237,13242,13247,13252,13259,13267,13271,13278,13287,13300,13308,13313,13317,13321,13349,13353,13372,13394,13408,13415,13419,13437,13444,13448,13455,13465,13469,13479,13484,13490,13494,13516,13526,13532,13536,13542,13551,13555,13580,13594,13604,13617,13623,13629,13635,13641,13647,13652,13659,13668,13673,13678],{"__ignoreMap":83},[87,13116,13117,13120,13123],{"class":89,"line":90},[87,13118,13119],{"class":97},"Object.",[87,13121,13122],{"class":93},"defineProperty",[87,13124,13125],{"class":97},"(obj, key, {\n",[87,13127,13128,13131,13133],{"class":89,"line":101},[87,13129,13130],{"class":97},"  enumerable: ",[87,13132,4139],{"class":104},[87,13134,3413],{"class":97},[87,13136,13137,13140,13142],{"class":89,"line":117},[87,13138,13139],{"class":97},"  configurable: ",[87,13141,4139],{"class":104},[87,13143,3413],{"class":97},[87,13145,13146,13148,13150,13152,13155],{"class":89,"line":130},[87,13147,4202],{"class":93},[87,13149,108],{"class":97},[87,13151,9050],{"class":562},[87,13153,13154],{"class":93}," reactiveGetter",[87,13156,2003],{"class":97},[87,13158,13159,13162,13165,13167,13170,13172,13175,13178,13181,13183],{"class":89,"line":224},[87,13160,13161],{"class":562},"      var",[87,13163,13164],{"class":97}," value ",[87,13166,162],{"class":562},[87,13168,13169],{"class":97}," getter ",[87,13171,6485],{"class":562},[87,13173,13174],{"class":97}," getter.",[87,13176,13177],{"class":93},"call",[87,13179,13180],{"class":97},"(obj) ",[87,13182,1204],{"class":562},[87,13184,13185],{"class":97}," val;\n",[87,13187,13188,13190],{"class":89,"line":246},[87,13189,5804],{"class":562},[87,13191,13192],{"class":97}," (Dep.target) {\n",[87,13194,13195,13198,13200,13202,13205,13207],{"class":89,"line":256},[87,13196,13197],{"class":562},"          if",[87,13199,5807],{"class":97},[87,13201,5810],{"class":104},[87,13203,13204],{"class":562}," !==",[87,13206,5816],{"class":165},[87,13208,4409],{"class":97},[87,13210,13211,13214,13217],{"class":89,"line":270},[87,13212,13213],{"class":97},"              dep.",[87,13215,13216],{"class":93},"depend",[87,13218,5676],{"class":97},[87,13220,13221],{"class":89,"line":280},[87,13222,13223],{"class":97},"                  target: obj,\n",[87,13225,13226,13229,13232,13235],{"class":89,"line":295},[87,13227,13228],{"class":97},"                  type: ",[87,13230,13231],{"class":165},"\"get\"",[87,13233,13234],{"class":1339}," \u002F* TrackOpTypes.GET *\u002F",[87,13236,3413],{"class":97},[87,13238,13239],{"class":89,"line":315},[87,13240,13241],{"class":97},"                  key: key\n",[87,13243,13244],{"class":89,"line":330},[87,13245,13246],{"class":97},"              });\n",[87,13248,13249],{"class":89,"line":351},[87,13250,13251],{"class":97},"          }\n",[87,13253,13254,13257],{"class":89,"line":360},[87,13255,13256],{"class":562},"          else",[87,13258,98],{"class":97},[87,13260,13261,13263,13265],{"class":89,"line":374},[87,13262,13213],{"class":97},[87,13264,13216],{"class":93},[87,13266,2026],{"class":97},[87,13268,13269],{"class":89,"line":383},[87,13270,13251],{"class":97},[87,13272,13273,13275],{"class":89,"line":398},[87,13274,13197],{"class":562},[87,13276,13277],{"class":97}," (childOb) {\n",[87,13279,13280,13283,13285],{"class":89,"line":418},[87,13281,13282],{"class":97},"              childOb.dep.",[87,13284,13216],{"class":93},[87,13286,2026],{"class":97},[87,13288,13289,13292,13294,13297],{"class":89,"line":434},[87,13290,13291],{"class":562},"              if",[87,13293,2658],{"class":97},[87,13295,13296],{"class":93},"isArray",[87,13298,13299],{"class":97},"(value)) {\n",[87,13301,13302,13305],{"class":89,"line":454},[87,13303,13304],{"class":93},"                  dependArray",[87,13306,13307],{"class":97},"(value);\n",[87,13309,13310],{"class":89,"line":463},[87,13311,13312],{"class":97},"              }\n",[87,13314,13315],{"class":89,"line":477},[87,13316,13251],{"class":97},[87,13318,13319],{"class":89,"line":486},[87,13320,5841],{"class":97},[87,13322,13323,13325,13328,13331,13333,13336,13339,13341,13344,13346],{"class":89,"line":3636},[87,13324,4273],{"class":562},[87,13326,13327],{"class":93}," isRef",[87,13329,13330],{"class":97},"(value) ",[87,13332,11085],{"class":562},[87,13334,13335],{"class":562}," !",[87,13337,13338],{"class":97},"shallow ",[87,13340,6485],{"class":562},[87,13342,13343],{"class":97}," value.value ",[87,13345,1204],{"class":562},[87,13347,13348],{"class":97}," value;\n",[87,13350,13351],{"class":89,"line":3641},[87,13352,3907],{"class":97},[87,13354,13355,13358,13360,13362,13365,13367,13370],{"class":89,"line":3653},[87,13356,13357],{"class":93},"  set",[87,13359,108],{"class":97},[87,13361,9050],{"class":562},[87,13363,13364],{"class":93}," reactiveSetter",[87,13366,791],{"class":97},[87,13368,13369],{"class":1168},"newVal",[87,13371,4409],{"class":97},[87,13373,13374,13376,13378,13380,13382,13384,13386,13388,13390,13392],{"class":89,"line":3663},[87,13375,13161],{"class":562},[87,13377,13164],{"class":97},[87,13379,162],{"class":562},[87,13381,13169],{"class":97},[87,13383,6485],{"class":562},[87,13385,13174],{"class":97},[87,13387,13177],{"class":93},[87,13389,13180],{"class":97},[87,13391,1204],{"class":562},[87,13393,13185],{"class":97},[87,13395,13396,13398,13400,13402,13405],{"class":89,"line":3670},[87,13397,5804],{"class":562},[87,13399,2658],{"class":97},[87,13401,2661],{"class":562},[87,13403,13404],{"class":93},"hasChanged",[87,13406,13407],{"class":97},"(value, newVal)) {\n",[87,13409,13410,13413],{"class":89,"line":3681},[87,13411,13412],{"class":562},"          return",[87,13414,114],{"class":97},[87,13416,13417],{"class":89,"line":3692},[87,13418,5841],{"class":97},[87,13420,13421,13423,13425,13427,13429,13431,13434],{"class":89,"line":3703},[87,13422,5804],{"class":562},[87,13424,5807],{"class":97},[87,13426,5810],{"class":104},[87,13428,13204],{"class":562},[87,13430,5816],{"class":165},[87,13432,13433],{"class":562}," &&",[87,13435,13436],{"class":97}," customSetter) {\n",[87,13438,13439,13442],{"class":89,"line":4382},[87,13440,13441],{"class":93},"          customSetter",[87,13443,2026],{"class":97},[87,13445,13446],{"class":89,"line":4387},[87,13447,5841],{"class":97},[87,13449,13450,13452],{"class":89,"line":4392},[87,13451,5804],{"class":562},[87,13453,13454],{"class":97}," (setter) {\n",[87,13456,13457,13460,13462],{"class":89,"line":4412},[87,13458,13459],{"class":97},"          setter.",[87,13461,13177],{"class":93},[87,13463,13464],{"class":97},"(obj, newVal);\n",[87,13466,13467],{"class":89,"line":4424},[87,13468,5841],{"class":97},[87,13470,13471,13474,13476],{"class":89,"line":4429},[87,13472,13473],{"class":562},"      else",[87,13475,9131],{"class":562},[87,13477,13478],{"class":97}," (getter) {\n",[87,13480,13481],{"class":89,"line":4434},[87,13482,13483],{"class":1339},"          \u002F\u002F #7981: for accessor properties without setter\n",[87,13485,13486,13488],{"class":89,"line":4440},[87,13487,13412],{"class":562},[87,13489,114],{"class":97},[87,13491,13492],{"class":89,"line":4450},[87,13493,5841],{"class":97},[87,13495,13496,13498,13500,13502,13505,13507,13509,13511,13513],{"class":89,"line":4478},[87,13497,13473],{"class":562},[87,13499,9131],{"class":562},[87,13501,2658],{"class":97},[87,13503,13504],{"class":93},"isRef",[87,13506,13330],{"class":97},[87,13508,11085],{"class":562},[87,13510,13335],{"class":562},[87,13512,13504],{"class":93},[87,13514,13515],{"class":97},"(newVal)) {\n",[87,13517,13518,13521,13523],{"class":89,"line":4498},[87,13519,13520],{"class":97},"          value.value ",[87,13522,162],{"class":562},[87,13524,13525],{"class":97}," newVal;\n",[87,13527,13528,13530],{"class":89,"line":4504},[87,13529,13412],{"class":562},[87,13531,114],{"class":97},[87,13533,13534],{"class":89,"line":4509},[87,13535,5841],{"class":97},[87,13537,13538,13540],{"class":89,"line":4514},[87,13539,13473],{"class":562},[87,13541,98],{"class":97},[87,13543,13544,13547,13549],{"class":89,"line":4519},[87,13545,13546],{"class":97},"          val ",[87,13548,162],{"class":562},[87,13550,13525],{"class":97},[87,13552,13553],{"class":89,"line":4538},[87,13554,5841],{"class":97},[87,13556,13557,13560,13562,13564,13566,13568,13571,13574,13577],{"class":89,"line":4555},[87,13558,13559],{"class":97},"      childOb ",[87,13561,162],{"class":562},[87,13563,13335],{"class":562},[87,13565,13338],{"class":97},[87,13567,11085],{"class":562},[87,13569,13570],{"class":93}," observe",[87,13572,13573],{"class":97},"(newVal, ",[87,13575,13576],{"class":104},"false",[87,13578,13579],{"class":97},", mock);\n",[87,13581,13582,13584,13586,13588,13590,13592],{"class":89,"line":4560},[87,13583,5804],{"class":562},[87,13585,5807],{"class":97},[87,13587,5810],{"class":104},[87,13589,13204],{"class":562},[87,13591,5816],{"class":165},[87,13593,4409],{"class":97},[87,13595,13596,13599,13602],{"class":89,"line":4565},[87,13597,13598],{"class":97},"          dep.",[87,13600,13601],{"class":93},"notify",[87,13603,5676],{"class":97},[87,13605,13606,13609,13612,13615],{"class":89,"line":4570},[87,13607,13608],{"class":97},"              type: ",[87,13610,13611],{"class":165},"\"set\"",[87,13613,13614],{"class":1339}," \u002F* TriggerOpTypes.SET *\u002F",[87,13616,3413],{"class":97},[87,13618,13620],{"class":89,"line":13619},52,[87,13621,13622],{"class":97},"              target: obj,\n",[87,13624,13626],{"class":89,"line":13625},53,[87,13627,13628],{"class":97},"              key: key,\n",[87,13630,13632],{"class":89,"line":13631},54,[87,13633,13634],{"class":97},"              newValue: newVal,\n",[87,13636,13638],{"class":89,"line":13637},55,[87,13639,13640],{"class":97},"              oldValue: value\n",[87,13642,13644],{"class":89,"line":13643},56,[87,13645,13646],{"class":97},"          });\n",[87,13648,13650],{"class":89,"line":13649},57,[87,13651,5841],{"class":97},[87,13653,13655,13657],{"class":89,"line":13654},58,[87,13656,13473],{"class":562},[87,13658,98],{"class":97},[87,13660,13662,13664,13666],{"class":89,"line":13661},59,[87,13663,13598],{"class":97},[87,13665,13601],{"class":93},[87,13667,2026],{"class":97},[87,13669,13671],{"class":89,"line":13670},60,[87,13672,5841],{"class":97},[87,13674,13676],{"class":89,"line":13675},61,[87,13677,1694],{"class":97},[87,13679,13681],{"class":89,"line":13680},62,[87,13682,1299],{"class":97},[16,13684,13685,13686,6866,13689,13692,13693,6866,13695,13698,13699,13702],{},"해당 함수는 객체의 속성마다 실행되는데, 그 속성에 대한 ",[27,13687,13688],{},"enumerable",[27,13690,13691],{},"configurable"," 과 ",[27,13694,4456],{},[27,13696,13697],{},"set"," 을 ",[27,13700,13701],{},"Object.defineProperty"," 로 추가한다. 반응형인 대상에 변경이 일어나면 아래 과정을 거친다.",[2953,13704,13705,13710,13713,13716,13722,13728,13735],{},[23,13706,13707,13709],{},[27,13708,13697],{}," 함수가 실행된다. (observe 혹은 proxy 를 통해서)",[23,13711,13712],{},"새로운 데이터로 변경이 되었을 때만 진행한다.",[23,13714,13715],{},"새로운 데이터로 값을 바꾼다.",[23,13717,13718,13721],{},[27,13719,13720],{},"observe"," 함수로 새로운 데이터에 대한 observe를 할당한다.",[23,13723,13724,13727],{},[27,13725,13726],{},"dep.notify"," 가 발생하고, 렌더링을 트리깅 한다.",[23,13729,13730,13731,13734],{},"Watcher 가 queue 에 추가하고, ",[27,13732,13733],{},"flushSchedulerQueue"," 를 통해 queue 를 플러시한다.",[23,13736,13737,13738,13741,13742,13745],{},"queue 에 있는 모든 watcher의 ",[27,13739,13740],{},"run"," 함수를 호출하고 vue 인스턴스들의 ",[27,13743,13744],{},"updated"," 훅을 호출한다.",[16,13747,13748],{},[513,13749],{"alt":515,"src":13750},"blog\u002Fimg\u002FVue2-Reactivity\u002FUntitled1.png",[16,13752,13753],{},"위 함수가 실행되는 부분은 다음과 같다.",[20,13755,13756,13759,13762,13765,13768,13775,13778],{},[23,13757,13758],{},"컴포넌트의 props 를 초기화할 때, props 데이터도 반응형이다. (initProps$1)",[23,13760,13761],{},"컴포넌트의 inject 를 초기화할 때 (initInjection)",[23,13763,13764],{},"컴포넌트의 data 를 초기화할 때(initData)",[23,13766,13767],{},"watch 를 등록할 때,",[23,13769,13770,13771,13774],{},"set 등으로 인한 Observer 할당시(",[788,13772,13773],{},"ob",") (observe)",[23,13776,13777],{},"vuex state 초기화 + mutate (initData, set)",[23,13779,13780],{},"…",[753,13782,13784],{"id":13783},"watcher","Watcher",[78,13786,13788],{"className":3379,"code":13787,"language":3381,"meta":83,"style":83},"var Watcher = \u002F** @class *\u002F (function () {\n    Watcher.prototype.addDep = function (dep) {};\n    Watcher.prototype.update = function () {\n        if (this.lazy) {\n            this.dirty = true;\n        }\n        else if (this.sync) {\n            this.run();\n        }\n        else {\n            queueWatcher(this);\n        }\n    };\n\u002F\u002F ...\n  return Watcher;\n}());\n",[27,13789,13790,13815,13842,13861,13873,13888,13892,13906,13916,13920,13926,13937,13941,13945,13950,13957],{"__ignoreMap":83},[87,13791,13792,13794,13797,13799,13802,13805,13808,13810,13812],{"class":89,"line":90},[87,13793,3173],{"class":562},[87,13795,13796],{"class":97}," Watcher ",[87,13798,162],{"class":562},[87,13800,13801],{"class":1339}," \u002F** ",[87,13803,13804],{"class":562},"@class",[87,13806,13807],{"class":1339}," *\u002F",[87,13809,2658],{"class":97},[87,13811,9050],{"class":562},[87,13813,13814],{"class":97}," () {\n",[87,13816,13817,13820,13822,13825,13827,13830,13832,13834,13836,13839],{"class":89,"line":101},[87,13818,13819],{"class":104},"    Watcher",[87,13821,1231],{"class":97},[87,13823,13824],{"class":104},"prototype",[87,13826,1231],{"class":97},[87,13828,13829],{"class":93},"addDep",[87,13831,1362],{"class":562},[87,13833,1997],{"class":562},[87,13835,2658],{"class":97},[87,13837,13838],{"class":1168},"dep",[87,13840,13841],{"class":97},") {};\n",[87,13843,13844,13846,13848,13850,13852,13855,13857,13859],{"class":89,"line":117},[87,13845,13819],{"class":104},[87,13847,1231],{"class":97},[87,13849,13824],{"class":104},[87,13851,1231],{"class":97},[87,13853,13854],{"class":93},"update",[87,13856,1362],{"class":562},[87,13858,1997],{"class":562},[87,13860,13814],{"class":97},[87,13862,13863,13866,13868,13870],{"class":89,"line":130},[87,13864,13865],{"class":562},"        if",[87,13867,2658],{"class":97},[87,13869,4229],{"class":104},[87,13871,13872],{"class":97},".lazy) {\n",[87,13874,13875,13878,13881,13883,13886],{"class":89,"line":224},[87,13876,13877],{"class":104},"            this",[87,13879,13880],{"class":97},".dirty ",[87,13882,162],{"class":562},[87,13884,13885],{"class":104}," true",[87,13887,114],{"class":97},[87,13889,13890],{"class":89,"line":246},[87,13891,2902],{"class":97},[87,13893,13894,13897,13899,13901,13903],{"class":89,"line":256},[87,13895,13896],{"class":562},"        else",[87,13898,9131],{"class":562},[87,13900,2658],{"class":97},[87,13902,4229],{"class":104},[87,13904,13905],{"class":97},".sync) {\n",[87,13907,13908,13910,13912,13914],{"class":89,"line":270},[87,13909,13877],{"class":104},[87,13911,1231],{"class":97},[87,13913,13740],{"class":93},[87,13915,2026],{"class":97},[87,13917,13918],{"class":89,"line":280},[87,13919,2902],{"class":97},[87,13921,13922,13924],{"class":89,"line":295},[87,13923,13896],{"class":562},[87,13925,98],{"class":97},[87,13927,13928,13931,13933,13935],{"class":89,"line":315},[87,13929,13930],{"class":93},"            queueWatcher",[87,13932,791],{"class":97},[87,13934,4229],{"class":104},[87,13936,1590],{"class":97},[87,13938,13939],{"class":89,"line":330},[87,13940,2902],{"class":97},[87,13942,13943],{"class":89,"line":351},[87,13944,9281],{"class":97},[87,13946,13947],{"class":89,"line":360},[87,13948,13949],{"class":1339},"\u002F\u002F ...\n",[87,13951,13952,13954],{"class":89,"line":374},[87,13953,2691],{"class":562},[87,13955,13956],{"class":97}," Watcher;\n",[87,13958,13959],{"class":89,"line":383},[87,13960,13961],{"class":97},"}());\n",[16,13963,13964,13965,13967,13968,13970,13971,13974,13975,13978,13979,13981],{},"우선 코드는 많은 부분을 생략했다. Watcher 의 경우 ",[27,13966,13697],{}," 발생 시 notify 를 전달할 Dependency 들을 관리한다. ",[27,13969,13601],{}," 발생 시  ",[27,13972,13973],{},"queueWatcher"," 를 통해 큐에 자기 자신을 담아서 실행을 기다린다. Watcher가 ",[27,13976,13977],{},"sync"," 속성을 가졌다면 큐에 넣어서 비동기로 작동하지 않고, 동기적으로 바로 ",[27,13980,13740],{}," 을 실행한다.",[78,13983,13985],{"className":3379,"code":13984,"language":3381,"meta":83,"style":83},"function flushSchedulerQueue() {\n    currentFlushTimestamp = getNow();\n    flushing = true;\n    var watcher, id;\n    queue.sort(function (a, b) { return a.id - b.id; });\n    for (index$1 = 0; index$1 \u003C queue.length; index$1++) {\n        watcher = queue[index$1];\n        if (watcher.before) {\n            watcher.before();\n        }\n        id = watcher.id;\n        has[id] = null;\n        watcher.run();\n        if (process.env.NODE_ENV !== 'production' && has[id] != null) {\n            circular[id] = (circular[id] || 0) + 1;\n            if (circular[id] > MAX_UPDATE_COUNT) {\n                warn$2('You may have an infinite update loop ' +\n                    (watcher.user\n                        ? \"in watcher with expression \\\"\".concat(watcher.expression, \"\\\"\")\n                        : \"in a component render function.\"), watcher.vm);\n                break;\n            }\n        }\n    }\n    var activatedQueue = activatedChildren.slice();\n    var updatedQueue = queue.slice();\n    resetSchedulerState();\n    callActivatedHooks(activatedQueue);\n    callUpdatedHooks(updatedQueue);\n}\n",[27,13986,13987,13996,14008,14019,14027,14060,14089,14099,14106,14116,14120,14130,14141,14150,14174,14197,14212,14225,14230,14259,14270,14277,14282,14286,14290,14306,14321,14328,14336,14344],{"__ignoreMap":83},[87,13988,13989,13991,13994],{"class":89,"line":90},[87,13990,9050],{"class":562},[87,13992,13993],{"class":93}," flushSchedulerQueue",[87,13995,2003],{"class":97},[87,13997,13998,14001,14003,14006],{"class":89,"line":101},[87,13999,14000],{"class":97},"    currentFlushTimestamp ",[87,14002,162],{"class":562},[87,14004,14005],{"class":93}," getNow",[87,14007,2026],{"class":97},[87,14009,14010,14013,14015,14017],{"class":89,"line":117},[87,14011,14012],{"class":97},"    flushing ",[87,14014,162],{"class":562},[87,14016,13885],{"class":104},[87,14018,114],{"class":97},[87,14020,14021,14024],{"class":89,"line":130},[87,14022,14023],{"class":562},"    var",[87,14025,14026],{"class":97}," watcher, id;\n",[87,14028,14029,14032,14035,14037,14039,14041,14043,14045,14048,14050,14052,14055,14057],{"class":89,"line":224},[87,14030,14031],{"class":97},"    queue.",[87,14033,14034],{"class":93},"sort",[87,14036,791],{"class":97},[87,14038,9050],{"class":562},[87,14040,2658],{"class":97},[87,14042,2943],{"class":1168},[87,14044,60],{"class":97},[87,14046,14047],{"class":1168},"b",[87,14049,9143],{"class":97},[87,14051,5438],{"class":562},[87,14053,14054],{"class":97}," a.id ",[87,14056,4835],{"class":562},[87,14058,14059],{"class":97}," b.id; });\n",[87,14061,14062,14065,14068,14070,14072,14075,14077,14080,14082,14085,14087],{"class":89,"line":246},[87,14063,14064],{"class":562},"    for",[87,14066,14067],{"class":97}," (index$1 ",[87,14069,162],{"class":562},[87,14071,4265],{"class":104},[87,14073,14074],{"class":97},"; index$1 ",[87,14076,152],{"class":562},[87,14078,14079],{"class":97}," queue.",[87,14081,4259],{"class":104},[87,14083,14084],{"class":97},"; index$1",[87,14086,12376],{"class":562},[87,14088,4409],{"class":97},[87,14090,14091,14094,14096],{"class":89,"line":256},[87,14092,14093],{"class":97},"        watcher ",[87,14095,162],{"class":562},[87,14097,14098],{"class":97}," queue[index$1];\n",[87,14100,14101,14103],{"class":89,"line":270},[87,14102,13865],{"class":562},[87,14104,14105],{"class":97}," (watcher.before) {\n",[87,14107,14108,14111,14114],{"class":89,"line":280},[87,14109,14110],{"class":97},"            watcher.",[87,14112,14113],{"class":93},"before",[87,14115,2026],{"class":97},[87,14117,14118],{"class":89,"line":295},[87,14119,2902],{"class":97},[87,14121,14122,14125,14127],{"class":89,"line":315},[87,14123,14124],{"class":97},"        id ",[87,14126,162],{"class":562},[87,14128,14129],{"class":97}," watcher.id;\n",[87,14131,14132,14135,14137,14139],{"class":89,"line":330},[87,14133,14134],{"class":97},"        has[id] ",[87,14136,162],{"class":562},[87,14138,1359],{"class":104},[87,14140,114],{"class":97},[87,14142,14143,14146,14148],{"class":89,"line":351},[87,14144,14145],{"class":97},"        watcher.",[87,14147,13740],{"class":93},[87,14149,2026],{"class":97},[87,14151,14152,14154,14156,14158,14160,14162,14164,14167,14170,14172],{"class":89,"line":360},[87,14153,13865],{"class":562},[87,14155,5807],{"class":97},[87,14157,5810],{"class":104},[87,14159,13204],{"class":562},[87,14161,5816],{"class":165},[87,14163,13433],{"class":562},[87,14165,14166],{"class":97}," has[id] ",[87,14168,14169],{"class":562},"!=",[87,14171,1359],{"class":104},[87,14173,4409],{"class":97},[87,14175,14176,14179,14181,14184,14186,14188,14190,14192,14195],{"class":89,"line":374},[87,14177,14178],{"class":97},"            circular[id] ",[87,14180,162],{"class":562},[87,14182,14183],{"class":97}," (circular[id] ",[87,14185,5630],{"class":562},[87,14187,4265],{"class":104},[87,14189,1172],{"class":97},[87,14191,6787],{"class":562},[87,14193,14194],{"class":104}," 1",[87,14196,114],{"class":97},[87,14198,14199,14202,14204,14207,14210],{"class":89,"line":383},[87,14200,14201],{"class":562},"            if",[87,14203,14183],{"class":97},[87,14205,14206],{"class":562},">",[87,14208,14209],{"class":104}," MAX_UPDATE_COUNT",[87,14211,4409],{"class":97},[87,14213,14214,14217,14219,14222],{"class":89,"line":398},[87,14215,14216],{"class":93},"                warn$2",[87,14218,791],{"class":97},[87,14220,14221],{"class":165},"'You may have an infinite update loop '",[87,14223,14224],{"class":562}," +\n",[87,14226,14227],{"class":89,"line":418},[87,14228,14229],{"class":97},"                    (watcher.user\n",[87,14231,14232,14235,14238,14241,14243,14245,14248,14251,14253,14255,14257],{"class":89,"line":434},[87,14233,14234],{"class":562},"                        ?",[87,14236,14237],{"class":165}," \"in watcher with expression ",[87,14239,14240],{"class":104},"\\\"",[87,14242,2135],{"class":165},[87,14244,1231],{"class":97},[87,14246,14247],{"class":93},"concat",[87,14249,14250],{"class":97},"(watcher.expression, ",[87,14252,2135],{"class":165},[87,14254,14240],{"class":104},[87,14256,2135],{"class":165},[87,14258,5501],{"class":97},[87,14260,14261,14264,14267],{"class":89,"line":454},[87,14262,14263],{"class":562},"                        :",[87,14265,14266],{"class":165}," \"in a component render function.\"",[87,14268,14269],{"class":97},"), watcher.vm);\n",[87,14271,14272,14275],{"class":89,"line":463},[87,14273,14274],{"class":562},"                break",[87,14276,114],{"class":97},[87,14278,14279],{"class":89,"line":477},[87,14280,14281],{"class":97},"            }\n",[87,14283,14284],{"class":89,"line":486},[87,14285,2902],{"class":97},[87,14287,14288],{"class":89,"line":3636},[87,14289,1689],{"class":97},[87,14291,14292,14294,14297,14299,14302,14304],{"class":89,"line":3641},[87,14293,14023],{"class":562},[87,14295,14296],{"class":97}," activatedQueue ",[87,14298,162],{"class":562},[87,14300,14301],{"class":97}," activatedChildren.",[87,14303,6808],{"class":93},[87,14305,2026],{"class":97},[87,14307,14308,14310,14313,14315,14317,14319],{"class":89,"line":3653},[87,14309,14023],{"class":562},[87,14311,14312],{"class":97}," updatedQueue ",[87,14314,162],{"class":562},[87,14316,14079],{"class":97},[87,14318,6808],{"class":93},[87,14320,2026],{"class":97},[87,14322,14323,14326],{"class":89,"line":3663},[87,14324,14325],{"class":93},"    resetSchedulerState",[87,14327,2026],{"class":97},[87,14329,14330,14333],{"class":89,"line":3670},[87,14331,14332],{"class":93},"    callActivatedHooks",[87,14334,14335],{"class":97},"(activatedQueue);\n",[87,14337,14338,14341],{"class":89,"line":3681},[87,14339,14340],{"class":93},"    callUpdatedHooks",[87,14342,14343],{"class":97},"(updatedQueue);\n",[87,14345,14346],{"class":89,"line":3692},[87,14347,133],{"class":97},[16,14349,14350],{},"여기서 큐의 Watcher 들을 실행하기 전에 정렬을 하는데, 이유는 3가지가 있다.",[2953,14352,14353,14356,14359],{},[23,14354,14355],{},"컴포넌트들이 부모부터 자식 순서로 업데이트되도록 한다. (부모가 자식보다 전에 만들어지기 때문에)",[23,14357,14358],{},"컴포넌트의 Watcher들은 render watcher 의 Watcher보다 먼저 업데이트한다.",[23,14360,14361],{},"부모 컴포넌트의 watcher 가 실행될 때 자식 컴포넌트가 파괴되었다면 그 watcher 들은 스킵될 수 있다.",[16,14363,14364,14365,13692,14367,14369,14370,14373],{},"queue 에 있는 watcher를 순회하며 ",[27,14366,14113],{},[27,14368,13740],{}," 을 차례대로 호출한다. 여기서 before 은 ",[27,14371,14372],{},"beforeUpdated"," 훅을 발생시킨다.",[16,14375,14376,14377,14380,14381,14384,14385,14388,14389,14392],{},"모두 완료되면 ",[27,14378,14379],{},"activateChildComponent"," 을 통해 ",[27,14382,14383],{},"_inactive"," 를 켜고 ",[788,14386,14387],{},"activated"," 훅을 발생시킨다. 대상 컴포넌트는 ",[27,14390,14391],{},"keepAlive"," 속성을 가진 컴포넌트들이다.",[16,14394,14395,14396,14399,14400,14373],{},"그 다음으로는 ",[27,14397,14398],{},"callUpdatedHooks"," 를 통해 컴포넌트의 ",[27,14401,13744],{},[78,14403,14405],{"className":3379,"code":14404,"language":3381,"meta":83,"style":83},"Watcher.prototype.run = function () {\n    if (this.active) {\n    var value = this.get();\n    if (value !== this.value ||\n      \u002F\u002F Deep watchers and watchers on Object\u002FArrays should fire even\n      \u002F\u002F when the value is the same, because the value may\n      \u002F\u002F have mutated.\n      isObject(value) || this.deep) {\n      \u002F\u002F set new value\n      var oldValue = this.value;\n      this.value = value;\n      if (this.user) {\n        var info = \"callback for watcher \\\"\".concat(this.expression, \"\\\"\");\n        invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);\n      }\n      else {\n        this.cb.call(this.vm, value, oldValue);\n      }\n    }\n    }\n};\n",[27,14406,14407,14425,14436,14452,14470,14475,14480,14485,14499,14504,14518,14528,14539,14575,14597,14601,14607,14624,14628,14632,14636],{"__ignoreMap":83},[87,14408,14409,14411,14413,14415,14417,14419,14421,14423],{"class":89,"line":90},[87,14410,13784],{"class":104},[87,14412,1231],{"class":97},[87,14414,13824],{"class":104},[87,14416,1231],{"class":97},[87,14418,13740],{"class":93},[87,14420,1362],{"class":562},[87,14422,1997],{"class":562},[87,14424,13814],{"class":97},[87,14426,14427,14429,14431,14433],{"class":89,"line":101},[87,14428,4224],{"class":562},[87,14430,2658],{"class":97},[87,14432,4229],{"class":104},[87,14434,14435],{"class":97},".active) {\n",[87,14437,14438,14440,14442,14444,14446,14448,14450],{"class":89,"line":117},[87,14439,14023],{"class":562},[87,14441,13164],{"class":97},[87,14443,162],{"class":562},[87,14445,4243],{"class":104},[87,14447,1231],{"class":97},[87,14449,4456],{"class":93},[87,14451,2026],{"class":97},[87,14453,14454,14456,14459,14462,14464,14467],{"class":89,"line":130},[87,14455,4224],{"class":562},[87,14457,14458],{"class":97}," (value ",[87,14460,14461],{"class":562},"!==",[87,14463,4243],{"class":104},[87,14465,14466],{"class":97},".value ",[87,14468,14469],{"class":562},"||\n",[87,14471,14472],{"class":89,"line":224},[87,14473,14474],{"class":1339},"      \u002F\u002F Deep watchers and watchers on Object\u002FArrays should fire even\n",[87,14476,14477],{"class":89,"line":246},[87,14478,14479],{"class":1339},"      \u002F\u002F when the value is the same, because the value may\n",[87,14481,14482],{"class":89,"line":256},[87,14483,14484],{"class":1339},"      \u002F\u002F have mutated.\n",[87,14486,14487,14490,14492,14494,14496],{"class":89,"line":270},[87,14488,14489],{"class":93},"      isObject",[87,14491,13330],{"class":97},[87,14493,5630],{"class":562},[87,14495,4243],{"class":104},[87,14497,14498],{"class":97},".deep) {\n",[87,14500,14501],{"class":89,"line":280},[87,14502,14503],{"class":1339},"      \u002F\u002F set new value\n",[87,14505,14506,14508,14511,14513,14515],{"class":89,"line":295},[87,14507,13161],{"class":562},[87,14509,14510],{"class":97}," oldValue ",[87,14512,162],{"class":562},[87,14514,4243],{"class":104},[87,14516,14517],{"class":97},".value;\n",[87,14519,14520,14522,14524,14526],{"class":89,"line":315},[87,14521,4481],{"class":104},[87,14523,14466],{"class":97},[87,14525,162],{"class":562},[87,14527,13348],{"class":97},[87,14529,14530,14532,14534,14536],{"class":89,"line":330},[87,14531,5804],{"class":562},[87,14533,2658],{"class":97},[87,14535,4229],{"class":104},[87,14537,14538],{"class":97},".user) {\n",[87,14540,14541,14544,14547,14549,14552,14554,14556,14558,14560,14562,14564,14567,14569,14571,14573],{"class":89,"line":351},[87,14542,14543],{"class":562},"        var",[87,14545,14546],{"class":97}," info ",[87,14548,162],{"class":562},[87,14550,14551],{"class":165}," \"callback for watcher ",[87,14553,14240],{"class":104},[87,14555,2135],{"class":165},[87,14557,1231],{"class":97},[87,14559,14247],{"class":93},[87,14561,791],{"class":97},[87,14563,4229],{"class":104},[87,14565,14566],{"class":97},".expression, ",[87,14568,2135],{"class":165},[87,14570,14240],{"class":104},[87,14572,2135],{"class":165},[87,14574,1590],{"class":97},[87,14576,14577,14580,14582,14584,14587,14589,14592,14594],{"class":89,"line":360},[87,14578,14579],{"class":93},"        invokeWithErrorHandling",[87,14581,791],{"class":97},[87,14583,4229],{"class":104},[87,14585,14586],{"class":97},".cb, ",[87,14588,4229],{"class":104},[87,14590,14591],{"class":97},".vm, [value, oldValue], ",[87,14593,4229],{"class":104},[87,14595,14596],{"class":97},".vm, info);\n",[87,14598,14599],{"class":89,"line":374},[87,14600,5841],{"class":97},[87,14602,14603,14605],{"class":89,"line":383},[87,14604,13473],{"class":562},[87,14606,98],{"class":97},[87,14608,14609,14612,14615,14617,14619,14621],{"class":89,"line":398},[87,14610,14611],{"class":104},"        this",[87,14613,14614],{"class":97},".cb.",[87,14616,13177],{"class":93},[87,14618,791],{"class":97},[87,14620,4229],{"class":104},[87,14622,14623],{"class":97},".vm, value, oldValue);\n",[87,14625,14626],{"class":89,"line":418},[87,14627,5841],{"class":97},[87,14629,14630],{"class":89,"line":434},[87,14631,1689],{"class":97},[87,14633,14634],{"class":89,"line":454},[87,14635,1689],{"class":97},[87,14637,14638],{"class":89,"line":463},[87,14639,14640],{"class":97},"};\n",[16,14642,14643,14644,14646,14647,14649],{},"결국은 Watcher 의 ",[27,14645,13740],{}," 함수를 위해서 위 과정들을 열심히 진행한 것이다. 그렇다면 ",[27,14648,13740],{}," 함수는 내부적으로 어떤 것을 처리할까?",[16,14651,14652,14653,14656,14657,14659],{},"디버깅을 걸어보면 알겠지만, 화면의 렌더링은 ",[27,14654,14655],{},"this.get"," 에서 끝난다. 그리고 그 뒤로는 콜백을 실행하는 것 뿐이다. 그럼 ",[27,14658,4456],{}," 함수를 살펴봐야 한다.",[78,14661,14663],{"className":3379,"code":14662,"language":3381,"meta":83,"style":83},"Watcher.prototype.get = function () {\n  pushTarget(this);\n  var value;\n  var vm = this.vm;\n  try {\n    value = this.getter.call(vm, vm);\n  }\n  catch (e) {\n    if (this.user) {\n      handleError(e, vm, \"getter for watcher \\\"\".concat(this.expression, \"\\\"\"));\n    }\n    else {\n      throw e;\n    }\n  }\n  finally {\n    \u002F\u002F \"touch\" every property so they are all tracked as\n    \u002F\u002F dependencies for deep watching\n    if (this.deep) {\n      traverse(value);\n    }\n    popTarget();\n    this.cleanupDeps();\n  }\n  return value;\n};\n",[27,14664,14665,14683,14694,14701,14715,14721,14738,14742,14750,14760,14794,14798,14804,14812,14816,14820,14827,14832,14837,14847,14854,14858,14865,14876,14880,14886],{"__ignoreMap":83},[87,14666,14667,14669,14671,14673,14675,14677,14679,14681],{"class":89,"line":90},[87,14668,13784],{"class":104},[87,14670,1231],{"class":97},[87,14672,13824],{"class":104},[87,14674,1231],{"class":97},[87,14676,4456],{"class":93},[87,14678,1362],{"class":562},[87,14680,1997],{"class":562},[87,14682,13814],{"class":97},[87,14684,14685,14688,14690,14692],{"class":89,"line":101},[87,14686,14687],{"class":93},"  pushTarget",[87,14689,791],{"class":97},[87,14691,4229],{"class":104},[87,14693,1590],{"class":97},[87,14695,14696,14699],{"class":89,"line":117},[87,14697,14698],{"class":562},"  var",[87,14700,13348],{"class":97},[87,14702,14703,14705,14708,14710,14712],{"class":89,"line":130},[87,14704,14698],{"class":562},[87,14706,14707],{"class":97}," vm ",[87,14709,162],{"class":562},[87,14711,4243],{"class":104},[87,14713,14714],{"class":97},".vm;\n",[87,14716,14717,14719],{"class":89,"line":224},[87,14718,2008],{"class":562},[87,14720,98],{"class":97},[87,14722,14723,14726,14728,14730,14733,14735],{"class":89,"line":246},[87,14724,14725],{"class":97},"    value ",[87,14727,162],{"class":562},[87,14729,4243],{"class":104},[87,14731,14732],{"class":97},".getter.",[87,14734,13177],{"class":93},[87,14736,14737],{"class":97},"(vm, vm);\n",[87,14739,14740],{"class":89,"line":256},[87,14741,1694],{"class":97},[87,14743,14744,14747],{"class":89,"line":270},[87,14745,14746],{"class":562},"  catch",[87,14748,14749],{"class":97}," (e) {\n",[87,14751,14752,14754,14756,14758],{"class":89,"line":280},[87,14753,4224],{"class":562},[87,14755,2658],{"class":97},[87,14757,4229],{"class":104},[87,14759,14538],{"class":97},[87,14761,14762,14765,14768,14771,14773,14775,14777,14779,14781,14783,14785,14787,14789,14791],{"class":89,"line":295},[87,14763,14764],{"class":93},"      handleError",[87,14766,14767],{"class":97},"(e, vm, ",[87,14769,14770],{"class":165},"\"getter for watcher ",[87,14772,14240],{"class":104},[87,14774,2135],{"class":165},[87,14776,1231],{"class":97},[87,14778,14247],{"class":93},[87,14780,791],{"class":97},[87,14782,4229],{"class":104},[87,14784,14566],{"class":97},[87,14786,2135],{"class":165},[87,14788,14240],{"class":104},[87,14790,2135],{"class":165},[87,14792,14793],{"class":97},"));\n",[87,14795,14796],{"class":89,"line":315},[87,14797,1689],{"class":97},[87,14799,14800,14802],{"class":89,"line":330},[87,14801,4286],{"class":562},[87,14803,98],{"class":97},[87,14805,14806,14809],{"class":89,"line":351},[87,14807,14808],{"class":562},"      throw",[87,14810,14811],{"class":97}," e;\n",[87,14813,14814],{"class":89,"line":360},[87,14815,1689],{"class":97},[87,14817,14818],{"class":89,"line":374},[87,14819,1694],{"class":97},[87,14821,14822,14825],{"class":89,"line":383},[87,14823,14824],{"class":562},"  finally",[87,14826,98],{"class":97},[87,14828,14829],{"class":89,"line":398},[87,14830,14831],{"class":1339},"    \u002F\u002F \"touch\" every property so they are all tracked as\n",[87,14833,14834],{"class":89,"line":418},[87,14835,14836],{"class":1339},"    \u002F\u002F dependencies for deep watching\n",[87,14838,14839,14841,14843,14845],{"class":89,"line":434},[87,14840,4224],{"class":562},[87,14842,2658],{"class":97},[87,14844,4229],{"class":104},[87,14846,14498],{"class":97},[87,14848,14849,14852],{"class":89,"line":454},[87,14850,14851],{"class":93},"      traverse",[87,14853,13307],{"class":97},[87,14855,14856],{"class":89,"line":463},[87,14857,1689],{"class":97},[87,14859,14860,14863],{"class":89,"line":477},[87,14861,14862],{"class":93},"    popTarget",[87,14864,2026],{"class":97},[87,14866,14867,14869,14871,14874],{"class":89,"line":486},[87,14868,4371],{"class":104},[87,14870,1231],{"class":97},[87,14872,14873],{"class":93},"cleanupDeps",[87,14875,2026],{"class":97},[87,14877,14878],{"class":89,"line":3636},[87,14879,1694],{"class":97},[87,14881,14882,14884],{"class":89,"line":3641},[87,14883,2691],{"class":562},[87,14885,13348],{"class":97},[87,14887,14888],{"class":89,"line":3653},[87,14889,14640],{"class":97},[16,14891,14892,14893,14896,14897,14899],{},"단순히 ",[27,14894,14895],{},"this.getter"," 를 호출한다. 이 ",[27,14898,13088],{}," 함수는 Watcher 초기화 시 외부로부터 받는다.",[78,14901,14903],{"className":3379,"code":14902,"language":3381,"meta":83,"style":83},"function mountComponent(vm, el, hydrating) {\n  vm.$el = el;\n  if (!vm.$options.render) {\n    vm.$options.render = createEmptyVNode;\n  }\n  callHook$1(vm, 'beforeMount');\n  var updateComponent;\n  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {\n    updateComponent = function () {\n      var name = vm._name;\n      var id = vm._uid;\n      var startTag = \"vue-perf-start:\".concat(id);\n      var endTag = \"vue-perf-end:\".concat(id);\n      mark(startTag);\n      **var vnode = vm._render();**\n      mark(endTag);\n      measure(\"vue \".concat(name, \" render\"), startTag, endTag);\n      mark(startTag);\n      vm._update(vnode, hydrating);\n      mark(endTag);\n      measure(\"vue \".concat(name, \" patch\"), startTag, endTag);\n    };\n  }\n  else {\n    updateComponent = function () {\n      vm._update(vm._render(), hydrating);\n    };\n  }\n  var watcherOptions = {\n    before: function () {\n      if (vm._isMounted && !vm._isDestroyed) {\n        callHook$1(vm, 'beforeUpdate');\n      }\n    }\n  };\n  if (process.env.NODE_ENV !== 'production') {\n    watcherOptions.onTrack = function (e) { return callHook$1(vm, 'renderTracked', [e]); };\n    watcherOptions.onTrigger = function (e) { return callHook$1(vm, 'renderTriggered', [e]); };\n  }\n  \u002F\u002F we set this to vm._watcher inside the watcher's constructor\n  \u002F\u002F since the watcher's initial patch may call $forceUpdate (e.g. inside child\n  \u002F\u002F component's mounted hook), which relies on vm._watcher being already defined\n  new Watcher(vm, updateComponent, noop, watcherOptions, true \u002F* isRenderWatcher *\u002F);\n  hydrating = false;\n  \u002F\u002F flush buffer for flush: \"pre\" watchers queued in setup()\n  var preWatchers = vm._preWatchers;\n  if (preWatchers) {\n    for (var i = 0; i \u003C preWatchers.length; i++) {\n      preWatchers[i].run();\n    }\n  }\n  if (vm.$vnode == null) {\n    vm._isMounted = true;\n    callHook$1(vm, 'mounted');\n  }\n  return vm;\n}\n",[27,14904,14905,14929,14939,14950,14960,14964,14977,14984,15006,15017,15029,15041,15060,15078,15086,15107,15114,15137,15143,15154,15160,15179,15183,15187,15194,15204,15218,15222,15226,15237,15248,15262,15274,15278,15282,15287,15301,15333,15361,15365,15370,15375,15380,15397,15409,15414,15426,15433,15464,15473,15477,15481,15494,15505,15517,15521,15528],{"__ignoreMap":83},[87,14906,14907,14909,14912,14914,14917,14919,14922,14924,14927],{"class":89,"line":90},[87,14908,9050],{"class":562},[87,14910,14911],{"class":93}," mountComponent",[87,14913,791],{"class":97},[87,14915,14916],{"class":1168},"vm",[87,14918,60],{"class":97},[87,14920,14921],{"class":1168},"el",[87,14923,60],{"class":97},[87,14925,14926],{"class":1168},"hydrating",[87,14928,4409],{"class":97},[87,14930,14931,14934,14936],{"class":89,"line":101},[87,14932,14933],{"class":97},"  vm.$el ",[87,14935,162],{"class":562},[87,14937,14938],{"class":97}," el;\n",[87,14940,14941,14943,14945,14947],{"class":89,"line":117},[87,14942,2435],{"class":562},[87,14944,2658],{"class":97},[87,14946,2661],{"class":562},[87,14948,14949],{"class":97},"vm.$options.render) {\n",[87,14951,14952,14955,14957],{"class":89,"line":130},[87,14953,14954],{"class":97},"    vm.$options.render ",[87,14956,162],{"class":562},[87,14958,14959],{"class":97}," createEmptyVNode;\n",[87,14961,14962],{"class":89,"line":224},[87,14963,1694],{"class":97},[87,14965,14966,14969,14972,14975],{"class":89,"line":246},[87,14967,14968],{"class":93},"  callHook$1",[87,14970,14971],{"class":97},"(vm, ",[87,14973,14974],{"class":165},"'beforeMount'",[87,14976,1590],{"class":97},[87,14978,14979,14981],{"class":89,"line":256},[87,14980,14698],{"class":562},[87,14982,14983],{"class":97}," updateComponent;\n",[87,14985,14986,14988,14990,14992,14994,14996,14998,15001,15003],{"class":89,"line":270},[87,14987,2435],{"class":562},[87,14989,5807],{"class":97},[87,14991,5810],{"class":104},[87,14993,13204],{"class":562},[87,14995,5816],{"class":165},[87,14997,13433],{"class":562},[87,14999,15000],{"class":97}," config.performance ",[87,15002,11085],{"class":562},[87,15004,15005],{"class":97}," mark) {\n",[87,15007,15008,15011,15013,15015],{"class":89,"line":280},[87,15009,15010],{"class":93},"    updateComponent",[87,15012,1362],{"class":562},[87,15014,1997],{"class":562},[87,15016,13814],{"class":97},[87,15018,15019,15021,15024,15026],{"class":89,"line":295},[87,15020,13161],{"class":562},[87,15022,15023],{"class":97}," name ",[87,15025,162],{"class":562},[87,15027,15028],{"class":97}," vm._name;\n",[87,15030,15031,15033,15036,15038],{"class":89,"line":315},[87,15032,13161],{"class":562},[87,15034,15035],{"class":97}," id ",[87,15037,162],{"class":562},[87,15039,15040],{"class":97}," vm._uid;\n",[87,15042,15043,15045,15048,15050,15053,15055,15057],{"class":89,"line":330},[87,15044,13161],{"class":562},[87,15046,15047],{"class":97}," startTag ",[87,15049,162],{"class":562},[87,15051,15052],{"class":165}," \"vue-perf-start:\"",[87,15054,1231],{"class":97},[87,15056,14247],{"class":93},[87,15058,15059],{"class":97},"(id);\n",[87,15061,15062,15064,15067,15069,15072,15074,15076],{"class":89,"line":351},[87,15063,13161],{"class":562},[87,15065,15066],{"class":97}," endTag ",[87,15068,162],{"class":562},[87,15070,15071],{"class":165}," \"vue-perf-end:\"",[87,15073,1231],{"class":97},[87,15075,14247],{"class":93},[87,15077,15059],{"class":97},[87,15079,15080,15083],{"class":89,"line":360},[87,15081,15082],{"class":93},"      mark",[87,15084,15085],{"class":97},"(startTag);\n",[87,15087,15088,15091,15094,15096,15099,15102,15105],{"class":89,"line":374},[87,15089,15090],{"class":562},"      **var",[87,15092,15093],{"class":97}," vnode ",[87,15095,162],{"class":562},[87,15097,15098],{"class":97}," vm.",[87,15100,15101],{"class":93},"_render",[87,15103,15104],{"class":97},"();",[87,15106,4943],{"class":562},[87,15108,15109,15111],{"class":89,"line":383},[87,15110,15082],{"class":93},[87,15112,15113],{"class":97},"(endTag);\n",[87,15115,15116,15119,15121,15124,15126,15128,15131,15134],{"class":89,"line":398},[87,15117,15118],{"class":93},"      measure",[87,15120,791],{"class":97},[87,15122,15123],{"class":165},"\"vue \"",[87,15125,1231],{"class":97},[87,15127,14247],{"class":93},[87,15129,15130],{"class":97},"(name, ",[87,15132,15133],{"class":165},"\" render\"",[87,15135,15136],{"class":97},"), startTag, endTag);\n",[87,15138,15139,15141],{"class":89,"line":418},[87,15140,15082],{"class":93},[87,15142,15085],{"class":97},[87,15144,15145,15148,15151],{"class":89,"line":434},[87,15146,15147],{"class":97},"      vm.",[87,15149,15150],{"class":93},"_update",[87,15152,15153],{"class":97},"(vnode, hydrating);\n",[87,15155,15156,15158],{"class":89,"line":454},[87,15157,15082],{"class":93},[87,15159,15113],{"class":97},[87,15161,15162,15164,15166,15168,15170,15172,15174,15177],{"class":89,"line":463},[87,15163,15118],{"class":93},[87,15165,791],{"class":97},[87,15167,15123],{"class":165},[87,15169,1231],{"class":97},[87,15171,14247],{"class":93},[87,15173,15130],{"class":97},[87,15175,15176],{"class":165},"\" patch\"",[87,15178,15136],{"class":97},[87,15180,15181],{"class":89,"line":477},[87,15182,9281],{"class":97},[87,15184,15185],{"class":89,"line":486},[87,15186,1694],{"class":97},[87,15188,15189,15192],{"class":89,"line":3636},[87,15190,15191],{"class":562},"  else",[87,15193,98],{"class":97},[87,15195,15196,15198,15200,15202],{"class":89,"line":3641},[87,15197,15010],{"class":93},[87,15199,1362],{"class":562},[87,15201,1997],{"class":562},[87,15203,13814],{"class":97},[87,15205,15206,15208,15210,15213,15215],{"class":89,"line":3653},[87,15207,15147],{"class":97},[87,15209,15150],{"class":93},[87,15211,15212],{"class":97},"(vm.",[87,15214,15101],{"class":93},[87,15216,15217],{"class":97},"(), hydrating);\n",[87,15219,15220],{"class":89,"line":3663},[87,15221,9281],{"class":97},[87,15223,15224],{"class":89,"line":3670},[87,15225,1694],{"class":97},[87,15227,15228,15230,15233,15235],{"class":89,"line":3681},[87,15229,14698],{"class":562},[87,15231,15232],{"class":97}," watcherOptions ",[87,15234,162],{"class":562},[87,15236,98],{"class":97},[87,15238,15239,15242,15244,15246],{"class":89,"line":3692},[87,15240,15241],{"class":93},"    before",[87,15243,108],{"class":97},[87,15245,9050],{"class":562},[87,15247,13814],{"class":97},[87,15249,15250,15252,15255,15257,15259],{"class":89,"line":3703},[87,15251,5804],{"class":562},[87,15253,15254],{"class":97}," (vm._isMounted ",[87,15256,11085],{"class":562},[87,15258,13335],{"class":562},[87,15260,15261],{"class":97},"vm._isDestroyed) {\n",[87,15263,15264,15267,15269,15272],{"class":89,"line":4382},[87,15265,15266],{"class":93},"        callHook$1",[87,15268,14971],{"class":97},[87,15270,15271],{"class":165},"'beforeUpdate'",[87,15273,1590],{"class":97},[87,15275,15276],{"class":89,"line":4387},[87,15277,5841],{"class":97},[87,15279,15280],{"class":89,"line":4392},[87,15281,1689],{"class":97},[87,15283,15284],{"class":89,"line":4412},[87,15285,15286],{"class":97},"  };\n",[87,15288,15289,15291,15293,15295,15297,15299],{"class":89,"line":4424},[87,15290,2435],{"class":562},[87,15292,5807],{"class":97},[87,15294,5810],{"class":104},[87,15296,13204],{"class":562},[87,15298,5816],{"class":165},[87,15300,4409],{"class":97},[87,15302,15303,15306,15309,15311,15313,15315,15318,15320,15322,15325,15327,15330],{"class":89,"line":4429},[87,15304,15305],{"class":97},"    watcherOptions.",[87,15307,15308],{"class":93},"onTrack",[87,15310,1362],{"class":562},[87,15312,1997],{"class":562},[87,15314,2658],{"class":97},[87,15316,15317],{"class":1168},"e",[87,15319,9143],{"class":97},[87,15321,5438],{"class":562},[87,15323,15324],{"class":93}," callHook$1",[87,15326,14971],{"class":97},[87,15328,15329],{"class":165},"'renderTracked'",[87,15331,15332],{"class":97},", [e]); };\n",[87,15334,15335,15337,15340,15342,15344,15346,15348,15350,15352,15354,15356,15359],{"class":89,"line":4434},[87,15336,15305],{"class":97},[87,15338,15339],{"class":93},"onTrigger",[87,15341,1362],{"class":562},[87,15343,1997],{"class":562},[87,15345,2658],{"class":97},[87,15347,15317],{"class":1168},[87,15349,9143],{"class":97},[87,15351,5438],{"class":562},[87,15353,15324],{"class":93},[87,15355,14971],{"class":97},[87,15357,15358],{"class":165},"'renderTriggered'",[87,15360,15332],{"class":97},[87,15362,15363],{"class":89,"line":4440},[87,15364,1694],{"class":97},[87,15366,15367],{"class":89,"line":4450},[87,15368,15369],{"class":1339},"  \u002F\u002F we set this to vm._watcher inside the watcher's constructor\n",[87,15371,15372],{"class":89,"line":4478},[87,15373,15374],{"class":1339},"  \u002F\u002F since the watcher's initial patch may call $forceUpdate (e.g. inside child\n",[87,15376,15377],{"class":89,"line":4498},[87,15378,15379],{"class":1339},"  \u002F\u002F component's mounted hook), which relies on vm._watcher being already defined\n",[87,15381,15382,15384,15387,15390,15392,15395],{"class":89,"line":4504},[87,15383,5671],{"class":562},[87,15385,15386],{"class":93}," Watcher",[87,15388,15389],{"class":97},"(vm, updateComponent, noop, watcherOptions, ",[87,15391,4139],{"class":104},[87,15393,15394],{"class":1339}," \u002F* isRenderWatcher *\u002F",[87,15396,1590],{"class":97},[87,15398,15399,15402,15404,15407],{"class":89,"line":4509},[87,15400,15401],{"class":97},"  hydrating ",[87,15403,162],{"class":562},[87,15405,15406],{"class":104}," false",[87,15408,114],{"class":97},[87,15410,15411],{"class":89,"line":4514},[87,15412,15413],{"class":1339},"  \u002F\u002F flush buffer for flush: \"pre\" watchers queued in setup()\n",[87,15415,15416,15418,15421,15423],{"class":89,"line":4519},[87,15417,14698],{"class":562},[87,15419,15420],{"class":97}," preWatchers ",[87,15422,162],{"class":562},[87,15424,15425],{"class":97}," vm._preWatchers;\n",[87,15427,15428,15430],{"class":89,"line":4538},[87,15429,2435],{"class":562},[87,15431,15432],{"class":97}," (preWatchers) {\n",[87,15434,15435,15437,15439,15441,15443,15445,15447,15450,15452,15455,15457,15460,15462],{"class":89,"line":4555},[87,15436,14064],{"class":562},[87,15438,2658],{"class":97},[87,15440,3173],{"class":562},[87,15442,12352],{"class":97},[87,15444,162],{"class":562},[87,15446,4265],{"class":104},[87,15448,15449],{"class":97},"; i ",[87,15451,152],{"class":562},[87,15453,15454],{"class":97}," preWatchers.",[87,15456,4259],{"class":104},[87,15458,15459],{"class":97},"; i",[87,15461,12376],{"class":562},[87,15463,4409],{"class":97},[87,15465,15466,15469,15471],{"class":89,"line":4560},[87,15467,15468],{"class":97},"      preWatchers[i].",[87,15470,13740],{"class":93},[87,15472,2026],{"class":97},[87,15474,15475],{"class":89,"line":4565},[87,15476,1689],{"class":97},[87,15478,15479],{"class":89,"line":4570},[87,15480,1694],{"class":97},[87,15482,15483,15485,15488,15490,15492],{"class":89,"line":13619},[87,15484,2435],{"class":562},[87,15486,15487],{"class":97}," (vm.$vnode ",[87,15489,4235],{"class":562},[87,15491,1359],{"class":104},[87,15493,4409],{"class":97},[87,15495,15496,15499,15501,15503],{"class":89,"line":13625},[87,15497,15498],{"class":97},"    vm._isMounted ",[87,15500,162],{"class":562},[87,15502,13885],{"class":104},[87,15504,114],{"class":97},[87,15506,15507,15510,15512,15515],{"class":89,"line":13631},[87,15508,15509],{"class":93},"    callHook$1",[87,15511,14971],{"class":97},[87,15513,15514],{"class":165},"'mounted'",[87,15516,1590],{"class":97},[87,15518,15519],{"class":89,"line":13637},[87,15520,1694],{"class":97},[87,15522,15523,15525],{"class":89,"line":13643},[87,15524,2691],{"class":562},[87,15526,15527],{"class":97}," vm;\n",[87,15529,15530],{"class":89,"line":13649},[87,15531,133],{"class":97},[16,15533,15534,15537],{},[27,15535,15536],{},"mountComponent"," 함수 실행 시 작동은 다음과 같다.",[2953,15539,15540,15543,15546,15556],{},[23,15541,15542],{},"beforeMount 훅 실행",[23,15544,15545],{},"watcher 옵션으로 beforeUpdate 훅을 등록한다.",[23,15547,15548,15551,15552,15555],{},[27,15549,15550],{},"vm._render"," 를 실행하는 ",[27,15553,15554],{},"vm._update"," 함수를 Watcher 옵션으로 넘긴다.",[23,15557,15558],{},"mounted 훅 실행",[16,15560,15561,15562,15564,15565,15567,15568,15570],{},"일부 경우에 따라 ",[27,15563,13088],{}," 함수가 달라질 순 있지만, 대부분의 경우 결론적으로는 ",[27,15566,15550],{}," 를 Watcher 의 ",[27,15569,13088],{}," 로 실행한다.",[78,15572,15574],{"className":3379,"code":15573,"language":3381,"meta":83,"style":83},"vnode = render.call(vm._renderProxy, vm.$createElement);\n",[27,15575,15576],{"__ignoreMap":83},[87,15577,15578,15581,15583,15586,15588],{"class":89,"line":90},[87,15579,15580],{"class":97},"vnode ",[87,15582,162],{"class":562},[87,15584,15585],{"class":97}," render.",[87,15587,13177],{"class":93},[87,15589,15590],{"class":97},"(vm._renderProxy, vm.$createElement);\n",[16,15592,15593],{},"최종적으로는 컴포넌트에 동적으로 생성된 render 함수를 실행하게 된다. 이 render 함수는 컴포넌트의 템플릿을 가지고 생성된다. 혹은 render 함수를 직접 구현하거나.",[16,15595,15596,15597,15600,15601,15603],{},"render 함수로 생성된 ",[27,15598,15599],{},"vnode"," 를 가지고 ",[27,15602,15554],{}," 함수를 실행한다.",[78,15605,15607],{"className":4002,"code":15606,"language":4004,"meta":83,"style":83},"Vue.prototype._update = function (vnode, hydrating) {\n    var vm = this;\n    var prevEl = vm.$el;\n    var prevVnode = vm._vnode;\n    var restoreActiveInstance = setActiveInstance(vm);\n    vm._vnode = vnode; \n\n    if (!prevVnode) {\n      \u002F\u002F initial render\n      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);\n    } else {\n      \u002F\u002F updates\n      vm.$el = vm.__patch__(prevVnode, vnode);\n    }\n    restoreActiveInstance();\n    if (prevEl) {\n      prevEl.__vue__ = null;\n    }\n    if (vm.$el) {\n      vm.$el.__vue__ = vm;\n    }\n    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {\n      vm.$parent.$el = vm.$el;\n    }\n  };\n",[27,15608,15609,15635,15647,15659,15671,15686,15696,15700,15711,15716,15735,15743,15748,15761,15765,15772,15779,15790,15794,15801,15810,15814,15835,15844,15848],{"__ignoreMap":83},[87,15610,15611,15613,15615,15617,15619,15621,15623,15625,15627,15629,15631,15633],{"class":89,"line":90},[87,15612,2858],{"class":104},[87,15614,1231],{"class":97},[87,15616,13824],{"class":104},[87,15618,1231],{"class":97},[87,15620,15150],{"class":93},[87,15622,1362],{"class":562},[87,15624,1997],{"class":562},[87,15626,2658],{"class":97},[87,15628,15599],{"class":1168},[87,15630,60],{"class":97},[87,15632,14926],{"class":1168},[87,15634,4409],{"class":97},[87,15636,15637,15639,15641,15643,15645],{"class":89,"line":101},[87,15638,14023],{"class":562},[87,15640,14707],{"class":97},[87,15642,162],{"class":562},[87,15644,4243],{"class":104},[87,15646,114],{"class":97},[87,15648,15649,15651,15654,15656],{"class":89,"line":117},[87,15650,14023],{"class":562},[87,15652,15653],{"class":97}," prevEl ",[87,15655,162],{"class":562},[87,15657,15658],{"class":97}," vm.$el;\n",[87,15660,15661,15663,15666,15668],{"class":89,"line":130},[87,15662,14023],{"class":562},[87,15664,15665],{"class":97}," prevVnode ",[87,15667,162],{"class":562},[87,15669,15670],{"class":97}," vm._vnode;\n",[87,15672,15673,15675,15678,15680,15683],{"class":89,"line":224},[87,15674,14023],{"class":562},[87,15676,15677],{"class":97}," restoreActiveInstance ",[87,15679,162],{"class":562},[87,15681,15682],{"class":93}," setActiveInstance",[87,15684,15685],{"class":97},"(vm);\n",[87,15687,15688,15691,15693],{"class":89,"line":246},[87,15689,15690],{"class":97},"    vm._vnode ",[87,15692,162],{"class":562},[87,15694,15695],{"class":97}," vnode; \n",[87,15697,15698],{"class":89,"line":256},[87,15699,1446],{"emptyLinePlaceholder":842},[87,15701,15702,15704,15706,15708],{"class":89,"line":270},[87,15703,4224],{"class":562},[87,15705,2658],{"class":97},[87,15707,2661],{"class":562},[87,15709,15710],{"class":97},"prevVnode) {\n",[87,15712,15713],{"class":89,"line":280},[87,15714,15715],{"class":1339},"      \u002F\u002F initial render\n",[87,15717,15718,15721,15723,15725,15728,15731,15733],{"class":89,"line":295},[87,15719,15720],{"class":97},"      vm.$el ",[87,15722,162],{"class":562},[87,15724,15098],{"class":97},[87,15726,15727],{"class":93},"__patch__",[87,15729,15730],{"class":97},"(vm.$el, vnode, hydrating, ",[87,15732,13576],{"class":104},[87,15734,1590],{"class":97},[87,15736,15737,15739,15741],{"class":89,"line":315},[87,15738,5365],{"class":97},[87,15740,9128],{"class":562},[87,15742,98],{"class":97},[87,15744,15745],{"class":89,"line":330},[87,15746,15747],{"class":1339},"      \u002F\u002F updates\n",[87,15749,15750,15752,15754,15756,15758],{"class":89,"line":351},[87,15751,15720],{"class":97},[87,15753,162],{"class":562},[87,15755,15098],{"class":97},[87,15757,15727],{"class":93},[87,15759,15760],{"class":97},"(prevVnode, vnode);\n",[87,15762,15763],{"class":89,"line":360},[87,15764,1689],{"class":97},[87,15766,15767,15770],{"class":89,"line":374},[87,15768,15769],{"class":93},"    restoreActiveInstance",[87,15771,2026],{"class":97},[87,15773,15774,15776],{"class":89,"line":383},[87,15775,4224],{"class":562},[87,15777,15778],{"class":97}," (prevEl) {\n",[87,15780,15781,15784,15786,15788],{"class":89,"line":398},[87,15782,15783],{"class":97},"      prevEl.__vue__ ",[87,15785,162],{"class":562},[87,15787,1359],{"class":104},[87,15789,114],{"class":97},[87,15791,15792],{"class":89,"line":418},[87,15793,1689],{"class":97},[87,15795,15796,15798],{"class":89,"line":434},[87,15797,4224],{"class":562},[87,15799,15800],{"class":97}," (vm.$el) {\n",[87,15802,15803,15806,15808],{"class":89,"line":454},[87,15804,15805],{"class":97},"      vm.$el.__vue__ ",[87,15807,162],{"class":562},[87,15809,15527],{"class":97},[87,15811,15812],{"class":89,"line":463},[87,15813,1689],{"class":97},[87,15815,15816,15818,15820,15822,15825,15827,15830,15832],{"class":89,"line":477},[87,15817,4224],{"class":562},[87,15819,15487],{"class":97},[87,15821,11085],{"class":562},[87,15823,15824],{"class":97}," vm.$parent ",[87,15826,11085],{"class":562},[87,15828,15829],{"class":97}," vm.$vnode ",[87,15831,4320],{"class":562},[87,15833,15834],{"class":97}," vm.$parent._vnode) {\n",[87,15836,15837,15840,15842],{"class":89,"line":486},[87,15838,15839],{"class":97},"      vm.$parent.$el ",[87,15841,162],{"class":562},[87,15843,15658],{"class":97},[87,15845,15846],{"class":89,"line":3636},[87,15847,1689],{"class":97},[87,15849,15850],{"class":89,"line":3641},[87,15851,15286],{"class":97},[16,15853,15854,15857,15858,15861],{},[27,15855,15856],{},"vm.__patch__"," 를 호출하면서, 실제 DOM;여기서는 ",[27,15859,15860],{},"vm.$el"," 에 업데이트한다.",[45,15863,15865],{"id":15864},"vue2의-반응성-문제-및-특징","Vue2의 반응성 문제 및 특징",[16,15867,15868,15869,10740,15871,15873],{},"Vue2의 반응성 구현에는 문제가 있다. 이는 자바스크립트의 한계 때문에 발생한 것이다. Vue의 데이터들은 추가, 감소가 발생했을 때는 변화를 감지하지 못한다. ",[27,15870,13088],{},[27,15872,13091],{}," 를 주입받지 못하기 때문이다.",[753,15875,15877],{"id":15876},"data-option-에-데이터-추가하기","data option 에 데이터 추가하기",[78,15879,15881],{"className":3379,"code":15880,"language":3381,"meta":83,"style":83},"data: {\n    some: 1\n}\n\nthis.other = 2;\n",[27,15882,15883,15890,15900,15904,15908],{"__ignoreMap":83},[87,15884,15885,15888],{"class":89,"line":90},[87,15886,15887],{"class":93},"data",[87,15889,7583],{"class":97},[87,15891,15892,15895,15897],{"class":89,"line":101},[87,15893,15894],{"class":93},"    some",[87,15896,108],{"class":97},[87,15898,15899],{"class":104},"1\n",[87,15901,15902],{"class":89,"line":117},[87,15903,133],{"class":97},[87,15905,15906],{"class":89,"line":130},[87,15907,1446],{"emptyLinePlaceholder":842},[87,15909,15910,15912,15915,15917,15919],{"class":89,"line":224},[87,15911,4229],{"class":104},[87,15913,15914],{"class":97},".other ",[87,15916,162],{"class":562},[87,15918,7877],{"class":104},[87,15920,114],{"class":97},[16,15922,15923,15924,15927,15928,15931],{},"컴포넌트의 data option 에 기존에 선언한 ",[27,15925,15926],{},"some"," 은 반응형이지만, ",[27,15929,15930],{},"other"," 는 반응형이 아니다. other 를 아무리 바꿔도 화면에는 아무런 영향을 주지 못한다.",[78,15933,15935],{"className":3379,"code":15934,"language":3381,"meta":83,"style":83},"this.$set(this, 'other', 2);\n",[27,15936,15937],{"__ignoreMap":83},[87,15938,15939,15941,15943,15946,15948,15950,15952,15955,15957,15959],{"class":89,"line":90},[87,15940,4229],{"class":104},[87,15942,1231],{"class":97},[87,15944,15945],{"class":93},"$set",[87,15947,791],{"class":97},[87,15949,4229],{"class":104},[87,15951,60],{"class":97},[87,15953,15954],{"class":165},"'other'",[87,15956,60],{"class":97},[87,15958,587],{"class":104},[87,15960,1590],{"class":97},[16,15962,15963,15964,15966],{},"하지만 Vue 인스턴스의 ",[27,15965,15945],{}," 함수를 이용해 반응형을 이끌어낼 수 있다.",[753,15968,15970],{"id":15969},"object-에-assign-으로-새로운-프로퍼티-추가하기","object 에 assign 으로 새로운 프로퍼티 추가하기",[16,15972,15973,6915,15976,15979,15980,15983,15984,15987,15988,15991],{},[27,15974,15975],{},"Object.assign",[27,15977,15978],{},"_.extend()"," 를 통한 객체 복사 역시 감지하지 못한다. 속성을 가져올 때는 ",[27,15981,15982],{},"[[Get]]"," 을 사용하고 속성을 지정할 때 ",[27,15985,15986],{},"[[Set]]"," 을 사용하기 때문에 ",[27,15989,15990],{},"getter\u002Fsetter"," 를 트리깅하지 못한다.",[78,15993,15995],{"className":3379,"code":15994,"language":3381,"meta":83,"style":83},"Object.assign(this.someObject, { a: 1, b: 2 }) \u002F\u002F 이렇게 하면 반응성을 이끌어내지 못함.\nthis.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })\n",[27,15996,15997,16023],{"__ignoreMap":83},[87,15998,15999,16001,16004,16006,16008,16011,16013,16016,16018,16020],{"class":89,"line":90},[87,16000,13119],{"class":97},[87,16002,16003],{"class":93},"assign",[87,16005,791],{"class":97},[87,16007,4229],{"class":104},[87,16009,16010],{"class":97},".someObject, { a: ",[87,16012,1579],{"class":104},[87,16014,16015],{"class":97},", b: ",[87,16017,587],{"class":104},[87,16019,9648],{"class":97},[87,16021,16022],{"class":1339},"\u002F\u002F 이렇게 하면 반응성을 이끌어내지 못함.\n",[87,16024,16025,16027,16030,16032,16035,16037,16040,16042,16044,16046,16048,16050],{"class":89,"line":101},[87,16026,4229],{"class":104},[87,16028,16029],{"class":97},".someObject ",[87,16031,162],{"class":562},[87,16033,16034],{"class":97}," Object.",[87,16036,16003],{"class":93},[87,16038,16039],{"class":97},"({}, ",[87,16041,4229],{"class":104},[87,16043,16010],{"class":97},[87,16045,1579],{"class":104},[87,16047,16015],{"class":97},[87,16049,587],{"class":104},[87,16051,16052],{"class":97}," })\n",[16,16054,16055],{},"그래서 새로운 객체에 대상을 2개로 엮어서 병합한 후에 새로운 객체로 반환해야 한다.",[753,16057,16059],{"id":16058},"배열-문제","배열 문제",[16,16061,16062],{},"배열을 다루면서 아래 행동은 변화를 감지할 수 없다.",[2953,16064,16065,16068],{},[23,16066,16067],{},"배열의 인덱스에 값을 넣으려는 것",[23,16069,16070],{},"배열의 크기를 바꾸는 것",[78,16072,16074],{"className":3379,"code":16073,"language":3381,"meta":83,"style":83},"var vm = new Vue({\n  data: {\n    items: ['a', 'b', 'c']\n  }\n})\nvm.items[1] = 'x'\nvm.items.length = 2\n",[27,16075,16076,16091,16096,16116,16120,16124,16139],{"__ignoreMap":83},[87,16077,16078,16080,16082,16084,16086,16089],{"class":89,"line":90},[87,16079,3173],{"class":562},[87,16081,14707],{"class":97},[87,16083,162],{"class":562},[87,16085,2672],{"class":562},[87,16087,16088],{"class":93}," Vue",[87,16090,5676],{"class":97},[87,16092,16093],{"class":89,"line":101},[87,16094,16095],{"class":97},"  data: {\n",[87,16097,16098,16101,16104,16106,16109,16111,16114],{"class":89,"line":117},[87,16099,16100],{"class":97},"    items: [",[87,16102,16103],{"class":165},"'a'",[87,16105,60],{"class":97},[87,16107,16108],{"class":165},"'b'",[87,16110,60],{"class":97},[87,16112,16113],{"class":165},"'c'",[87,16115,3902],{"class":97},[87,16117,16118],{"class":89,"line":130},[87,16119,1694],{"class":97},[87,16121,16122],{"class":89,"line":224},[87,16123,5107],{"class":97},[87,16125,16126,16129,16131,16134,16136],{"class":89,"line":246},[87,16127,16128],{"class":97},"vm.items[",[87,16130,1579],{"class":104},[87,16132,16133],{"class":97},"] ",[87,16135,162],{"class":562},[87,16137,16138],{"class":165}," 'x'\n",[87,16140,16141,16144,16146,16148],{"class":89,"line":256},[87,16142,16143],{"class":97},"vm.items.",[87,16145,4259],{"class":104},[87,16147,1362],{"class":562},[87,16149,16150],{"class":104}," 2\n",[16,16152,16153],{},"아래 두 코드는 반응성을 이끌어내지 못했다. 그렇다면 배열은 어떻게 다뤄야 반응형일까? 배열의 특정 함수들을 이용해야 한다.",[20,16155,16156,16159,16162,16165],{},[23,16157,16158],{},"push 배열에 뒤에 값을 추가",[23,16160,16161],{},"shift 배열 맨 처음 값을 빼냄",[23,16163,16164],{},"unshift 배열 앞에 값을 추가",[23,16166,16167],{},"splice 일부를 교체함",[78,16169,16171],{"className":3379,"code":16170,"language":3381,"meta":83,"style":83},"this.$set(this.items, 1, changedValue);\n",[27,16172,16173],{"__ignoreMap":83},[87,16174,16175,16177,16179,16181,16183,16185,16188,16190],{"class":89,"line":90},[87,16176,4229],{"class":104},[87,16178,1231],{"class":97},[87,16180,15945],{"class":93},[87,16182,791],{"class":97},[87,16184,4229],{"class":104},[87,16186,16187],{"class":97},".items, ",[87,16189,1579],{"class":104},[87,16191,16192],{"class":97},", changedValue);\n",[16,16194,16195,16196,16198],{},"혹은 ",[27,16197,15945],{}," 함수를 이용해 특정 인덱스의 값을 변경한다.",[767,16200,16202],{"id":16201},"object-배열","Object 배열",[78,16204,16206],{"className":3379,"code":16205,"language":3381,"meta":83,"style":83},"animals: [\n    {id: 1, comments: 'cat',},\n    {id: 2, comments: 'bird',},\n    {id: 3, comments: 'dog',},\n]\n\ncreated() {\n    this.animals[3] = {id: 4, comments: 'cow'};\n}\n",[27,16207,16208,16215,16231,16244,16258,16262,16266,16273,16299],{"__ignoreMap":83},[87,16209,16210,16213],{"class":89,"line":90},[87,16211,16212],{"class":93},"animals",[87,16214,10104],{"class":97},[87,16216,16217,16220,16222,16225,16228],{"class":89,"line":101},[87,16218,16219],{"class":97},"    {id: ",[87,16221,1579],{"class":104},[87,16223,16224],{"class":97},", comments: ",[87,16226,16227],{"class":165},"'cat'",[87,16229,16230],{"class":97},",},\n",[87,16232,16233,16235,16237,16239,16242],{"class":89,"line":117},[87,16234,16219],{"class":97},[87,16236,587],{"class":104},[87,16238,16224],{"class":97},[87,16240,16241],{"class":165},"'bird'",[87,16243,16230],{"class":97},[87,16245,16246,16248,16251,16253,16256],{"class":89,"line":130},[87,16247,16219],{"class":97},[87,16249,16250],{"class":104},"3",[87,16252,16224],{"class":97},[87,16254,16255],{"class":165},"'dog'",[87,16257,16230],{"class":97},[87,16259,16260],{"class":89,"line":224},[87,16261,3902],{"class":97},[87,16263,16264],{"class":89,"line":246},[87,16265,1446],{"emptyLinePlaceholder":842},[87,16267,16268,16271],{"class":89,"line":256},[87,16269,16270],{"class":93},"created",[87,16272,2003],{"class":97},[87,16274,16275,16277,16280,16282,16284,16286,16289,16292,16294,16297],{"class":89,"line":270},[87,16276,4371],{"class":104},[87,16278,16279],{"class":97},".animals[",[87,16281,16250],{"class":104},[87,16283,16133],{"class":97},[87,16285,162],{"class":562},[87,16287,16288],{"class":97}," {id: ",[87,16290,16291],{"class":104},"4",[87,16293,16224],{"class":97},[87,16295,16296],{"class":165},"'cow'",[87,16298,14640],{"class":97},[87,16300,16301],{"class":89,"line":280},[87,16302,133],{"class":97},[16,16304,16305],{},"우리는 개발을 하면서 흔하게 Object 를 여러개 담아놓은 배열을 다루게 된다. 이를 리스트 렌더링을 통해 화면에 심심치 않게 보여준다. 그런데 아까 얘기하기로는 Vue2의 배열은 인덱스에 해당하는 값을 바꾸려고 하면 반응성이 안나타난다고 했다.",[78,16307,16309],{"className":3379,"code":16308,"language":3381,"meta":83,"style":83},"this.animals[1].comments = \"whale\";\n",[27,16310,16311],{"__ignoreMap":83},[87,16312,16313,16315,16317,16319,16322,16324,16327],{"class":89,"line":90},[87,16314,4229],{"class":104},[87,16316,16279],{"class":97},[87,16318,1579],{"class":104},[87,16320,16321],{"class":97},"].comments ",[87,16323,162],{"class":562},[87,16325,16326],{"class":165}," \"whale\"",[87,16328,114],{"class":97},[16,16330,16331],{},"배열 1번의 객체의 데이터를 바꾸려고 하니, 세상에나 반응한다! 배열 내부의 객체는 반응형이다.",[16,16333,16334],{},[513,16335],{"alt":515,"src":16336},"blog\u002Fimg\u002FVue2-Reactivity\u002FUntitled2.png",[16,16338,16339],{},"이는 콘솔에 데이터를 찍어보면 간단하게 알 수 있다. 배열의 경우 getter, setter 함수는 없지만 Observer 를 갖고 있다.",[16,16341,16342],{},[513,16343],{"alt":515,"src":16344},"blog\u002Fimg\u002FVue2-Reactivity\u002FUntitled3.png",[16,16346,16347],{},"다만 내부에 있는 객체 3개는 각각 속성에 대해서 getter, setter 를 갖고 있다. 그렇기 때문에 배열 내부의 객체에 대해서는 반응형이다.",[16,16349,16350],{},[513,16351],{"alt":515,"src":16352},"blog\u002Fimg\u002FVue2-Reactivity\u002FUntitled4.png",[16,16354,16355,16356,16358,16359,16362],{},"단 ",[27,16357,16270],{}," 에서 인덱스로 할당한 객체의 경우 Observer 할당이 안되어 있다. 또 getter, setter 역시 주입되지 않았기 때문에, ",[27,16360,16361],{},"this.animals[3].comments = \"some\";"," 코드는 반응형이 아니다.",[78,16364,16366],{"className":3379,"code":16365,"language":3381,"meta":83,"style":83},"const newObject = { \n  \"id\": 1, \n  \"comments\": \"새로운 오브젝트를 넣음!\", \n};\nthis.animals[1] = newObject; \u002F\u002F 반응형 x\nthis.$set(this.animals, 1, newObject); \u002F\u002F 반응형 o\nthis.animals[1].comments = \"내가 바꾸었다!!!\"; \u002F\u002F 반응형 o\nthis.$set(this.animals[1], 'comments', \"내가 바꾸었다!!!\"); \u002F\u002F 반응형 o\n",[27,16367,16368,16379,16390,16402,16406,16424,16447,16467],{"__ignoreMap":83},[87,16369,16370,16372,16375,16377],{"class":89,"line":90},[87,16371,1964],{"class":562},[87,16373,16374],{"class":104}," newObject",[87,16376,1362],{"class":562},[87,16378,5603],{"class":97},[87,16380,16381,16384,16386,16388],{"class":89,"line":101},[87,16382,16383],{"class":165},"  \"id\"",[87,16385,108],{"class":97},[87,16387,1579],{"class":104},[87,16389,10403],{"class":97},[87,16391,16392,16395,16397,16400],{"class":89,"line":117},[87,16393,16394],{"class":165},"  \"comments\"",[87,16396,108],{"class":97},[87,16398,16399],{"class":165},"\"새로운 오브젝트를 넣음!\"",[87,16401,10403],{"class":97},[87,16403,16404],{"class":89,"line":130},[87,16405,14640],{"class":97},[87,16407,16408,16410,16412,16414,16416,16418,16421],{"class":89,"line":224},[87,16409,4229],{"class":104},[87,16411,16279],{"class":97},[87,16413,1579],{"class":104},[87,16415,16133],{"class":97},[87,16417,162],{"class":562},[87,16419,16420],{"class":97}," newObject; ",[87,16422,16423],{"class":1339},"\u002F\u002F 반응형 x\n",[87,16425,16426,16428,16430,16432,16434,16436,16439,16441,16444],{"class":89,"line":246},[87,16427,4229],{"class":104},[87,16429,1231],{"class":97},[87,16431,15945],{"class":93},[87,16433,791],{"class":97},[87,16435,4229],{"class":104},[87,16437,16438],{"class":97},".animals, ",[87,16440,1579],{"class":104},[87,16442,16443],{"class":97},", newObject); ",[87,16445,16446],{"class":1339},"\u002F\u002F 반응형 o\n",[87,16448,16449,16451,16453,16455,16457,16459,16462,16465],{"class":89,"line":256},[87,16450,4229],{"class":104},[87,16452,16279],{"class":97},[87,16454,1579],{"class":104},[87,16456,16321],{"class":97},[87,16458,162],{"class":562},[87,16460,16461],{"class":165}," \"내가 바꾸었다!!!\"",[87,16463,16464],{"class":97},"; ",[87,16466,16446],{"class":1339},[87,16468,16469,16471,16473,16475,16477,16479,16481,16483,16485,16488,16490,16493,16496],{"class":89,"line":270},[87,16470,4229],{"class":104},[87,16472,1231],{"class":97},[87,16474,15945],{"class":93},[87,16476,791],{"class":97},[87,16478,4229],{"class":104},[87,16480,16279],{"class":97},[87,16482,1579],{"class":104},[87,16484,10686],{"class":97},[87,16486,16487],{"class":165},"'comments'",[87,16489,60],{"class":97},[87,16491,16492],{"class":165},"\"내가 바꾸었다!!!\"",[87,16494,16495],{"class":97},"); ",[87,16497,16446],{"class":1339},[16,16499,16500],{},"물론 위 코드를 하나의 함수에서 실행하면 모든 변경대상이 반응형이다. 왜냐하면 반응을 감지하고 re-rendering 할 때는, 바뀐 데이터를 모두 적용하기 때문이다. 데이터는 변경이 되어 있지만, 화면에 적용만 안 되어 있던 것이다. 렌더링 시점에 모든 변경사항이 적용된다.",[767,16502,16504],{"id":16503},"다중-배열","다중 배열",[78,16506,16508],{"className":3379,"code":16507,"language":3381,"meta":83,"style":83},"twoDimensionalArray: [\n  [\n    {id: 1, cat: 'first'},\n    {id: 2, cat: 'first'},\n  ],\n  [\n    {id: 1, cat: 'second'},\n    {id: 2, cat: 'second'},\n  ],\n  [],\n]\n\ncreated() {\n    this.twoDimensionalArray[2] = [\n    {id: 1, cat: 'third'},\n    {id: 2, cat: 'third'},\n  ]\n  this.twoDimensionalArray[3] = [\n    {id: 1, cat: 'forth'},\n    {id: 2, cat: 'forth'},\n  ]\n}\n",[27,16509,16510,16517,16522,16536,16548,16552,16556,16569,16581,16585,16590,16594,16598,16604,16619,16632,16644,16648,16663,16676,16688,16692],{"__ignoreMap":83},[87,16511,16512,16515],{"class":89,"line":90},[87,16513,16514],{"class":93},"twoDimensionalArray",[87,16516,10104],{"class":97},[87,16518,16519],{"class":89,"line":101},[87,16520,16521],{"class":97},"  [\n",[87,16523,16524,16526,16528,16531,16534],{"class":89,"line":117},[87,16525,16219],{"class":97},[87,16527,1579],{"class":104},[87,16529,16530],{"class":97},", cat: ",[87,16532,16533],{"class":165},"'first'",[87,16535,7632],{"class":97},[87,16537,16538,16540,16542,16544,16546],{"class":89,"line":130},[87,16539,16219],{"class":97},[87,16541,587],{"class":104},[87,16543,16530],{"class":97},[87,16545,16533],{"class":165},[87,16547,7632],{"class":97},[87,16549,16550],{"class":89,"line":224},[87,16551,9786],{"class":97},[87,16553,16554],{"class":89,"line":246},[87,16555,16521],{"class":97},[87,16557,16558,16560,16562,16564,16567],{"class":89,"line":256},[87,16559,16219],{"class":97},[87,16561,1579],{"class":104},[87,16563,16530],{"class":97},[87,16565,16566],{"class":165},"'second'",[87,16568,7632],{"class":97},[87,16570,16571,16573,16575,16577,16579],{"class":89,"line":270},[87,16572,16219],{"class":97},[87,16574,587],{"class":104},[87,16576,16530],{"class":97},[87,16578,16566],{"class":165},[87,16580,7632],{"class":97},[87,16582,16583],{"class":89,"line":280},[87,16584,9786],{"class":97},[87,16586,16587],{"class":89,"line":295},[87,16588,16589],{"class":97},"  [],\n",[87,16591,16592],{"class":89,"line":315},[87,16593,3902],{"class":97},[87,16595,16596],{"class":89,"line":330},[87,16597,1446],{"emptyLinePlaceholder":842},[87,16599,16600,16602],{"class":89,"line":351},[87,16601,16270],{"class":93},[87,16603,2003],{"class":97},[87,16605,16606,16608,16611,16613,16615,16617],{"class":89,"line":360},[87,16607,4371],{"class":104},[87,16609,16610],{"class":97},".twoDimensionalArray[",[87,16612,587],{"class":104},[87,16614,16133],{"class":97},[87,16616,162],{"class":562},[87,16618,5666],{"class":97},[87,16620,16621,16623,16625,16627,16630],{"class":89,"line":374},[87,16622,16219],{"class":97},[87,16624,1579],{"class":104},[87,16626,16530],{"class":97},[87,16628,16629],{"class":165},"'third'",[87,16631,7632],{"class":97},[87,16633,16634,16636,16638,16640,16642],{"class":89,"line":383},[87,16635,16219],{"class":97},[87,16637,587],{"class":104},[87,16639,16530],{"class":97},[87,16641,16629],{"class":165},[87,16643,7632],{"class":97},[87,16645,16646],{"class":89,"line":398},[87,16647,10360],{"class":97},[87,16649,16650,16653,16655,16657,16659,16661],{"class":89,"line":418},[87,16651,16652],{"class":104},"  this",[87,16654,16610],{"class":97},[87,16656,16250],{"class":104},[87,16658,16133],{"class":97},[87,16660,162],{"class":562},[87,16662,5666],{"class":97},[87,16664,16665,16667,16669,16671,16674],{"class":89,"line":434},[87,16666,16219],{"class":97},[87,16668,1579],{"class":104},[87,16670,16530],{"class":97},[87,16672,16673],{"class":165},"'forth'",[87,16675,7632],{"class":97},[87,16677,16678,16680,16682,16684,16686],{"class":89,"line":454},[87,16679,16219],{"class":97},[87,16681,587],{"class":104},[87,16683,16530],{"class":97},[87,16685,16673],{"class":165},[87,16687,7632],{"class":97},[87,16689,16690],{"class":89,"line":463},[87,16691,10360],{"class":97},[87,16693,16694],{"class":89,"line":477},[87,16695,133],{"class":97},[16,16697,16698],{},"만약 다중 배열의 경우 어떻게 될까? 배열 요소가 배열이고, 그 안에는 객체들이 있는 구조이다.",[78,16700,16702],{"className":3379,"code":16701,"language":3381,"meta":83,"style":83},"const newObject = { \n  \"id\": 1, \n  \"comments\": \"새로운 오브젝트를 넣음!\", \n};\nthis.twoDimensionalArray[1][1] = newObject \u002F\u002F 반응형 x\nthis.$set(this.twoDimensionalArray[1], 1, newObject); \u002F\u002F 반응형 o\nthis.twoDimensionalArray[0][0].cat = \"내가 바꾸었다!!!\"; \u002F\u002F 반응형 o\nthis.twoDimensionalArray[2][0].cat = \"새로 넣은 오브젝트인데 바뀔까?\"; \u002F\u002F 반응형 x\nthis.$set(this.twoDimensionalArray[3][1], 'comments', \"내가 바꾸었다!!!\"); \u002F\u002F 반응형 x\n",[27,16703,16704,16714,16724,16734,16738,16760,16784,16807,16830],{"__ignoreMap":83},[87,16705,16706,16708,16710,16712],{"class":89,"line":90},[87,16707,1964],{"class":562},[87,16709,16374],{"class":104},[87,16711,1362],{"class":562},[87,16713,5603],{"class":97},[87,16715,16716,16718,16720,16722],{"class":89,"line":101},[87,16717,16383],{"class":165},[87,16719,108],{"class":97},[87,16721,1579],{"class":104},[87,16723,10403],{"class":97},[87,16725,16726,16728,16730,16732],{"class":89,"line":117},[87,16727,16394],{"class":165},[87,16729,108],{"class":97},[87,16731,16399],{"class":165},[87,16733,10403],{"class":97},[87,16735,16736],{"class":89,"line":130},[87,16737,14640],{"class":97},[87,16739,16740,16742,16744,16746,16749,16751,16753,16755,16758],{"class":89,"line":224},[87,16741,4229],{"class":104},[87,16743,16610],{"class":97},[87,16745,1579],{"class":104},[87,16747,16748],{"class":97},"][",[87,16750,1579],{"class":104},[87,16752,16133],{"class":97},[87,16754,162],{"class":562},[87,16756,16757],{"class":97}," newObject ",[87,16759,16423],{"class":1339},[87,16761,16762,16764,16766,16768,16770,16772,16774,16776,16778,16780,16782],{"class":89,"line":246},[87,16763,4229],{"class":104},[87,16765,1231],{"class":97},[87,16767,15945],{"class":93},[87,16769,791],{"class":97},[87,16771,4229],{"class":104},[87,16773,16610],{"class":97},[87,16775,1579],{"class":104},[87,16777,10686],{"class":97},[87,16779,1579],{"class":104},[87,16781,16443],{"class":97},[87,16783,16446],{"class":1339},[87,16785,16786,16788,16790,16792,16794,16796,16799,16801,16803,16805],{"class":89,"line":256},[87,16787,4229],{"class":104},[87,16789,16610],{"class":97},[87,16791,1574],{"class":104},[87,16793,16748],{"class":97},[87,16795,1574],{"class":104},[87,16797,16798],{"class":97},"].cat ",[87,16800,162],{"class":562},[87,16802,16461],{"class":165},[87,16804,16464],{"class":97},[87,16806,16446],{"class":1339},[87,16808,16809,16811,16813,16815,16817,16819,16821,16823,16826,16828],{"class":89,"line":270},[87,16810,4229],{"class":104},[87,16812,16610],{"class":97},[87,16814,587],{"class":104},[87,16816,16748],{"class":97},[87,16818,1574],{"class":104},[87,16820,16798],{"class":97},[87,16822,162],{"class":562},[87,16824,16825],{"class":165}," \"새로 넣은 오브젝트인데 바뀔까?\"",[87,16827,16464],{"class":97},[87,16829,16423],{"class":1339},[87,16831,16832,16834,16836,16838,16840,16842,16844,16846,16848,16850,16852,16854,16856,16858,16860],{"class":89,"line":280},[87,16833,4229],{"class":104},[87,16835,1231],{"class":97},[87,16837,15945],{"class":93},[87,16839,791],{"class":97},[87,16841,4229],{"class":104},[87,16843,16610],{"class":97},[87,16845,16250],{"class":104},[87,16847,16748],{"class":97},[87,16849,1579],{"class":104},[87,16851,10686],{"class":97},[87,16853,16487],{"class":165},[87,16855,60],{"class":97},[87,16857,16492],{"class":165},[87,16859,16495],{"class":97},[87,16861,16423],{"class":1339},[16,16863,16864],{},"기본적으로 배열의 인덱스에 직접 할당하는 것은 무조건 반응형이 아니다.",[16,16866,16867,16868,16870],{},"2번 인덱스의 객체는, 처음에는 빈 배열이다가 인덱스에 직접 할당되었다. 그래서 반응형 설정이 되지 않았고, 다중배열 안의 객체 속성을 바꾸려고 했을 때 반응이 없다. 처음부터 할당이 되어 있거나, ",[27,16869,5826],{}," 등으로 추가된 항목이 아니기 때문이다. 반대 상황으로 0번 인덱스와 1번 인덱스는 반응형으로 내부 속성을 변경하려고 할 때 반응형이다.",[16,16872,16873,16874,16876],{},"3번 인덱스는 처음부터 객체에 없다가, 인덱스로 할당된 객체이다. 2번과 사실상 같은 유형이다. 이 경우 ",[27,16875,15945],{}," 함수로도 반응이 없다.",[16,16878,16879,16880,16882],{},"3번 항목이야 ",[27,16881,5826],{}," 를 통해 항목을 추가하면 된다곤 하지만, 2번 항목을 반응형으로 할당하려면 어떻게 해야할까?",[78,16884,16886],{"className":3379,"code":16885,"language":3381,"meta":83,"style":83},"this.twoDimensionalArray[2].push(...[\n  {id: 1, cat: 'third'},\n  {id: 2, cat: 'third'},\n])\nthis.twoDimensionalArray.splice(2, 1, [\n  {id: 1, cat: 'third'},\n  {id: 2, cat: 'third'},\n]);\n",[27,16887,16888,16907,16920,16932,16937,16958,16970,16982],{"__ignoreMap":83},[87,16889,16890,16892,16894,16896,16899,16901,16903,16905],{"class":89,"line":90},[87,16891,4229],{"class":104},[87,16893,16610],{"class":97},[87,16895,587],{"class":104},[87,16897,16898],{"class":97},"].",[87,16900,5826],{"class":93},[87,16902,791],{"class":97},[87,16904,1438],{"class":562},[87,16906,3811],{"class":97},[87,16908,16909,16912,16914,16916,16918],{"class":89,"line":101},[87,16910,16911],{"class":97},"  {id: ",[87,16913,1579],{"class":104},[87,16915,16530],{"class":97},[87,16917,16629],{"class":165},[87,16919,7632],{"class":97},[87,16921,16922,16924,16926,16928,16930],{"class":89,"line":117},[87,16923,16911],{"class":97},[87,16925,587],{"class":104},[87,16927,16530],{"class":97},[87,16929,16629],{"class":165},[87,16931,7632],{"class":97},[87,16933,16934],{"class":89,"line":130},[87,16935,16936],{"class":97},"])\n",[87,16938,16939,16941,16944,16947,16949,16951,16953,16955],{"class":89,"line":224},[87,16940,4229],{"class":104},[87,16942,16943],{"class":97},".twoDimensionalArray.",[87,16945,16946],{"class":93},"splice",[87,16948,791],{"class":97},[87,16950,587],{"class":104},[87,16952,60],{"class":97},[87,16954,1579],{"class":104},[87,16956,16957],{"class":97},", [\n",[87,16959,16960,16962,16964,16966,16968],{"class":89,"line":246},[87,16961,16911],{"class":97},[87,16963,1579],{"class":104},[87,16965,16530],{"class":97},[87,16967,16629],{"class":165},[87,16969,7632],{"class":97},[87,16971,16972,16974,16976,16978,16980],{"class":89,"line":256},[87,16973,16911],{"class":97},[87,16975,587],{"class":104},[87,16977,16530],{"class":97},[87,16979,16629],{"class":165},[87,16981,7632],{"class":97},[87,16983,16984],{"class":89,"line":270},[87,16985,9090],{"class":97},[20,16987,16988,16991],{},[23,16989,16990],{},"spread 문으로 내용물을 펼쳐서 push 를 하는 방법",[23,16992,16993,16995],{},[27,16994,16946],{}," 함수로 대상 교체하기",[16,16997,16998],{},"배열은 간단하게 생각하면 꼭 함수를 거쳐야 한다고 보면 된다. 그럼 쉽게 반응형을 끌어낼 수 있다.",[753,17000,17002],{"id":17001},"비동기-업데이트-큐","비동기 업데이트 큐",[16,17004,17005,17006,17009],{},"이 부분은 딱히 문제는 아니지만 알아두면 좋은 Vue 의 특성이다. Vue 의 UI 업데이트를 위해 컴파일되는 Virtual DOM은 비동기로 생성되고 화면에 렌더링된다. 즉, queue 에 담긴 여러 변경사항들을 flush 하기 전까지는 바로 화면에 적용되지 않고, 한 번에 렌더링한다. 그 시점은 다음 ",[788,17007,17008],{},"tick"," 이 된다. 또, queue 에 담긴 Watcher 중 중복인 항목은 제거하고 최신으로 유지한다.",[78,17011,17013],{"className":4002,"code":17012,"language":4004,"meta":83,"style":83},"template: `\u003Cspan>{{msg}}\u003C\u002Fspan>`,\nata: {\n    msg: \"before update\",\n}\nmethods: {\n    update: () => {\n        this.msg = \"updated\";\n    console.log(this.$el.textContent) \u002F\u002F => 'not updated'\n    this.$nextTick(function () {\n      console.log(this.$el.textContent) \u002F\u002F => 'updated'\n    })\n    }\n}\n",[27,17014,17015,17026,17033,17045,17049,17056,17067,17081,17097,17112,17127,17131,17135],{"__ignoreMap":83},[87,17016,17017,17019,17021,17024],{"class":89,"line":90},[87,17018,2114],{"class":93},[87,17020,108],{"class":97},[87,17022,17023],{"class":165},"`\u003Cspan>{{msg}}\u003C\u002Fspan>`",[87,17025,3413],{"class":97},[87,17027,17028,17031],{"class":89,"line":101},[87,17029,17030],{"class":93},"ata",[87,17032,7583],{"class":97},[87,17034,17035,17038,17040,17043],{"class":89,"line":117},[87,17036,17037],{"class":93},"    msg",[87,17039,108],{"class":97},[87,17041,17042],{"class":165},"\"before update\"",[87,17044,3413],{"class":97},[87,17046,17047],{"class":89,"line":130},[87,17048,133],{"class":97},[87,17050,17051,17054],{"class":89,"line":224},[87,17052,17053],{"class":93},"methods",[87,17055,7583],{"class":97},[87,17057,17058,17061,17063,17065],{"class":89,"line":246},[87,17059,17060],{"class":93},"    update",[87,17062,12629],{"class":97},[87,17064,1175],{"class":562},[87,17066,98],{"class":97},[87,17068,17069,17071,17074,17076,17079],{"class":89,"line":256},[87,17070,14611],{"class":104},[87,17072,17073],{"class":97},".msg ",[87,17075,162],{"class":562},[87,17077,17078],{"class":165}," \"updated\"",[87,17080,114],{"class":97},[87,17082,17083,17085,17087,17089,17091,17094],{"class":89,"line":270},[87,17084,2450],{"class":97},[87,17086,1221],{"class":93},[87,17088,791],{"class":97},[87,17090,4229],{"class":104},[87,17092,17093],{"class":97},".$el.textContent) ",[87,17095,17096],{"class":1339},"\u002F\u002F => 'not updated'\n",[87,17098,17099,17101,17103,17106,17108,17110],{"class":89,"line":280},[87,17100,4371],{"class":104},[87,17102,1231],{"class":97},[87,17104,17105],{"class":93},"$nextTick",[87,17107,791],{"class":97},[87,17109,9050],{"class":562},[87,17111,13814],{"class":97},[87,17113,17114,17116,17118,17120,17122,17124],{"class":89,"line":295},[87,17115,6380],{"class":97},[87,17117,1221],{"class":93},[87,17119,791],{"class":97},[87,17121,4229],{"class":104},[87,17123,17093],{"class":97},[87,17125,17126],{"class":1339},"\u002F\u002F => 'updated'\n",[87,17128,17129],{"class":89,"line":315},[87,17130,11641],{"class":97},[87,17132,17133],{"class":89,"line":330},[87,17134,1689],{"class":97},[87,17136,17137],{"class":89,"line":351},[87,17138,133],{"class":97},[16,17140,17141,17142,17145,17146,17149,17150,17152],{},"간단하게 update 함수를 통해 변경된 ",[27,17143,17144],{},"msg"," 는 실제 DOM에 적용되기 전이라서 함수 실행 중에는 이전 데이터로 여전히 남아 있다. 그러다 함수 호출이 끝나고, 다음 이벤트 루프가 발생할 때가 되어서야 갱신된 데이터가 화면에 보인다. 이를 추적할 수 있는 게 ",[27,17147,17148],{},"this.$nextTick"," 이다. 이는 함수의 호출을 다음 ",[788,17151,17008],{}," 으로 미루는 작업이다. tick 에 대해서는 추후에 이벤트 루프에 대해서 작성할 때 자세하게 다룰 것이다.",[753,17154,17156],{"id":17155},"created-와-mounted","created 와 mounted",[16,17158,17159],{},"앞서 Vue 컴포넌트는 mount 시점에 렌더링을 한 번 실행한다는 것을 알았다. beforeMounted → 렌더링 → mounted 순서로 훅이 실행된다. 그렇기 때문에 발생하는 현상이 있다. created 훅과 mounted 훅에서 data 에 반응형이 아닌 데이터를 추가를 하면 UI에 표시가 될까 안될까?",[78,17161,17163],{"className":4002,"code":17162,"language":4004,"meta":83,"style":83},"template: `\n    \u003Cdiv v-for=\"item of notReactive\" :key=\"item.id\">\n    {{item}}\n  \u003C\u002Fdiv>\n`,\ncreated() {\n  this.notReactive[1] = {id: 2, cat: 'reactive from created'};\n},\nmounted() {\n  this.notReactive[2] = {id: 3, cat: 'not reactive from mounted'};\n},\ndata: () => {\n  return {\n    notReactive: [\n      {id: 1, cat: 'reactive'},\n    ]\n  }\n}\n",[27,17164,17165,17174,17179,17184,17189,17196,17202,17226,17230,17237,17260,17264,17274,17280,17285,17299,17304,17308],{"__ignoreMap":83},[87,17166,17167,17169,17171],{"class":89,"line":90},[87,17168,2114],{"class":93},[87,17170,108],{"class":97},[87,17172,17173],{"class":165},"`\n",[87,17175,17176],{"class":89,"line":101},[87,17177,17178],{"class":165},"    \u003Cdiv v-for=\"item of notReactive\" :key=\"item.id\">\n",[87,17180,17181],{"class":89,"line":117},[87,17182,17183],{"class":165},"    {{item}}\n",[87,17185,17186],{"class":89,"line":130},[87,17187,17188],{"class":165},"  \u003C\u002Fdiv>\n",[87,17190,17191,17194],{"class":89,"line":224},[87,17192,17193],{"class":165},"`",[87,17195,3413],{"class":97},[87,17197,17198,17200],{"class":89,"line":246},[87,17199,16270],{"class":93},[87,17201,2003],{"class":97},[87,17203,17204,17206,17209,17211,17213,17215,17217,17219,17221,17224],{"class":89,"line":256},[87,17205,16652],{"class":104},[87,17207,17208],{"class":97},".notReactive[",[87,17210,1579],{"class":104},[87,17212,16133],{"class":97},[87,17214,162],{"class":562},[87,17216,16288],{"class":97},[87,17218,587],{"class":104},[87,17220,16530],{"class":97},[87,17222,17223],{"class":165},"'reactive from created'",[87,17225,14640],{"class":97},[87,17227,17228],{"class":89,"line":270},[87,17229,7632],{"class":97},[87,17231,17232,17235],{"class":89,"line":280},[87,17233,17234],{"class":93},"mounted",[87,17236,2003],{"class":97},[87,17238,17239,17241,17243,17245,17247,17249,17251,17253,17255,17258],{"class":89,"line":295},[87,17240,16652],{"class":104},[87,17242,17208],{"class":97},[87,17244,587],{"class":104},[87,17246,16133],{"class":97},[87,17248,162],{"class":562},[87,17250,16288],{"class":97},[87,17252,16250],{"class":104},[87,17254,16530],{"class":97},[87,17256,17257],{"class":165},"'not reactive from mounted'",[87,17259,14640],{"class":97},[87,17261,17262],{"class":89,"line":315},[87,17263,7632],{"class":97},[87,17265,17266,17268,17270,17272],{"class":89,"line":330},[87,17267,15887],{"class":93},[87,17269,12629],{"class":97},[87,17271,1175],{"class":562},[87,17273,98],{"class":97},[87,17275,17276,17278],{"class":89,"line":351},[87,17277,2691],{"class":562},[87,17279,98],{"class":97},[87,17281,17282],{"class":89,"line":360},[87,17283,17284],{"class":97},"    notReactive: [\n",[87,17286,17287,17290,17292,17294,17297],{"class":89,"line":374},[87,17288,17289],{"class":97},"      {id: ",[87,17291,1579],{"class":104},[87,17293,16530],{"class":97},[87,17295,17296],{"class":165},"'reactive'",[87,17298,7632],{"class":97},[87,17300,17301],{"class":89,"line":383},[87,17302,17303],{"class":97},"    ]\n",[87,17305,17306],{"class":89,"line":398},[87,17307,1694],{"class":97},[87,17309,17310],{"class":89,"line":418},[87,17311,133],{"class":97},[16,17313,17314,17315,17318,17319,17322],{},"말로는 헷갈릴 수 있으니 예시를 보자. 0번 인덱스는 데이터 초기화 시점에 이미 할당되어 있기 때문에 당연하게도 반응형이다. 그렇다면, 1번 인덱스와 2번 인덱스는 반응형일까? 우리가 테스트했을 때는 배열의 인덱스에 할당한 대상은 반응형이 아니다. 그렇기 때문에, UI에는 0번 인덱스 항목만 보일 거라고 예상한다. 결과는 0번 1번 인덱스 항목은 출력되고, 2번 인덱스 항목은 출력되지 않는다. 즉, created 에서 넣은 객체는 렌더가 되었고, mounted 에서 넣은 객체는 무시됐다. 렌더링과 훅의 순서 때문이다. created 시점에서는 렌더링 전이기 때문에, 렌더링 시점에 데이터는 인지되고 적용된다. 하지만 mounted 시점에는 이미 렌더링이 끝난 시점이라, 이때 추가된 데이터는 값만 바뀐 채 렌더링은 무시된다. 여담으로 mounted 에서 넣은 데이터는 ",[27,17316,17317],{},"this.$forceUpdate"," 를 호출하면 화면에 보인다. ",[27,17320,17321],{},"watcher.update"," 를 강제로 호출하기 때문이다.",[78,17324,17326],{"className":4002,"code":17325,"language":4004,"meta":83,"style":83},"methods: {\n  onClick() {\n        \u002F\u002F 아래를 개별로 실행\n        \u002F\u002F 한번에 실행하면 1번 행이 반응형이기 때문에, 모두 렌더링 된다.\n    this.notReactive[0].cat = 'reactive from onClick'; \u002F\u002F 반응형\n        this.$set(this.notReactive[1], 'cat', 'reactive from onClick') \u002F\u002F 반응형 x\n    this.notReactive[1].cat = 'reactive from onClick'; \u002F\u002F 반응형 x\n    this.notReactive[2].cat = 'reactive from onClick'; \u002F\u002F 반응형 x\n  }\n},\n",[27,17327,17328,17334,17341,17346,17351,17371,17400,17418,17436,17440],{"__ignoreMap":83},[87,17329,17330,17332],{"class":89,"line":90},[87,17331,17053],{"class":93},[87,17333,7583],{"class":97},[87,17335,17336,17339],{"class":89,"line":101},[87,17337,17338],{"class":93},"  onClick",[87,17340,2003],{"class":97},[87,17342,17343],{"class":89,"line":117},[87,17344,17345],{"class":1339},"        \u002F\u002F 아래를 개별로 실행\n",[87,17347,17348],{"class":89,"line":130},[87,17349,17350],{"class":1339},"        \u002F\u002F 한번에 실행하면 1번 행이 반응형이기 때문에, 모두 렌더링 된다.\n",[87,17352,17353,17355,17357,17359,17361,17363,17366,17368],{"class":89,"line":224},[87,17354,4371],{"class":104},[87,17356,17208],{"class":97},[87,17358,1574],{"class":104},[87,17360,16798],{"class":97},[87,17362,162],{"class":562},[87,17364,17365],{"class":165}," 'reactive from onClick'",[87,17367,16464],{"class":97},[87,17369,17370],{"class":1339},"\u002F\u002F 반응형\n",[87,17372,17373,17375,17377,17379,17381,17383,17385,17387,17389,17391,17393,17396,17398],{"class":89,"line":246},[87,17374,14611],{"class":104},[87,17376,1231],{"class":97},[87,17378,15945],{"class":93},[87,17380,791],{"class":97},[87,17382,4229],{"class":104},[87,17384,17208],{"class":97},[87,17386,1579],{"class":104},[87,17388,10686],{"class":97},[87,17390,16227],{"class":165},[87,17392,60],{"class":97},[87,17394,17395],{"class":165},"'reactive from onClick'",[87,17397,1172],{"class":97},[87,17399,16423],{"class":1339},[87,17401,17402,17404,17406,17408,17410,17412,17414,17416],{"class":89,"line":256},[87,17403,4371],{"class":104},[87,17405,17208],{"class":97},[87,17407,1579],{"class":104},[87,17409,16798],{"class":97},[87,17411,162],{"class":562},[87,17413,17365],{"class":165},[87,17415,16464],{"class":97},[87,17417,16423],{"class":1339},[87,17419,17420,17422,17424,17426,17428,17430,17432,17434],{"class":89,"line":270},[87,17421,4371],{"class":104},[87,17423,17208],{"class":97},[87,17425,587],{"class":104},[87,17427,16798],{"class":97},[87,17429,162],{"class":562},[87,17431,17365],{"class":165},[87,17433,16464],{"class":97},[87,17435,16423],{"class":1339},[87,17437,17438],{"class":89,"line":280},[87,17439,1694],{"class":97},[87,17441,17442],{"class":89,"line":295},[87,17443,7632],{"class":97},[16,17445,17446],{},"mount 시점에 렌더링되어 화면에 나타났다고 해서 반응형인 건 아니다. mount 이후 데이터를 바꾸려고 하면 반응형이 아니기 때문에 반응이 없다.",[825,17448,17449],{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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}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);}",{"title":83,"searchDepth":101,"depth":101,"links":17451},[17452,17453,17457],{"id":13063,"depth":101,"text":13064},{"id":13076,"depth":101,"text":13077,"children":17454},[17455,17456],{"id":13098,"depth":117,"text":13099},{"id":13783,"depth":117,"text":13784},{"id":15864,"depth":101,"text":15865,"children":17458},[17459,17460,17461,17462,17463],{"id":15876,"depth":117,"text":15877},{"id":15969,"depth":117,"text":15970},{"id":16058,"depth":117,"text":16059},{"id":17001,"depth":117,"text":17002},{"id":17155,"depth":117,"text":17156},"2022-07-12","Vue2 Vue3 의 Reactivity 비교. Vue2 Reactive에 대한 설명. VNODE, Watcher, Dependency, ref, reactive, update, vue hook 에 대해서 전반적인 설명.","vue2-reactivity",{},"\u002Fblog\u002Fvue2-reactivity",{"title":13058,"description":17465},{"loc":17468},"blog\u002Fvue2-reactivity",[2858,13051,17473,13063,17474],"Vue Reactivity","Vue Rendering","PCwRkYcqS4KfYl_Y-eDKDjD3PubmnUNsbNY4TJdYlJk",{"id":17477,"title":17478,"body":17479,"created":17527,"description":17528,"extension":839,"filename":17529,"meta":17530,"navigation":842,"path":17533,"seo":17534,"sitemap":17535,"stem":17536,"subject":847,"tags":17537,"updated":17527,"volume":1859,"__hash__":17538},"blog\u002Fblog\u002Fword-wrap-overflow-wrap.md","word-wrap 이 overflow-wrap 로 변경됨",{"type":8,"value":17480,"toc":17525},[17481,17486,17493,17499,17516],[16,17482,17483],{},[513,17484],{"alt":515,"src":17485},"blog\u002Fimg\u002Fword-wrap-overflow-wrap\u002Fword-wrap-overflow-wrap1.png",[16,17487,17488,17489,17492],{},"어느날 gemini 가 리뷰에서 알려준 지식이 있다. 대충 보고 ",[27,17490,17491],{},"overflow-wrap"," 을 쓰라는데 뭐가 다르길래 쓰라고 하지해서 자세히 읽어봤다. alias 격이면 더더욱 바꿀 필요가 없지 않은가 했다. s",[1108,17494],{"description":17495,"favicon":17496,"host":3088,"title":17497,"url":17498},"overflow-wrap CSS 요소는 어떤 문자가 내용 칸 밖으로 넘치지 않게 브라우저가 단어 마디 안에 줄을 바꿔야 할 것인지 아닌지를 정할 때 사용됩니다.","https:\u002F\u002Fdeveloper.mozilla.org\u002Ffavicon.ico","overflow-wrap - CSS: Cascading Style Sheets | MDN","https:\u002F\u002Fdeveloper.mozilla.org\u002Fko\u002Fdocs\u002FWeb\u002FCSS\u002Foverflow-wrap",[1718,17500,17501],{},[16,17502,17503,17505,17506,17509,17510,17512,17513,17515],{},[87,17504,1724],{},"\n이 속성은 처음에 마이크로소프트에서 표준이 아니고 접두어가 없는",[27,17507,17508],{},"word-wrap","으로 나왔고, 대부분 브라우저에서 똑같은 이름으로 구현되었습니다. 요즘은",[27,17511,17491],{},"으로 다시 지어지고,",[27,17514,17508],{},"은 동의어가 되었습니다.",[16,17517,17518,17519,17521,17522,17524],{},"같은 단어이지만 이제는 ",[27,17520,17491],{}," 을 써야한다. mdn 에는 ",[27,17523,17508],{}," 메뉴도 없다.",{"title":83,"searchDepth":101,"depth":101,"links":17526},[],"2025-08-11","지금까지 사용했던 word-wrap 이 overflow-wrap 으로 변경되었다.","word-wrap-overflow-wrap",{"references":17531},[17532],"[[overflow-wrap (word-wrap)]]","\u002Fblog\u002Fword-wrap-overflow-wrap",{"title":17478,"description":17528},{"loc":17533},"blog\u002Fword-wrap-overflow-wrap",[82],"Pn1qGLCR08PZ5_yn9SIAzniHhg_phr5Fypq6kXNJ3F0",[17540,17542,17546,17550,17554,17558,17562,17565,17569,17573,17576,17580,17583,17587,17591,17595,17599,17603,17606,17609,17613,17617,17621,17625,17628,17631,17635,17639,17642,17646,17650,17654,17658,17662,17666,17670,17674,17678,17682,17686,17689,17693,17697,17701,17704,17707,17711,17715,17719,17723,17727,17731,17735,17739,17742,17745,17749,17753,17757,17759,17763,17767,17771,17774,17778,17782,17786,17790,17794,17798,17802,17806,17810,17813,17817,17821,17825,17829,17833,17837,17840,17844,17848,17851,17855,17857,17861,17865,17869,17873,17877,17881,17883,17887,17891,17895,17899,17903,17907,17911,17915,17919,17923,17927,17931],{"id":843,"title":6,"titles":17541,"content":838,"level":90},[],{"id":17543,"title":14,"titles":17544,"content":17545,"level":90},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#box-model",[],"![Untitled](blog\u002Fimg\u002Finteract-text-overflow-white-space-width\u002Fimage copy.png) Content: 실제 내용이 들어가는 영역. width와 height가 기본적으로 지정하는 영역.Padding: 콘텐츠와 테두리 사이의 여백인데 배경색은 이 영역까지 적용됨.Border: 패딩을 감싸는 선.Margin: 테두리 바깥의 공간으로, 다른 요소와의 간격을 벌릴 때 사용. 배경색이 적용되지 않는 투명한 영역",{"id":17547,"title":47,"titles":17548,"content":17549,"level":101},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#box-sizing",[14],"content-box : 컨텐츠 영역만 width 로 잡겠다.\nwidth: 100px, padding: 20px인 경우 : 전체 너비는 140px이 된다.border-box : 테두리까지 포함한 크기가 width 이다. (margin 은 아님)\n전체 너비가 100px 이고 컨텐츠 영역이 60px 로 줄어든다.",{"id":17551,"title":76,"titles":17552,"content":17553,"level":90},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#_1줄-말줄임",[],".line-clamp-1-normal {\n  overflow: hidden;\n  text-overflow: ellipsis;\n} 부모의 width 가 결정되어 있는 경우에는 white-space: nowrap 이 없어도 말줄임이 발생한다. \u003Cdiv class=\"w-80 overflow-hidden\">\n    \u003Cdiv class=\"flex items-center\">\n        \u003Cdiv class=\"w-6 h-6\">1\u003C\u002Fdiv>\n        \u003Cdiv class=\"flex-1 min-w-0\">\n            \u003Cdiv class=\"text-overflow-ellipsis overflow-hidden\">white-space: nowrap 없어서 줄바꿈되고 말줄임도 안일어남\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv>2024.01.01\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n    \u003Cdiv class=\"flex items-center\">\n        \u003Cdiv class=\"w-6 h-6\">2\u003C\u002Fdiv>\n        \u003Cdiv class=\"flex-1 min-w-0\">\n            \u003Cdiv class=\"truncate\">의도한대로 줄바꿈이 일어난 경우입니다람쥐\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv>2024.01.02\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n    \u003Cdiv class=\"flex items-center\">\n        \u003Cdiv class=\"w-6 h-6\">3\u003C\u002Fdiv>\n        \u003Cdiv class=\"flex-1\">\n            \u003Cdiv class=\"truncate\">min-width: 0 없어서 줄어들지 않음\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv>2024.01.03\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Fdiv> ![Untitled](blog\u002Fimg\u002Finteract-text-overflow-white-space-width\u002Fimage copy2.png) flexbox 내부에서는 자식 컴포넌트의 말줄임을 할 땐, 너비에 맞춰서 자동으로 줄바꿈이 되기 때문에 white-space: nowrap 속성이 필요해진다.\nflexbox 안의 다른 요소와 함께 너비가 결정되는 경우 min-width: 0px; 가 필요하기도 하다.",{"id":17555,"title":509,"titles":17556,"content":17557,"level":101},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#min-w-0",[76],"기본적으로 min-width : auto; 로 작동한다. 이는 최소한의 너비를 보장하라는 뜻이다. 보통을 글자의 경우 자동 줄바꿈이 되는데, whitespace-nowrap 과 만나게 되면 자신의 너비를 유지하기 위해서 overflow 가 발생한다. 부모 컨테이너의 너비가 한정되어 있어도 줄바꿈을 절대 하지 않기 때문에 벗어나면서 컨텐츠 영역이 보장되기 때문에 overflow 가 발생하지 않는다. min-width: 0px; 를 주게되면. 0px 까지 줄어들 수 있어 라는 뜻이 된다. 그래서 최소 너비를 보장하지 않아도, 줄어들 수 있고, nowrap 과 만나도, 줄어들 수 있기 때문에 text overflow 가 발생할 수 있게 된다.",{"id":17559,"title":534,"titles":17560,"content":17561,"level":101},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#_2줄-말줄임",[76],".\btext-over {\n    max-width: 253px;\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n    text-overflow: ellipsis;\n} 이렇게만 했을 때는 분명 2줄 말줄임이었는데 1줄 말줄임으로 표시되었다. css 만 봐서는 문제가 없어보였다. .text-over {\n    max-width: 253px;\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: normal; \u002F\u002F 이게 들어가야 함\n} 그런데 나도 모르게 상속된 white-space 속성이 줄바꿈을 방지해서 1줄까지만 표시되고 말줄임이 일어났던 것이다. -webkit-line-clamp 가 2이상 작동하려면 일단 줄바꿈이 일어나야 한다.  white-space 를 지정해주어야 정해진 줄 수에서 말줄임이 일어난다. white-space: nowrap; 인 경우는 애초에 줄바꿈이 일어나지 않기 때문에 2줄이 되지 않는다.",{"id":17563,"title":748,"titles":17564,"content":751,"level":101},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#알아두면-좋을-다른-auto-속성들",[76],{"id":17566,"title":29,"titles":17567,"content":17568,"level":117},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#width",[76,748],"width 는 element 가 block 이냐 inline 이냐에 따라서 다르다. block : 가로 전체 100%inline : 컨텐츠의 너비만큼",{"id":17570,"title":770,"titles":17571,"content":17572,"level":130},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#_100-와의-차이점",[76,748,29],"width: 100% : border, padding 제외 컨텐츠 영역만 100%\nbox-sizing: border-box; 를 주면 auto 와 마찬가지로 작동함width: auto : 해당 요소의 전체 너비(margin + border + padding + content)가 부모 요소의 콘텐츠 영역(content area)에 맞춰진다. padding 값을 추가하면 컨텐츠 영역은 줄어든다는 것.",{"id":17574,"title":808,"titles":17575,"content":83,"level":117},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#table",[76,748],{"id":17577,"title":811,"titles":17578,"content":17579,"level":130},"\u002Fblog\u002F250724-interact-text-overflow-white-space-width#table-layout",[76,748,808],"\u003Ctable> 의 열 너비값을 어떻게 정할지 정한다. auto 가 기본이라서 가장 넓은 콘텐츠에 맞춰서 자동으로 지정된다. -> 컨텐츠의 크기를 모두 게산하고 나서야 열의 너비를 알고 렌더링하기 때문에 속도가 조금 느릴 수 있음. fixed 를 사용하면 첫 행의 너비만으로 결정한다. html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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}",{"id":1501,"title":853,"titles":17581,"content":17582,"level":90},[],"웹과 안드로이드에서 사진의 EXIF 데이터를 다루는 방법. 브라우저의 보안 특성 상 웹에서는 EXIF 를 의도적으로 누락시킴. **EXIF(Exchangeable Image File Format)**는 사진 이미지 데이터 자체 외에, 그 사진이 어떻게, 언제, 어디서, 무엇으로 찍혔는지에 대한 구체적인 **메타데이터(데이터에 대한 데이터)**를 담고 있다. 카메라 정보: 어떤 장비로 사진을 찍었는지 알려준다.\n카메라 제조사 및 모델: 예) SAMSUNG, Apple, SM-S928N, iPhone 15 Pro렌즈 정보: 사용된 렌즈의 종류, 초점 거리 등촬영 설정값: 사진을 어떤 설정으로 찍었는지에 대한 기술적인 정보.\nISO 감도: 빛에 얼마나 민감하게 반응했는지조리개 값 (F-stop): 렌즈를 얼마나 열었는지 (심도 표현과 관련)셔터 속도: 얼마나 빠른 속도로 빛을 받아들였는지 (움직임 표현과 관련)노출 보정, 화이트 밸런스, 플래시 사용 여부 등시간 및 위치 정보: 사진 찍은 시간, 위경도\n촬영 날짜 및 시간: 사진을 찍은 정확한 날짜와 시간.GPS 좌표 (위도, 경도): 카메라의 위치 정보 기록 기능이 켜져 있어야 함. 내가 필요한 건 시간 및 위치 정보가 남기를 바란다.",{"id":17584,"title":945,"titles":17585,"content":17586,"level":90},"\u002Fblog\u002F250819-exif#webview-태그를-통해서-업로드-시",[],"브라우저에서 \u003Cinput type=\"file\"> 를 통해 파일을 선택하면, 안드로이드의 파일 선택기가 열리고, 브라우저는 파일의 복사본을 만들어둔다. 이 과정에서 브라우저는 개인정보 유출을 막기 위해서 EXIF 데이터를 의도적으로 삭제한다. 웹뷰의 경우 onShowFileChooser 에서 개발자가 EXIF 정보가 보존되게 할 수 있다. ACCESS_MEDIA_LOCATION 권한을 사용자에게 허가받으면 파일의 위치 정보를 보존할 수 있다 \u003Cuses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" \u002F>\n\u003Cuses-permission android:name=\"android.permission.ACCESS_MEDIA_LOCATION\" \u002F> onShowFileChooser 에서 파일 선택 결과를 ActivityResultLauncher 로 받는데, 이때 Uri 를 가공하지 않고 그대로 filePathCallback.onReceiveValue() 에 전달하면 된다. \u002F\u002F EXIF 정보 디버그 출력  \ntry {  \n    val uri = data?.data  \n    if (uri != null) {  \n        activity.contentResolver.openInputStream(uri)?.use { inputStream ->  \n            val exif = ExifInterface(inputStream)  \n            Log.d(TAG, \"Single File EXIF Info:\")  \n            Log.d(TAG, \"  DateTime: ${exif.getAttribute(ExifInterface.TAG_DATETIME)}\")  \n            Log.d(TAG, \"  GPS Latitude: ${exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)}\")  \n            Log.d(TAG, \"  GPS Longitude: ${exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)}\")  \n            Log.d(TAG, \"  Make: ${exif.getAttribute(ExifInterface.TAG_MAKE)}\")  \n            Log.d(TAG, \"  Model: ${exif.getAttribute(ExifInterface.TAG_MODEL)}\")  \n            Log.d(TAG, \"  Orientation: ${exif.getAttribute(ExifInterface.TAG_ORIENTATION)}\")  \n        }  \n    }  \n} catch (e: Exception) {  \n    Log.e(TAG, \"EXIF 정보 읽기 실패\", e)  \n} Single File EXIF Info:\n   DateTime: 2025:07:18 08:43:28\n   GPS Latitude: 37\u002F1,0\u002F1,0\u002F100000\n   GPS Longitude: 127\u002F1,0\u002F1,0\u002F00000\n   Make: samsung\n   Model: SM-N960N\n   Orientation: 6 ACCESS_MEDIA_LOCATION 권한을 부여하면 GPS 정보가 정상적으로 기록된다. 그렇지 않으면 0\u002F1과 같은 형태로 기록된다. 따라서 사용자에게 권한을 명시적으로 받아야 한다.",{"id":17588,"title":1106,"titles":17589,"content":17590,"level":90},"\u002Fblog\u002F250819-exif#웹에서-exif-정보-가져오기",[],"웹에서 파일 업로드를 했을 때 EXIF 정보가 누락되는 현상이 있다. 하지만 위 사이트에서는 정상적으로 작동한다. 무슨 차이가 있을까? 위 페이지는 파일을 \u003Cform> 태그를 통해 그대로 서버로 전달하기 때문에 EXIF 데이터의 손실 없이 전달되어 분석이 가능한 것이다. 반면 브라우저 JavaScript에서 파일 데이터에 직접 접근하려고 하면 브라우저 보안 정책상 일부 정보(EXIF 등)가 차단될 수 있다.",{"id":17592,"title":1128,"titles":17593,"content":17594,"level":101},"\u002Fblog\u002F250819-exif#웹뷰에서-native-로직을-통해-전달된-경우",[1106],"앱이 OS 권한을 얻어서 브라우저 기본 정책이 아닌 앱의 권한으로 데이터를 직접 전달한다. 따라서 앱을 통해 파일을 선택한 경우 웹에서도 EXIF 데이터를 조회하고 다룰 수 있게 된다. 다만 EXIF 도 개인정보로 취급할 수 있기 때문에 저장과 노출에 주의를 해야한다. 서버에 저장할 경우 정말 필요한 만큼만 특정되지 않을 정도의 정보만 취급하거나, 저장하지 않는 게 좋다. 정부 페이지에서 EXIF 데이터의 위험성과 대응법도 홍보하고 있다.",{"id":17596,"title":1147,"titles":17597,"content":17598,"level":117},"\u002Fblog\u002F250819-exif#데이터-형식",[1106,1128],"fileList.forEach((file) => {\n    exifr.parse(file)\n        .then((metadata: any) => {\n            console.log(`[EXIF] ${file.name}`, metadata);\n        })\n        .catch((error: any) => {\n            console.error(`[EXIF] Failed to parse EXIF for ${file.name}`, error);\n        });\n}); exifr 라이브러리를 활용해서 EXIF 데이터를 분석했다. parse 결과값에서 원하는 정보를 활용하면 된다.",{"id":17600,"title":1309,"titles":17601,"content":17602,"level":101},"\u002Fblog\u002F250819-exif#exif-데이터-삽입하기-piexif-라이브러리",[1106],"이미지를 후처리하다가 EXIF 데이터가 소실되는 경우도 있다. 예를 들어 캔버스(Canvas)에 이미지를 그리고 추가적인 처리를 한 뒤 다시 이미지로 추출했을 때는 EXIF 데이터가 없는 상태가 된다. 이러한 경우에는 미리 가지고 있던 EXIF 데이터를 저장해두었다가 결과 이미지에 주입하는 방법을 사용할 수 있다. exifr 라이브러리는 읽기 전용 라이브러리이므로, EXIF 데이터를 파일에 쓰는 기능은 없다. piexif 라이브러리를 사용해보자. \u002F\u002F EXIF 저장\nlet exifStr: string | null = null;\ntry {\n    const originalDataURL = await fileToDataURL(origFile);\n    const exifObj = piexif.load(originalDataURL);\n    exifStr = piexif.dump(exifObj);\n} catch (e) { ... }\n\n\u002F\u002F EXIF 삽입\ntry {\n    finalDataURL = piexif.insert(exifStr, resizeResult.dataURL);\n} catch (e) { ... } 원본의 exif 가 그대로 주입된다. 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 .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"id":1854,"title":1512,"titles":17604,"content":17605,"level":90},[],"accordion 을 만들며 height 를 조정해 접고 펼침을 구현할 때, 애니메이션의 딜레이가 발생하는 이유와 헤결, 경험을 공유. .memo-text {\n  word-break: break-word;\n  max-height: 48px;\n  transition: max-height 0.5s cubic-bezier(0, 1, 0, 1);\n  overflow: hidden;\n  position: relative;\n  will-change: max-height;\n  \n  &.memo-text-expanded {\n    max-height: 1000px;\n    transition: max-height 1s ease-in-out;\n    \n    &::after {\n      opacity: 0;\n    }\n  }\n} 이렇게 2줄 이상일 때 아래 그라데이션이 생기고 (::after 부분) 접고 펼 수 있는 기능이다. max-height 를 조절해서 구현했는데, 펼칠 때는 너무 빠르고, 닫을 때는 너무 느린 현상이 발생했다. 여기에서 처럼 cubic-bezier 를 조절했다. !hint\nIt's because you're animating between 0 and 1000px max-height, but your content is only about 120px high. The delay is the animation happening on the 880 pixels you can't see.\nmax-height 가 1000px 에서 0으로 주는 동안 120px 짜리의 컨텐츠는 아무 동작을 안하는 것처럼 보이는 것이다. .text {\n  overflow: hidden;\n  max-height: 0;\n  transition: max-height 0.5s cubic-bezier(0, 1, 0, 1);\n  &.full {\n    max-height: 1000px;\n    transition: max-height 1s ease-in-out;\n} 그래서 이렇게 극단적으로 애니메이션 동작 지점을 단축시키면서 해결한듯 하다. html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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);}",{"id":2854,"title":1863,"titles":17607,"content":17608,"level":90},[],"Vue에서 부모 컴포넌트의 비동기 초기화 작업이 완료된 시점을 자식 컴포넌트에게 안전하게 전달하는 방법을 알아봅니다. Promise.withResolvers를 활용한 패턴을 소개 일반적으로 Vue에서는 부모가 자식에게 값을 전달하고, 자식은 이벤트를 통해 부모에게 데이터를 전달하는 Props Down, Events Up 방식을 따른다. 부모가 자식의 내부 함수를 직접 호출하는 방식은 컴포넌트 간 결합도를 높이므로 지양하는 편이다. 하지만 부모 컴포넌트의 특정 비동기 작업이 완료된 시점을 자식 컴포넌트가 정확히 알아야 할 때가 있다. 예를 들어, 부모가 비동기로 초기화 데이터를 모두 가져온 후에만 자식 컴포넌트가 로직을 수행해야 하는 경우다. 부모의 isInitialized 변수를 자식에게 전달하고, 자식은 watch 로 감지하기.자식의 parentInitialized() 함수를 defineExpose 로 export 한 후 template ref 로 호출하기.promise 를 전달해서 적절한 시점에 자식이 then 블럭에서 기능 수행하기. 이 중 3번째 방법을 고안해냈다. 첫 번째 방법(watch)은 의도하지 않은 시점에 변수가 변경되어 사이드 이펙트가 발생할 가능성이 있다. 단순한 값이 아닌 복잡한 초기화 흐름을 제어하고 싶을 때, 예기치 않은 타이밍에 감지가 일어나 로직이 꼬일 수 있다. 또한 자식 컴포넌트가 마운트되기 전에 이미 부모의 초기화가 끝나버렸다면 감지가 불가능할 수도 있다. 두 번째 방법(defineExpose)은 부모가 자식의 구현 상세(메서드명 등)를 알아야 하므로 결합도가 높아진다.",{"id":17610,"title":1913,"titles":17611,"content":17612,"level":90},"\u002Fblog\u002F260119-tracking-parent-the-time#promise-전달해서-자식도-초기화-시점-감지하기",[],"ECMAScript 2024 에 추가된 Promise.withResolvers를 활용하면 완료 시점을 깔끔하게 싱크할 수 있다. 일반적인 비동기 함수는 실행과 동시에 Promise를 반환한다. 하지만 Promise.withResolvers를 사용하면 Promise 객체와 이를 해결할 수 있는 resolve 함수를 분리하여 선언할 수 있다. 덕분에 부모는 원하는 복잡한 로직을 모두 수행한 뒤, 아주 정밀한 타이밍에 자식에게 신호를 보낸다. \u003Cscript setup lang=\"ts\">\n\u002F\u002F Promise와 resolve 함수를 외부로 추출하여 선언\nconst initReady = Promise.withResolvers\u003Cvoid>();\n\nasync function runInit() {\n  try {\n    \u002F\u002F API 호출, 로컬 스토리지 확인, 권한 체크 등 복잡한 초기화 수행\n    await doSomethingComplex();\n    \n    \u002F\u002F 모든 작업이 끝난 원하는 시점에 약속 이행\n    initReady.resolve();\n  } catch (error) {\n    initReady.reject(error);\n  }\n}\n\nonMounted(() => {\n  runInit();\n});\n\u003C\u002Fscript>\n\u003Ctemplate>\n  \u003CChild :init-ready=\"initReady.promise\" \u002F>\n\u003C\u002Ftemplate> 부모는 initReady라는 통신 창구를 만들어둔다. initReady.resolve()가 호출되는 순간 대기하고 있던 자식 컴포넌트들에게 신호가 전달된다. 자식에게는 initReady.promise 객체만 Props로 전달하면 된다. \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  initReady: Promise\u003Cvoid>\n}>();\n\nonMounted(async () => {\n  \u002F\u002F 부모의 초기화가 끝날 때까지 대기\n  await props.initReady;\n  \n  \u002F\u002F 이후 로직 실행\n  console.log(\"부모의 준비가 끝나 자식 로직을 시작합니다.\");\n});\n\u003C\u002Fscript> await props.initReady를 통해 부모의 초기화가 완료된 이후에만 로직이 실행되도록 보장할 수 있다.",{"id":17614,"title":2298,"titles":17615,"content":17616,"level":90},"\u002Fblog\u002F260119-tracking-parent-the-time#자식이-v-if-로-마운트의-on-off-를-반복할-때",[],"자식 컴포넌트가 v-if에 의해 언마운트되었다가 다시 마운트되는 경우, initReady 신호를 놓치지 않을까 걱정할 수 있다. 하지만 Promise는 한 번 resolve 되면 그 상태(Fulfilled)가 유지된다. 따라서 자식 컴포넌트가 다시 생성되어 await props.initReady를 실행하더라도, 이미 완료된 Promise이므로 즉시 다음 로직이 실행된다. 매번 상태를 다시 확인할 필요 없이 안정적인 동작을 보장한다.",{"id":17618,"title":2318,"titles":17619,"content":17620,"level":90},"\u002Fblog\u002F260119-tracking-parent-the-time#provide-inject-로-깊은-여러-컴포넌트에-전달",[],"컴포넌트 계층이 깊거나(Prop Drilling 문제), 여러 자식 컴포넌트가 동시에 신호를 받아야 하는 경우 provide와 inject를 활용할 수 있다. const initReady = Promise.withResolvers\u003Cvoid>();\n\u002F\u002F promise 를 provide\nprovide('initReady', initReady.promise); 부모는 provide를 통해 Promise 객체를 하위 컴포넌트들에 공급한다. \u002F\u002F promise 주입받기\nconst initReady = inject\u003CPromise\u003Cvoid>>('initReady');\n\nonMounted(async () => {\n  if (initReady) {\n    await initReady;\n    console.log(\"초기화 완료 후 자식 로직 실행\");\n  }\n}); inject를 통해 받아와서 사용하면 된다. 하지만 단순히 문자열 키('initReady')로 주입받으면 타입 추론이 안 되거나, 데이터의 출처를 명확히 알기 어려워 유지보수가 힘들어질 수 있다.",{"id":17622,"title":2479,"titles":17623,"content":17624,"level":101},"\u002Fblog\u002F260119-tracking-parent-the-time#composable-를-활용해-추적을-용이하게-하기",[2318],"provide\u002Finject 패턴은 키 관리나 타입 정의가 번거로울 수 있으므로, Composable 함수(Hook)로 캡슐화하여 사용하는 것을 선호한다. import { provide, inject, InjectionKey } from 'vue';\n\u002F\u002F 심볼을 사용하여 고유한 키 정의 \nconst InitReadyKey: InjectionKey\u003CPromise\u003Cvoid>> = Symbol('InitReady');\n\n\u002F**\n * 부모에서 호출할 provide 함수\n *\u002F\nexport function provideInit(promise: Promise\u003Cvoid>) {\n  provide(InitReadyKey, promise);\n}\n\n\u002F** \n * 자식에서 사용할 composable\n *\u002F\nexport function useInit() {\n  const initReady = inject(InitReadyKey);\n  \u002F\u002F provide 없으면 에러\n  if (!initReady) {\n    throw new Error('useInit()은 provideInit() 범위 내에서 사용해야 합니다.');\n  }\n  return initReady;\n} 구조는 간단하다. provide와 inject 로직을 하나의 파일에서 관리하여 키(InjectionKey)와 타입의 일관성을 유지한다. \u003Cscript setup lang=\"ts\">\nimport { provideInit } from '.\u002FuseInit';\nconst initReady = Promise.withResolvers\u003Cvoid>();\nprovideInit(initReady.promise); \u002F\u002F 추상화된 함수 호출\n\u003C\u002Fscript>\n\n\u003Cscript setup lang=\"ts\">\nimport { useInit } from '.\u002FuseInit';\nconst initReady = useInit(); \u002F\u002F 내부 로직을 몰라도 안전하게 주입받음\n\u003C\u002Fscript> 타입 추론도 제공하고, 단일 파일에서 파악이 가능하기 때문에 가독성도 높고 유지보수하기 좋다. 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 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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"id":2988,"title":2865,"titles":17626,"content":17627,"level":90},[],"안드로이드 노티피케이션을 터치해 액티비티를 진입할 때 onCreate 에서 전달받는 데이터가 원치 않는 값으로 들어온다. 해결 방법을 찾아보자. fun sendNoti(title: String, message: String, data: Map\u003CString, String>) {  \n    val intent = Intent(this, MainActivity::class.java).apply {  \n        flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP\n        data.get(\"key\")?.let {value -> \n            this.putExtra(\"key\", value)\n        }\n    }\n    val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_MUTABLE) \n    val notificationBuilder = NotificationCompat.Builder(this, channelId)\n        .setContentIntent(pendingIntent)  \n} 이렇게 넣어주었는데, 노티를 띄울 때는\nintent Bundle[{key=newvalue}]\n로 표시되고 onCreate 에서 노티 데이터를 받았을 때는\nbundle Bundle[{key=oldvalue}] 로 표시된다. 가장 최근으로 띄운 노티의 데이터가 아닌, 이젠 노티의 데이터를 사용하는 듯 하다. Android keeps caching my intents Extras, how to declare a pending intent that keeps fresh extras? - Stack Overflow 가장 최신의 노티 데이터를 항상 덮어 쓰게끔 하면 된다! FLAG_UPDATE_CURRENT 플래그를 사용하고requestCode 를 0 고정 값이 아니라 유니크한 값으로 지정해주면 된다. val pendingIntent = PendingIntent.getActivity(this, System.currentTimeMillis().toInt(), intent, FLAG_MUTABLE) 우선은 별다른 규칙 없이 현재 타임스탬프를 지정해주니까, 내가 넣어준 마지막 값으로 잘 들어온다. 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);}",{"id":3263,"title":2998,"titles":17629,"content":17630,"level":90},[],"CSS 변수의 투명도를 조절하는 방법을 자세히 다루고, color-mix 함수를 통해 배경에 효과적인 투명도를 적용하는 기법을 소개한다. 다양한 컬러 코드 및 변수를 활용하여 디자인에 깊이를 더하는 방법을 배울 수 있다 --color-base-20: #ddd; 이런식으로 정의한 컬러 코드 값의 변수로 배경에 투명도를 섞어서 사용할 일이 생겼다. background-color: rgba(--color-base-20, 0.5);\nbackground-color: rgba(var(--color-base-20), 0.5);\nbackground: var(--color-base-20) rgba(0, 0, 0, 0.5); 등 다양한 형태로 사용해봤지만, 모두 작동하지 않는다. hex 형태의 컬러코드는 rgba 에서 사용이 불가능하다. --color-base-20: 240, 240, 240;\nbackground-color: rgba(var(--color-base-20), 0.5); 변수를 rgb 값을 찢어놓는 방법도 있다. 그런데 기존 변수들은 건들고 싶지 않았다.",{"id":17632,"title":3084,"titles":17633,"content":17634,"level":101},"\u002Fblog\u002Fcolor-mix-hash-value-transparency#color-mix",[2998],"background-color: color-mix(in srgb, var(--color-base-20), #0000 50%); 이렇게 색상을 섞을 수 있는 mixin 이 있다. 타입 : hsl, lch, srgb, lab, custom-color-space색상 : 색상 % 투명도만 주고 싶은 경우이기 때문에, 첫번째 색상은 원본 색상을 주고, 두번째는 완전 투명한 색상에 투명도를 50%로 주어서 섞었다. 그럼 rgba(--var, 0.5) 와 동일한 효과가 된다. 웬만하면 다 쓸 수 있는듯~",{"id":17636,"title":3142,"titles":17637,"content":17638,"level":101},"\u002Fblog\u002Fcolor-mix-hash-value-transparency#tailwindcss-를-사용하면-alpha-를-사용할-수-있다",[2998],".my-element {\n  color: --alpha(var(--color-lime-300) \u002F 50%);\n}\n\u002F* compiled css *\u002F\n.my-element {\n  color: color-mix(in oklab, var(--color-lime-300) 50%, transparent);\n} 결국 css 로 컴파일되면 color-mix 이긴 하다. html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"id":7524,"title":3272,"titles":17640,"content":17641,"level":90},[],"Github.io 와 Vue3 를 활용해서 블로그를 시작. SPA를 prerendering 하여 개별 페이지를 생성하고, 메타태그를 추가. 구글 검색 엔진에 등록할 사이트맵 자동 생성. 마크다운을 html 로 변환. Google Analytics 도입으로 방문자 수 체크. 개발 블로그를 작성하기 위해서 github.io 를 사용하기로 했다. velog나 tistory, medium 같은 것들도 있지만, 내가 완전히 커스터마이징 가능한 페이지를 갖고 싶었다. 그렇다고 서버를 돌리거나, AWS를 통해서 구축하는 수고까지는 하지 않으려 했다. 지금 와서 생각해보면 거의 비슷한 공수가 들었다. Notion도 고려했는데, 내 개인 노트로 아주 잘 활용하고 있으나 블로그로 사용하기에는 너무 에디터다. 이렇게 쓰니까 깃헙을 고른 이유는 그냥 해보고싶어서에 가까워 보인다. 페이지를 구축할 언어로는 내가 지금 사용하고 있는 Vue 프레임워크를 사용할 것이다. 단순히 정말 지금 내가 익숙한 것이기 때문에 골랐다. 그렇다고 블로그에 대단한 기능을 넣을 것은 아니다. 나중에 게시글 뿐 아니라 포트폴리오의 역할도 해줄 수 있을 것 같아서, 주력 프레임워크를 선택해야겠다고 다짐했다. ENJOY DEV 참고한 블로그는 위 블로그이다. 내가 하려던 것을 똑같이 이미 구현해서 사용하고 계셨기 때문에 참고하기에 너무 훌륭한 블로그였다. 직접 블로그에 포스팅도 해주셨는데, 중간중간 소스도 염탐했다.",{"id":17643,"title":3294,"titles":17644,"content":17645,"level":101},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#githubio-페이지-만들기",[3272],"당연히 Github 계정이 있어야한다. [유저명.github.io](http:\u002F\u002F유저명.github.io) 라고 프로젝트를 하나 만든다. 이렇게 프로젝트를 만들면 자동으로 Github Pages 를 사용하겠다고 알리는 격이다. 필수는 아니고, 나중에 Github Pages 를 사용하겠다고 따로 설정할 수도 있다. 배포 대상 브랜치를 선택하고, 폴더를 선택하여 저장하면 끝이다. 하지만 이름을 짓기도 귀찮고, 굳이 따로 설정할 필요가 없으니 규칙을 따라서 진행한다.",{"id":17647,"title":3318,"titles":17648,"content":17649,"level":101},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#github-actions-로-github-pages-배포",[3272],"Github Pages 로 사용되는 레포는 대부분 결과물을 관리하는 레포이다. 레포에 푸시가 되면 Github 봇이 감지하여, 자동으로 Github Pages 배포를 실행한다. 나는 vue 앱을 빌드하고 결과물을 배포하는 것까지 한번에 관리를 하고 싶다. 방법은 세 가지가 있다. gh-pages 활용수동 빌드github actions 등록 내가 선택한 방법은 3번이고, 1번은 시도도 안해봤다. 하지만 gh-pages 를 많이들 사용한다. 간단하게 설명을 하자면,",{"id":17651,"title":3346,"titles":17652,"content":17653,"level":117},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#gh-pages",[3272,3318],"gh-pages 라는 라이브러리를 가지고, 빌드 결과물을 gh-pages 브랜치 에 publish 하는 것이다. 기본적으로는 package.json 파일의 homepage 속성에 해당하는 url 로 배포를 하게 된다.",{"id":17655,"title":3337,"titles":17656,"content":17657,"level":117},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#수동-빌드",[3272,3318],"수동 빌드 는 Github Pages 설정 페이지에서 호스팅할 폴더를 고를 수 있다. 기본적으로 root 폴더이며, \u002Fdocs 폴더를 추가로 지정할 수 있다. 해당 레포 브랜치의 docs 폴더를 호스팅하겠다는 것이다. \u002F\u002F vue.config.js\nmodule.exports = {\n  outputDir: 'docs',\n} 빌드의 결과물을 docs 폴더에 생성되게 하고, 레포에 올려주기면 하면 된다. vue 의 경우 outputDir 속성을 docs 로 변경해주면 간단하게 결과 폴더가 바뀐다. 추가로 .gitignore 에 docs 폴더가 있다면 항목을 제거해서 원격 레포에 푸시해주면 자동으로 봇이 실행되면서 페이지를 배포한다.",{"id":17659,"title":3432,"titles":17660,"content":17661,"level":117},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#github-actions",[3272,3318],"Github Actions 는 간단하게 설명하면, CICD 툴이다. 젠킨스나, aws pipeline, circleCI 같은 역할을 한다. Gitlab 에도 비슷한 기능이 있는데, 이쪽은 아직 써보지 못했다. Github Actions 는 프로젝트에 포함된 yaml 파일을 기반으로 명령을 실행한다. name: Deployment to Github Pages\non:\n  push:\n    branches:\n      - deploy\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@master\n      - name: Set up Node.js\n        uses: actions\u002Fsetup-node@master\n        with:\n          node-version: 16.x\n      \n      - name: Install dependencies\n        run: npm install\n\n      - name: Build\n        run: npm run build\n        env:\n          NODE_ENV: production\n      \n      - name: Deploy\n        uses: peaceiris\u002Factions-gh-pages@v2.5.0\n        env:\n          PERSONAL_TOKEN: $\\{\\{secrets.GH_TOKEN\\}\\}\n          PUBLISH_BRANCH: master\n          PUBLISH_DIR: .\u002Fdist\n          SCRIPT_MODE: true on 은 트리거로 Github 대부분의 훅을 활용할 수 있다. 가장 기본적으로 푸시가 있으며, 라벨, 이슈 관리, 풀 리퀘스트, 스케줄 등이 있다. 자세한 건 Github Docs 에 자세히 작성되었다. jobs 는 말 그대로 작업이며, 적힌대로 쭉 작업을 진행한다. 위 코드를 간략히 보자면, “deploy 브랜치의 푸시가 감지되면, 코드를 체크아웃한 후에 npm install 로 종속성을 모두 다운로드 받고, npm run build 로 빌드를 한 후, gh-pages 아티팩트를 활용해서 master 브랜치에 dist 폴더에 담긴 결과물을 배포” 이다. 이 때 활용하게 되는 것이 Github Secrets 이다. Actions 실행할 때 필요한 암호키나 레포에 노출되지 말아야 하는 중요한 데이터의 경우 레포 설정에 저장해놓고 사용함으로써 외부로 노출시키지 않아도 된다. 간단하게 이름-값 으로 이루어져 있어서 대충봐도 키를 등록할 수 있을 것이다. 데이터를 사용할 때는 PERSONAL_TOKEN: $\\{\\{secrets.GH_TOKEN\\}\\} 이런 식으로 사용한다. master 브랜치에 결과물이 배포되기 때문에 Pages 설정을 알맞게 바꿔준다. Github Workflow 는 두 번 돌게 된다. deploy 브랜치에 배포를 하면 Github Actions 가 실행되어 빌드하고, master 브랜치에 결과물을 푸시한다.그럼 Gihub 봇이 알아채고 페이지에 배포를 시작한다. 봇이 돌린 Workflow 에서 배포가 완료된 우리의 페이지도 확인할 수 있다. 아래 Artifacts 는 Jekyll 이 빌드한 결과물이다. 실제로 보면 npm build 결과를 그대로 래핑하는 것과 매한가지다.",{"id":17663,"title":3793,"titles":17664,"content":17665,"level":101},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#블로그-포스트-목록-관리하기",[3272],"따로 서버를 관리할 것이 아니라면 레포에 글 목록도 관리를 하는 게 맞다고 판단했다. 클라에서 모든 것을 해결하는 것은 어렵지 않다. 데이터를 직접 작성해서 갖고 있으면 된다. \u002F\u002F public\u002Fposts\u002Fpostlist.json\n[\n  {\n    \"url\": \"d5c653f34310b85f731a497e64970059921fba363647f5cc1e39ead9ea9cf76f\",\n    \"fileName\": \"first-post\",\n    \"title\": \"블로그를 Github.io 로 시작하는 첫 포스트\",\n        \"description\": \"요약을 써줘요\",\n    \"createdAt\": \"2022-06-24\",\n    \"updatedAt\": \"2022-06-25\",\n    \"tags\": [\"tag1\"]\n  },\n  {\n    \"url\": \"8689a6f9a9428af5a3570a0236abdb9abf62409077138a3d02a676b3a3f757f4\",\n    \"fileName\": \"second-post\",\n    \"title\": \"두 번째 포스트\",\n        \"description\": \"요약을 써줘요\",\n    \"createdAt\": \"2022-06-24\",\n    \"updatedAt\": \"2022-06-25\",\n    \"tags\": [\"tag1\"]\n  }\n] static 하게 직접 관리할 대상이기 때문에, public 폴더에 두었다. Vue 빌드 시 public 폴더 안의 내용물은 그대로 결과폴더에 복사되기 때문에, 깃헙에서 바로 읽어올 수 있다. import { VuexModule, Module, Mutation, Action } from \"vuex-module-decorators\";\nimport axios from \"axios\";\n\nexport interface Post {\n  url: string;\n  fileName: string;\n    description: string;\n  title: string;\n  tags: string[];\n  data: string;\n}\n\n@Module({ namespaced: true, })\nclass Posts extends VuexModule {\n  public postList:Post[] = [];\n  public currentUrl = '';\n\n  get currentPost(): Post | undefined {\n    if(this.currentUrl == null || this.currentUrl == '' || this.postList.length \u003C= 0){\n      return undefined;\n    }\n    else {\n      return this.postList.find(post => {\n        return post.url === this.currentUrl;\n      })\n    }\n  }\n  \n  @Mutation\n  public setPostList(postList: Post[]) {\n    this.postList = postList;\n  }\n  @Mutation\n  public setCurrentUrl(currentUrl: string) {\n    this.currentUrl = currentUrl;\n  }\n\n  @Action\n  public requestGetPostList() {\n    axios.get(`\u002Fposts\u002Fpostlist.json`).then(res => {\n      this.context.commit(\"setPostList\", res.data);\n    });\n  }\n\n  @Action\n  public moveCurrentUrl(url: string) {\n    this.context.commit(\"setCurrentUrl\", url);\n  }\n}\n\nexport default Posts; posts store의 기능은 다음과 같다. 포스트 목록을 postlist.json 에서 읽어오기(사이트 접속 시 한 번)\n사이트 접속 시 한 번이라고 판단한 근거는 기술 블로그를 찾아다닐 때는 보통 구글링을 통해서 유입된다. 그렇다고 한번 들어온 사람이 그 블로그의 다른 글까지도 샅샅이 뒤져보는 경우는 거의 없다. 원하는 정보를 확인한 후 바로 나가는 것이 태반이다. 또 대부분 최신의 포스팅 보다는 최소 한 두달 전의 게시글을 확인하기 마련이며, 글 목록을 계속해서 갱신할 필요는 없다고 판단했다.포스트 페이지에서 전달받은 포스트 url 을 갖고 해당하는 포스트를 조회한다.(포스트 페이지 진입 시)\n이 부분은 갖고 있는 포스팅 목록에서 단순히 조회하는 기능이다. 추후에 글이 엄청 많아져서 조회에 걸리는 데 부하가 걸리면 행복한 고민이니까 나중에 하자. \u002F\u002F App.vue\nimport { useStore } from 'vuex';\nconst store = useStore();\nstore.dispatch(\"Posts\u002FrequestGetPostList\"); 사이트 최초 접속 시에만 포스트 목록을 가져오게끔 App.vue 의 setup 훅에서 조회하도록 했다. \u002F\u002F DetailView.vue\nimport { ref, defineProps, onMounted, computed } from 'vue';\nimport { useRoute } from 'vue-router';\nimport { useStore } from 'vuex';\n\nconst store = useStore();\nconst route = useRoute();\nconst paramId = route.params.id;\n\nstore.dispatch(\"Posts\u002FmoveCurrentUrl\", paramId);\nconst post = computed(() => {\n  return store.getters[\"Posts\u002FcurrentPost\"];\n}); DetailView 가 포스트 내용을 보여줄 뷰인데, 포스트 url 을 확인한 후 해당하는 포스트 데이터를 가져오게 했다. 이걸 토대로 글 목록을 보여주니 잘 나온다.",{"id":17667,"title":4814,"titles":17668,"content":17669,"level":101},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#markdown-적용하기",[3272],"나는 개발 노트를 2014년부터 사용했는데, 에버노트 - Typora - Joplin - 노션 순서로 넘어왔다. Typora 와 Joplin 은 사용기간이 매우 짧아서 에버노트 사용 후 노션으로 넘어왔다고 봐도 무방하다. 그 이유가 마크다운이었던 만큼, 개발 블로그는 무조건 마크다운이다 라고 생각했다. npm i showdown\nnpm i showdown-table showdown 이라는 라이브러리를 사용할 것이다. 참고한 블로그에서 사용했기 때문이다. 왠지 마크다운 변환 라이브러리가 많을 것 같아서 다 알아봐야하는 데 시간 좀 걸리겠다 했는데, 빠르게 정할 수 있었다. 이 라이브러리는 단순하게 md 데이터를 html 로 마크업해주는 역할이다. 단순 스트링을 인자로 넘겨주면 된다. showdown-table 은 이름 그대로 table 까지 지원해주는 확장라이브러리이다. 그 말은 즉슨 showdown 라이브러리에서는 기본으로 지워하지 않는다는 뜻이다. \u002F\u002F src\u002Fstore\u002Fposts.ts\n@Action\npublic requestGetMarkdoen(postName: string) {\n  return axios.get(`\u002Fposts\u002F${postName}.md`).then(res => {\n    const markdownPost = res.data;\n    **const converter = new showdown.Converter()**\n        converter.setOption('tables', true);\n    **const md2html = converter.makeHtml(markdownPost);\n    return md2html;**\n  });\n} 사용 방법도 간단하다. converter 를 할당해주고, makeHtml 함수를 호출하면 끝이다. table 확장을 추가할 때, 공식 Github 문서에서는 new showdown.Converter({ extensions: ['table'] }) 로 사용하라고 하지만, md 변환이 전혀 안되는 문제가 있어서 다른 방법을 찾았다. \u002F\u002F DetailView.vue\n\u003Cdiv v-html=\"postContents\">\u003C\u002Fdiv>\n\n\u002F\u002F script\nonMounted(() => {\n  store.dispatch(\"Posts\u002FrequestGetMarkdoen\", post.value.fileName).then((res) => {\n    postContents.value = res;\n  })\n}) DOM의 innerValue 로 넣어주면 간단하게 md 로 작성했던 포스팅이 html 이 렌더링된다. 간단-",{"id":17671,"title":5114,"titles":17672,"content":17673,"level":101},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#prerendering",[3272],"Github Pages 특성상 SPA 지원을 안하기 때문에, 홈이 아닌 다른 URL을 접속하려고 하면 404 페이지가 뜬다. 물론 SPA 사이트도 홈에서 라우팅을 한다면 작동은 하겠지만은 누가 블로그를 홈에서부터 여행을 하겠는가. 구글엔진은 SPA도 js를 실행해 크롤링한다고는 하는데, Github Pages는 홈을 제외한 페이지에 아예 접속조차 안되며, SEO도 신경써야하니 Prerendering 은 필수이다. npm i -D prerender-spa-plugin -> webpack4\nnpm i -D prerender-spa-wp5-plugin -> webpack5 한참 애를 먹었던 녀석이다. 참고하던 블로그에서는 Vue가 내부적으로 Webpack4 를 사용해서 원본 라이브러리로도 잘 돌아가지만, 내가 만든 프로젝트에서는 계속 에러가 발생했다. prerender-spa-plugin Unable to prerender all routes! 바로 이 에러인데, 내용도 모른다. 콜스택은 에러를 뿌려주는 코드의 에러로만 나타나서 어디가 문제인지도 한눈에 알아볼 수 없었다. 개같이 찾아봤지만 확실한 답을 얻지 못하고, 아예 라이브러리 소스를 디버깅해보기로 했다. 저 에러를 콘솔에 찍는 부분부터 역으로 코드를 확인해서 답을 얻었다. Webpack5 에서의 complier.outputFileSystem 에는 mkdirp 가 없는데, 이걸 사용하려고 해서 에러가 발생했다. Webpack5로의 마이그레이션이 전혀 안된 것이다. 그제야 Github에서도 2018년에 업데이트가 끝났다는 걸 확인했다. 소스를 바꿔서 사용해야하나 싶었다. \u002F\u002F node_modules\u002Fprerender-spa-plugin\u002Fes6\u002Findex.js\n\n\u002F\u002F 58 line\n\u002F\u002F mkdirp 함수를 대체한다.\nconst mkdirp = function (dir, opts) {\n  return new Promise((resolve, reject) => {\n    console.log('\\ndir', dir, opts, '\\n');\n    try {\n      compilerFS.mkdirp(dir, opts, (err, made) => err === null ? resolve(made) : reject(err))\n    } catch(e) {\n      compilerFS.mkdir(dir, opts, (err, made) => err === null ? resolve(made) : reject(err))\n    }\n  })\n}\n\n\u002F\u002F 124 line\n\u002F\u002F recursive 옵션을 추가해준다.\nreturn mkdirp(path.dirname(processedRoute.outputPath), {recursive: true}) 소스 수정 항목은 매우 간단하다. mkdir 함수로 바꾸면 되는데 이러면 또 문제가 생긴다. 포스팅 여러개를 렌더링할 때 mkdir 을 여러번 호출하는데, 중복이 된 경우에도 예외가 발생해서 렌더링이 안된다. 그래서 recursive 옵션까지 주어야 완벽하게 호환이 된다. 하지만 문제는 Github Actions 를 사용한다는 것이다. Actions 가 실행될 때는 npm install 로 종속성 다운로드 후 바로 빌드를 하기 때문에 바뀐 소스를 적용하지 못한다. 그래서 결국 Webpack5 버전으로 누군가 새로 올려준 라이브러리를 받아서 진행했다. 그게 prerender-spa-wp5-plugin 이다. const path = require('path')\nconst PrerenderSpaPlugin = require('prerender-spa-wp5-plugin')\n\nconst posts = require('.\u002Fpublic\u002Fposts\u002Fpostlist.json')\nconst routes = posts.map(post => `\u002F${post.url}`)\nconst paths = posts.map(post => {\n  return { \n    path: `\u002F${post.name}\u002F`,\n    lastmod: post.lastmod || post.date,\n    changefreq: 'yearly'\n  }\n})\n\nmodule.exports = [\n  new PrerenderSpaPlugin({\n    staticDir: path.join(__dirname, 'dist'),\n    routes: [\"\u002F\", ...routes],\n    renderer: new PrerenderSpaPlugin.PuppeteerRenderer({\n      renderAfterElementExists: '#app',\n    }),\n  }),\n];\n\u002F\u002F vue.config.js\nconst webpackPlugins = require('.\u002Fwebpack.plugin');\nmodule.exports = {\n    configureWebpack: (config) => {\n      if (process.env.NODE_ENV === 'production') {\n        config.plugins.push(...webpackPlugins); \u002F\u002F 상단에서 정의한 postPlugins 내용 삽입\n      }\n    },\n} 위와 같이 Webpack 플러그인을 작성한 후 등록해주면 간단하게 끝난다. 빌드하면 결과물 폴더에, 변환한 URL에 해당하는 index.html 파일들이 우수수 빌드가 된 걸 확인할 수 있다. 그리고 포스팅 URL 로 바로 접속도 가능해졌다.",{"id":17675,"title":5865,"titles":17676,"content":17677,"level":101},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#sitemap-자동-생성",[3272],"npm i -D sitemap-webpack-plugin 사이트맵 생성에는 sitemap-webpack-plugin 을 사용한다. 개발자들의 스승님인 구글 검색 엔진에 블로그가 더 잘 노출이 되고, 크롤링에 도움을 주기위해서 사이트맵은 필요하다. Webpack 이 참 플러그인 생태계가 좋다. const SitemapPlugin = require('sitemap-webpack-plugin').default;\nconst paths = posts.map(post => {\n  return { \n        path: `\u002F${post.url}\u002F`,\n    lastmod: post.updatedAt || post.createdAt,\n    changefreq: 'yearly'\n  }\n})\n\nmodule.exports = [\n    ...,\n  new SitemapPlugin({\n    base: process.env.VUE_APP_BASE_URL,\n    paths\n  })\n]; 또 플러그인을 작성하고, 빌드하면 끝이다. \u003Curl>\n    \u003Cloc>https:\u002F\u002Flhs-source.github.io\u002Fd5c653f34310b85f731a497e64970059921fba363647f5cc1e39ead9ea9cf76f\u002F\u003C\u002Floc>\n    \u003Clastmod>2022-06-25T00:00:00.000Z\u003C\u002Flastmod>\n    \u003Cchangefreq>yearly\u003C\u002Fchangefreq>\n\u003C\u002Furl>\n\u003Curl>\n    \u003Cloc>https:\u002F\u002Flhs-source.github.io\u002F8689a6f9a9428af5a3570a0236abdb9abf62409077138a3d02a676b3a3f757f4\u002F\u003C\u002Floc>\n    \u003Clastmod>2022-06-25T00:00:00.000Z\u003C\u002Flastmod>\n    \u003Cchangefreq>yearly\u003C\u002Fchangefreq>\n\u003C\u002Furl> 이렇게 구글 엔진에 등록할 sitemap.xml 이 저절로 만들어진다. 결과물 폴더에 만들어지기 때문에, 브라우저에서도 바로 접속할 수 있다.",{"id":17679,"title":6173,"titles":17680,"content":17681,"level":117},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#사이트맵-등록하기",[3272,5865],"Google Search Console 구글 검색 콘솔에 접속한다. 우선은 사이트를 생성하고, 그 사이트에 사이트맵을 등록할 것이다. URL 을 입력하고 다음으로 넘어가자. \u003C!DOCTYPE html>\n\u003Chtml lang=\"\">\n  \u003Chead>\n    \u003Cmeta name=\"google-site-verification\" content=\"p4lZFdtKOsDj6_1O2f_PsufQnO068kmB_Sgrs5UlweQ\" \u002F>\n    \u003C\u002Fhead>\n\u003C\u002Fhtml> 제일 간단한 메타태그 추가를 통해 사이트 인증을 진행한다. 사이트 루트에 메타태그가 있어야하기 때문에, public\u002Findex.html 에 메타태그를 추가해주자. 블로그 루트 페이지에 추가된 것을 확인하고 다시 콘솔로 간다. 소유권을 확인받았다. 사이트맵은 자동생성되었기 때문에, \u002Fsitemap.xml 를 통해 접속할 수 있다. URL로 한번 접속해보고 데이터가 잘 오는 걸 확인하고 제출하자. 성공!",{"id":17683,"title":6322,"titles":17684,"content":17685,"level":101},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#메타태그-작성하기",[3272],"블로그 내용에 대해서 잘 알려주기 위해 렌더링하는 포스팅 페이지에 메타태그를 추가하자. 누군가 링크를 퍼가거나, 검색엔진에서 긁어갈 때 메타태그가 아주 중요한 역할을 한다. module.exports = [\n  new PrerenderSpaPlugin({\n    postProcess(renderedHtml) {\n            let { html, route } = renderedHtml;\n      console.log('renderedHtml', renderedHtml);\n      const foundPost = posts.find(post => route.includes(post.url))\n      if(foundPost == null) {\n        return renderedHtml;\n      }\n      const { title, description, tags } = foundPost;\n      const titleText = title ? title.replace(\u002F\u003Cbr>\u002Fig, '') : process.env.VUE_APP_TITLE\n      const descriptionText = description || '이현수 개발기'\n      const tagsText = tags || '개발, 프론트엔드, 블로그, github pages, Vue3'\n      const url = `${process.env.VUE_APP_BASE_URL}${route}`\n      const imgUrl = `${process.env.VUE_APP_BASE_URL}\u002Fimages\u002Fthumbnail.jpg`\n\n      const metaData = `\n        \u003Ctitle>${titleText}\u003C\u002Ftitle>\n        \u003Cmeta name=\"title\" content=\"${titleText}\" \u002F>\n        \u003Cmeta name=\"description\" content=\"${descriptionText}\" \u002F>\n        \u003Cmeta name=\"keywords\" content=\"${tagsText}\" \u002F>\n        \u003Cmeta property=\"og:url\" content=\"${url}\" \u002F>\n        \u003Cmeta property=\"og:type\" content=\"article\" \u002F>\n        \u003Cmeta property=\"og:title\" content=\"${titleText}\" \u002F>\n        \u003Cmeta property=\"og:description\" content=\"${descriptionText}\" \u002F>\n        \u003Cmeta property=\"og:image\" content=\"${imgUrl}\" \u002F>\n        \u003Cmeta property=\"twitter:card\" content=\"${imgUrl}\" \u002F>\n        \u003Cmeta property=\"twitter:url\" content=\"${url}\" \u002F>\n        \u003Cmeta property=\"twitter:title\" content=\"${titleText}\" \u002F>\n        \u003Cmeta property=\"twitter:description\" content=\"${descriptionText}\" \u002F>\n        \u003Cmeta property=\"twitter:image\" content=\"${imgUrl}\" \u002F>\n      `;\n      const start = html.indexOf('\u003Chead>') + '\u003Chead>'.length;\n      html = html.slice(0, start) + metaData + html.slice(start);\n      renderedRoute.html = html;\n      return renderedRoute;\n    }, 아까 사용했던 PrerenderSpaPlugin 을 활용한다. 훅 중 하나인 postProcess 에 메타 태그를 심어주는 코드를 작성한다. 필수로 들어가면 좋을 태그는 title, description , keyword 라고 한다.",{"id":17687,"title":6874,"titles":17688,"content":6877,"level":101},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#google-analytics-추가",[3272],{"id":17690,"title":6881,"titles":17691,"content":17692,"level":117},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#ga4-계정-생성하기",[3272,6874],"Redirecting... GA 콘솔에 들어가서 계정과 속성을 차례대로 만들어준다. 속성 단위로 데이터를 종합하고 집계해서 대시보드를 작성한다고 보면 된다. 속성을 만들 때 시간대는 모두 대한민국으로 맞춰주면 된다. 그 후 데이터스트림을 만들어준다. 스트림 이름은 적당히 알아볼 수 있게 하고, 중요한 것은 “측정 ID” 이다. 이것이 데이터를 전송하는 데 사용하는 키가 된다.",{"id":17694,"title":6908,"titles":17695,"content":17696,"level":130},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#로컬-테스트용-데이터스트림-만들기",[3272,6874,6881],"스트림 URL 에는 localhost 혹은 127.0.0.1 같은 루프백 주소는 입력할 수 없다. 그렇다고 테스트할 때마다 매번 Github에 배포하는 것도 번거로운 일이다. 적어도 1분에서 3분정도 걸리는데 그 시간이 누적되면 손해가 이만저만이 아니다. ##\n## Host Database\n#\n## localhost is used to configure the loopback interface\n## when the system is booting.  Do not change this entry.\n##\n127.0.0.1   localhost\n255.255.255.255 broadcasthost\n::1             localhost\n## Added by Docker Desktop\n## To allow the same kube context to work on the host and the container:\n127.0.0.1 kubernetes.docker.internal\n**127.0.0.1 blog.local**\n## End of section hosts 파일에 DNS 정보를 넣어서 가상의 URL을 만들어준 후에, 이 URL로 로컬 테스트를 진행하면 된다. 나는 단순하게 blog.local 이라고 정했다. 나의 경우 실제 오픈한 블로그와 로컬 테스트 블로그 각각에서 잡히는 데이터를 분리하고 싶었다. 그래서 아예 속성 자체를 분리했다. 그래야 섞이지 않고 올바르게 데이터를 쌓을 수 있다. \u002F\u002F vue.config.js\nmodule.exports = {\n    devServer: {\n        \u002F\u002F webpack4\n        disableHostCheck: true,\n        \u002F\u002F webpack5\n    \u002F\u002F allowedHosts: ['.host.com', 'host2.com'],\n    allowedHosts: 'all',\n  }\n} vue.config.js 에다가 추가로 설정이 필요하다. Webpack4 를 사용 중이라면 disableHostCheck 속성을, Webpack5 사용 중이라면 allowedHosts 를 사용한다.",{"id":17698,"title":7222,"titles":17699,"content":17700,"level":117},"\u002Fblog\u002Fmaking-blog-githubio-vue3-1#vue에서-ga4로-데이터-전송하기",[3272,6874],"npm install vue-gtag-next Vue에서는 vue-gtag라는 라이브러리를 사용해서 간단하게 GA 태그를 심을 수 있다. 하지만 vue-gtag 는 Vue2 까지 지원하며, Vue3 에서는 vue-gtag-next 를 사용해야한다. 사용법도 조금은 다르다. \u002F\u002F main.ts\nimport VueGTag from \"vue-gtag-next\";\n\nlet GAID = \"G-XXYYZZXXTT\";  \u002F\u002F dev\nif(process.env.NODE_ENV === \"production\") {\n    GAID = \"G-XXYYZZXXEE\";  \u002F\u002F prod\n}\n\nconst app = createApp(App)\n.use(store)\n.use(router)\n**.use(VueGTag, {\n    property: {\n        id: GAID, \u002F\u002F prod\n    }\n})**\n.mount('#app') 우선 main.ts 에서 “측정 ID” 를 가지고 GA 초기화를 해야한다. 실서버와 로컬 환경의 키를 분리해서 할당하고는, Vue app의 use 로 등록해주자. \u002F\u002F router\u002Findex.ts\nimport { trackRouter } from 'vue-gtag-next'\nconst router = createRouter({\n    ...\n})\n\ntrackRouter(router); trackRouter 로 간단하게 Vue-Router 와 연동하여 라우팅 페이지 뷰를 자동으로 인식하고 데이터를 전송할 수 있다. html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"id":7666,"title":7540,"titles":17702,"content":17703,"level":90},[],"npm의 의존성 설치 실패 문제. @rollup\u002Frollup-darwin-arm64 패키지가 애플 실리콘에서만 설치 가능하다. Windows 및 Linux 환경에서의 설치 불가 원인. optionalDependencies의 사용 방법. npm ERR! code EBADPLATFORMnpm ERR! notsup Unsupported platform for @rollup\u002Frollup-darwin-arm64@4.30.1: wanted {\"os\":\"darwin\",\"cpu\":\"arm64\"} (current: {\"os\":\"win32\",\"cpu\":\"x64\"})npm ERR! notsup Valid os:   darwinnpm ERR! notsup Actual os:  win32npm ERR! notsup Valid cpu:  arm64npm ERR! notsup Actual cpu: x64 \"dependencies\": {\n    \"@rollup\u002Frollup-darwin-arm64\": \"^4.30.1\",\n} package.json 파일에서 dependencies 에 @rollup\u002Frollup-darwin-arm64 를 종속성으로 등록해두었지만, 실행하는 환경이 Windows 혹은 Linux 등 애플실레콘이 아닐 때 일 때 설치할 수 없기 때문에 발생한다. dependencies 나 devDependencies 의 경우 설치에 실패하면 무조건 중단되며 실패로 끝난다. \"optionalDependencies\": {\n    \"@rollup\u002Frollup-darwin-arm64\": \"^4.30.1\"\n}, optionalDependencies에 등록한 의존성은 설치 실패해도 전체 설치 과정에 영향을 주지 않는 의존성이다. 즉, 설치 가능하면 설치하고, 불가능하면 무시한다. 플랫폼 환경에 따라서 설치해야하는 의존성이 있는 경우, 특히 이 파트를 사용한다. @rollup\u002Frollup-darwin-arm64는 애플실리콘에서만 설치되는 패키지이다. html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}",{"id":8463,"title":7679,"titles":17705,"content":17706,"level":90},[],"Nuxt 프로젝트에서 Pinia를 설정하고 사용하는 방법을 안내. Pinia 설치, Nuxt 설정 파일에 추가하는 방법, 스토어 생성을 포함하여 컴포넌트에서의 사용 예시. 서버 사이드 렌더링(SSR) 설정 및 일반적인 오류 해결 방법. Nuxt 프로젝트에서 Pinia를 사용하는 방법은 Vue3와 다르다.",{"id":17708,"title":7688,"titles":17709,"content":17710,"level":101},"\u002Fblog\u002Fnuxt-pinia-get-active-pinia#pinia-설치",[7679],"npm install @pinia\u002Fnuxt\nyarn add @pinia\u002Fnuxt",{"id":17712,"title":7719,"titles":17713,"content":17714,"level":101},"\u002Fblog\u002Fnuxt-pinia-get-active-pinia#nuxt-설정-파일에-pinia-추가",[7679],"export default defineNuxtConfig({\n  modules: ['@pinia\u002Fnuxt'],\n}) nuxt.config.ts 파일에 모듈을 추가해야 한다.",{"id":17716,"title":7760,"titles":17717,"content":17718,"level":101},"\u002Fblog\u002Fnuxt-pinia-get-active-pinia#스토어-생성",[7679],"stores 폴더를 만들고 그 안에 스토어 파일을 작성한다. Nuxt에서 stores 폴더를 자동으로 인식해서 따로 import할 필요 없다. 예시: stores\u002Fcounter.ts import { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useCounterStore = defineStore('counter', () => {\n  const count = ref(0)\n  const doubleCount = computed(() => count.value * 2)\n\n  function increment() {\n    count.value++\n  }\n\n  return { count, doubleCount, increment }\n})",{"id":17720,"title":7924,"titles":17721,"content":17722,"level":101},"\u002Fblog\u002Fnuxt-pinia-get-active-pinia#컴포넌트에서-사용하기",[7679],"컴포넌트에서 useCounterStore를 불러와서 사용하면 된다. \u003Ctemplate>\n  \u003Cdiv>\n    \u003Cp>Count: {{ counter.count }}\u003C\u002Fp>\n    \u003Cbutton @click=\"counter.increment\">증가\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst counter = useCounterStore()\n\u003C\u002Fscript>",{"id":17724,"title":8050,"titles":17725,"content":17726,"level":101},"\u002Fblog\u002Fnuxt-pinia-get-active-pinia#선택-서버-사이드-렌더링ssr-설정",[7679],"Pinia는 SSR도 지원한다. 특별한 설정 없이도 기본적으로 SSR에서 잘 작동하지만, 커스텀이 필요하면 nuxt.config.ts에 아래와 같이 옵션을 추가할 수 있다. export default defineNuxtConfig({\n  modules: ['@pinia\u002Fnuxt'],\n  pinia: {\n    autoImports: ['defineStore', 'acceptHMRUpdate'],\n  },\n}) 이렇게 하면 Pinia를 Nuxt 프로젝트에서 손쉽게 사용할 수 있다.",{"id":17728,"title":8114,"titles":17729,"content":17730,"level":90},"\u002Fblog\u002Fnuxt-pinia-get-active-pinia#getactivepinia-was-called-but-there-was-no-active-pinia-are-you-trying-to-use-a-store-before-calling-appusepinia",[],"[nuxt] error caught during app initialization Error: 🍍: \"getActivePinia()\" was called but there was no active Pinia. Are you trying to use a store before calling \"app.use(pinia)\"?See https:\u002F\u002Fpinia.vuejs.org\u002Fcore-concepts\u002Foutside-component-usage.html for help.This will fail in production.\nat useStore (pinia.js?v=859d6957:1322:13) 다 설정하고 보니 위 에러가 발생했다. 모듈 누락 확인@pinia\u002Fnuxt 추가했는지 확인비동기 로직 이후 스토어 접근 ㄴㄴ",{"id":17732,"title":8155,"titles":17733,"content":17734,"level":117},"\u002Fblog\u002Fnuxt-pinia-get-active-pinia#캐시-및-라이브러리-다시-설치",[8114],"rm -rf node_modules\u002F.cache\nrm -rf .nuxt\nrm -rf node_modules\nnpm install  # 또는 yarn install\nnpm run dev 잔류 캐시나 이전버전 라이브러리 등의 충돌이 있을 수 있으니! !hint\nWARN  Module pinia is disabled due to incompatibility issues:                      오후 9:18:14nuxt Nuxt version ^2.0.0 || >=3.13.0 is required but currently using 3.12.1 버전도 확인해보자. 호환이 안되는 라이브러리일 수 있다. 나의 경우 nuxt 는 3.12.1 버전인데, pinia 2.3.x 버전을 사용하려면 3.13.0 이상의 버전을 요한다고 경고한다. 라이브러리현재 버전권장 버전Nuxt3.12.13.13.0 이상Pinia^2.3.02.3.x (OK)@pinia\u002Fnuxt^0.9.00.9.x (OK) npm install nuxt@latest nuxt 버전을 최신버전으로 업데이트 한다. 그럼 아래 pinia 를 수동등록하지 않아도 위 방법대로 잘 동작한다.",{"id":17736,"title":8304,"titles":17737,"content":17738,"level":117},"\u002Fblog\u002Fnuxt-pinia-get-active-pinia#pinia-사용을-수동으로-등록",[8114],"\u002F\u002F plugins\u002Fpinia.ts\nimport { defineNuxtPlugin } from '#app'\nimport { createPinia } from 'pinia'\n\nexport default defineNuxtPlugin((nuxtApp) => {\n  const pinia = createPinia()\n  nuxtApp.vueApp.use(pinia)\n}) 결국 이 방법으로 해결했다. nuxt 를 \"dependencies\": {\n    \"@pinia\u002Fnuxt\": \"^0.9.0\",\n    \"nuxt\": \"^3.12.1\",\n    \"pinia\": \"^2.3.0\",\n}, 이 버전들의 조합으로는 수동으로 등록하는 것이 답이었다 html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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}",{"id":8597,"title":8473,"titles":17740,"content":17741,"level":90},[],"FCM 알림을 테스트하는 간단한 방법. Postman 을 비롯한 curl 기반 HTTP 요청 프로그램으로 테스트하기. POST https:\u002F\u002Ffcm.googleapis.com\u002Ffcm\u002Fsend 위 경로로 메세지를 보낸다. 헤더 데이터 Authorization : key=AAAXXXXXXContent-Type : application\u002Fjson Authorization 은 key=서버키 형태로 넣는다. {\n    \"to\":\"FCMToken\",\n    \"data\" : {\n      \"title\":\"제목\",\n      \"body\":\"내용\",\n      \"otherKey\":\"asdf\",\n    }\n} body 는 정해진 데이터대로 넣는다. html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}",{"id":9307,"title":8607,"titles":17743,"content":17744,"level":90},[],"Tailwind CSS의 테마 커스터마이징 방법을 소개하며, 사용자 정의 색상 변수를 설정하여 디자인을 개선하는 기술을 다룬다. 다양한 테마 설정과 CSS 변수 사용 사례를 통해 원하는 스타일을 구현하는 방법을 배울 수 있다. tailwindcss 에서 미리 기 정의된 color 프리셋을 제공한다. font, background 등등. 다만 기존에 사용하던 색상들을 tailwindcss 체계에 녹이고 싶다. text-blackcolor: var(--color-black); \u002F* #000 *\u002Ftext-whitecolor: var(--color-white); \u002F* #fff *\u002Ftext-red-50color: var(--color-red-50); \u002F* oklch(0.971 0.013 17.38) *\u002F 비슷한 형태로, 우리의 color 프리셋을 사용해보자.",{"id":17746,"title":8661,"titles":17747,"content":17748,"level":101},"\u002Fblog\u002Ftailwindcss-color-preset-with-theme#font-color-preset-custom",[8607],"\u003Cp class=\"text-[#50d71e] ...\">\u003C\u002Fp>\n\u003Cp class=\"text-(--my-color) ...\">\u003C\u002Fp> 이런식으로 color 코드를 직접 넣거나, variables 를 지정하면 커스텀 컬러를 사용할 수 있다. 하지만 나는 text-error-90 형식으로 사용하고 싶다.",{"id":17750,"title":8721,"titles":17751,"content":17752,"level":101},"\u002Fblog\u002Ftailwindcss-color-preset-with-theme#customizing-theme",[8607],"테마를 수정하는 방식을 제공한다. @theme {\n  --color-base-10: #FFFFFF;\n  --color-base-20: #F8F8F9;\n  --color-base-30: #ECEDF0;\n  ...\n} \u003Cp class=\"text-base-30\">\u003C\u002Fp> text-base-30 bg-base-30 이런식으로 색상을 사용할 수 있게 된다. tailwindcss 가 변수를 읽어 자동으로 utility class 를 생성해준다. 오오 다음은 테마를 적용해보자. @custom-variant dark (&:where(.dark, .dark *));\n@theme {\n  --color-custom-10: #FFFFFF;\n  --color-custom-20: #F8F8F9;\n  --color-custom-30: #ECEDF0;\n    ...\n}\n@variant dark {\n  --color-custom-10: #0D152A;\n  --color-custom-20: #303445;\n  --color-custom-30: #555C6C;\n    ...\n} 이렇게 @variant dark 로 컬러를 선언해주면, @custom-variant dark 에 의해서, .dark 클래스 아래의 요소들은 variables 가 갈아끼워진다. \u003Chtml class=\"dark\">\u003C\u002Fhtml> 위 형태로 html 태그의 클래스에 dark 가 포함되면, @variant dark 의 변수들이 사용되는 구조이다. \u003Cdiv class=\"text-custom-70 dark:text-custom-50\">test color tailwindcss\u003C\u002Fdiv>\n\u003Cdiv class=\"text-custom-70\">test color tailwindcss\u003C\u002Fdiv> tailwindcss 에서는 dark mode 일 때는 dark:bg-base-10 이런식으로 특정 조건일 때 클래스를 적용할 수 있도록 기능을 제공한다.\ndark:* 로 dark 모드일 때 데이터를 지정하여, dark 모드가 되었을 때 70 이 아닌 50을 사용하겠다는 뜻이 된다. 다만, dark 모드로 갈아끼워진 variable 의 50을 사용하게 되는 것을 주의해야 한다. \u002F**\n * # 테마 컬러를 변경한다.\n *\u002F\nfunction changeTheme(theme: ColorTheme) {\n    document.documentElement.classList.remove(...[\"dark-mode\", ...]);\n    if (theme === ColorTheme.DARK) {\n        document.documentElement.classList.add(\"dark-mode\");\n    } else if (theme === ColorTheme.SOMETHEME) { ... }\n    localStorage.setItem(STORAGE_KEY, theme);\n    colorTheme.value = theme;\n} 테마 변경을 body 의 클래스로 활용했었다. html 태그의 클래스를 바꿔주기 위해서 대상을 document.documentElement 로 변경해주었다. @variant 는 html 태그의 클래스를 인식한다.",{"id":17754,"title":9198,"titles":17755,"content":17756,"level":101},"\u002Fblog\u002Ftailwindcss-color-preset-with-theme#vue3-에서-css-variables-값-가져오기",[8607],"const root = document.querySelector(':root');\nif(root) {\n    return {\n        100: getComputedStyle(root).getPropertyValue(`--color-${p}-100`),\n        ...\n    };\n} @variant 로 지정한 테마의 색상 변수는 :root 요소를 찾아야 한다. 계산된 style 을 찾아서 변수의 값을 가져오자. html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}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}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"id":9470,"title":9315,"titles":17758,"content":9467,"level":90},[],{"id":17760,"title":9321,"titles":17761,"content":17762,"level":101},"\u002Fblog\u002Ftailwindcss-storybook-version-error#error-enoent-no-such-file-or-directory-stat-cusersprojectpathiframehtml",[9315],"어느날 갑자기 CICD가 안되어서 내용을 보니 storybook 빌드를 하면서 에러가 발생했었다. 분명 로컬에서는 되는데 원격 환경에서만 에러가 발생하는 것이었다. 사용 중인 버전정보는 다음과 같다, tailwindcss@4.0.8storybook@8.6.0vite@6.1.0vue3@3.5.13 {projectname} build-storybook\nstorybook build\n\n@storybook\u002Fcore v8.6.0\n\ninfo => Cleaning outputDir: storybook-static\ninfo => Loading presets\nWARN The \"@storybook\u002Faddon-mdx-gfm\" addon is meant as a migration assistant for Storybook 8.0; and will likely be removed in a future version.\nWARN It's recommended you read this document:\nWARN https:\u002F\u002Fstorybook.js.org\u002Fdocs\u002Fwriting-docs\u002Fmdx#markdown-tables-arent-rendering-correctly\nWARN\nWARN Once you've made the necessary changes, you can remove the addon from your package.json and storybook config.\ninfo => Building manager..\ninfo => Manager built (236 ms)\ninfo => Building preview..\nmode production\nnpm_package_version 2.12.3\nplugin 'rollup-plugin-html-env' uses deprecated 'transform' option. Use 'handler' option instead.\nvite v6.1.0 building for production...\ntransforming (1) virtual:@storybook\\builder-vite\\vite-app.jsC:\\Users\\PROJECTPATH\\node_modules\\storybook\\bin\\index.cjs:23\nthrow error;\n^\n\nError: ENOENT: no such file or directory, stat 'C:\\Users\\PROJECTPATH\\iframe.html'\nat async Object.stat (node:internal\u002Ffs\u002Fpromises:1036:18)\nat async C.addBuildDependency (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002F@tailwindcss\u002Fvite\u002Fdist\u002Findex.mjs:1:5226) {\nerrno: -4058,\ncode: 'ENOENT',\nsyscall: 'stat',\npath: 'C:\\Users\\PROJECTPATH\\iframe.html'\n} iframe.html 파일을 찾지 못해서 발생한 에러이다. storybook 과 tailwindcss 를 같이 사용할 때, 미리보기 파일이 생성이 안되면서 발생하는 것으로 보인다. 헤당 이슈 혹은 비슷한 이슈가 이미 오픈되어 있었다. 이 문제를 발견한 시점으로부터 바로 하루 전이었다. !success\n여기서 insider 라는 용어를 처음 알았다. 공식적으로 발표되지 않았지만, 사용해볼 수 있는 버전을 뜻하는듯 하다. 내용으로 봐서는 4.0.8 버전에 일부 문제가 있었던 것 같고, 4.0.9 버전에서 픽스가 되어서 바로 사용해볼 수 있었다. \"tailwindcss\": \"^4.0.9\" 으로 세팅해서 lock 파일과 node_modules 폴더를 모두 지우고 다시 설치해서 이 문제는 해결되었다.",{"id":17764,"title":9389,"titles":17765,"content":17766,"level":101},"\u002Fblog\u002Ftailwindcss-storybook-version-error#cannot-be-closed-a",[9315],"✗ Build failed in 8.40s\n=> Failed to build the preview\n[vite:build-html] Cannot be closed: a\n    at ParseHTML.onCloseTagEndEvent (.\\node_modules\\vite-plugin-html-env\\lib\\parse\\index.js:313:15)\n    at ParseHTML.parse (.\\node_modules\\vite-plugin-html-env\\lib\\parse\\index.js:189:16)\n    at transform (.\\node_modules\\vite-plugin-html-env\\lib\\index.js:148:19)\n    at applyHtmlTransforms (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Fvite\u002Fdist\u002Fnode\u002Fchunks\u002Fdep-ByPKlqZ5.js:42504:23)\n    at async Object.generateBundle (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Fvite\u002Fdist\u002Fnode\u002Fchunks\u002Fdep-ByPKlqZ5.js:42244:18)\n    at async Bundle.generate (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Frollup\u002Fdist\u002Fes\u002Fshared\u002Fnode-entry.js:20116:9)\n    at async file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Frollup\u002Fdist\u002Fes\u002Fshared\u002Fnode-entry.js:22805:27\n    at async catchUnfinishedHookActions (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Frollup\u002Fdist\u002Fes\u002Fshared\u002Fnode-entry.js:22187:16)\n    at async buildEnvironment (file:\u002F\u002F\u002FC:\u002FUsers\u002FPROJECTPATH\u002Fnode_modules\u002Fvite\u002Fdist\u002Fnode\u002Fchunks\u002Fdep-ByPKlqZ5.js:51450:16)\n    at async build (.\\node_modules\\@storybook\\builder-vite\\dist\\index.js:80:230) 다시 최신 커밋으로 돌아가 tailwindcss 버전만 4.0.9 로 올린다고 성공하진 않았다. storybook 버전은 그대로 사용해야 충돌이 안나는듯 했다. 이 에러는 storybook 의 에러로 보인다. story 파일도 모두 지우고 테스트해봐도 같은 결과인 걸 보면, 작성한 stories 에서 문제가 있던 건 아닌 거로 보인다. 문제가 된 커밋을 찾아보니, 이번엔 storybook 의 버전 문제였다. 원래 되던 것이 안되면 무조건 버전 문제다. 8.4.7 -> 8.6.0 으로 올렸던 것이 문제가 되었었다. 이 커밋을 revert 하고 시도하니 해결되었다. 무작정 신버전이 능사는아니다.",{"id":17768,"title":9413,"titles":17769,"content":17770,"level":101},"\u002Fblog\u002Ftailwindcss-storybook-version-error#tailwindcssoxide-linux-x64-gnu",[9315],"failed to load config from \u002Fhome\u002Frunner\u002Fwork\u002FUpbox-2.0-Front-Application\u002FUpbox-2.0-Front-Application\u002Fvite.config.ts\n=> Failed to build the preview\nError: Cannot find module '@tailwindcss\u002Foxide-linux-x64-gnu'\nRequire stack:\n- .\u002Fnode_modules\u002F@tailwindcss\u002Foxide\u002Findex.js\n    at Function.\u003Canonymous> (node:internal\u002Fmodules\u002Fcjs\u002Floader:1225:15)\n    at Module._resolveFilename (.\u002Fnode_modules\u002Fesbuild-register\u002Fdist\u002Fnode.js:4794:36)\n    at Function._load (node:internal\u002Fmodules\u002Fcjs\u002Floader:1055:27)\n    at TracingChannel.traceSync (node:diagnostics_channel:322:14)\n    at wrapModuleLoad (node:internal\u002Fmodules\u002Fcjs\u002Floader:220:24)\n    at Module.require (node:internal\u002Fmodules\u002Fcjs\u002Floader:1311:12)\n    at require (node:internal\u002Fmodules\u002Fhelpers:136:16)\n    at Object.\u003Canonymous> (.\u002Fnode_modules\u002F@tailwindcss\u002Foxide\u002Findex.js:190:31)\n    at Module._compile (node:internal\u002Fmodules\u002Fcjs\u002Floader:1554:14)\n    at node:internal\u002Fmodules\u002Fcjs\u002Floader:1706:10 github actions 에서 빌드를 하려니 또 이런 에러가 발생한다. ^4.0.6 버전일 때는 에러가 없었는데, ^4.0.9 로 올리면서 에러가 발생한듯 하다. 일단 optionalDependencies 에 추가해주었다. \"optionalDependencies\": {\n    \"@tailwindcss\u002Foxide-linux-x64-gnu\": \"^4.0.9\"\n} 버전은 tailwindcss 버전과 맞추면 될듯 하다. html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}",{"id":12320,"title":9481,"titles":17772,"content":17773,"level":90},[],"기존 vue-cli 를 사용한 vue 프로젝트로 github pages 를 구축했는데, 대세에 따라 vite 로 변경하기로 했다. 기존 vue-cli 를 사용한 vue 프로젝트로 github pages 를 구축했는데, 대세에 따라 vite 로 변경하기로 했다. 말이 변경이지 새로 만들었다. 기존 vue-cli 로 했던 것에서 vite 로 변경하며 필요한 부분만 작성하였고, github actions 등의 기능은 Github.io + Vue3 로 블로그 만들기 를 참고하면 된다.",{"id":17775,"title":9498,"titles":17776,"content":17777,"level":101},"\u002Fblog\u002Fvite-vue-and-github-pages#vite-프로젝트-생성",[9481],"npm create vite@latest 우선 빈 vite 프로젝트를 만들어준다. 우리는 vue3 와 typescript 를 활용해서 개발할 것이다.",{"id":17779,"title":9510,"titles":17780,"content":17781,"level":101},"\u002Fblog\u002Fvite-vue-and-github-pages#vite-ssg",[9481],"Vite SSGhttps:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvite-ssg vite 로 static site generating 하는 라이브러리이다. 우리가 만드는 포스트 마다 정적 사이트로 만들어준다 npm i -D vite-ssg vue-router @vueuse\u002Fhead 위 커맨드로 vue-router  와 @vueuse\u002Fhead  도 함께 설치해주라고 나와 있는데, 나는 @unhead\u002Fvue 를 사용했다. \u002F\u002F package.json\n\"scripts\": {\n  \"build:ssg\": \"vite-ssg build\"\n} 빌드 커맨드를 대체 혹은 추가해준다. export const createApp = ViteSSG(\n    \u002F\u002F 루트 컴포넌트\n    App,\n    { routes },\n    ({ app, router, routes, isClient, initialState }) => {\n        \u002F\u002F 플러그인 세팅\n    },\n) createApp  을 ViteSSG  로 변경한다. 이러면 빌드했을 때, SPA 앱이 아니라, MPA로 빌드가 된다. 각 라우트마다 html 페이지가 따로 생성된다.",{"id":17783,"title":9678,"titles":17784,"content":17785,"level":101},"\u002Fblog\u002Fvite-vue-and-github-pages#vite-plugin-pages-sitemap",[9481],"vite-plugin-pages-sitemaphttps:\u002F\u002Fgithub.com\u002Fjbaubree\u002Fvite-plugin-pages-sitemap 사이트맵 자동 생성기이다. vite-ssg  와 vite-plugin-pages  를 같이 지원한다. npm install -D vite-plugin-pages-sitemap import Pages from 'vite-plugin-pages'\nimport generateSitemap from 'vite-plugin-pages-sitemap'\nexport default {\n  plugins: [\n    \u002F\u002F ...\n    Pages({\n      onRoutesGenerated: routes => (generateSitemap({ routes })),\n    }),\n  ],\n} npm run build  를 하면 바로 사이트맵과 로보츠 파일이 만들어진다. 기본 사용법대로 사이트맵 파일을 만들면 주소가 localhost  로 되는데, hostname  을 변경해서 내 사이트 주소를 설정해주어야 한다.",{"id":17787,"title":9697,"titles":17788,"content":17789,"level":101},"\u002Fblog\u002Fvite-vue-and-github-pages#vite-plugin-pages",[9481],"vite-plugin-pageshttps:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvite-plugin-pages 파일 시스템 기반의 라우트를 생성한다. npm install -D vite-plugin-pages 이녀석도 vue-router 가 필요한데, 위에서 설치했기 때문에 생략한다. \u002F\u002F vite.config.ts\nimport Pages from 'vite-plugin-pages';\nexport default defineConfig({\n  plugins: [\n    Pages({\n      extensions: ['vue', 'md'],\n      pagesDir: 'src\u002Fpages',\n      \u002F**\n       * 생성된 라우트에 추가 로직을 작성한다.\n       * @param route 생성된 라우트\n       * @returns \n       *\u002F\n      extendRoute(route) {\n        console.log('route', route);\n        return route;\n      }\n    }),\n  ],\n}) Pages  플러그인을 설정파일에 추가하자. md 파일이 있는 경로와, 확장자를 추가해주면 된다.",{"id":17791,"title":9997,"titles":17792,"content":17793,"level":117},"\u002Fblog\u002Fvite-vue-and-github-pages#파일-추가하기",[9481,9697],"Pages  플러그인에 등록해둔 경로에 .vue  혹은 .md  파일을 만들면 자동으로 라우트로 인식한다. 기본적으로 만들어야 할 파일이 두가지 있다. [...all].vue → 접속한 페이지에 해당하는 라우트가 없을 때 띄워줄 404 페이지index.vue → 루트 페이지 나머지는 파일을 만드는대로 라우트에 등록된다.",{"id":17795,"title":10031,"titles":17796,"content":17797,"level":117},"\u002Fblog\u002Fvite-vue-and-github-pages#nested-routes",[9481,9697],"Nested Routeshttps:\u002F\u002Fgithub.com\u002Fhannoeru\u002Fvite-plugin-pages#nested-routes 메인 페이지와 포스트 페이지의 디자인을 완전히 분리하고 nested routes 로 관리할 것이다. route {\n  path: '\u002Fposts',\n  component: '\u002Fsrc\u002Fpages\u002Fposts.vue',\n  customBlock: undefined,\n  children: [\n    {\n      name: 'posts-post2',\n      path: 'post2',\n      component: '\u002Fsrc\u002Fpages\u002Fposts\u002Fpost2.md',\n      customBlock: undefined,\n      props: true\n    },\n    \u002F\u002F ....\n  ],\n  props: true\n} \u002Fpages 폴더 아레, posts  로 폴더와 vue 파일을 하나씩 만든다. 그럼 자동으로 nested routes 로 인식한다. \u003Cscript setup>\u003C\u002Fscript>\n\u003Ctemplate>\n  \u003Cdiv class=\"post-index-wrapper\">\n    \u003Crouter-view>\u003C\u002Frouter-view>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle>\n\u003C\u002Fstyle> posts.vue  파일에는 router-view  를 사용하면 끝!",{"id":17799,"title":10291,"titles":17800,"content":17801,"level":101},"\u002Fblog\u002Fvite-vue-and-github-pages#vite-plugin-vue-layouts",[9481],"vite-plugin-vue-layoutshttps:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvite-plugin-vue-layouts vite-plugin-pages 와 세트이다. main 에서 createApp 을 할 때, 라우트를 인식시키기 위함이다. 무조건 필요한지는 잘 모르겠다. 더 알아봐야 함. npm install -D vite-plugin-vue-layouts \u002F\u002F vite.config.ts\nimport Layouts from 'vite-plugin-vue-layouts';\nexport default defineConfig({\n  plugins: [\n    Layouts(),\n  ]\n}) vite 설정에 Layouts  추가하기 \u002F\u002F tsconfig.json\n\"compileOptions\": {\n    \"types\": [\n      \"vite\u002Fclient\", \n      \"vite-plugin-pages\u002Fclient\", \n      \"vite-plugin-vue-layouts\u002Fclient\", \n      \"node\"\n    ],\n} virtual  import 를 인식하기 위해서 types 항목을 추가한다. \u002F\u002F main.ts\n\u002F\u002F virtual:generated-layouts\n\u002F\u002F   tsconfig.json-compilerOptions-types\n\u002F\u002F   \"vite-plugin-vue-layouts\u002Fclient\"\nimport { setupLayouts } from 'virtual:generated-layouts'\n\u002F\u002F virtual:generated-pages\n\u002F\u002F   tsconfig.json-compilerOptions-types\n\u002F\u002F   \"vite-plugin-pages\u002Fclient\"\nimport generatedRoutes from 'virtual:generated-pages'\n\nconst routes = setupLayouts(generatedRoutes)\nexport const createApp = ViteSSG(\n    \u002F\u002F 루트 컴포넌트\n    App,\n    { routes },\n    ({ app, router, routes, isClient, initialState }) => {\n        \u002F\u002F 플러그인 세팅\n    },\n) pages 플러그인이 셍성한 라우트를 main 에서 적용시킨다.",{"id":17803,"title":10590,"titles":17804,"content":17805,"level":101},"\u002Fblog\u002Fvite-vue-and-github-pages#vite-plugin-md",[9481],"vite-plugin-mdhttps:\u002F\u002Fgithub.com\u002Fantfu\u002Fvite-plugin-md 마크다운을 뷰 컴포넌트로 만들어준다. npm i vite-plugin-md -D import Markdown from 'vite-plugin-md'\n\nexport default defineConfig({\n  plugins: [\n    vue({\n      include: [\u002F\\.vue$\u002F, \u002F\\.md$\u002F], \u002F\u002F 마크다운 파일도 인식하기\n    }),\n    Markdown(),\n  ],\n}) 기본적으로 있던 vue  플러그인에 md  파일도 인식할 수 있게 항목을 추가해주고, Markdown  플로그인을 추가한다.",{"id":17807,"title":10724,"titles":17808,"content":17809,"level":117},"\u002Fblog\u002Fvite-vue-and-github-pages#메타태그-추가",[9481,10590],"GitHub - mdit-vue\u002Fvite-plugin-vue-markdown: Compile Markdown to Vue component 페이지별로 메타태그를 추가하고싶은데, .md 파일로는 어떻게 해줘야 할까?\nvite-ssg 와 vite-plugin-md 에서 이미 vueuse\u002Fhead 를 사용한다. 그래서 unhead\u002Fvue 를 사용해보려고 했지만, 충돌 문제가 있어서 vueuse\u002Fhead 의 방식을 따르기로 했다. ---\nmeta:\n  - name: My Cool App\n    description: cool things happen to people who use cool apps\n--- md 파일의 최상단에 --- 로 구분선을 위아래로 넣어주고, 안에 내용을 넣어준다. ---\nmeta:\n  - name: description\n    content: Github.io 와 Vue3 를 활용해서 블로그를 시작. SPA를 prerendering 하여 개별 페이지를 생성하고, 메타태그를 추가. 구글 검색 엔진에 등록할 사이트맵 자동 생성. 마크다운을 html 로 변환. Google Analytics 도입으로 방문자 수 체크.\n--- 이런 형식도 된다. \u003Ctitle>Github.io + Vue3 로 블로그 만들기\u003C\u002Ftitle>\n\u003Cmeta name=\"description\"\ncontent=\"Github.io 와 Vue3 를 활용해서 블로그를 시작. SPA를 prerendering 하여 개별 페이지를 생성하고, 메타태그를 추가. 구글 검색 엔진에 등록할 사이트맵 자동 생성. 마크다운을 html 로 변환. Google Analytics 도입으로 방문자 수 체크.\">\n\u003Cmeta property=\"og:title\" content=\"Github.io + Vue3 로 블로그 만들기\"> html 파일에 자동으로 meta 태그가 추가된다.",{"id":17811,"title":10921,"titles":17812,"content":83,"level":101},"\u002Fblog\u002Fvite-vue-and-github-pages#markdown-it-플러그인",[9481],{"id":17814,"title":10924,"titles":17815,"content":17816,"level":117},"\u002Fblog\u002Fvite-vue-and-github-pages#markdown-it-anchor",[9481,10921],"markdown-it-anchorhttps:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmarkdown-it-anchor H  태그에 기본적으로 anchor 를 달아서 이동할 수 있게 한다.  한글도 지원한다.",{"id":17818,"title":10943,"titles":17819,"content":17820,"level":117},"\u002Fblog\u002Fvite-vue-and-github-pages#markdown-it-table-of-contents",[9481,10921],"markdown-it-table-of-contentshttps:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmarkdown-it-table-of-contents [[toc]]  를 사용하면 목차를 만들 수 있다.",{"id":17822,"title":10963,"titles":17823,"content":17824,"level":130},"\u002Fblog\u002Fvite-vue-and-github-pages#목차-요소를-따로-떼서-다른-곳에-붙이기",[9481,10921,10943],"목차가 md 페이지 컴포넌트의 상단에 고정되어 있어서 원하는 곳에서 사용할 수 없다. 예를 들어, 오른쪽 부분에 고정을 시킨다던지. onMounted(() => {\n  \u002F\u002F 목차를 markdown-body 내부에서 제거한 후, \n  \u002F\u002F 옆에 붙인다.\n  const mdBody = document.querySelector(\".markdown-body\");\n  const targetToc = document.querySelector(\".new-table-of-contents\");\n  const toc = document.querySelector(\".markdown-body .table-of-contents\");\n  const main = document.querySelector(\"main\");\n\n  if(mdBody && targetToc && toc && main) {\n    targetToc.innerHTML = toc?.innerHTML!;\n    \u002F\u002F 라우트 변경 시 `.table-of-contents` 의 innerHTML 이 한번 제거되고 undefined 로 뜨기 때문에\n    \u002F\u002F display: none 만 붙여서 숨긴다.\n    (toc as HTMLElement)?.style.setProperty(\"display\", \"none\");\n  }\n}) 자바스크립트로 간단하게 innerHTML  을 붙이려는 요소에 넣어주고, 해당 요소는 제거하면 된댜. !attention\n원래는  toc 를 remove 로 제거했으나, 그러면 라우트를 두번 이상 이동할 때, 이미 제거된 .table-of-contents 에서 내용을 찾아 undefined 를 얻게 된다. 라우트 시 toc 요소가 없으면 생성하지 못하는듯 하다.\n그래서 display: none; 속성만 주어서 보이지만 않게 한다. watch(() => route.fullPath, () => {\n    \u002F\u002F SPA 라우트가 바뀔 때마다 toc 변경\n    nextTick(() => {\n        createToc();\n    });\n}); 라우트가 변경될 때마다 TOC를 옮겨주기 위해서 watch 를 추가한다.",{"id":17826,"title":11249,"titles":17827,"content":17828,"level":117},"\u002Fblog\u002Fvite-vue-and-github-pages#markdown-it-prism",[9481,10921],"markdown-it-prismhttps:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmarkdown-it-prism prism  라이브러를 래핑했다. 마크다운 파일 내의 소스코드를 위한 pre  태그를 하이라이팅한다. export default defineConfig({\n  plugins: [\n    Markdown({\n      markdownItSetup(md) {\n        \u002F\u002F prism 코드 하이라이터\n        md.use(MDPrism)\n      },\n    }),\n  ],\n}) markdownItSetup 함수에서 use 로 사용선언하면 자동으로 마크다운 코드 영역은 하이라이팅 된다. 클래스가 부여되기만 해서 css 는 직접 넣어주어야 한다. \u003Cstyle>\n@use \"..\u002Fassets\u002Fscss\u002Fprism-vscode.scss\"; \n\u003C\u002Fstyle> 우리의 경우 포스트가 RouterView 에서 자동으로 페이지화되어 보여지기 때문에, scoped style 로는 스타일을 적용할 수 없다. 그래서 전역 css 로 선언해주어야 한다.",{"id":17830,"title":11390,"titles":17831,"content":17832,"level":130},"\u002Fblog\u002Fvite-vue-and-github-pages#테마",[9481,10921,11249],"GitHub - PrismJS\u002Fprism-themes: A wider selection of Prism themes\n여러가지 테마를 모아둔 깃헙이다. 가장 익숙한 vscode 를 사용했다.",{"id":17834,"title":11401,"titles":17835,"content":17836,"level":101},"\u002Fblog\u002Fvite-vue-and-github-pages#pinia",[9481],"piniahttps:\u002F\u002Fpinia.vuejs.org\u002Fgetting-started.html#installation\nvite-ssg piniahttps:\u002F\u002Fgithub.com\u002Fantfu\u002Fvite-ssg#initial-state 포스트 목록을 관리하기 위해서 pinia 를 설치하자. npm install pinia \u002F\u002F main.ts\nexport const createApp = ViteSSG(\n  App,\n  { routes },\n  ({ app, router, initialState }) => {\n    const pinia = createPinia()\n    app.use(pinia)\n\n    if (import.meta.env.SSR)\n      initialState.pinia = pinia.state.value\n    else\n      pinia.state.value = initialState.pinia || {}\n\n    router.beforeEach((to, from, next) => {\n      const store = useRootStore(pinia)\n      if (!store.ready)\n        \u002F\u002F perform the (user-implemented) store action to fill the store's state\n        store.initialize()\n      next()\n    })\n  },\n) vite-ssg 깃헙에서 나온 설명서인데, 글로벌로 store 를 등록하는 게 아니라면, app.use  까지만 해주어도 된다. \u002F\u002F src\u002Fstore\u002Fposts.ts\nimport axios from 'axios';\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue';\n\nexport interface Post {\n  url: string;\n  fileName: string;\n  title: string;\n  description: string;\n  tags: string[];\n  data: string;\n}\n\nexport const usePosts = defineStore('post', () => {\n  const postList = ref\u003CPost[]>([]);\n  const currentUrl = ref\u003Cstring>('');\n\n  async function fetchPostList(\n) {\n    axios.get\u003CPost[]>(`\u002Fpostlist.json`).then(res => {\n      postList.value = res.data;\n      return res.data;\n    });\n  }\n\n  return {\n    postList,\n    currentUrl,\n    fetchPostList,\n  }\n}) post 목록을 가져오는 store 를 하나 만든다. 포스트를 단순히 json 파일로 저장해두고, 사용할 것이다. \u002F\u002F public\u002Fpostlist.json\n[\n  {\n      \"url\": \"Vue-Prefetch\",\n      \"fileName\": \"Vue-Prefetch\",\n      \"title\": \"Vue Code Spliting 간단히 알아보기 (+ Prefetch)\",\n      \"description\": \"Vue 프로젝트의 Code Spliting 이 어떻게 작동하는지와 webpack prefetch 옵션 에 대해서 간략한 설명.\",\n      \"createdAt\": \"2022-08-05\",\n      \"updatedAt\": \"2022-08-05\",\n      \"tags\": [\"Vue\", \"Vue2\", \"Vue3\", \"Code Spliting\", \"코드분산\", \"Prefecth\", \"Webpack\"]\n  },\n  \u002F\u002F ...\n] public 폴더에 파일을 두면, 그 항목은 사이트의 루트에 포함되게 된다. 같은 호스트이기 때문에 cors 없이 axios 로 동적으로 조회할 수 있다.",{"id":17838,"title":12101,"titles":17839,"content":83,"level":101},"\u002Fblog\u002Fvite-vue-and-github-pages#문제",[9481],{"id":17841,"title":12105,"titles":17842,"content":17843,"level":117},"\u002Fblog\u002Fvite-vue-and-github-pages#vite-ssg-gsap",[9481,12101],"[vite-ssg] An internal error occurred.\n[vite-ssg] Please report an issue, if none already exists: https:\u002F\u002Fgithub.com\u002Fantfu\u002Fvite-ssg\u002Fissues\nfile:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002F.vite-ssg-temp\u002Fmain.mjs:6\nimport { ScrollTrigger } from \"gsap\u002FScrollTrigger.js\";\n         ^^^^^^^^^^^^^\nSyntaxError: Named export 'ScrollTrigger' not found. The requested module 'gsap\u002FScrollTrigger.js' is a CommonJS module, which may not support all module.exports as named exports.\nCommonJS modules can always be imported via the default export, for example using:\n\nimport pkg from 'gsap\u002FScrollTrigger.js';\nconst { ScrollTrigger } = pkg;\n\n    at ModuleJob._instantiate (node:internal\u002Fmodules\u002Fesm\u002Fmodule_job:123:21)\n    at async ModuleJob.run (node:internal\u002Fmodules\u002Fesm\u002Fmodule_job:189:5)\n    at async Promise.all (index 0)\n    at async ESMLoader.import (node:internal\u002Fmodules\u002Fesm\u002Floader:530:24)\n    at async build (file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Fvite-ssg\u002Fdist\u002Fshared\u002Fvite-ssg.62550b28.mjs:996:87)\n    at async Object.handler (file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Fvite-ssg\u002Fdist\u002Fnode\u002Fcli.mjs:29:5)\n\nNode.js v18.12.1 javascript - GSAP ScrollTrigger with Next.js - Stack Overflow import MPP from 'gsap\u002FMotionPathPlugin';\nimport ScrollTrigger from 'gsap\u002FScrollTrigger';\n\nconst { MotionPathPlugin } = MPP;\ngsap.registerPlugin(ScrollTrigger);\ngsap.registerPlugin(MotionPathPlugin); 이런식으로 바꿔주었다. vite-ssg 로 사용할 때는 다른 빌드 방식을 사용하나보다. esbuild 라거나,, commonJS 라거나.. 그래서 \u002Fdist 폴더 안의 라이브러리를 사용해주었다.",{"id":17845,"title":12210,"titles":17846,"content":17847,"level":117},"\u002Fblog\u002Fvite-vue-and-github-pages#axios",[9481,12101],"vite-ssg 는 정적 html 사이트를 생성하기 때문에 setup 안에 있는 함수를 무조건 실행한다. 그래서 최초에 데이터를 가져오는 axios 함수가 실행되면서 없는 서버로 데이터를 가져오려고 해서 에러가 발생한다. file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Faxios\u002Flib\u002Fcore\u002FAxiosError.js:89\n  AxiosError.call(axiosError, error.message, code, config, request, response);\n             ^\nAxiosError: connect ECONNREFUSED ::1:80\n    at AxiosError.from (file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Faxios\u002Flib\u002Fcore\u002FAxiosError.js:89:14)\n    at RedirectableRequest.handleRequestError (file:\u002F\u002F\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Faxios\u002Flib\u002Fadapters\u002Fhttp.js:591:25)\n    at RedirectableRequest.emit (node:events:513:28)\n    at eventHandlers.\u003Ccomputed> (\u002FUsers\u002Fhslee\u002FWorkspaces\u002Fsomething\u002Flhs-source.github.io\u002Fnode_modules\u002Ffollow-redirects\u002Findex.js:14:24)\n    at ClientRequest.emit (node:events:513:28)\n    at Socket.socketErrorListener (node:_http_client:494:9)\n    at Socket.emit (node:events:513:28)\n    at emitErrorNT (node:internal\u002Fstreams\u002Fdestroy:151:8)\n    at emitErrorCloseNT (node:internal\u002Fstreams\u002Fdestroy:116:3)\n    at process.processTicksAndRejections (node:internal\u002Fprocess\u002Ftask_queues:82:21) { ::1:80 경로로 데이터를 가져오면서 에러가 발생한 것이다. const posts = usePosts();\n\nonMounted(() => {\n  animPostList();\n  posts.fetchPostList();\n}) onMounted 내부에서 fetch 를 실행하면, setup 훅과 다르게 빌드 시 실행하지 않는다. html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"id":12517,"title":12328,"titles":17849,"content":17850,"level":90},[],"setInterval 로 돌아가는 로직을 vitest 의 Fake Timer 를 사용해 테스트. fetch 함수가 포함된 interval 로직은 flushPromise 없이 다음 루프가 돌지 않음. vitest 공식 홈페이지 #fake-timers 타이머를 가짜로 돌려서, 원하는 시간만큼 컨트롤할 수 있다. 시간을 기다리지 않고, 임의로 시간을 흐르게 해서 빠른 테스트를 가능하게 한다. let i = 0\nsetInterval(() => console.log(++i), 50)\n\nvi.advanceTimersByTime(150)\n\n\u002F\u002F log: 1\n\u002F\u002F log: 2\n\u002F\u002F log: 3",{"id":17852,"title":12425,"titles":17853,"content":17854,"level":101},"\u002Fblog\u002Fvitest-timer-flushpromise#타이머-내부에-async-fetch-코드가-있다면-flushpromise-필수",[12328],"async function fetchFunction() {\n  await fetchDetail();\n    \u002F\u002F ...\n}\n\nfunction startTimer() {\n  refreshTimer = setInterval(fetchFunction, 5 * 1000);\n} 이런식으로 fetch 함수가 타이머 함수 내부에 실행되는 경우, flushPromise 없이는 다음 타이머가 동작하지 않는다. vi.advanceTimersByTime()은 시간만 진행시킬 뿐, 비동기 작업의 완료를 보장하지 않는다. 즉, 비동기 작업은 flushPromise 호출 전까지 대기한다. html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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);}",{"id":13046,"title":12529,"titles":17856,"content":13043,"level":90},[],{"id":17858,"title":12535,"titles":17859,"content":17860,"level":101},"\u002Fblog\u002Fvue-prefetch#code-spliting",[12529],"우리가 Vue 프로젝트에서 Code Spliting 을 사용하는 이유는 빌드 결과물의 데이터 양을 분산시키기 위해서다. 기본적으로 SPA 프론트엔드 프로젝트를 번들링할 때, 스크립트 파일은 하나로 뭉쳐진다. 프로젝트 크기에 따라서 이 파일의 크기는 선형으로 증가하며, 초기에 모든 기능을 로드하는 SPA 특성상 초기 렌더링 시간이 급격하게 늘어난다.",{"id":17862,"title":12546,"titles":17863,"content":17864,"level":117},"\u002Fblog\u002Fvue-prefetch#vue-에서는-어떻게-기능을-사용하나",[12529,12535],"import Home from '..\u002Fviews\u002FHomeView.vue'\n{\n  path: '\u002F',\n  name: 'Home',\n  component: Home\n}, {\n  path: '\u002F:id',\n  name: 'Datail',\n  component: () => import(\u002F* webpackChunkName: \"detail\" *\u002F '..\u002Fviews\u002FDetailView.vue')\n} vue-router 에서 컴포넌트를 정적으로 로드할 경우 app.js 에 컴포넌트 코드가 함께 번들링된다. 위 코드의 Detail 컴포넌트 처럼 함수를 통해 동적으로 로드할 경우 코드는 분산된다. webpackChunkName 에 정의된 이름으로 하나의 청크로 묶인다. 서로 다른 두 개의 라우트에서 같은 webpackChunkName 을 사용하면, 하나의 파일에 두 라우트 컴포넌트가 묶인다.",{"id":17866,"title":12662,"titles":17867,"content":17868,"level":130},"\u002Fblog\u002Fvue-prefetch#만약-서로-다른-라우트에서-같은-컴포넌트를-사용한다면",[12529,12535,12546],"{\n    \u002F\u002F Input 컴포넌트를 사용\n  path: '\u002F',\n  name: 'Home',\n    component: () => import(\u002F* webpackChunkName: \"home\" *\u002F '..\u002Fviews\u002FHomeView.vue')\n}, {\n    \u002F\u002F 마찬가지로 Input 컴포넌트를 사용\n  path: '\u002F:id',\n  name: 'Datail',\n  component: () => import(\u002F* webpackChunkName: \"detail\" *\u002F '..\u002Fviews\u002FDetailView.vue')\n} 만약 Home과 Detail 컴포넌트 모두 Input 컴포넌트를 공통으로 사용하고 있다면, 코드는 어떻게 묶일까. home.jsdetail.jshome-detail.js 위처럼 공통된 컴포넌트는 별도의 파일로 묶이며 각 페이지 진입 시에 home-detail.js 파일을 똑같이 로드한다. Home 컴포넌트 진입 시 home.js 와 home-detail.js 로드 + detail.js 로드 안함Detail 컴포넌트 진입 시 detail.js 와 home-detail.js 로드 + home.js 로드 안함",{"id":17870,"title":12810,"titles":17871,"content":17872,"level":101},"\u002Fblog\u002Fvue-prefetch#prefetch",[12529],"Code Spliting 을 통해 번들을 여러 파일로 분산하고, 필요에 따라 브라우저가 동적으로 요청하게끔 하는 것으로 초기 렌더링 시간을 줄일 수 있게 되었다. 그런데 여기서 설명하는 prefetch 라는 것은 분산 시킨 코드들을 index.html 에서 미리 로드해 갖고 있는 것이다. \u003Chead>\n    \u003Clink href=\"\u002Fjs\u002Fsomepart1.js\" rel=\"prefetch\">\n    \u003Clink href=\"\u002Fjs\u002Fsomepart2.js\" rel=\"prefetch\">\n\u003Chead> 이런 식으로 말이다. 이렇게 되면 코드를 분산시켰음에도 처음에 다 로드를 하게 된다. 의도한대로 작동하지 않는다.",{"id":17874,"title":12894,"titles":17875,"content":17876,"level":101},"\u002Fblog\u002Fvue-prefetch#vue-cli",[12529],"module.exports = {\n    chainWebpack: config => {\n        config.plugins.delete('prefetch');\n    }\n} vue-cli 에서는 기본적으로 사용하게끔 웹팩 설정이 되어 있다. 하지만 끄게끔 권장한다. vue.config.json 에다가 prefetch 플러그인을 제거하면 간단하게 기능을 끌 수 있다. {\n    path: '\u002Fpage',\n    name: 'SomePage',\n    component: () => import(\u002F* webpackPrefetch: true *\u002F '@\u002Fsrc\u002FSomePage.vue'),\n} 단 전략에 따라서 미리 로드해야하는 코드에 대해서는 vue-router 의 주석으로 prefetch 를 적용할 수 있다.",{"id":17878,"title":9476,"titles":17879,"content":17880,"level":101},"\u002Fblog\u002Fvue-prefetch#vite",[12529],"Vite 의 경우 Webpack 이 아닌 Rollup 을 사용한다. 기본적인 웹팩 설정들은 사용할 수 없고, 기본적으로 동적로딩을 하는 경우 모두 항상 코드는 분산되어 빌드된다. 추가로 알아보아야 함! html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"id":17468,"title":13058,"titles":17882,"content":17465,"level":90},[],{"id":17884,"title":13064,"titles":17885,"content":17886,"level":101},"\u002Fblog\u002Fvue2-reactivity#반응성",[13058],"Vue 프레임워크로 컴포넌트를 작업하다보면, 데이터를 바꿨을 뿐인데 화면에 바뀐 데이터를 바로바로 반영하는 것이 그저 익숙할 것이다. 그런데 Vue를 비롯한 프론트엔드 프레임워크를 사용하지 않을 때, 데이터만 바꿨다고 화면에 바로 뿌려줬는가? 아니다. 개발자가 직접 DOM을 수정해서 보여주어야 했다. 아니면 서버에서 바뀐 데이터로 렌더링한 결과물을 새로 받아야 했다. Vue에서는 컴포넌트의 데이터 정보만 바꾸면 화면에 알아서 적용된다. 이를 반응성이라 한다. 컴포넌트의 데이터를 관찰하고 있다가 변경이 감지되면 Virtual DOM을 다시 컴파일한 후 만들어진 트리를 실제 DOM에 적용시킨다. 개발자는 이러한 내부적 로직은 알 필요 없이 데이터에 집중하는 개발을 하도록 Vue 는 유도하고 있다. 하지만 개발자는 내가 사용하는 도구; 여기서는 프레임워크가 어떻게 작동하는 녀석인지 알아야 입맛대로 작업할 수 있고, 문제 해결을 할 수 있다. 처음엔 쉬워도 언젠가 구조적 문제에 직면하는 게 개발자 삶이다.",{"id":17888,"title":13077,"titles":17889,"content":17890,"level":101},"\u002Fblog\u002Fvue2-reactivity#vue2에서는-반응성을-어떻게-구현했나",[13058],"Vue의 모든 컴포넌트는 Watcher 를 갖고 있다. 또 data option에 등록한 데이터는 자동으로 getter 와 setter 를 주입받는다. 데이터의 setter 가 실행되는 순간, 컴포넌트의 Watcher 가 이를 감지하고, 렌더 함수를 발동시킨다. 그로 인해 Virtual DOM을 다시 구성하게 되고, 이것이 화면에 나타나는 것이다. data 뿐 아니라 computed 도 마찬가지다. data 의 변경 감지 → computed 로직 실행 → computed 변경 감지 → 렌더링의 순서로 이루어진다.",{"id":17892,"title":13099,"titles":17893,"content":17894,"level":117},"\u002Fblog\u002Fvue2-reactivity#gettersetter-주입",[13058,13077],"Vue의 코드를 조금 살펴보면서 반응형을 어떻게 구현했는지 간단하게 알아보자. Vue의 데이터에 주입하는 getter\u002Fsetter은 defineReactive 라는 함수에서 주입한다. Object.defineProperty(obj, key, {\n  enumerable: true,\n  configurable: true,\n  get: function reactiveGetter() {\n      var value = getter ? getter.call(obj) : val;\n      if (Dep.target) {\n          if (process.env.NODE_ENV !== 'production') {\n              dep.depend({\n                  target: obj,\n                  type: \"get\" \u002F* TrackOpTypes.GET *\u002F,\n                  key: key\n              });\n          }\n          else {\n              dep.depend();\n          }\n          if (childOb) {\n              childOb.dep.depend();\n              if (isArray(value)) {\n                  dependArray(value);\n              }\n          }\n      }\n      return isRef(value) && !shallow ? value.value : value;\n  },\n  set: function reactiveSetter(newVal) {\n      var value = getter ? getter.call(obj) : val;\n      if (!hasChanged(value, newVal)) {\n          return;\n      }\n      if (process.env.NODE_ENV !== 'production' && customSetter) {\n          customSetter();\n      }\n      if (setter) {\n          setter.call(obj, newVal);\n      }\n      else if (getter) {\n          \u002F\u002F #7981: for accessor properties without setter\n          return;\n      }\n      else if (isRef(value) && !isRef(newVal)) {\n          value.value = newVal;\n          return;\n      }\n      else {\n          val = newVal;\n      }\n      childOb = !shallow && observe(newVal, false, mock);\n      if (process.env.NODE_ENV !== 'production') {\n          dep.notify({\n              type: \"set\" \u002F* TriggerOpTypes.SET *\u002F,\n              target: obj,\n              key: key,\n              newValue: newVal,\n              oldValue: value\n          });\n      }\n      else {\n          dep.notify();\n      }\n  }\n}); 해당 함수는 객체의 속성마다 실행되는데, 그 속성에 대한 enumerable , configurable 과 get , set 을 Object.defineProperty 로 추가한다. 반응형인 대상에 변경이 일어나면 아래 과정을 거친다. set 함수가 실행된다. (observe 혹은 proxy 를 통해서)새로운 데이터로 변경이 되었을 때만 진행한다.새로운 데이터로 값을 바꾼다.observe 함수로 새로운 데이터에 대한 observe를 할당한다.dep.notify 가 발생하고, 렌더링을 트리깅 한다.Watcher 가 queue 에 추가하고, flushSchedulerQueue 를 통해 queue 를 플러시한다.queue 에 있는 모든 watcher의 run 함수를 호출하고 vue 인스턴스들의 updated 훅을 호출한다. 위 함수가 실행되는 부분은 다음과 같다. 컴포넌트의 props 를 초기화할 때, props 데이터도 반응형이다. (initProps$1)컴포넌트의 inject 를 초기화할 때 (initInjection)컴포넌트의 data 를 초기화할 때(initData)watch 를 등록할 때,set 등으로 인한 Observer 할당시(ob) (observe)vuex state 초기화 + mutate (initData, set)…",{"id":17896,"title":13784,"titles":17897,"content":17898,"level":117},"\u002Fblog\u002Fvue2-reactivity#watcher",[13058,13077],"var Watcher = \u002F** @class *\u002F (function () {\n    Watcher.prototype.addDep = function (dep) {};\n    Watcher.prototype.update = function () {\n        if (this.lazy) {\n            this.dirty = true;\n        }\n        else if (this.sync) {\n            this.run();\n        }\n        else {\n            queueWatcher(this);\n        }\n    };\n\u002F\u002F ...\n  return Watcher;\n}()); 우선 코드는 많은 부분을 생략했다. Watcher 의 경우 set 발생 시 notify 를 전달할 Dependency 들을 관리한다. notify 발생 시  queueWatcher 를 통해 큐에 자기 자신을 담아서 실행을 기다린다. Watcher가 sync 속성을 가졌다면 큐에 넣어서 비동기로 작동하지 않고, 동기적으로 바로 run 을 실행한다. function flushSchedulerQueue() {\n    currentFlushTimestamp = getNow();\n    flushing = true;\n    var watcher, id;\n    queue.sort(function (a, b) { return a.id - b.id; });\n    for (index$1 = 0; index$1 \u003C queue.length; index$1++) {\n        watcher = queue[index$1];\n        if (watcher.before) {\n            watcher.before();\n        }\n        id = watcher.id;\n        has[id] = null;\n        watcher.run();\n        if (process.env.NODE_ENV !== 'production' && has[id] != null) {\n            circular[id] = (circular[id] || 0) + 1;\n            if (circular[id] > MAX_UPDATE_COUNT) {\n                warn$2('You may have an infinite update loop ' +\n                    (watcher.user\n                        ? \"in watcher with expression \\\"\".concat(watcher.expression, \"\\\"\")\n                        : \"in a component render function.\"), watcher.vm);\n                break;\n            }\n        }\n    }\n    var activatedQueue = activatedChildren.slice();\n    var updatedQueue = queue.slice();\n    resetSchedulerState();\n    callActivatedHooks(activatedQueue);\n    callUpdatedHooks(updatedQueue);\n} 여기서 큐의 Watcher 들을 실행하기 전에 정렬을 하는데, 이유는 3가지가 있다. 컴포넌트들이 부모부터 자식 순서로 업데이트되도록 한다. (부모가 자식보다 전에 만들어지기 때문에)컴포넌트의 Watcher들은 render watcher 의 Watcher보다 먼저 업데이트한다.부모 컴포넌트의 watcher 가 실행될 때 자식 컴포넌트가 파괴되었다면 그 watcher 들은 스킵될 수 있다. queue 에 있는 watcher를 순회하며 before 과 run 을 차례대로 호출한다. 여기서 before 은 beforeUpdated 훅을 발생시킨다. 모두 완료되면 activateChildComponent 을 통해 _inactive 를 켜고 activated 훅을 발생시킨다. 대상 컴포넌트는 keepAlive 속성을 가진 컴포넌트들이다. 그 다음으로는 callUpdatedHooks 를 통해 컴포넌트의 updated 훅을 발생시킨다. Watcher.prototype.run = function () {\n    if (this.active) {\n    var value = this.get();\n    if (value !== this.value ||\n      \u002F\u002F Deep watchers and watchers on Object\u002FArrays should fire even\n      \u002F\u002F when the value is the same, because the value may\n      \u002F\u002F have mutated.\n      isObject(value) || this.deep) {\n      \u002F\u002F set new value\n      var oldValue = this.value;\n      this.value = value;\n      if (this.user) {\n        var info = \"callback for watcher \\\"\".concat(this.expression, \"\\\"\");\n        invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);\n      }\n      else {\n        this.cb.call(this.vm, value, oldValue);\n      }\n    }\n    }\n}; 결국은 Watcher 의 run 함수를 위해서 위 과정들을 열심히 진행한 것이다. 그렇다면 run 함수는 내부적으로 어떤 것을 처리할까? 디버깅을 걸어보면 알겠지만, 화면의 렌더링은 this.get 에서 끝난다. 그리고 그 뒤로는 콜백을 실행하는 것 뿐이다. 그럼 get 함수를 살펴봐야 한다. Watcher.prototype.get = function () {\n  pushTarget(this);\n  var value;\n  var vm = this.vm;\n  try {\n    value = this.getter.call(vm, vm);\n  }\n  catch (e) {\n    if (this.user) {\n      handleError(e, vm, \"getter for watcher \\\"\".concat(this.expression, \"\\\"\"));\n    }\n    else {\n      throw e;\n    }\n  }\n  finally {\n    \u002F\u002F \"touch\" every property so they are all tracked as\n    \u002F\u002F dependencies for deep watching\n    if (this.deep) {\n      traverse(value);\n    }\n    popTarget();\n    this.cleanupDeps();\n  }\n  return value;\n}; 단순히 this.getter 를 호출한다. 이 getter 함수는 Watcher 초기화 시 외부로부터 받는다. function mountComponent(vm, el, hydrating) {\n  vm.$el = el;\n  if (!vm.$options.render) {\n    vm.$options.render = createEmptyVNode;\n  }\n  callHook$1(vm, 'beforeMount');\n  var updateComponent;\n  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {\n    updateComponent = function () {\n      var name = vm._name;\n      var id = vm._uid;\n      var startTag = \"vue-perf-start:\".concat(id);\n      var endTag = \"vue-perf-end:\".concat(id);\n      mark(startTag);\n      **var vnode = vm._render();**\n      mark(endTag);\n      measure(\"vue \".concat(name, \" render\"), startTag, endTag);\n      mark(startTag);\n      vm._update(vnode, hydrating);\n      mark(endTag);\n      measure(\"vue \".concat(name, \" patch\"), startTag, endTag);\n    };\n  }\n  else {\n    updateComponent = function () {\n      vm._update(vm._render(), hydrating);\n    };\n  }\n  var watcherOptions = {\n    before: function () {\n      if (vm._isMounted && !vm._isDestroyed) {\n        callHook$1(vm, 'beforeUpdate');\n      }\n    }\n  };\n  if (process.env.NODE_ENV !== 'production') {\n    watcherOptions.onTrack = function (e) { return callHook$1(vm, 'renderTracked', [e]); };\n    watcherOptions.onTrigger = function (e) { return callHook$1(vm, 'renderTriggered', [e]); };\n  }\n  \u002F\u002F we set this to vm._watcher inside the watcher's constructor\n  \u002F\u002F since the watcher's initial patch may call $forceUpdate (e.g. inside child\n  \u002F\u002F component's mounted hook), which relies on vm._watcher being already defined\n  new Watcher(vm, updateComponent, noop, watcherOptions, true \u002F* isRenderWatcher *\u002F);\n  hydrating = false;\n  \u002F\u002F flush buffer for flush: \"pre\" watchers queued in setup()\n  var preWatchers = vm._preWatchers;\n  if (preWatchers) {\n    for (var i = 0; i \u003C preWatchers.length; i++) {\n      preWatchers[i].run();\n    }\n  }\n  if (vm.$vnode == null) {\n    vm._isMounted = true;\n    callHook$1(vm, 'mounted');\n  }\n  return vm;\n} mountComponent 함수 실행 시 작동은 다음과 같다. beforeMount 훅 실행watcher 옵션으로 beforeUpdate 훅을 등록한다.vm._render 를 실행하는 vm._update 함수를 Watcher 옵션으로 넘긴다.mounted 훅 실행 일부 경우에 따라 getter 함수가 달라질 순 있지만, 대부분의 경우 결론적으로는 vm._render 를 Watcher 의 getter 로 실행한다. vnode = render.call(vm._renderProxy, vm.$createElement); 최종적으로는 컴포넌트에 동적으로 생성된 render 함수를 실행하게 된다. 이 render 함수는 컴포넌트의 템플릿을 가지고 생성된다. 혹은 render 함수를 직접 구현하거나. render 함수로 생성된 vnode 를 가지고 vm._update 함수를 실행한다. Vue.prototype._update = function (vnode, hydrating) {\n    var vm = this;\n    var prevEl = vm.$el;\n    var prevVnode = vm._vnode;\n    var restoreActiveInstance = setActiveInstance(vm);\n    vm._vnode = vnode; \n\n    if (!prevVnode) {\n      \u002F\u002F initial render\n      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);\n    } else {\n      \u002F\u002F updates\n      vm.$el = vm.__patch__(prevVnode, vnode);\n    }\n    restoreActiveInstance();\n    if (prevEl) {\n      prevEl.__vue__ = null;\n    }\n    if (vm.$el) {\n      vm.$el.__vue__ = vm;\n    }\n    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {\n      vm.$parent.$el = vm.$el;\n    }\n  }; vm.__patch__ 를 호출하면서, 실제 DOM;여기서는 vm.$el 에 업데이트한다.",{"id":17900,"title":15865,"titles":17901,"content":17902,"level":101},"\u002Fblog\u002Fvue2-reactivity#vue2의-반응성-문제-및-특징",[13058],"Vue2의 반응성 구현에는 문제가 있다. 이는 자바스크립트의 한계 때문에 발생한 것이다. Vue의 데이터들은 추가, 감소가 발생했을 때는 변화를 감지하지 못한다. getter 와 setter 를 주입받지 못하기 때문이다.",{"id":17904,"title":15877,"titles":17905,"content":17906,"level":117},"\u002Fblog\u002Fvue2-reactivity#data-option-에-데이터-추가하기",[13058,15865],"data: {\n    some: 1\n}\n\nthis.other = 2; 컴포넌트의 data option 에 기존에 선언한 some 은 반응형이지만, other 는 반응형이 아니다. other 를 아무리 바꿔도 화면에는 아무런 영향을 주지 못한다. this.$set(this, 'other', 2); 하지만 Vue 인스턴스의 $set 함수를 이용해 반응형을 이끌어낼 수 있다.",{"id":17908,"title":15970,"titles":17909,"content":17910,"level":117},"\u002Fblog\u002Fvue2-reactivity#object-에-assign-으로-새로운-프로퍼티-추가하기",[13058,15865],"Object.assign 혹은 _.extend() 를 통한 객체 복사 역시 감지하지 못한다. 속성을 가져올 때는 [[Get]] 을 사용하고 속성을 지정할 때 [[Set]] 을 사용하기 때문에 getter\u002Fsetter 를 트리깅하지 못한다. Object.assign(this.someObject, { a: 1, b: 2 }) \u002F\u002F 이렇게 하면 반응성을 이끌어내지 못함.\nthis.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 }) 그래서 새로운 객체에 대상을 2개로 엮어서 병합한 후에 새로운 객체로 반환해야 한다.",{"id":17912,"title":16059,"titles":17913,"content":17914,"level":117},"\u002Fblog\u002Fvue2-reactivity#배열-문제",[13058,15865],"배열을 다루면서 아래 행동은 변화를 감지할 수 없다. 배열의 인덱스에 값을 넣으려는 것배열의 크기를 바꾸는 것 var vm = new Vue({\n  data: {\n    items: ['a', 'b', 'c']\n  }\n})\nvm.items[1] = 'x'\nvm.items.length = 2 아래 두 코드는 반응성을 이끌어내지 못했다. 그렇다면 배열은 어떻게 다뤄야 반응형일까? 배열의 특정 함수들을 이용해야 한다. push 배열에 뒤에 값을 추가shift 배열 맨 처음 값을 빼냄unshift 배열 앞에 값을 추가splice 일부를 교체함 this.$set(this.items, 1, changedValue); 혹은 $set 함수를 이용해 특정 인덱스의 값을 변경한다.",{"id":17916,"title":16202,"titles":17917,"content":17918,"level":130},"\u002Fblog\u002Fvue2-reactivity#object-배열",[13058,15865,16059],"animals: [\n    {id: 1, comments: 'cat',},\n    {id: 2, comments: 'bird',},\n    {id: 3, comments: 'dog',},\n]\n\ncreated() {\n    this.animals[3] = {id: 4, comments: 'cow'};\n} 우리는 개발을 하면서 흔하게 Object 를 여러개 담아놓은 배열을 다루게 된다. 이를 리스트 렌더링을 통해 화면에 심심치 않게 보여준다. 그런데 아까 얘기하기로는 Vue2의 배열은 인덱스에 해당하는 값을 바꾸려고 하면 반응성이 안나타난다고 했다. this.animals[1].comments = \"whale\"; 배열 1번의 객체의 데이터를 바꾸려고 하니, 세상에나 반응한다! 배열 내부의 객체는 반응형이다. 이는 콘솔에 데이터를 찍어보면 간단하게 알 수 있다. 배열의 경우 getter, setter 함수는 없지만 Observer 를 갖고 있다. 다만 내부에 있는 객체 3개는 각각 속성에 대해서 getter, setter 를 갖고 있다. 그렇기 때문에 배열 내부의 객체에 대해서는 반응형이다. 단 created 에서 인덱스로 할당한 객체의 경우 Observer 할당이 안되어 있다. 또 getter, setter 역시 주입되지 않았기 때문에, this.animals[3].comments = \"some\"; 코드는 반응형이 아니다. const newObject = { \n  \"id\": 1, \n  \"comments\": \"새로운 오브젝트를 넣음!\", \n};\nthis.animals[1] = newObject; \u002F\u002F 반응형 x\nthis.$set(this.animals, 1, newObject); \u002F\u002F 반응형 o\nthis.animals[1].comments = \"내가 바꾸었다!!!\"; \u002F\u002F 반응형 o\nthis.$set(this.animals[1], 'comments', \"내가 바꾸었다!!!\"); \u002F\u002F 반응형 o 물론 위 코드를 하나의 함수에서 실행하면 모든 변경대상이 반응형이다. 왜냐하면 반응을 감지하고 re-rendering 할 때는, 바뀐 데이터를 모두 적용하기 때문이다. 데이터는 변경이 되어 있지만, 화면에 적용만 안 되어 있던 것이다. 렌더링 시점에 모든 변경사항이 적용된다.",{"id":17920,"title":16504,"titles":17921,"content":17922,"level":130},"\u002Fblog\u002Fvue2-reactivity#다중-배열",[13058,15865,16059],"twoDimensionalArray: [\n  [\n    {id: 1, cat: 'first'},\n    {id: 2, cat: 'first'},\n  ],\n  [\n    {id: 1, cat: 'second'},\n    {id: 2, cat: 'second'},\n  ],\n  [],\n]\n\ncreated() {\n    this.twoDimensionalArray[2] = [\n    {id: 1, cat: 'third'},\n    {id: 2, cat: 'third'},\n  ]\n  this.twoDimensionalArray[3] = [\n    {id: 1, cat: 'forth'},\n    {id: 2, cat: 'forth'},\n  ]\n} 만약 다중 배열의 경우 어떻게 될까? 배열 요소가 배열이고, 그 안에는 객체들이 있는 구조이다. const newObject = { \n  \"id\": 1, \n  \"comments\": \"새로운 오브젝트를 넣음!\", \n};\nthis.twoDimensionalArray[1][1] = newObject \u002F\u002F 반응형 x\nthis.$set(this.twoDimensionalArray[1], 1, newObject); \u002F\u002F 반응형 o\nthis.twoDimensionalArray[0][0].cat = \"내가 바꾸었다!!!\"; \u002F\u002F 반응형 o\nthis.twoDimensionalArray[2][0].cat = \"새로 넣은 오브젝트인데 바뀔까?\"; \u002F\u002F 반응형 x\nthis.$set(this.twoDimensionalArray[3][1], 'comments', \"내가 바꾸었다!!!\"); \u002F\u002F 반응형 x 기본적으로 배열의 인덱스에 직접 할당하는 것은 무조건 반응형이 아니다. 2번 인덱스의 객체는, 처음에는 빈 배열이다가 인덱스에 직접 할당되었다. 그래서 반응형 설정이 되지 않았고, 다중배열 안의 객체 속성을 바꾸려고 했을 때 반응이 없다. 처음부터 할당이 되어 있거나, push 등으로 추가된 항목이 아니기 때문이다. 반대 상황으로 0번 인덱스와 1번 인덱스는 반응형으로 내부 속성을 변경하려고 할 때 반응형이다. 3번 인덱스는 처음부터 객체에 없다가, 인덱스로 할당된 객체이다. 2번과 사실상 같은 유형이다. 이 경우 $set 함수로도 반응이 없다. 3번 항목이야 push 를 통해 항목을 추가하면 된다곤 하지만, 2번 항목을 반응형으로 할당하려면 어떻게 해야할까? this.twoDimensionalArray[2].push(...[\n  {id: 1, cat: 'third'},\n  {id: 2, cat: 'third'},\n])\nthis.twoDimensionalArray.splice(2, 1, [\n  {id: 1, cat: 'third'},\n  {id: 2, cat: 'third'},\n]); spread 문으로 내용물을 펼쳐서 push 를 하는 방법splice 함수로 대상 교체하기 배열은 간단하게 생각하면 꼭 함수를 거쳐야 한다고 보면 된다. 그럼 쉽게 반응형을 끌어낼 수 있다.",{"id":17924,"title":17002,"titles":17925,"content":17926,"level":117},"\u002Fblog\u002Fvue2-reactivity#비동기-업데이트-큐",[13058,15865],"이 부분은 딱히 문제는 아니지만 알아두면 좋은 Vue 의 특성이다. Vue 의 UI 업데이트를 위해 컴파일되는 Virtual DOM은 비동기로 생성되고 화면에 렌더링된다. 즉, queue 에 담긴 여러 변경사항들을 flush 하기 전까지는 바로 화면에 적용되지 않고, 한 번에 렌더링한다. 그 시점은 다음 tick 이 된다. 또, queue 에 담긴 Watcher 중 중복인 항목은 제거하고 최신으로 유지한다. template: `\u003Cspan>{{msg}}\u003C\u002Fspan>`,\nata: {\n    msg: \"before update\",\n}\nmethods: {\n    update: () => {\n        this.msg = \"updated\";\n    console.log(this.$el.textContent) \u002F\u002F => 'not updated'\n    this.$nextTick(function () {\n      console.log(this.$el.textContent) \u002F\u002F => 'updated'\n    })\n    }\n} 간단하게 update 함수를 통해 변경된 msg 는 실제 DOM에 적용되기 전이라서 함수 실행 중에는 이전 데이터로 여전히 남아 있다. 그러다 함수 호출이 끝나고, 다음 이벤트 루프가 발생할 때가 되어서야 갱신된 데이터가 화면에 보인다. 이를 추적할 수 있는 게 this.$nextTick 이다. 이는 함수의 호출을 다음 tick 으로 미루는 작업이다. tick 에 대해서는 추후에 이벤트 루프에 대해서 작성할 때 자세하게 다룰 것이다.",{"id":17928,"title":17156,"titles":17929,"content":17930,"level":117},"\u002Fblog\u002Fvue2-reactivity#created-와-mounted",[13058,15865],"앞서 Vue 컴포넌트는 mount 시점에 렌더링을 한 번 실행한다는 것을 알았다. beforeMounted → 렌더링 → mounted 순서로 훅이 실행된다. 그렇기 때문에 발생하는 현상이 있다. created 훅과 mounted 훅에서 data 에 반응형이 아닌 데이터를 추가를 하면 UI에 표시가 될까 안될까? template: `\n    \u003Cdiv v-for=\"item of notReactive\" :key=\"item.id\">\n    {{item}}\n  \u003C\u002Fdiv>\n`,\ncreated() {\n  this.notReactive[1] = {id: 2, cat: 'reactive from created'};\n},\nmounted() {\n  this.notReactive[2] = {id: 3, cat: 'not reactive from mounted'};\n},\ndata: () => {\n  return {\n    notReactive: [\n      {id: 1, cat: 'reactive'},\n    ]\n  }\n} 말로는 헷갈릴 수 있으니 예시를 보자. 0번 인덱스는 데이터 초기화 시점에 이미 할당되어 있기 때문에 당연하게도 반응형이다. 그렇다면, 1번 인덱스와 2번 인덱스는 반응형일까? 우리가 테스트했을 때는 배열의 인덱스에 할당한 대상은 반응형이 아니다. 그렇기 때문에, UI에는 0번 인덱스 항목만 보일 거라고 예상한다. 결과는 0번 1번 인덱스 항목은 출력되고, 2번 인덱스 항목은 출력되지 않는다. 즉, created 에서 넣은 객체는 렌더가 되었고, mounted 에서 넣은 객체는 무시됐다. 렌더링과 훅의 순서 때문이다. created 시점에서는 렌더링 전이기 때문에, 렌더링 시점에 데이터는 인지되고 적용된다. 하지만 mounted 시점에는 이미 렌더링이 끝난 시점이라, 이때 추가된 데이터는 값만 바뀐 채 렌더링은 무시된다. 여담으로 mounted 에서 넣은 데이터는 this.$forceUpdate 를 호출하면 화면에 보인다. watcher.update 를 강제로 호출하기 때문이다. methods: {\n  onClick() {\n        \u002F\u002F 아래를 개별로 실행\n        \u002F\u002F 한번에 실행하면 1번 행이 반응형이기 때문에, 모두 렌더링 된다.\n    this.notReactive[0].cat = 'reactive from onClick'; \u002F\u002F 반응형\n        this.$set(this.notReactive[1], 'cat', 'reactive from onClick') \u002F\u002F 반응형 x\n    this.notReactive[1].cat = 'reactive from onClick'; \u002F\u002F 반응형 x\n    this.notReactive[2].cat = 'reactive from onClick'; \u002F\u002F 반응형 x\n  }\n}, mount 시점에 렌더링되어 화면에 나타났다고 해서 반응형인 건 아니다. mount 이후 데이터를 바꾸려고 하면 반응형이 아니기 때문에 반응이 없다. 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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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}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);}",{"id":17533,"title":17478,"titles":17932,"content":17933,"level":90},[],"지금까지 사용했던 word-wrap 이 overflow-wrap 으로 변경되었다. 어느날 gemini 가 리뷰에서 알려준 지식이 있다. 대충 보고 overflow-wrap 을 쓰라는데 뭐가 다르길래 쓰라고 하지해서 자세히 읽어봤다. alias 격이면 더더욱 바꿀 필요가 없지 않은가 했다. s !hint\n이 속성은 처음에 마이크로소프트에서 표준이 아니고 접두어가 없는word-wrap으로 나왔고, 대부분 브라우저에서 똑같은 이름으로 구현되었습니다. 요즘은overflow-wrap으로 다시 지어지고,word-wrap은 동의어가 되었습니다. 같은 단어이지만 이제는 overflow-wrap 을 써야한다. mdn 에는 word-wrap 메뉴도 없다.",{"tags":17935,"subjects":17976},[17936,17937,17938,17939,17940,17941,17942,17943,17944,17945,17946,17947,17948,17949,17950,17951,17952,17953,17954,17955,17956,17957,17958,17959,17960,17961,17962,17963,17964,17965,17966,17967,17968,17969,17970,17971,17972,17973,17974,17975],{"tagName":82,"count":224},{"tagName":2858,"count":130},{"tagName":3268,"count":117},{"tagName":7529,"count":117},{"tagName":2992,"count":101},{"tagName":7530,"count":101},{"tagName":7531,"count":101},{"tagName":7532,"count":101},{"tagName":1257,"count":101},{"tagName":8468,"count":101},{"tagName":9476,"count":101},{"tagName":13051,"count":101},{"tagName":1507,"count":90},{"tagName":1508,"count":90},{"tagName":1932,"count":90},{"tagName":2860,"count":90},{"tagName":2861,"count":90},{"tagName":2994,"count":90},{"tagName":7533,"count":90},{"tagName":7534,"count":90},{"tagName":7672,"count":90},{"tagName":7673,"count":90},{"tagName":7674,"count":90},{"tagName":7675,"count":90},{"tagName":8204,"count":90},{"tagName":8469,"count":90},{"tagName":8602,"count":90},{"tagName":8603,"count":90},{"tagName":9477,"count":90},{"tagName":12522,"count":90},{"tagName":12523,"count":90},{"tagName":12524,"count":90},{"tagName":12525,"count":90},{"tagName":12535,"count":90},{"tagName":13052,"count":90},{"tagName":13053,"count":90},{"tagName":13054,"count":90},{"tagName":17473,"count":90},{"tagName":13063,"count":90},{"tagName":17474,"count":90},[17977,17978,17979,17980,17981,17982,17983],{"subjectName":2858,"count":246},{"subjectName":847,"count":224},{"subjectName":2992,"count":101},{"subjectName":1505,"count":90},{"subjectName":7670,"count":90},{"subjectName":8243,"count":90},{"subjectName":9474,"count":90},1775316729213]