[{"data":1,"prerenderedAt":2982},["ShallowReactive",2],{"blog-post-\u002Fko\u002Fcss-has-selector":3,"blog-related-source":2690},{"id":4,"title":5,"body":6,"description":2679,"extension":2680,"meta":2681,"navigation":766,"path":2685,"seo":2686,"sitemap":2687,"stem":2688,"__hash__":2689},"ko\u002Fko\u002Fcss-has-selector.md","CSS :has() 선택자 실전 가이드: 자바스크립트 없이 부모를 선택하는 법",{"type":7,"value":8,"toc":2666},"minimark",[9,14,32,35,122,128,172,185,188,261,264,267,271,277,833,846,848,852,857,1345,1352,1370,1372,1376,1398,1651,1654,1703,1706,1708,1712,1715,2266,2278,2280,2284,2295,2402,2409,2411,2415,2420,2429,2476,2480,2541,2543,2547,2633,2662],[10,11,13],"h2",{"id":12},"css의-오랜-숙원-부모-선택자","CSS의 오랜 숙원, \"부모 선택자\"",[15,16,17,18,22,23,27,28,31],"p",{},"CSS 선택자는 오랫동안 ",[19,20,21],"strong",{},"아래 방향으로만"," 내려갈 수 있었다. ",[24,25,26],"code",{},".card img","처럼 부모 안의 자식은 고를 수 있어도, \"이미지를 ",[19,29,30],{},"가진"," 카드\"처럼 자식을 기준으로 부모를 고르는 건 불가능했다.",[15,33,34],{},"그래서 이런 코드가 흔했다.",[36,37,42],"pre",{"className":38,"code":39,"language":40,"meta":41,"style":41},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F 체크박스가 체크되면 부모 카드에 클래스를 붙이는 코드\ncheckbox.addEventListener(\"change\", (e) => {\n  e.target.closest(\".card\").classList.toggle(\"is-selected\", e.target.checked);\n});\n","js","",[24,43,44,53,88,116],{"__ignoreMap":41},[45,46,49],"span",{"class":47,"line":48},"line",1,[45,50,52],{"class":51},"sJ8bj","\u002F\u002F 체크박스가 체크되면 부모 카드에 클래스를 붙이는 코드\n",[45,54,56,60,64,67,71,74,78,81,85],{"class":47,"line":55},2,[45,57,59],{"class":58},"sVt8B","checkbox.",[45,61,63],{"class":62},"sScJk","addEventListener",[45,65,66],{"class":58},"(",[45,68,70],{"class":69},"sZZnC","\"change\"",[45,72,73],{"class":58},", (",[45,75,77],{"class":76},"s4XuR","e",[45,79,80],{"class":58},") ",[45,82,84],{"class":83},"szBVR","=>",[45,86,87],{"class":58}," {\n",[45,89,91,94,97,99,102,105,108,110,113],{"class":47,"line":90},3,[45,92,93],{"class":58},"  e.target.",[45,95,96],{"class":62},"closest",[45,98,66],{"class":58},[45,100,101],{"class":69},"\".card\"",[45,103,104],{"class":58},").classList.",[45,106,107],{"class":62},"toggle",[45,109,66],{"class":58},[45,111,112],{"class":69},"\"is-selected\"",[45,114,115],{"class":58},", e.target.checked);\n",[45,117,119],{"class":47,"line":118},4,[45,120,121],{"class":58},"});\n",[15,123,124,127],{},[24,125,126],{},":has()","가 나오면서 이 코드는 CSS 한 줄이 됐다.",[36,129,133],{"className":130,"code":131,"language":132,"meta":41,"style":41},"language-css shiki shiki-themes github-light github-dark",".card:has(input:checked) {\n  border-color: orange;\n}\n","css",[24,134,135,152,167],{"__ignoreMap":41},[45,136,137,140,142,146,149],{"class":47,"line":48},[45,138,139],{"class":62},".card:has",[45,141,66],{"class":58},[45,143,145],{"class":144},"s9eBZ","input",[45,147,148],{"class":62},":checked",[45,150,151],{"class":58},") {\n",[45,153,154,158,161,164],{"class":47,"line":55},[45,155,157],{"class":156},"sj4cs","  border-color",[45,159,160],{"class":58},": ",[45,162,163],{"class":156},"orange",[45,165,166],{"class":58},";\n",[45,168,169],{"class":47,"line":90},[45,170,171],{"class":58},"}\n",[173,174,175],"blockquote",{},[15,176,177,180,181,184],{},[24,178,179],{},"A:has(B)","는 \"B를 포함하고 있는 A\"를 선택한다. 괄호 안의 조건을 만족하는 ",[19,182,183],{},"바깥 요소","에 스타일이 적용된다.",[15,186,187],{},"괄호 안에는 자손뿐 아니라 형제 관계도 쓸 수 있어서, 사실상 \"조건부 선택자\"처럼 쓸 수 있다.",[189,190,191,204],"table",{},[192,193,194],"thead",{},[195,196,197,201],"tr",{},[198,199,200],"th",{},"쓰는 법",[198,202,203],{},"의미",[205,206,207,217,227,237,247],"tbody",{},[195,208,209,214],{},[210,211,212],"td",{},[24,213,179],{},[210,215,216],{},"자손 중에 B가 있는 A",[195,218,219,224],{},[210,220,221],{},[24,222,223],{},"A:has(> B)",[210,225,226],{},"바로 아래 자식으로 B가 있는 A",[195,228,229,234],{},[210,230,231],{},[24,232,233],{},"A:has(+ B)",[210,235,236],{},"바로 다음 형제가 B인 A",[195,238,239,244],{},[210,240,241],{},[24,242,243],{},"A:has(~ B)",[210,245,246],{},"뒤쪽 형제 중에 B가 있는 A",[195,248,249,254],{},[210,250,251],{},[24,252,253],{},"A:not(:has(B))",[210,255,256,257,260],{},"B가 ",[19,258,259],{},"없는"," A",[15,262,263],{},"이제 실전 예제를 직접 눌러보면서 보자. 각 예제의 위쪽은 실제 화면이고, 아래 탭에서 코드를 볼 수 있다.",[265,266],"hr",{},[10,268,270],{"id":269},"예제-1-선택하면-강조되는-카드","예제 1. 선택하면 강조되는 카드",[15,272,273,274],{},"요금제 선택 화면에서 자주 보는 UI다. 라디오 버튼을 고르면 그 카드 전체가 강조된다. ",[19,275,276],{},"자바스크립트는 한 줄도 없다.",[278,279,280,545],"code-group",{},[36,281,285],{"className":282,"code":283,"language":284,"meta":41,"style":41},"language-html shiki shiki-themes github-light github-dark","\u003Cdiv class=\"plans\">\n  \u003Clabel class=\"plan\">\n    \u003Cinput type=\"radio\" name=\"plan\" \u002F>\n    \u003Cstrong>Basic\u003C\u002Fstrong>\n    \u003Cspan>월 0원\u003C\u002Fspan>\n  \u003C\u002Flabel>\n  \u003Clabel class=\"plan\">\n    \u003Cinput type=\"radio\" name=\"plan\" checked \u002F>\n    \u003Cstrong>Pro\u003C\u002Fstrong>\n    \u003Cspan>월 9,900원\u003C\u002Fspan>\n  \u003C\u002Flabel>\n  \u003Clabel class=\"plan\">\n    \u003Cinput type=\"radio\" name=\"plan\" \u002F>\n    \u003Cstrong>Team\u003C\u002Fstrong>\n    \u003Cspan>월 29,000원\u003C\u002Fspan>\n  \u003C\u002Flabel>\n\u003C\u002Fdiv>\n","html",[24,286,287,307,324,349,362,376,386,401,425,439,453,462,477,498,512,526,535],{"__ignoreMap":41},[45,288,289,292,295,298,301,304],{"class":47,"line":48},[45,290,291],{"class":58},"\u003C",[45,293,294],{"class":144},"div",[45,296,297],{"class":62}," class",[45,299,300],{"class":58},"=",[45,302,303],{"class":69},"\"plans\"",[45,305,306],{"class":58},">\n",[45,308,309,312,315,317,319,322],{"class":47,"line":55},[45,310,311],{"class":58},"  \u003C",[45,313,314],{"class":144},"label",[45,316,297],{"class":62},[45,318,300],{"class":58},[45,320,321],{"class":69},"\"plan\"",[45,323,306],{"class":58},[45,325,326,329,331,334,336,339,342,344,346],{"class":47,"line":90},[45,327,328],{"class":58},"    \u003C",[45,330,145],{"class":144},[45,332,333],{"class":62}," type",[45,335,300],{"class":58},[45,337,338],{"class":69},"\"radio\"",[45,340,341],{"class":62}," name",[45,343,300],{"class":58},[45,345,321],{"class":69},[45,347,348],{"class":58}," \u002F>\n",[45,350,351,353,355,358,360],{"class":47,"line":118},[45,352,328],{"class":58},[45,354,19],{"class":144},[45,356,357],{"class":58},">Basic\u003C\u002F",[45,359,19],{"class":144},[45,361,306],{"class":58},[45,363,365,367,369,372,374],{"class":47,"line":364},5,[45,366,328],{"class":58},[45,368,45],{"class":144},[45,370,371],{"class":58},">월 0원\u003C\u002F",[45,373,45],{"class":144},[45,375,306],{"class":58},[45,377,379,382,384],{"class":47,"line":378},6,[45,380,381],{"class":58},"  \u003C\u002F",[45,383,314],{"class":144},[45,385,306],{"class":58},[45,387,389,391,393,395,397,399],{"class":47,"line":388},7,[45,390,311],{"class":58},[45,392,314],{"class":144},[45,394,297],{"class":62},[45,396,300],{"class":58},[45,398,321],{"class":69},[45,400,306],{"class":58},[45,402,404,406,408,410,412,414,416,418,420,423],{"class":47,"line":403},8,[45,405,328],{"class":58},[45,407,145],{"class":144},[45,409,333],{"class":62},[45,411,300],{"class":58},[45,413,338],{"class":69},[45,415,341],{"class":62},[45,417,300],{"class":58},[45,419,321],{"class":69},[45,421,422],{"class":62}," checked",[45,424,348],{"class":58},[45,426,428,430,432,435,437],{"class":47,"line":427},9,[45,429,328],{"class":58},[45,431,19],{"class":144},[45,433,434],{"class":58},">Pro\u003C\u002F",[45,436,19],{"class":144},[45,438,306],{"class":58},[45,440,442,444,446,449,451],{"class":47,"line":441},10,[45,443,328],{"class":58},[45,445,45],{"class":144},[45,447,448],{"class":58},">월 9,900원\u003C\u002F",[45,450,45],{"class":144},[45,452,306],{"class":58},[45,454,456,458,460],{"class":47,"line":455},11,[45,457,381],{"class":58},[45,459,314],{"class":144},[45,461,306],{"class":58},[45,463,465,467,469,471,473,475],{"class":47,"line":464},12,[45,466,311],{"class":58},[45,468,314],{"class":144},[45,470,297],{"class":62},[45,472,300],{"class":58},[45,474,321],{"class":69},[45,476,306],{"class":58},[45,478,480,482,484,486,488,490,492,494,496],{"class":47,"line":479},13,[45,481,328],{"class":58},[45,483,145],{"class":144},[45,485,333],{"class":62},[45,487,300],{"class":58},[45,489,338],{"class":69},[45,491,341],{"class":62},[45,493,300],{"class":58},[45,495,321],{"class":69},[45,497,348],{"class":58},[45,499,501,503,505,508,510],{"class":47,"line":500},14,[45,502,328],{"class":58},[45,504,19],{"class":144},[45,506,507],{"class":58},">Team\u003C\u002F",[45,509,19],{"class":144},[45,511,306],{"class":58},[45,513,515,517,519,522,524],{"class":47,"line":514},15,[45,516,328],{"class":58},[45,518,45],{"class":144},[45,520,521],{"class":58},">월 29,000원\u003C\u002F",[45,523,45],{"class":144},[45,525,306],{"class":58},[45,527,529,531,533],{"class":47,"line":528},16,[45,530,381],{"class":58},[45,532,314],{"class":144},[45,534,306],{"class":58},[45,536,538,541,543],{"class":47,"line":537},17,[45,539,540],{"class":58},"\u003C\u002F",[45,542,294],{"class":144},[45,544,306],{"class":58},[36,546,548],{"className":130,"code":547,"language":132,"meta":41,"style":41},".plans { display: flex; gap: 10px; flex-wrap: wrap; }\n.plan { display: grid; gap: 4px; padding: 14px 18px; border: 2px solid #ddd; border-radius: 12px; cursor: pointer; min-width: 110px; transition: all 0.15s; }\n.plan input { margin: 0 0 4px; }\n.plan span { color: #666; font-size: 14px; }\n\n\u002F* 체크된 라디오를 가진 카드 *\u002F\n.plan:has(input:checked) {\n  border-color: #f59e0b;\n  background: #fffbeb;\n  transform: translateY(-2px);\n}\n",[24,549,550,593,705,732,762,768,773,786,797,809,829],{"__ignoreMap":41},[45,551,552,555,558,561,563,566,569,572,574,577,580,582,585,587,590],{"class":47,"line":48},[45,553,554],{"class":62},".plans",[45,556,557],{"class":58}," { ",[45,559,560],{"class":156},"display",[45,562,160],{"class":58},[45,564,565],{"class":156},"flex",[45,567,568],{"class":58},"; ",[45,570,571],{"class":156},"gap",[45,573,160],{"class":58},[45,575,576],{"class":156},"10",[45,578,579],{"class":83},"px",[45,581,568],{"class":58},[45,583,584],{"class":156},"flex-wrap",[45,586,160],{"class":58},[45,588,589],{"class":156},"wrap",[45,591,592],{"class":58},"; }\n",[45,594,595,598,600,602,604,607,609,611,613,616,618,620,623,625,628,630,633,635,637,640,642,645,647,650,653,655,658,660,663,665,667,670,672,675,677,680,682,685,687,689,692,694,697,700,703],{"class":47,"line":55},[45,596,597],{"class":62},".plan",[45,599,557],{"class":58},[45,601,560],{"class":156},[45,603,160],{"class":58},[45,605,606],{"class":156},"grid",[45,608,568],{"class":58},[45,610,571],{"class":156},[45,612,160],{"class":58},[45,614,615],{"class":156},"4",[45,617,579],{"class":83},[45,619,568],{"class":58},[45,621,622],{"class":156},"padding",[45,624,160],{"class":58},[45,626,627],{"class":156},"14",[45,629,579],{"class":83},[45,631,632],{"class":156}," 18",[45,634,579],{"class":83},[45,636,568],{"class":58},[45,638,639],{"class":156},"border",[45,641,160],{"class":58},[45,643,644],{"class":156},"2",[45,646,579],{"class":83},[45,648,649],{"class":156}," solid",[45,651,652],{"class":156}," #ddd",[45,654,568],{"class":58},[45,656,657],{"class":156},"border-radius",[45,659,160],{"class":58},[45,661,662],{"class":156},"12",[45,664,579],{"class":83},[45,666,568],{"class":58},[45,668,669],{"class":156},"cursor",[45,671,160],{"class":58},[45,673,674],{"class":156},"pointer",[45,676,568],{"class":58},[45,678,679],{"class":156},"min-width",[45,681,160],{"class":58},[45,683,684],{"class":156},"110",[45,686,579],{"class":83},[45,688,568],{"class":58},[45,690,691],{"class":156},"transition",[45,693,160],{"class":58},[45,695,696],{"class":156},"all",[45,698,699],{"class":156}," 0.15",[45,701,702],{"class":83},"s",[45,704,592],{"class":58},[45,706,707,709,712,714,717,719,722,725,728,730],{"class":47,"line":90},[45,708,597],{"class":62},[45,710,711],{"class":144}," input",[45,713,557],{"class":58},[45,715,716],{"class":156},"margin",[45,718,160],{"class":58},[45,720,721],{"class":156},"0",[45,723,724],{"class":156}," 0",[45,726,727],{"class":156}," 4",[45,729,579],{"class":83},[45,731,592],{"class":58},[45,733,734,736,739,741,744,746,749,751,754,756,758,760],{"class":47,"line":118},[45,735,597],{"class":62},[45,737,738],{"class":144}," span",[45,740,557],{"class":58},[45,742,743],{"class":156},"color",[45,745,160],{"class":58},[45,747,748],{"class":156},"#666",[45,750,568],{"class":58},[45,752,753],{"class":156},"font-size",[45,755,160],{"class":58},[45,757,627],{"class":156},[45,759,579],{"class":83},[45,761,592],{"class":58},[45,763,764],{"class":47,"line":364},[45,765,767],{"emptyLinePlaceholder":766},true,"\n",[45,769,770],{"class":47,"line":378},[45,771,772],{"class":51},"\u002F* 체크된 라디오를 가진 카드 *\u002F\n",[45,774,775,778,780,782,784],{"class":47,"line":388},[45,776,777],{"class":62},".plan:has",[45,779,66],{"class":58},[45,781,145],{"class":144},[45,783,148],{"class":62},[45,785,151],{"class":58},[45,787,788,790,792,795],{"class":47,"line":403},[45,789,157],{"class":156},[45,791,160],{"class":58},[45,793,794],{"class":156},"#f59e0b",[45,796,166],{"class":58},[45,798,799,802,804,807],{"class":47,"line":427},[45,800,801],{"class":156},"  background",[45,803,160],{"class":58},[45,805,806],{"class":156},"#fffbeb",[45,808,166],{"class":58},[45,810,811,814,816,819,821,824,826],{"class":47,"line":441},[45,812,813],{"class":156},"  transform",[45,815,160],{"class":58},[45,817,818],{"class":156},"translateY",[45,820,66],{"class":58},[45,822,823],{"class":156},"-2",[45,825,579],{"class":83},[45,827,828],{"class":58},");\n",[45,830,831],{"class":47,"line":455},[45,832,171],{"class":58},[15,834,835,836,839,840,842,843,845],{},"핵심은 마지막 규칙 하나다. ",[24,837,838],{},"input:checked","를 ",[19,841,30],{}," ",[24,844,597],{},"을 고른다.",[265,847],{},[10,849,851],{"id":850},"예제-2-입력이-틀리면-폼-전체가-반응","예제 2. 입력이 틀리면 폼 전체가 반응",[15,853,854,856],{},[24,855,126],{},"는 폼 상태와 특히 잘 어울린다. 아래 예제에서 이메일 칸에 아무 글자나 쓰고 다른 곳을 클릭해보자.",[278,858,859,998],{},[36,860,862],{"className":282,"code":861,"language":284,"meta":41,"style":41},"\u003Cform class=\"signup\">\n  \u003Cdiv class=\"field\">\n    \u003Clabel for=\"email\">이메일\u003C\u002Flabel>\n    \u003Cinput id=\"email\" type=\"email\" required placeholder=\"you@example.com\" \u002F>\n  \u003C\u002Fdiv>\n  \u003Cp class=\"hint\">이메일 형식이 올바르지 않아요.\u003C\u002Fp>\n  \u003Cbutton>가입하기\u003C\u002Fbutton>\n\u003C\u002Fform>\n",[24,863,864,880,895,916,948,956,976,990],{"__ignoreMap":41},[45,865,866,868,871,873,875,878],{"class":47,"line":48},[45,867,291],{"class":58},[45,869,870],{"class":144},"form",[45,872,297],{"class":62},[45,874,300],{"class":58},[45,876,877],{"class":69},"\"signup\"",[45,879,306],{"class":58},[45,881,882,884,886,888,890,893],{"class":47,"line":55},[45,883,311],{"class":58},[45,885,294],{"class":144},[45,887,297],{"class":62},[45,889,300],{"class":58},[45,891,892],{"class":69},"\"field\"",[45,894,306],{"class":58},[45,896,897,899,901,904,906,909,912,914],{"class":47,"line":90},[45,898,328],{"class":58},[45,900,314],{"class":144},[45,902,903],{"class":62}," for",[45,905,300],{"class":58},[45,907,908],{"class":69},"\"email\"",[45,910,911],{"class":58},">이메일\u003C\u002F",[45,913,314],{"class":144},[45,915,306],{"class":58},[45,917,918,920,922,925,927,929,931,933,935,938,941,943,946],{"class":47,"line":118},[45,919,328],{"class":58},[45,921,145],{"class":144},[45,923,924],{"class":62}," id",[45,926,300],{"class":58},[45,928,908],{"class":69},[45,930,333],{"class":62},[45,932,300],{"class":58},[45,934,908],{"class":69},[45,936,937],{"class":62}," required",[45,939,940],{"class":62}," placeholder",[45,942,300],{"class":58},[45,944,945],{"class":69},"\"you@example.com\"",[45,947,348],{"class":58},[45,949,950,952,954],{"class":47,"line":364},[45,951,381],{"class":58},[45,953,294],{"class":144},[45,955,306],{"class":58},[45,957,958,960,962,964,966,969,972,974],{"class":47,"line":378},[45,959,311],{"class":58},[45,961,15],{"class":144},[45,963,297],{"class":62},[45,965,300],{"class":58},[45,967,968],{"class":69},"\"hint\"",[45,970,971],{"class":58},">이메일 형식이 올바르지 않아요.\u003C\u002F",[45,973,15],{"class":144},[45,975,306],{"class":58},[45,977,978,980,983,986,988],{"class":47,"line":388},[45,979,311],{"class":58},[45,981,982],{"class":144},"button",[45,984,985],{"class":58},">가입하기\u003C\u002F",[45,987,982],{"class":144},[45,989,306],{"class":58},[45,991,992,994,996],{"class":47,"line":403},[45,993,540],{"class":58},[45,995,870],{"class":144},[45,997,306],{"class":58},[36,999,1001],{"className":130,"code":1000,"language":132,"meta":41,"style":41},".signup { display: grid; gap: 8px; max-width: 280px; padding: 16px; border: 2px solid #ddd; border-radius: 12px; }\n.field { display: grid; gap: 4px; }\ninput { font: inherit; padding: 6px 8px; }\n.hint { display: none; margin: 0; color: crimson; font-size: 14px; }\nbutton { font: inherit; padding: 6px; }\n\n\u002F* 입력칸에 포커스가 있으면 라벨 색을 바꾼다 *\u002F\n.field:has(input:focus) label { color: #f59e0b; font-weight: 700; }\n\n\u002F* 틀린 입력이 있으면: 폼 테두리, 안내 문구, 버튼이 함께 바뀐다 *\u002F\n.signup:has(input:user-invalid) { border-color: crimson; }\n.signup:has(input:user-invalid) .hint { display: block; }\n.signup:has(input:user-invalid) button { opacity: 0.4; pointer-events: none; }\n",[24,1002,1003,1076,1101,1133,1176,1200,1204,1209,1245,1249,1254,1281,1308],{"__ignoreMap":41},[45,1004,1005,1008,1010,1012,1014,1016,1018,1020,1022,1025,1027,1029,1032,1034,1037,1039,1041,1043,1045,1048,1050,1052,1054,1056,1058,1060,1062,1064,1066,1068,1070,1072,1074],{"class":47,"line":48},[45,1006,1007],{"class":62},".signup",[45,1009,557],{"class":58},[45,1011,560],{"class":156},[45,1013,160],{"class":58},[45,1015,606],{"class":156},[45,1017,568],{"class":58},[45,1019,571],{"class":156},[45,1021,160],{"class":58},[45,1023,1024],{"class":156},"8",[45,1026,579],{"class":83},[45,1028,568],{"class":58},[45,1030,1031],{"class":156},"max-width",[45,1033,160],{"class":58},[45,1035,1036],{"class":156},"280",[45,1038,579],{"class":83},[45,1040,568],{"class":58},[45,1042,622],{"class":156},[45,1044,160],{"class":58},[45,1046,1047],{"class":156},"16",[45,1049,579],{"class":83},[45,1051,568],{"class":58},[45,1053,639],{"class":156},[45,1055,160],{"class":58},[45,1057,644],{"class":156},[45,1059,579],{"class":83},[45,1061,649],{"class":156},[45,1063,652],{"class":156},[45,1065,568],{"class":58},[45,1067,657],{"class":156},[45,1069,160],{"class":58},[45,1071,662],{"class":156},[45,1073,579],{"class":83},[45,1075,592],{"class":58},[45,1077,1078,1081,1083,1085,1087,1089,1091,1093,1095,1097,1099],{"class":47,"line":55},[45,1079,1080],{"class":62},".field",[45,1082,557],{"class":58},[45,1084,560],{"class":156},[45,1086,160],{"class":58},[45,1088,606],{"class":156},[45,1090,568],{"class":58},[45,1092,571],{"class":156},[45,1094,160],{"class":58},[45,1096,615],{"class":156},[45,1098,579],{"class":83},[45,1100,592],{"class":58},[45,1102,1103,1105,1107,1110,1112,1115,1117,1119,1121,1124,1126,1129,1131],{"class":47,"line":90},[45,1104,145],{"class":144},[45,1106,557],{"class":58},[45,1108,1109],{"class":156},"font",[45,1111,160],{"class":58},[45,1113,1114],{"class":156},"inherit",[45,1116,568],{"class":58},[45,1118,622],{"class":156},[45,1120,160],{"class":58},[45,1122,1123],{"class":156},"6",[45,1125,579],{"class":83},[45,1127,1128],{"class":156}," 8",[45,1130,579],{"class":83},[45,1132,592],{"class":58},[45,1134,1135,1138,1140,1142,1144,1147,1149,1151,1153,1155,1157,1159,1161,1164,1166,1168,1170,1172,1174],{"class":47,"line":118},[45,1136,1137],{"class":62},".hint",[45,1139,557],{"class":58},[45,1141,560],{"class":156},[45,1143,160],{"class":58},[45,1145,1146],{"class":156},"none",[45,1148,568],{"class":58},[45,1150,716],{"class":156},[45,1152,160],{"class":58},[45,1154,721],{"class":156},[45,1156,568],{"class":58},[45,1158,743],{"class":156},[45,1160,160],{"class":58},[45,1162,1163],{"class":156},"crimson",[45,1165,568],{"class":58},[45,1167,753],{"class":156},[45,1169,160],{"class":58},[45,1171,627],{"class":156},[45,1173,579],{"class":83},[45,1175,592],{"class":58},[45,1177,1178,1180,1182,1184,1186,1188,1190,1192,1194,1196,1198],{"class":47,"line":364},[45,1179,982],{"class":144},[45,1181,557],{"class":58},[45,1183,1109],{"class":156},[45,1185,160],{"class":58},[45,1187,1114],{"class":156},[45,1189,568],{"class":58},[45,1191,622],{"class":156},[45,1193,160],{"class":58},[45,1195,1123],{"class":156},[45,1197,579],{"class":83},[45,1199,592],{"class":58},[45,1201,1202],{"class":47,"line":378},[45,1203,767],{"emptyLinePlaceholder":766},[45,1205,1206],{"class":47,"line":388},[45,1207,1208],{"class":51},"\u002F* 입력칸에 포커스가 있으면 라벨 색을 바꾼다 *\u002F\n",[45,1210,1211,1214,1216,1218,1221,1223,1225,1227,1229,1231,1233,1235,1238,1240,1243],{"class":47,"line":403},[45,1212,1213],{"class":62},".field:has",[45,1215,66],{"class":58},[45,1217,145],{"class":144},[45,1219,1220],{"class":62},":focus",[45,1222,80],{"class":58},[45,1224,314],{"class":144},[45,1226,557],{"class":58},[45,1228,743],{"class":156},[45,1230,160],{"class":58},[45,1232,794],{"class":156},[45,1234,568],{"class":58},[45,1236,1237],{"class":156},"font-weight",[45,1239,160],{"class":58},[45,1241,1242],{"class":156},"700",[45,1244,592],{"class":58},[45,1246,1247],{"class":47,"line":427},[45,1248,767],{"emptyLinePlaceholder":766},[45,1250,1251],{"class":47,"line":441},[45,1252,1253],{"class":51},"\u002F* 틀린 입력이 있으면: 폼 테두리, 안내 문구, 버튼이 함께 바뀐다 *\u002F\n",[45,1255,1256,1259,1261,1263,1266,1269,1272,1275,1277,1279],{"class":47,"line":455},[45,1257,1258],{"class":62},".signup:has",[45,1260,66],{"class":58},[45,1262,145],{"class":144},[45,1264,1265],{"class":58},":",[45,1267,1268],{"class":144},"user-invalid",[45,1270,1271],{"class":58},") { ",[45,1273,1274],{"class":156},"border-color",[45,1276,160],{"class":58},[45,1278,1163],{"class":156},[45,1280,592],{"class":58},[45,1282,1283,1285,1287,1289,1291,1293,1295,1297,1299,1301,1303,1306],{"class":47,"line":464},[45,1284,1258],{"class":62},[45,1286,66],{"class":58},[45,1288,145],{"class":144},[45,1290,1265],{"class":58},[45,1292,1268],{"class":144},[45,1294,80],{"class":58},[45,1296,1137],{"class":62},[45,1298,557],{"class":58},[45,1300,560],{"class":156},[45,1302,160],{"class":58},[45,1304,1305],{"class":156},"block",[45,1307,592],{"class":58},[45,1309,1310,1312,1314,1316,1318,1320,1322,1324,1326,1329,1331,1334,1336,1339,1341,1343],{"class":47,"line":479},[45,1311,1258],{"class":62},[45,1313,66],{"class":58},[45,1315,145],{"class":144},[45,1317,1265],{"class":58},[45,1319,1268],{"class":144},[45,1321,80],{"class":58},[45,1323,982],{"class":144},[45,1325,557],{"class":58},[45,1327,1328],{"class":156},"opacity",[45,1330,160],{"class":58},[45,1332,1333],{"class":156},"0.4",[45,1335,568],{"class":58},[45,1337,1338],{"class":156},"pointer-events",[45,1340,160],{"class":58},[45,1342,1146],{"class":156},[45,1344,592],{"class":58},[15,1346,1347,1348,1351],{},"입력칸 하나의 상태로 ",[19,1349,1350],{},"라벨, 폼 테두리, 안내 문구, 버튼","이 한꺼번에 바뀐다. 예전에는 입력 이벤트를 듣고 상태를 만들어서 각 요소에 클래스를 붙여야 했던 일이다.",[1353,1354,1355],"tip",{},[173,1356,1357],{},[15,1358,1359,1362,1363,1366,1367,1369],{},[24,1360,1361],{},":user-invalid","는 사용자가 입력을 건드린 뒤에만 적용되는 의사 클래스다. ",[24,1364,1365],{},":invalid","를 쓰면 페이지에 들어오자마자 빈 칸이 전부 \"틀렸다\"고 표시되니, 에러 표시에는 ",[24,1368,1361],{},"가 더 자연스럽다.",[265,1371],{},[10,1373,1375],{"id":1374},"예제-3-앞쪽-형제-선택하기-별점-ui","예제 3. 앞쪽 형제 선택하기: 별점 UI",[15,1377,1378,1379,1382,1383,1386,1387,1390,1391,1394,1395,1397],{},"CSS의 ",[24,1380,1381],{},"~",", ",[24,1384,1385],{},"+"," 결합자는 ",[19,1388,1389],{},"뒤쪽"," 형제만 고를 수 있었다. 별점에 마우스를 올렸을 때 \"그 별과 ",[19,1392,1393],{},"앞의 별들","\"을 칠하는 게 어려웠던 이유다. ",[24,1396,126],{},"를 쓰면 앞쪽도 고를 수 있다. 별 위에 마우스를 올려보자.",[278,1399,1400,1494],{},[36,1401,1403],{"className":282,"code":1402,"language":284,"meta":41,"style":41},"\u003Cdiv class=\"stars\">\n  \u003Cspan>★\u003C\u002Fspan>\u003Cspan>★\u003C\u002Fspan>\u003Cspan>★\u003C\u002Fspan>\u003Cspan>★\u003C\u002Fspan>\u003Cspan>★\u003C\u002Fspan>\n\u003C\u002Fdiv>\n\u003Cp class=\"caption\">별에 마우스를 올려보세요\u003C\u002Fp>\n",[24,1404,1405,1420,1466,1474],{"__ignoreMap":41},[45,1406,1407,1409,1411,1413,1415,1418],{"class":47,"line":48},[45,1408,291],{"class":58},[45,1410,294],{"class":144},[45,1412,297],{"class":62},[45,1414,300],{"class":58},[45,1416,1417],{"class":69},"\"stars\"",[45,1419,306],{"class":58},[45,1421,1422,1424,1426,1429,1431,1434,1436,1438,1440,1442,1444,1446,1448,1450,1452,1454,1456,1458,1460,1462,1464],{"class":47,"line":55},[45,1423,311],{"class":58},[45,1425,45],{"class":144},[45,1427,1428],{"class":58},">★\u003C\u002F",[45,1430,45],{"class":144},[45,1432,1433],{"class":58},">\u003C",[45,1435,45],{"class":144},[45,1437,1428],{"class":58},[45,1439,45],{"class":144},[45,1441,1433],{"class":58},[45,1443,45],{"class":144},[45,1445,1428],{"class":58},[45,1447,45],{"class":144},[45,1449,1433],{"class":58},[45,1451,45],{"class":144},[45,1453,1428],{"class":58},[45,1455,45],{"class":144},[45,1457,1433],{"class":58},[45,1459,45],{"class":144},[45,1461,1428],{"class":58},[45,1463,45],{"class":144},[45,1465,306],{"class":58},[45,1467,1468,1470,1472],{"class":47,"line":90},[45,1469,540],{"class":58},[45,1471,294],{"class":144},[45,1473,306],{"class":58},[45,1475,1476,1478,1480,1482,1484,1487,1490,1492],{"class":47,"line":118},[45,1477,291],{"class":58},[45,1479,15],{"class":144},[45,1481,297],{"class":62},[45,1483,300],{"class":58},[45,1485,1486],{"class":69},"\"caption\"",[45,1488,1489],{"class":58},">별에 마우스를 올려보세요\u003C\u002F",[45,1491,15],{"class":144},[45,1493,306],{"class":58},[36,1495,1497],{"className":130,"code":1496,"language":132,"meta":41,"style":41},".stars { font-size: 40px; color: #ddd; cursor: pointer; }\n.stars span { transition: color 0.1s; }\n.caption { color: #666; margin: 4px 0 0; }\n\n\u002F* 1) 어떤 별이든 마우스가 올라가 있으면, 우선 전부 노랗게 *\u002F\n.stars:has(span:hover) span { color: #f59e0b; }\n\n\u002F* 2) 마우스가 올라간 별의 \"뒤쪽\" 별들은 다시 회색으로 *\u002F\n.stars span:hover ~ span { color: #ddd; }\n",[24,1498,1499,1534,1555,1584,1588,1593,1619,1623,1628],{"__ignoreMap":41},[45,1500,1501,1504,1506,1508,1510,1513,1515,1517,1519,1521,1524,1526,1528,1530,1532],{"class":47,"line":48},[45,1502,1503],{"class":62},".stars",[45,1505,557],{"class":58},[45,1507,753],{"class":156},[45,1509,160],{"class":58},[45,1511,1512],{"class":156},"40",[45,1514,579],{"class":83},[45,1516,568],{"class":58},[45,1518,743],{"class":156},[45,1520,160],{"class":58},[45,1522,1523],{"class":156},"#ddd",[45,1525,568],{"class":58},[45,1527,669],{"class":156},[45,1529,160],{"class":58},[45,1531,674],{"class":156},[45,1533,592],{"class":58},[45,1535,1536,1538,1540,1542,1544,1546,1548,1551,1553],{"class":47,"line":55},[45,1537,1503],{"class":62},[45,1539,738],{"class":144},[45,1541,557],{"class":58},[45,1543,691],{"class":156},[45,1545,160],{"class":58},[45,1547,743],{"class":156},[45,1549,1550],{"class":156}," 0.1",[45,1552,702],{"class":83},[45,1554,592],{"class":58},[45,1556,1557,1560,1562,1564,1566,1568,1570,1572,1574,1576,1578,1580,1582],{"class":47,"line":90},[45,1558,1559],{"class":62},".caption",[45,1561,557],{"class":58},[45,1563,743],{"class":156},[45,1565,160],{"class":58},[45,1567,748],{"class":156},[45,1569,568],{"class":58},[45,1571,716],{"class":156},[45,1573,160],{"class":58},[45,1575,615],{"class":156},[45,1577,579],{"class":83},[45,1579,724],{"class":156},[45,1581,724],{"class":156},[45,1583,592],{"class":58},[45,1585,1586],{"class":47,"line":118},[45,1587,767],{"emptyLinePlaceholder":766},[45,1589,1590],{"class":47,"line":364},[45,1591,1592],{"class":51},"\u002F* 1) 어떤 별이든 마우스가 올라가 있으면, 우선 전부 노랗게 *\u002F\n",[45,1594,1595,1598,1600,1602,1605,1607,1609,1611,1613,1615,1617],{"class":47,"line":378},[45,1596,1597],{"class":62},".stars:has",[45,1599,66],{"class":58},[45,1601,45],{"class":144},[45,1603,1604],{"class":62},":hover",[45,1606,80],{"class":58},[45,1608,45],{"class":144},[45,1610,557],{"class":58},[45,1612,743],{"class":156},[45,1614,160],{"class":58},[45,1616,794],{"class":156},[45,1618,592],{"class":58},[45,1620,1621],{"class":47,"line":388},[45,1622,767],{"emptyLinePlaceholder":766},[45,1624,1625],{"class":47,"line":403},[45,1626,1627],{"class":51},"\u002F* 2) 마우스가 올라간 별의 \"뒤쪽\" 별들은 다시 회색으로 *\u002F\n",[45,1629,1630,1632,1634,1636,1639,1641,1643,1645,1647,1649],{"class":47,"line":427},[45,1631,1503],{"class":62},[45,1633,738],{"class":144},[45,1635,1604],{"class":62},[45,1637,1638],{"class":83}," ~",[45,1640,738],{"class":144},[45,1642,557],{"class":58},[45,1644,743],{"class":156},[45,1646,160],{"class":58},[45,1648,1523],{"class":156},[45,1650,592],{"class":58},[15,1652,1653],{},"결과적으로 \"마우스가 올라간 별까지\" 칠해진다. 형제 관계로 직접 쓰면 이렇게도 표현할 수 있다.",[36,1655,1657],{"className":130,"code":1656,"language":132,"meta":41,"style":41},"\u002F* 바로 다음 형제가 hover 중인 요소 = hover 된 요소의 바로 앞 형제 *\u002F\nli:has(+ li:hover) {\n  transform: scale(1.1);\n}\n",[24,1658,1659,1664,1683,1699],{"__ignoreMap":41},[45,1660,1661],{"class":47,"line":48},[45,1662,1663],{"class":51},"\u002F* 바로 다음 형제가 hover 중인 요소 = hover 된 요소의 바로 앞 형제 *\u002F\n",[45,1665,1666,1669,1672,1674,1676,1679,1681],{"class":47,"line":55},[45,1667,1668],{"class":144},"li",[45,1670,1671],{"class":62},":has",[45,1673,66],{"class":58},[45,1675,1385],{"class":83},[45,1677,1678],{"class":144}," li",[45,1680,1604],{"class":62},[45,1682,151],{"class":58},[45,1684,1685,1687,1689,1692,1694,1697],{"class":47,"line":90},[45,1686,813],{"class":156},[45,1688,160],{"class":58},[45,1690,1691],{"class":156},"scale",[45,1693,66],{"class":58},[45,1695,1696],{"class":156},"1.1",[45,1698,828],{"class":58},[45,1700,1701],{"class":47,"line":118},[45,1702,171],{"class":58},[15,1704,1705],{},"macOS Dock처럼 마우스를 올린 아이콘 양옆이 함께 커지는 효과를 이렇게 만든다.",[265,1707],{},[10,1709,1711],{"id":1710},"예제-4-개수에-따라-레이아웃-바꾸기","예제 4. 개수에 따라 레이아웃 바꾸기",[15,1713,1714],{},"\"항목이 4개 이상이면 2열로\" 같은 조건도 CSS만으로 된다. 아래 버튼으로 항목을 늘리고 줄여보자. (항목 추가\u002F삭제에만 자바스크립트를 썼고, 레이아웃 전환은 CSS가 한다.)",[278,1716,1717,1848,2106],{},[36,1718,1720],{"className":282,"code":1719,"language":284,"meta":41,"style":41},"\u003Cdiv class=\"controls\">\n  \u003Cbutton id=\"add\">항목 추가\u003C\u002Fbutton>\n  \u003Cbutton id=\"remove\">항목 삭제\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003Cul class=\"list\">\n  \u003Cli>항목 1\u003C\u002Fli>\n  \u003Cli>항목 2\u003C\u002Fli>\n  \u003Cli>항목 3\u003C\u002Fli>\n\u003C\u002Ful>\n",[24,1721,1722,1737,1757,1777,1785,1801,1814,1827,1840],{"__ignoreMap":41},[45,1723,1724,1726,1728,1730,1732,1735],{"class":47,"line":48},[45,1725,291],{"class":58},[45,1727,294],{"class":144},[45,1729,297],{"class":62},[45,1731,300],{"class":58},[45,1733,1734],{"class":69},"\"controls\"",[45,1736,306],{"class":58},[45,1738,1739,1741,1743,1745,1747,1750,1753,1755],{"class":47,"line":55},[45,1740,311],{"class":58},[45,1742,982],{"class":144},[45,1744,924],{"class":62},[45,1746,300],{"class":58},[45,1748,1749],{"class":69},"\"add\"",[45,1751,1752],{"class":58},">항목 추가\u003C\u002F",[45,1754,982],{"class":144},[45,1756,306],{"class":58},[45,1758,1759,1761,1763,1765,1767,1770,1773,1775],{"class":47,"line":90},[45,1760,311],{"class":58},[45,1762,982],{"class":144},[45,1764,924],{"class":62},[45,1766,300],{"class":58},[45,1768,1769],{"class":69},"\"remove\"",[45,1771,1772],{"class":58},">항목 삭제\u003C\u002F",[45,1774,982],{"class":144},[45,1776,306],{"class":58},[45,1778,1779,1781,1783],{"class":47,"line":118},[45,1780,540],{"class":58},[45,1782,294],{"class":144},[45,1784,306],{"class":58},[45,1786,1787,1789,1792,1794,1796,1799],{"class":47,"line":364},[45,1788,291],{"class":58},[45,1790,1791],{"class":144},"ul",[45,1793,297],{"class":62},[45,1795,300],{"class":58},[45,1797,1798],{"class":69},"\"list\"",[45,1800,306],{"class":58},[45,1802,1803,1805,1807,1810,1812],{"class":47,"line":378},[45,1804,311],{"class":58},[45,1806,1668],{"class":144},[45,1808,1809],{"class":58},">항목 1\u003C\u002F",[45,1811,1668],{"class":144},[45,1813,306],{"class":58},[45,1815,1816,1818,1820,1823,1825],{"class":47,"line":388},[45,1817,311],{"class":58},[45,1819,1668],{"class":144},[45,1821,1822],{"class":58},">항목 2\u003C\u002F",[45,1824,1668],{"class":144},[45,1826,306],{"class":58},[45,1828,1829,1831,1833,1836,1838],{"class":47,"line":403},[45,1830,311],{"class":58},[45,1832,1668],{"class":144},[45,1834,1835],{"class":58},">항목 3\u003C\u002F",[45,1837,1668],{"class":144},[45,1839,306],{"class":58},[45,1841,1842,1844,1846],{"class":47,"line":427},[45,1843,540],{"class":58},[45,1845,1791],{"class":144},[45,1847,306],{"class":58},[36,1849,1851],{"className":130,"code":1850,"language":132,"meta":41,"style":41},".controls { display: flex; gap: 6px; margin-bottom: 10px; }\nbutton { font: inherit; padding: 4px 10px; }\n.list { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }\n.list li { padding: 8px 12px; border-radius: 8px; background: #f4f4f5; }\n\n\u002F* 4번째 항목이 존재하면 = 항목이 4개 이상이면 2열 *\u002F\n.list:has(> li:nth-child(4)) { grid-template-columns: 1fr 1fr; }\n\n\u002F* 7개 이상이면 3열 *\u002F\n.list:has(> li:nth-child(7)) { grid-template-columns: 1fr 1fr 1fr; }\n",[24,1852,1853,1889,1918,1968,2011,2015,2020,2060,2064,2069],{"__ignoreMap":41},[45,1854,1855,1858,1860,1862,1864,1866,1868,1870,1872,1874,1876,1878,1881,1883,1885,1887],{"class":47,"line":48},[45,1856,1857],{"class":62},".controls",[45,1859,557],{"class":58},[45,1861,560],{"class":156},[45,1863,160],{"class":58},[45,1865,565],{"class":156},[45,1867,568],{"class":58},[45,1869,571],{"class":156},[45,1871,160],{"class":58},[45,1873,1123],{"class":156},[45,1875,579],{"class":83},[45,1877,568],{"class":58},[45,1879,1880],{"class":156},"margin-bottom",[45,1882,160],{"class":58},[45,1884,576],{"class":156},[45,1886,579],{"class":83},[45,1888,592],{"class":58},[45,1890,1891,1893,1895,1897,1899,1901,1903,1905,1907,1909,1911,1914,1916],{"class":47,"line":55},[45,1892,982],{"class":144},[45,1894,557],{"class":58},[45,1896,1109],{"class":156},[45,1898,160],{"class":58},[45,1900,1114],{"class":156},[45,1902,568],{"class":58},[45,1904,622],{"class":156},[45,1906,160],{"class":58},[45,1908,615],{"class":156},[45,1910,579],{"class":83},[45,1912,1913],{"class":156}," 10",[45,1915,579],{"class":83},[45,1917,592],{"class":58},[45,1919,1920,1923,1925,1928,1930,1932,1934,1936,1938,1940,1942,1944,1946,1948,1950,1952,1954,1956,1958,1960,1962,1964,1966],{"class":47,"line":90},[45,1921,1922],{"class":62},".list",[45,1924,557],{"class":58},[45,1926,1927],{"class":156},"list-style",[45,1929,160],{"class":58},[45,1931,1146],{"class":156},[45,1933,568],{"class":58},[45,1935,622],{"class":156},[45,1937,160],{"class":58},[45,1939,721],{"class":156},[45,1941,568],{"class":58},[45,1943,716],{"class":156},[45,1945,160],{"class":58},[45,1947,721],{"class":156},[45,1949,568],{"class":58},[45,1951,560],{"class":156},[45,1953,160],{"class":58},[45,1955,606],{"class":156},[45,1957,568],{"class":58},[45,1959,571],{"class":156},[45,1961,160],{"class":58},[45,1963,1123],{"class":156},[45,1965,579],{"class":83},[45,1967,592],{"class":58},[45,1969,1970,1972,1974,1976,1978,1980,1982,1984,1987,1989,1991,1993,1995,1997,1999,2001,2004,2006,2009],{"class":47,"line":118},[45,1971,1922],{"class":62},[45,1973,1678],{"class":144},[45,1975,557],{"class":58},[45,1977,622],{"class":156},[45,1979,160],{"class":58},[45,1981,1024],{"class":156},[45,1983,579],{"class":83},[45,1985,1986],{"class":156}," 12",[45,1988,579],{"class":83},[45,1990,568],{"class":58},[45,1992,657],{"class":156},[45,1994,160],{"class":58},[45,1996,1024],{"class":156},[45,1998,579],{"class":83},[45,2000,568],{"class":58},[45,2002,2003],{"class":156},"background",[45,2005,160],{"class":58},[45,2007,2008],{"class":156},"#f4f4f5",[45,2010,592],{"class":58},[45,2012,2013],{"class":47,"line":364},[45,2014,767],{"emptyLinePlaceholder":766},[45,2016,2017],{"class":47,"line":378},[45,2018,2019],{"class":51},"\u002F* 4번째 항목이 존재하면 = 항목이 4개 이상이면 2열 *\u002F\n",[45,2021,2022,2025,2027,2030,2032,2035,2037,2039,2042,2045,2047,2050,2053,2056,2058],{"class":47,"line":388},[45,2023,2024],{"class":62},".list:has",[45,2026,66],{"class":58},[45,2028,2029],{"class":83},">",[45,2031,1678],{"class":144},[45,2033,2034],{"class":62},":nth-child",[45,2036,66],{"class":58},[45,2038,615],{"class":156},[45,2040,2041],{"class":58},")) { ",[45,2043,2044],{"class":156},"grid-template-columns",[45,2046,160],{"class":58},[45,2048,2049],{"class":156},"1",[45,2051,2052],{"class":83},"fr",[45,2054,2055],{"class":156}," 1",[45,2057,2052],{"class":83},[45,2059,592],{"class":58},[45,2061,2062],{"class":47,"line":403},[45,2063,767],{"emptyLinePlaceholder":766},[45,2065,2066],{"class":47,"line":427},[45,2067,2068],{"class":51},"\u002F* 7개 이상이면 3열 *\u002F\n",[45,2070,2071,2073,2075,2077,2079,2081,2083,2086,2088,2090,2092,2094,2096,2098,2100,2102,2104],{"class":47,"line":441},[45,2072,2024],{"class":62},[45,2074,66],{"class":58},[45,2076,2029],{"class":83},[45,2078,1678],{"class":144},[45,2080,2034],{"class":62},[45,2082,66],{"class":58},[45,2084,2085],{"class":156},"7",[45,2087,2041],{"class":58},[45,2089,2044],{"class":156},[45,2091,160],{"class":58},[45,2093,2049],{"class":156},[45,2095,2052],{"class":83},[45,2097,2055],{"class":156},[45,2099,2052],{"class":83},[45,2101,2055],{"class":156},[45,2103,2052],{"class":83},[45,2105,592],{"class":58},[36,2107,2109],{"className":38,"code":2108,"language":40,"meta":41,"style":41},"const list = document.querySelector(\".list\");\ndocument.querySelector(\"#add\").addEventListener(\"click\", () => {\n  const li = document.createElement(\"li\");\n  li.textContent = `항목 ${list.children.length + 1}`;\n  list.append(li);\n});\ndocument.querySelector(\"#remove\").addEventListener(\"click\", () => list.lastElementChild?.remove());\n",[24,2110,2111,2135,2164,2185,2219,2230,2234],{"__ignoreMap":41},[45,2112,2113,2116,2119,2122,2125,2128,2130,2133],{"class":47,"line":48},[45,2114,2115],{"class":83},"const",[45,2117,2118],{"class":156}," list",[45,2120,2121],{"class":83}," =",[45,2123,2124],{"class":58}," document.",[45,2126,2127],{"class":62},"querySelector",[45,2129,66],{"class":58},[45,2131,2132],{"class":69},"\".list\"",[45,2134,828],{"class":58},[45,2136,2137,2140,2142,2144,2147,2150,2152,2154,2157,2160,2162],{"class":47,"line":55},[45,2138,2139],{"class":58},"document.",[45,2141,2127],{"class":62},[45,2143,66],{"class":58},[45,2145,2146],{"class":69},"\"#add\"",[45,2148,2149],{"class":58},").",[45,2151,63],{"class":62},[45,2153,66],{"class":58},[45,2155,2156],{"class":69},"\"click\"",[45,2158,2159],{"class":58},", () ",[45,2161,84],{"class":83},[45,2163,87],{"class":58},[45,2165,2166,2169,2171,2173,2175,2178,2180,2183],{"class":47,"line":90},[45,2167,2168],{"class":83},"  const",[45,2170,1678],{"class":156},[45,2172,2121],{"class":83},[45,2174,2124],{"class":58},[45,2176,2177],{"class":62},"createElement",[45,2179,66],{"class":58},[45,2181,2182],{"class":69},"\"li\"",[45,2184,828],{"class":58},[45,2186,2187,2190,2192,2195,2198,2201,2204,2206,2209,2212,2214,2217],{"class":47,"line":118},[45,2188,2189],{"class":58},"  li.textContent ",[45,2191,300],{"class":83},[45,2193,2194],{"class":69}," `항목 ${",[45,2196,2197],{"class":58},"list",[45,2199,2200],{"class":69},".",[45,2202,2203],{"class":58},"children",[45,2205,2200],{"class":69},[45,2207,2208],{"class":156},"length",[45,2210,2211],{"class":83}," +",[45,2213,2055],{"class":156},[45,2215,2216],{"class":69},"}`",[45,2218,166],{"class":58},[45,2220,2221,2224,2227],{"class":47,"line":364},[45,2222,2223],{"class":58},"  list.",[45,2225,2226],{"class":62},"append",[45,2228,2229],{"class":58},"(li);\n",[45,2231,2232],{"class":47,"line":378},[45,2233,121],{"class":58},[45,2235,2236,2238,2240,2242,2245,2247,2249,2251,2253,2255,2257,2260,2263],{"class":47,"line":388},[45,2237,2139],{"class":58},[45,2239,2127],{"class":62},[45,2241,66],{"class":58},[45,2243,2244],{"class":69},"\"#remove\"",[45,2246,2149],{"class":58},[45,2248,63],{"class":62},[45,2250,66],{"class":58},[45,2252,2156],{"class":69},[45,2254,2159],{"class":58},[45,2256,84],{"class":83},[45,2258,2259],{"class":58}," list.lastElementChild?.",[45,2261,2262],{"class":62},"remove",[45,2264,2265],{"class":58},"());\n",[15,2267,2268,2271,2272,839,2275,2277],{},[24,2269,2270],{},"li:nth-child(4)","가 ",[19,2273,2274],{},"존재하는지",[24,2276,126],{},"로 묻는 방식이다. 이런 기법을 수량 쿼리(Quantity Query)라고 부른다.",[265,2279],{},[10,2281,2283],{"id":2282},"예제-5-페이지-전체-상태-바꾸기","예제 5. 페이지 전체 상태 바꾸기",[15,2285,2286,839,2288,2290,2291,2294],{},[24,2287,126],{},[24,2289,284],{},"이나 ",[24,2292,2293],{},"body","에 걸면 페이지 어디에 있는 요소든 조건으로 쓸 수 있다.",[36,2296,2298],{"className":130,"code":2297,"language":132,"meta":41,"style":41},"\u002F* 모달이 열려 있으면 뒤 페이지 스크롤을 막는다 *\u002F\nbody:has(dialog[open]) {\n  overflow: hidden;\n}\n\n\u002F* 다크 모드 체크박스가 체크되면 페이지 전체 색을 바꾼다 *\u002F\nhtml:has(#dark-mode:checked) {\n  color-scheme: dark;\n  background: #111;\n  color: #eee;\n}\n",[24,2299,2300,2305,2325,2337,2341,2345,2350,2363,2375,2386,2398],{"__ignoreMap":41},[45,2301,2302],{"class":47,"line":48},[45,2303,2304],{"class":51},"\u002F* 모달이 열려 있으면 뒤 페이지 스크롤을 막는다 *\u002F\n",[45,2306,2307,2309,2311,2313,2316,2319,2322],{"class":47,"line":55},[45,2308,2293],{"class":144},[45,2310,1671],{"class":62},[45,2312,66],{"class":58},[45,2314,2315],{"class":144},"dialog",[45,2317,2318],{"class":58},"[",[45,2320,2321],{"class":62},"open",[45,2323,2324],{"class":58},"]) {\n",[45,2326,2327,2330,2332,2335],{"class":47,"line":90},[45,2328,2329],{"class":156},"  overflow",[45,2331,160],{"class":58},[45,2333,2334],{"class":156},"hidden",[45,2336,166],{"class":58},[45,2338,2339],{"class":47,"line":118},[45,2340,171],{"class":58},[45,2342,2343],{"class":47,"line":364},[45,2344,767],{"emptyLinePlaceholder":766},[45,2346,2347],{"class":47,"line":378},[45,2348,2349],{"class":51},"\u002F* 다크 모드 체크박스가 체크되면 페이지 전체 색을 바꾼다 *\u002F\n",[45,2351,2352,2354,2356,2358,2361],{"class":47,"line":388},[45,2353,284],{"class":144},[45,2355,1671],{"class":62},[45,2357,66],{"class":58},[45,2359,2360],{"class":62},"#dark-mode:checked",[45,2362,151],{"class":58},[45,2364,2365,2368,2370,2373],{"class":47,"line":403},[45,2366,2367],{"class":156},"  color-scheme",[45,2369,160],{"class":58},[45,2371,2372],{"class":156},"dark",[45,2374,166],{"class":58},[45,2376,2377,2379,2381,2384],{"class":47,"line":427},[45,2378,801],{"class":156},[45,2380,160],{"class":58},[45,2382,2383],{"class":156},"#111",[45,2385,166],{"class":58},[45,2387,2388,2391,2393,2396],{"class":47,"line":441},[45,2389,2390],{"class":156},"  color",[45,2392,160],{"class":58},[45,2394,2395],{"class":156},"#eee",[45,2397,166],{"class":58},[45,2399,2400],{"class":47,"line":455},[45,2401,171],{"class":58},[15,2403,2404,2405,2408],{},"모달을 열 때마다 ",[24,2406,2407],{},"document.body.style.overflow = \"hidden\"","을 넣고, 닫을 때 되돌리는 코드를 지울 수 있다.",[265,2410],{},[10,2412,2414],{"id":2413},"쓰기-전에-알아둘-점","쓰기 전에 알아둘 점",[2416,2417,2419],"h3",{"id":2418},"브라우저-지원","브라우저 지원",[15,2421,2422,2424,2425,2428],{},[24,2423,126],{},"는 Chrome·Edge 105, Safari 15.4, Firefox 121부터 지원된다. 2023년 말부터 주요 브라우저에서 모두 쓸 수 있다. 오래된 브라우저까지 챙겨야 한다면 ",[24,2426,2427],{},"@supports","로 감싸서 지원할 때만 적용한다.",[36,2430,2432],{"className":130,"code":2431,"language":132,"meta":41,"style":41},"@supports selector(:has(a)) {\n  .plan:has(input:checked) { border-color: #f59e0b; }\n}\n",[24,2433,2434,2451,2472],{"__ignoreMap":41},[45,2435,2436,2438,2441,2443,2445,2448],{"class":47,"line":48},[45,2437,2427],{"class":83},[45,2439,2440],{"class":58}," selector(",[45,2442,1671],{"class":62},[45,2444,66],{"class":58},[45,2446,2447],{"class":144},"a",[45,2449,2450],{"class":58},")) {\n",[45,2452,2453,2456,2458,2460,2462,2464,2466,2468,2470],{"class":47,"line":55},[45,2454,2455],{"class":62},"  .plan:has",[45,2457,66],{"class":58},[45,2459,145],{"class":144},[45,2461,148],{"class":62},[45,2463,1271],{"class":58},[45,2465,1274],{"class":156},[45,2467,160],{"class":58},[45,2469,794],{"class":156},[45,2471,592],{"class":58},[45,2473,2474],{"class":47,"line":90},[45,2475,171],{"class":58},[2416,2477,2479],{"id":2478},"주의할-점","주의할 점",[189,2481,2482,2491],{},[192,2483,2484],{},[195,2485,2486,2488],{},[198,2487,2479],{},[198,2489,2490],{},"설명",[205,2492,2493,2509,2520,2531],{},[195,2494,2495,2503],{},[210,2496,2497,2499,2500,2502],{},[24,2498,126],{}," 안에 ",[24,2501,126],{},"는 못 쓴다",[210,2504,2505,2508],{},[24,2506,2507],{},".a:has(.b:has(.c))","는 무효다",[195,2510,2511,2514],{},[210,2512,2513],{},"괄호 안에 의사 요소는 못 쓴다",[210,2515,2516,2519],{},[24,2517,2518],{},":has(::before)","는 동작하지 않는다",[195,2521,2522,2525],{},[210,2523,2524],{},"너무 넓은 범위는 피한다",[210,2526,2527,2530],{},[24,2528,2529],{},"body:has(*:hover)","처럼 페이지 전체를 계속 검사하게 만드는 선택자는 성능에 부담이 된다. 범위를 좁힐수록 좋다",[195,2532,2533,2536],{},[210,2534,2535],{},"하나가 틀리면 규칙 전체가 무시된다",[210,2537,2538,2540],{},[24,2539,126],{}," 안의 목록 중 하나라도 잘못된 선택자가 있으면 그 규칙 전체가 적용되지 않는다",[265,2542],{},[10,2544,2546],{"id":2545},"정리-왜-알아두면-좋은가","정리: 왜 알아두면 좋은가",[189,2548,2549,2561],{},[192,2550,2551],{},[195,2552,2553,2556],{},[198,2554,2555],{},"하고 싶은 것",[198,2557,2558,2560],{},[24,2559,126],{}," 해법",[205,2562,2563,2573,2583,2593,2603,2613,2623],{},[195,2564,2565,2568],{},[210,2566,2567],{},"체크된 항목의 카드 강조",[210,2569,2570],{},[24,2571,2572],{},".card:has(input:checked)",[195,2574,2575,2578],{},[210,2576,2577],{},"입력 에러 시 폼 전체 반응",[210,2579,2580],{},[24,2581,2582],{},"form:has(input:user-invalid)",[195,2584,2585,2588],{},[210,2586,2587],{},"포커스된 입력칸의 라벨 강조",[210,2589,2590],{},[24,2591,2592],{},".field:has(input:focus) label",[195,2594,2595,2598],{},[210,2596,2597],{},"이미지가 있는 카드만 다른 레이아웃",[210,2599,2600],{},[24,2601,2602],{},".card:has(img)",[195,2604,2605,2608],{},[210,2606,2607],{},"앞쪽 형제 선택",[210,2609,2610],{},[24,2611,2612],{},"li:has(+ li:hover)",[195,2614,2615,2618],{},[210,2616,2617],{},"개수에 따른 레이아웃",[210,2619,2620],{},[24,2621,2622],{},".list:has(> li:nth-child(4))",[195,2624,2625,2628],{},[210,2626,2627],{},"모달 열림 시 스크롤 잠금",[210,2629,2630],{},[24,2631,2632],{},"body:has(dialog[open])",[15,2634,2635,2637,2638,2641,2642,2644,2645,2647,2648,2650,2651,2654,2655,2657,2658,2661],{},[24,2636,126],{},"의 진짜 가치는 ",[19,2639,2640],{},"\"상태를 위해 클래스를 붙였다 뗐다 하는 자바스크립트\"를 지울 수 있다","는 데 있다. 상태는 이미 HTML에 있다. 체크됐는지(",[24,2643,148],{},"), 입력이 틀렸는지(",[24,2646,1361],{},"), 마우스가 올라가 있는지(",[24,2649,1604],{},"), 열려 있는지(",[24,2652,2653],{},"[open]","). ",[24,2656,126],{},"는 그 상태를 CSS가 직접 읽게 해준다. 다음에 ",[24,2659,2660],{},"classList.toggle","을 쓰려는 순간, CSS로 되지 않는지 먼저 떠올려보자.",[2663,2664,2665],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .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 .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}",{"title":41,"searchDepth":55,"depth":55,"links":2667},[2668,2669,2670,2671,2672,2673,2674,2678],{"id":12,"depth":55,"text":13},{"id":269,"depth":55,"text":270},{"id":850,"depth":55,"text":851},{"id":1374,"depth":55,"text":1375},{"id":1710,"depth":55,"text":1711},{"id":2282,"depth":55,"text":2283},{"id":2413,"depth":55,"text":2414,"children":2675},[2676,2677],{"id":2418,"depth":90,"text":2419},{"id":2478,"depth":90,"text":2479},{"id":2545,"depth":55,"text":2546},"체크하면 카드가 강조되고, 입력이 틀리면 폼 전체가 반응하고, 별점에 마우스를 올리면 앞의 별까지 칠해진다. 예전엔 자바스크립트가 필요했던 일을 CSS :has() 한 줄로 해결하는 실전 예제를 직접 눌러보며 정리했다.","md",{"date":2682,"field":2683,"tags":2684},"2026.10.08","frontend",[132],"\u002Fko\u002Fcss-has-selector",{"title":5,"description":2679},{"loc":2685},"ko\u002Fcss-has-selector","aoQde6Ax7OXRKY32n6c7YErNYGLRuR-iXdM2Zr3itso",[2691,2702,2705,2713,2720,2728,2735,2746,2754,2762,2774,2782,2791,2797,2803,2810,2817,2824,2831,2837,2846,2855,2861,2868,2874,2881,2888,2896,2903,2910,2919,2926,2933,2940,2946,2954,2961,2969,2975],{"path":2692,"title":2693,"description":2694,"meta":2695},"\u002Fko\u002Fadsense-ads-txt-and-seo-indexing-bug","애드센스 ads.txt 경고를 받고 파헤치다가 찾은 진짜 색인 버그","구글 애드센스가 ads.txt 문제를 알려왔는데, 확인해보니 파일은 멀쩡했다. 대신 서치콘솔 색인 수가 적었던 진짜 원인을 Nuxt Content 슬러그 불일치 버그에서 찾은 과정을 정리한다.",{"date":2696,"field":2697,"tags":2698},"2026.09.17","blog",[2699,2700,2701],"seo","adsense","nuxt-content",{"path":2685,"title":5,"description":2679,"meta":2703},{"date":2682,"field":2683,"tags":2704},[132],{"path":2706,"title":2707,"description":2708,"meta":2709},"\u002Fko\u002Fcss-layouts","CSS Flexbox 완전 정복 가이드","Flexbox를 활용한 반응형 레이아웃 구현 방법과 실전 예제. 수직 정렬, 네비게이션 바, 카드 레이아웃까지 완벽 정리!",{"date":2710,"image":2711,"field":2683,"tags":2712},"2025.01.25","css.jpg",[132],{"path":2714,"title":2715,"description":2716,"meta":2717},"\u002Fko\u002Fcss-login-form","로그인","로그인 UI를 그려보자",{"date":2718,"field":2683,"tags":2719},"2025.03.29",[284,132],{"path":2721,"title":2722,"description":2723,"meta":2724},"\u002Fko\u002Fcsv-parser-cleaning","복잡한 CSV 데이터를 안전하게 파싱하는 방법","JavaScript에서 큰따옴표, 줄바꿈, 쉼표 등 복잡한 CSV 데이터를 안전하게 객체로 변환하는 전체 과정 설명",{"date":2725,"field":2683,"tags":2726},"2025.06.19",[2727],"javascript",{"path":2729,"title":2730,"description":2731,"meta":2732},"\u002Fko\u002Fdebounce-guide","debounce와 throttle이란??","반복되는 함수 호출을 제어하는 기술인 디바운스(debounce)와 쓰로틀(throttle)을 알아보자.",{"date":2733,"field":2683,"tags":2734},"2025.03.26",[2727],{"path":2736,"title":2737,"description":2738,"meta":2739},"\u002Fko\u002Fdeclarative-vs-procedural-programming","선언적 프로그래밍 vs 절차형 프로그래밍","선언적 프로그래밍과 절차형 프로그래밍은 서로 대비되는 대표적인 패러다임이다. 두 패러다임의 차이를 알아보고, 각 패러다임에 해당하는 언어와 Vue\u002FReact의 특징을 살펴보자.",{"date":2740,"field":2741,"tags":2742},"2025.05.24","concepts",[2743,2744,2745],"paradigm","vue","react",{"path":2747,"title":2748,"description":2749,"meta":2750},"\u002Fko\u002Fdesign-guide","Design-Guide","A comprehensive guide on design principles and best practices for developers.",{"date":2751,"field":2683,"tags":2752},"2025.01.19",[2753],"design-system",{"path":2755,"title":2756,"description":2757,"meta":2758},"\u002Fko\u002Fdikw-pyramid","DIKW 피라미드 정리: 데이터가 지혜가 되기까지","Data, Information, Knowledge, Wisdom 4단계로 구성된 DIKW 피라미드 개념을 로그\u002F분석 데이터 같은 실제 개발 사례에 대입해서 정리한다.",{"date":2759,"field":2741,"tags":2760},"2026.09.18",[2761],"data",{"path":2763,"title":2764,"description":2765,"meta":2766},"\u002Fko\u002Fdisplaying-commits-with-github-actions","깃허브 액션으로 커밋 로그를 블로그에 띄우기: Nuxt와 Vercel 활용법","깃허브 액션을 통해 커밋 데이터를 자동으로 수집하고, Nuxt와 Vercel로 UI에 노출하는 방법을 소개합니다.",{"date":2767,"image":2768,"field":2769,"tags":2770},"2025.02.28","github-commits.jpg","infra",[2771,2772,2773],"github-actions","vercel","nuxt",{"path":2775,"title":2776,"description":2777,"meta":2778},"\u002Fko\u002Fdocker-auto-build-run","로컬에서 커밋 시 도커 빌드 후 컨테이너 시작하기","로컬에서 커밋 시 도커 빌드 후 컨테이너 시작하기.",{"date":2779,"field":2769,"tags":2780},"2025.03.10",[2781],"docker",{"path":2783,"title":2784,"description":2785,"meta":2786},"\u002Fko\u002Fhow-to-mount-storage","라즈베리파이4에 저장장치 마운트 시키기","라즈베리파이에 저장장치를 연결하고 파티션 생성부터 포맷, 마운트까지의 전체 과정을 정리한 기록입니다.",{"date":2787,"field":2769,"tags":2788},"2025.07.11",[2789,2790],"raspberry-pi","linux",{"path":2792,"title":2793,"description":2793,"meta":2794},"\u002Fko\u002Fhtml","HTML이란 무엇인가",{"date":2710,"image":2795,"field":2683,"tags":2796},"html.jpg",[284],{"path":2798,"title":2799,"description":2800,"meta":2801},"\u002Fko\u002Fhtml-features-without-javascript","자바스크립트 없이 HTML만으로 되는 것들: 아직도 div로 다 만들고 있다면","아코디언, 팝오버, 모달, 자동완성, 폼 검증, 이미지 지연 로딩까지. 라이브러리 없이 HTML 태그와 속성만으로 해결되는 기능을 예제와 함께 정리했다. 코드는 줄고 접근성은 덤으로 따라온다.",{"date":2682,"field":2683,"tags":2802},[284],{"path":2804,"title":2805,"description":2806,"meta":2807},"\u002Fko\u002Fhtml2","미치광이 카드 컴포넌트","미치광이 카드 컴포넌트는 어떤 느낌일까",{"date":2808,"field":2683,"tags":2809},"2025.03.28",[284,132],{"path":2811,"title":2812,"description":2812,"meta":2813},"\u002Fko\u002Fhttps-guide","Let's Encrypt를 이용해서 HTTPS 적용하기",{"date":2814,"field":2769,"tags":2815},"2025.03.11",[2816],"https",{"path":2818,"title":2819,"description":2820,"meta":2821},"\u002Fko\u002Fjavascript-event-loop","자바스크립트 이벤트 루프 완전 정복: 마이크로태스크와 매크로태스크의 실행 순서","setTimeout(0)보다 Promise.then이 먼저 실행되는 이유는? 콜 스택, 태스크 큐, 마이크로태스크 큐가 어떤 순서로 돌아가는지 예제로 정리하고, 브라우저와 Node.js의 차이, 실무에서 UI 멈춤을 피하는 방법까지 알아보자.",{"date":2682,"field":2741,"tags":2822},[2727,2823],"nodejs",{"path":2825,"title":2826,"description":2827,"meta":2828},"\u002Fko\u002Fjs-data-01","JavaScript 데이터01","JavaScript 데이터 다루는 문제를 풀어보자",{"date":2829,"field":2683,"tags":2830},"2025.03.08",[2727],{"path":2832,"title":2833,"description":2834,"meta":2835},"\u002Fko\u002Fjs-data-02","JavaScript 데이터02","복잡한 데이터 구조를 다루는 실전 문제를 해결해보자.",{"date":2829,"field":2683,"tags":2836},[2727],{"path":2838,"title":2839,"description":2840,"meta":2841},"\u002Fko\u002Fmarkdown-guid","마크다운 문법","마크다운 문법을 알아보자",{"date":2842,"image":2843,"field":2697,"tags":2844},"2025.03.02","markdown.webp",[2845],"markdown",{"path":2847,"title":2848,"description":2849,"meta":2850},"\u002Fko\u002Fminio-guide","MinIO 완벽 가이드: 설치부터 Node.js 연동까지","MinIO는 오픈소스 기반의 고성능 객체 스토리지 서버입니다. AWS S3와 호환되는 API를 제공하여 클라우드 네이티브 애플리케이션을 위한 완벽한 스토리지 솔루션으로 자리잡고 있습니다.",{"date":2851,"field":2852,"tags":2853},"2025.11.23","backend",[2854,2823],"minio",{"path":2856,"title":2857,"description":2858,"meta":2859},"\u002Fko\u002Fnitro-wildcard-redirect-url-migration","글 주소 프리픽스를 통째로 바꿔야 할 때: Nitro wildcard 리다이렉트로 안전하게 옮기기","\u002Fblog\u002F[슬러그]로 쓰던 글 주소를 \u002Fko\u002F[슬러그]로 옮기면서, 기존 링크\u002F북마크를 하나도 안 깨뜨리고 처리한 방법. Nitro routeRules의 와일드카드 리다이렉트 패턴과 실제로 같이 바꿔야 했던 범위를 정리한다.",{"date":2682,"field":2697,"tags":2860},[2773,2699],{"path":2862,"title":2863,"description":2864,"meta":2865},"\u002Fko\u002Fnode-simple-server","Node.js 간단한 서버 만들기","Node.js로 간단한 HTTP 서버를 만드는 방법",{"date":2866,"field":2852,"tags":2867},"2025.01.30",[2823],{"path":2869,"title":2870,"description":2871,"meta":2872},"\u002Fko\u002Fnuxt-colormode","Nuxt3 다크모드 적용하기","Nuxt3에 다크모드 적용하여 관리하기",{"date":2718,"field":2683,"tags":2873},[2773],{"path":2875,"title":2876,"description":2877,"meta":2878},"\u002Fko\u002Fnuxt-content-vercel-empty-query-sqlite","배포만 하면 블로그 글이 하나도 안 보이던 버그: Nuxt Content와 Vercel의 \u002Ftmp 문제","로컬에서는 멀쩡한데 Vercel에 올리면 queryCollection()이 빈 배열만 반환하던 버그를 추적한 과정. 원인은 @nuxt\u002Fcontent가 Vercel의 읽기 전용 파일시스템을 지원하기 전 버전에 고정돼 있었던 것.",{"date":2879,"field":2697,"tags":2880},"2026.10.07",[2701,2772],{"path":2882,"title":2883,"description":2884,"meta":2885},"\u002Fko\u002Fnuxt-runtimeconfig-appconfig","Nuxt 설정 runtimeConfig vs app.config 완전 정복","Nuxt3에서 전역 설정을 어떻게 관리하고, 어떤 기준으로 나누어야 할까? 다크모드 예시부터 실무 기준 설정 전략까지 총정리",{"date":2886,"field":2683,"tags":2887},"2025.06.16",[2773],{"path":2889,"title":2890,"description":2891,"meta":2892},"\u002Fko\u002Fnuxt-useasyncdata-cache-error","useAsyncData 캐싱 문제와 Key 동적 설정으로 해결하기","이전 포스트의 데이터가 계속 출력되는 캐싱 관련 문제를 해결하는 방법",{"date":2893,"field":2683,"tags":2894,"image":2895},"2025.01.24",[2773],"cache.jpg",{"path":2897,"title":2898,"description":2899,"meta":2900},"\u002Fko\u002Fnuxt-vercel-sql-error","Nuxt 3에서 Vercel 배포 시 발생한 md 파일 로딩 문제 해결기","Nuxt 3 웹 어플리케이션에서 Vercel 배포 환경에서 발생한 md 파일 로딩 문제를 분석하고 해결한 방법에 대해 공유합니다.",{"date":2893,"image":2901,"field":2683,"tags":2902},"ssg.jpg",[2773,2701,2772],{"path":2904,"title":2905,"description":2906,"meta":2907},"\u002Fko\u002Fnuxt3-pinia-guide","nuxt3 Pinia 적용하기","Nuxt3 프로젝트에서 Pinia를 적용해보자 @pinia\u002Fnuxt",{"date":2718,"field":2683,"tags":2908},[2773,2909],"pinia",{"path":2911,"title":2912,"description":2913,"meta":2914},"\u002Fko\u002Fnuxt3-unit-test","Nuxt 프로젝트에 유닛 테스트 코드 추가하기","Nuxt 3 프로젝트에서 Vitest를 활용한 테스트 환경 설정과 유틸리티 함수 테스트 방법",{"date":2915,"image":2916,"field":2683,"tags":2917},"2025.03.07","unit-test.jpg",[2773,2918],"vitest",{"path":2920,"title":2921,"description":2922,"meta":2923},"\u002Fko\u002Fpm2-guide","프로세스 매니저 PM2","배포환경에서 유용하게 사용할 수 있는 프로세스 매니저를 알아보자.",{"date":2779,"field":2769,"tags":2924},[2925,2823],"pm2",{"path":2927,"title":2928,"description":2929,"meta":2930},"\u002Fko\u002Freact-createportal","React createPortal 완벽 가이드","React의 `createPortal`을 활용한 UI 구현법, 활용 사례, 장단점 및 코드 예제를 소개합니다.",{"date":2931,"field":2683,"tags":2932},"2025.01.27",[2745],{"path":2934,"title":2935,"description":2936,"meta":2937},"\u002Fko\u002Frouting","Routing","Nuxt file-system routing creates a route for every file in the pages\u002F directory.",{"date":2938,"field":2683,"tags":2939},"2025.01.22",[2773],{"path":2941,"title":2942,"description":2943,"meta":2944},"\u002Fko\u002Fsemantic-html-common-mistakes","의미 있는 HTML 쓰기: 대부분이 하는 시맨틱 HTML 실수 7가지","화면에서는 똑같아 보여도 div로 만든 버튼, label 없는 입력칸, 크기로 고른 제목 태그는 키보드 사용자와 스크린 리더, 검색 엔진에게 전혀 다르게 보인다. 직접 눌러볼 수 있는 예제로 흔한 시맨틱 HTML 실수와 고치는 법을 정리했다.",{"date":2682,"field":2683,"tags":2945},[284],{"path":2947,"title":2948,"description":2949,"meta":2950},"\u002Fko\u002Ftanstack-query-guide","TanStack Query란 무엇인가?","TanStack Query가 웹 애플리케이션에서 데이터 가져오기, 캐싱, 상태 관리를 어떻게 개선하는지 알아본다.",{"date":2951,"field":2683,"tags":2952},"2025.04.15",[2745,2953],"tanstack-query",{"path":2955,"title":2956,"description":2957,"meta":2958},"\u002Fko\u002Fuse-context","리엑트 상태관리 useContext()","React에서 `useContext`를 사용하면 컴포넌트 트리에 깊이 중첩된 컴포넌트에서도 prop drilling 없이 상태를 공유할 수 있습니다. 이 글에서는 `useContext`의 개념, 사용 방법, 장단점, 그리고 실전 예제를 함께 알아보겠습니다.",{"date":2959,"field":2683,"tags":2960},"2025.02.03",[2745],{"path":2962,"title":2963,"description":2964,"meta":2965},"\u002Fko\u002Fvite-plugin-style-inject","리엑티 컴포넌트를 패키지 빌드 시 CSS 같이 적용시키기","dist에 css에 파일이 생성되었으나, 적용 안 되는 이유를 알아보고 에러를 처리하자",{"date":2966,"field":2683,"tags":2967},"2025.04.06",[2968,2745],"vite",{"path":2970,"title":2971,"description":2972,"meta":2973},"\u002Fko\u002Fvue-component-guide","Vue의 \u003Ccomponent :is \u002F> 완벽 정리: 동적 렌더링의 모든 것","Vue에서 \u003Ccomponent :is \u002F>를 활용한 동적 컴포넌트 렌더링 방법과 VNode, 컴포넌트 객체의 차이를 실전 예제로 이해하기",{"date":2710,"field":2683,"tags":2974},[2744],{"path":2976,"title":2977,"description":2978,"meta":2979},"\u002Fko\u002Fvue-query-ref-value-trap-nuxt3","Vue Query에서 데이터가 안 보이는 이유? Nuxt 3와 Ref 구조의 함정","Nuxt 3 + vue-query를 사용할 때 API 데이터가 `v-if` 조건문에서 제대로 표시되지 않는 현상을 해결한 경험을 바탕으로, Ref 구조와 template 디렉티브에서의 차이를 자세히 설명합니다.",{"date":2980,"field":2683,"tags":2981},"2025.06.08",[2773,2744,2953],1791459768133]