[{"data":1,"prerenderedAt":1137},["ShallowReactive",2],{"blog-post-\u002Fko\u002Fload-more-button-pagination-seo":3,"blog-related-source":763},{"id":4,"title":5,"body":6,"description":751,"extension":752,"meta":753,"navigation":291,"path":758,"seo":759,"sitemap":760,"stem":761,"__hash__":762},"ko\u002Fko\u002Fload-more-button-pagination-seo.md","\"더 보기\" 버튼 페이지네이션과 SEO: 구글 크롤러는 버튼을 누르지 않는다",{"type":7,"value":8,"toc":741},"minimark",[9,14,23,30,87,94,97,101,104,121,204,215,217,221,224,372,392,394,398,405,457,464,479,481,485,549,559,562,579,593,595,599,606,616,628,641,643,646,700,707,711,737],[10,11,13],"h2",{"id":12},"목록을-바꿨을-뿐인데","목록을 바꿨을 뿐인데",[15,16,17,18,22],"p",{},"이 블로그는 10월 7일에 목록 화면을 새로 만들면서 글을 처음에 10개만 보여주고, 나머지는 ",[19,20,21],"strong",{},"\"더 보기\" 버튼","을 누르면 10개씩 더 보이게 바꿨다. 화면도 깔끔해지고 사용자 입장에서는 아무 문제가 없었다.",[15,24,25,26,29],{},"그런데 서치 콘솔에서 색인 상태를 조사하다가, 목록 페이지의 ",[19,27,28],{},"서버 HTML","을 열어보고 이걸 발견했다.",[31,32,33,49],"table",{},[34,35,36],"thead",{},[37,38,39,43,46],"tr",{},[40,41,42],"th",{},"목록",[40,44,45],{},"전체 글",[40,47,48],{},"HTML에 링크가 있는 글",[50,51,52,71],"tbody",{},[37,53,54,63,66],{},[55,56,57,58,62],"td",{},"한국어 (",[59,60,61],"code",{},"\u002F",")",[55,64,65],{},"49개",[55,67,68],{},[19,69,70],{},"10개",[37,72,73,79,82],{},[55,74,75,76,62],{},"영어 (",[59,77,78],{},"\u002Fen",[55,80,81],{},"17개",[55,83,84],{},[19,85,86],{},"11개",[15,88,89,90,93],{},"나머지 글은 \"더 보기\"를 눌러야 화면에 나오는데, ",[19,91,92],{},"구글 크롤러는 그 버튼을 누르지 않는다."," 사람에게는 49개가 다 보이는 목록이, 크롤러에게는 10개짜리 목록이었던 것이다.",[95,96],"hr",{},[10,98,100],{"id":99},"구글-크롤러는-정말-버튼을-누르지-않을까","구글 크롤러는 정말 버튼을 누르지 않을까",[15,102,103],{},"\"버튼을 안 누른다\"는 말은 블로그마다 흔히 나오지만, 근거를 구글 문서에서 직접 확인해 봤다.",[105,106,107],"blockquote",{},[15,108,109,110,120],{},"구글은 ",[19,111,112,115,116,119],{},[59,113,114],{},"href"," 속성이 있는 ",[59,117,118],{},"\u003Ca>"," 요소","로 된 링크만 안정적으로 따라간다. 클릭해야 내용이 바뀌는 자바스크립트 동작은 실행하지 않는다.",[31,122,123,133],{},[34,124,125],{},[37,126,127,130],{},[40,128,129],{},"출처",[40,131,132],{},"내용 (요약)",[50,134,135,157,173,191],{},[37,136,137,146],{},[55,138,139],{},[140,141,145],"a",{"href":142,"rel":143},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Flinks-crawlable?hl=ko",[144],"nofollow","Google 검색 센터: 링크 권장사항",[55,147,148,149,115,151,153,154,156],{},"일반적으로 구글은 ",[59,150,114],{},[59,152,118],{}," 요소인 링크만 크롤링할 수 있다. ",[59,155,114],{},"가 없거나 스크립트 이벤트로 링크처럼 동작하는 요소에서는 URL을 안정적으로 뽑아내지 못한다",[37,158,159,166],{},[55,160,161],{},[140,162,165],{"href":163,"rel":164},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Fecommerce\u002Fpagination-and-incremental-page-loading?hl=ko",[144],"Google 검색 센터: 페이지로 나누기, 점진적 페이지 로드",[55,167,168,169,172],{},"\"더 보기\" 버튼과 무한 스크롤을 점진적 로드 방식으로 소개하면서, 검색엔진이 모든 콘텐츠를 찾을 수 있도록 각 페이지를 ",[19,170,171],{},"링크로 순서대로 연결","하라고 권장한다",[37,174,175,182],{},[55,176,177],{},[140,178,181],{"href":179,"rel":180},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Fjavascript\u002Fjavascript-seo-basics?hl=ko",[144],"Google 검색 센터: 자바스크립트 SEO 기본사항",[55,183,184,185,187,188,190],{},"Googlebot은 자바스크립트를 렌더링하지만, 링크는 ",[59,186,114],{},"가 있는 ",[59,189,118],{},"에서 찾는다",[37,192,193,201],{},[55,194,195,200],{},[140,196,199],{"href":197,"rel":198},"https:\u002F\u002Fwww.searchenginejournal.com\u002Fgooglebot-doesnt-click-on-buttons-what-to-use-instead\u002F400242\u002F",[144],"Search Engine Journal: Googlebot Doesn't Click on Buttons"," (영문 외부 기사)",[55,202,203],{},"구글의 마틴 스플릿(Martin Splitt)이 Googlebot은 버튼을 클릭하지 않으니, 버튼 대신 일반 링크를 쓰라고 답한 내용을 다룬다",[15,205,206,207,210,211,214],{},"여기서 헷갈리기 쉬운 점이 있다. Googlebot은 ",[19,208,209],{},"자바스크립트를 실행해서 화면을 렌더링한다."," 그래서 \"자바스크립트로 만든 내용은 구글이 못 본다\"는 말은 틀리다. 정확히는 ",[19,212,213],{},"페이지를 처음 열었을 때 렌더링되는 내용은 보지만, 사용자가 무언가를 눌러야 나오는 내용은 보지 못한다."," \"더 보기\" 뒤의 글은 바로 이 경우다.",[95,216],{},[10,218,220],{"id":219},"버튼과-링크-무엇이-다른가","버튼과 링크, 무엇이 다른가",[15,222,223],{},"화면에서는 똑같이 생긴 \"더 보기\"라도 HTML이 다르면 크롤러에게는 완전히 다르게 보인다.",[225,226,231],"pre",{"className":227,"code":228,"language":229,"meta":230,"style":230},"language-html shiki shiki-themes github-light github-dark","\u003C!-- ❌ 크롤러가 따라갈 주소가 없다. 클릭해야만 글이 추가된다 -->\n\u003Cbutton type=\"button\" onclick=\"loadMore()\">더 보기\u003C\u002Fbutton>\n\n\u003C!-- ❌ 링크처럼 보이지만 href가 없어서 주소를 뽑아낼 수 없다 -->\n\u003Cspan class=\"link\" onclick=\"location.href='\u002Fpage\u002F2'\">더 보기\u003C\u002Fspan>\n\n\u003C!-- ✅ 크롤러가 \u002Fpage\u002F2 로 가서 다음 글 목록을 읽을 수 있다 -->\n\u003Ca href=\"\u002Fpage\u002F2\">더 보기\u003C\u002Fa>\n","html","",[59,232,233,242,286,293,299,340,345,351],{"__ignoreMap":230},[234,235,238],"span",{"class":236,"line":237},"line",1,[234,239,241],{"class":240},"sJ8bj","\u003C!-- ❌ 크롤러가 따라갈 주소가 없다. 클릭해야만 글이 추가된다 -->\n",[234,243,245,249,253,257,260,264,267,269,272,275,278,281,283],{"class":236,"line":244},2,[234,246,248],{"class":247},"sVt8B","\u003C",[234,250,252],{"class":251},"s9eBZ","button",[234,254,256],{"class":255},"sScJk"," type",[234,258,259],{"class":247},"=",[234,261,263],{"class":262},"sZZnC","\"button\"",[234,265,266],{"class":255}," onclick",[234,268,259],{"class":247},[234,270,271],{"class":262},"\"",[234,273,274],{"class":255},"loadMore",[234,276,277],{"class":262},"()\"",[234,279,280],{"class":247},">더 보기\u003C\u002F",[234,282,252],{"class":251},[234,284,285],{"class":247},">\n",[234,287,289],{"class":236,"line":288},3,[234,290,292],{"emptyLinePlaceholder":291},true,"\n",[234,294,296],{"class":236,"line":295},4,[234,297,298],{"class":240},"\u003C!-- ❌ 링크처럼 보이지만 href가 없어서 주소를 뽑아낼 수 없다 -->\n",[234,300,302,304,306,309,311,314,316,318,320,323,326,328,331,334,336,338],{"class":236,"line":301},5,[234,303,248],{"class":247},[234,305,234],{"class":251},[234,307,308],{"class":255}," class",[234,310,259],{"class":247},[234,312,313],{"class":262},"\"link\"",[234,315,266],{"class":255},[234,317,259],{"class":247},[234,319,271],{"class":262},[234,321,322],{"class":247},"location",[234,324,325],{"class":262},".",[234,327,114],{"class":247},[234,329,259],{"class":330},"szBVR",[234,332,333],{"class":262},"'\u002Fpage\u002F2'\"",[234,335,280],{"class":247},[234,337,234],{"class":251},[234,339,285],{"class":247},[234,341,343],{"class":236,"line":342},6,[234,344,292],{"emptyLinePlaceholder":291},[234,346,348],{"class":236,"line":347},7,[234,349,350],{"class":240},"\u003C!-- ✅ 크롤러가 \u002Fpage\u002F2 로 가서 다음 글 목록을 읽을 수 있다 -->\n",[234,352,354,356,358,361,363,366,368,370],{"class":236,"line":353},8,[234,355,248],{"class":247},[234,357,140],{"class":251},[234,359,360],{"class":255}," href",[234,362,259],{"class":247},[234,364,365],{"class":262},"\"\u002Fpage\u002F2\"",[234,367,280],{"class":247},[234,369,140],{"class":251},[234,371,285],{"class":247},[15,373,374,375,378,379,382,383,386,387,391],{},"이 블로그의 \"더 보기\"는 첫 번째 경우였다. ",[59,376,377],{},"\u003Cbutton>","은 어떤 동작을 실행하는 요소이고, 어딘가로 ",[19,380,381],{},"이동하는"," 요소는 ",[59,384,385],{},"\u003Ca href>","다. 둘의 차이는 ",[140,388,390],{"href":389},"\u002Fko\u002Fsemantic-html-common-mistakes","의미 있는 HTML 쓰기"," 글에도 정리했다.",[95,393],{},[10,395,397],{"id":396},"내-사이트도-같은-문제가-있는지-확인하기","내 사이트도 같은 문제가 있는지 확인하기",[15,399,400,401,404],{},"가장 간단한 방법은 ",[19,402,403],{},"서버가 보내는 HTML에 글 링크가 몇 개 있는지 세는 것","이다.",[225,406,410],{"className":407,"code":408,"language":409,"meta":230,"style":230},"language-bash shiki shiki-themes github-light github-dark","# 목록 페이지 HTML에서 글 주소(\u002Fko\u002F...)로 가는 링크 수 세기\ncurl -s https:\u002F\u002Fiambilly-blog.vercel.app\u002F | grep -o 'href=\"\u002Fko\u002F[^\"]*\"' | sort -u | wc -l\n","bash",[59,411,412,417],{"__ignoreMap":230},[234,413,414],{"class":236,"line":237},[234,415,416],{"class":240},"# 목록 페이지 HTML에서 글 주소(\u002Fko\u002F...)로 가는 링크 수 세기\n",[234,418,419,422,426,429,432,435,438,441,443,446,449,451,454],{"class":236,"line":244},[234,420,421],{"class":255},"curl",[234,423,425],{"class":424},"sj4cs"," -s",[234,427,428],{"class":262}," https:\u002F\u002Fiambilly-blog.vercel.app\u002F",[234,430,431],{"class":330}," |",[234,433,434],{"class":255}," grep",[234,436,437],{"class":424}," -o",[234,439,440],{"class":262}," 'href=\"\u002Fko\u002F[^\"]*\"'",[234,442,431],{"class":330},[234,444,445],{"class":255}," sort",[234,447,448],{"class":424}," -u",[234,450,431],{"class":330},[234,452,453],{"class":255}," wc",[234,455,456],{"class":424}," -l\n",[15,458,459,460,463],{},"이 숫자가 전체 글 수보다 훨씬 작다면, 나머지 글은 링크로는 닿을 수 없다는 뜻이다. 자바스크립트로 렌더링하는 사이트라면 서치 콘솔의 ",[19,461,462],{},"URL 검사 → 크롤링된 페이지 보기","에서 구글이 렌더링한 HTML을 확인할 수도 있다. 어느 쪽이든 버튼을 눌러야 나오는 글은 거기에 없다.",[15,465,466,467,470,471,474,475,478],{},"또 하나의 확인 방법은 ",[19,468,469],{},"링크만 따라가는 크롤러를 돌려 보는 것","이다. 이 블로그는 Nuxt의 빌드 크롤러(",[59,472,473],{},"nitro.prerender.crawlLinks",")가 홈에서 링크를 따라가며 페이지를 미리 만드는데, 고치기 전에는 이 크롤러가 찾은 페이지가 ",[19,476,477],{},"35개","뿐이었다. 글 상세 페이지의 \"함께 읽으면 좋은 글\" 링크까지 다 따라가도 그 정도였다.",[95,480],{},[10,482,484],{"id":483},"해결-방법-세-가지","해결 방법 세 가지",[31,486,487,500],{},[34,488,489],{},[37,490,491,494,497],{},[40,492,493],{},"방법",[40,495,496],{},"어떻게",[40,498,499],{},"어울리는 경우",[50,501,502,523,538],{},[37,503,504,507,520],{},[55,505,506],{},"번호 페이지네이션",[55,508,509,512,513,516,517,519],{},[59,510,511],{},"\u002Fpage\u002F2",", ",[59,514,515],{},"\u002Fpage\u002F3","처럼 페이지마다 실제 주소를 만들고 ",[59,518,385],{},"로 연결",[55,521,522],{},"글이 수백 개 이상인 사이트",[37,524,525,528,535],{},[55,526,527],{},"링크로 된 \"더 보기\"",[55,529,530,531,534],{},"\"더 보기\"를 ",[59,532,533],{},"\u003Ca href=\"\u002Fpage\u002F2\">","로 만들고, 자바스크립트가 있으면 클릭을 가로채서 그 자리에 글을 붙인다",[55,536,537],{},"지금 화면(더 보기)을 유지하고 싶을 때",[37,539,540,543,546],{},[55,541,542],{},"전체 글 목록 페이지",[55,544,545],{},"모든 글을 링크로 나열한 페이지를 하나 만들고, 모든 페이지에서 그곳으로 링크",[55,547,548],{},"글이 수십~수백 개인 블로그",[15,550,551,552,555,556,558],{},"두 번째 방법은 ",[19,553,554],{},"자바스크립트가 없어도 동작하는 링크를 먼저 만들고, 그 위에 자바스크립트로 더 나은 경험을 얹는"," 방식이다. 이때 ",[59,557,511],{}," 주소로 직접 들어가도 서버가 2페이지 글 목록을 실제로 보여줘야 한다. 주소만 있고 내용이 없으면 크롤러가 가도 소용이 없다.",[15,560,561],{},"이 블로그는 세 번째 방법을 골랐다. 글이 49개라 한 페이지에 다 들어가고, 목록 화면은 그대로 둘 수 있기 때문이다.",[563,564,565,576],"ul",{},[566,567,568,571,572,575],"li",{},[59,569,570],{},"\u002Farchive","(한국어), ",[59,573,574],{},"\u002Fen\u002Farchive","(영어): 모든 글을 연도별 일반 링크로 나열",[566,577,578],{},"모든 페이지 푸터와 목록의 \"더 보기\" 아래에 이 페이지로 가는 링크 추가",[15,580,581,582,585,586,588,589,592],{},"다시 빌드하자 크롤러가 ",[19,583,584],{},"한국어 49\u002F49개, 영어 17\u002F17개 글에 모두 도달","했고, 미리 만든 페이지도 35개에서 159개로 늘었다. URL 검사에서도 ",[59,587,570],{},"가 ",[19,590,591],{},"\"URL이 Google에 등록되어 있음\""," 으로 확인됐다.",[95,594],{},[10,596,598],{"id":597},"사이트맵이-있으면-괜찮지-않나","사이트맵이 있으면 괜찮지 않나?",[15,600,601,602,605],{},"이 블로그는 사이트맵에 모든 글이 들어가 있었다. 그러니 \"더 보기\" 문제가 있어도 구글은 사이트맵으로 글을 ",[19,603,604],{},"발견","할 수는 있었다.",[15,607,608,609,611,612,615],{},"다만 사이트맵은 \"이런 주소들이 있다\"는 ",[19,610,42],{},"일 뿐이다. 사이트 안에서 링크로 연결되지 않은 글은 구글이 그 글이 얼마나 중요한지 판단할 단서가 적다. 구글 문서도 사이트맵과 별개로 ",[19,613,614],{},"모든 콘텐츠를 링크로 찾을 수 있게"," 하라고 권장한다. 사이트맵은 링크 구조를 보완하는 것이지 대신하는 것이 아니다.",[617,618,619],"warning",{},[105,620,621],{},[15,622,623,624,627],{},"한 가지 분명히 해 둘 점이 있다. 이 \"더 보기\" 문제는 ",[19,625,626],{},"10월 7일부터 10월 13일까지"," 있었다. 그 전부터 이 블로그의 색인 수가 적었던 건 다른 원인(예전 사이트맵 버그, 서버 에러 기간, 주소 이전)이 컸다. 그러니 \"더 보기 버튼 때문에 색인이 안 됐다\"고 말할 수는 없다. 다만 이 상태로 계속 뒀다면 새로 쓴 글들이 링크로는 발견되지 않는 구조였기 때문에, 원인 조사 중에 발견하고 바로 고쳤다.",[15,629,630,631,635,636,640],{},"예전 사이트맵 버그는 ",[140,632,634],{"href":633},"\u002Fko\u002Fadsense-ads-txt-and-seo-indexing-bug","애드센스 ads.txt 경고를 받고 파헤치다가 찾은 진짜 색인 버그"," 글에, 서치 콘솔 상태 읽는 법은 ",[140,637,639],{"href":638},"\u002Fko\u002Fsearch-console-discovered-crawled-not-indexed","서치 콘솔 \"발견됨 \u002F 크롤링됨 - 현재 색인이 생성되지 않음\" 차이"," 글에 정리했다.",[95,642],{},[10,644,645],{"id":645},"정리",[31,647,648,658],{},[34,649,650],{},[37,651,652,655],{},[40,653,654],{},"기억할 것",[40,656,657],{},"내용",[50,659,660,672,680,688],{},[37,661,662,665],{},[55,663,664],{},"크롤러가 따라가는 것",[55,666,667,187,669,671],{},[59,668,114],{},[59,670,118],{}," 링크",[37,673,674,677],{},[55,675,676],{},"크롤러가 하지 않는 것",[55,678,679],{},"버튼 클릭, 스크롤 같은 사용자 동작",[37,681,682,685],{},[55,683,684],{},"확인 방법",[55,686,687],{},"서버 HTML(또는 URL 검사의 렌더링된 HTML)에 글 링크가 몇 개 있는지 센다",[37,689,690,693],{},[55,691,692],{},"해결",[55,694,695,696,699],{},"번호 페이지네이션, 링크로 된 \"더 보기\", 전체 글 목록 페이지 중 하나로 ",[19,697,698],{},"모든 글이 링크로 닿게"," 한다",[15,701,702,703,706],{},"화면을 바꿀 때는 사람이 보는 화면만이 아니라 ",[19,704,705],{},"크롤러가 보는 HTML","도 함께 바뀐다. 목록이나 메뉴처럼 다른 페이지로 이어지는 부분을 바꿨다면, 바꾼 뒤에 링크 수를 한 번 세어 보는 습관이 도움이 된다.",[10,708,710],{"id":709},"참고-자료","참고 자료",[563,712,713,719,725,731],{},[566,714,715],{},[140,716,718],{"href":142,"rel":717},[144],"Google 검색 센터: Google 링크 권장사항",[566,720,721],{},[140,722,724],{"href":163,"rel":723},[144],"Google 검색 센터: 페이지로 나누기, 점진적 페이지 로드 및 이러한 요소가 Google 검색에 미치는 영향",[566,726,727],{},[140,728,730],{"href":179,"rel":729},[144],"Google 검색 센터: 자바스크립트 SEO 기본사항 이해하기",[566,732,733],{},[140,734,736],{"href":197,"rel":735},[144],"Search Engine Journal: Googlebot Doesn't Click on Buttons, What to Use Instead (영문)",[738,739,740],"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 .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 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}",{"title":230,"searchDepth":244,"depth":244,"links":742},[743,744,745,746,747,748,749,750],{"id":12,"depth":244,"text":13},{"id":99,"depth":244,"text":100},{"id":219,"depth":244,"text":220},{"id":396,"depth":244,"text":397},{"id":483,"depth":244,"text":484},{"id":597,"depth":244,"text":598},{"id":645,"depth":244,"text":645},{"id":709,"depth":244,"text":710},"목록을 '더 보기' 버튼으로 바꿨더니 글 49개 중 10개만 링크로 연결돼 있었다. 구글 크롤러가 버튼을 누르지 않는 이유를 구글 문서로 확인하고, 모든 글을 \u003Ca href> 링크로 잇는 방법을 정리했다.","md",{"date":754,"field":755,"tags":756},"2026.10.14","blog",[757,229],"seo","\u002Fko\u002Fload-more-button-pagination-seo",{"title":5,"description":751},{"loc":758},"ko\u002Fload-more-button-pagination-seo","3cWuKbMpyA5_jGJJtgt4y2zAcEYpPfmD1UvVehhE70w",[764,771,781,789,797,805,812,820,827,838,846,854,866,874,883,890,896,903,909,915,923,929,935,941,947,954,960,967,970,979,987,993,1000,1007,1013,1020,1027,1035,1042,1049,1055,1064,1070,1077,1084,1089,1094,1102,1109,1117,1123,1130],{"path":633,"title":634,"description":765,"meta":766},"구글 애드센스가 ads.txt 문제를 알려왔는데, 확인해보니 파일은 멀쩡했다. 대신 서치콘솔 색인 수가 적었던 진짜 원인을 Nuxt Content 슬러그 불일치 버그에서 찾은 과정을 정리한다.",{"date":767,"field":755,"tags":768},"2026.09.17",[757,769,770],"adsense","nuxt-content",{"path":772,"title":773,"description":774,"meta":775},"\u002Fko\u002Fapi-error-response-format","API 에러 응답 형식 설계하기: 제각각인 에러 응답을 RFC 9457 Problem Details로 통일하는 법","API마다 제각각인 에러 응답을 RFC 9457 Problem Details로 통일하는 법. 필드별 검증 에러, traceId, 내부 정보 숨기기, Node.js 단일 에러 처리까지 정리했다.",{"date":776,"field":777,"tags":778},"2026.10.08","backend",[779,780],"api","http",{"path":782,"title":783,"description":784,"meta":785},"\u002Fko\u002Fcors-guide","CORS 에러 완전 정리: 왜 막히는지부터 preflight, credentials까지 올바르게 해결하기","CORS 에러는 서버가 아니라 브라우저가 막은 것이다. 같은 출처 정책, preflight, credentials, Expose-Headers와 잘못된 해결법까지 실제 브라우저로 확인하며 정리했다.",{"date":776,"field":777,"tags":786},[780,787,788],"web-security","nodejs",{"path":790,"title":791,"description":792,"meta":793},"\u002Fko\u002Fcss-has-selector","CSS :has() 선택자 실전 가이드: 자바스크립트 없이 부모를 선택하는 법","체크하면 카드가 강조되고, 입력이 틀리면 폼 전체가 반응하고, 별점에 마우스를 올리면 앞의 별까지 칠해진다. 예전엔 자바스크립트가 필요했던 일을 CSS :has() 한 줄로 해결하는 실전 예제를 직접 눌러보며 정리했다.",{"date":776,"field":794,"tags":795},"frontend",[796],"css",{"path":798,"title":799,"description":800,"meta":801},"\u002Fko\u002Fcss-layouts","CSS Flexbox 완전 정복 가이드","Flexbox를 활용한 반응형 레이아웃 구현 방법과 실전 예제. 수직 정렬, 네비게이션 바, 카드 레이아웃까지 완벽 정리!",{"date":802,"image":803,"field":794,"tags":804},"2025.01.25","css.jpg",[796],{"path":806,"title":807,"description":808,"meta":809},"\u002Fko\u002Fcss-login-form","로그인","HTML과 CSS로 로그인 카드 UI를 만들어보는 예제. 라벨, 아이콘이 들어간 입력칸, 버튼을 BEM 클래스로 구성하고 결과 화면을 코드와 함께 확인할 수 있다.",{"date":810,"field":794,"tags":811},"2025.03.29",[229,796],{"path":813,"title":814,"description":815,"meta":816},"\u002Fko\u002Fcsv-parser-cleaning","복잡한 CSV 데이터를 안전하게 파싱하는 방법","JavaScript에서 큰따옴표, 줄바꿈, 쉼표 등 복잡한 CSV 데이터를 안전하게 객체로 변환하는 전체 과정 설명",{"date":817,"field":794,"tags":818},"2025.06.19",[819],"javascript",{"path":821,"title":822,"description":823,"meta":824},"\u002Fko\u002Fdebounce-guide","debounce와 throttle이란??","반복되는 함수 호출을 제어하는 기술인 디바운스(debounce)와 쓰로틀(throttle)을 알아보자.",{"date":825,"field":794,"tags":826},"2025.03.26",[819],{"path":828,"title":829,"description":830,"meta":831},"\u002Fko\u002Fdeclarative-vs-procedural-programming","선언적 프로그래밍 vs 절차형 프로그래밍","선언적 프로그래밍과 절차형 프로그래밍은 서로 대비되는 대표적인 패러다임이다. 두 패러다임의 차이를 알아보고, 각 패러다임에 해당하는 언어와 Vue\u002FReact의 특징을 살펴보자.",{"date":832,"field":833,"tags":834},"2025.05.24","concepts",[835,836,837],"paradigm","vue","react",{"path":839,"title":840,"description":841,"meta":842},"\u002Fko\u002Fdesign-guide","Design-Guide","학습 서비스 EduTrack을 예로 든 웹 디자인 가이드. 색상 팔레트, 타이포그래피, 아이콘, 반응형 레이아웃과 UI 컴포넌트, 접근성까지 화면 설계 기준을 정리했다.",{"date":843,"field":794,"tags":844},"2025.01.19",[845],"design-system",{"path":847,"title":848,"description":849,"meta":850},"\u002Fko\u002Fdikw-pyramid","DIKW 피라미드 정리: 데이터가 지혜가 되기까지","Data, Information, Knowledge, Wisdom 4단계로 구성된 DIKW 피라미드 개념을 로그\u002F분석 데이터 같은 실제 개발 사례에 대입해서 정리한다.",{"date":851,"field":833,"tags":852},"2026.09.18",[853],"data",{"path":855,"title":856,"description":857,"meta":858},"\u002Fko\u002Fdisplaying-commits-with-github-actions","깃허브 액션으로 커밋 로그를 블로그에 띄우기: Nuxt와 Vercel 활용법","깃허브 액션을 통해 커밋 데이터를 자동으로 수집하고, Nuxt와 Vercel로 UI에 노출하는 방법을 소개합니다.",{"date":859,"image":860,"field":861,"tags":862},"2025.02.28","github-commits.jpg","infra",[863,864,865],"github-actions","vercel","nuxt",{"path":867,"title":868,"description":869,"meta":870},"\u002Fko\u002Fdocker-auto-build-run","로컬에서 커밋 시 도커 빌드 후 컨테이너 시작하기","로컬에서 커밋할 때마다 Docker 이미지를 자동으로 빌드하고 컨테이너를 다시 띄우는 방법. 셸 스크립트를 작성하고 Git 훅으로 연결하는 과정을 정리했다.",{"date":871,"field":861,"tags":872},"2025.03.10",[873],"docker",{"path":875,"title":876,"description":877,"meta":878},"\u002Fko\u002Fhow-to-mount-storage","라즈베리파이4에 저장장치 마운트 시키기","라즈베리파이에 저장장치를 연결하고 파티션 생성부터 포맷, 마운트까지의 전체 과정을 정리한 기록입니다.",{"date":879,"field":861,"tags":880},"2025.07.11",[881,882],"raspberry-pi","linux",{"path":884,"title":885,"description":886,"meta":887},"\u002Fko\u002Fhtml","HTML이란 무엇인가","HTML의 역할과 기본 문서 구조를 정리하고, 단일 페이지, 내비게이션 포함, 그리드 레이아웃처럼 자주 쓰는 페이지 뼈대를 예제 코드로 살펴본다.",{"date":802,"image":888,"field":794,"tags":889},"html.jpg",[229],{"path":891,"title":892,"description":893,"meta":894},"\u002Fko\u002Fhtml-features-without-javascript","자바스크립트 없이 HTML만으로 되는 것들: 아직도 div로 다 만들고 있다면","아코디언, 팝오버, 모달, 자동완성, 폼 검증, 이미지 지연 로딩까지. 라이브러리 없이 HTML 태그와 속성만으로 해결되는 기능을 예제와 함께 정리했다. 코드는 줄고 접근성은 덤으로 따라온다.",{"date":776,"field":794,"tags":895},[229],{"path":897,"title":898,"description":899,"meta":900},"\u002Fko\u002Fhtml2","미치광이 카드 컴포넌트","글리치 효과와 움직이는 도형으로 꾸민 '미치광이' 카드 컴포넌트를 HTML과 CSS 애니메이션만으로 만들어보는 예제. 결과 화면을 바로 확인할 수 있다.",{"date":901,"field":794,"tags":902},"2025.03.28",[229,796],{"path":904,"title":905,"description":906,"meta":907},"\u002Fko\u002Fhttp-cache-control-guide","HTTP 캐시 제대로 쓰기: Cache-Control, ETag, no-cache와 no-store의 차이","no-cache는 '캐시하지 마라'가 아니다. max-age, no-cache, no-store, ETag와 304, 배포 후 옛날 화면이 보이는 원인, Vercel CDN 캐시까지 직접 확인하며 정리했다.",{"date":776,"field":777,"tags":908},[780,864],{"path":910,"title":911,"description":912,"meta":913},"\u002Fko\u002Fhttp-status-codes-guide","HTTP 상태 코드 올바르게 쓰기: 400 vs 422, 401 vs 403, 200 vs 201 vs 204 헷갈릴 때 보는 정리","400 vs 422, 401 vs 403, 200 vs 201 vs 204처럼 헷갈리는 HTTP 상태 코드를 상황별로 정리하고, 301이 POST를 GET으로 바꾸는 리다이렉트 함정까지 다뤘다.",{"date":776,"field":777,"tags":914},[780,779],{"path":916,"title":917,"description":918,"meta":919},"\u002Fko\u002Fhttps-guide","Let's Encrypt를 이용해서 HTTPS 적용하기","무료 인증서인 Let's Encrypt와 Certbot으로 VM에 올린 웹 서비스에 HTTPS를 적용하는 방법. 인증서 발급부터 nginx 설정, 브라우저 확인까지 단계별로 정리했다.",{"date":920,"field":861,"tags":921},"2025.03.11",[922],"https",{"path":924,"title":925,"description":926,"meta":927},"\u002Fko\u002Fjavascript-async-error-handling","자바스크립트 비동기 에러 올바르게 처리하기: try\u002Fcatch가 못 잡는 경우부터 Promise.all vs allSettled까지","try\u002Fcatch가 비동기 에러를 못 잡는 이유, fetch의 res.ok, Promise.all vs allSettled, 반복문 속 await, 타임아웃과 요청 취소까지 비동기 에러 처리 함정을 정리했다.",{"date":776,"field":794,"tags":928},[819],{"path":930,"title":931,"description":932,"meta":933},"\u002Fko\u002Fjavascript-event-loop","자바스크립트 이벤트 루프 완전 정복: 마이크로태스크와 매크로태스크의 실행 순서","setTimeout(0)보다 Promise.then이 먼저 실행되는 이유는? 콜 스택, 태스크 큐, 마이크로태스크 큐가 어떤 순서로 돌아가는지 예제로 정리하고, 브라우저와 Node.js의 차이, 실무에서 UI 멈춤을 피하는 방법까지 알아보자.",{"date":776,"field":833,"tags":934},[819,788],{"path":936,"title":937,"description":938,"meta":939},"\u002Fko\u002Fjavascript-higher-order-functions","자바스크립트 고차 함수 완전 정복: map, filter, reduce부터 직접 만드는 once, memoize까지","map, filter, reduce를 제대로 쓰는 법부터 once, memoize, pipe, 커링을 직접 만들어보고 map(parseInt) 같은 함정까지 자바스크립트 고차 함수를 정리했다.",{"date":776,"field":794,"tags":940},[819],{"path":942,"title":943,"description":944,"meta":945},"\u002Fko\u002Fjavascript-nullish-coalescing-optional-chaining","자바스크립트 ?? vs || 완벽 정리: null, undefined, 0, 빈 문자열을 올바르게 처리하는 법","볼륨 0이 50으로 바뀌고, 빈 닉네임이 '익명'이 되는 버그는 대부분 || 때문이다. ??와 ||의 차이, ?. 옵셔널 체이닝, ??=, 기본 매개변수가 적용되는 조건, null 검사하는 올바른 방법까지 직접 값을 넣어보며 정리했다.",{"date":776,"field":794,"tags":946},[819],{"path":948,"title":949,"description":950,"meta":951},"\u002Fko\u002Fjs-data-01","JavaScript 데이터01","배열 필터링과 변환, 합계 계산, 그룹화, 정렬, 중첩 데이터 처리, 중복 제거까지 자바스크립트로 데이터를 다루는 연습 문제를 난이도별로 풀어본다.",{"date":952,"field":794,"tags":953},"2025.03.08",[819],{"path":955,"title":956,"description":957,"meta":958},"\u002Fko\u002Fjs-data-02","JavaScript 데이터02","주문 데이터 필터링과 총액 계산, 고객별 주문 요약처럼 실무에 가까운 자바스크립트 데이터 처리 문제를 reduce 등 배열 메서드로 풀어본다.",{"date":952,"field":794,"tags":959},[819],{"path":961,"title":962,"description":963,"meta":964},"\u002Fko\u002Fjwt-vs-session-auth","JWT vs 세션 인증 비교: 무엇을 써야 할까? 저장 위치, 로그아웃, 보안까지 정리","JWT는 암호화가 아니라 서명이다. 세션과 JWT의 동작 방식, 강제 로그아웃 문제, localStorage와 httpOnly 쿠키 중 저장 위치, 리프레시 토큰까지 비교했다.",{"date":776,"field":777,"tags":965},[966,787],"auth",{"path":758,"title":5,"description":751,"meta":968},{"date":754,"field":755,"tags":969},[757,229],{"path":971,"title":972,"description":973,"meta":974},"\u002Fko\u002Fmarkdown-guid","마크다운 문법","제목, 강조, 목록, 링크, 이미지, 코드 블록까지 블로그 글과 README 작성에 필요한 마크다운 문법을 예제와 함께 한 번에 정리했다.",{"date":975,"image":976,"field":755,"tags":977},"2025.03.02","markdown.webp",[978],"markdown",{"path":980,"title":981,"description":982,"meta":983},"\u002Fko\u002Fminio-guide","MinIO 완벽 가이드: 설치부터 Node.js 연동까지","MinIO는 오픈소스 기반의 고성능 객체 스토리지 서버입니다. AWS S3와 호환되는 API를 제공하여 클라우드 네이티브 애플리케이션을 위한 완벽한 스토리지 솔루션으로 자리잡고 있습니다.",{"date":984,"field":777,"tags":985},"2025.11.23",[986,788],"minio",{"path":988,"title":989,"description":990,"meta":991},"\u002Fko\u002Fnitro-wildcard-redirect-url-migration","글 주소 프리픽스를 통째로 바꿔야 할 때: Nitro wildcard 리다이렉트로 안전하게 옮기기","\u002Fblog\u002F[슬러그]로 쓰던 글 주소를 \u002Fko\u002F[슬러그]로 옮기면서, 기존 링크\u002F북마크를 하나도 안 깨뜨리고 처리한 방법. Nitro routeRules의 와일드카드 리다이렉트 패턴과 실제로 같이 바꿔야 했던 범위를 정리한다.",{"date":776,"field":755,"tags":992},[865,757],{"path":994,"title":995,"description":996,"meta":997},"\u002Fko\u002Fnode-simple-server","Node.js 간단한 서버 만들기","Node.js 기본 http 모듈로 간단한 웹 서버를 만들고 실행하는 방법. 서버 코드 작성부터 Express로 라우팅을 확장하는 과정까지 정리했다.",{"date":998,"field":777,"tags":999},"2025.01.30",[788],{"path":1001,"title":1002,"description":1003,"meta":1004},"\u002Fko\u002Fnodejs-graceful-shutdown","Node.js 서버 안전하게 종료하기 (Graceful Shutdown): 배포할 때마다 요청이 끊기는 이유","배포할 때마다 요청이 끊긴다면 SIGTERM을 처리하지 않았기 때문이다. server.close(), keep-alive 함정, Docker와 PM2의 신호 전달 문제까지 실행하며 정리했다.",{"date":776,"field":777,"tags":1005},[788,873,1006],"pm2",{"path":1008,"title":1009,"description":1010,"meta":1011},"\u002Fko\u002Fnuxt-colormode","Nuxt3 다크모드 적용하기","@nuxtjs\u002Fcolor-mode로 Nuxt 3에 다크 모드를 적용하는 방법. 설정, useColorMode()와 $colorMode의 차이, 토글 버튼과 초기 테마 설정까지 정리했다.",{"date":810,"field":794,"tags":1012},[865],{"path":1014,"title":1015,"description":1016,"meta":1017},"\u002Fko\u002Fnuxt-content-vercel-empty-query-sqlite","배포만 하면 블로그 글이 하나도 안 보이던 버그: Nuxt Content와 Vercel의 \u002Ftmp 문제","로컬에서는 멀쩡한데 Vercel에 올리면 queryCollection()이 빈 배열만 반환하던 버그를 추적한 과정. 원인은 @nuxt\u002Fcontent가 Vercel의 읽기 전용 파일시스템을 지원하기 전 버전에 고정돼 있었던 것.",{"date":1018,"field":755,"tags":1019},"2026.10.07",[770,864],{"path":1021,"title":1022,"description":1023,"meta":1024},"\u002Fko\u002Fnuxt-runtimeconfig-appconfig","Nuxt 설정 runtimeConfig vs app.config 완전 정복","Nuxt3에서 전역 설정을 어떻게 관리하고, 어떤 기준으로 나누어야 할까? 다크모드 예시부터 실무 기준 설정 전략까지 총정리",{"date":1025,"field":794,"tags":1026},"2025.06.16",[865],{"path":1028,"title":1029,"description":1030,"meta":1031},"\u002Fko\u002Fnuxt-useasyncdata-cache-error","useAsyncData 캐싱 문제와 Key 동적 설정으로 해결하기","다른 글로 이동해도 이전 글 데이터가 계속 보이는 Nuxt 3 버그. useAsyncData의 key 기반 캐싱이 원인이었고, key를 동적으로 바꿔 해결한 과정을 정리했다.",{"date":1032,"field":794,"tags":1033,"image":1034},"2025.01.24",[865],"cache.jpg",{"path":1036,"title":1037,"description":1038,"meta":1039},"\u002Fko\u002Fnuxt-vercel-sql-error","Nuxt 3에서 Vercel 배포 시 발생한 md 파일 로딩 문제 해결기","Nuxt 3 웹 어플리케이션에서 Vercel 배포 환경에서 발생한 md 파일 로딩 문제를 분석하고 해결한 방법에 대해 공유합니다.",{"date":1032,"image":1040,"field":794,"tags":1041},"ssg.jpg",[865,770,864],{"path":1043,"title":1044,"description":1045,"meta":1046},"\u002Fko\u002Fnuxt3-pinia-guide","nuxt3 Pinia 적용하기","Nuxt 3 프로젝트에 @pinia\u002Fnuxt로 Pinia를 설치하고 스토어를 만드는 방법. 상태 값 읽기, 액션으로 값 바꾸기까지 예제로 정리했다.",{"date":810,"field":794,"tags":1047},[865,1048],"pinia",{"path":1050,"title":1051,"description":1052,"meta":1053},"\u002Fko\u002Fnuxt3-seo-setup","Nuxt 3 SEO 설정 정리: sitemap, canonical, hreflang, 구조화 데이터(JSON-LD)","Nuxt 3와 Nuxt Content 블로그에 실제로 적용한 SEO 설정을 코드로 정리했다. 사이트맵 lastmod, canonical과 필터 주소 noindex, 한\u002F영 hreflang, JSON-LD 날짜 형식, prerender까지.",{"date":754,"field":755,"tags":1054},[757,865],{"path":1056,"title":1057,"description":1058,"meta":1059},"\u002Fko\u002Fnuxt3-unit-test","Nuxt 프로젝트에 유닛 테스트 코드 추가하기","Nuxt 3 프로젝트에서 Vitest를 활용한 테스트 환경 설정과 유틸리티 함수 테스트 방법",{"date":1060,"image":1061,"field":794,"tags":1062},"2025.03.07","unit-test.jpg",[865,1063],"vitest",{"path":1065,"title":1066,"description":1067,"meta":1068},"\u002Fko\u002Fpm2-guide","프로세스 매니저 PM2","Node.js 배포 환경에서 쓰는 프로세스 매니저 PM2. 설치와 실행부터 프로세스 목록, 재시작, 로그 확인, 모니터링까지 자주 쓰는 명령어를 정리했다.",{"date":871,"field":861,"tags":1069},[1006,788],{"path":1071,"title":1072,"description":1073,"meta":1074},"\u002Fko\u002Freact-createportal","React createPortal 완벽 가이드","React의 `createPortal`을 활용한 UI 구현법, 활용 사례, 장단점 및 코드 예제를 소개합니다.",{"date":1075,"field":794,"tags":1076},"2025.01.27",[837],{"path":1078,"title":1079,"description":1080,"meta":1081},"\u002Fko\u002Frouting","Routing","Nuxt file-system routing creates a route for every file in the pages\u002F directory.",{"date":1082,"field":794,"tags":1083},"2025.01.22",[865],{"path":638,"title":1085,"description":1086,"meta":1087},"구글 서치 콘솔 \"발견됨 \u002F 크롤링됨 - 현재 색인이 생성되지 않음\" 차이와 확인할 것","서치 콘솔의 '발견됨 - 현재 색인이 생성되지 않음'과 '크롤링됨 - 현재 색인이 생성되지 않음'은 멈춘 단계가 다르다. 두 상태의 뜻과 각각 확인할 것, 보고서와 site: 숫자가 다른 이유까지 정리했다.",{"date":754,"field":755,"tags":1088},[757],{"path":389,"title":1090,"description":1091,"meta":1092},"의미 있는 HTML 쓰기: 대부분이 하는 시맨틱 HTML 실수 7가지","화면에서는 똑같아 보여도 div로 만든 버튼, label 없는 입력칸, 크기로 고른 제목 태그는 키보드 사용자와 스크린 리더, 검색 엔진에게 전혀 다르게 보인다. 직접 눌러볼 수 있는 예제로 흔한 시맨틱 HTML 실수와 고치는 법을 정리했다.",{"date":776,"field":794,"tags":1093},[229],{"path":1095,"title":1096,"description":1097,"meta":1098},"\u002Fko\u002Ftanstack-query-guide","TanStack Query란 무엇인가?","TanStack Query가 웹 애플리케이션에서 데이터 가져오기, 캐싱, 상태 관리를 어떻게 개선하는지 알아본다.",{"date":1099,"field":794,"tags":1100},"2025.04.15",[837,1101],"tanstack-query",{"path":1103,"title":1104,"description":1105,"meta":1106},"\u002Fko\u002Fuse-context","리엑트 상태관리 useContext()","React에서 `useContext`를 사용하면 컴포넌트 트리에 깊이 중첩된 컴포넌트에서도 prop drilling 없이 상태를 공유할 수 있습니다. 이 글에서는 `useContext`의 개념, 사용 방법, 장단점, 그리고 실전 예제를 함께 알아보겠습니다.",{"date":1107,"field":794,"tags":1108},"2025.02.03",[837],{"path":1110,"title":1111,"description":1112,"meta":1113},"\u002Fko\u002Fvite-plugin-style-inject","리엑티 컴포넌트를 패키지 빌드 시 CSS 같이 적용시키기","React 컴포넌트를 Vite 라이브러리 모드로 빌드하면 CSS가 적용되지 않는 문제. 원인을 분석하고 vite-plugin-lib-inject-css로 자동 주입해 해결했다.",{"date":1114,"field":794,"tags":1115},"2025.04.06",[1116,837],"vite",{"path":1118,"title":1119,"description":1120,"meta":1121},"\u002Fko\u002Fvue-component-guide","Vue의 \u003Ccomponent :is \u002F> 완벽 정리: 동적 렌더링의 모든 것","Vue에서 \u003Ccomponent :is \u002F>를 활용한 동적 컴포넌트 렌더링 방법과 VNode, 컴포넌트 객체의 차이를 실전 예제로 이해하기",{"date":802,"field":794,"tags":1122},[836],{"path":1124,"title":1125,"description":1126,"meta":1127},"\u002Fko\u002Fvue-query-ref-value-trap-nuxt3","Vue Query에서 데이터가 안 보이는 이유? Nuxt 3와 Ref 구조의 함정","Nuxt 3 + vue-query를 사용할 때 API 데이터가 `v-if` 조건문에서 제대로 표시되지 않는 현상을 해결한 경험을 바탕으로, Ref 구조와 template 디렉티브에서의 차이를 자세히 설명합니다.",{"date":1128,"field":794,"tags":1129},"2025.06.08",[865,836,1101],{"path":1131,"title":1132,"description":1133,"meta":1134},"\u002Fko\u002Fweb-security-sql-injection-xss-csrf","웹 보안 기본 3대 공격: SQL Injection, XSS, CSRF 원리와 방어법","로그인 폼 한 줄로 관리자가 되고 댓글 하나로 스크립트가 실행된다. SQL Injection, XSS, CSRF의 원리를 직접 확인하고 각각을 막는 방법을 정리했다.",{"date":1135,"field":777,"tags":1136},"2026.10.09",[787,788],1791974052180]