[{"data":1,"prerenderedAt":12724},["ShallowReactive",2],{"navigation_docs_en":3,"landing_en":176,"main-posts-feed":198,"main-posts-nav":12639},[4,114,162],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":27},"Note (개발 지식)","emojione-v1:note-pad","\u002Fen\u002Fnote","en\u002F1.note",[10,28,42,68,78,88,104],{"title":11,"icon":12,"path":13,"stem":14,"children":15,"page":27},"Spring","logos:spring-icon","\u002Fen\u002Fnote\u002Fspring","en\u002F1.note\u002F0.spring",[16],{"title":17,"icon":18,"path":19,"stem":20,"children":21,"page":27},"Spring Ai","custom:ai","\u002Fen\u002Fnote\u002Fspring\u002Fspring-ai","en\u002F1.note\u002F0.spring\u002F1.spring-ai",[22],{"title":23,"path":24,"stem":25,"icon":26},"Spring AI 핵심 개념과 LLM 애플리케이션 구조 이해","\u002Fen\u002Fnote\u002Fspring\u002Fspring-ai\u002Fspring-ai","en\u002F1.note\u002F0.spring\u002F1.spring-ai\u002F1.spring-ai","lucide:file-text",false,{"title":29,"icon":30,"path":31,"stem":32,"children":33,"page":27},"Javascript","skill-icons:javascript","\u002Fen\u002Fnote\u002Fjavascript","en\u002F1.note\u002F1.javascript",[34,38],{"title":35,"path":36,"stem":37,"icon":26},"Promise.all과 구조 분해 할당을 활용한 데이터 처리 패턴","\u002Fen\u002Fnote\u002Fjavascript\u002Fdestructuring","en\u002F1.note\u002F1.javascript\u002F1.destructuring",{"title":39,"path":40,"stem":41,"icon":26},"JavaScript 배열 고차 함수와 데이터 처리 패턴 이해","\u002Fen\u002Fnote\u002Fjavascript\u002Ffilter","en\u002F1.note\u002F1.javascript\u002F2.filter",{"title":43,"icon":44,"path":45,"stem":46,"children":47,"page":27},"Nuxt4","logos:nuxt-icon","\u002Fen\u002Fnote\u002Fnuxt4","en\u002F1.note\u002F2.nuxt4",[48,52,56,60,64],{"title":49,"path":50,"stem":51,"icon":26},"Nuxt 4 프레임워크 구조와 핵심 아키텍처 이해","\u002Fen\u002Fnote\u002Fnuxt4\u002Fnuxt-info","en\u002F1.note\u002F2.nuxt4\u002F1.nuxt-info",{"title":53,"path":54,"stem":55,"icon":26},"Nuxt useAsyncData 동작 원리와 SSR 데이터 페칭 최적화","\u002Fen\u002Fnote\u002Fnuxt4\u002Fnuxt-use_async_data","en\u002F1.note\u002F2.nuxt4\u002F2.nuxt-use_async_data",{"title":57,"path":58,"stem":59,"icon":26},"Nuxt Content v3 데이터 모델과 Query 시스템 이해","\u002Fen\u002Fnote\u002Fnuxt4\u002Fnuxt-content-v3","en\u002F1.note\u002F2.nuxt4\u002F3.nuxt-content-v3",{"title":61,"path":62,"stem":63,"icon":26},"Nuxt Auto-imports 내부 동작과 컴파일 과정 분석","\u002Fen\u002Fnote\u002Fnuxt4\u002Fnuxt-auto-import","en\u002F1.note\u002F2.nuxt4\u002F4.nuxt-auto-import",{"title":65,"path":66,"stem":67,"icon":26},"Nuxt 렌더링 방식 이해: SSR, SSG, ISR, CSR 구현 방법","\u002Fen\u002Fnote\u002Fnuxt4\u002Fnuxt-rendering-strategy","en\u002F1.note\u002F2.nuxt4\u002F5.nuxt-rendering-strategy",{"title":69,"icon":70,"path":71,"stem":72,"children":73,"page":27},"Nitro","unjs:nitro","\u002Fen\u002Fnote\u002Fnitro","en\u002F1.note\u002F3.nitro",[74],{"title":75,"path":76,"stem":77,"icon":26},"Nitro 아키텍처와 서버 엔진 구조","\u002Fen\u002Fnote\u002Fnitro\u002Fnitro","en\u002F1.note\u002F3.nitro\u002Fnitro",{"title":79,"icon":80,"path":81,"stem":82,"children":83,"page":27},"Devops","vscode-icons:file-type-pipeline","\u002Fen\u002Fnote\u002Fdevops","en\u002F1.note\u002F4.devops",[84],{"title":85,"path":86,"stem":87,"icon":26},"서버사이드 WebAssembly(Wasm)와 차세대 실행 환경","\u002Fen\u002Fnote\u002Fdevops\u002Fwebassembly","en\u002F1.note\u002F4.devops\u002F1.webAssembly",{"title":89,"icon":90,"path":91,"stem":92,"children":93,"page":27},"Tooling","lucide:wrench","\u002Fen\u002Fnote\u002Ftooling","en\u002F1.note\u002F4.tooling",[94],{"title":95,"icon":96,"path":97,"stem":98,"children":99,"page":27},"Package Manager","devicon:npm","\u002Fen\u002Fnote\u002Ftooling\u002Fpackage-manager","en\u002F1.note\u002F4.tooling\u002F1.package-manager",[100],{"title":101,"path":102,"stem":103,"icon":26},"Package Lock 파일의 역할과 의존성 관리 원리","\u002Fen\u002Fnote\u002Ftooling\u002Fpackage-manager\u002Fpackage-lock","en\u002F1.note\u002F4.tooling\u002F1.package-manager\u002F1.package-lock",{"title":105,"icon":106,"path":107,"stem":108,"children":109,"page":27},"Web","streamline-plump:web","\u002Fen\u002Fnote\u002Fweb","en\u002F1.note\u002F4.web",[110],{"title":111,"path":112,"stem":113,"icon":26},"웹 애플리케이션 렌더링 아키텍처 이해","\u002Fen\u002Fnote\u002Fweb\u002Frendering-modes","en\u002F1.note\u002F4.web\u002F2.rendering-modes",{"title":115,"icon":116,"path":117,"stem":118,"children":119,"page":27},"Project (개발일지)","icon-park-solid:branch-two","\u002Fen\u002Fproject","en\u002F2.project",[120],{"title":121,"icon":122,"path":123,"stem":124,"children":125,"page":27},"Brolog","custom:symbol","\u002Fen\u002Fproject\u002Fbrolog","en\u002F2.project\u002F1.brolog",[126,130,134,138,142,146,150,154,158],{"title":127,"path":128,"stem":129,"icon":26},"Brolog 프로젝트 개요","\u002Fen\u002Fproject\u002Fbrolog\u002Foutline","en\u002F2.project\u002F1.brolog\u002F1.outline",{"title":131,"path":132,"stem":133,"icon":26},"Brolog 프로젝트 구조","\u002Fen\u002Fproject\u002Fbrolog\u002Fstructure","en\u002F2.project\u002F1.brolog\u002F2.structure",{"title":135,"path":136,"stem":137,"icon":26},"[Nuxt Content] 네비게이션 아이콘 커스텀 빌드","\u002Fen\u002Fproject\u002Fbrolog\u002Fcustom-icon","en\u002F2.project\u002F1.brolog\u002F3.custom-icon",{"title":139,"path":140,"stem":141,"icon":26},"[Nuxt 4] Auto-imports 동작 원리와 자동 import 시스템 이해","\u002Fen\u002Fproject\u002Fbrolog\u002Fnuxt-auto-import","en\u002F2.project\u002F1.brolog\u002F4.nuxt-auto-import",{"title":143,"path":144,"stem":145,"icon":26},"[Brolog] 개발 블로그 메인 페이지 구현","\u002Fen\u002Fproject\u002Fbrolog\u002Fdevelop-main","en\u002F2.project\u002F1.brolog\u002F5.develop-main",{"title":147,"path":148,"stem":149,"icon":26},"[Brolog] Nuxt Multi Layer 프로젝트 Vercel 배포 (1)","\u002Fen\u002Fproject\u002Fbrolog\u002Fvercel-deploy1","en\u002F2.project\u002F1.brolog\u002F6.vercel-deploy1",{"title":151,"path":152,"stem":153,"icon":26},"[Brolog] Nuxt Multi Layer 프로젝트 Vercel 배포 (2) - Build Pipeline 오류 분석과 Nuxt 실행 구조 이해","\u002Fen\u002Fproject\u002Fbrolog\u002Fvercel-deploy2-error-analyze","en\u002F2.project\u002F1.brolog\u002F7.vercel-deploy2-error-analyze",{"title":155,"path":156,"stem":157,"icon":26},"[Brolog] AI Assistant 적용 과정 - Agent 구조와 API 사용량 관리 시 고려사항","\u002Fen\u002Fproject\u002Fbrolog\u002Fai-assistant-application-process","en\u002F2.project\u002F1.brolog\u002F8.ai-assistant-application-process",{"title":159,"path":160,"stem":161,"icon":26},"[Brolog] Nuxt Multi Layer 프로젝트 Vercel 배포 (3) - Runtime 기능 문제 분석과 AI Assistant 전환","\u002Fen\u002Fproject\u002Fbrolog\u002Fvercel-deploy3-ai-integration","en\u002F2.project\u002F1.brolog\u002F9.vercel-deploy3-ai-integration",{"title":163,"icon":164,"path":165,"stem":166,"children":167,"page":27},"Error (트러블슈팅)","emojione:stop-sign","\u002Fen\u002Ftrouble-shooting","en\u002F3.trouble-shooting",[168],{"title":121,"icon":122,"path":169,"stem":170,"children":171,"page":27},"\u002Fen\u002Ftrouble-shooting\u002Fbrolog","en\u002F3.trouble-shooting\u002F1.brolog",[172],{"title":173,"path":174,"stem":175,"icon":26},"[Nuxt Content] Nuxt Icon 오류 해결과 Custom Collection 기반 오프라인 아이콘 구성","\u002Fen\u002Ftrouble-shooting\u002Fbrolog\u002Fnuxt-icon-restricted-network-troubleshooting","en\u002F3.trouble-shooting\u002F1.brolog\u002F1.nuxt-icon-restricted-network-troubleshooting",{"id":177,"title":178,"body":179,"description":178,"extension":188,"meta":189,"navigation":190,"path":191,"seo":192,"stem":196,"__hash__":197},"landing_en\u002Fen\u002Findex.md","",{"type":180,"value":181,"toc":185},"minimark",[182],[183,184],"home",{},{"title":178,"searchDepth":186,"depth":186,"links":187},2,[],"md",{},true,"\u002Fen",{"title":193,"description":194,"ogImage":195},"Brolog | 개발 기록과 기술 아카이브","개발 과정에서 경험한 문제와 해결 과정을 기록하는 개인 기술 블로그입니다.","\u002Fimages\u002Fbro-ai-thumbnail.png","en\u002Findex","4qRU8xvYrP3bBvIEDiu4DT8L7ocXwvErcguH_oFSnRI",[199,879,1615,2234,3409,3916,4229,4609,5040,5333,5943,6262,7155,7934,8544,8791,9360,10143,11304,11547,12073,12350,12468],{"id":200,"title":155,"body":201,"date":861,"description":862,"extension":188,"links":863,"meta":864,"navigation":872,"path":873,"seo":874,"stem":877,"__hash__":878},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F11.ai-assistant-application-process.md",{"type":180,"value":202,"toc":854},[203,850],[204,205,207,212,216,219,222,225,228,231,234,238,241,244,255,258,261,264,271,274,277,280,283,285,289,292,299,302,305,308,311,314,317,320,323,325,329,332,335,342,345,352,355,362,365,368,370,374,377,380,416,419,422,429,432,439,442,445,471,473,477,480,485,500,503,506,509,516,519,522,524,528,542,545,547,554,557,560,563,575,577,581,584,587,631,633,637,640,642,649,652,655,662,665,668,674,677,679,683,686,689,691,698,701,704,706,710,713,715,722,725,728,735,738,740,744,747,750,757,760,767,770,773,775,779,782,789,792,795,802,805,807,810,813,816,819,826,829,832,835,838,841,844,847],"u-page-card",{":spotlight":206},"true",[208,209,211],"h1",{"id":210},"ai-assistant-적용-이후-확인한-agent-실행-구조와-api-사용량-문제","🧩 AI Assistant 적용 이후 확인한 Agent 실행 구조와 API 사용량 문제",[213,214,215],"p",{},"이전 글에서는 Brolog 프로젝트에 AI Assistant 기능을 적용하면서 발생했던 문제와 해결 과정을 정리했다.",[213,217,218],{},"Vercel AI Gateway 대신 Google AI Studio API를 사용하도록 변경했고, AI SDK Provider 버전 충돌 문제까지 해결하면서 기본적인 AI 요청 흐름을 구성할 수 있었다.",[213,220,221],{},"하지만 실제 테스트 과정에서 예상하지 못한 부분을 발견했다.",[213,223,224],{},"사용자는 질문 하나만 입력했지만, 내부적으로는 여러 번의 AI API 요청이 발생하고 있었다.",[213,226,227],{},"처음에는 API 호출 로직이나 설정 문제라고 생각했지만, 원인은 MCP와 Tool을 사용하는 Agent 구조 자체에 있었다.",[213,229,230],{},"이번 글에서는 MCP 기반 Agent 환경에서 하나의 질문이 어떻게 여러 LLM 호출로 처리되는지, 그리고 무료 API 환경에서 사용량을 어떻게 관리해야 하는지 정리한다.",[232,233],"hr",{},[208,235,237],{"id":236},"실제-로그에서-확인한-ai-agent-api-호출-구조","🔍 실제 로그에서 확인한 AI Agent API 호출 구조",[213,239,240],{},"MCP 기반 AI Assistant를 적용한 이후 실제 동작을 확인하기 위해 Google AI Studio 사용량과 Vercel Function 로그를 함께 확인했다.",[213,242,243],{},"처음 예상했던 것은:",[245,246,252],"pre",{"className":247,"code":249,"filename":250,"language":251,"meta":178},[248],"language-text","사용자 질문 1회\n\n↓\n\nAI API 요청 1회\n","Expected Flow","text",[253,254,249],"code",{"__ignoreMap":178},[213,256,257],{},"와 비슷한 구조였다.",[213,259,260],{},"하지만 실제 로그를 확인해보니 하나의 질문에서도 여러 번의 모델 호출이 발생하고 있었다.",[213,262,263],{},"예를 들어:",[245,265,269],{"className":266,"code":267,"filename":268,"language":251,"meta":178},[248],"사용자 질문\n\n↓\n\nLLM 판단\n\n↓\n\nMCP Tool 실행\n\n↓\n\n추가 LLM 호출\n\n↓\n\n최종 답변 생성\n","Agent Execution Flow",[253,270,267],{"__ignoreMap":178},[213,272,273],{},"과 같은 과정이 진행되면서,",[213,275,276],{},"사용자가 입력한 질문 수보다 실제 API 요청 수와 Token 사용량이 더 크게 발생했다.",[213,278,279],{},"이 과정을 통해 AI Agent 환경에서는 단순히 \"몇 번 질문했는가\"가 아니라,",[213,281,282],{},"하나의 요청이 내부적으로 몇 단계의 모델 호출을 발생시키는지까지 고려해야 한다는 점을 확인했다.",[232,284],{},[208,286,288],{"id":287},"️-일반적인-ai-호출-방식과-agent-실행-구조의-차이","🛠️ 일반적인 AI 호출 방식과 Agent 실행 구조의 차이",[213,290,291],{},"일반적인 AI API 호출 구조는 다음과 같다.",[245,293,297],{"className":294,"code":295,"filename":296,"language":251,"meta":178},[248],"사용자 질문\n\n↓\n\nLLM 요청\n\n↓\n\nAI 답변 반환\n","Simple AI Flow",[253,298,295],{"__ignoreMap":178},[213,300,301],{},"사용자 입장에서는 질문 하나에 답변 하나가 반환되기 때문에 하나의 API 요청만 발생한다고 생각하기 쉽다.",[213,303,304],{},"하지만 MCP(Model Context Protocol)와 Tool을 사용하는 Agent 구조에서는 처리 과정이 달라진다.",[213,306,307],{},"Agent는 단순히 답변을 생성하는 것이 아니라,",[213,309,310],{},"사용자의 요청을 분석하고,",[213,312,313],{},"필요한 데이터를 검색하고,",[213,315,316],{},"검색 결과를 다시 판단한 뒤,",[213,318,319],{},"최종 답변을 생성한다.",[213,321,322],{},"즉 하나의 질문이 여러 단계의 LLM 호출 과정으로 처리될 수 있다.",[232,324],{},[208,326,328],{"id":327},"mcp-tool-기반-문서-검색-agent-실행-흐름","🔄 MCP Tool 기반 문서 검색 Agent 실행 흐름",[213,330,331],{},"Brolog의 AI Assistant는 문서 기반 질문 답변을 위해 MCP Tool을 활용한다.",[213,333,334],{},"예를 들어 사용자가 다음과 같은 질문을 입력한다고 가정한다.",[245,336,340],{"className":337,"code":338,"filename":339,"language":251,"meta":178},[248],"\"Nuxt 설치 방법 알려줘\"\n","User Prompt",[253,341,338],{"__ignoreMap":178},[213,343,344],{},"내부적으로는 다음과 같은 과정이 진행될 수 있다.",[245,346,350],{"className":347,"code":348,"filename":349,"language":251,"meta":178},[248],"\n1. LLM 호출\n\n사용자 질문 분석\n\n↓\n\n문서 검색 필요 여부 판단\n\n\n2. MCP Tool 호출\n\nlist-pages 실행\n\n↓\n\n관련 문서 목록 탐색\n\n\n3. LLM 호출\n\nTool 결과 확인\n\n↓\n\n필요한 문서 판단\n\n\n4. MCP Tool 호출\n\nget-page 실행\n\n↓\n\n실제 문서 내용 조회\n\n\n5. LLM 호출\n\n검색 결과 기반 최종 답변 생성\n","MCP Agent Flow",[253,351,348],{"__ignoreMap":178},[213,353,354],{},"사용자는 질문 한 번만 했지만 내부적으로는:",[245,356,360],{"className":357,"code":358,"filename":359,"language":251,"meta":178},[248],"\n사용자 질문 1회\n\n=\n\nLLM 요청 여러 번\n\n+\n\nTool 실행 여러 번\n","API Usage",[253,361,358],{"__ignoreMap":178},[213,363,364],{},"이 발생할 수 있다.",[213,366,367],{},"따라서 AI 서비스 사용량을 관리할 때는 단순히 사용자 질문 개수만 보는 것이 아니라 Agent 내부에서 발생하는 모델 호출 횟수까지 고려해야 한다.",[232,369],{},[208,371,373],{"id":372},"️-mcp-agent-실행-과정과-step-제한의-필요성","⚙️ MCP Agent 실행 과정과 Step 제한의 필요성",[213,375,376],{},"AI SDK에서는 Agent 실행 단계를 제한할 수 있다.",[213,378,379],{},"예:",[245,381,386],{"className":382,"code":383,"filename":384,"language":385,"meta":178,"style":178},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","stopWhen: stepCountIs(5)\n","Agent Config","ts",[253,387,388],{"__ignoreMap":178},[389,390,393,397,401,405,409,413],"span",{"class":391,"line":392},"line",1,[389,394,396],{"class":395},"sBMFI","stopWhen",[389,398,400],{"class":399},"sMK4o",":",[389,402,404],{"class":403},"s2Zo4"," stepCountIs",[389,406,408],{"class":407},"sTEyZ","(",[389,410,412],{"class":411},"sbssI","5",[389,414,415],{"class":407},")\n",[213,417,418],{},"여기서 Step은 단순히 답변 횟수를 의미하지 않는다.",[213,420,421],{},"Agent 내부에서는 다음 과정들이 포함된다.",[245,423,427],{"className":424,"code":425,"filename":426,"language":251,"meta":178},[248],"\nLLM 판단\n\n↓\n\nTool 호출\n\n↓\n\nTool 결과 확인\n\n↓\n\n추가 판단\n","Agent Step",[253,428,425],{"__ignoreMap":178},[213,430,431],{},"Step 제한을 너무 높게 설정하면 다음 문제가 발생할 수 있다.",[245,433,437],{"className":434,"code":435,"filename":436,"language":251,"meta":178},[248],"\n불필요한 LLM 호출 증가\n\n↓\n\nAPI 사용량 증가\n\n↓\n\n응답 시간 증가\n\n↓\n\n무료 Tier 한도 빠른 소모\n","Problem",[253,438,435],{"__ignoreMap":178},[213,440,441],{},"문서 검색 기반 AI Assistant에서는 무한한 탐색보다 제한된 검색 과정이 더 적합하기 때문에 적절한 Step 제한이 필요하다.",[213,443,444],{},"Brolog에서는 다음과 같은 기준으로 관리하는 것이 적합하다고 판단했다.",[245,446,448],{"className":382,"code":447,"filename":384,"language":385,"meta":178,"style":178},"const MAX_STEPS = 4 ~ 5\n",[253,449,450],{"__ignoreMap":178},[389,451,452,456,459,462,465,468],{"class":391,"line":392},[389,453,455],{"class":454},"spNyl","const",[389,457,458],{"class":407}," MAX_STEPS ",[389,460,461],{"class":399},"=",[389,463,464],{"class":411}," 4",[389,466,467],{"class":399}," ~",[389,469,470],{"class":411}," 5\n",[232,472],{},[208,474,476],{"id":475},"️-token과-retry-설정을-통한-api-사용량-관리","🎛️ Token과 Retry 설정을 통한 API 사용량 관리",[213,478,479],{},"Agent 실행 과정에서는 Step뿐 아니라 출력량과 재시도 횟수도 API 사용량에 영향을 준다.",[481,482,484],"h2",{"id":483},"maxoutputtokens","maxOutputTokens",[245,486,489],{"className":382,"code":487,"filename":488,"language":385,"meta":178,"style":178},"maxOutputTokens: 2048\n","AI Config",[253,490,491],{"__ignoreMap":178},[389,492,493,495,497],{"class":391,"line":392},[389,494,484],{"class":395},[389,496,400],{"class":399},[389,498,499],{"class":411}," 2048\n",[213,501,502],{},"AI 모델이 생성할 수 있는 최대 출력 Token 수를 의미한다.",[213,504,505],{},"높게 설정하면 긴 답변 생성이 가능하지만,",[213,507,508],{},"반대로:",[245,510,514],{"className":511,"code":512,"filename":513,"language":251,"meta":178},[248],"\n더 많은 Token 사용\n\n↓\n\n비용 증가 가능\n\n↓\n\n응답 시간 증가\n","Token Usage",[253,515,512],{"__ignoreMap":178},[213,517,518],{},"문제가 발생할 수 있다.",[213,520,521],{},"문서 기반 Q&A 서비스에서는 일반적으로 1024~2048 정도면 충분하다고 판단했다.",[232,523],{},[481,525,527],{"id":526},"maxretries","maxRetries",[245,529,531],{"className":382,"code":530,"filename":488,"language":385,"meta":178,"style":178},"maxRetries: 1\n",[253,532,533],{"__ignoreMap":178},[389,534,535,537,539],{"class":391,"line":392},[389,536,527],{"class":395},[389,538,400],{"class":399},[389,540,541],{"class":411}," 1\n",[213,543,544],{},"API 요청 실패 시 재시도 횟수를 의미한다.",[213,546,379],{},[245,548,552],{"className":549,"code":550,"filename":551,"language":251,"meta":178},[248],"\n첫 요청 실패\n\n↓\n\n재시도\n\n↓\n\n성공\n","Retry Flow",[253,553,550],{"__ignoreMap":178},[213,555,556],{},"형태로 동작한다.",[213,558,559],{},"하지만 무료 API 환경에서는 재시도 역시 추가 요청으로 계산된다.",[213,561,562],{},"Brolog 환경에서는 다음과 같은 기준으로 관리하는 것이 적합하다고 판단했다.",[245,564,565],{"className":382,"code":530,"filename":488,"language":385,"meta":178,"style":178},[253,566,567],{"__ignoreMap":178},[389,568,569,571,573],{"class":391,"line":392},[389,570,527],{"class":395},[389,572,400],{"class":399},[389,574,541],{"class":411},[232,576],{},[208,578,580],{"id":579},"google-ai-studio-api-제한과-사용량-관리-이해","📊 Google AI Studio API 제한과 사용량 관리 이해",[213,582,583],{},"Google AI Studio에서 제공하는 모델들은 무료 사용 범위가 존재하지만 API 호출 제한이 존재한다.",[213,585,586],{},"대표적인 제한 항목은 다음과 같다.",[588,589,590,603],"table",{},[591,592,593],"thead",{},[594,595,596,600],"tr",{},[597,598,599],"th",{},"항목",[597,601,602],{},"의미",[604,605,606,615,623],"tbody",{},[594,607,608,612],{},[609,610,611],"td",{},"RPM",[609,613,614],{},"Requests Per Minute",[594,616,617,620],{},[609,618,619],{},"TPM",[609,621,622],{},"Tokens Per Minute",[594,624,625,628],{},[609,626,627],{},"RPD",[609,629,630],{},"Requests Per Day",[232,632],{},[481,634,636],{"id":635},"rpm-requests-per-minute","RPM (Requests Per Minute)",[213,638,639],{},"분당 API 요청 수 제한이다.",[213,641,379],{},[245,643,647],{"className":644,"code":645,"filename":646,"language":251,"meta":178},[248],"RPM 15\n","RPM Example",[253,648,645],{"__ignoreMap":178},[213,650,651],{},"이라면 1분 동안 최대 15개의 API 요청이 가능하다.",[213,653,654],{},"중요한 점은:",[245,656,660],{"className":657,"code":658,"filename":659,"language":251,"meta":178},[248],"\n사용자 질문 횟수 != API 요청 횟수\n","Agent Request Count",[253,661,658],{"__ignoreMap":178},[213,663,664],{},"라는 것이다.",[213,666,667],{},"Agent 구조에서는:",[245,669,672],{"className":670,"code":671,"language":251,"meta":178},[248],"질문 1회\n\n↓\n\nLLM 호출 4회\n",[253,673,671],{"__ignoreMap":178},[213,675,676],{},"가 발생하면 RPM 기준으로 4개의 요청을 사용한다.",[232,678],{},[481,680,682],{"id":681},"tpm-tokens-per-minute","TPM (Tokens Per Minute)",[213,684,685],{},"분당 처리 가능한 Token 양이다.",[213,687,688],{},"입력 Token과 출력 Token을 모두 포함한다.",[213,690,379],{},[245,692,696],{"className":693,"code":694,"filename":695,"language":251,"meta":178},[248],"\n사용자 질문\n1000 Token\n\n+\n\n검색 문서\n5000 Token\n\n+\n\nAI 답변\n1000 Token\n\n=\n\n7000 Token\n","Token Calculation",[253,697,694],{"__ignoreMap":178},[213,699,700],{},"을 사용할 수 있다.",[213,702,703],{},"문서 검색 AI에서는 긴 문서를 Context로 전달하는 경우가 많기 때문에 RPM보다 TPM 제한이 먼저 문제가 될 수도 있다.",[232,705],{},[481,707,709],{"id":708},"rpd-requests-per-day","RPD (Requests Per Day)",[213,711,712],{},"하루 API 요청 제한이다.",[213,714,379],{},[245,716,720],{"className":717,"code":718,"filename":719,"language":251,"meta":178},[248],"RPD 1500\n","Daily Limit",[253,721,718],{"__ignoreMap":178},[213,723,724],{},"이면 하루 동안 최대 1500번의 API 요청이 가능하다.",[213,726,727],{},"하지만 Agent 구조에서는:",[245,729,733],{"className":730,"code":731,"filename":732,"language":251,"meta":178},[248],"\n사용자 질문 1회\n\n↓\n\n여러 API 요청 발생\n","Daily Usage",[253,734,731],{"__ignoreMap":178},[213,736,737],{},"하기 때문에 실제 사용자 질문 가능 수는 더 적다.",[232,739],{},[208,741,743],{"id":742},"무료-api-환경에서-고려해야-할-비용-관리-요소","💰 무료 API 환경에서 고려해야 할 비용 관리 요소",[213,745,746],{},"웹 서비스에서 ChatGPT나 Gemini를 사용하는 것과 API를 사용하는 것은 다르다.",[213,748,749],{},"웹 서비스:",[245,751,755],{"className":752,"code":753,"filename":754,"language":251,"meta":178},[248],"\n서비스 제공자가 비용 관리\n\n↓\n\n사용자는 서비스 이용\n","Service Model",[253,756,753],{"__ignoreMap":178},[213,758,759],{},"API:",[245,761,765],{"className":762,"code":763,"filename":764,"language":251,"meta":178},[248],"\n개발자가 직접 모델 호출\n\n↓\n\n사용량 제한과 비용 관리 필요\n","API Model",[253,766,763],{"__ignoreMap":178},[213,768,769],{},"구조다.",[213,771,772],{},"따라서 Google AI Studio, OpenAI API, Vercel AI Gateway 모두 API 사용량 관리 관점으로 접근해야 한다.",[232,774],{},[208,776,778],{"id":777},"ai-기능-구현-과정에서-확인한-운영-고려사항","🚀 AI 기능 구현 과정에서 확인한 운영 고려사항",[213,780,781],{},"현재 Brolog에서는 다음 기준으로 AI 사용량을 관리할 예정이다.",[245,783,787],{"className":784,"code":785,"filename":786,"language":251,"meta":178},[248],"\nStep 제한\n\n+\n\nmaxOutputTokens 제한\n\n+\n\nRetry 제한\n\n+\n\n무료 Tier 모델 활용\n\n+\n\n환경 변수 기반 Model 관리\n","Management Strategy",[253,788,785],{"__ignoreMap":178},[213,790,791],{},"특히 MCP 기반 문서 챗봇은 일반적인 Chat API보다 내부 AI 호출 횟수가 증가할 수 있다.",[213,793,794],{},"따라서 비용 기준은:",[245,796,800],{"className":797,"code":798,"filename":799,"language":251,"meta":178},[248],"\n사용자 질문 개수\n\n×\n\nAgent 내부 실행 Step\n","Cost Consideration",[253,801,798],{"__ignoreMap":178},[213,803,804],{},"관점으로 접근해야 한다.",[232,806],{},[208,808,809],{"id":809},"마무리",[213,811,812],{},"AI Assistant를 추가하면서 단순히 API Key를 연결하면 끝날 것이라고 생각했지만,",[213,814,815],{},"실제 Agent 구조에서는 하나의 질문도 여러 단계의 AI 판단 과정으로 처리된다는 것을 확인하며 고려해야 할 것이 많다는 것을 확인했다.",[213,817,818],{},"특히 MCP와 Tool을 사용하는 구조에서는:",[245,820,824],{"className":821,"code":822,"filename":823,"language":251,"meta":178},[248],"\n사용자 질문\n\n↓\n\nLLM 판단\n\n↓\n\nTool 실행\n\n↓\n\n추가 판단\n\n↓\n\n최종 답변\n","Agent Flow",[253,825,822],{"__ignoreMap":178},[213,827,828],{},"이라는 흐름이 발생한다.",[213,830,831],{},"따라서 AI 기능을 운영할 때는 단순히 \"몇 명의 사용자가 몇 번 질문했는가\"가 아니라,",[213,833,834],{},"내부적으로 몇 번의 모델 호출이 발생하는지를 기준으로 사용량과 비용을 관리해야 한다.",[213,836,837],{},"이번 과정을 통해 AI Assistant를 적용할 때는 단순히 모델을 연결하고 답변을 생성하는 것만 고려해서는 안 된다는 점을 확인했다.",[213,839,840],{},"Agent 구조에서는 Tool 호출과 추가 LLM 실행 과정으로 인해 예상보다 많은 API 요청이 발생할 수 있으며,\n이러한 호출 구조와 사용량 제한까지 함께 고려해야 한다는 것을 알게 되었다.",[213,842,843],{},"이번 경험을 통해 AI 기능 구현은 단순히 API를 연결하는 것으로 끝나는 작업이 아니라,",[213,845,846],{},"Agent가 어떤 방식으로 동작하고,\n내부적으로 얼마나 많은 모델 호출이 발생하며,\n이를 어떻게 제어할 것인지까지 설계해야 하는 영역이라는 점을 알게 되었다.",[213,848,849],{},"앞으로 Brolog뿐만 아니라 다른 AI 기반 프로젝트를 구현할 때도\n기능 구현 과정에서 이러한 운영 요소들을 함께 고려하며 설계해 나갈 예정이다.",[851,852,853],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}",{"title":178,"searchDepth":186,"depth":186,"links":855},[856,857,858,859,860],{"id":483,"depth":186,"text":484},{"id":526,"depth":186,"text":527},{"id":635,"depth":186,"text":636},{"id":681,"depth":186,"text":682},{"id":708,"depth":186,"text":709},"2026-07-24","AI Assistant를 적용하면서 확인한 Agent 구조의 API 호출 흐름과 MCP Tool 사용 과정에서 발생하는 API 사용량 증가, 무료 API 환경에서 고려해야 할 운영 요소를 정리합니다.",null,{"tags":865,"image":871},[121,866,867,868,869,870],"AI","MCP","AI SDK","Google AI Studio","Agent","\u002Fimages\u002Fbro-ai-thumbnail2.png",{"icon":26},"\u002Fko\u002Fproject\u002Fbrolog\u002Fai-assistant-application-process",{"title":875,"description":876},"MCP 기반 AI Assistant Agent 구조와 API 사용량 관리 방법","MCP 기반 AI Agent에서 하나의 질문이 여러 LLM 호출로 처리되는 이유와 RPM, TPM, RPD 제한 및 API 사용량 관리 방법을 정리합니다.","ko\u002F2.project\u002F1.brolog\u002F11.ai-assistant-application-process","-yM37PazvKEXHgd5zET5IzMkG44gFffQ1fUcN5qtNMo",{"id":880,"title":159,"body":881,"date":1601,"description":1602,"extension":188,"links":863,"meta":1603,"navigation":1608,"path":1609,"seo":1610,"stem":1613,"__hash__":1614},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F10.vercel-deploy3-ai-integration.md",{"type":180,"value":882,"toc":1595},[883,1592],[204,884,885,889,896,901,912,918,921,961,964,967,970,973,980,985,988,991,994,997,999,1003,1007,1014,1020,1023,1026,1032,1039,1042,1045,1048,1055,1058,1061,1063,1067,1070,1075,1078,1081,1084,1087,1090,1093,1096,1099,1102,1105,1107,1111,1114,1117,1120,1127,1130,1133,1136,1139,1142,1145,1148,1151,1154,1157,1159,1163,1166,1169,1197,1200,1207,1210,1213,1216,1219,1222,1269,1272,1274,1278,1282,1285,1288,1291,1298,1301,1304,1307,1310,1312,1316,1319,1322,1325,1332,1335,1338,1341,1348,1351,1354,1357,1359,1363,1366,1369,1376,1379,1382,1385,1388,1391,1394,1396,1400,1403,1410,1413,1416,1419,1422,1424,1436,1439,1441,1445,1448,1451,1467,1470,1473,1480,1483,1486,1493,1496,1499,1502,1516,1519,1522,1524,1528,1531,1538,1541,1548,1551,1553,1555,1558,1561,1564,1567,1570,1573,1576,1583,1586,1589],{":spotlight":206},[208,886,888],{"id":887},"generate-배포-이후-발견한-runtime-문제","🚀 Generate 배포 이후 발견한 Runtime 문제",[213,890,891,892,895],{},"이전 글에서는 Nuxt Multi Layer 구조로 구성된 Brolog 프로젝트를 Vercel에 배포하면서 발생한 ",[253,893,894],{},"layer\u002F.nuxt"," 생성 오류를 분석하고,",[213,897,898],{},[253,899,900],{},"nuxi prepare",[213,902,903,904,907,908,911],{},"과정과 ",[253,905,906],{},"build",", ",[253,909,910],{},"generate"," 방식의 차이를 확인했다.",[213,913,914,915,917],{},"초기 배포 과정에서는 정적 콘텐츠 중심의 블로그라는 목적을 고려하여 ",[253,916,910],{}," 방식을 선택했다.",[213,919,920],{},"적용했던 Build Command는 다음과 같다.",[245,922,927],{"className":923,"code":924,"filename":925,"language":926,"meta":178,"style":178},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","npx nuxi prepare ..\u002Flayer && npx nuxi generate --extends ..\u002Flayer\n","Terminal","bash",[253,928,929],{"__ignoreMap":178},[389,930,931,934,938,941,944,947,950,952,955,958],{"class":391,"line":392},[389,932,933],{"class":395},"npx",[389,935,937],{"class":936},"sfazB"," nuxi",[389,939,940],{"class":936}," prepare",[389,942,943],{"class":936}," ..\u002Flayer",[389,945,946],{"class":399}," &&",[389,948,949],{"class":395}," npx",[389,951,937],{"class":936},[389,953,954],{"class":936}," generate",[389,956,957],{"class":936}," --extends",[389,959,960],{"class":936}," ..\u002Flayer\n",[213,962,963],{},"결과적으로 정적 페이지 생성과 배포 자체는 정상적으로 완료되었다.",[213,965,966],{},"하지만 실제 서비스 환경에서 기능을 확인하는 과정에서 예상하지 못한 문제가 발견되었다.",[213,968,969],{},"Brolog은 단순한 Markdown 기반 문서 사이트가 아니라,",[213,971,972],{},"다음과 같은 Nuxt Runtime 기반 기능을 함께 사용하고 있었다.",[245,974,978],{"className":975,"code":976,"filename":977,"language":251,"meta":178},[248],"Nuxt Content\n\n+\n\nNuxt OG Image\n\n+\n\nAI Assistant\n\n+\n\nNitro Runtime\n","Runtime Features",[253,979,976],{"__ignoreMap":178},[213,981,982,984],{},[253,983,910],{}," 방식은 정적 결과물을 생성하는 데에는 적합하지만,",[213,986,987],{},"빌드 이후에도 서버 실행 환경이 필요한 기능까지 동일하게 제공하지는 않는다.",[213,989,990],{},"이번 글에서는 generate 배포 이후 확인한 Runtime 기능 문제와,",[213,992,993],{},"이를 해결하기 위해 Build 방식으로 변경한 과정,",[213,995,996],{},"그리고 Google AI Studio 전환 과정에서 발생한 SDK 버전 충돌 해결 과정을 기록한다.",[232,998],{},[208,1000,1002],{"id":1001},"_1-generate-배포-이후-확인한-문제","1. Generate 배포 이후 확인한 문제",[481,1004,1006],{"id":1005},"nuxt-og-image-동작-확인","Nuxt OG Image 동작 확인",[213,1008,1009,1010,1013],{},"Brolog에서는 페이지 공유 시 사용할 Open Graph Image 생성을 위해 ",[253,1011,1012],{},"nuxt-og-image","를 사용하고 있다.",[213,1015,1016,1017,1019],{},"하지만 ",[253,1018,910],{}," 방식으로 배포한 이후 Vercel Production Deployment Overview에서 확인되는 Preview Image가 정상적으로 생성되지 않는 현상을 확인했다.",[213,1021,1022],{},"처음에는 단순한 SEO 메타데이터 설정 문제라고 생각했지만,",[213,1024,1025],{},"구조를 확인하면서 원인은 배포 방식 차이에 있었다.",[213,1027,1028,1029,1031],{},"Nuxt의 ",[253,1030,910],{}," 방식은 빌드 시점에 페이지를 렌더링하여 정적 결과물을 생성한다.",[245,1033,1037],{"className":1034,"code":1035,"filename":1036,"language":251,"meta":178},[248],"Build Time\n\n↓\n\nHTML \u002F Asset 생성\n\n↓\n\nStatic Hosting\n","Generate Flow",[253,1038,1035],{"__ignoreMap":178},[213,1040,1041],{},"구조로 동작한다.",[213,1043,1044],{},"반면 Nuxt OG Image는 단순히 이미지 파일을 미리 생성하는 기능이 아니라,",[213,1046,1047],{},"요청 시점에 Nitro Runtime에서 이미지를 생성하는 방식으로 동작한다.",[245,1049,1053],{"className":1050,"code":1051,"filename":1052,"language":251,"meta":178},[248],"Request\n\n↓\n\nNitro Server Route\n\n↓\n\nOG Image 생성\n\n↓\n\nResponse\n","OG Image Runtime Flow",[253,1054,1051],{"__ignoreMap":178},[213,1056,1057],{},"과정이 필요하다.",[213,1059,1060],{},"따라서 완전한 Static Output 환경에서는 Nitro Runtime 기반 기능들이 기대한 방식으로 동작하지 않을 수 있다는 점을 확인했다.",[232,1062],{},[481,1064,1066],{"id":1065},"ai-assistant-동작-문제","AI Assistant 동작 문제",[213,1068,1069],{},"또 다른 문제는 Docus에서 제공하는 AI Assistant 기능이었다.",[213,1071,1072,1074],{},[253,1073,910],{}," 방식으로 배포한 이후 AI Assistant를 실행했을 때 정상적인 답변 UI가 표시되지 않았다.",[213,1076,1077],{},"대신 화면을 덮는 형태의 비정상적인 팝업이 나타났고,",[213,1079,1080],{},"내부에는 HTML 코드 형태의 응답 내용이 그대로 노출되는 현상이 발생했다.",[213,1082,1083],{},"처음에는 AI Provider 설정 문제라고 판단했지만,",[213,1085,1086],{},"응답 흐름을 확인하면서 다른 원인을 확인했다.",[213,1088,1089],{},"AI Assistant 요청은 단순히 브라우저에서 외부 API를 직접 호출하는 구조가 아니라,",[213,1091,1092],{},"Nuxt Server Runtime을 통해 API 요청과 Response Stream 처리가 이루어진다.",[213,1094,1095],{},"하지만 Static Generate 환경에서는 이러한 Runtime Layer가 정상적으로 유지되지 않았고,",[213,1097,1098],{},"클라이언트는 AI SDK가 기대하는 Response Format이 아닌 HTML Error Response를 전달받고 있었다.",[213,1100,1101],{},"이를 통해 문제의 핵심은 단순 API Key 설정이 아니라,",[213,1103,1104],{},"배포 방식과 Runtime 요구사항의 불일치라는 점을 확인했다.",[232,1106],{},[208,1108,1110],{"id":1109},"_2-generate에서-build-방식으로-변경","2. Generate에서 Build 방식으로 변경",[213,1112,1113],{},"문제를 확인한 이후 현재 Brolog 프로젝트에서 필요한 실행 환경을 다시 판단했다.",[213,1115,1116],{},"처음에는 개발 블로그라는 목적 때문에 정적 생성 방식이 적합하다고 생각했다.",[213,1118,1119],{},"하지만 실제 사용하는 기능을 기준으로 보면:",[245,1121,1125],{"className":1122,"code":1123,"filename":1124,"language":251,"meta":178},[248],"\nNuxt Content\n\n+\n\nOG Image Generator\n\n+\n\nAI Assistant\n\n+\n\nServer Runtime 기능\n","Runtime Requirements",[253,1126,1123],{"__ignoreMap":178},[213,1128,1129],{},"을 포함하고 있었다.",[213,1131,1132],{},"Nuxt 3\u002F4의 서버 실행 환경은 Nitro Runtime 기반으로 구성된다.",[213,1134,1135],{},"SSR 렌더링,",[213,1137,1138],{},"API Route,",[213,1140,1141],{},"Server Middleware,",[213,1143,1144],{},"Runtime Config,",[213,1146,1147],{},"OG Image Generator와 같은 기능들은 Nitro Runtime 위에서 실행된다.",[213,1149,1150],{},"따라서 이러한 기능을 사용하는 프로젝트에서는 단순히 정적 파일 생성 여부보다,",[213,1152,1153],{},"Runtime Layer가 필요한지 판단하는 것이 중요하다.",[213,1155,1156],{},"이번 프로젝트에서는 Static Output보다 Nitro Runtime이 포함되는 Build 방식이 더 적합하다고 판단했다.",[232,1158],{},[208,1160,1162],{"id":1161},"_3-build-방식-전환과-메모리-제한-해결","3. Build 방식 전환과 메모리 제한 해결",[213,1164,1165],{},"이전 글에서 확인했던 것처럼 일반적인 Nuxt Build 과정은 Vercel 환경에서 메모리 부족 문제가 발생했다.",[213,1167,1168],{},"기본 Build Command:",[245,1170,1172],{"className":923,"code":1171,"filename":925,"language":926,"meta":178,"style":178},"npx nuxi prepare ..\u002Flayer && npx nuxi build --extends ..\u002Flayer\n",[253,1173,1174],{"__ignoreMap":178},[389,1175,1176,1178,1180,1182,1184,1186,1188,1190,1193,1195],{"class":391,"line":392},[389,1177,933],{"class":395},[389,1179,937],{"class":936},[389,1181,940],{"class":936},[389,1183,943],{"class":936},[389,1185,946],{"class":399},[389,1187,949],{"class":395},[389,1189,937],{"class":936},[389,1191,1192],{"class":936}," build",[389,1194,957],{"class":936},[389,1196,960],{"class":936},[213,1198,1199],{},"실행 결과:",[245,1201,1205],{"className":1202,"code":1203,"filename":1204,"language":251,"meta":178},[248],"Error: Command exited with SIGKILL\n\nOut of Memory event was detected during the build.\n","Error Log",[253,1206,1203],{"__ignoreMap":178},[213,1208,1209],{},"Vercel Build Container 환경은 제한된 리소스에서 실행되기 때문에,",[213,1211,1212],{},"Nuxt Build 과정에서 생성되는 번들링 과정과 Nitro Server Bundle 생성 과정이 메모리 제한에 도달한 것으로 판단했다.",[213,1214,1215],{},"Brolog에서 사용하는 기능 특성상 Nitro Runtime이 필요한 기능들이 존재했고,",[213,1217,1218],{},"결국 Node 메모리 제한을 조정하는 방향으로 다시 시도했다.",[213,1220,1221],{},"최종 Build Command는 다음과 같다.",[245,1223,1225],{"className":923,"code":1224,"filename":925,"language":926,"meta":178,"style":178},"export NODE_OPTIONS=\"--max-old-space-size=6144\" && npx nuxi prepare ..\u002Flayer && pnpm run build --extends ..\u002Flayer\n",[253,1226,1227],{"__ignoreMap":178},[389,1228,1229,1232,1235,1237,1240,1243,1245,1247,1249,1251,1253,1255,1257,1260,1263,1265,1267],{"class":391,"line":392},[389,1230,1231],{"class":454},"export",[389,1233,1234],{"class":407}," NODE_OPTIONS",[389,1236,461],{"class":399},[389,1238,1239],{"class":399},"\"",[389,1241,1242],{"class":936},"--max-old-space-size=6144",[389,1244,1239],{"class":399},[389,1246,946],{"class":399},[389,1248,949],{"class":395},[389,1250,937],{"class":936},[389,1252,940],{"class":936},[389,1254,943],{"class":936},[389,1256,946],{"class":399},[389,1258,1259],{"class":395}," pnpm",[389,1261,1262],{"class":936}," run",[389,1264,1192],{"class":936},[389,1266,957],{"class":936},[389,1268,960],{"class":936},[213,1270,1271],{},"결과적으로 Vercel 환경에서 Build가 정상적으로 완료되었다.",[232,1273],{},[208,1275,1277],{"id":1276},"_4-build-전환-이후-확인한-변화","4. Build 전환 이후 확인한 변화",[481,1279,1281],{"id":1280},"nuxt-og-image-정상-동작","Nuxt OG Image 정상 동작",[213,1283,1284],{},"Build 방식으로 전환한 이후 Vercel Production Deployment Overview에서 Preview Image가 정상적으로 생성되는 것을 확인했다.",[213,1286,1287],{},"기존 generate 방식에서는 확인되지 않았던 Brolog 메인 화면 기반 이미지가 정상적으로 표시되었다.",[213,1289,1290],{},"이 과정에서 확인한 핵심은 다음과 같다.",[245,1292,1296],{"className":1293,"code":1294,"filename":1295,"language":251,"meta":178},[248],"\ngenerate\n\n↓\n\nStatic Output 생성\n\n↓\n\n정적 파일 제공\n\n\nbuild\n\n↓\n\nNitro Server Bundle 생성\n\n↓\n\nRuntime 요청 처리 가능\n","Generate vs Build",[253,1297,1294],{"__ignoreMap":178},[213,1299,1300],{},"Nuxt에서 사용하는 일부 기능은 단순히 페이지를 생성하는 것에서 끝나는 것이 아니라,",[213,1302,1303],{},"배포 이후 요청을 처리할 Runtime 환경을 필요로 한다.",[213,1305,1306],{},"이번 문제는 기능 자체의 오류라기보다,",[213,1308,1309],{},"프로젝트가 요구하는 Runtime 환경과 배포 방식의 차이에서 발생한 문제였다.",[232,1311],{},[481,1313,1315],{"id":1314},"ai-assistant-오류-형태-변경","AI Assistant 오류 형태 변경",[213,1317,1318],{},"Build 전환 이후 기존처럼 화면 전체를 덮는 HTML 팝업 형태의 문제는 사라졌다.",[213,1320,1321],{},"하지만 AI Assistant 요청은 여전히 정상적인 답변을 반환하지 않았다.",[213,1323,1324],{},"확인 결과 HTTP Status 자체는:",[245,1326,1330],{"className":1327,"code":1328,"filename":1329,"language":251,"meta":178},[248],"200 OK\n","HTTP Status",[253,1331,1328],{"__ignoreMap":178},[213,1333,1334],{},"였지만,",[213,1336,1337],{},"Response Body 내부에는 오류 내용이 포함되어 있었다.",[213,1339,1340],{},"즉:",[245,1342,1346],{"className":1343,"code":1344,"filename":1345,"language":251,"meta":178},[248],"\nHTTP Layer\n\n↓\n\n정상 응답\n\n↓\n\nAI Provider 요청 과정에서 Error 발생\n","Response Flow",[253,1347,1344],{"__ignoreMap":178},[213,1349,1350],{},"구조였다.",[213,1352,1353],{},"네트워크 요청 자체는 성공했지만,",[213,1355,1356],{},"AI Model Provider 연결 과정에서 문제가 발생하고 있었다.",[232,1358],{},[208,1360,1362],{"id":1361},"_5-vercel-ai-gateway-문제-확인","5. Vercel AI Gateway 문제 확인",[213,1364,1365],{},"로그를 확인하면서 AI 오류 원인을 추적했다.",[213,1367,1368],{},"기존 AI Assistant 구조는 다음과 같았다.",[245,1370,1374],{"className":1371,"code":1372,"filename":1373,"language":251,"meta":178},[248],"\nBrolog AI Assistant\n\n↓\n\nVercel AI Gateway\n\n↓\n\nAI Model Provider\n","AI Flow",[253,1375,1372],{"__ignoreMap":178},[213,1377,1378],{},"문제는 Vercel AI Gateway 사용 조건이었다.",[213,1380,1381],{},"Vercel AI Gateway를 사용하기 위해서는 결제 수단 등록이 필요했다.",[213,1383,1384],{},"무료 사용 범위가 존재하더라도,",[213,1386,1387],{},"개인 프로젝트 환경에서는 예상하지 못한 과금 가능성을 관리하는 것이 중요하다고 판단했다.",[213,1389,1390],{},"따라서 AI Provider를 직접 관리하는 방식으로 변경하기로 했다.",[213,1392,1393],{},"최종적으로 Google AI Studio API를 사용하는 방향으로 변경했다.",[232,1395],{},[208,1397,1399],{"id":1398},"_6-google-ai-studio-provider-변경","6. Google AI Studio Provider 변경",[213,1401,1402],{},"변경 이후 구조는 다음과 같다.",[245,1404,1408],{"className":1405,"code":1406,"filename":1407,"language":251,"meta":178},[248],"\nBrolog AI Assistant\n\n↓\n\nAI SDK\n\n↓\n\nGoogle AI Studio API\n\n↓\n\nGemini Model\n","AI Provider Architecture",[253,1409,1406],{"__ignoreMap":178},[213,1411,1412],{},"Google AI Studio에서는 무료 사용 가능한 모델을 제공하고 있었고,",[213,1414,1415],{},"개인 개발 블로그 환경에서는 충분히 활용 가능한 수준이라고 판단했다.",[213,1417,1418],{},"또한 제공되는 모델은 변경될 수 있기 때문에,",[213,1420,1421],{},"모델명을 코드 내부에 직접 작성하지 않고 환경 변수로 분리했다.",[213,1423,379],{},[245,1425,1430],{"className":1426,"code":1427,"filename":1428,"language":1429,"meta":178,"style":178},"language-env shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","GOOGLE_AI_MODEL=gemini-model-name\n","Environment Variable","env",[253,1431,1432],{"__ignoreMap":178},[389,1433,1434],{"class":391,"line":392},[389,1435,1427],{},[213,1437,1438],{},"이렇게 구성하면 모델 변경이 필요할 때 코드 수정 없이 환경 변수만 변경하면 된다.",[232,1440],{},[208,1442,1444],{"id":1443},"_7-ai-sdk-provider-버전-충돌-해결","7. AI SDK Provider 버전 충돌 해결",[213,1446,1447],{},"Google Provider 적용 과정에서 새로운 문제가 발생했다.",[213,1449,1450],{},"Google AI Studio 연동을 위해 다음 패키지를 설치했다.",[245,1452,1454],{"className":923,"code":1453,"filename":925,"language":926,"meta":178,"style":178},"pnpm add @ai-sdk\u002Fgoogle\n",[253,1455,1456],{"__ignoreMap":178},[389,1457,1458,1461,1464],{"class":391,"line":392},[389,1459,1460],{"class":395},"pnpm",[389,1462,1463],{"class":936}," add",[389,1465,1466],{"class":936}," @ai-sdk\u002Fgoogle\n",[213,1468,1469],{},"하지만 설치된 버전은 4.x 버전이었다.",[213,1471,1472],{},"기존 프로젝트에서는:",[245,1474,1478],{"className":1475,"code":1476,"filename":1477,"language":251,"meta":178},[248],"ai@6\n","Installed Version",[253,1479,1476],{"__ignoreMap":178},[213,1481,1482],{},"버전을 사용하고 있었다.",[213,1484,1485],{},"확인 결과 내부 의존성 구조에서 차이가 있었다.",[245,1487,1491],{"className":1488,"code":1489,"filename":1490,"language":251,"meta":178},[248],"\nai@6\n\n└── @ai-sdk\u002Fprovider@3\n\n\n@ai-sdk\u002Fgoogle@4\n\n└── @ai-sdk\u002Fprovider@4\n","Dependency Tree",[253,1492,1489],{"__ignoreMap":178},[213,1494,1495],{},"같은 AI SDK 생태계 패키지였지만,",[213,1497,1498],{},"Major Version 차이로 인해 내부 Provider Interface가 호환되지 않았다.",[213,1500,1501],{},"결과적으로 Google Provider 버전을 프로젝트의 AI SDK 버전에 맞춰 변경했다.",[245,1503,1505],{"className":923,"code":1504,"filename":925,"language":926,"meta":178,"style":178},"pnpm add @ai-sdk\u002Fgoogle@3\n",[253,1506,1507],{"__ignoreMap":178},[389,1508,1509,1511,1513],{"class":391,"line":392},[389,1510,1460],{"class":395},[389,1512,1463],{"class":936},[389,1514,1515],{"class":936}," @ai-sdk\u002Fgoogle@3\n",[213,1517,1518],{},"이후 Provider Dependency 충돌이 해결되었고,",[213,1520,1521],{},"Google AI Studio 기반 AI Assistant가 정상적으로 동작했다.",[232,1523],{},[208,1525,1527],{"id":1526},"_8-현재-brolog-배포-구조","8. 현재 Brolog 배포 구조",[213,1529,1530],{},"현재 최종 배포 구조는 다음과 같다.",[245,1532,1536],{"className":1533,"code":1534,"filename":1535,"language":251,"meta":178},[248],"\nVercel Build\n\n↓\n\nnuxi prepare ..\u002Flayer\n\n↓\n\nNuxt Build\n\n↓\n\nNitro Runtime Deployment\n\n↓\n\nGoogle AI Studio API\n","Final Architecture",[253,1537,1534],{"__ignoreMap":178},[213,1539,1540],{},"AI Model 설정은:",[245,1542,1546],{"className":1543,"code":1544,"filename":1545,"language":251,"meta":178},[248],"\nEnvironment Variable\n\n↓\n\nModel 선택\n\n↓\n\nAI SDK Provider 연결\n","Model Management",[253,1547,1544],{"__ignoreMap":178},[213,1549,1550],{},"구조로 관리하고 있다.",[232,1552],{},[208,1554,809],{"id":809},[213,1556,1557],{},"이번 배포 과정에서는 단순히 빌드 성공 여부보다,",[213,1559,1560],{},"프로젝트에서 사용하는 기능이 어떤 실행 환경을 요구하는지 확인하는 과정이 중요했다.",[213,1562,1563],{},"처음에는 블로그 서비스라는 목적 때문에 generate 방식이 적합하다고 판단했지만,",[213,1565,1566],{},"실제 사용하고 있는 기능이 Nitro Runtime 기반 기능을 포함하고 있었기 때문에 Build 방식이 더 적합했다.",[213,1568,1569],{},"또한 AI 기능 적용 과정에서는 단순히 API Provider만 변경하는 것이 아니라,",[213,1571,1572],{},"AI SDK 생태계 내부 Dependency와 Provider Version 호환성까지 함께 확인해야 했다.",[213,1574,1575],{},"최종적으로 Brolog은:",[245,1577,1581],{"className":1578,"code":1579,"filename":1580,"language":251,"meta":178},[248],"\nNuxt Multi Layer\n\n+\n\nNuxt Build\n\n+\n\nNitro Runtime\n\n+\n\nGoogle AI Studio\n\n+\n\nEnvironment Variable 기반 Model 관리\n","Final Stack",[253,1582,1579],{"__ignoreMap":178},[213,1584,1585],{},"구조로 운영하게 되었다.",[213,1587,1588],{},"이번 과정은 단순한 배포 설정 변경이 아니라,",[213,1590,1591],{},"Nuxt 애플리케이션이 정적 생성과 Runtime 실행 환경 사이에서 어떻게 동작하는지 확인할 수 있었던 과정이었다.",[851,1593,1594],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}",{"title":178,"searchDepth":186,"depth":186,"links":1596},[1597,1598,1599,1600],{"id":1005,"depth":186,"text":1006},{"id":1065,"depth":186,"text":1066},{"id":1280,"depth":186,"text":1281},{"id":1314,"depth":186,"text":1315},"2026-07-23","Nuxt Multi Layer 프로젝트를 Vercel에 배포한 이후 발생한 Nuxt OG Image와 AI Assistant 문제를 분석하고 Build 방식 전환, Google AI Studio 적용 과정과 SDK 버전 충돌 해결 과정을 기록합니다.",{"tags":1604,"image":1607},[121,43,1605,69,868,869,1606],"Vercel","Deployment","\u002Fimages\u002Fbro-ai-thumbnail3.png",{"icon":26},"\u002Fko\u002Fproject\u002Fbrolog\u002Fvercel-deploy3-ai-integration",{"title":1611,"description":1612},"Nuxt Multi Layer Vercel 배포 - Nuxt Runtime과 AI Assistant 문제 해결 과정","Nuxt generate 배포 이후 발생한 Runtime 기반 기능 문제를 분석하고 Nitro 기반 Build 전환, Vercel AI Gateway 변경, Google AI Studio 적용 과정을 정리합니다.","ko\u002F2.project\u002F1.brolog\u002F10.vercel-deploy3-ai-integration","3iXXdXzhJhSX1jCW26my99O9UzuYF3whbTH5AkIKlEU",{"id":1616,"title":151,"body":1617,"date":2220,"description":2221,"extension":188,"links":863,"meta":2222,"navigation":2227,"path":2228,"seo":2229,"stem":2232,"__hash__":2233},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F9.vercel-deploy2-error-analyze.md",{"type":180,"value":1618,"toc":2215},[1619,2212],[204,1620,1621,1625,1628,1634,1637,1645,1648,1673,1676,1678,1682,1685,1692,1695,1702,1709,1712,1731,1738,1741,1743,1751,1757,1760,1763,1770,1773,1780,1783,1789,1791,1795,1801,1816,1819,1822,1829,1831,1838,1841,1844,1849,1852,1854,1858,1861,1868,1871,1875,1878,1892,1894,1898,1901,1912,1914,1918,1921,1924,1926,1930,1939,1942,1944,1948,1961,1966,1969,1976,1979,1982,1993,1996,2003,2005,2009,2021,2026,2028,2035,2038,2040,2051,2054,2056,2060,2063,2124,2152,2155,2162,2165,2167,2171,2174,2177,2184,2190,2193,2196,2203,2209],{":spotlight":206},[208,1622,1624],{"id":1623},"build-error를-이해하기-위한-nuxt-실행-구조-분석","🔍 Build Error를 이해하기 위한 Nuxt 실행 구조 분석",[213,1626,1627],{},"1편의 배포 과정에서 가장 처음 마주했던 문제는 다음 오류였다.",[245,1629,1632],{"className":1630,"code":1631,"filename":1204,"language":251,"meta":178},[248],"layer\u002F.nuxt\u002Ftsconfig.app.json not found\n",[253,1633,1631],{"__ignoreMap":178},[213,1635,1636],{},"처음에는 단순히 Vercel 설정 문제라고 생각했다.",[213,1638,1639,1640,1644],{},"하지만 문제를 해결하기 위해 과정을 따라가면서 확인해보니, 핵심 원인은 단순한 배포 설정이 아니라 ",[1641,1642,1643],"strong",{},"Nuxt가 프로젝트를 준비하고 빌드하는 과정 자체에 대한 이해 부족","이었다.",[213,1646,1647],{},"특히 이번 문제를 이해하기 위해서는 다음 개념들을 다시 확인할 필요가 있었다.",[1649,1650,1651,1655,1660,1666,1670],"ul",{},[1652,1653,1654],"li",{},"Nuxt Multi Layer 구조",[1652,1656,1657,1659],{},[253,1658,900],{},"가 생성하는 결과물",[1652,1661,1662,1663,1665],{},"Nuxt ",[253,1664,906],{}," 과정",[1652,1667,1662,1668,1665],{},[253,1669,910],{},[1652,1671,1672],{},"SSR과 SSG의 차이",[213,1674,1675],{},"이번 글에서는 배포 과정에서 발생한 오류를 기준으로 Nuxt Build Pipeline이 실제로 어떻게 동작하는지 정리한다.",[232,1677],{},[208,1679,1681],{"id":1680},"_1-nuxt-multi-layer-구조-이해","1. Nuxt Multi Layer 구조 이해",[213,1683,1684],{},"Brolog 프로젝트는 Nuxt Docus 기반으로 구성되어 있으며,\nDocus 내부 구조에서 사용하는 Nuxt Layer 구조를 그대로 활용하고 있다.",[213,1686,1687,1688,1691],{},"Docus는 문서 사이트 제작을 위한 테마 레이어를 제공하고,\n실제 서비스 영역인 ",[253,1689,1690],{},"docs"," 프로젝트는 해당 Layer를 확장하는 형태로 동작한다.",[213,1693,1694],{},"이를 통해 기본 제공되는 컴포넌트, Layout, Middleware 등을 그대로 사용하면서,\n필요한 경우 동일한 경로에 파일을 생성하여 프로젝트 단위로 Override할 수 있다.",[245,1696,1700],{"className":1697,"code":1698,"filename":1699,"language":251,"meta":178},[248],"Nuxt Content + Theme + Layer Architecture\n","Architecture",[253,1701,1698],{"__ignoreMap":178},[245,1703,1707],{"className":1704,"code":1705,"filename":1706,"language":251,"meta":178},[248],"Nuxt\n ├── Nuxt Content (Markdown\u002FData)\n │\n └── Docus (문서 사이트 프레임워크)\n       └── Nuxt Layer 사용\n","Layer Structure",[253,1708,1705],{"__ignoreMap":178},[213,1710,1711],{},"Nuxt에서는 다음과 같은 방식으로 Layer를 확장한다.",[245,1713,1715],{"className":923,"code":1714,"filename":925,"language":926,"meta":178,"style":178},"nuxt build docs --extends ..\u002Flayer\n",[253,1716,1717],{"__ignoreMap":178},[389,1718,1719,1722,1724,1727,1729],{"class":391,"line":392},[389,1720,1721],{"class":395},"nuxt",[389,1723,1192],{"class":936},[389,1725,1726],{"class":936}," docs",[389,1728,957],{"class":936},[389,1730,960],{"class":936},[213,1732,1733,1734,1737],{},"여기서 중요한 점은 ",[253,1735,1736],{},"layer","가 단순히 파일을 가져오는 폴더가 아니라는 것이다.",[213,1739,1740],{},"Nuxt는 빌드 과정에서 Layer 정보를 분석하고, 현재 애플리케이션과 병합하는 준비 과정이 필요하다.",[232,1742],{},[208,1744,1746,1747,1750],{"id":1745},"_2-nuxt-디렉토리의-역할","2. ",[253,1748,1749],{},".nuxt"," 디렉토리의 역할",[213,1752,1753,1754,1756],{},"Nuxt 프로젝트를 개발하다 보면 ",[253,1755,1749],{},"라는 디렉토리가 생성된다.",[213,1758,1759],{},"Nuxt가 프로젝트 구조를 분석하고 자동 생성하는 빌드 준비 결과물이다.",[213,1761,1762],{},"대표적으로 포함되는 내용은 다음과 같다.",[245,1764,1768],{"className":1765,"code":1766,"filename":1767,"language":251,"meta":178},[248],".nuxt\n\n├── tsconfig.app.json\n├── imports.d.ts\n├── components.d.ts\n├── nuxt.config.mjs\n└── generated files\n",".nuxt Directory",[253,1769,1766],{"__ignoreMap":178},[213,1771,1772],{},"이 파일들은 다음 과정에서 활용된다.",[245,1774,1778],{"className":1775,"code":1776,"filename":1777,"language":251,"meta":178},[248],"소스 코드 분석\n\n↓\n\nNuxt 내부 메타데이터 생성\n\n↓\n\nTypeScript 설정 생성\n\n↓\n\nAuto Import 정보 생성\n\n↓\n\nVite 빌드 준비\n","Prepare Flow",[253,1779,1776],{"__ignoreMap":178},[213,1781,1782],{},"로컬 환경에서는 개발 서버 실행 과정에서 이미 생성되어 있기 때문에 문제가 보이지 않았다.",[213,1784,1785,1786,1788],{},"하지만 Vercel은 항상 새로운 환경에서 프로젝트를 설치하기 때문에 기존 ",[253,1787,1749],{}," 결과물이 존재하지 않는 상태에서 빌드가 시작된다.",[232,1790],{},[208,1792,1794],{"id":1793},"_3-nuxi-prepare의-역할","3. nuxi prepare의 역할",[213,1796,1797,1798,1800],{},"이번 오류를 해결하는 핵심은 ",[253,1799,900],{},"였다.",[245,1802,1804],{"className":923,"code":1803,"filename":925,"language":926,"meta":178,"style":178},"npx nuxi prepare ..\u002Flayer\n",[253,1805,1806],{"__ignoreMap":178},[389,1807,1808,1810,1812,1814],{"class":391,"line":392},[389,1809,933],{"class":395},[389,1811,937],{"class":936},[389,1813,940],{"class":936},[389,1815,960],{"class":936},[213,1817,1818],{},"이 명령어는 빌드 이전 단계에서 Nuxt 프로젝트를 분석하고 필요한 파일을 생성하는 준비 과정이다.",[213,1820,1821],{},"대략적인 흐름은 다음과 같다.",[245,1823,1827],{"className":1824,"code":1825,"filename":1826,"language":251,"meta":178},[248],"Nuxt Layer 분석\n\n↓\n\nModule 초기화\n\n↓\n\nAuto Import 분석\n\n↓\n\nType 정의 생성\n\n↓\n\n.nuxt 디렉토리 생성\n","Prepare Process",[253,1828,1825],{"__ignoreMap":178},[213,1830,1340],{},[245,1832,1836],{"className":1833,"code":1834,"filename":1835,"language":251,"meta":178},[248],"prepare 실행 전\n\nlayer\u002F.nuxt 없음\n\n\nprepare 실행 후\n\nlayer\u002F.nuxt 생성\n","Prepare Result",[253,1837,1834],{"__ignoreMap":178},[213,1839,1840],{},"이 된다.",[213,1842,1843],{},"처음 발생했던 오류:",[245,1845,1847],{"className":1846,"code":1631,"filename":1204,"language":251,"meta":178},[248],[253,1848,1631],{"__ignoreMap":178},[213,1850,1851],{},"는 결국 Layer에 대한 prepare 과정이 먼저 실행되지 않았기 때문에 발생한 문제였다.",[232,1853],{},[208,1855,1857],{"id":1856},"_4-nuxt-build-pipeline-흐름","4. Nuxt Build Pipeline 흐름",[213,1859,1860],{},"Nuxt Build의 전체 흐름은 다음과 같다.",[245,1862,1866],{"className":1863,"code":1864,"filename":1865,"language":251,"meta":178},[248],"Source Code\n\n↓\n\nnuxi prepare\n\n↓\n\n.nuxt 생성\n\n↓\n\nVite Compile\n\n↓\n\nVue 코드 변환\n\n↓\n\nNitro Build\n\n↓\n\nDeployment Output 생성\n","Build Pipeline",[253,1867,1864],{"__ignoreMap":178},[213,1869,1870],{},"각 단계별 역할은 다음과 같다.",[481,1872,1874],{"id":1873},"prepare-단계","① Prepare 단계",[213,1876,1877],{},"프로젝트 구조와 설정을 분석한다.",[1649,1879,1880,1883,1886,1889],{},[1652,1881,1882],{},"Layer 병합",[1652,1884,1885],{},"Module 등록",[1652,1887,1888],{},"Auto Import 생성",[1652,1890,1891],{},"TypeScript 설정 생성",[232,1893],{},[481,1895,1897],{"id":1896},"compile-단계","② Compile 단계",[213,1899,1900],{},"Vite를 통해 실제 애플리케이션 코드를 변환한다.",[1649,1902,1903,1906,1909],{},[1652,1904,1905],{},"Vue SFC 변환",[1652,1907,1908],{},"TypeScript 처리",[1652,1910,1911],{},"Bundle 생성",[232,1913],{},[481,1915,1917],{"id":1916},"nitro-단계","③ Nitro 단계",[213,1919,1920],{},"Nuxt의 서버 엔진인 Nitro가 배포 환경에 맞는 결과물을 생성한다.",[213,1922,1923],{},"SSR 방식에서는 이 단계에서 서버 실행 결과물이 만들어진다.",[232,1925],{},[208,1927,1929],{"id":1928},"_5-build와-generate의-차이","5. build와 generate의 차이",[213,1931,1932,1933,1935,1936,1938],{},"이번 배포 과정에서 가장 고민했던 부분은 ",[253,1934,906],{},"와 ",[253,1937,910],{},"의 차이였다.",[213,1940,1941],{},"둘은 비슷해 보이지만 결과적으로 배포 방식이 다르다.",[232,1943],{},[208,1945,1947],{"id":1946},"nuxt-build","nuxt build",[245,1949,1951],{"className":923,"code":1950,"filename":925,"language":926,"meta":178,"style":178},"nuxi build\n",[253,1952,1953],{"__ignoreMap":178},[389,1954,1955,1958],{"class":391,"line":392},[389,1956,1957],{"class":395},"nuxi",[389,1959,1960],{"class":936}," build\n",[213,1962,1963,1965],{},[253,1964,906],{},"는 서버 실행 환경을 포함한 애플리케이션을 생성한다.",[213,1967,1968],{},"흐름:",[245,1970,1974],{"className":1971,"code":1972,"filename":1973,"language":251,"meta":178},[248],"사용자 요청\n\n↓\n\nNitro Server 실행\n\n↓\n\n페이지 생성\n\n↓\n\nHTML Response 반환\n","SSR Flow",[253,1975,1972],{"__ignoreMap":178},[213,1977,1978],{},"대표적으로 SSR(Server Side Rendering) 방식이다.",[213,1980,1981],{},"적합한 서비스:",[1649,1983,1984,1987,1990],{},[1652,1985,1986],{},"로그인 기반 서비스",[1652,1988,1989],{},"실시간 데이터 서비스",[1652,1991,1992],{},"사용자별 페이지",[213,1994,1995],{},"결과물:",[245,1997,2001],{"className":1998,"code":1999,"filename":2000,"language":251,"meta":178},[248],".output\u002F\n","Output",[253,2002,1999],{"__ignoreMap":178},[232,2004],{},[208,2006,2008],{"id":2007},"nuxt-generate","nuxt generate",[245,2010,2012],{"className":923,"code":2011,"filename":925,"language":926,"meta":178,"style":178},"nuxi generate\n",[253,2013,2014],{"__ignoreMap":178},[389,2015,2016,2018],{"class":391,"line":392},[389,2017,1957],{"class":395},[389,2019,2020],{"class":936}," generate\n",[213,2022,2023,2025],{},[253,2024,910],{},"는 빌드 시점에 페이지를 미리 생성한다.",[213,2027,1968],{},[245,2029,2033],{"className":2030,"code":2031,"filename":2032,"language":251,"meta":178},[248],"Build Time\n\n↓\n\n페이지 생성\n\n↓\n\nHTML 파일 생성\n\n↓\n\nStatic Hosting 배포\n","SSG Flow",[253,2034,2031],{"__ignoreMap":178},[213,2036,2037],{},"대표적으로 SSG(Static Site Generation) 방식이다.",[213,2039,1981],{},[1649,2041,2042,2045,2048],{},[1652,2043,2044],{},"블로그",[1652,2046,2047],{},"문서 사이트",[1652,2049,2050],{},"기술 아카이브",[213,2052,2053],{},"Brolog 역시 개발 기록과 기술 문서를 제공하는 목적이기 때문에 정적 생성 방식이 적합했다.",[232,2055],{},[208,2057,2059],{"id":2058},"_6-csr-ssr-ssg-isr-차이","6. CSR, SSR, SSG, ISR 차이",[213,2061,2062],{},"Nuxt에서 렌더링 방식을 이해하기 위해서는 CSR, SSR, SSG, ISR 차이를 이해해야 한다.",[588,2064,2065,2078],{},[591,2066,2067],{},[594,2068,2069,2072,2075],{},[597,2070,2071],{},"방식",[597,2073,2074],{},"생성 시점",[597,2076,2077],{},"특징",[604,2079,2080,2091,2102,2113],{},[594,2081,2082,2085,2088],{},[609,2083,2084],{},"CSR",[609,2086,2087],{},"브라우저 실행 시",[609,2089,2090],{},"SPA 구조, 초기 JS 로딩 필요",[594,2092,2093,2096,2099],{},[609,2094,2095],{},"SSR",[609,2097,2098],{},"요청 시 서버",[609,2100,2101],{},"최신 데이터 제공 가능",[594,2103,2104,2107,2110],{},[609,2105,2106],{},"SSG",[609,2108,2109],{},"빌드 시점",[609,2111,2112],{},"빠른 응답, 정적 콘텐츠 적합",[594,2114,2115,2118,2121],{},[609,2116,2117],{},"ISR",[609,2119,2120],{},"일부 재생성",[609,2122,2123],{},"SSG + 동적 갱신",[2125,2126,2127,2130,2136,2139,2146],"blockquote",{},[213,2128,2129],{},"📌 추가 정리",[213,2131,2132,2133,2135],{},"이번 배포 과정에서는 ",[253,2134,910],{},"를 선택하면서 SSG 방식으로 배포하게 되었지만, Nuxt는 하나의 렌더링 방식만 지원하는 프레임워크가 아니다.",[213,2137,2138],{},"CSR, SSR, SSG, ISR 각각의 차이와 Nuxt에서 이를 어떻게 선택하고 적용하는지는 별도의 글에서 정리한다.",[213,2140,2141,2142],{},"👉 ",[2143,2144,111],"a",{"href":2145},"\u002Fnote\u002Fweb\u002Frendering-modes",[213,2147,2141,2148],{},[2143,2149,2151],{"href":2150},"\u002Fnote\u002Fnuxt4\u002Fnuxt-rendering-strategy","Nuxt 렌더링 방식 이해: SSR, SSG, ISR 구현 방법",[213,2153,2154],{},"Brolog 프로젝트 기준으로 보면:",[245,2156,2160],{"className":2157,"code":2158,"filename":2159,"language":251,"meta":178},[248],"기술 블로그\n\n↓\n\nContent 중심 \u002F 변경 빈도 낮음\n\n↓\n\nSEO 중요\n\n↓\n\n정적 페이지 적합\n\n↓\n\nSSG(generate) 선택\n","Decision Flow",[253,2161,2158],{"__ignoreMap":178},[213,2163,2164],{},"이라는 판단이 가능했다.",[232,2166],{},[208,2168,2170],{"id":2169},"_7-이번-오류를-통해-이해한-nuxt-실행-구조","7. 이번 오류를 통해 이해한 Nuxt 실행 구조",[213,2172,2173],{},"이번 문제는 단순히 Vercel 설정을 변경해서 해결한 문제가 아니었다.",[213,2175,2176],{},"실제 원인은 다음 구조였다.",[245,2178,2182],{"className":2179,"code":2180,"filename":2181,"language":251,"meta":178},[248],"Nuxt Multi Layer\n\n+\n\nVercel 신규 빌드 환경\n\n+\n\nLayer prepare 과정 필요\n","Cause Analysis",[253,2183,2180],{"__ignoreMap":178},[213,2185,2186,2187,2189],{},"로컬에서는 이미 생성되어 있던 ",[253,2188,1749],{}," 파일이 있었기 때문에 문제가 드러나지 않았다.",[213,2191,2192],{},"하지만 CI\u002FCD 환경에서는 항상 초기 상태에서 시작하기 때문에 Nuxt가 필요한 준비 과정을 명확하게 지정해야 한다.",[213,2194,2195],{},"최종 배포 흐름은 다음과 같다.",[245,2197,2201],{"className":2198,"code":2199,"filename":2200,"language":251,"meta":178},[248],"Vercel Build Start\n\n↓\n\nlayer prepare\n\n↓\n\ndocs + layer 병합\n\n↓\n\nNuxt generate\n\n↓\n\nStatic Deployment\n","Deployment Flow",[253,2202,2199],{"__ignoreMap":178},[213,2204,2205,2206,2208],{},"이번 경험을 통해 Nuxt 프로젝트에서는 단순히 ",[253,2207,906],{}," 명령어를 실행하는 것이 아니라,",[213,2210,2211],{},"프로젝트 구조와 배포 방식에 맞는 Build Pipeline을 이해하는 것이 중요하다는 것을 확인했다.",[851,2213,2214],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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":178,"searchDepth":186,"depth":186,"links":2216},[2217,2218,2219],{"id":1873,"depth":186,"text":1874},{"id":1896,"depth":186,"text":1897},{"id":1916,"depth":186,"text":1917},"2026-07-22","Nuxt Multi Layer 프로젝트에서 발생한 layer .nuxt 생성 오류를 기반으로 Nuxt 실행 구조, nuxi prepare 과정, build와 generate의 차이를 분석",{"tags":2223,"image":2226},[43,2224,1605,2225,2095,2106],"NuxtLayer","BuildPipeline","\u002Fimages\u002Fbro-build-thumbnail.png",{"icon":26},"\u002Fko\u002Fproject\u002Fbrolog\u002Fvercel-deploy2-error-analyze",{"title":2230,"description":2231},"Nuxt Multi Layer Build Pipeline과 nuxi prepare, build, generate 실행 구조 분석","Vercel 배포 과정에서 발생한 layer .nuxt 오류를 통해 Nuxt Layer 구조와 Build Pipeline, nuxi prepare, SSR, SSG, generate 동작 방식을 정리합니다.","ko\u002F2.project\u002F1.brolog\u002F9.vercel-deploy2-error-analyze","ZduvCWUVy5SpB_CdG2MXsgLZgUfbf_DRj-ODLEHChkY",{"id":2235,"title":65,"body":2236,"date":3396,"description":3397,"extension":188,"links":863,"meta":3398,"navigation":3402,"path":3403,"seo":3404,"stem":3407,"__hash__":3408},"docs_ko\u002Fko\u002F1.note\u002F2.nuxt4\u002F5.nuxt-rendering-strategy.md",{"type":180,"value":2237,"toc":3379},[2238,3376],[204,2239,2240,2244,2251,2254,2268,2271,2274,2276,2280,2283,2286,2293,2296,2300,2303,2423,2426,2430,2433,2444,2451,2454,2456,2460,2463,2466,2473,2476,2478,2482,2485,2606,2609,2611,2615,2620,2623,2627,2630,2634,2641,2644,2646,2650,2653,2660,2663,2665,2669,2675,2692,2695,2702,2705,2707,2711,2714,2804,2807,2809,2813,2816,2829,2831,2835,2838,2841,2848,2850,2854,2868,2950,2953,2960,2962,2966,2969,2976,2979,2990,2993,2995,2999,3053,3055,3059,3062,3065,3067,3231,3234,3241,3244,3246,3250,3258,3260,3277,3280,3283,3290,3293,3296,3303,3305,3307,3323,3326,3328,3335,3338,3340,3347,3349,3351,3354,3357,3367,3370,3373],{":spotlight":206},[208,2241,2243],{"id":2242},"nuxt-렌더링-방식-이해하기","🌐 Nuxt 렌더링 방식 이해하기",[213,2245,2246,2247,2250],{},"웹 애플리케이션을 개발할 때 중요한 결정 중 하나는 ",[1641,2248,2249],{},"페이지를 언제 생성할 것인가","이다.",[213,2252,2253],{},"같은 Nuxt 애플리케이션이라도 설정에 따라:",[1649,2255,2256,2259,2262,2265],{},[1652,2257,2258],{},"브라우저에서 생성하는 방식 (CSR)",[1652,2260,2261],{},"서버 요청마다 생성하는 방식 (SSR)",[1652,2263,2264],{},"빌드 시 미리 생성하는 방식 (SSG)",[1652,2266,2267],{},"일정 주기로 재생성하는 방식 (ISR)",[213,2269,2270],{},"으로 동작 방식을 변경할 수 있다.",[213,2272,2273],{},"Nuxt는 특정 하나의 렌더링 방식만 제공하는 프레임워크가 아니라, 페이지 성격에 맞게 여러 렌더링 전략을 조합할 수 있도록 설계되어 있다.",[232,2275],{},[208,2277,2279],{"id":2278},"_1-csr-client-side-rendering","1. CSR (Client Side Rendering)",[213,2281,2282],{},"CSR은 브라우저에서 JavaScript를 실행한 뒤 화면을 생성하는 방식이다.",[213,2284,2285],{},"전통적인 SPA(Vue CLI, React SPA 등)에서 사용하는 대표적인 방식이다.",[245,2287,2291],{"className":2288,"code":2289,"filename":2290,"language":251,"meta":178},[248],"\n사용자 요청\n\n↓\n\nHTML 반환\n(빈 Root Element)\n\n↓\n\nJavaScript 다운로드\n\n↓\n\n브라우저에서 Vue 실행\n\n↓\n\n화면 렌더링\n","CSR Rendering Flow",[253,2292,2289],{"__ignoreMap":178},[213,2294,2295],{},"초기 HTML에는 실제 콘텐츠가 포함되지 않고, 브라우저에서 Vue 애플리케이션이 실행된 이후 화면이 생성된다.",[481,2297,2299],{"id":2298},"nuxt에서-csr-사용하기","Nuxt에서 CSR 사용하기",[213,2301,2302],{},"Nuxt는 기본적으로 SSR을 지원하지만 특정 페이지를 CSR 방식으로 변경할 수 있다.",[245,2304,2309],{"className":2305,"code":2306,"filename":2307,"language":2308,"meta":178,"style":178},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\n\u003Cscript setup>\ndefinePageMeta({\n  ssr: false\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    Client Side Rendering 페이지\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","pages\u002Fadmin.vue","vue",[253,2310,2311,2316,2331,2342,2354,2362,2372,2377,2387,2398,2404,2414],{"__ignoreMap":178},[389,2312,2313],{"class":391,"line":392},[389,2314,2315],{"emptyLinePlaceholder":190},"\n",[389,2317,2318,2321,2325,2328],{"class":391,"line":186},[389,2319,2320],{"class":399},"\u003C",[389,2322,2324],{"class":2323},"swJcz","script",[389,2326,2327],{"class":454}," setup",[389,2329,2330],{"class":399},">\n",[389,2332,2334,2337,2339],{"class":391,"line":2333},3,[389,2335,2336],{"class":403},"definePageMeta",[389,2338,408],{"class":407},[389,2340,2341],{"class":399},"{\n",[389,2343,2345,2348,2350],{"class":391,"line":2344},4,[389,2346,2347],{"class":2323},"  ssr",[389,2349,400],{"class":399},[389,2351,2353],{"class":2352},"sfNiH"," false\n",[389,2355,2357,2360],{"class":391,"line":2356},5,[389,2358,2359],{"class":399},"}",[389,2361,415],{"class":407},[389,2363,2365,2368,2370],{"class":391,"line":2364},6,[389,2366,2367],{"class":399},"\u003C\u002F",[389,2369,2324],{"class":2323},[389,2371,2330],{"class":399},[389,2373,2375],{"class":391,"line":2374},7,[389,2376,2315],{"emptyLinePlaceholder":190},[389,2378,2380,2382,2385],{"class":391,"line":2379},8,[389,2381,2320],{"class":399},[389,2383,2384],{"class":2323},"template",[389,2386,2330],{"class":399},[389,2388,2390,2393,2396],{"class":391,"line":2389},9,[389,2391,2392],{"class":399},"  \u003C",[389,2394,2395],{"class":2323},"div",[389,2397,2330],{"class":399},[389,2399,2401],{"class":391,"line":2400},10,[389,2402,2403],{"class":407},"    Client Side Rendering 페이지\n",[389,2405,2407,2410,2412],{"class":391,"line":2406},11,[389,2408,2409],{"class":399},"  \u003C\u002F",[389,2411,2395],{"class":2323},[389,2413,2330],{"class":399},[389,2415,2417,2419,2421],{"class":391,"line":2416},12,[389,2418,2367],{"class":399},[389,2420,2384],{"class":2323},[389,2422,2330],{"class":399},[213,2424,2425],{},"해당 페이지는 서버에서 HTML을 생성하지 않고 브라우저에서만 렌더링된다.",[481,2427,2429],{"id":2428},"csr-사용-사례","CSR 사용 사례",[213,2431,2432],{},"적합한 영역:",[1649,2434,2435,2438,2441],{},[1652,2436,2437],{},"관리자 페이지",[1652,2439,2440],{},"인증이 필요한 대시보드",[1652,2442,2443],{},"SEO가 중요하지 않은 서비스 영역",[245,2445,2449],{"className":2446,"code":2447,"filename":2448,"language":251,"meta":178},[248],"\u002Fadmin\u002Fdashboard\n\n\u002Fmember\u002Fsettings\n","CSR Page Example",[253,2450,2447],{"__ignoreMap":178},[213,2452,2453],{},"검색 엔진 노출보다 사용자 인터랙션이 중요한 경우 적합하다.",[232,2455],{},[208,2457,2459],{"id":2458},"_2-ssr-server-side-rendering","2. SSR (Server Side Rendering)",[213,2461,2462],{},"SSR은 사용자가 페이지를 요청할 때 서버에서 HTML을 생성하는 방식이다.",[213,2464,2465],{},"Nuxt의 기본 렌더링 방식이다.",[245,2467,2471],{"className":2468,"code":2469,"filename":2470,"language":251,"meta":178},[248],"\n사용자 요청\n\n↓\n\nNuxt Server\n\n↓\n\nVue Component 실행\n\n↓\n\nHTML 생성\n\n↓\n\n브라우저 전달\n\n↓\n\nHydration\n","SSR Rendering Flow",[253,2472,2469],{"__ignoreMap":178},[213,2474,2475],{},"서버에서 완성된 HTML을 전달하기 때문에 초기 화면 표시가 빠르고 SEO에 유리하다.",[232,2477],{},[481,2479,2481],{"id":2480},"nuxt-기본-ssr-동작","Nuxt 기본 SSR 동작",[213,2483,2484],{},"별도의 설정 없이 Nuxt 페이지는 SSR로 동작한다.",[245,2486,2489],{"className":2305,"code":2487,"filename":2488,"language":2308,"meta":178,"style":178},"\n\u003Cscript setup>\n\nconst { data } = await useFetch('\u002Fapi\u002Fposts')\n\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n\n  \u003Cdiv>\n    {{ data }}\n  \u003C\u002Fdiv>\n\n\u003C\u002Ftemplate>\n","pages\u002Fposts.vue",[253,2490,2491,2495,2505,2509,2543,2547,2555,2559,2567,2571,2579,2584,2592,2597],{"__ignoreMap":178},[389,2492,2493],{"class":391,"line":392},[389,2494,2315],{"emptyLinePlaceholder":190},[389,2496,2497,2499,2501,2503],{"class":391,"line":186},[389,2498,2320],{"class":399},[389,2500,2324],{"class":2323},[389,2502,2327],{"class":454},[389,2504,2330],{"class":399},[389,2506,2507],{"class":391,"line":2333},[389,2508,2315],{"emptyLinePlaceholder":190},[389,2510,2511,2513,2516,2519,2521,2524,2528,2531,2533,2536,2539,2541],{"class":391,"line":2344},[389,2512,455],{"class":454},[389,2514,2515],{"class":399}," {",[389,2517,2518],{"class":407}," data ",[389,2520,2359],{"class":399},[389,2522,2523],{"class":399}," =",[389,2525,2527],{"class":2526},"s7zQu"," await",[389,2529,2530],{"class":403}," useFetch",[389,2532,408],{"class":407},[389,2534,2535],{"class":399},"'",[389,2537,2538],{"class":936},"\u002Fapi\u002Fposts",[389,2540,2535],{"class":399},[389,2542,415],{"class":407},[389,2544,2545],{"class":391,"line":2356},[389,2546,2315],{"emptyLinePlaceholder":190},[389,2548,2549,2551,2553],{"class":391,"line":2364},[389,2550,2367],{"class":399},[389,2552,2324],{"class":2323},[389,2554,2330],{"class":399},[389,2556,2557],{"class":391,"line":2374},[389,2558,2315],{"emptyLinePlaceholder":190},[389,2560,2561,2563,2565],{"class":391,"line":2379},[389,2562,2320],{"class":399},[389,2564,2384],{"class":2323},[389,2566,2330],{"class":399},[389,2568,2569],{"class":391,"line":2389},[389,2570,2315],{"emptyLinePlaceholder":190},[389,2572,2573,2575,2577],{"class":391,"line":2400},[389,2574,2392],{"class":399},[389,2576,2395],{"class":2323},[389,2578,2330],{"class":399},[389,2580,2581],{"class":391,"line":2406},[389,2582,2583],{"class":407},"    {{ data }}\n",[389,2585,2586,2588,2590],{"class":391,"line":2416},[389,2587,2409],{"class":399},[389,2589,2395],{"class":2323},[389,2591,2330],{"class":399},[389,2593,2595],{"class":391,"line":2594},13,[389,2596,2315],{"emptyLinePlaceholder":190},[389,2598,2600,2602,2604],{"class":391,"line":2599},14,[389,2601,2367],{"class":399},[389,2603,2384],{"class":2323},[389,2605,2330],{"class":399},[213,2607,2608],{},"위 코드는 서버에서 먼저 실행되어 HTML 생성 과정에 포함된다.",[232,2610],{},[481,2612,2614],{"id":2613},"ssr-장점","SSR 장점",[2616,2617,2619],"h3",{"id":2618},"seo-최적화","SEO 최적화",[213,2621,2622],{},"검색 엔진 크롤러가 완성된 HTML을 받을 수 있다.",[2616,2624,2626],{"id":2625},"최신-데이터-제공","최신 데이터 제공",[213,2628,2629],{},"요청 시 서버에서 데이터를 조회하기 때문에 항상 최신 상태를 유지할 수 있다.",[2616,2631,2633],{"id":2632},"동적-서비스-적합","동적 서비스 적합",[245,2635,2639],{"className":2636,"code":2637,"filename":2638,"language":251,"meta":178},[248],"\n\u002Fprofile\u002F:id\n\n\u002Fproduct\u002F:id\n\n\u002Fdashboard\n","SSR Page Example",[253,2640,2637],{"__ignoreMap":178},[213,2642,2643],{},"사용자마다 다른 데이터를 보여줘야 하는 페이지에 적합하다.",[232,2645],{},[208,2647,2649],{"id":2648},"_3-ssg-static-site-generation","3. SSG (Static Site Generation)",[213,2651,2652],{},"SSG는 빌드 시점에 HTML을 미리 생성하는 방식이다.",[245,2654,2658],{"className":2655,"code":2656,"filename":2657,"language":251,"meta":178},[248],"\nBuild 실행\n\n↓\n\nNuxt 페이지 실행\n\n↓\n\nHTML 생성\n\n↓\n\n정적 파일 저장\n\n↓\n\nCDN 제공\n","SSG Rendering Flow",[253,2659,2656],{"__ignoreMap":178},[213,2661,2662],{},"사용자가 방문할 때 서버에서 생성하지 않고 이미 만들어진 HTML 파일을 반환한다.",[232,2664],{},[481,2666,2668],{"id":2667},"nuxt에서-ssg-생성하기","Nuxt에서 SSG 생성하기",[213,2670,2671,2672,2674],{},"Nuxt에서는 ",[253,2673,910],{}," 명령어를 사용한다.",[245,2676,2678],{"className":923,"code":2677,"filename":925,"language":926,"meta":178,"style":178},"\nnpx nuxi generate\n",[253,2679,2680,2684],{"__ignoreMap":178},[389,2681,2682],{"class":391,"line":392},[389,2683,2315],{"emptyLinePlaceholder":190},[389,2685,2686,2688,2690],{"class":391,"line":186},[389,2687,933],{"class":395},[389,2689,937],{"class":936},[389,2691,2020],{"class":936},[213,2693,2694],{},"빌드 결과:",[245,2696,2700],{"className":2697,"code":2698,"filename":2699,"language":251,"meta":178},[248],"\n.output\u002F\n\n├── public\n\n└── generated html\n","Generate Output Structure",[253,2701,2698],{"__ignoreMap":178},[213,2703,2704],{},"형태의 정적 결과물이 생성된다.",[232,2706],{},[481,2708,2710],{"id":2709},"특정-페이지-정적-생성","특정 페이지 정적 생성",[213,2712,2713],{},"Nuxt 설정에서 prerender 대상을 지정할 수 있다.",[245,2715,2718],{"className":382,"code":2716,"filename":2717,"language":385,"meta":178,"style":178},"\nexport default defineNuxtConfig({\n\n  routeRules: {\n\n    '\u002Fblog\u002F**': {\n      prerender: true\n    }\n\n  }\n\n})\n","nuxt.config.ts",[253,2719,2720,2724,2738,2742,2752,2756,2770,2780,2785,2789,2794,2798],{"__ignoreMap":178},[389,2721,2722],{"class":391,"line":392},[389,2723,2315],{"emptyLinePlaceholder":190},[389,2725,2726,2728,2731,2734,2736],{"class":391,"line":186},[389,2727,1231],{"class":2526},[389,2729,2730],{"class":2526}," default",[389,2732,2733],{"class":403}," defineNuxtConfig",[389,2735,408],{"class":407},[389,2737,2341],{"class":399},[389,2739,2740],{"class":391,"line":2333},[389,2741,2315],{"emptyLinePlaceholder":190},[389,2743,2744,2747,2749],{"class":391,"line":2344},[389,2745,2746],{"class":2323},"  routeRules",[389,2748,400],{"class":399},[389,2750,2751],{"class":399}," {\n",[389,2753,2754],{"class":391,"line":2356},[389,2755,2315],{"emptyLinePlaceholder":190},[389,2757,2758,2761,2764,2766,2768],{"class":391,"line":2364},[389,2759,2760],{"class":399},"    '",[389,2762,2763],{"class":2323},"\u002Fblog\u002F**",[389,2765,2535],{"class":399},[389,2767,400],{"class":399},[389,2769,2751],{"class":399},[389,2771,2772,2775,2777],{"class":391,"line":2374},[389,2773,2774],{"class":2323},"      prerender",[389,2776,400],{"class":399},[389,2778,2779],{"class":2352}," true\n",[389,2781,2782],{"class":391,"line":2379},[389,2783,2784],{"class":399},"    }\n",[389,2786,2787],{"class":391,"line":2389},[389,2788,2315],{"emptyLinePlaceholder":190},[389,2790,2791],{"class":391,"line":2400},[389,2792,2793],{"class":399},"  }\n",[389,2795,2796],{"class":391,"line":2406},[389,2797,2315],{"emptyLinePlaceholder":190},[389,2799,2800,2802],{"class":391,"line":2416},[389,2801,2359],{"class":399},[389,2803,415],{"class":407},[213,2805,2806],{},"해당 경로는 빌드 과정에서 HTML이 생성된다.",[232,2808],{},[481,2810,2812],{"id":2811},"ssg-사용-사례","SSG 사용 사례",[213,2814,2815],{},"정적 콘텐츠 중심 서비스:",[1649,2817,2818,2821,2823,2826],{},[1652,2819,2820],{},"기술 블로그",[1652,2822,2047],{},[1652,2824,2825],{},"포트폴리오",[1652,2827,2828],{},"마케팅 페이지",[232,2830],{},[208,2832,2834],{"id":2833},"_4-isr-incremental-static-regeneration","4. ISR (Incremental Static Regeneration)",[213,2836,2837],{},"ISR은 SSG와 SSR의 중간 형태다.",[213,2839,2840],{},"기본적으로 정적 페이지를 제공하지만, 일정 시간이 지나면 서버에서 페이지를 다시 생성한다.",[245,2842,2846],{"className":2843,"code":2844,"filename":2845,"language":251,"meta":178},[248],"\n첫 요청\n\n↓\n\n기존 생성된 HTML 반환\n\n↓\n\n재생성 시간 확인\n\n↓\n\n필요하면 백그라운드 재생성\n\n↓\n\n다음 요청부터 새로운 HTML 제공\n","ISR Rendering Flow",[253,2847,2844],{"__ignoreMap":178},[232,2849],{},[481,2851,2853],{"id":2852},"nuxt에서-isr-사용하기","Nuxt에서 ISR 사용하기",[213,2855,2671,2856,2859,2860,2863,2864,2867],{},[253,2857,2858],{},"routeRules","의 ",[253,2861,2862],{},"swr"," 또는 ",[253,2865,2866],{},"isr"," 옵션을 활용한다.",[245,2869,2871],{"className":382,"code":2870,"filename":2717,"language":385,"meta":178,"style":178},"\nexport default defineNuxtConfig({\n\n  routeRules: {\n\n    '\u002Fposts\u002F**': {\n      swr: 3600\n    }\n\n  }\n\n})\n",[253,2872,2873,2877,2889,2893,2901,2905,2918,2928,2932,2936,2940,2944],{"__ignoreMap":178},[389,2874,2875],{"class":391,"line":392},[389,2876,2315],{"emptyLinePlaceholder":190},[389,2878,2879,2881,2883,2885,2887],{"class":391,"line":186},[389,2880,1231],{"class":2526},[389,2882,2730],{"class":2526},[389,2884,2733],{"class":403},[389,2886,408],{"class":407},[389,2888,2341],{"class":399},[389,2890,2891],{"class":391,"line":2333},[389,2892,2315],{"emptyLinePlaceholder":190},[389,2894,2895,2897,2899],{"class":391,"line":2344},[389,2896,2746],{"class":2323},[389,2898,400],{"class":399},[389,2900,2751],{"class":399},[389,2902,2903],{"class":391,"line":2356},[389,2904,2315],{"emptyLinePlaceholder":190},[389,2906,2907,2909,2912,2914,2916],{"class":391,"line":2364},[389,2908,2760],{"class":399},[389,2910,2911],{"class":2323},"\u002Fposts\u002F**",[389,2913,2535],{"class":399},[389,2915,400],{"class":399},[389,2917,2751],{"class":399},[389,2919,2920,2923,2925],{"class":391,"line":2374},[389,2921,2922],{"class":2323},"      swr",[389,2924,400],{"class":399},[389,2926,2927],{"class":411}," 3600\n",[389,2929,2930],{"class":391,"line":2379},[389,2931,2784],{"class":399},[389,2933,2934],{"class":391,"line":2389},[389,2935,2315],{"emptyLinePlaceholder":190},[389,2937,2938],{"class":391,"line":2400},[389,2939,2793],{"class":399},[389,2941,2942],{"class":391,"line":2406},[389,2943,2315],{"emptyLinePlaceholder":190},[389,2945,2946,2948],{"class":391,"line":2416},[389,2947,2359],{"class":399},[389,2949,415],{"class":407},[213,2951,2952],{},"의미:",[245,2954,2958],{"className":2955,"code":2956,"filename":2957,"language":251,"meta":178},[248],"\n3600초 동안 생성된 HTML 재사용\n\n↓\n\n시간 만료 후 새로운 HTML 생성\n","ISR Cache Strategy",[253,2959,2956],{"__ignoreMap":178},[232,2961],{},[481,2963,2965],{"id":2964},"isr-사용-사례","ISR 사용 사례",[213,2967,2968],{},"자주 변경되지만 매 요청마다 SSR 할 필요가 없는 서비스.",[245,2970,2974],{"className":2971,"code":2972,"filename":2973,"language":251,"meta":178},[248],"\n\u002Fnews\n\n\u002Fproducts\n\n\u002Franking\n","ISR Page Example",[253,2975,2972],{"__ignoreMap":178},[213,2977,2978],{},"특히:",[1649,2980,2981,2984,2987],{},[1652,2982,2983],{},"쇼핑몰 상품 페이지",[1652,2985,2986],{},"뉴스 사이트",[1652,2988,2989],{},"콘텐츠 플랫폼",[213,2991,2992],{},"같은 서비스에서 유용하다.",[232,2994],{},[208,2996,2998],{"id":2997},"_5-ssr-ssg-isr-선택-기준","5. SSR \u002F SSG \u002F ISR 선택 기준",[588,3000,3001,3012],{},[591,3002,3003],{},[594,3004,3005,3007,3009],{},[597,3006,2071],{},[597,3008,2074],{},[597,3010,3011],{},"적합한 서비스",[604,3013,3014,3023,3033,3043],{},[594,3015,3016,3018,3020],{},[609,3017,2084],{},[609,3019,2087],{},[609,3021,3022],{},"관리자 페이지, 앱 서비스",[594,3024,3025,3027,3030],{},[609,3026,2095],{},[609,3028,3029],{},"요청 시 서버 생성",[609,3031,3032],{},"로그인 기반 서비스, 동적 데이터",[594,3034,3035,3037,3040],{},[609,3036,2106],{},[609,3038,3039],{},"빌드 시 생성",[609,3041,3042],{},"블로그, 문서 사이트",[594,3044,3045,3047,3050],{},[609,3046,2117],{},[609,3048,3049],{},"일정 주기 재생성",[609,3051,3052],{},"뉴스, 상품, 콘텐츠 서비스",[232,3054],{},[208,3056,3058],{"id":3057},"_6-nuxt에서는-여러-방식을-함께-사용할-수-있다","6. Nuxt에서는 여러 방식을 함께 사용할 수 있다",[213,3060,3061],{},"Nuxt의 장점은 프로젝트 전체가 하나의 렌더링 방식으로 고정되지 않는다는 점이다.",[213,3063,3064],{},"페이지별로 렌더링 전략을 다르게 설정할 수 있다.",[213,3066,379],{},[245,3068,3070],{"className":382,"code":3069,"filename":2717,"language":385,"meta":178,"style":178},"\nexport default defineNuxtConfig({\n\n  routeRules: {\n\n    \u002F\u002F 블로그 글\n    '\u002Fblog\u002F**': {\n      prerender: true\n    },\n\n    \u002F\u002F 상품 페이지\n    '\u002Fproducts\u002F**': {\n      swr: 3600\n    },\n\n    \u002F\u002F 관리자 페이지\n    '\u002Fadmin\u002F**': {\n      ssr: false\n    }\n\n  }\n\n})\n",[253,3071,3072,3076,3088,3092,3100,3104,3110,3122,3130,3135,3139,3144,3157,3165,3169,3174,3180,3194,3204,3209,3214,3219,3224],{"__ignoreMap":178},[389,3073,3074],{"class":391,"line":392},[389,3075,2315],{"emptyLinePlaceholder":190},[389,3077,3078,3080,3082,3084,3086],{"class":391,"line":186},[389,3079,1231],{"class":2526},[389,3081,2730],{"class":2526},[389,3083,2733],{"class":403},[389,3085,408],{"class":407},[389,3087,2341],{"class":399},[389,3089,3090],{"class":391,"line":2333},[389,3091,2315],{"emptyLinePlaceholder":190},[389,3093,3094,3096,3098],{"class":391,"line":2344},[389,3095,2746],{"class":2323},[389,3097,400],{"class":399},[389,3099,2751],{"class":399},[389,3101,3102],{"class":391,"line":2356},[389,3103,2315],{"emptyLinePlaceholder":190},[389,3105,3106],{"class":391,"line":2364},[389,3107,3109],{"class":3108},"sHwdD","    \u002F\u002F 블로그 글\n",[389,3111,3112,3114,3116,3118,3120],{"class":391,"line":2374},[389,3113,2760],{"class":399},[389,3115,2763],{"class":2323},[389,3117,2535],{"class":399},[389,3119,400],{"class":399},[389,3121,2751],{"class":399},[389,3123,3124,3126,3128],{"class":391,"line":2379},[389,3125,2774],{"class":2323},[389,3127,400],{"class":399},[389,3129,2779],{"class":2352},[389,3131,3132],{"class":391,"line":2389},[389,3133,3134],{"class":399},"    },\n",[389,3136,3137],{"class":391,"line":2400},[389,3138,2315],{"emptyLinePlaceholder":190},[389,3140,3141],{"class":391,"line":2406},[389,3142,3143],{"class":3108},"    \u002F\u002F 상품 페이지\n",[389,3145,3146,3148,3151,3153,3155],{"class":391,"line":2416},[389,3147,2760],{"class":399},[389,3149,3150],{"class":2323},"\u002Fproducts\u002F**",[389,3152,2535],{"class":399},[389,3154,400],{"class":399},[389,3156,2751],{"class":399},[389,3158,3159,3161,3163],{"class":391,"line":2594},[389,3160,2922],{"class":2323},[389,3162,400],{"class":399},[389,3164,2927],{"class":411},[389,3166,3167],{"class":391,"line":2599},[389,3168,3134],{"class":399},[389,3170,3172],{"class":391,"line":3171},15,[389,3173,2315],{"emptyLinePlaceholder":190},[389,3175,3177],{"class":391,"line":3176},16,[389,3178,3179],{"class":3108},"    \u002F\u002F 관리자 페이지\n",[389,3181,3183,3185,3188,3190,3192],{"class":391,"line":3182},17,[389,3184,2760],{"class":399},[389,3186,3187],{"class":2323},"\u002Fadmin\u002F**",[389,3189,2535],{"class":399},[389,3191,400],{"class":399},[389,3193,2751],{"class":399},[389,3195,3197,3200,3202],{"class":391,"line":3196},18,[389,3198,3199],{"class":2323},"      ssr",[389,3201,400],{"class":399},[389,3203,2353],{"class":2352},[389,3205,3207],{"class":391,"line":3206},19,[389,3208,2784],{"class":399},[389,3210,3212],{"class":391,"line":3211},20,[389,3213,2315],{"emptyLinePlaceholder":190},[389,3215,3217],{"class":391,"line":3216},21,[389,3218,2793],{"class":399},[389,3220,3222],{"class":391,"line":3221},22,[389,3223,2315],{"emptyLinePlaceholder":190},[389,3225,3227,3229],{"class":391,"line":3226},23,[389,3228,2359],{"class":399},[389,3230,415],{"class":407},[213,3232,3233],{},"하나의 Nuxt 프로젝트 안에서:",[245,3235,3239],{"className":3236,"code":3237,"filename":3238,"language":251,"meta":178},[248],"\nBlog\n\n→ SSG\n\n\nProduct\n\n→ ISR\n\n\nAdmin\n\n→ CSR\n\n\nUser Profile\n\n→ SSR\n","Hybrid Rendering Strategy",[253,3240,3237],{"__ignoreMap":178},[213,3242,3243],{},"처럼 목적에 맞는 조합이 가능하다.",[232,3245],{},[208,3247,3249],{"id":3248},"_7-build와-generate의-관계","7. build와 generate의 관계",[213,3251,3252,3253,1935,3255,3257],{},"Nuxt에서 자주 혼동하는 부분은 ",[253,3254,906],{},[253,3256,910],{},"의 차이다.",[481,3259,906],{"id":906},[245,3261,3263],{"className":923,"code":3262,"filename":925,"language":926,"meta":178,"style":178},"\nnpx nuxi build\n",[253,3264,3265,3269],{"__ignoreMap":178},[389,3266,3267],{"class":391,"line":392},[389,3268,2315],{"emptyLinePlaceholder":190},[389,3270,3271,3273,3275],{"class":391,"line":186},[389,3272,933],{"class":395},[389,3274,937],{"class":936},[389,3276,1960],{"class":936},[213,3278,3279],{},"Nuxt 애플리케이션을 프로덕션 실행 형태로 빌드한다.",[213,3281,3282],{},"결과:",[245,3284,3288],{"className":3285,"code":3286,"filename":3287,"language":251,"meta":178},[248],"\n.output\u002F\n\n├── server\n├── public\n└── nitro\n","Build Output Structure",[253,3289,3286],{"__ignoreMap":178},[213,3291,3292],{},"Node 서버 또는 Serverless 환경에서 실행된다.",[213,3294,3295],{},"대표 방식:",[245,3297,3301],{"className":3298,"code":3299,"filename":3300,"language":251,"meta":178},[248],"\nSSR\n","Build Rendering Target",[253,3302,3299],{"__ignoreMap":178},[232,3304],{},[481,3306,910],{"id":910},[245,3308,3309],{"className":923,"code":2677,"filename":925,"language":926,"meta":178,"style":178},[253,3310,3311,3315],{"__ignoreMap":178},[389,3312,3313],{"class":391,"line":392},[389,3314,2315],{"emptyLinePlaceholder":190},[389,3316,3317,3319,3321],{"class":391,"line":186},[389,3318,933],{"class":395},[389,3320,937],{"class":936},[389,3322,2020],{"class":936},[213,3324,3325],{},"정적 HTML 파일을 생성한다.",[213,3327,3282],{},[245,3329,3333],{"className":3330,"code":3331,"filename":3332,"language":251,"meta":178},[248],"\n.output\u002Fpublic\n\n├── index.html\n├── blog\u002Fpage.html\n└── assets\n","Generate Static Files",[253,3334,3331],{"__ignoreMap":178},[213,3336,3337],{},"CDN 기반 정적 배포에 적합하다.",[213,3339,3295],{},[245,3341,3345],{"className":3342,"code":3343,"filename":3344,"language":251,"meta":178},[248],"\nSSG\n","Generate Rendering Target",[253,3346,3343],{"__ignoreMap":178},[232,3348],{},[208,3350,809],{"id":809},[213,3352,3353],{},"Nuxt는 단순히 SSR 프레임워크가 아니다.",[213,3355,3356],{},"하나의 프로젝트 안에서:",[1649,3358,3359,3361,3363,3365],{},[1652,3360,2084],{},[1652,3362,2095],{},[1652,3364,2106],{},[1652,3366,2117],{},[213,3368,3369],{},"모든 렌더링 전략을 선택적으로 사용할 수 있는 하이브리드 프레임워크다.",[213,3371,3372],{},"중요한 것은 어떤 방식이 더 좋다는 것이 아니라,",[213,3374,3375],{},"페이지의 목적과 데이터 변경 주기에 맞는 렌더링 전략을 선택하는 것이다.",[851,3377,3378],{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":178,"searchDepth":186,"depth":186,"links":3380},[3381,3382,3383,3384,3389,3390,3391,3392,3393,3394,3395],{"id":2298,"depth":186,"text":2299},{"id":2428,"depth":186,"text":2429},{"id":2480,"depth":186,"text":2481},{"id":2613,"depth":186,"text":2614,"children":3385},[3386,3387,3388],{"id":2618,"depth":2333,"text":2619},{"id":2625,"depth":2333,"text":2626},{"id":2632,"depth":2333,"text":2633},{"id":2667,"depth":186,"text":2668},{"id":2709,"depth":186,"text":2710},{"id":2811,"depth":186,"text":2812},{"id":2852,"depth":186,"text":2853},{"id":2964,"depth":186,"text":2965},{"id":906,"depth":186,"text":906},{"id":910,"depth":186,"text":910},"2026-07-21","Nuxt에서 지원하는 SSR, SSG, ISR 렌더링 방식의 차이와 각각의 사용 방법, 데이터 패칭 전략을 정리",{"tags":3399,"image":3401},[43,2095,2106,2117,2084,3400],"Rendering","images\u002Fnuxt-thumbnail.png",{"icon":26},"\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-rendering-strategy",{"title":3405,"description":3406},"Nuxt SSR, SSG, ISR 렌더링 방식과 구현 방법 정리","Nuxt 애플리케이션에서 서버 사이드 렌더링, 정적 사이트 생성, 증분 정적 생성을 사용하는 방법과 적절한 선택 기준을 설명합니다.","ko\u002F1.note\u002F2.nuxt4\u002F5.nuxt-rendering-strategy","c6w3222KChey1YkEx3dOXtT2A7RdlmbTR5WVFTNuXNU",{"id":3410,"title":147,"body":3411,"date":3396,"description":3905,"extension":188,"links":863,"meta":3906,"navigation":3909,"path":3910,"seo":3911,"stem":3914,"__hash__":3915},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F8.vercel-deploy1.md",{"type":180,"value":3412,"toc":3902},[3413,3899],[204,3414,3415,3419,3422,3425,3428,3431,3434,3436,3440,3443,3449,3457,3467,3469,3473,3476,3479,3482,3489,3492,3514,3517,3520,3523,3526,3532,3544,3550,3553,3555,3559,3566,3621,3631,3634,3648,3651,3656,3659,3661,3665,3672,3675,3678,3681,3683,3689,3692,3695,3697,3701,3704,3709,3712,3715,3741,3744,3750,3753,3756,3763,3766,3772,3775,3777,3781,3784,3810,3813,3818,3824,3831,3834,3846,3854,3857,3859,3863,3866,3869,3875,3881,3884,3887,3893,3896],{":spotlight":206},[208,3416,3418],{"id":3417},"brolog-vercel-배포를-시작하며","🚀 Brolog Vercel 배포를 시작하며",[213,3420,3421],{},"Brolog는 Nuxt 4 기반으로 제작한 개인 개발 블로그 프로젝트다.",[213,3423,3424],{},"처음 프로젝트를 만들 때부터 단순히 블로그 기능만 구현하는 것보다, 앞으로 진행할 프로젝트와 기술 학습 내용을 지속적으로 기록할 수 있는 개발 환경을 만드는 것을 목표로 했다.",[213,3426,3427],{},"현재 Brolog는 Nuxt Docus Theme 기반으로 구성되어 있으며, 공통 기능과 설정을 별도의 Nuxt Layer로 분리하는 Multi Layer 구조를 사용하고 있다.",[213,3429,3430],{},"로컬 개발 환경에서는 정상적으로 동작하고 있었지만, 실제 서비스를 제공하기 위해 Vercel 배포를 진행하면서 예상하지 못했던 몇 가지 문제를 만나게 되었다.",[213,3432,3433],{},"이번 글에서는 처음 Vercel 환경을 구성하면서 확인했던 과정을 기록한다.",[232,3435],{},[481,3437,3439],{"id":3438},"프로젝트-구조","📂 프로젝트 구조",[213,3441,3442],{},"현재 프로젝트는 다음과 같은 구조로 구성되어 있다.",[245,3444,3447],{"className":3445,"code":3446,"language":251,"meta":178},[248],"Brolog\n\n├── docs\n│   ├── pages\n│   ├── content\n│   ├── app.vue\n│   └── nuxt.config.ts\n│\n├── layer\n│   ├── components\n│   ├── composables\n│   ├── middleware\n│   └── nuxt.config.ts\n│\n└── package.json\n",[253,3448,3446],{"__ignoreMap":178},[213,3450,3451,3453,3454,3456],{},[253,3452,1690],{},"는 실제 배포 대상이 되는 Nuxt 애플리케이션이며,\n",[253,3455,1736],{},"는 공통 컴포넌트와 설정을 제공하는 확장 레이어 역할을 한다.",[213,3458,3459,3460,3462,3463,3466],{},"즉, 최종적으로 실행되는 Nuxt 애플리케이션은 ",[253,3461,1690],{}," 단독이 아니라,\n",[253,3464,3465],{},"docs + layer","가 결합된 형태이다.",[232,3468],{},[208,3470,3472],{"id":3471},"_1차-배포-시도와-build-error-발생","🚨 1차 배포 시도와 Build Error 발생",[213,3474,3475],{},"Brolog 프로젝트를 Vercel에 배포하기 위해 먼저 GitHub 저장소를 연결하고 기본 배포 설정을 확인했다.",[213,3477,3478],{},"Vercel은 프로젝트 구조를 기준으로 일부 설정값을 자동으로 감지하고 있었고, 당시에는 별도의 설정 변경 없이 기본값으로 배포를 진행했다.",[213,3480,3481],{},"초기 설정은 다음과 같았다.",[245,3483,3487],{"className":3484,"code":3485,"filename":3486,"language":251,"meta":178},[248],"Root Directory : docs\n","Vercel Settings",[253,3488,3485],{"__ignoreMap":178},[213,3490,3491],{},"Build Command는 Vercel에서 제공하는 기본 설정값을 그대로 사용했다.",[245,3493,3495],{"className":923,"code":3494,"filename":925,"language":926,"meta":178,"style":178},"npm run build or nuxt build\n",[253,3496,3497],{"__ignoreMap":178},[389,3498,3499,3502,3504,3506,3509,3512],{"class":391,"line":392},[389,3500,3501],{"class":395},"npm",[389,3503,1262],{"class":936},[389,3505,1192],{"class":936},[389,3507,3508],{"class":936}," or",[389,3510,3511],{"class":936}," nuxt",[389,3513,1960],{"class":936},[213,3515,3516],{},"처음에는 로컬 환경에서 정상적으로 동작하고 있었기 때문에, 기본 빌드 명령어만으로도 배포가 가능할 것이라고 판단했다.",[213,3518,3519],{},"하지만 실제 Vercel 빌드 환경에서는 예상하지 못한 오류가 발생했고, 단순한 설정 문제가 아닌 Nuxt Multi Layer 구조와 빌드 과정 자체를 확인해야 했다.",[213,3521,3522],{},"처음에는 단순한 Vercel 설정 문제라고 생각했지만, 오류 메시지를 확인하면서 문제가 Nuxt Multi Layer 구조와 빌드 과정에 있다는 것을 알게 되었다.",[213,3524,3525],{},"발생한 오류는 다음과 같다.",[245,3527,3530],{"className":3528,"code":3529,"filename":1204,"language":251,"meta":178},[248],"Nuxt build error:\n\nTSConfckParseError:\n[vite:esbuild] parsing\n\n\u002Fvercel\u002Fpath0\u002Flayer\u002F.nuxt\u002Ftsconfig.app.json\n\nfailed:\n\nENOENT: no such file or directory\n",[253,3531,3529],{"__ignoreMap":178},[213,3533,3534,3535,3537,3538,3540,3541,3543],{},"오류 위치를 보면 실제 서비스 애플리케이션인 ",[253,3536,1690],{},"가 아니라 ",[253,3539,1736],{}," 디렉토리 내부의 ",[253,3542,1749],{}," 파일을 찾고 있었다.",[213,3545,3546,3547,3549],{},"로컬 환경에서는 이미 개발 과정에서 Nuxt prepare 과정이 실행되면서 ",[253,3548,1749],{}," 관련 파일들이 생성되어 있었지만,",[213,3551,3552],{},"Vercel은 새로운 빌드 컨테이너에서 시작하기 때문에 기존 생성 파일이 존재하지 않는다.",[232,3554],{},[208,3556,3558],{"id":3557},"_1-packagejson의-build-script-확인","1. package.json의 build script 확인",[213,3560,3561,3562,3565],{},"오류 원인을 찾기 위해 프로젝트의 ",[253,3563,3564],{},"package.json","을 다시 확인했다.",[245,3567,3571],{"className":3568,"code":3569,"filename":3564,"language":3570,"meta":178,"style":178},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"scripts\": {\n    \"docs:build\": \"nuxt build docs --extends ..\u002Flayer\"\n  }\n}\n","json",[253,3572,3573,3577,3591,3612,3616],{"__ignoreMap":178},[389,3574,3575],{"class":391,"line":392},[389,3576,2341],{"class":399},[389,3578,3579,3582,3585,3587,3589],{"class":391,"line":186},[389,3580,3581],{"class":399},"  \"",[389,3583,3584],{"class":454},"scripts",[389,3586,1239],{"class":399},[389,3588,400],{"class":399},[389,3590,2751],{"class":399},[389,3592,3593,3596,3599,3601,3603,3606,3609],{"class":391,"line":2333},[389,3594,3595],{"class":399},"    \"",[389,3597,3598],{"class":395},"docs:build",[389,3600,1239],{"class":399},[389,3602,400],{"class":399},[389,3604,3605],{"class":399}," \"",[389,3607,3608],{"class":936},"nuxt build docs --extends ..\u002Flayer",[389,3610,3611],{"class":399},"\"\n",[389,3613,3614],{"class":391,"line":2344},[389,3615,2793],{"class":399},[389,3617,3618],{"class":391,"line":2356},[389,3619,3620],{"class":399},"}\n",[213,3622,3623,3624,3626,3627,3630],{},"기존 프로젝트 구조에서는 ",[253,3625,1690],{}," 애플리케이션을 빌드하면서 ",[253,3628,3629],{},"..\u002Flayer","를 extends 하는 방식이었다.",[213,3632,3633],{},"그래서 Vercel Build Command를 다음과 같이 변경해보았다.",[245,3635,3637],{"className":923,"code":3636,"filename":925,"language":926,"meta":178,"style":178},"pnpm run docs:build\n",[253,3638,3639],{"__ignoreMap":178},[389,3640,3641,3643,3645],{"class":391,"line":392},[389,3642,1460],{"class":395},[389,3644,1262],{"class":936},[389,3646,3647],{"class":936}," docs:build\n",[213,3649,3650],{},"하지만 결과는 동일했다.",[245,3652,3654],{"className":3653,"code":1631,"language":251,"meta":178},[248],[253,3655,1631],{"__ignoreMap":178},[213,3657,3658],{},"처음에는 build 명령어가 문제라고 생각했지만, 실제 원인은 다른 곳에 있었다.",[232,3660],{},[208,3662,3664],{"id":3663},"_2-root-directory를-비웠을-때-발생한-새로운-문제","2. Root Directory를 비웠을 때 발생한 새로운 문제",[213,3666,3667,3668,3671],{},"다음으로 Vercel Root Directory를 기본값인 프로젝트 루트(",[253,3669,3670],{},".\u002F",")로 변경했다.",[213,3673,3674],{},"그러자 빌드는 진행되었지만 예상하지 못한 결과가 나왔다.",[213,3676,3677],{},"배포된 페이지는 내가 제작한 Brolog 페이지가 아니라 Nuxt 기본 Welcome 페이지였다.",[213,3679,3680],{},"처음에는 빌드 성공처럼 보였지만, 실제로는 다른 Nuxt 프로젝트를 대상으로 빌드하고 있었다.",[213,3682,1340],{},[245,3684,3687],{"className":3685,"code":3686,"filename":3486,"language":251,"meta":178},[248],"Root Directory: .\u002F\n\n↓\n\n프로젝트 루트 기준 Nuxt 실행\n\n↓\n\ndocs 애플리케이션이 아닌 기본 Nuxt 앱 빌드\n",[253,3688,3686],{"__ignoreMap":178},[213,3690,3691],{},"가 발생한 것이다.",[213,3693,3694],{},"이 경험을 통해 Vercel의 Root Directory 설정은 단순히 파일 위치를 지정하는 옵션이 아니라, 실제 Nuxt 프로젝트의 기준점을 결정하는 중요한 설정이라는 것을 확인했다.",[232,3696],{},[208,3698,3700],{"id":3699},"_3-nuxi-prepare-과정의-필요성-확인","3. nuxi prepare 과정의 필요성 확인",[213,3702,3703],{},"여러 시도 끝에 이번 프로젝트 구조에서는",[213,3705,3706,3708],{},[253,3707,1736],{}," 역시 별도의 Nuxt 프로젝트 컨텍스트로 처리되기 때문에,",[213,3710,3711],{},"배포 과정에서 먼저 Layer의 Nuxt 준비 과정이 필요하다는 점을 확인했다.",[213,3713,3714],{},"최종적으로 성공한 Build Command는 다음이었다.",[245,3716,3717],{"className":923,"code":924,"filename":925,"language":926,"meta":178,"style":178},[253,3718,3719],{"__ignoreMap":178},[389,3720,3721,3723,3725,3727,3729,3731,3733,3735,3737,3739],{"class":391,"line":392},[389,3722,933],{"class":395},[389,3724,937],{"class":936},[389,3726,940],{"class":936},[389,3728,943],{"class":936},[389,3730,946],{"class":399},[389,3732,949],{"class":395},[389,3734,937],{"class":936},[389,3736,954],{"class":936},[389,3738,957],{"class":936},[389,3740,960],{"class":936},[213,3742,3743],{},"Vercel 설정은:",[245,3745,3748],{"className":3746,"code":3747,"filename":3486,"language":251,"meta":178},[248],"Root Directory\n\ndocs\n",[253,3749,3747],{"__ignoreMap":178},[213,3751,3752],{},"로 유지했다.",[213,3754,3755],{},"동작 흐름은 다음과 같다.",[245,3757,3761],{"className":3758,"code":3759,"filename":3760,"language":251,"meta":178},[248],"Vercel Build 시작\n\n↓\n\ndocs 기준 실행\n\n↓\n\nlayer prepare 실행\n\n↓\n\nlayer\u002F.nuxt 생성\n\n↓\n\ndocs + layer 병합\n\n↓\n\nNuxt generate 실행\n\n↓\n\n정적 결과물 생성\n","Build Flow",[253,3762,3759],{"__ignoreMap":178},[213,3764,3765],{},"기존 오류의 원인이었던:",[245,3767,3770],{"className":3768,"code":1631,"filename":3769,"language":251,"meta":178},[248],"Build Log",[253,3771,1631],{"__ignoreMap":178},[213,3773,3774],{},"문제가 해결되었다.",[232,3776],{},[208,3778,3780],{"id":3779},"_4-build-방식도-시도해보았지만-발생한-문제","4. build 방식도 시도해보았지만 발생한 문제",[213,3782,3783],{},"generate 이전에 다음 명령어도 테스트했다.",[245,3785,3786],{"className":923,"code":1171,"filename":925,"language":926,"meta":178,"style":178},[253,3787,3788],{"__ignoreMap":178},[389,3789,3790,3792,3794,3796,3798,3800,3802,3804,3806,3808],{"class":391,"line":392},[389,3791,933],{"class":395},[389,3793,937],{"class":936},[389,3795,940],{"class":936},[389,3797,943],{"class":936},[389,3799,946],{"class":399},[389,3801,949],{"class":395},[389,3803,937],{"class":936},[389,3805,1192],{"class":936},[389,3807,957],{"class":936},[389,3809,960],{"class":936},[213,3811,3812],{},"하지만 결과는 다음과 같았다.",[245,3814,3816],{"className":3815,"code":1203,"filename":1204,"language":251,"meta":178},[248],[253,3817,1203],{"__ignoreMap":178},[213,3819,3820,3821,3823],{},"즉, ",[253,3822,906],{}," 과정 자체가 Vercel 빌드 컨테이너의 메모리 제한을 초과한 상황이었다.",[213,3825,3826,3827,3830],{},"처음에는 Vercel 환경에서 Node 메모리 제한을 조정하는 방식(",[253,3828,3829],{},"NODE_OPTIONS=--max-old-space-size"," 등)으로 해결할 수 있을지 고민했다.",[213,3832,3833],{},"하지만 현재 Brolog 프로젝트의 목적은 별도의 서버 런타임이 필요한 애플리케이션 운영이 아니라, 개발 과정과 기술 기록을 제공하는 정적 콘텐츠 중심의 블로그 서비스였다.",[213,3835,3836,3837,3839,3840,3842,3843,3845],{},"따라서 서버 빌드 방식인 ",[253,3838,906],{},"를 유지하기보다 정적 생성 방식인 ",[253,3841,910],{},"가 현재 배포 목적에 더 적합하다고 판단했고, 최종적으로 ",[253,3844,910],{}," 방식을 선택하게 되었다.",[213,3847,3848,3849,1935,3851,3853],{},"다만 이 과정에서 ",[253,3850,906],{},[253,3852,910],{},"가 단순히 명령어 차이가 아니라 Nuxt 애플리케이션을 배포하는 방식 자체의 차이라는 점을 확인할 필요가 생겼다.",[213,3855,3856],{},"해당 내용은 다음 글에서 별도로 정리한다.",[232,3858],{},[208,3860,3862],{"id":3861},"_7-이번-배포-과정에서-확인한-핵심-내용","7. 이번 배포 과정에서 확인한 핵심 내용",[213,3864,3865],{},"이번 문제는 단순한 Vercel 설정 문제가 아니었다.",[213,3867,3868],{},"핵심 원인은 다음 세 가지였다.",[245,3870,3873],{"className":3871,"code":3872,"language":251,"meta":178},[248],"Nuxt Multi Layer 구조\n\n+\n\n새로운 Vercel 빌드 환경\n\n+\n\nLayer 생성 컨텍스트 준비 과정 부재\n",[253,3874,3872],{"__ignoreMap":178},[213,3876,3877,3878,3880],{},"로컬 환경에서는 이미 생성되어 있던 ",[253,3879,1749],{}," 관련 파일들이 있었지만,",[213,3882,3883],{},"Vercel에서는 항상 초기 상태에서 프로젝트를 준비하기 때문에 Layer 역시 별도의 준비 과정이 필요했다.",[213,3885,3886],{},"최종 배포 흐름은 다음과 같이 정리할 수 있다.",[245,3888,3891],{"className":3889,"code":3890,"language":251,"meta":178},[248],"docs Root Directory\n\n↓\n\nlayer prepare\n\n↓\n\nNuxt generate\n\n↓\n\nVercel Static Deployment\n",[253,3892,3890],{"__ignoreMap":178},[213,3894,3895],{},"이번 과정을 통해 Nuxt Layer 구조에서는 단순히 build 명령어만 실행하는 것이 아니라,",[213,3897,3898],{},"프로젝트 구조에 맞는 준비 과정과 배포 방식을 함께 고려해야 한다는 점을 확인했다.",[851,3900,3901],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}",{"title":178,"searchDepth":186,"depth":186,"links":3903},[3904],{"id":3438,"depth":186,"text":3439},"Nuxt Multi Layer 구조로 구성된 Brolog 프로젝트를 Vercel에 배포하며 발생한 빌드 오류와 해결 과정을 기록",{"tags":3907,"image":2226},[121,43,1605,1606,3908],"Multi Layer",{"icon":26},"\u002Fko\u002Fproject\u002Fbrolog\u002Fvercel-deploy1",{"title":3912,"description":3913},"Nuxt Multi Layer 프로젝트 Vercel 배포 과정과 빌드 오류 해결 기록","Nuxt Layer 기반 프로젝트를 Vercel 환경에 배포하며 발생한 layer .nuxt 오류, Root Directory 설정 문제, 최종 배포 성공 과정을 정리합니다.","ko\u002F2.project\u002F1.brolog\u002F8.vercel-deploy1","9usgJ5C8vy4jjFLhc23NMRE1OECwvrL9wEVEOK2To48",{"id":3917,"title":3918,"body":3919,"date":4214,"description":4215,"extension":188,"links":863,"meta":4216,"navigation":4221,"path":4223,"seo":4224,"stem":4227,"__hash__":4228},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F6.custom-error-and-default-thumbnail.md","[Brolog] 브랜드 아이덴티티 적용 - Custom 404 페이지와 Default No Image 구현",{"type":180,"value":3920,"toc":4212},[3921,4209],[204,3922,3923,3927,3930,3933,3936,3939,3942,3945,3947,3951,3954,3957,3960,3963,3969,3972,3974,3978,3981,3983,4030,4033,4036,4039,4046,4057,4060,4062,4066,4069,4072,4075,4082,4085,4088,4091,4097,4100,4103,4105,4109,4112,4115,4122,4125,4132,4143,4146,4149,4151,4155,4158,4161,4168,4171,4173,4177,4180,4183,4194,4197,4200,4203,4206],{":spotlight":206},[208,3924,3926],{"id":3925},"brolog-브랜드-아이덴티티-적용","🎨 Brolog 브랜드 아이덴티티 적용",[213,3928,3929],{},"이전 글에서는 Brolog 메인 페이지를 구성하면서 개발 기록을 위한 화면 구조와 콘텐츠 탐색 환경을 구축했다.",[213,3931,3932],{},"메인 화면과 게시글 구조를 정리한 이후 프로젝트를 실제 서비스 형태로 발전시키면서 한 가지 아쉬운 부분이 있었다.",[213,3934,3935],{},"기능적으로는 정상적으로 동작하고 있었지만, 이미지가 없는 게시글이나 존재하지 않는 페이지처럼 예외 상황에서는 Docus 기본 UI가 그대로 노출되고 있었다.",[213,3937,3938],{},"Brolog는 단순히 Markdown 문서를 보여주는 블로그가 아니라 개발 과정과 프로젝트 경험을 기록하는 개인 플랫폼으로 확장하는 것을 목표로 하고 있다.",[213,3940,3941],{},"따라서 프로젝트를 대표하는 디자인 요소가 필요하다고 판단했고, Brolog 캐릭터를 제작한 뒤 이를 기반으로 공통 이미지 리소스와 Error Page를 구성했다.",[213,3943,3944],{},"이번 글에서는 Brolog 브랜드 리소스를 제작하고, Nuxt Docus Layer 구조를 활용해 Custom 404 페이지와 Default No Image를 적용한 과정을 기록한다.",[232,3946],{},[208,3948,3950],{"id":3949},"️-프로젝트-캐릭터와-디자인-리소스-제작","🖌️ 프로젝트 캐릭터와 디자인 리소스 제작",[213,3952,3953],{},"Brolog의 대부분의 콘텐츠는 Markdown 기반 문서로 관리되고 있다.",[213,3955,3956],{},"개발 문서 중심의 사이트에서는 텍스트 비중이 높기 때문에 작은 이미지 요소라도 프로젝트의 인상을 결정하는 중요한 요소가 될 수 있다고 판단했다.",[213,3958,3959],{},"그래서 프로젝트를 대표하는 캐릭터와 로고를 제작하고, 이를 다양한 상황에서 활용할 수 있도록 리소스를 구성했다.",[213,3961,3962],{},"제작한 주요 리소스는 다음과 같다.",[245,3964,3967],{"className":3965,"code":3966,"language":251,"meta":178},[248],"Project Logo\n\n+\n\nHeader Logo\n\n+\n\nDefault No Image\n\n+\n\n404 Not Found Image\n",[253,3968,3966],{"__ignoreMap":178},[213,3970,3971],{},"각 이미지는 단순한 장식 목적이 아니라 사용자가 현재 어떤 상황인지 자연스럽게 이해할 수 있도록 상태를 표현하는 용도로 활용했다.",[232,3973],{},[208,3975,3977],{"id":3976},"️-게시글-thumbnail-fallback-처리","🖼️ 게시글 Thumbnail Fallback 처리",[213,3979,3980],{},"Brolog의 게시글은 Nuxt Content 기반으로 Markdown 파일을 관리하고 있으며, Frontmatter metadata를 통해 게시글 정보를 정의한다.",[213,3982,379],{},[245,3984,3989],{"className":3985,"code":3986,"filename":3987,"language":3988,"meta":178,"style":178},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","---\ntitle: 'Example Post'\nimage: '\u002Fimages\u002Fexample.png'\n---\n","Markdown Frontmatter","yaml",[253,3990,3991,3996,4012,4026],{"__ignoreMap":178},[389,3992,3993],{"class":391,"line":392},[389,3994,3995],{"class":395},"---\n",[389,3997,3998,4001,4003,4006,4009],{"class":391,"line":186},[389,3999,4000],{"class":2323},"title",[389,4002,400],{"class":399},[389,4004,4005],{"class":399}," '",[389,4007,4008],{"class":936},"Example Post",[389,4010,4011],{"class":399},"'\n",[389,4013,4014,4017,4019,4021,4024],{"class":391,"line":2333},[389,4015,4016],{"class":2323},"image",[389,4018,400],{"class":399},[389,4020,4005],{"class":399},[389,4022,4023],{"class":936},"\u002Fimages\u002Fexample.png",[389,4025,4011],{"class":399},[389,4027,4028],{"class":391,"line":2344},[389,4029,3995],{"class":395},[213,4031,4032],{},"기본적으로 게시글별 대표 이미지를 직접 지정할 수 있지만, 모든 글에서 이미지를 관리하는 것은 번거로운 작업이 될 수 있다.",[213,4034,4035],{},"따라서 이미지 정보가 없는 게시글은 프로젝트 기본 이미지를 자동으로 표시하도록 fallback 구조를 적용했다.",[213,4037,4038],{},"동작 방식은 다음과 같다.",[245,4040,4044],{"className":4041,"code":4042,"filename":4043,"language":251,"meta":178},[248],"게시글 Image 존재\n\n↓\n\n등록된 이미지 사용\n\n\n게시글 Image 없음\n\n↓\n\nDefault No Image 적용\n","Thumbnail Fallback Flow",[253,4045,4042],{"__ignoreMap":178},[2395,4047,4051],{"className":4048},[4049,4050],"flex","justify-center",[4052,4053],"img",{"alt":4054,"src":4055,"width":4056},"Brolog No Image","\u002Fimages\u002FnoImage.png",400,[213,4058,4059],{},"이를 통해 게시글 목록에서 이미지가 없는 경우에도 동일한 UI 구조를 유지할 수 있었고, 프로젝트 전체 디자인 방향성도 유지할 수 있었다.",[232,4061],{},[208,4063,4065],{"id":4064},"nuxt-error-page-구조-확인","🚧 Nuxt Error Page 구조 확인",[213,4067,4068],{},"존재하지 않는 주소로 접근했을 때는 기존 Docus에서 제공하는 기본 Error Page가 표시되고 있었다.",[213,4070,4071],{},"기능적으로는 문제가 없었지만, 프로젝트 전체 UI와는 다소 다른 느낌을 주고 있었다.",[213,4073,4074],{},"그래서 Brolog 전용 Custom 404 페이지를 제작하고 적용하기로 했다.",[2395,4076,4078],{"className":4077},[4049,4050],[4052,4079],{"alt":4080,"src":4081,"width":4056},"Brolog 404 Page","\u002Fimages\u002F404.png",[213,4083,4084],{},"구현 과정에서 확인한 점은 Nuxt Layer 구조였다.",[213,4086,4087],{},"Brolog는 Nuxt Docus를 Layer 형태로 사용하고 있기 때문에 기본 Error Page 역시 Layer 내부에서 제공되고 있었다.",[213,4089,4090],{},"기존 구조는 다음과 같다.",[245,4092,4095],{"className":4093,"code":4094,"filename":1706,"language":251,"meta":178},[248],"layer\n\n└── app\n\n    └── error.vue\n",[253,4096,4094],{"__ignoreMap":178},[213,4098,4099],{},"처음에는 Layer 내부 파일을 직접 수정하는 방법을 생각했지만,",[213,4101,4102],{},"Nuxt Layer 구조에서는 extends 된 Layer 파일을 직접 변경하지 않고 프로젝트 영역에서 동일한 경로의 파일을 생성하면 Override가 가능하다.",[232,4104],{},[208,4106,4108],{"id":4107},"docus-layer-override를-활용한-custom-404-구현","🔄 Docus Layer Override를 활용한 Custom 404 구현",[213,4110,4111],{},"기존 Layer 파일을 수정하는 대신 서비스 프로젝트 영역에 새로운 Error Page를 추가했다.",[213,4113,4114],{},"구조는 다음과 같다.",[245,4116,4120],{"className":4117,"code":4118,"filename":4119,"language":251,"meta":178},[248],"docs\n\n└── app\n\n    └── error.vue\n","Override Structure",[253,4121,4118],{"__ignoreMap":178},[213,4123,4124],{},"Nuxt Layer 우선순위는 다음과 같이 동작한다.",[245,4126,4130],{"className":4127,"code":4128,"filename":4129,"language":251,"meta":178},[248],"현재 프로젝트\n\n↓\n\nextends 된 Layer\n\n↓\n\n기본 Nuxt 파일\n","Nuxt Layer Priority",[253,4131,4128],{"__ignoreMap":178},[213,4133,4134,4135,4138,4139,4142],{},"따라서 ",[253,4136,4137],{},"docs\u002Fapp\u002Ferror.vue","가 존재하면 Layer 내부의 ",[253,4140,4141],{},"layer\u002Fapp\u002Ferror.vue","보다 우선 적용된다.",[213,4144,4145],{},"결과적으로 Docus 기본 Error Page는 유지하면서,\nBrolog 프로젝트에서 필요한 Custom Error 화면만 교체할 수 있었다.",[213,4147,4148],{},"이 과정을 통해 Nuxt Layer 구조에서는 Framework 내부 파일을 직접 수정하기보다 Override 방식을 활용하는 것이 유지보수 측면에서 더 적합하다는 점을 확인했다.",[232,4150],{},[208,4152,4154],{"id":4153},"️-header-logo와-브랜드-요소-적용","🏷️ Header Logo와 브랜드 요소 적용",[213,4156,4157],{},"추가적으로 프로젝트 Header 영역의 Logo도 Brolog 전용 이미지로 변경했다.",[213,4159,4160],{},"Docus에서 제공하는 검색 기능과 Navigation 구조는 그대로 활용하고,\n브랜드 정체성이 필요한 영역만 교체하는 방식으로 구성했다.",[245,4162,4166],{"className":4163,"code":4164,"filename":4165,"language":251,"meta":178},[248],"Docus 기본 기능\n\n+\n\nCustom Logo\n\n+\n\nCustom Error Page\n\n+\n\nCustom Thumbnail\n","Customization Scope",[253,4167,4164],{"__ignoreMap":178},[213,4169,4170],{},"Framework가 제공하는 기능은 유지하면서 필요한 부분만 확장하는 방식으로 프로젝트를 구성할 수 있었다.",[232,4172],{},[208,4174,4176],{"id":4175},"이번-작업에서-확인한-점","💡 이번 작업에서 확인한 점",[213,4178,4179],{},"이번 작업은 단순히 이미지를 추가하는 작업이 아니라 프로젝트 전체 경험을 정리하는 과정이었다.",[213,4181,4182],{},"사용자가 정상적인 페이지뿐 아니라,",[1649,4184,4185,4188,4191],{},[1652,4186,4187],{},"이미지가 없는 게시글",[1652,4189,4190],{},"존재하지 않는 페이지",[1652,4192,4193],{},"프로젝트 첫 화면",[213,4195,4196],{},"등 다양한 상황에서 Brolog만의 일관된 경험을 제공할 수 있도록 구성했다.",[213,4198,4199],{},"또한 Nuxt Docus 기반 프로젝트에서는 모든 기능을 직접 구현하기보다,",[213,4201,4202],{},"기존 Framework 구조를 이해하고 필요한 부분만 Override 하는 방식이 효율적이라는 점을 확인했다.",[213,4204,4205],{},"Docus가 제공하는 Content 관리, 검색, Navigation 구조는 유지하면서,\n프로젝트 목적에 맞는 UI와 브랜드 요소는 Custom Component와 Layer Override를 통해 확장할 수 있었다.",[213,4207,4208],{},"앞으로 Brolog에서도 Framework가 제공하는 기본 기능은 활용하고,\n서비스 목적에 필요한 영역은 직접 확장하는 방식으로 프로젝트를 발전시켜 나갈 예정이다.",[851,4210,4211],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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":178,"searchDepth":186,"depth":186,"links":4213},[],"2026-07-20","Brolog 캐릭터를 활용해 브랜드 리소스를 제작하고, Nuxt Docus Layer Override 구조를 활용해 Custom Error Page와 게시글 Default Image를 적용한 과정을 기록합니다.",{"tags":4217},[121,43,4218,4219,4220],"Docus","Branding","Vue",{"icon":4222},"lucide:image","\u002Fko\u002Fproject\u002Fbrolog\u002Fcustom-error-and-default-thumbnail",{"title":4225,"description":4226},"Brolog Custom 404 페이지와 Default No Image 적용 과정","Nuxt Docus Layer 구조를 이해하고 Error Page Override, 이미지 fallback 처리, 프로젝트 브랜드 리소스 적용 과정을 정리합니다.","ko\u002F2.project\u002F1.brolog\u002F6.custom-error-and-default-thumbnail","rfSspgOwbkvAt0Yt0s3Xf082u0vcQoHT6kRXYgh89eo",{"id":4230,"title":143,"body":4231,"date":4597,"description":4598,"extension":188,"links":863,"meta":4599,"navigation":4602,"path":4603,"seo":4604,"stem":4607,"__hash__":4608},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F5.develop-main.md",{"type":180,"value":4232,"toc":4592},[4233,4237,4240,4243,4246,4249,4260,4263,4266,4268,4272,4275,4281,4284,4286,4290,4293,4296,4303,4310,4318,4325,4332,4334,4338,4341,4344,4347,4354,4356,4360,4363,4366,4369,4408,4411,4414,4416,4420,4423,4425,4461,4464,4467,4471,4478,4482,4485,4492,4494,4498,4501,4504,4508,4511,4518,4521,4524,4531,4534,4536,4540,4543,4545,4552,4555,4558,4565,4567,4571,4574,4577,4583,4586,4589],[208,4234,4236],{"id":4235},"brolog-메인-페이지-개발-시작","🏠 Brolog 메인 페이지 개발 시작",[213,4238,4239],{},"Brolog는 개발 과정과 프로젝트 경험을 기록하기 위해 제작한 개인 기술 블로그 프로젝트다.",[213,4241,4242],{},"처음 프로젝트를 시작했을 때는 Nuxt Docus Theme에서 제공하는 기본 문서 구조를 활용하는 방향으로 개발을 진행했다.",[213,4244,4245],{},"Docus는 문서 사이트를 구축하기 위한 기능들이 잘 구성되어 있었지만, 실제로 개발 기록을 계속 쌓아가면서 기존 구조만으로는 원하는 블로그 경험을 만들기 어렵다고 판단했다.",[213,4247,4248],{},"특히 개발 기록을 관리하는 목적에서는 단순히 최신 글을 보여주는 것보다,",[1649,4250,4251,4254,4257],{},[1652,4252,4253],{},"어떤 기술을 다뤘는지",[1652,4255,4256],{},"어떤 프로젝트에서 작성된 글인지",[1652,4258,4259],{},"관련된 글들을 어떻게 연결해서 볼 수 있는지",[213,4261,4262],{},"를 쉽게 탐색할 수 있는 구조가 필요했다.",[213,4264,4265],{},"그래서 Brolog의 메인 화면은 Docus의 기본 랜딩 페이지를 그대로 사용하는 대신, 프로젝트 목적에 맞는 Custom Home 구조로 직접 구현하기로 했다.",[232,4267],{},[208,4269,4271],{"id":4270},"brolog-home-화면-설계-방향","🎨 Brolog Home 화면 설계 방향",[213,4273,4274],{},"메인 페이지는 개발 문서 아카이브 형태를 목표로 다음과 같이 전체적인 레이아웃을 구상했다.",[245,4276,4279],{"className":4277,"code":4278,"filename":1699,"language":251,"meta":178},[248],"Hero Section\n\n↓\n\nTag Filter\n\n↓\n\nPost Timeline\n\n↓\n\nCategory Tree\n",[253,4280,4278],{"__ignoreMap":178},[213,4282,4283],{},"상단에서는 Brolog라는 프로젝트의 성격을 보여주고, 중앙에서는 작성한 글의 흐름을 확인하며, 좌측에서는 파일 구조 기반으로 원하는 문서를 탐색할 수 있도록 설계했다.",[232,4285],{},[208,4287,4289],{"id":4288},"프로젝트-아이덴티티를-위한-hero-영역-구성","🚀 프로젝트 아이덴티티를 위한 Hero 영역 구성",[213,4291,4292],{},"처음에는 메인 페이지 상단에 단순한 제목과 설명만 표시할 계획이었다.",[213,4294,4295],{},"하지만 프로젝트를 진행할수록 Brolog만의 개성과 아이덴티티를 표현할 수 있는 요소가 필요하다고 느꼈다. 이에 따라 Hero 영역을 별도로 구성하고 다음과 같은 형태로 제작했다.",[245,4297,4301],{"className":4298,"code":4299,"filename":4300,"language":251,"meta":178},[248],"Brolog Title + Project Description + Terminal UI\n","UI Structure",[253,4302,4299],{"__ignoreMap":178},[213,4304,4305,4306,4309],{},"개발 블로그라는 특징을 직관적으로 살리기 위해 일반적인 이미지 중심 Hero 대신 ",[1641,4307,4308],{},"터미널 형태의 UI","를 적용했다.",[2395,4311,4313],{"className":4312},[4049,4050],[4052,4314],{"alt":4315,"src":4316,"width":4317},"termainalHero","\u002Fimages\u002FterminalHero.png",700,[213,4319,4320,4321,4324],{},"또한 이번 프로젝트를 단순히 글을 모아두는 공간에 그치지 않고, 하나의 독립된 프로젝트 브랜드로 발전시키고자 ",[1641,4322,4323],{},"프로젝트를 대표하는 캐릭터와 로고를 직접 제작","해 메인 페이지와 Header 영역 전반에 녹여냈다.",[2395,4326,4328],{"className":4327},[4049,4050],[4052,4329],{"alt":4330,"src":4331,"width":4056},"logo","\u002Flogo\u002Flogo.png",[232,4333],{},[208,4335,4337],{"id":4336},"시간-흐름-기반-post-timeline-구현","📝 시간 흐름 기반 Post Timeline 구현",[213,4339,4340],{},"기존 Docus 구조는 개별 문서를 탐색하는 데에는 적합하지만, 블로그 메인 페이지처럼 작성한 글들을 목록 형태로 모아볼 수 있는 기본 기능이 없었다.",[213,4342,4343],{},"Brolog에서는 개발 과정을 기록하는 특성상 작성 순서와 작업의 흐름이 중요하기 때문에, 중앙 영역에 Post Timeline 형태의 컴포넌트를 구현했다.",[213,4345,4346],{},"각 포스트는 작성 날짜를 기준으로 정렬되어, 시간의 흐름에 따라 프로젝트가 어떻게 발전해 나갔는지 한눈에 확인할 수 있도록 했다.",[2395,4348,4350],{"className":4349},[4049,4050],[4052,4351],{"alt":4352,"src":4353,"width":4317},"timeline","\u002Fimages\u002Ftimeline.png",[232,4355],{},[208,4357,4359],{"id":4358},"콘텐츠-성격에-맞춘-category-tree-구성","🌳 콘텐츠 성격에 맞춘 Category Tree 구성",[213,4361,4362],{},"Brolog의 콘텐츠는 모두 Nuxt Content 기반 Markdown 파일로 관리하고 있다.",[213,4364,4365],{},"프로젝트를 진행하면서 단순히 글을 시간순으로 나열하는 것만으로는 원하는 내용을 다시 찾아보기 어렵다는 점을 느꼈다. 카테고리별로 구조를 나누어두는 편이 글을 찾고 관리하기에 훨씬 직관적이고 편할 것이라 생각했다.",[213,4367,4368],{},"이에 따라 콘텐츠의 성격을 크게 세 가지로 분류했다.",[2395,4370,4374,4383],{"className":4371},[4049,4372,4373],"items-center","gap-6",[213,4375,4376],{},[4052,4377],{"alt":4378,"src":4379,"width":4380,"className":4381},"categoryTree","\u002Fimages\u002FcategoryTree.png",300,[4382],"flex-shrink-0",[2395,4384,4385,4405],{},[1649,4386,4387,4393,4399],{},[1652,4388,4389,4392],{},[1641,4390,4391],{},"Note"," : 개발 과정에서 학습한 개념이나 아키텍처, 기술 정리",[1652,4394,4395,4398],{},[1641,4396,4397],{},"Project"," : 사이드 프로젝트를 진행하며 기록하는 개발 일지",[1652,4400,4401,4404],{},[1641,4402,4403],{},"Error"," : 개발 과정에서 발생한 문제와 해결 과정을 기록한 트러블슈팅",[213,4406,4407],{},"이러한 카테고리 분류를 바탕으로, 메인 화면 좌측에는 전체 구조를 트리 형태로 한눈에 보여주는 Category Tree 컴포넌트를 구현했다.",[213,4409,4410],{},"단순히 폴더 구조를 시각화하는 것에 그치지 않고, 현재 어떤 주제의 글을 읽고 있는지 자연스럽게 인지하며 관련 문서까지 매끄럽게 탐색할 수 있도록 설계했다.",[213,4412,4413],{},"아울러 앞으로 새로운 프로젝트나 기술 카테고리가 늘어나더라도 기존 구조를 해치지 않고 유연하게 확장할 수 있는 토대를 마련했다.",[232,4415],{},[208,4417,4419],{"id":4418},"️-metadata-기반-tag-filter-구현","🏷️ Metadata 기반 Tag Filter 구현",[213,4421,4422],{},"각 포스트는 Markdown Frontmatter에 Metadata 정보를 가지고 있다.",[213,4424,379],{},[245,4426,4429],{"className":3985,"code":4427,"filename":4428,"language":3988,"meta":178,"style":178},"tags:\n  - Nuxt\n  - Vercel\n  - Deployment\n","Frontmatter",[253,4430,4431,4439,4447,4454],{"__ignoreMap":178},[389,4432,4433,4436],{"class":391,"line":392},[389,4434,4435],{"class":2323},"tags",[389,4437,4438],{"class":399},":\n",[389,4440,4441,4444],{"class":391,"line":186},[389,4442,4443],{"class":399},"  -",[389,4445,4446],{"class":936}," Nuxt\n",[389,4448,4449,4451],{"class":391,"line":2333},[389,4450,4443],{"class":399},[389,4452,4453],{"class":936}," Vercel\n",[389,4455,4456,4458],{"class":391,"line":2344},[389,4457,4443],{"class":399},[389,4459,4460],{"class":936}," Deployment\n",[213,4462,4463],{},"처음에는 단순히 글의 하단에 정보를 표시하는 Badge 용도로만 사용했으나, 작성되는 글이 많아질수록 특정 기술이나 주제별로 글을 모아볼 수 있는 필터링 기능의 필요성을 느꼈다.",[213,4465,4466],{},"이에 따라 기존 포스트의 Metadata에 담긴 Tag 정보를 활용하여 Tag Filter 컴포넌트를 구현했다.",[481,4468,4470],{"id":4469},"동작-구조","🔄 동작 구조",[245,4472,4476],{"className":4473,"code":4474,"filename":4475,"language":251,"meta":178},[248],"Markdown Metadata\n\n↓\n\nTag 데이터 수집\n\n↓\n\nTag Filter Component\n\n↓\n\n해당 Tag 포함 Post 출력\n","Data Flow",[253,4477,4474],{"__ignoreMap":178},[481,4479,4481],{"id":4480},"️-구현-결과","🖥️ 구현 결과",[213,4483,4484],{},"이를 통해 사용자는 특정 기술이나 관심 분야와 관련된 글만 빠르게 탐색할 수 있다.",[2395,4486,4488],{"className":4487},[4049,4050],[4052,4489],{"alt":4490,"src":4491,"width":4317},"tagFilter","\u002Fimages\u002FtagFilter.png",[232,4493],{},[208,4495,4497],{"id":4496},"기존-docus-기능-활용과-custom-영역-분리","🔎 기존 Docus 기능 활용과 Custom 영역 분리",[213,4499,4500],{},"메인 페이지를 구성하면서 모든 기능을 새롭게 만드는 방향은 선택하지 않았다.",[213,4502,4503],{},"Docus에서 이미 안정적으로 제공하는 기능은 적극 활용하고, Brolog만의 사용자 경험이 필요한 영역만 직접 커스터마이징하여 개발했다.",[481,4505,4507],{"id":4506},"️-역할-분담","🛠️ 역할 분담",[213,4509,4510],{},"기존 유지 기능:",[245,4512,4516],{"className":4513,"code":4514,"filename":4515,"language":251,"meta":178},[248],"Search System + Content Rendering + Nuxt Content Integration\n","Framework Feature",[253,4517,4514],{"__ignoreMap":178},[213,4519,4520],{},"이미 검증된 검색 시스템과 마크다운 렌더링 환경은 그대로 가져가 개발 공수를 줄이고 안정성을 확보했다.",[213,4522,4523],{},"커스터마이징 영역:",[245,4525,4529],{"className":4526,"code":4527,"filename":4528,"language":251,"meta":178},[248],"Home Layout + Post Timeline + Category Navigation + Tag Filter + Branding Element\n","Custom Feature",[253,4530,4527],{"__ignoreMap":178},[213,4532,4533],{},"블로그의 핵심인 타임라인과 카테고리 트리, 태그 필터를 직접 구현해 콘텐츠 탐색 경험을 극대화했다.",[232,4535],{},[208,4537,4539],{"id":4538},"️-component-기반-home-구조-설계","⚙️ Component 기반 Home 구조 설계",[213,4541,4542],{},"최종적으로 메인 페이지는 Vue Component 단위로 분리했다.",[213,4544,4114],{},[245,4546,4550],{"className":4547,"code":4548,"filename":4549,"language":251,"meta":178},[248],"Home Page\n\n├── Hero Component\n\n├── Tag Filter Component\n\n├── Post Timeline Component\n\n└── Category Tree Component\n","Component Structure",[253,4551,4548],{"__ignoreMap":178},[213,4553,4554],{},"각 기능을 독립적인 컴포넌트로 분리한 덕분에, 추후 새로운 탐색 방식이나 UI 개선이 필요할 때 기존 코드를 건드리지 않고도 쉽게 확장할 수 있도록 구조화했다.",[213,4556,4557],{},"이러한 컴포넌트들이 유기적으로 결합되어 완성된 Brolog의 최종 메인 화면은 다음과 같다.",[2395,4559,4561],{"className":4560},[4049,4050],[4052,4562],{"alt":4563,"src":4564,"width":4317},"main","\u002Fimages\u002Fmain.png",[232,4566],{},[208,4568,4570],{"id":4569},"개발을-마치며-개발-아카이브로의-도약","💡 개발을 마치며: 개발 아카이브로의 도약",[213,4572,4573],{},"Docus는 Nuxt Content 기반 문서 사이트를 빠르게 구축할 수 있는 훌륭한 프레임워크다. 하지만 개발 기록을 지속적으로 쌓는 개인 기술 블로그로 운영하기 위해서는 프로젝트 목적에 맞는 독자적인 구조 설계가 필요했다.",[213,4575,4576],{},"특히 아래의 요소들은 단순히 템플릿을 쓰는 것을 넘어 서비스 목적에 맞게 직접 고민하고 녹여내야 했다.",[245,4578,4581],{"className":4579,"code":4580,"filename":1699,"language":251,"meta":178},[248],"Content Structure + Metadata 활용 + Navigation Experience + Brand Identity\n",[253,4582,4580],{"__ignoreMap":178},[213,4584,4585],{},"이번 메인 페이지 개발을 통해 Brolog는 단순한 문서 뷰어를 넘어, 나의 개발 과정과 기술 성장을 지속적으로 기록하고 관리할 수 있는 진정한 의미의 개인 개발 아카이브로 거듭나게 되었다.",[213,4587,4588],{},"이 후속 글에서는 이렇게 구성한 Brolog 프로젝트를 실제 서비스 환경에 배포하는 과정에서 마주했던 Nuxt Multi Layer, Vercel Build, AI Assistant 연동 등 다양한 트러블슈팅과 해결 과정을 기록한다.",[851,4590,4591],{},"html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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":178,"searchDepth":186,"depth":186,"links":4593},[4594,4595,4596],{"id":4469,"depth":186,"text":4470},{"id":4480,"depth":186,"text":4481},{"id":4506,"depth":186,"text":4507},"2026-07-19","Nuxt 기반 프로젝트에서 개발 기록을 효과적으로 관리하기 위해 Hero 영역, Timeline, Category Tree, Tag Filter 구조를 직접 구현한 과정을 기록합니다.",{"tags":4600,"image":2226},[121,43,4601,4218,4220],"Nuxt Content",{"icon":26},"\u002Fko\u002Fproject\u002Fbrolog\u002Fdevelop-main",{"title":4605,"description":4606},"Brolog 메인 페이지 개발 과정","Nuxt Content와 Docus Theme을 기반으로 개발 기록 블로그 메인 화면을 구성하고 콘텐츠 탐색 경험을 개선한 과정을 정리합니다.","ko\u002F2.project\u002F1.brolog\u002F5.develop-main","RZUWx-gOqQNuvTFpMrDJ2nG9saWf7Cplxy-sSJMiVwA",{"id":4610,"title":101,"body":4611,"date":5026,"description":5027,"extension":188,"links":863,"meta":5028,"navigation":5033,"path":5034,"seo":5035,"stem":5038,"__hash__":5039},"docs_ko\u002Fko\u002F1.note\u002F4.tooling\u002F1.package-manager\u002F1.package-lock.md",{"type":180,"value":4612,"toc":5012},[4613,5009],[204,4614,4615,4619,4632,4638,4644,4646,4652,4656,4665,4668,4715,4726,4732,4738,4741,4743,4747,4750,4756,4759,4765,4771,4777,4779,4783,4786,4789,4795,4798,4800,4804,4807,4813,4816,4819,4821,4825,4828,4867,4870,4884,4890,4892,4896,4903,4906,4909,4912,4926,4933,4935,4939,4979,4986,4999,5002],{":spotlight":206},[481,4616,4618],{"id":4617},"_1-lock-파일은-왜-존재할까","1. Lock 파일은 왜 존재할까?",[213,4620,4621,4622,907,4625,907,4628,4631],{},"Node.js 생태계에서 프로젝트를 생성하거나 패키지를 설치하면 ",[253,4623,4624],{},"package-lock.json",[253,4626,4627],{},"yarn.lock",[253,4629,4630],{},"pnpm-lock.yaml","과 같은 Lock 파일이 자동으로 생성된다.",[213,4633,4634,4635,2250],{},"처음에는 단순히 패키지 설치 과정에서 생성되는 부수적인 파일처럼 보이지만, 실제로는 ",[1641,4636,4637],{},"프로젝트의 의존성(Dependency)을 항상 동일한 상태로 재현하기 위한 핵심 메커니즘",[213,4639,4640,4641,4643],{},"특히 여러 명의 개발자가 협업하거나 CI\u002FCD 환경에서 프로젝트를 빌드할 경우, Lock 파일이 없다면 동일한 ",[253,4642,3564],{},"을 사용하더라도 서로 다른 버전의 패키지가 설치되어 예상하지 못한 오류가 발생할 수 있다.",[232,4645],{},[481,4647,1746,4649,4651],{"id":4648},"_2-packagejson만으로는-충분하지-않은-이유",[253,4650,3564],{},"만으로는 충분하지 않은 이유",[2616,4653,4655],{"id":4654},"semver시맨틱-버저닝의-특성","① SemVer(시맨틱 버저닝)의 특성",[213,4657,4658,4660,4661,4664],{},[253,4659,3564],{},"은 프로젝트가 필요로 하는 패키지 목록과 ",[1641,4662,4663],{},"허용 가능한 버전 범위","를 정의하는 파일이다.",[213,4666,4667],{},"예를 들어 다음과 같은 의존성이 있다고 가정해보자.",[245,4669,4671],{"className":3568,"code":4670,"language":3570,"meta":178,"style":178},"{\n  \"dependencies\": {\n    \"vue\": \"^3.5.0\"\n  }\n}\n",[253,4672,4673,4677,4690,4707,4711],{"__ignoreMap":178},[389,4674,4675],{"class":391,"line":392},[389,4676,2341],{"class":399},[389,4678,4679,4681,4684,4686,4688],{"class":391,"line":186},[389,4680,3581],{"class":399},[389,4682,4683],{"class":454},"dependencies",[389,4685,1239],{"class":399},[389,4687,400],{"class":399},[389,4689,2751],{"class":399},[389,4691,4692,4694,4696,4698,4700,4702,4705],{"class":391,"line":2333},[389,4693,3595],{"class":399},[389,4695,2308],{"class":395},[389,4697,1239],{"class":399},[389,4699,400],{"class":399},[389,4701,3605],{"class":399},[389,4703,4704],{"class":936},"^3.5.0",[389,4706,3611],{"class":399},[389,4708,4709],{"class":391,"line":2344},[389,4710,2793],{"class":399},[389,4712,4713],{"class":391,"line":2356},[389,4714,3620],{"class":399},[213,4716,4717,4718,4721,4722,4725],{},"여기서 ",[253,4719,4720],{},"^"," 기호는 정확히 ",[253,4723,4724],{},"3.5.0","만 의미하는 것이 아니다.",[245,4727,4730],{"className":4728,"code":4729,"language":251,"meta":178},[248],"^3.5.0\n│\n├── 3.5.1\n├── 3.5.4\n├── 3.6.0\n└── 3.x.x\n",[253,4731,4729],{"__ignoreMap":178},[213,4733,3820,4734,4737],{},[253,4735,4736],{},"3.x.x"," 범위 내에서 최신 버전을 설치할 수 있도록 허용한다.",[213,4739,4740],{},"이러한 방식은 항상 최신 패치와 기능 개선을 받을 수 있다는 장점이 있지만, 시간이 지날수록 설치되는 버전이 달라질 수 있다는 문제도 함께 발생한다.",[232,4742],{},[2616,4744,4746],{"id":4745},"동일한-프로젝트인데-결과가-달라지는-이유","② 동일한 프로젝트인데 결과가 달라지는 이유",[213,4748,4749],{},"예를 들어 오늘 프로젝트를 생성했다면 다음과 같이 설치될 수 있다.",[245,4751,4754],{"className":4752,"code":4753,"language":251,"meta":178},[248],"Vue 3.5.2\n",[253,4755,4753],{"__ignoreMap":178},[213,4757,4758],{},"하지만 한 달 뒤 동일한 프로젝트를 새로 설치하면 Registry에는 새로운 버전이 등록되어 있을 수 있다.",[245,4760,4763],{"className":4761,"code":4762,"language":251,"meta":178},[248],"Vue 3.6.1\n",[253,4764,4762],{"__ignoreMap":178},[213,4766,4767,4768,4770],{},"개발자는 같은 ",[253,4769,3564],{},"을 사용했지만 실제 설치 결과는 달라진다.",[213,4772,4773,4774],{},"이처럼 ",[1641,4775,4776],{},"버전 범위만으로는 항상 동일한 개발 환경을 보장할 수 없다.",[232,4778],{},[481,4780,4782],{"id":4781},"_3-lock-파일이-해결하는-문제","3. Lock 파일이 해결하는 문제",[213,4784,4785],{},"Lock 파일은 실제 설치된 패키지의 정확한 버전과 다운로드 위치, 그리고 하위 의존성까지 모두 기록한다.",[213,4787,4788],{},"즉,",[245,4790,4793],{"className":4791,"code":4792,"language":251,"meta":178},[248],"package.json\n        ↓\n허용 가능한 버전 범위\n\npackage-lock.json\n        ↓\n실제로 설치된 정확한 버전\n",[253,4794,4792],{"__ignoreMap":178},[213,4796,4797],{},"이러한 정보를 저장하기 때문에 이후에는 Registry에 새로운 버전이 공개되더라도 Lock 파일을 기준으로 동일한 패키지가 다시 설치된다.",[232,4799],{},[2616,4801,4803],{"id":4802},"의존성-해결dependency-resolution-과정","의존성 해결(Dependency Resolution) 과정",[213,4805,4806],{},"패키지를 설치하면 Package Manager는 다음과 같은 순서로 동작한다.",[245,4808,4811],{"className":4809,"code":4810,"language":251,"meta":178},[248],"package.json\n      │\n      ▼\n버전 범위(SemVer) 확인\n      │\n      ▼\n패키지 Registry 조회\n      │\n      ▼\n최적의 버전 선택\n      │\n      ▼\n하위 Dependency 분석\n      │\n      ▼\nLock 파일 생성\n      │\n      ▼\nnode_modules 구성\n",[253,4812,4810],{"__ignoreMap":178},[213,4814,4815],{},"한 번 생성된 Lock 파일은 이후 설치 과정에서 기준 정보로 사용된다.",[213,4817,4818],{},"따라서 Package Manager는 다시 버전을 계산하지 않고 Lock 파일을 그대로 참고하여 동일한 의존성 트리를 복원한다.",[232,4820],{},[481,4822,4824],{"id":4823},"_4-npm-yarn-pnpm은-각각-어떤-lock-파일을-사용할까","4. npm, Yarn, pnpm은 각각 어떤 Lock 파일을 사용할까?",[213,4826,4827],{},"Node.js 생태계에는 대표적으로 세 가지 Package Manager가 존재하며 각각 서로 다른 Lock 파일을 사용한다.",[588,4829,4830,4840],{},[591,4831,4832],{},[594,4833,4834,4837],{},[597,4835,95],{"align":4836},"left",[597,4838,4839],{"align":4836},"Lock 파일",[604,4841,4842,4850,4859],{},[594,4843,4844,4846],{},[609,4845,3501],{"align":4836},[609,4847,4848],{"align":4836},[253,4849,4624],{},[594,4851,4852,4855],{},[609,4853,4854],{"align":4836},"Yarn",[609,4856,4857],{"align":4836},[253,4858,4627],{},[594,4860,4861,4863],{},[609,4862,1460],{"align":4836},[609,4864,4865],{"align":4836},[253,4866,4630],{},[213,4868,4869],{},"파일 형식은 다르지만 목적은 모두 동일하다.",[1649,4871,4872,4875,4878,4881],{},[1652,4873,4874],{},"설치된 패키지 버전 고정",[1652,4876,4877],{},"의존성 트리 저장",[1652,4879,4880],{},"동일한 개발 환경 재현",[1652,4882,4883],{},"CI\u002FCD 환경의 일관성 유지",[213,4885,3820,4886,4889],{},[1641,4887,4888],{},"파일 형식만 다를 뿐 모두 동일한 역할을 수행하는 파일","이라고 이해하면 된다.",[232,4891],{},[481,4893,4895],{"id":4894},"_5-lock-파일을-git에-포함해야-하는-이유","5. Lock 파일을 Git에 포함해야 하는 이유",[213,4897,4898,4899,4902],{},"Lock 파일은 자동 생성되는 파일이지만 ",[1641,4900,4901],{},"일반적으로 Git 저장소에 함께 커밋","한다.",[213,4904,4905],{},"그 이유는 프로젝트에 참여하는 모든 개발자가 동일한 의존성 환경을 사용할 수 있도록 하기 위해서다.",[213,4907,4908],{},"만약 Lock 파일을 제외한 채 저장소를 공유하면 각 개발자의 설치 시점에 따라 서로 다른 버전의 라이브러리가 설치될 수 있다.",[213,4910,4911],{},"반대로 Lock 파일이 존재하면 다음과 같은 환경에서도 항상 동일한 결과를 얻을 수 있다.",[1649,4913,4914,4917,4920,4923],{},[1652,4915,4916],{},"새로운 개발자가 프로젝트를 처음 설치할 때",[1652,4918,4919],{},"CI\u002FCD 서버에서 자동 빌드를 수행할 때",[1652,4921,4922],{},"운영 서버에서 프로젝트를 배포할 때",[1652,4924,4925],{},"오랜 시간이 지난 뒤 프로젝트를 다시 실행할 때",[213,4927,4928,4929,4932],{},"결국 Lock 파일은 ",[1641,4930,4931],{},"프로젝트의 개발 환경을 재현하기 위한 기준 문서"," 역할을 한다.",[232,4934],{},[481,4936,4938],{"id":4937},"_7-결론-및-핵심-정리","7. 결론 및 핵심 정리",[588,4940,4941,4951],{},[591,4942,4943],{},[594,4944,4945,4948],{},[597,4946,4947],{"align":4836},"구분",[597,4949,4950],{"align":4836},"역할",[604,4952,4953,4962,4969],{},[594,4954,4955,4959],{},[609,4956,4957],{"align":4836},[253,4958,3564],{},[609,4960,4961],{"align":4836},"필요한 패키지와 허용 버전 정의",[594,4963,4964,4966],{},[609,4965,4839],{"align":4836},[609,4967,4968],{"align":4836},"실제 설치된 정확한 버전 기록",[594,4970,4971,4976],{},[609,4972,4973],{"align":4836},[253,4974,4975],{},"node_modules",[609,4977,4978],{"align":4836},"실제 패키지 파일이 저장되는 디렉터리",[213,4980,4981,4982,4985],{},"Lock 파일은 단순히 자동 생성되는 파일이 아니라 ",[1641,4983,4984],{},"의존성 관리의 기준점(Single Source of Truth)"," 역할을 수행한다.",[213,4987,4988,4990,4991,4994,4995,4998],{},[253,4989,3564],{},"이 프로젝트가 ",[1641,4992,4993],{},"무엇을 필요로 하는지","를 정의하는 문서라면, Lock 파일은 ",[1641,4996,4997],{},"실제로 무엇이 설치되었는지","를 기록하는 문서라고 볼 수 있다.",[213,5000,5001],{},"프로젝트 규모가 커질수록 의존성의 수는 기하급수적으로 증가하며, 각 패키지는 다시 수많은 하위 의존성을 포함한다. 이러한 환경에서 Lock 파일은 모든 의존성 트리를 고정하여 누구나 동일한 개발 환경에서 작업할 수 있도록 보장한다.",[213,5003,5004,5005,5008],{},"Node.js 생태계에서 ",[1641,5006,5007],{},"재현 가능한 빌드(Reproducible Build)"," 와 안정적인 협업이 가능한 이유 역시 바로 이러한 Lock 파일 메커니즘 덕분이라고 할 수 있다.",[851,5010,5011],{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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":178,"searchDepth":186,"depth":186,"links":5013},[5014,5015,5020,5023,5024,5025],{"id":4617,"depth":186,"text":4618},{"id":4648,"depth":186,"text":5016,"children":5017},"2. package.json만으로는 충분하지 않은 이유",[5018,5019],{"id":4654,"depth":2333,"text":4655},{"id":4745,"depth":2333,"text":4746},{"id":4781,"depth":186,"text":4782,"children":5021},[5022],{"id":4802,"depth":2333,"text":4803},{"id":4823,"depth":186,"text":4824},{"id":4894,"depth":186,"text":4895},{"id":4937,"depth":186,"text":4938},"2026-07-16","npm, yarn, pnpm의 Lock 파일이 생성되는 이유와 재현 가능한 의존성 관리 메커니즘 분석",{"tags":5029,"image":5032},[95,3501,1460,5030,5031],"yarn","Node.js","\u002Fimages\u002FpackageManager-thumbnail.png",{"icon":26},"\u002Fko\u002Fnote\u002Ftooling\u002Fpackage-manager\u002Fpackage-lock",{"title":5036,"description":5037},"package-lock.json, yarn.lock, pnpm-lock.yaml 역할과 차이점 이해","package-lock.json, yarn.lock, pnpm-lock.yaml이 필요한 이유와 의존성 해결(Dependency Resolution), SemVer, 재현 가능한 빌드 원리를 알아봅니다.","ko\u002F1.note\u002F4.tooling\u002F1.package-manager\u002F1.package-lock","jqWS66lVP5y8rS3QyHU3CvoWqBX5J0FHwVR7tdOeAy0",{"id":5041,"title":85,"body":5042,"date":5319,"description":5320,"extension":188,"links":863,"meta":5321,"navigation":5326,"path":5327,"seo":5328,"stem":5331,"__hash__":5332},"docs_ko\u002Fko\u002F1.note\u002F4.devops\u002F1.webAssembly.md",{"type":180,"value":5043,"toc":5303},[5044],[204,5045,5046,5050,5053,5068,5070,5074,5077,5081,5084,5088,5098,5105,5107,5111,5118,5124,5127,5129,5133,5136,5240,5242,5246,5249,5256,5262,5266,5269,5273,5283,5285,5289,5292],{":spotlight":206},[481,5047,5049],{"id":5048},"_1-wasm-브라우저-탈출기","🧭 1. Wasm, 브라우저 탈출기",[213,5051,5052],{},"과거 WebAssembly(이하 Wasm)는 프론트엔드 개발자들의 전유물 혹은 웹에서 C++이나 Rust 기반의 무거운 게임\u002F피그마(Figma) 같은 그래픽 툴을 돌리기 위한 고성능 '브라우저 샌드박스 엔진'으로만 취급받았다. 브라우저 내부의 자바스크립트 엔진 성능 한계를 극복하기 위한 특수 목적용 가속기였던 셈이다.",[213,5054,5055,5056,5059,5060,5063,5064,5067],{},"그러나 최근 클라우드 및 인프라 생태계는 ",[1641,5057,5058],{},"Wasm의 서버사이드 확장","에 주목하며 거대한 패러다임 전환을 맞이하고 있다. 핵심 골자는 브라우저라는 감옥을 탈출한 Wasm이 시스템 자원에 접근할 수 있는 ",[1641,5061,5062],{},"WASI(WebAssembly System Interface)"," 표준을 확립하면서, 가상화 인프라의 절대강자였던 ",[1641,5065,5066],{},"도커(Docker) 컨테이너 아키텍처","를 보완하고 대체하는 백엔드 솔루션으로 진화했다는 점이다.",[232,5069],{},[481,5071,5073],{"id":5072},"️-2-아키텍처-및-핵심-메커니즘-도커-vs-wasm","🏗️ 2. 아키텍처 및 핵심 메커니즘: 도커 vs Wasm",[213,5075,5076],{},"가상화 환경을 격리하고 실행하는 방식의 차이를 보면 두 기술의 목적지와 구조적 차이가 명확하게 드러난다.",[2616,5078,5080],{"id":5079},"가상화-계층의-오버헤드overhead-제거","① 가상화 계층의 오버헤드(Overhead) 제거",[213,5082,5083],{},"도커는 독립된 환경을 만들기 위해 리눅스 커널 수준에서 게스트 OS 계층을 복제하거나 최소한의 파일 시스템 레이어를 통째로 패키징해야 한다. 이로 인해 가벼운 애플리케이션도 최소 수백 MB 단위의 컨테이너 용량을 차지하며, 컨테이너가 최초 구동될 때 파일 시스템을 마운트하고 네트워크 인터페이스를 켜는 데 초(seconds) 단위의 지연 시간이 필수적으로 발생한다.",[2616,5085,5087],{"id":5086},"wasm-runtime-기반의-초경량-샌드박싱","② Wasm runtime 기반의 초경량 샌드박싱",[213,5089,5090,5091,5097],{},"반면 서버사이드 Wasm은 OS나 하드웨어를 통째로 가상화하지 않는다. Rust, Go, C++ 등으로 작성된 소스코드를 단 하나의 고유한 기계어 바이트코드인 ",[1641,5092,5093,5096],{},[253,5094,5095],{},".wasm"," 바이너리 파일","로 컴파일한다.",[213,5099,5100,5101,5104],{},"이후 호스트 시스템 위에 상주하는 초경량 Wasm 가상 머신 런타임(예: Wasmtime, Wasmer)이 이 바이너리를 읽어 즉시 네이티브 코드로 변환 후 구동한다. OS 레이어와 데몬 프로세스가 완전히 생략되었기 때문에 용량은 수 MB 이하로 줄어들고, 구동 속도는 ",[1641,5102,5103],{},"1~5밀리초($\\text{ms}$)"," 수준으로 즉시 수렴한다.",[232,5106],{},[481,5108,5110],{"id":5109},"️-핵심-표준-기술-wasi-webassembly-system-interface","🛠️ 핵심 표준 기술: WASI (WebAssembly System Interface)",[213,5112,5113,5114,5117],{},"Wasm이 브라우저 외부 백엔드 서버에서 실행되기 위해 가장 핵심이 되는 기술적 기반은 ",[1641,5115,5116],{},"WASI","다.",[213,5119,5120,5121,5117],{},"원래 Wasm은 브라우저 안에서만 돌도록 설계되었기 때문에 보안상 컴퓨터의 파일 시스템을 읽거나 네트워크 소켓을 열 수 없었다. WASI는 Wasm 바이너리가 OS 호스트의 자원(파일 API, 네트워크 통신, 시스템 시간 등)과 안전하게 통신할 수 있도록 다리를 놓아주는 ",[1641,5122,5123],{},"추상화된 OS 인터페이스 표준 세트",[213,5125,5126],{},"덕분에 개발자는 특정 OS나 특정 CPU 아키텍처(x86, ARM 등)에 종속되지 않는 이식성 높은 백엔드 프로그램을 단 한 번의 빌드로 구현할 수 있게 되었다.",[232,5128],{},[481,5130,5132],{"id":5131},"가상화-기술-세부-비교","🤝 가상화 기술 세부 비교",[213,5134,5135],{},"실무 인프라 설계 관점에서 두 가상화 기술이 가지는 성능적, 구조적 차이는 다음과 같이 요약된다.",[588,5137,5138,5151],{},[591,5139,5140],{},[594,5141,5142,5145,5148],{},[597,5143,5144],{"align":4836},"비교 항목",[597,5146,5147],{"align":4836},"도커 (Docker Container)",[597,5149,5150],{"align":4836},"서버사이드 Wasm (WebAssembly)",[604,5152,5153,5166,5182,5198,5214,5227],{},[594,5154,5155,5160,5163],{},[609,5156,5157],{"align":4836},[1641,5158,5159],{},"가상화 대상",[609,5161,5162],{"align":4836},"리눅스 커널 \u002F 파일 시스템 (OS 수준 격리)",[609,5164,5165],{"align":4836},"언어별 컴파일된 바이트코드 (애플리케이션 수준 격리)",[594,5167,5168,5173,5176],{},[609,5169,5170],{"align":4836},[1641,5171,5172],{},"기동 속도 (Startup Time)",[609,5174,5175],{"align":4836},"수 초 ($\\text{sec}$) 단위 지연 발생",[609,5177,5178,5181],{"align":4836},[1641,5179,5180],{},"1~5밀리초 ($\\text{ms}$)"," 미만 (Cold Start 제로)",[594,5183,5184,5189,5192],{},[609,5185,5186],{"align":4836},[1641,5187,5188],{},"메모리 \u002F 파일 크기",[609,5190,5191],{"align":4836},"최소 수십 MB ~ 수 GB 단위 (무거움)",[609,5193,5194,5197],{"align":4836},[1641,5195,5196],{},"수 KB ~ 수 MB 단위"," (초경량)",[594,5199,5200,5205,5208],{},[609,5201,5202],{"align":4836},[1641,5203,5204],{},"이식성 (Cross-Platform)",[609,5206,5207],{"align":4836},"타겟 CPU 아키텍처(x86\u002FARM)별 이미지 빌드 필요",[609,5209,5210,5211],{"align":4836},"한 번 빌드하면 ",[1641,5212,5213],{},"아키텍처 무관하게 동일 바이너리 실행",[594,5215,5216,5221,5224],{},[609,5217,5218],{"align":4836},[1641,5219,5220],{},"메모리 관리 안전성",[609,5222,5223],{"align":4836},"호스트 OS 커널 공유로 인한 취약점 존재 가능",[609,5225,5226],{"align":4836},"가상 머신 내부 메모리 공간이 완전히 격리된 샌드박스 형태",[594,5228,5229,5234,5237],{},[609,5230,5231],{"align":4836},[1641,5232,5233],{},"생태계 성숙도",[609,5235,5236],{"align":4836},"매우 성숙함 (모든 백엔드 언어\u002F프레임워크 지원)",[609,5238,5239],{"align":4836},"성장 중 (Rust\u002FGo\u002FC++ 궁합 최상, Python\u002FJava는 런타임 이식 진행 중)",[232,5241],{},[481,5243,5245],{"id":5244},"_3-프로덕션-적용-및-인프라-실무-사례","📦 3. 프로덕션 적용 및 인프라 실무 사례",[213,5247,5248],{},"이 기술은 결코 이론 속의 유토피아가 아니다. 이미 글로벌 테크 기업들은 거대 트래픽을 처리하는 코어 인프라에 Wasm을 심어 막대한 비용 절감과 성능 향상을 누리고 있다.",[2616,5250,5252,5253],{"id":5251},"cloudflare-workers-workerd","① Cloudflare Workers & ",[253,5254,5255],{},"workerd",[213,5257,5258,5259,5261],{},"우리가 모던 웹 프레임워크(Next.js, Nuxt 4 등)를 서버리스 엣지에 배포할 때 만나는 Cloudflare 인프라의 핵심 심장이 바로 Wasm 기반의 오픈소스 런타임 **",[253,5260,5255],{},"**이다. 전 세계 분산 에지 서버에서 Node.js 전체 인스턴스를 무겁게 띄우는 대신, Wasm 컨텍스트만 V8 격리 기술 안에서 1ms 만에 스위칭하여 지연 시간 없는 초고속 SSR 분산 처리를 구현한다.",[2616,5263,5265],{"id":5264},"vercel-functions-fluid-compute","② Vercel Functions (Fluid Compute)",[213,5267,5268],{},"Vercel 역시 백엔드 인프라의 고질적인 지연 시간(Cold Start)을 혁신적으로 줄이기 위해 V8 격리 기술 및 서버사이드 Wasm 샌드박싱 아키텍처를 적극 도입했다. 유저의 HTTP 요청이 들어오는 즉시 유휴 자원을 긁어모아 밀리초 단위로 백엔드 코드를 깨우는 원동력이 바로 Wasm의 가벼움에서 나온다.",[2616,5270,5272],{"id":5271},"엔터프라이즈-플러그인-아키텍처-istio-envoy-proxy","③ 엔터프라이즈 플러그인 아키텍처 (Istio \u002F Envoy Proxy)",[213,5274,5275,5276,5279,5280,5282],{},"쿠버네티스 환경에서 서비스 간 트래픽을 제어하는 프록시 엔진인 ",[1641,5277,5278],{},"Envoy","나 API 게이트웨이 생태계에서는 메인 서버를 재시작하지 않고도 실시간으로 기능을 확장할 수 있는 '플러그인 시스템'의 표준으로 Wasm을 채택했다. Rust로 커스텀 인증\u002F필터 로직을 짜서 ",[253,5281,5095],{}," 파일로 던지면 Envoy 엔진이 런타임 중에 이를 동적으로 흡수하여 트래픽을 필터링한다.",[232,5284],{},[481,5286,5288],{"id":5287},"_4-결론-및-결합-구조의-이점","🧠 4. 결론 및 결합 구조의 이점",[213,5290,5291],{},"서버사이드 Wasm의 부상이 도커의 완전한 종말을 의미하는 것은 아니다. 네트워크 소켓의 세밀한 제어나 리눅스 커널의 로우 레벨 기능을 모두 활용해야 하는 무거운 데이터베이스 시스템, 거대한 스프링 기반 레거시 비즈니스 시스템은 여전히 도커 컨테이너가 적합하다.",[213,5293,5294,5295,5298,5299,5302],{},"대신 HTTP 요청에 따라 즉각적으로 즉시 반응해야 하는 ",[1641,5296,5297],{},"이벤트 기반 마이크로서비스(FaaS)",", 이미지 프로세싱이나 비디오 인코딩, AI 추론(Inference)처럼 특정 CPU 집중 연산이 극도로 빠르게 필요한 ",[1641,5300,5301],{},"엣지 컴퓨팅 영역","에서 Wasm은 도커 대비 압도적인 인프라 밀집도와 비용 효율성을 보장한다. 인프라의 경량화와 빠른 반응 속도가 핵심인 모던 클라우드 아키텍처에서 Wasm은 도커를 보완하는 가장 완벽한 파트너 기술로 안착했다.",{"title":178,"searchDepth":186,"depth":186,"links":5304},[5305,5306,5310,5311,5312,5318],{"id":5048,"depth":186,"text":5049},{"id":5072,"depth":186,"text":5073,"children":5307},[5308,5309],{"id":5079,"depth":2333,"text":5080},{"id":5086,"depth":2333,"text":5087},{"id":5109,"depth":186,"text":5110},{"id":5131,"depth":186,"text":5132},{"id":5244,"depth":186,"text":5245,"children":5313},[5314,5316,5317],{"id":5251,"depth":2333,"text":5315},"① Cloudflare Workers & workerd",{"id":5264,"depth":2333,"text":5265},{"id":5271,"depth":2333,"text":5272},{"id":5287,"depth":186,"text":5288},"2026-07-13","WebAssembly와 WASI가 서버 환경으로 확장되는 과정과 Docker 컨테이너 기반 아키텍처와의 차이점, 엣지 환경에서의 활용 사례 분석",{"tags":5322},[5323,5324,5325,1699],"WebAssembly","Wasm","Docker",{"icon":26},"\u002Fko\u002Fnote\u002Fdevops\u002Fwebassembly",{"title":5329,"description":5330},"서버사이드 WebAssembly(Wasm)와 WASI 아키텍처 이해","WebAssembly가 백엔드 실행 환경으로 확장되는 원리와 WASI, Docker 컨테이너 아키텍처와의 차이를 정리합니다.","ko\u002F1.note\u002F4.devops\u002F1.webAssembly","PS0BnvK0ey9zQhKLTh5uuPhMTQr893-l1BQbAi3iBl0",{"id":5334,"title":57,"body":5335,"date":5930,"description":5931,"extension":188,"links":863,"meta":5932,"navigation":5936,"path":5937,"seo":5938,"stem":5941,"__hash__":5942},"docs_ko\u002Fko\u002F1.note\u002F2.nuxt4\u002F3.nuxt-content-v3.md",{"type":180,"value":5336,"toc":5913},[5337,5910],[204,5338,5339,5343,5346,5360,5362,5366,5369,5376,5380,5387,5391,5413,5445,5447,5451,5461,5567,5571,5628,5630,5634,5644,5654,5657,5754,5763,5769,5875,5877,5881,5888],{":spotlight":206},[481,5340,5342],{"id":5341},"_1-nuxt-content-v3의-탄생-배경","1. Nuxt Content v3의 탄생 배경",[213,5344,5345],{},"과거 Nuxt 3 시절의 Nuxt Content v2는 마크다운(.md) 파일을 실시간으로 읽고 메모리 내에서 인덱싱하는 가벼운 파서 역할을 수행했다. 하지만 프로젝트 규모가 커지고 문서의 양이 방대해질수록 빌드 속도 저하와 데이터 쿼리의 한계가 명확해졌다.",[213,5347,5348,5351,5352,5355,5356,5359],{},[1641,5349,5350],{},"Nuxt Content v3","는 ",[1641,5353,5354],{},"Nuxt 4 아키텍처","에 맞춰 바닥부터 완전히 재설계되었다. 핵심 골자는 정적 파일들을 파싱한 뒤, 내부적으로 강력한 ",[1641,5357,5358],{},"SQLite 데이터베이스 엔진","을 구축하여 데이터를 관계형 DB처럼 관리하는 구조적 대격변을 이뤄냈다는 점이다.",[232,5361],{},[481,5363,5365],{"id":5364},"_2-아키텍처-및-핵심-메커니즘","2. 아키텍처 및 핵심 메커니즘",[213,5367,5368],{},"Nuxt Content v3의 데이터 처리 파이프라인은 다음과 같은 3단계 레이어로 작동한다.",[245,5370,5374],{"className":5371,"code":5372,"filename":5373,"language":251,"meta":178},[248],"[ 마크다운 파일 (.md) ] \n       │  (빌드 \u002F 개발 서버 구동 시점 파싱)\n       ▼\n[ Nitro SQLite 임베디드 DB (인덱싱 완료) ] ──▶ content.config.ts (스키마 제어)\n       │  (런타임 시점 높은 효율의 SQL 조회)\n       ▼\n[ queryCollection() API 호출 ]\n","Process",[253,5375,5372],{"__ignoreMap":178},[2616,5377,5379],{"id":5378},"로컬-파일의-데이터베이스화","① 로컬 파일의 데이터베이스화",[213,5381,5382,5383,5386],{},"프로젝트가 실행되면 Nitro 엔진은 ",[253,5384,5385],{},"content\u002F"," 디렉토리 내의 모든 마크다운 파일과 Front-matter 설정을 스캔한다. 이 정보들은 휘발성 텍스트가 아닌, 엄격하게 규격화된 **SQLite 데이터베이스 테이블의 로우(Row)**로 치환되어 메모리에 상주한다.",[2616,5388,5390],{"id":5389},"contentconfigts를-통한-스키마schema-제어","② content.config.ts를 통한 스키마(Schema) 제어",[213,5392,5393,5394,907,5397,5400,5401,5404,5405,5408,5409,5412],{},"v3로 넘어오면서 가장 엄격해진 부분이다. 이전 버전처럼 프론트매터에 임의의 필드(예: ",[253,5395,5396],{},"date",[253,5398,5399],{},"author",")를 넣는다고 해서 쿼리 엔진이 자동으로 인지하지 못한다.\n반드시 데이터베이스 테이블의 컬럼을 선언하듯, ",[253,5402,5403],{},"content.config.ts"," 파일에서 ",[253,5406,5407],{},"defineCollection","과 ",[253,5410,5411],{},"Zod"," 라이브러리를 활용해 스키마를 선언해주어야 정상적인 인덱싱 및 정렬 처리가 가능하다.",[2125,5414,5415],{},[213,5416,5417,5418,5421,5422,5425,5426,5428,5429,5432,5433,5436,5437,5440,5441,5444],{},"🚨 ",[1641,5419,5420],{},"실무 트러블슈팅 팁 (No Such Column Error)","\n만약 특정 다국어 폴더(",[253,5423,5424],{},"en\u002F",")에는 ",[253,5427,5396],{}," 필드가 누락되어 있고, 특정 폴더(",[253,5430,5431],{},"ko\u002F",")에만 존재할 때, 스키마에 이를 명시하지 않고 정렬(",[253,5434,5435],{},".order('date')",")을 시도하면 데이터베이스 단에서 ",[253,5438,5439],{},"no such column: \"date\""," 에러를 뿜으며 빌드가 터진다. 이를 방지하기 위해 반드시 ",[253,5442,5443],{},".optional()"," 구조로 컬럼의 존재를 DB 인프라에 사전 등록해 주어야 한다.",[232,5446],{},[481,5448,5450],{"id":5449},"_3-nuxt-4와-nuxt-content-v3의-명확한-역할-분담","3. Nuxt 4와 Nuxt Content v3의 명확한 역할 분담",[213,5452,5453,5454,1935,5457,5460],{},"코드를 작성할 때 가장 혼동하기 쉬운 두 컴포저블(",[253,5455,5456],{},"useAsyncData",[253,5458,5459],{},"queryCollection",")의 역할은 명확하게 분리되어 있다.",[245,5462,5467],{"className":5463,"code":5464,"filename":5465,"language":5466,"meta":178,"style":178},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const { data: posts } = await useAsyncData('posts_key', () => \n  queryCollection('docs_ko').order('date', 'DESC').all()\n)\n","Example","typescript",[253,5468,5469,5513,5563],{"__ignoreMap":178},[389,5470,5471,5473,5475,5478,5480,5483,5485,5487,5489,5492,5494,5496,5499,5501,5504,5507,5510],{"class":391,"line":392},[389,5472,455],{"class":454},[389,5474,2515],{"class":399},[389,5476,5477],{"class":2323}," data",[389,5479,400],{"class":399},[389,5481,5482],{"class":407}," posts ",[389,5484,2359],{"class":399},[389,5486,2523],{"class":399},[389,5488,2527],{"class":2526},[389,5490,5491],{"class":403}," useAsyncData",[389,5493,408],{"class":407},[389,5495,2535],{"class":399},[389,5497,5498],{"class":936},"posts_key",[389,5500,2535],{"class":399},[389,5502,5503],{"class":399},",",[389,5505,5506],{"class":399}," ()",[389,5508,5509],{"class":454}," =>",[389,5511,5512],{"class":407}," \n",[389,5514,5515,5518,5520,5522,5525,5527,5530,5533,5536,5538,5540,5542,5544,5546,5548,5551,5553,5555,5557,5560],{"class":391,"line":186},[389,5516,5517],{"class":403},"  queryCollection",[389,5519,408],{"class":407},[389,5521,2535],{"class":399},[389,5523,5524],{"class":936},"docs_ko",[389,5526,2535],{"class":399},[389,5528,5529],{"class":407},")",[389,5531,5532],{"class":399},".",[389,5534,5535],{"class":403},"order",[389,5537,408],{"class":407},[389,5539,2535],{"class":399},[389,5541,5396],{"class":936},[389,5543,2535],{"class":399},[389,5545,5503],{"class":399},[389,5547,4005],{"class":399},[389,5549,5550],{"class":936},"DESC",[389,5552,2535],{"class":399},[389,5554,5529],{"class":407},[389,5556,5532],{"class":399},[389,5558,5559],{"class":403},"all",[389,5561,5562],{"class":407},"()\n",[389,5564,5565],{"class":391,"line":2333},[389,5566,415],{"class":407},[2616,5568,5570],{"id":5569},"역할-분담-테이블","역할 분담 테이블",[588,5572,5573,5586],{},[591,5574,5575],{},[594,5576,5577,5580,5583],{},[597,5578,5579],{"align":4836},"기능 컴포저블",[597,5581,5582],{"align":4836},"소속 생태계",[597,5584,5585],{"align":4836},"핵심 역할 및 기능",[604,5587,5588,5608],{},[594,5589,5590,5597,5601],{},[609,5591,5592],{"align":4836},[1641,5593,5594],{},[253,5595,5596],{},"queryCollection()",[609,5598,5599],{"align":4836},[1641,5600,5350],{},[609,5602,5603,5604,5607],{"align":4836},"내장 SQLite DB에서 타겟 컬렉션 데이터를 정렬\u002F필터링하여 ",[1641,5605,5606],{},"가져오는 행위"," 자체를 담당 (SQL의 SELECT 문 역할)",[594,5609,5610,5617,5622],{},[609,5611,5612],{"align":4836},[1641,5613,5614],{},[253,5615,5616],{},"useAsyncData()",[609,5618,5619],{"align":4836},[1641,5620,5621],{},"Nuxt 4 (Core)",[609,5623,5624,5625],{"align":4836},"데이터 패칭 흐름 제어. 서버(SSR)에서 긁어온 데이터를 캐싱하여, 브라우저가 하이드레이션될 때 ",[1641,5626,5627],{},"중복 API 요청이 발생하지 않도록 방어",[232,5629],{},[481,5631,5633],{"id":5632},"_4-실무에서-주로-사용되는-신형-쿼리-문법","4. 실무에서 주로 사용되는 신형 쿼리 문법",[213,5635,5636,5637,907,5640,5643],{},"v2의 ",[253,5638,5639],{},".sort()",[253,5641,5642],{},".where()"," 형태의 체이닝 문법은 v3에서 완벽한 데이터베이스 친화적(SQL-Like) 메서드로 대체되었다.",[2616,5645,5647,5648,907,5651,5529],{"id":5646},"최신-날짜순-전체-조회-order-all","① 최신 날짜순 전체 조회 (",[253,5649,5650],{},".order()",[253,5652,5653],{},".all()",[213,5655,5656],{},"컬렉션 명칭을 인자로 받아 데이터를 정렬한 뒤 전체 데이터를 객체 배열로 반환한다.",[245,5658,5660],{"className":5463,"code":5659,"filename":5465,"language":5466,"meta":178,"style":178},"const { data: articles } = await useAsyncData('articles', () =>\n  queryCollection('docs_ko')\n    .order('date', 'DESC') \u002F\u002F 👈 첫 번째 인자는 컬럼명, 두 번째 인자는 방향(DESC\u002FASC)\n    .all()\n)\n",[253,5661,5662,5699,5713,5742,5750],{"__ignoreMap":178},[389,5663,5664,5666,5668,5670,5672,5675,5677,5679,5681,5683,5685,5687,5690,5692,5694,5696],{"class":391,"line":392},[389,5665,455],{"class":454},[389,5667,2515],{"class":399},[389,5669,5477],{"class":2323},[389,5671,400],{"class":399},[389,5673,5674],{"class":407}," articles ",[389,5676,2359],{"class":399},[389,5678,2523],{"class":399},[389,5680,2527],{"class":2526},[389,5682,5491],{"class":403},[389,5684,408],{"class":407},[389,5686,2535],{"class":399},[389,5688,5689],{"class":936},"articles",[389,5691,2535],{"class":399},[389,5693,5503],{"class":399},[389,5695,5506],{"class":399},[389,5697,5698],{"class":454}," =>\n",[389,5700,5701,5703,5705,5707,5709,5711],{"class":391,"line":186},[389,5702,5517],{"class":403},[389,5704,408],{"class":407},[389,5706,2535],{"class":399},[389,5708,5524],{"class":936},[389,5710,2535],{"class":399},[389,5712,415],{"class":407},[389,5714,5715,5718,5720,5722,5724,5726,5728,5730,5732,5734,5736,5739],{"class":391,"line":2333},[389,5716,5717],{"class":399},"    .",[389,5719,5535],{"class":403},[389,5721,408],{"class":407},[389,5723,2535],{"class":399},[389,5725,5396],{"class":936},[389,5727,2535],{"class":399},[389,5729,5503],{"class":399},[389,5731,4005],{"class":399},[389,5733,5550],{"class":936},[389,5735,2535],{"class":399},[389,5737,5738],{"class":407},") ",[389,5740,5741],{"class":3108},"\u002F\u002F 👈 첫 번째 인자는 컬럼명, 두 번째 인자는 방향(DESC\u002FASC)\n",[389,5743,5744,5746,5748],{"class":391,"line":2344},[389,5745,5717],{"class":399},[389,5747,5559],{"class":403},[389,5749,5562],{"class":407},[389,5751,5752],{"class":391,"line":2356},[389,5753,415],{"class":407},[2616,5755,5757,5758,907,5760,5529],{"id":5756},"특정-조건-검색-where-first","② 특정 조건 검색 (",[253,5759,5642],{},[253,5761,5762],{},".first()",[213,5764,5765,5766,5768],{},"단 한 건의 고유 문서만 매핑하여 가져올 때는 끝에 ",[253,5767,5762],{},"를 결합한다.",[245,5770,5772],{"className":5463,"code":5771,"filename":5465,"language":5466,"meta":178,"style":178},"const { data: page } = await useAsyncData('current_page', () =>\n  queryCollection('docs_ko')\n    .where('path', '=', '\u002Fproject\u002Fbrolog') \u002F\u002F 👈 정확한 비교 연산자 주입 구조\n    .first()\n)\n",[253,5773,5774,5810,5824,5862,5871],{"__ignoreMap":178},[389,5775,5776,5778,5780,5782,5784,5787,5789,5791,5793,5795,5797,5799,5802,5804,5806,5808],{"class":391,"line":392},[389,5777,455],{"class":454},[389,5779,2515],{"class":399},[389,5781,5477],{"class":2323},[389,5783,400],{"class":399},[389,5785,5786],{"class":407}," page ",[389,5788,2359],{"class":399},[389,5790,2523],{"class":399},[389,5792,2527],{"class":2526},[389,5794,5491],{"class":403},[389,5796,408],{"class":407},[389,5798,2535],{"class":399},[389,5800,5801],{"class":936},"current_page",[389,5803,2535],{"class":399},[389,5805,5503],{"class":399},[389,5807,5506],{"class":399},[389,5809,5698],{"class":454},[389,5811,5812,5814,5816,5818,5820,5822],{"class":391,"line":186},[389,5813,5517],{"class":403},[389,5815,408],{"class":407},[389,5817,2535],{"class":399},[389,5819,5524],{"class":936},[389,5821,2535],{"class":399},[389,5823,415],{"class":407},[389,5825,5826,5828,5831,5833,5835,5838,5840,5842,5844,5846,5848,5850,5852,5855,5857,5859],{"class":391,"line":2333},[389,5827,5717],{"class":399},[389,5829,5830],{"class":403},"where",[389,5832,408],{"class":407},[389,5834,2535],{"class":399},[389,5836,5837],{"class":936},"path",[389,5839,2535],{"class":399},[389,5841,5503],{"class":399},[389,5843,4005],{"class":399},[389,5845,461],{"class":936},[389,5847,2535],{"class":399},[389,5849,5503],{"class":399},[389,5851,4005],{"class":399},[389,5853,5854],{"class":936},"\u002Fproject\u002Fbrolog",[389,5856,2535],{"class":399},[389,5858,5738],{"class":407},[389,5860,5861],{"class":3108},"\u002F\u002F 👈 정확한 비교 연산자 주입 구조\n",[389,5863,5864,5866,5869],{"class":391,"line":2344},[389,5865,5717],{"class":399},[389,5867,5868],{"class":403},"first",[389,5870,5562],{"class":407},[389,5872,5873],{"class":391,"line":2356},[389,5874,415],{"class":407},[232,5876],{},[481,5878,5880],{"id":5879},"_5-결론-및-실무-인사이트","5. 결론 및 실무 인사이트",[213,5882,5883,5884,5887],{},"Nuxt 4와 Nuxt Content v3의 결합은 단순한 버전 업그레이드가 아니다. 파일 기반 컨텐츠 관리 시스템을 ",[1641,5885,5886],{},"엔터프라이즈급 DB 아키텍처"," 수준으로 격상시킨 진화다.",[1649,5889,5890,5896],{},[1652,5891,5892,5895],{},[1641,5893,5894],{},"엄격한 스키마 정의 필수:"," 마크다운을 작성할 때 프론트매터 규칙이 엄격해진 만큼, 공통 스키마 확장을 통해 데이터 무결성을 먼저 확보해야 정렬 및 필터링 과정에서 런타임 에러를 방지할 수 있다.",[1652,5897,5898,5901,5902,5905,5906,5909],{},[1641,5899,5900],{},"구조적 이점:"," 내장 컴포넌트나 코드 블록 내부의 문자열을 파서가 기만적으로 추적하여 ",[253,5903,5904],{},"\u002Fundefined"," 404 링크 에러를 터트리는 독특한 파싱 메커니즘이 존재하므로, 본문 작성 시 태그(",[253,5907,5908],{},"\u003Cimg>"," 등) 표현이나 에셋 경로는 철저히 백틱 처리하거나 이스케이프하여 작성하는 숙련도가 필요하다.",[851,5911,5912],{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":178,"searchDepth":186,"depth":186,"links":5914},[5915,5916,5920,5923,5929],{"id":5341,"depth":186,"text":5342},{"id":5364,"depth":186,"text":5365,"children":5917},[5918,5919],{"id":5378,"depth":2333,"text":5379},{"id":5389,"depth":2333,"text":5390},{"id":5449,"depth":186,"text":5450,"children":5921},[5922],{"id":5569,"depth":2333,"text":5570},{"id":5632,"depth":186,"text":5633,"children":5924},[5925,5927],{"id":5646,"depth":2333,"text":5926},"① 최신 날짜순 전체 조회 (.order(), .all())",{"id":5756,"depth":2333,"text":5928},"② 특정 조건 검색 (.where(), .first())",{"id":5879,"depth":186,"text":5880},"2026-07-11","Nuxt Content v3의 SQLite 기반 데이터 구조, Collections 시스템, Schema 정의 방식과 콘텐츠 조회 메커니즘 정리",{"tags":5933,"image":3401},[43,5934,1699,5935],"NuxtContent","brolog-log",{"icon":26},"\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-content-v3",{"title":5939,"description":5940},"Nuxt Content v3 데이터 모델과 Collections 구조 분석","Nuxt Content v3에서 콘텐츠가 저장되고 조회되는 과정과 SQLite 기반 데이터 모델, Collections, Query API 구조를 설명합니다.","ko\u002F1.note\u002F2.nuxt4\u002F3.nuxt-content-v3","T5_vMjqgv3GOUjiRgTOwBf8cKUMlbashZnQ4Gca_V14",{"id":5944,"title":61,"body":5945,"date":5930,"description":6249,"extension":188,"links":863,"meta":6250,"navigation":6255,"path":6256,"seo":6257,"stem":6260,"__hash__":6261},"docs_ko\u002Fko\u002F1.note\u002F2.nuxt4\u002F4.nuxt-auto-import.md",{"type":180,"value":5946,"toc":6233},[5947],[204,5948,5949,5953,5972,5982,5984,5988,5991,5997,6008,6031,6035,6061,6063,6067,6070,6074,6156,6158,6162,6169,6173,6182,6186,6202,6204,6208,6215],{":spotlight":206},[481,5950,5952],{"id":5951},"_1-정적-선언-방식의-한계와-추상화-레이어의-도입","1. 정적 선언 방식의 한계와 추상화 레이어의 도입",[213,5954,5955,5956,5959,5960,5963,5964,5967,5968,5971],{},"순수 Vue 3 컴포지션 API 기반의 프론트엔드 개발 환경에서는 컴포넌트를 작성할 때마다 파일 최상단에 ",[253,5957,5958],{},"import { ref, computed, onMounted } from 'vue'","와 같은 명시적 선언 구문이 강제된다. 또한 프로젝트의 규모가 커질수록 ",[253,5961,5962],{},"composables\u002F","나 ",[253,5965,5966],{},"components\u002F"," 디렉토리 내의 소스를 가져오기 위해 복잡한 상대 경로(",[253,5969,5970],{},"..\u002F..\u002Fcomponents\u002F",")를 수동으로 계산해야 하는 피로감이 동반된다.",[213,5973,5974,5977,5978,5981],{},[1641,5975,5976],{},"Nuxt 4","는 이러한 반복적이고 정적인 선언 지옥을 해결하기 위해 엔진 자체에 강력한 ",[1641,5979,5980],{},"Auto-imports(자동 임포트)"," 인프라를 기본 탑재했다. 이 시스템은 단순한 글로벌 변수 바인딩이 아니며, 빌드 타임에 가상 추상화 레이어를 형성하여 개발 생산성(DX)과 프로덕션 최적화(Performance)를 동시에 충족하는 정밀한 컴파일러 메커니즘으로 작동한다.",[232,5983],{},[481,5985,5987],{"id":5986},"_2-auto-imports-가상-링킹-아키텍처-및-동작-파이프라인","2. Auto-imports 가상 링킹 아키텍처 및 동작 파이프라인",[213,5989,5990],{},"Nuxt 4 엔진이 명시적 import 구문 없이도 코드의 참조 무결성을 유지하는 파이프라인은 크게 두 가지 단계(개발 시점의 가상 타입 맵 생성, 빌드 시점의 동적 주입)로 분리되어 작동한다.",[245,5992,5995],{"className":5993,"code":5994,"language":251,"meta":178},[248],"[ 개발자 코드 작성 ] ── (import 구문 생략 상태)\n       │\n       ├─► [ 개발 서버 \u002F nuxi prepare ] ──► .nuxt\u002F 디렉토리 내 가상 타입 생성 (*.d.ts)\n       │                                     └─ VS Code 등 IDE에서 정적 분석 및 자동완성 매핑\n       │\n       ▼ (프로덕션 빌드 롤업 컴파일러 가동)\n[ 정적 코드 분석 (AST Parser) ]\n       │\n       ├─► 전역 등록 맵과 소스 코드 내 식별자 매칭 검증\n       ▼\n[ 코드 트랜스파일링 (Transpiling) ] ──► 필요한 원본 import 구문을 실제 컴파일 결과물에 자동 주입\n       │\n       ▼\n[ 트리 쉐이킹 및 최종 번들 아웃풋 ] ──► 사용하지 않는 자원은 완벽히 제거된 최적화 상태 유지\n",[253,5996,5994],{"__ignoreMap":178},[2616,5998,6000,6001,6004,6005,5529],{"id":5999},"nuxt-디렉토리-내부의-가상-타입-맵핑-dts","① ",[253,6002,6003],{},".nuxt\u002F"," 디렉토리 내부의 가상 타입 맵핑 (",[253,6006,6007],{},"d.ts",[213,6009,6010,6011,6014,6015,6018,6019,6022,6023,6026,6027,6030],{},"개발 서버(",[253,6012,6013],{},"npm run dev",")를 실행하거나 ",[253,6016,6017],{},"npx nuxi prepare"," 명령어를 트리거하면, Nuxt 4 엔진은 프로젝트 루트와 하위 레이어의 에셋 경로를 정밀하게 스캔한다.\n그 후 ",[253,6020,6021],{},".nuxt\u002Fimports.d.ts"," 및 ",[253,6024,6025],{},".nuxt\u002Fcomponents.d.ts","와 같은 ",[1641,6028,6029],{},"엠비언트 타입 선언 파일","들을 실시간으로 빌드한다. 이 가상 지도가 프로젝트 전역에 바인딩되기 때문에 TypeScript 컴파일러와 IDE는 파일 상단에 import문이 없어도 해당 함수의 원본 출처와 매개변수 타입 스펙을 정확하게 인지하고 자동완성을 지원하게 된다.",[2616,6032,6034],{"id":6033},"추상-구문-트리ast-기반의-동적-주입-및-트리-쉐이킹tree-shaking","② 추상 구문 트리(AST) 기반의 동적 주입 및 트리 쉐이킹(Tree-shaking)",[213,6036,6037,6038,6041,6042,6045,6046,5963,6049,6052,6053,6056,6057,6060],{},"글로벌 ",[253,6039,6040],{},"window"," 객체에 변수나 컴포넌트를 강제로 할당하는 방식은 런타임 오염과 번들 크기 비대화를 초래한다. Nuxt 4는 이 문제를 ",[1641,6043,6044],{},"컴파일 타임의 코드 생성","으로 우회한다.\n최종 프로덕션 빌드 시점에서 Vite 및 Rollup 기반의 내장 컴파일러는 소스 코드를 AST(Abstract Syntax Tree) 구조로 파싱한다. 코드 내에 ",[253,6047,6048],{},"ref",[253,6050,6051],{},"\u003CCustomButton \u002F>"," 같은 식별자가 감지되면, 컴파일러가 번들링 직전 단계에서 ",[253,6054,6055],{},"import { ref } from 'vue'"," 코드를 ",[1641,6058,6059],{},"컴파일된 가상 자바스크립트 파일 상단에 물리적으로 결합","시킨다. 결과적으로 런타임 환경에서는 명시적으로 import를 작성한 코드와 완벽히 동일하게 동작하므로 트리 쉐이킹과 코드 스플리팅 이점이 훼손되지 않는다.",[232,6062],{},[481,6064,6066],{"id":6065},"_3-구조적-자동-스캔-범위-및-디렉토리-규칙","3. 구조적 자동 스캔 범위 및 디렉토리 규칙",[213,6068,6069],{},"Nuxt 4의 오토 임포트 시스템은 프레임워크 코어와 확장 모듈, 그리고 사용자가 지정한 물리적 디렉토리 규칙을 기반으로 삼원화되어 관리된다.",[2616,6071,6073],{"id":6072},"자동-임포트-대상-분석-및-특징","🤝 자동 임포트 대상 분석 및 특징",[588,6075,6076,6089],{},[591,6077,6078],{},[594,6079,6080,6083,6086],{},[597,6081,6082],{"align":4836},"구분 팩터",[597,6084,6085],{"align":4836},"스캔 대상 및 소속 생태계",[597,6087,6088],{"align":4836},"내부 동작 원리 및 특징",[604,6090,6091,6118,6141],{},[594,6092,6093,6098,6105],{},[609,6094,6095],{"align":4836},[1641,6096,6097],{},"Core APIs",[609,6099,6100,907,6102,6104],{"align":4836},[253,6101,2308],{},[253,6103,1721],{}," Core",[609,6106,6107,907,6109,907,6112,907,6115,6117],{"align":4836},[253,6108,6048],{},[253,6110,6111],{},"computed",[253,6113,6114],{},"useFetch",[253,6116,5456],{}," 등 프레임워크 핵심 런타임에 내장된 컴포저블을 상시 링킹 명단에 상주 처리함.",[594,6119,6120,6125,6130],{},[609,6121,6122],{"align":4836},[1641,6123,6124],{},"Components",[609,6126,6127,6129],{"align":4836},[253,6128,5966],{}," 디렉토리 전체",[609,6131,6132,6133,6136,6137,6140],{"align":4836},"파일 시스템 기반의 컴포넌트 자동 스캔 시스템. 하위 폴더 구조를 탐색하여 중첩 경로일 경우 폴더명을 접두사로 결합함. (예: ",[253,6134,6135],{},"components\u002Fblog\u002FCard.vue"," ➡️ ",[253,6138,6139],{},"\u003CBlogCard \u002F>"," 태그로 변환)",[594,6142,6143,6148,6153],{},[609,6144,6145],{"align":4836},[1641,6146,6147],{},"Composables",[609,6149,6150,6152],{"align":4836},[253,6151,5962],{}," 및 레이어 소스",[609,6154,6155],{"align":4836},"개발자가 직접 작성한 커스텀 헬퍼 함수 및 상태 관리용 함수를 자동 수집. 파일명과 export 이름을 키값으로 매핑하여 전역 공유 인프라에 태움.",[232,6157],{},[481,6159,6161],{"id":6160},"_4-확장-모듈modules과-레이어-구조에서의-오토-임포트-확장","4. 확장 모듈(Modules)과 레이어 구조에서의 오토 임포트 확장",[213,6163,6164,6165,6168],{},"이 시스템은 서드파티 모듈 및 Nuxt 4의 멀티 레이어(",[253,6166,6167],{},"extends",") 아키텍처와 호환될 때 높은 확장성을 보여준다.",[2616,6170,6172],{"id":6171},"nuxt-content-v3-등-외부-모듈의-연동-메커니즘","① Nuxt Content v3 등 외부 모듈의 연동 메커니즘",[213,6174,6175,6177,6178,6181],{},[253,6176,5596],{},"과 같은 신형 데이터 API 역시, Nuxt Content v3 모듈 컨텍스트 내부에서 ",[253,6179,6180],{},"addImports"," 코어를 통해 **\"해당 모듈이 활성화될 때 이 함수 스펙을 Nuxt 엔진의 글로벌 오토 임포트 레지스트리에 주입해달라\"**고 명시했기 때문에 개발자가 임포트 없이 가져다 쓸 수 있는 구조가 성립된다.",[2616,6183,6185],{"id":6184},"멀티-레이어-환경에서의-병합-순서","② 멀티 레이어 환경에서의 병합 순서",[213,6187,6188,6189,6192,6193,5963,6195,6197,6198,6201],{},"하위 레이어(",[253,6190,6191],{},"layer\u002F",")에 정의된 ",[253,6194,5966],{},[253,6196,5962],{}," 역시 메인 프로젝트(",[253,6199,6200],{},"docs\u002F",") 빌드 타임에 우선순위 체계를 거쳐 자동으로 병합(Merge)된다. 메인 프로젝트에서 동일한 이름의 컴포넌트나 함수를 선언할 경우, 오토 임포트 맵은 메인 소스를 최우선 순위로 지정하여 레이어 원본을 자연스럽게 오버라이딩(Overriding)하도록 설계되어 있다.",[232,6203],{},[481,6205,6207],{"id":6206},"_5-결론-및-아키텍처적-유의사항","5. 결론 및 아키텍처적 유의사항",[213,6209,6210,6211,6214],{},"Nuxt 4의 Auto-imports는 개발자에게 선언의 자유를 부여하고 가독성을 극대화하는 훌륭한 추상화 장치다. 그러나 모든 편리함은 프레임워크가 규정한 ",[1641,6212,6213],{},"엄격한 디렉토리 명명 규칙과 구조적 약속"," 위에서만 성립된다.",[1649,6216,6217,6223],{},[1652,6218,6219,6222],{},[1641,6220,6221],{},"명칭 충돌 제어:"," 서로 다른 폴더나 모듈에서 완전히 동일한 이름의 컴포저블을 내보낼 경우 오토 임포트 맵에 충돌이 발생하여 의도치 않은 함수가 호출될 위험성이 있으므로 고유한 네임스페이스 규칙을 정립하는 것이 안전하다.",[1652,6224,6225,6228,6229,6232],{},[1641,6226,6227],{},"추상화 레이어의 이해:"," 임포트 생략 뒤에서 실시간으로 가상 타입 파일(",[253,6230,6231],{},".d.ts",")을 컴파일하고 AST 분석을 제어하는 Nuxt 4 컴파일러의 내부 설계를 이해하고 활용하는 것은, 복잡한 모노레포나 멀티 레이어 프로젝트를 빌드 오류 없이 안정적으로 유지 관리하기 위한 핵심 기반 지식이다.",{"title":178,"searchDepth":186,"depth":186,"links":6234},[6235,6236,6241,6244,6248],{"id":5951,"depth":186,"text":5952},{"id":5986,"depth":186,"text":5987,"children":6237},[6238,6240],{"id":5999,"depth":2333,"text":6239},"① .nuxt\u002F 디렉토리 내부의 가상 타입 맵핑 (d.ts)",{"id":6033,"depth":2333,"text":6034},{"id":6065,"depth":186,"text":6066,"children":6242},[6243],{"id":6072,"depth":2333,"text":6073},{"id":6160,"depth":186,"text":6161,"children":6245},[6246,6247],{"id":6171,"depth":2333,"text":6172},{"id":6184,"depth":2333,"text":6185},{"id":6206,"depth":186,"text":6207},"Nuxt의 Auto-imports 시스템이 정적 분석을 통해 Composable과 Component를 연결하는 방식과 빌드 타임 처리 과정을 분석",{"tags":6251,"image":3401},[43,6252,1699,6253,6254],"AutoImports","Vue3","Compiler",{"icon":26},"\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-auto-import",{"title":6258,"description":6259},"Nuxt Auto-imports 내부 원리와 컴파일 과정 분석","Nuxt Auto-imports가 import 선언을 자동으로 처리하는 방식과 가상 모듈, 정적 분석, 트리 쉐이킹 구조를 살펴봅니다.","ko\u002F1.note\u002F2.nuxt4\u002F4.nuxt-auto-import","GL4C6a8j7c-t5hcpXpjB_lVHQlVCd1KvrpBxSBvpPqk",{"id":6263,"title":35,"body":6264,"date":7139,"description":7140,"extension":188,"links":863,"meta":7141,"navigation":7148,"path":7149,"seo":7150,"stem":7153,"__hash__":7154},"docs_ko\u002Fko\u002F1.note\u002F1.javascript\u002F1.destructuring.md",{"type":180,"value":6265,"toc":7124},[6266,7121],[204,6267,6268,6272,6287,6294,6296,6300,6303,6383,6393,6479,6481,6485,6496,6507,6509,6513,6521,6604,6607,6614,6616,6620,6623,6663,6666,6673,6676,6678,6682,6685,6688,6748,6751,6753,6757,6760,6805,6808,6815,6818,6820,6824,6827,6877,6879,6886,6889,6891,6895,6898,7006,7009,7016,7018,7022,7027,7032,7038,7041,7043,7047,7050,7057,7059,7063,7066,7072,7074,7078,7084,7091,7093,7097,7100,7105,7112,7115],{":spotlight":206},[208,6269,6271],{"id":6270},"가독성과-퍼포먼스를-모두-잡는-모던-자바스크립트-비동기-패턴","가독성과 퍼포먼스를 모두 잡는 모던 자바스크립트 비동기 패턴",[213,6273,6274,6282,6283,6286],{},[4052,6275],{"alt":6276,"className":6277,"src":6280,"width":6281},"자바스크립트 썸네일",[6278,6279],"mx-auto","mb-10","\u002Fimages\u002Fjavascript_thumbnail.png",450,"\n자바스크립트에서 복수의 비동기 데이터(API, 파일 시스템 등)를 처리할 때 단순히 ",[253,6284,6285],{},"await","를 순차적으로 나열하는 방식은 동기적 정체를 유발한다.",[213,6288,6289,6290,6293],{},"모던 자바스크립트(ES6+)는 ",[253,6291,6292],{},"Promise.all","과 구조 분해 할당(Destructuring Assignment)을 결합하여, 여러 비동기 요청을 병렬로 처리함과 동시에 결과 배열의 깊은 오브젝트 내부 속성까지 한 줄로 추출하는 강력한 표현력을 제공한다.",[232,6295],{},[481,6297,6299],{"id":6298},"_1-순차적-await와-병렬-promiseall의-실행-흐름-차이","1. 순차적 await와 병렬 Promise.all의 실행 흐름 차이",[213,6301,6302],{},"독립적인 비동기 연산 여러 개를 연이어 수행할 때, 직관적으로 작성된 코드는 대개 앞선 연산이 끝나기를 기다리는 병목을 발생시킨다.",[245,6304,6309],{"className":6305,"code":6306,"filename":6307,"language":6308,"meta":178,"style":178},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\n\u002F\u002F ❌ 순차적 실행: 총 2초 이상 소요되는 비효율적인 패턴\nconst postsResult = await useAsyncData('posts', ...) \u002F\u002F 1초 대기\nconst navResult = await useAsyncData('nav', ...) \u002F\u002F 앞선 연산 종료 후 1초 대기\n","Sequential Async Pattern","js",[253,6310,6311,6315,6320,6352],{"__ignoreMap":178},[389,6312,6313],{"class":391,"line":392},[389,6314,2315],{"emptyLinePlaceholder":190},[389,6316,6317],{"class":391,"line":186},[389,6318,6319],{"class":3108},"\u002F\u002F ❌ 순차적 실행: 총 2초 이상 소요되는 비효율적인 패턴\n",[389,6321,6322,6324,6327,6329,6331,6333,6335,6337,6340,6342,6344,6347,6349],{"class":391,"line":2333},[389,6323,455],{"class":454},[389,6325,6326],{"class":407}," postsResult ",[389,6328,461],{"class":399},[389,6330,2527],{"class":2526},[389,6332,5491],{"class":403},[389,6334,408],{"class":407},[389,6336,2535],{"class":399},[389,6338,6339],{"class":936},"posts",[389,6341,2535],{"class":399},[389,6343,5503],{"class":399},[389,6345,6346],{"class":399}," ...",[389,6348,5738],{"class":407},[389,6350,6351],{"class":3108},"\u002F\u002F 1초 대기\n",[389,6353,6354,6356,6359,6361,6363,6365,6367,6369,6372,6374,6376,6378,6380],{"class":391,"line":2344},[389,6355,455],{"class":454},[389,6357,6358],{"class":407}," navResult ",[389,6360,461],{"class":399},[389,6362,2527],{"class":2526},[389,6364,5491],{"class":403},[389,6366,408],{"class":407},[389,6368,2535],{"class":399},[389,6370,6371],{"class":936},"nav",[389,6373,2535],{"class":399},[389,6375,5503],{"class":399},[389,6377,6346],{"class":399},[389,6379,5738],{"class":407},[389,6381,6382],{"class":3108},"\u002F\u002F 앞선 연산 종료 후 1초 대기\n",[213,6384,6385,6386,6388,6389,6392],{},"반면 ",[253,6387,6292],{},"은 인자로 전달된 Promise 배열을 동시에 실행하여 병렬 처리한다.\n",[6390,6391],"br",{},"모든 Promise가 Fulfilled 상태가 되는 시점에 결과값들을 원본 요청 순서에 맞는 배열 형태로 반환한다.",[245,6394,6397],{"className":6305,"code":6395,"filename":6396,"language":6308,"meta":178,"style":178},"\n\u002F\u002F ✨ 병렬 실행: 동시에 요청하여 총 1초 소요\nconst results = await Promise.all([\n  useAsyncData('posts', ...),\n  useAsyncData('nav', ...)\n])\n\u002F\u002F results => [posts의 결과 오브젝트, nav의 결과 오브젝트]\n","Parallel Promise Pattern",[253,6398,6399,6403,6408,6429,6451,6469,6474],{"__ignoreMap":178},[389,6400,6401],{"class":391,"line":392},[389,6402,2315],{"emptyLinePlaceholder":190},[389,6404,6405],{"class":391,"line":186},[389,6406,6407],{"class":3108},"\u002F\u002F ✨ 병렬 실행: 동시에 요청하여 총 1초 소요\n",[389,6409,6410,6412,6415,6417,6419,6422,6424,6426],{"class":391,"line":2333},[389,6411,455],{"class":454},[389,6413,6414],{"class":407}," results ",[389,6416,461],{"class":399},[389,6418,2527],{"class":2526},[389,6420,6421],{"class":395}," Promise",[389,6423,5532],{"class":399},[389,6425,5559],{"class":403},[389,6427,6428],{"class":407},"([\n",[389,6430,6431,6434,6436,6438,6440,6442,6444,6446,6448],{"class":391,"line":2344},[389,6432,6433],{"class":403},"  useAsyncData",[389,6435,408],{"class":407},[389,6437,2535],{"class":399},[389,6439,6339],{"class":936},[389,6441,2535],{"class":399},[389,6443,5503],{"class":399},[389,6445,6346],{"class":399},[389,6447,5529],{"class":407},[389,6449,6450],{"class":399},",\n",[389,6452,6453,6455,6457,6459,6461,6463,6465,6467],{"class":391,"line":2356},[389,6454,6433],{"class":403},[389,6456,408],{"class":407},[389,6458,2535],{"class":399},[389,6460,6371],{"class":936},[389,6462,2535],{"class":399},[389,6464,5503],{"class":399},[389,6466,6346],{"class":399},[389,6468,415],{"class":407},[389,6470,6471],{"class":391,"line":2364},[389,6472,6473],{"class":407},"])\n",[389,6475,6476],{"class":391,"line":2374},[389,6477,6478],{"class":3108},"\u002F\u002F results => [posts의 결과 오브젝트, nav의 결과 오브젝트]\n",[232,6480],{},[481,6482,6484],{"id":6483},"_2-구조-분해-할당destructuring의-핵심-메커니즘","2. 구조 분해 할당(Destructuring)의 핵심 메커니즘",[213,6486,6487,6488,6491,6492,6495],{},"구조 분해 할당은 배열 리터럴(",[253,6489,6490],{},"[ ]",") 또는 객체 리터럴(",[253,6493,6494],{},"{ }",") 형태를 좌항에 선언하여 우항의 데이터 구조에서 필요한 값만 추출하는 문법이다.",[213,6497,6498,6499,6502,6503,6506],{},"단순히 ",[253,6500,6501],{},"results[0]","이나 ",[253,6504,6505],{},"results.data"," 같은 접근 코드를 줄이는 것이 아니라, 데이터 구조 자체를 코드에 표현하는 선언적인 방식이다.",[232,6508],{},[2616,6510,6512],{"id":6511},"_2-1-배열-구조-분해-할당의-순서-매핑","📦 2-1) 배열 구조 분해 할당의 순서 매핑",[213,6514,6515,6517,6518,6520],{},[253,6516,6292],{},"이 반환하는 결과는 순서가 보장되는 배열이다.\n",[6390,6519],{},"좌항에 배열 형태를 작성하면 우항 배열의 인덱스 순서에 따라 값이 자동으로 매핑된다.",[245,6522,6525],{"className":6305,"code":6523,"filename":6524,"language":6308,"meta":178,"style":178},"\u002F\u002F 배열의 0번째와 1번째 결과를 각각 변수에 할당\nconst [postsResult, navResult] = await Promise.all([\n  useAsyncData('posts', ...),\n  useAsyncData('nav', ...)\n])\n","Array Destructuring",[253,6526,6527,6532,6562,6582,6600],{"__ignoreMap":178},[389,6528,6529],{"class":391,"line":392},[389,6530,6531],{"class":3108},"\u002F\u002F 배열의 0번째와 1번째 결과를 각각 변수에 할당\n",[389,6533,6534,6536,6539,6542,6544,6547,6550,6552,6554,6556,6558,6560],{"class":391,"line":186},[389,6535,455],{"class":454},[389,6537,6538],{"class":399}," [",[389,6540,6541],{"class":407},"postsResult",[389,6543,5503],{"class":399},[389,6545,6546],{"class":407}," navResult",[389,6548,6549],{"class":399},"]",[389,6551,2523],{"class":399},[389,6553,2527],{"class":2526},[389,6555,6421],{"class":395},[389,6557,5532],{"class":399},[389,6559,5559],{"class":403},[389,6561,6428],{"class":407},[389,6563,6564,6566,6568,6570,6572,6574,6576,6578,6580],{"class":391,"line":2333},[389,6565,6433],{"class":403},[389,6567,408],{"class":407},[389,6569,2535],{"class":399},[389,6571,6339],{"class":936},[389,6573,2535],{"class":399},[389,6575,5503],{"class":399},[389,6577,6346],{"class":399},[389,6579,5529],{"class":407},[389,6581,6450],{"class":399},[389,6583,6584,6586,6588,6590,6592,6594,6596,6598],{"class":391,"line":2344},[389,6585,6433],{"class":403},[389,6587,408],{"class":407},[389,6589,2535],{"class":399},[389,6591,6371],{"class":936},[389,6593,2535],{"class":399},[389,6595,5503],{"class":399},[389,6597,6346],{"class":399},[389,6599,415],{"class":407},[389,6601,6602],{"class":391,"line":2356},[389,6603,6473],{"class":407},[213,6605,6606],{},"동작 구조:",[245,6608,6612],{"className":6609,"code":6610,"filename":6611,"language":251,"meta":178},[248],"\nPromise.all 결과\n\n[\n  posts 결과,\n  nav 결과\n]\n\n↓\n\nconst [\n  postsResult,\n  navResult\n]\n\n↓\n\n각 변수에 순서대로 할당\n","Array Mapping Flow",[253,6613,6610],{"__ignoreMap":178},[232,6615],{},[2616,6617,6619],{"id":6618},"️-2-2-객체-구조-분해-할당과-키key-매핑","🗂️ 2-2) 객체 구조 분해 할당과 키(Key) 매핑",[213,6621,6622],{},"객체 구조 분해 할당은 배열과 다르게 순서가 아닌 프로퍼티 Key를 기준으로 값을 찾는다.",[245,6624,6627],{"className":6305,"code":6625,"filename":6626,"language":6308,"meta":178,"style":178},"const {\n  data,\n  pending,\n  error\n} = result\n","Object Destructuring",[253,6628,6629,6635,6642,6649,6654],{"__ignoreMap":178},[389,6630,6631,6633],{"class":391,"line":392},[389,6632,455],{"class":454},[389,6634,2751],{"class":399},[389,6636,6637,6640],{"class":391,"line":186},[389,6638,6639],{"class":407},"  data",[389,6641,6450],{"class":399},[389,6643,6644,6647],{"class":391,"line":2333},[389,6645,6646],{"class":407},"  pending",[389,6648,6450],{"class":399},[389,6650,6651],{"class":391,"line":2344},[389,6652,6653],{"class":407},"  error\n",[389,6655,6656,6658,6660],{"class":391,"line":2356},[389,6657,2359],{"class":399},[389,6659,2523],{"class":399},[389,6661,6662],{"class":407}," result\n",[213,6664,6665],{},"객체 내부 구조:",[245,6667,6671],{"className":6668,"code":6669,"filename":6670,"language":251,"meta":178},[248],"{\n  data: [...],\n  pending: false,\n  error: null\n}\n\n↓\n\n{\n  data,\n  pending,\n  error\n}\n\n↓\n\n동일한 Key 기준으로 값 추출\n","Object Mapping Flow",[253,6672,6669],{"__ignoreMap":178},[213,6674,6675],{},"객체의 순서가 변경되어도 Key 이름만 동일하면 정상적으로 값을 가져온다.",[232,6677],{},[481,6679,6681],{"id":6680},"_3-중첩된-객체-구조-분해와-변수명-재정의aliasing","3. 중첩된 객체 구조 분해와 변수명 재정의(Aliasing)",[213,6683,6684],{},"비동기 데이터 처리 라이브러리는 단순한 데이터만 반환하지 않는다.",[213,6686,6687],{},"일반적으로 다음과 같은 Wrapper Object 형태를 반환한다.",[245,6689,6692],{"className":3568,"code":6690,"filename":6691,"language":3570,"meta":178,"style":178},"\n{\n  \"data\": [],\n  \"pending\": false,\n  \"error\": null\n}\n","Async Response Structure",[253,6693,6694,6698,6702,6716,6730,6744],{"__ignoreMap":178},[389,6695,6696],{"class":391,"line":392},[389,6697,2315],{"emptyLinePlaceholder":190},[389,6699,6700],{"class":391,"line":186},[389,6701,2341],{"class":399},[389,6703,6704,6706,6709,6711,6713],{"class":391,"line":2333},[389,6705,3581],{"class":399},[389,6707,6708],{"class":454},"data",[389,6710,1239],{"class":399},[389,6712,400],{"class":399},[389,6714,6715],{"class":399}," [],\n",[389,6717,6718,6720,6723,6725,6727],{"class":391,"line":2344},[389,6719,3581],{"class":399},[389,6721,6722],{"class":454},"pending",[389,6724,1239],{"class":399},[389,6726,400],{"class":399},[389,6728,6729],{"class":399}," false,\n",[389,6731,6732,6734,6737,6739,6741],{"class":391,"line":2356},[389,6733,3581],{"class":399},[389,6735,6736],{"class":454},"error",[389,6738,1239],{"class":399},[389,6740,400],{"class":399},[389,6742,6743],{"class":399}," null\n",[389,6745,6746],{"class":391,"line":2364},[389,6747,3620],{"class":399},[213,6749,6750],{},"이 중 필요한 값만 추출하고 새로운 변수명을 지정할 수 있다.",[232,6752],{},[2616,6754,6756],{"id":6755},"_3-1-️-변수명-재정의-aliasing","3-1) 🏷️ 변수명 재정의 (Aliasing)",[213,6758,6759],{},"객체 구조 분해에서는 다음과 같은 문법을 사용할 수 있다.",[245,6761,6764],{"className":6305,"code":6762,"filename":6763,"language":6308,"meta":178,"style":178},"\u002F\u002F data 속성을 posts라는 이름으로 변경\nconst { data: posts } = await useAsyncData('posts', ...)\n","Property Aliasing",[253,6765,6766,6771],{"__ignoreMap":178},[389,6767,6768],{"class":391,"line":392},[389,6769,6770],{"class":3108},"\u002F\u002F data 속성을 posts라는 이름으로 변경\n",[389,6772,6773,6775,6777,6779,6781,6783,6785,6787,6789,6791,6793,6795,6797,6799,6801,6803],{"class":391,"line":186},[389,6774,455],{"class":454},[389,6776,2515],{"class":399},[389,6778,5477],{"class":2323},[389,6780,400],{"class":399},[389,6782,5482],{"class":407},[389,6784,2359],{"class":399},[389,6786,2523],{"class":399},[389,6788,2527],{"class":2526},[389,6790,5491],{"class":403},[389,6792,408],{"class":407},[389,6794,2535],{"class":399},[389,6796,6339],{"class":936},[389,6798,2535],{"class":399},[389,6800,5503],{"class":399},[389,6802,6346],{"class":399},[389,6804,415],{"class":407},[213,6806,6807],{},"동작:",[245,6809,6813],{"className":6810,"code":6811,"filename":6812,"language":251,"meta":178},[248],"\n{\n  data: [...]\n}\n\n↓\n\ndata 추출\n\n↓\n\nposts 변수에 할당\n","Aliasing Flow",[253,6814,6811],{"__ignoreMap":178},[213,6816,6817],{},"이를 통해 변수 이름 충돌을 방지하고 현재 Context에 맞는 이름을 사용할 수 있다.",[232,6819],{},[2616,6821,6823],{"id":6822},"_3-2-️-기본값default-value-지정","3-2) 🛡️ 기본값(Default Value) 지정",[213,6825,6826],{},"구조 분해 할당에서는 값이 존재하지 않을 경우를 대비해 기본값을 지정할 수 있다.",[245,6828,6831],{"className":6305,"code":6829,"filename":6830,"language":6308,"meta":178,"style":178},"\u002F\u002F data가 undefined인 경우 빈 배열 사용\nconst { data: posts = [] } = await useAsyncData('posts', ...)\n","Default Value",[253,6832,6833,6838],{"__ignoreMap":178},[389,6834,6835],{"class":391,"line":392},[389,6836,6837],{"class":3108},"\u002F\u002F data가 undefined인 경우 빈 배열 사용\n",[389,6839,6840,6842,6844,6846,6848,6850,6852,6855,6857,6859,6861,6863,6865,6867,6869,6871,6873,6875],{"class":391,"line":186},[389,6841,455],{"class":454},[389,6843,2515],{"class":399},[389,6845,5477],{"class":2323},[389,6847,400],{"class":399},[389,6849,5482],{"class":407},[389,6851,461],{"class":399},[389,6853,6854],{"class":407}," [] ",[389,6856,2359],{"class":399},[389,6858,2523],{"class":399},[389,6860,2527],{"class":2526},[389,6862,5491],{"class":403},[389,6864,408],{"class":407},[389,6866,2535],{"class":399},[389,6868,6339],{"class":936},[389,6870,2535],{"class":399},[389,6872,5503],{"class":399},[389,6874,6346],{"class":399},[389,6876,415],{"class":407},[213,6878,6807],{},[245,6880,6884],{"className":6881,"code":6882,"filename":6883,"language":251,"meta":178},[248],"\ndata 존재\n\n↓\n\ndata 값 사용\n\n\ndata 없음\n\n↓\n\n[] 기본값 사용\n","Default Value Flow",[253,6885,6882],{"__ignoreMap":178},[213,6887,6888],{},"비동기 요청 실패나 초기 렌더링 과정에서 안전하게 데이터를 처리할 수 있다.",[232,6890],{},[481,6892,6894],{"id":6893},"_4-최종-형태-복합-중첩-구조-분해","4. 최종 형태: 복합 중첩 구조 분해",[213,6896,6897],{},"배열 구조 분해 내부에 객체 구조 분해, Alias, 기본값 설정을 함께 적용하면 다음과 같은 형태가 된다.",[245,6899,6902],{"className":6305,"code":6900,"filename":6901,"language":6308,"meta":178,"style":178},"const [\n  { data: posts = [] },\n  { data: navData = [] }\n] = await Promise.all([\n  useAsyncData('main-posts-feed', ...),\n  useAsyncData('main-posts-nav', ...)\n])\n","Nested Destructuring Pattern",[253,6903,6904,6911,6929,6946,6962,6983,7002],{"__ignoreMap":178},[389,6905,6906,6908],{"class":391,"line":392},[389,6907,455],{"class":454},[389,6909,6910],{"class":399}," [\n",[389,6912,6913,6916,6918,6920,6922,6924,6926],{"class":391,"line":186},[389,6914,6915],{"class":399},"  {",[389,6917,5477],{"class":2323},[389,6919,400],{"class":399},[389,6921,5482],{"class":407},[389,6923,461],{"class":399},[389,6925,6854],{"class":407},[389,6927,6928],{"class":399},"},\n",[389,6930,6931,6933,6935,6937,6940,6942,6944],{"class":391,"line":2333},[389,6932,6915],{"class":399},[389,6934,5477],{"class":2323},[389,6936,400],{"class":399},[389,6938,6939],{"class":407}," navData ",[389,6941,461],{"class":399},[389,6943,6854],{"class":407},[389,6945,3620],{"class":399},[389,6947,6948,6950,6952,6954,6956,6958,6960],{"class":391,"line":2344},[389,6949,6549],{"class":399},[389,6951,2523],{"class":399},[389,6953,2527],{"class":2526},[389,6955,6421],{"class":395},[389,6957,5532],{"class":399},[389,6959,5559],{"class":403},[389,6961,6428],{"class":407},[389,6963,6964,6966,6968,6970,6973,6975,6977,6979,6981],{"class":391,"line":2356},[389,6965,6433],{"class":403},[389,6967,408],{"class":407},[389,6969,2535],{"class":399},[389,6971,6972],{"class":936},"main-posts-feed",[389,6974,2535],{"class":399},[389,6976,5503],{"class":399},[389,6978,6346],{"class":399},[389,6980,5529],{"class":407},[389,6982,6450],{"class":399},[389,6984,6985,6987,6989,6991,6994,6996,6998,7000],{"class":391,"line":2364},[389,6986,6433],{"class":403},[389,6988,408],{"class":407},[389,6990,2535],{"class":399},[389,6992,6993],{"class":936},"main-posts-nav",[389,6995,2535],{"class":399},[389,6997,5503],{"class":399},[389,6999,6346],{"class":399},[389,7001,415],{"class":407},[389,7003,7004],{"class":391,"line":2374},[389,7005,6473],{"class":407},[213,7007,7008],{},"위 코드는 다음 과정을 한 번에 수행한다.",[245,7010,7014],{"className":7011,"code":7012,"filename":7013,"language":251,"meta":178},[248],"\nPromise.all 실행\n\n↓\n\n[\n  posts Response Object,\n  nav Response Object\n]\n\n↓\n\n첫 번째 객체에서 data 추출\n\n↓\n\nposts 변수 저장\n\n\n두 번째 객체에서 data 추출\n\n↓\n\nnavData 변수 저장\n","Nested Destructuring Flow",[253,7015,7012],{"__ignoreMap":178},[232,7017],{},[2616,7019,7021],{"id":7020},"동작-프로세스-분석","🔍 동작 프로세스 분석",[7023,7024,7026],"h4",{"id":7025},"_1-병렬-요청","1. 병렬 요청",[213,7028,7029,7031],{},[253,7030,6292],{}," 내부의 비동기 함수들이 동시에 실행된다.",[245,7033,7036],{"className":7034,"code":7035,"language":251,"meta":178},[248],"Request A 시작\n+\nRequest B 시작\n",[253,7037,7035],{"__ignoreMap":178},[213,7039,7040],{},"순차 실행이 아니기 때문에 전체 실행 시간은 가장 오래 걸리는 요청 기준으로 결정된다.",[232,7042],{},[7023,7044,7046],{"id":7045},"_2-결과-배열-생성","2. 결과 배열 생성",[213,7048,7049],{},"모든 Promise가 완료되면 Promise.all은 결과 배열을 반환한다.",[245,7051,7055],{"className":7052,"code":7053,"filename":7054,"language":251,"meta":178},[248],"[\n  Response Object A,\n  Response Object B\n]\n","Promise.all Result",[253,7056,7053],{"__ignoreMap":178},[232,7058],{},[7023,7060,7062],{"id":7061},"_3-배열-분해","3. 배열 분해",[213,7064,7065],{},"좌항의 배열 구조 분해가 실행되어 각각의 결과를 분리한다.",[245,7067,7070],{"className":7068,"code":7069,"language":251,"meta":178},[248],"[\n  Object A,\n  Object B\n]\n\n↓\n\n[\n  first,\n  second\n]\n",[253,7071,7069],{"__ignoreMap":178},[232,7073],{},[7023,7075,7077],{"id":7076},"_4-객체-내부-값-추출","4. 객체 내부 값 추출",[213,7079,7080,7081,7083],{},"각 Object 내부의 ",[253,7082,6708],{}," 속성을 찾아 변수에 저장한다.",[245,7085,7089],{"className":7086,"code":7087,"filename":7088,"language":251,"meta":178},[248],"\nObject A.data\n\n↓\n\nposts\n\n\nObject B.data\n\n↓\n\nnavData\n","Final Binding",[253,7090,7087],{"__ignoreMap":178},[232,7092],{},[481,7094,7096],{"id":7095},"정리","📌 정리",[213,7098,7099],{},"복합 구조 분해 할당은 여러 단계에 걸쳐 작성해야 했던 데이터 접근 코드를 하나의 선언으로 표현할 수 있게 해준다.",[213,7101,7102,7104],{},[253,7103,6292],{},"과 함께 사용하면 다음과 같은 장점을 얻을 수 있다.",[245,7106,7110],{"className":7107,"code":7108,"filename":7109,"language":251,"meta":178},[248],"\nPromise.all\n\n↓\n\n비동기 요청 병렬 처리\n\n+\n\nDestructuring\n\n↓\n\n필요한 데이터만 추출\n\n+\n\nAliasing\n\n↓\n\n명확한 변수명 사용\n\n+\n\nDefault Value\n\n↓\n\n안정적인 예외 처리\n","Async Pattern Benefits",[253,7111,7108],{"__ignoreMap":178},[213,7113,7114],{},"결과적으로 모던 자바스크립트의 비동기 패턴은 단순히 실행 속도를 개선하는 것을 넘어, 복잡한 데이터 흐름을 더 읽기 쉽고 유지보수 가능한 코드로 표현할 수 있도록 발전했다.",[213,7116,7117,7118,7120],{},"특히 Nuxt와 같은 데이터 페칭 기반 프레임워크에서는 여러 API 요청을 동시에 처리하고 필요한 상태만 추출하는 패턴이 자주 사용되므로, ",[253,7119,6292],{},"과 구조 분해 할당 조합은 중요한 개발 패턴으로 활용된다.",[851,7122,7123],{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":178,"searchDepth":186,"depth":186,"links":7125},[7126,7127,7131,7135,7138],{"id":6298,"depth":186,"text":6299},{"id":6483,"depth":186,"text":6484,"children":7128},[7129,7130],{"id":6511,"depth":2333,"text":6512},{"id":6618,"depth":2333,"text":6619},{"id":6680,"depth":186,"text":6681,"children":7132},[7133,7134],{"id":6755,"depth":2333,"text":6756},{"id":6822,"depth":2333,"text":6823},{"id":6893,"depth":186,"text":6894,"children":7136},[7137],{"id":7020,"depth":2333,"text":7021},{"id":7095,"depth":186,"text":7096},"2026-07-10","비동기 요청 결과를 구조 분해하고 객체 형태로 매핑하는 JavaScript 데이터 처리 방식 분석",{"tags":7142,"image":7147},[7143,7144,7145,7146],"JavaScript","ES6","Asynchronous","Destructuring","images\u002Fjavascript_thumbnail.png",{"icon":26},"\u002Fko\u002Fnote\u002Fjavascript\u002Fdestructuring",{"title":7151,"description":7152},"Promise.all 데이터 처리와 JavaScript 객체 매핑 패턴","Promise.all 결과를 구조 분해 할당과 객체 매핑으로 처리하는 효율적인 데이터 가공 방법을 정리합니다.","ko\u002F1.note\u002F1.javascript\u002F1.destructuring","T3AKq_ikxIDVtdfa0ql6PcU_z839XjtWWTNsl9HCxu0",{"id":7156,"title":39,"body":7157,"date":7139,"description":7921,"extension":188,"links":863,"meta":7922,"navigation":7927,"path":7928,"seo":7929,"stem":7932,"__hash__":7933},"docs_ko\u002Fko\u002F1.note\u002F1.javascript\u002F2.filter.md",{"type":180,"value":7158,"toc":7903},[7159,7900],[204,7160,7161,7165,7185,7187,7191,7194,7201,7215,7217,7224,7234,7304,7308,7313,7346,7361,7387,7389,7393,7402,7413,7420,7454,7512,7519,7534,7575,7585,7592,7761,7768,7775,7859,7861,7865],{":spotlight":206},[208,7162,7164],{"id":7163},"고차-함수-구조를-통해-이해하는-선언적-배열-제어","고차 함수 구조를 통해 이해하는 선언적 배열 제어",[213,7166,7167,7170,7171,907,7174,907,7177,7180,7181,7184],{},[4052,7168],{"alt":6276,"className":7169,"src":6280,"width":6281},[6278,6279],"\n자바스크립트에서 배열을 조작할 때 사용하는 ",[253,7172,7173],{},"filter",[253,7175,7176],{},"some",[253,7178,7179],{},"map"," 등은 모두 **고차 함수(Higher-Order Function)**라는 프로그래밍 패러다임 위에 구축되어 있다. 단순히 반복문을 대체하는 편의 기능 수준을 넘어, 함수를 다루는 자바스크립트만의 독특한 문법적 성격을 이해해야 ",[253,7182,7183],{},"filter(Boolean)"," 같은 생략형 패턴의 진짜 원리를 파악할 수 있다. 고차 함수의 본질적인 개념과 함께, 실무에서 데이터 흐름을 명확하게 통제해 주는 유용한 배열 고차 함수들을 해부한다.",[232,7186],{},[481,7188,7190],{"id":7189},"_1-고차-함수higher-order-function란-무엇인가","1. 고차 함수(Higher-Order Function)란 무엇인가?",[213,7192,7193],{},"자바스크립트에서 함수는 **'일급 객체(First-Class Object)'**다. 이는 함수를 일반 변수나 객체와 완벽하게 동일하게 취급할 수 있음을 의미한다. 함수를 다른 함수의 인자로 전달할 수도 있고, 함수가 또 다른 함수를 결과로 리턴할 수도 있다.",[213,7195,7196,7197,7200],{},"이 메커니즘을 따르는 함수를 ",[1641,7198,7199],{},"고차 함수","라고 부른다.",[1649,7202,7203,7209],{},[1652,7204,7205,7208],{},[1641,7206,7207],{},"고차 함수의 조건:"," 함수를 인자로 받거나(Callback 함수), 함수를 반환하는 함수",[1652,7210,7211,7214],{},[1641,7212,7213],{},"이점:"," 어떻게(How) 배열을 순회할지 프로그래밍하는 상세 로직은 자바스크립트 내부 엔진에 위임하고, 개발자는 무엇을(What) 필터링하거나 변형할지 '조건과 비즈니스 로직'만 함수 형태로 주입하면 되므로 코드가 선언적이고 간결해진다.",[232,7216],{},[481,7218,1746,7220,7223],{"id":7219},"_2-filterboolean의-구조적-작동-원리",[253,7221,7222],{},".filter(Boolean)","의 구조적 작동 원리",[213,7225,7226,7227,7230,7231,7233],{},"카테고리 경로 등을 split한 후 유효하지 않은 문자열이나 ",[253,7228,7229],{},"null"," 등을 제거하기 위해 사용하는 ",[253,7232,7183],{},"은 고차 함수의 성격을 100% 활용한 대표적인 문법이다.",[245,7235,7237],{"className":6305,"code":7236,"language":6308,"meta":178,"style":178},"const pathParts = \"\u002Fcontent\u002Fko\u002Ftech\u002Fspring\u002F\u002F\".split('\u002F')\n\u002F\u002F 결과: ['', 'content', 'ko', 'tech', 'spring', '', '']\n\nconst cleanParts = pathParts.filter(Boolean)\n\u002F\u002F 결과: ['content', 'ko', 'tech', 'spring']\n",[253,7238,7239,7271,7276,7280,7299],{"__ignoreMap":178},[389,7240,7241,7243,7246,7248,7250,7253,7255,7257,7260,7262,7264,7267,7269],{"class":391,"line":392},[389,7242,455],{"class":454},[389,7244,7245],{"class":407}," pathParts ",[389,7247,461],{"class":399},[389,7249,3605],{"class":399},[389,7251,7252],{"class":936},"\u002Fcontent\u002Fko\u002Ftech\u002Fspring\u002F\u002F",[389,7254,1239],{"class":399},[389,7256,5532],{"class":399},[389,7258,7259],{"class":403},"split",[389,7261,408],{"class":407},[389,7263,2535],{"class":399},[389,7265,7266],{"class":936},"\u002F",[389,7268,2535],{"class":399},[389,7270,415],{"class":407},[389,7272,7273],{"class":391,"line":186},[389,7274,7275],{"class":3108},"\u002F\u002F 결과: ['', 'content', 'ko', 'tech', 'spring', '', '']\n",[389,7277,7278],{"class":391,"line":2333},[389,7279,2315],{"emptyLinePlaceholder":190},[389,7281,7282,7284,7287,7289,7292,7294,7296],{"class":391,"line":2344},[389,7283,455],{"class":454},[389,7285,7286],{"class":407}," cleanParts ",[389,7288,461],{"class":399},[389,7290,7291],{"class":407}," pathParts",[389,7293,5532],{"class":399},[389,7295,7173],{"class":403},[389,7297,7298],{"class":407},"(Boolean)\n",[389,7300,7301],{"class":391,"line":2356},[389,7302,7303],{"class":3108},"\u002F\u002F 결과: ['content', 'ko', 'tech', 'spring']\n",[2616,7305,7307],{"id":7306},"왜-괄호-없이-함수-이름만-써도-작동할까","🔍 왜 괄호 없이 함수 이름만 써도 작동할까?",[213,7309,7310,7312],{},[253,7311,7173],{},"는 자바스크립트가 제공하는 고차 함수이며, 인자로 '참\u002F거짓을 반환하는 콜백 함수'를 받도록 설계되어 있다. 원래 식을 생략 없이 풀어서 쓰면 아래와 같다.",[245,7314,7316],{"className":6305,"code":7315,"language":6308,"meta":178,"style":178},"const cleanParts = pathParts.filter(item => Boolean(item))\n",[253,7317,7318],{"__ignoreMap":178},[389,7319,7320,7322,7324,7326,7328,7330,7332,7334,7338,7340,7343],{"class":391,"line":392},[389,7321,455],{"class":454},[389,7323,7286],{"class":407},[389,7325,461],{"class":399},[389,7327,7291],{"class":407},[389,7329,5532],{"class":399},[389,7331,7173],{"class":403},[389,7333,408],{"class":407},[389,7335,7337],{"class":7336},"sHdIc","item",[389,7339,5509],{"class":454},[389,7341,7342],{"class":403}," Boolean",[389,7344,7345],{"class":407},"(item))\n",[213,7347,7348,7349,7352,7353,7356,7357,7360],{},"자바스크립트의 고차 함수 구동 시, 인자로 전달하는 익명 함수(",[253,7350,7351],{},"item => ...",")의 매개변수 구조가 내부적으로 호출할 타겟 함수(",[253,7354,7355],{},"Boolean",")의 매개변수 구조와 1:1로 정확히 일치하면, 중간 전달자 역할을 하는 익명 함수 구문을 생략하고 ",[1641,7358,7359],{},"함수의 참조(Reference) 이름 자체","를 직접 넘길 수 있다.",[213,7362,7363,7365,7366,7368,7369,7371,7372,7375,7376,7378,7379,7382,7383,7386],{},[253,7364,7173],{}," 엔진은 순회 중인 원소(",[253,7367,7337],{},")를 ",[253,7370,7355],{}," 함수에 그대로 주입하고, ",[253,7373,7374],{},"Boolean()","은 값이 있으면 ",[253,7377,206],{},", 빈 문자열이나 ",[253,7380,7381],{},"undefined"," 같은 Falsy 값이면 ",[253,7384,7385],{},"false","를 내뱉으므로 완벽하게 필터링이 완료된다.",[232,7388],{},[481,7390,7392],{"id":7391},"_3-효율적-배열-제어를-위한-주요-고차-함수-리스트","3. 효율적 배열 제어를 위한 주요 고차 함수 리스트",[213,7394,7395,7396,7398,7399,7401],{},"흔히 쓰이는 ",[253,7397,7179],{},"이나 기본 ",[253,7400,7173],{}," 외에, 특정 비즈니스 조건을 효율적으로 끊어내거나 정밀하게 다룰 때 유용한 고차 함수들이다.",[2616,7403,7405,7406,7409,7410],{"id":7404},"_1-조기-종료로-연산을-최적화하는-some-every","⚡ 1) 조기 종료로 연산을 최적화하는 ",[253,7407,7408],{},".some()"," \u002F ",[253,7411,7412],{},".every()",[213,7414,7415,7416,7419],{},"배열 내부의 조건 충족 여부를 판단할 때, ",[253,7417,7418],{},"filter().length > 0","을 쓰면 배열 전체를 끝까지 순회하므로 메모리와 연산 낭비가 발생한다. 반면 이 둘은 조건 만족 즉시 동작을 멈춘다.",[1649,7421,7422,7437],{},[1652,7423,7424,7429,7430,7433,7434,7436],{},[1641,7425,7426,400],{},[253,7427,7428],{},"some()"," 배열 원소 중 ",[1641,7431,7432],{},"단 하나라도"," 조건 함수를 만족하면 즉시 순회를 중단(Short-circuit)하고 ",[253,7435,206],{},"를 반환한다.",[1652,7438,7439,7444,7445,7447,7448,7450,7451,7453],{},[1641,7440,7441,400],{},[253,7442,7443],{},"every()"," 모든 원소가 조건을 만족해야만 ",[253,7446,206],{},"를 반환하며, 순회 중 ",[1641,7449,7432],{}," 탈락 조건이 발견되면 그 즉시 순회를 멈추고 ",[253,7452,7385],{},"를 뱉는다.",[245,7455,7457],{"className":6305,"code":7456,"language":6308,"meta":178,"style":178},"\u002F\u002F activeTags 중 현재 글의 태그에 매칭되는 게 하나라도 나오면 탐색 즉시 종료\nconst matchTag = post.meta.tags.some(tag => activeTags.value.includes(tag))\n",[253,7458,7459,7464],{"__ignoreMap":178},[389,7460,7461],{"class":391,"line":392},[389,7462,7463],{"class":3108},"\u002F\u002F activeTags 중 현재 글의 태그에 매칭되는 게 하나라도 나오면 탐색 즉시 종료\n",[389,7465,7466,7468,7471,7473,7476,7478,7481,7483,7485,7487,7489,7491,7494,7496,7499,7501,7504,7506,7509],{"class":391,"line":186},[389,7467,455],{"class":454},[389,7469,7470],{"class":407}," matchTag ",[389,7472,461],{"class":399},[389,7474,7475],{"class":407}," post",[389,7477,5532],{"class":399},[389,7479,7480],{"class":407},"meta",[389,7482,5532],{"class":399},[389,7484,4435],{"class":407},[389,7486,5532],{"class":399},[389,7488,7176],{"class":403},[389,7490,408],{"class":407},[389,7492,7493],{"class":7336},"tag",[389,7495,5509],{"class":454},[389,7497,7498],{"class":407}," activeTags",[389,7500,5532],{"class":399},[389,7502,7503],{"class":407},"value",[389,7505,5532],{"class":399},[389,7507,7508],{"class":403},"includes",[389,7510,7511],{"class":407},"(tag))\n",[2616,7513,7515,7516],{"id":7514},"_2-다차원-구조를-파괴하며-결합하는-flatmap","🧩 2) 다차원 구조를 파괴하며 결합하는 ",[253,7517,7518],{},".flatMap()",[213,7520,7521,7522,7525,7526,7529,7530,7533],{},"배열을 가공한 결과물이 중첩 배열(2차원 구조)로 떨어질 때, 이를 1차원 평탄화 배열로 변환하기 위해 객체 내부에서 ",[253,7523,7524],{},"map()","을 돌린 후 ",[253,7527,7528],{},"flat()","을 연이어 호출하는 중복 연산을 자주 수행한다. ",[253,7531,7532],{},"flatMap","은 이 두 고차 함수 아키텍처를 단 한 번의 순회 매핑 인터페이스로 병합한 구조다.",[245,7535,7537],{"className":6305,"code":7536,"language":6308,"meta":178,"style":178},"\u002F\u002F 각 카테고리 오브젝트가 가지고 있는 하위 태그 배열들을 하나의 단일 리스트로 가공\nconst totalTags = categories.flatMap(category => category.tags)\n",[253,7538,7539,7544],{"__ignoreMap":178},[389,7540,7541],{"class":391,"line":392},[389,7542,7543],{"class":3108},"\u002F\u002F 각 카테고리 오브젝트가 가지고 있는 하위 태그 배열들을 하나의 단일 리스트로 가공\n",[389,7545,7546,7548,7551,7553,7556,7558,7560,7562,7565,7567,7570,7572],{"class":391,"line":186},[389,7547,455],{"class":454},[389,7549,7550],{"class":407}," totalTags ",[389,7552,461],{"class":399},[389,7554,7555],{"class":407}," categories",[389,7557,5532],{"class":399},[389,7559,7532],{"class":403},[389,7561,408],{"class":407},[389,7563,7564],{"class":7336},"category",[389,7566,5509],{"class":454},[389,7568,7569],{"class":407}," category",[389,7571,5532],{"class":399},[389,7573,7574],{"class":407},"tags)\n",[2616,7576,7578,7579,7409,7582],{"id":7577},"_3-정밀한-후방-탐색을-지원하는-findlast-findlastindex","🔍 3) 정밀한 후방 탐색을 지원하는 ",[253,7580,7581],{},".findLast()",[253,7583,7584],{},".findLastIndex()",[213,7586,7587,7588,7591],{},"조건에 부합하는 요소를 배열의 처음(인덱스 0)이 아닌, ",[1641,7589,7590],{},"배열의 맨 마지막(역순)부터"," 훑으며 찾아내려가는 고차 함수다. 시간 순서대로 정렬된 아카이브 데이터나 로그 기록에서 '가장 최근에 추가된 타겟 요소'의 위치 및 값을 정확히 타격할 때 유용하다.",[245,7593,7595],{"className":6305,"code":7594,"language":6308,"meta":178,"style":178},"const logs = [\n  { action: 'login', date: '01일' },\n  { action: 'write', date: '02일' },\n  { action: 'login', date: '03일' }\n]\n\n\u002F\u002F 뒤에서부터 거꾸로 뒤져 가장 최근에 발생한 login 객체를 즉시 반환\nconst lastLogin = logs.findLast(log => log.action === 'login') \u002F\u002F { action: 'login', date: '03일' }\n",[253,7596,7597,7608,7641,7671,7701,7706,7710,7715],{"__ignoreMap":178},[389,7598,7599,7601,7604,7606],{"class":391,"line":392},[389,7600,455],{"class":454},[389,7602,7603],{"class":407}," logs ",[389,7605,461],{"class":399},[389,7607,6910],{"class":407},[389,7609,7610,7612,7615,7617,7619,7622,7624,7626,7629,7631,7633,7636,7638],{"class":391,"line":186},[389,7611,6915],{"class":399},[389,7613,7614],{"class":2323}," action",[389,7616,400],{"class":399},[389,7618,4005],{"class":399},[389,7620,7621],{"class":936},"login",[389,7623,2535],{"class":399},[389,7625,5503],{"class":399},[389,7627,7628],{"class":2323}," date",[389,7630,400],{"class":399},[389,7632,4005],{"class":399},[389,7634,7635],{"class":936},"01일",[389,7637,2535],{"class":399},[389,7639,7640],{"class":399}," },\n",[389,7642,7643,7645,7647,7649,7651,7654,7656,7658,7660,7662,7664,7667,7669],{"class":391,"line":2333},[389,7644,6915],{"class":399},[389,7646,7614],{"class":2323},[389,7648,400],{"class":399},[389,7650,4005],{"class":399},[389,7652,7653],{"class":936},"write",[389,7655,2535],{"class":399},[389,7657,5503],{"class":399},[389,7659,7628],{"class":2323},[389,7661,400],{"class":399},[389,7663,4005],{"class":399},[389,7665,7666],{"class":936},"02일",[389,7668,2535],{"class":399},[389,7670,7640],{"class":399},[389,7672,7673,7675,7677,7679,7681,7683,7685,7687,7689,7691,7693,7696,7698],{"class":391,"line":2344},[389,7674,6915],{"class":399},[389,7676,7614],{"class":2323},[389,7678,400],{"class":399},[389,7680,4005],{"class":399},[389,7682,7621],{"class":936},[389,7684,2535],{"class":399},[389,7686,5503],{"class":399},[389,7688,7628],{"class":2323},[389,7690,400],{"class":399},[389,7692,4005],{"class":399},[389,7694,7695],{"class":936},"03일",[389,7697,2535],{"class":399},[389,7699,7700],{"class":399}," }\n",[389,7702,7703],{"class":391,"line":2356},[389,7704,7705],{"class":407},"]\n",[389,7707,7708],{"class":391,"line":2364},[389,7709,2315],{"emptyLinePlaceholder":190},[389,7711,7712],{"class":391,"line":2374},[389,7713,7714],{"class":3108},"\u002F\u002F 뒤에서부터 거꾸로 뒤져 가장 최근에 발생한 login 객체를 즉시 반환\n",[389,7716,7717,7719,7722,7724,7727,7729,7732,7734,7737,7739,7742,7744,7747,7750,7752,7754,7756,7758],{"class":391,"line":2379},[389,7718,455],{"class":454},[389,7720,7721],{"class":407}," lastLogin ",[389,7723,461],{"class":399},[389,7725,7726],{"class":407}," logs",[389,7728,5532],{"class":399},[389,7730,7731],{"class":403},"findLast",[389,7733,408],{"class":407},[389,7735,7736],{"class":7336},"log",[389,7738,5509],{"class":454},[389,7740,7741],{"class":407}," log",[389,7743,5532],{"class":399},[389,7745,7746],{"class":407},"action ",[389,7748,7749],{"class":399},"===",[389,7751,4005],{"class":399},[389,7753,7621],{"class":936},[389,7755,2535],{"class":399},[389,7757,5738],{"class":407},[389,7759,7760],{"class":3108},"\u002F\u002F { action: 'login', date: '03일' }\n",[2616,7762,7764,7765],{"id":7763},"_4-술어-함수-기반의-정밀-인덱스-교체-findindex","🧪 4) 술어 함수 기반의 정밀 인덱스 교체 ",[253,7766,7767],{},".findIndex()",[213,7769,7770,7771,7774],{},"객체들로 채워진 복잡한 데이터 배열 내부에서 특정 속성 값 조건을 만족하는 원소의 정확한 방 번호(인덱스)를 추출할 때 쓰인다. 원본 데이터 가공 및 특정 요소 교체 작업 시 수동 ",[253,7772,7773],{},"for"," 루프 검사 코드를 완전히 대체한다.",[245,7776,7778],{"className":6305,"code":7777,"language":6308,"meta":178,"style":178},"\u002F\u002F 태그 리스트 중 삭제 대상 태그의 인덱스를 찾아 확보한 뒤 splice 연산으로 연계\nconst targetIndex = activeTags.value.findIndex(t => t === tag)\nif (targetIndex > -1) activeTags.value.splice(targetIndex, 1)\n",[253,7779,7780,7785,7820],{"__ignoreMap":178},[389,7781,7782],{"class":391,"line":392},[389,7783,7784],{"class":3108},"\u002F\u002F 태그 리스트 중 삭제 대상 태그의 인덱스를 찾아 확보한 뒤 splice 연산으로 연계\n",[389,7786,7787,7789,7792,7794,7796,7798,7800,7802,7805,7807,7810,7812,7815,7817],{"class":391,"line":186},[389,7788,455],{"class":454},[389,7790,7791],{"class":407}," targetIndex ",[389,7793,461],{"class":399},[389,7795,7498],{"class":407},[389,7797,5532],{"class":399},[389,7799,7503],{"class":407},[389,7801,5532],{"class":399},[389,7803,7804],{"class":403},"findIndex",[389,7806,408],{"class":407},[389,7808,7809],{"class":7336},"t",[389,7811,5509],{"class":454},[389,7813,7814],{"class":407}," t ",[389,7816,7749],{"class":399},[389,7818,7819],{"class":407}," tag)\n",[389,7821,7822,7825,7828,7831,7834,7837,7840,7842,7844,7846,7849,7852,7854,7857],{"class":391,"line":2333},[389,7823,7824],{"class":2526},"if",[389,7826,7827],{"class":407}," (targetIndex ",[389,7829,7830],{"class":399},">",[389,7832,7833],{"class":399}," -",[389,7835,7836],{"class":411},"1",[389,7838,7839],{"class":407},") activeTags",[389,7841,5532],{"class":399},[389,7843,7503],{"class":407},[389,7845,5532],{"class":399},[389,7847,7848],{"class":403},"splice",[389,7850,7851],{"class":407},"(targetIndex",[389,7853,5503],{"class":399},[389,7855,7856],{"class":411}," 1",[389,7858,415],{"class":407},[232,7860],{},[481,7862,7864],{"id":7863},"요약","📌 요약",[1649,7866,7867,7876,7885],{},[1652,7868,7869,7872,7873,7875],{},[1641,7870,7871],{},"일급 객체의 활용:"," 자바스크립트의 배열 메서드들은 함수를 데이터처럼 다룰 수 있는 일급 객체적 특성을 활용한 ",[1641,7874,7199],{}," 기반으로 설계되어 있다.",[1652,7877,7878,7881,7882,7884],{},[1641,7879,7880],{},"생략형 패턴의 본질:"," ",[253,7883,7222],{},"은 콜백 함수 아규먼트 구조의 일치성을 활용해 중간 익명 함수 레이어를 걷어낸 고차 함수 지향적 축약 표현이다.",[1652,7886,7887,7890,7891,7893,7894,7896,7897,7899],{},[1641,7888,7889],{},"적재적소의 선택:"," 무조건적인 전체 순회를 수행하는 ",[253,7892,7173],{}," 외에도, 단일 매칭 즉시 스코프를 빠져나오는 ",[253,7895,7176],{},"이나 역방향 고차 함수인 ",[253,7898,7731],{}," 등을 적절히 배치해야 정밀하고 낭비 없는 데이터 파이프라인을 구축할 수 있다.",[851,7901,7902],{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":178,"searchDepth":186,"depth":186,"links":7904},[7905,7906,7910,7920],{"id":7189,"depth":186,"text":7190},{"id":7219,"depth":186,"text":7907,"children":7908},"2. .filter(Boolean)의 구조적 작동 원리",[7909],{"id":7306,"depth":2333,"text":7307},{"id":7391,"depth":186,"text":7392,"children":7911},[7912,7914,7916,7918],{"id":7404,"depth":2333,"text":7913},"⚡ 1) 조기 종료로 연산을 최적화하는 .some() \u002F .every()",{"id":7514,"depth":2333,"text":7915},"🧩 2) 다차원 구조를 파괴하며 결합하는 .flatMap()",{"id":7577,"depth":2333,"text":7917},"🔍 3) 정밀한 후방 탐색을 지원하는 .findLast() \u002F .findLastIndex()",{"id":7763,"depth":2333,"text":7919},"🧪 4) 술어 함수 기반의 정밀 인덱스 교체 .findIndex()",{"id":7863,"depth":186,"text":7864},"map, filter, some 등 배열 고차 함수의 동작 원리와 filter(Boolean) 패턴을 활용한 데이터 가공 방법 정리",{"tags":7923,"image":7147},[7143,7924,7925,7926],"Functional-Programming","Array","High-Order-Function",{"icon":26},"\u002Fko\u002Fnote\u002Fjavascript\u002Ffilter",{"title":7930,"description":7931},"JavaScript 배열 고차 함수 map filter some 이해","JavaScript 배열 메서드인 map, filter, some의 동작 원리와 실무에서 자주 사용하는 데이터 처리 패턴을 설명합니다.","ko\u002F1.note\u002F1.javascript\u002F2.filter","0wYDNw9srdZXaqJ0uHKW-ZabY7vYZ0mtkRpHmaHFaWo",{"id":7935,"title":135,"body":7936,"date":7139,"description":8530,"extension":188,"links":863,"meta":8531,"navigation":8537,"path":8538,"seo":8539,"stem":8542,"__hash__":8543},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F3.custom-icon.md",{"type":180,"value":7937,"toc":8516},[7938,8513],[204,7939,7940,7944,7956,7971,7985,7991,8085,8107,8113,8115,8123,8135,8139,8150,8152,8156,8160,8163,8314,8318,8329,8340,8347,8357,8486,8488,8492],{":spotlight":206},[481,7941,7943],{"id":7942},"_1-내가-직접-만든-이미지를-메뉴-아이콘으로-넣고-싶다","🧐 1. 내가 직접 만든 이미지를 메뉴 아이콘으로 넣고 싶다",[2395,7945,7948,7953],{"className":7946},[4049,4372,4050,7947],"gap-10",[4052,7949],{"alt":7950,"src":7951,"width":7952},"brolog symbol","\u002Flogo\u002Fsymbol.png",100,[4052,7954],{"alt":7950,"src":7955,"width":7952},"\u002Flogo\u002Fsymbol-dark.png",[213,7957,7958,7959,7962,7963,7966,7967,7970],{},"현재 개발 중인 ",[253,7960,7961],{},"brolog"," 프로젝트는 ",[1641,7964,7965],{},"Nuxt 4 프레임워크와 Nuxt Content 모듈","을 기반으로 구축된 기술 블로그\u002F문서화 시스템이다. 이 환경에서는 사이드바 메뉴를 구성하기 위해 Nuxt Content가 제공하는 전용 네비게이션 설정 파일인 ",[253,7968,7969],{},"navigation.yml","을 활용하게 된다.",[213,7972,7973,7974,907,7977,7980,7981,7984],{},"메인 사이드바 메뉴 구조를 설계하던 중, 메뉴명 앞에 흔한 오픈소스 아이콘(",[253,7975,7976],{},"lucide:*",[253,7978,7979],{},"mdi:*",") 대신 ",[1641,7982,7983],{},"내가 직접 디자인하고 제작한 brolog만의 전용 브랜드 로고와 커스텀 그래픽 이미지","를 넣고 싶다는 니즈가 생겼다.",[213,7986,7987,7988,7990],{},"하지만 기본적으로 Nuxt Content의 ",[253,7989,7969],{}," 구조는 철저히 정형화된 외부 아이콘셋 명칭만 인식하는 스펙을 가지고 있다.",[245,7992,7994],{"className":3985,"code":7993,"filename":7969,"language":3988,"meta":178,"style":178},"# navigation.yml (기존 형태)\n- title: \"대시보드\"\n  path: \u002Fdashboard\n  icon: \"lucide:layout\" # 오픈소스 아이콘 이름만 지원됨\n- title: \"내가 만든 전용 로고 메뉴\"\n  path: \u002Fbrand\n  icon: \"\u002Fimages\u002Fmy-logo.png\" # ❌ 엑스박스 발생 및 컴포넌트 깨짐 현상\n",[253,7995,7996,8001,8018,8028,8045,8060,8069],{"__ignoreMap":178},[389,7997,7998],{"class":391,"line":392},[389,7999,8000],{"class":3108},"# navigation.yml (기존 형태)\n",[389,8002,8003,8006,8009,8011,8013,8016],{"class":391,"line":186},[389,8004,8005],{"class":399},"-",[389,8007,8008],{"class":2323}," title",[389,8010,400],{"class":399},[389,8012,3605],{"class":399},[389,8014,8015],{"class":936},"대시보드",[389,8017,3611],{"class":399},[389,8019,8020,8023,8025],{"class":391,"line":2333},[389,8021,8022],{"class":2323},"  path",[389,8024,400],{"class":399},[389,8026,8027],{"class":936}," \u002Fdashboard\n",[389,8029,8030,8033,8035,8037,8040,8042],{"class":391,"line":2344},[389,8031,8032],{"class":2323},"  icon",[389,8034,400],{"class":399},[389,8036,3605],{"class":399},[389,8038,8039],{"class":936},"lucide:layout",[389,8041,1239],{"class":399},[389,8043,8044],{"class":3108}," # 오픈소스 아이콘 이름만 지원됨\n",[389,8046,8047,8049,8051,8053,8055,8058],{"class":391,"line":2356},[389,8048,8005],{"class":399},[389,8050,8008],{"class":2323},[389,8052,400],{"class":399},[389,8054,3605],{"class":399},[389,8056,8057],{"class":936},"내가 만든 전용 로고 메뉴",[389,8059,3611],{"class":399},[389,8061,8062,8064,8066],{"class":391,"line":2364},[389,8063,8022],{"class":2323},[389,8065,400],{"class":399},[389,8067,8068],{"class":936}," \u002Fbrand\n",[389,8070,8071,8073,8075,8077,8080,8082],{"class":391,"line":2374},[389,8072,8032],{"class":2323},[389,8074,400],{"class":399},[389,8076,3605],{"class":399},[389,8078,8079],{"class":936},"\u002Fimages\u002Fmy-logo.png",[389,8081,1239],{"class":399},[389,8083,8084],{"class":3108}," # ❌ 엑스박스 발생 및 컴포넌트 깨짐 현상\n",[213,8086,8087,8088,8091,8092,8095,8096,8099,8100,907,8103,8106],{},"Nuxt Content 내부 스펙상 ",[253,8089,8090],{},"icon"," 필드는 무조건 ",[253,8093,8094],{},"@nuxt\u002Ficon"," 모듈의 ",[253,8097,8098],{},"\u003CIcon name=\"...\" \u002F>"," 컴포넌트와 긴밀하게 결합해 내부적으로 파싱된다. 이 때문에 저 자리에 일반 이미지 웹 경로(",[253,8101,8102],{},"png",[253,8104,8105],{},"jpg",")를 무작정 적으면 렌더링 규격이 맞지 않아 아예 화면에 출려되지 않는 한계가 존재했다.",[213,8108,8109,8110,8112],{},"내부 컴포넌트를 커스텀하여 이미지도 사용이 가능하도록 수정하려다, 관점을 조금 바꾸어 보았다.\n",[6390,8111],{}," \"기존 내장 컴포넌트 코드를 인위적으로 수정하여 이미지 태그 분기문으로 제어하는 대신, 내가 제작한 그래픽 리소스 자체를 Nuxt 아이콘 시스템이 다룰 수 있는 규격화된 아이콘 패키지로 빌드하여 매핑하는 방법이 없을까?\" 라는 궁금증이 떠올랐고, 그에 맞는 해법을 찾아냈다.",[232,8114],{},[481,8116,8118,8119,8122],{"id":8117},"️-2-해결-전략-내가-만든-이미지를-customcollections로-규격화","🛠️ 2. 해결 전략: 내가 만든 이미지를 ",[253,8120,8121],{},"customCollections","로 규격화",[213,8124,8125,8126,8129,8130,8134],{},"해답은 Nuxt 4의 핵심 아키텍처인 멀티 레이어 구조에서 풀렸다. 하위 레이어(",[253,8127,8128],{},"layer\u002Fnuxt.config.ts",")에 이미 마중물처럼 뚫려 있던 ",[1641,8131,8132],{},[253,8133,8121],{}," 인프라 설정을 발견하면서 명쾌하게 가닥이 잡혔다.",[2616,8136,8138],{"id":8137},"핵심-메커니즘","💡 핵심 메커니즘",[213,8140,8141,8142,8145,8146,8149],{},"내가 직접 만든 그래픽 이미지를 순수 ",[1641,8143,8144],{},"SVG 포맷","으로 저장한 뒤, 모듈이 지정한 로컬 아이콘 디렉토리에 정적 배치하는 방식이다. 이렇게 하면 Nuxt Icon 엔진이 빌드(또는 개발 서버 구동) 시점에 해당 폴더 안의 SVG 파일들을 스캔하여, 마치 Lucide나 Material Design Icons처럼 ",[1641,8147,8148],{},"우리가 직접 커스텀한 독립 아이콘셋","으로 빌드 컨텍스트에 포함시킨다.",[232,8151],{},[481,8153,8155],{"id":8154},"_3-실제-적용-및-해결-프로세스","📦 3. 실제 적용 및 해결 프로세스",[2616,8157,8159],{"id":8158},"_1단계-레이어-설정의-커스텀-디렉토리-경로-확인","① 1단계: 레이어 설정의 커스텀 디렉토리 경로 확인",[213,8161,8162],{},"Nuxt 4 구조에 맞게 세팅된 하위 레이어 설정 파일에 로컬 아이콘 소스 디렉토리가 다음과 같이 명시되어 있는 상태였다.",[245,8164,8166],{"className":5463,"code":8165,"filename":8128,"language":5466,"meta":178,"style":178},"\u002F\u002F layer\u002Fnuxt.config.ts\nexport default defineNuxtConfig({\n  icon: {\n    customCollections: [\n      {\n        prefix: 'custom', \u002F\u002F 👈 내가 만든 아이콘들을 호출할 고유 접두사(네임스페이스)\n        dir: resolve('.\u002Fapp\u002Fassets\u002Ficons'), \u002F\u002F 👈 내가 만든 SVG 파일들을 모아둘 보관소\n      },\n    ],\n    clientBundle: {\n      scan: true,\n      includeCustomCollections: true, \u002F\u002F 로컬 커스텀 번들에 포함 처리\n    }\n  },\n})\n",[253,8167,8168,8173,8185,8193,8202,8207,8226,8252,8257,8264,8273,8285,8299,8303,8308],{"__ignoreMap":178},[389,8169,8170],{"class":391,"line":392},[389,8171,8172],{"class":3108},"\u002F\u002F layer\u002Fnuxt.config.ts\n",[389,8174,8175,8177,8179,8181,8183],{"class":391,"line":186},[389,8176,1231],{"class":2526},[389,8178,2730],{"class":2526},[389,8180,2733],{"class":403},[389,8182,408],{"class":407},[389,8184,2341],{"class":399},[389,8186,8187,8189,8191],{"class":391,"line":2333},[389,8188,8032],{"class":2323},[389,8190,400],{"class":399},[389,8192,2751],{"class":399},[389,8194,8195,8198,8200],{"class":391,"line":2344},[389,8196,8197],{"class":2323},"    customCollections",[389,8199,400],{"class":399},[389,8201,6910],{"class":407},[389,8203,8204],{"class":391,"line":2356},[389,8205,8206],{"class":399},"      {\n",[389,8208,8209,8212,8214,8216,8219,8221,8223],{"class":391,"line":2364},[389,8210,8211],{"class":2323},"        prefix",[389,8213,400],{"class":399},[389,8215,4005],{"class":399},[389,8217,8218],{"class":936},"custom",[389,8220,2535],{"class":399},[389,8222,5503],{"class":399},[389,8224,8225],{"class":3108}," \u002F\u002F 👈 내가 만든 아이콘들을 호출할 고유 접두사(네임스페이스)\n",[389,8227,8228,8231,8233,8236,8238,8240,8243,8245,8247,8249],{"class":391,"line":2374},[389,8229,8230],{"class":2323},"        dir",[389,8232,400],{"class":399},[389,8234,8235],{"class":403}," resolve",[389,8237,408],{"class":407},[389,8239,2535],{"class":399},[389,8241,8242],{"class":936},".\u002Fapp\u002Fassets\u002Ficons",[389,8244,2535],{"class":399},[389,8246,5529],{"class":407},[389,8248,5503],{"class":399},[389,8250,8251],{"class":3108}," \u002F\u002F 👈 내가 만든 SVG 파일들을 모아둘 보관소\n",[389,8253,8254],{"class":391,"line":2379},[389,8255,8256],{"class":399},"      },\n",[389,8258,8259,8262],{"class":391,"line":2389},[389,8260,8261],{"class":407},"    ]",[389,8263,6450],{"class":399},[389,8265,8266,8269,8271],{"class":391,"line":2400},[389,8267,8268],{"class":2323},"    clientBundle",[389,8270,400],{"class":399},[389,8272,2751],{"class":399},[389,8274,8275,8278,8280,8283],{"class":391,"line":2406},[389,8276,8277],{"class":2323},"      scan",[389,8279,400],{"class":399},[389,8281,8282],{"class":2352}," true",[389,8284,6450],{"class":399},[389,8286,8287,8290,8292,8294,8296],{"class":391,"line":2416},[389,8288,8289],{"class":2323},"      includeCustomCollections",[389,8291,400],{"class":399},[389,8293,8282],{"class":2352},[389,8295,5503],{"class":399},[389,8297,8298],{"class":3108}," \u002F\u002F 로컬 커스텀 번들에 포함 처리\n",[389,8300,8301],{"class":391,"line":2594},[389,8302,2784],{"class":399},[389,8304,8305],{"class":391,"line":2599},[389,8306,8307],{"class":399},"  },\n",[389,8309,8310,8312],{"class":391,"line":3171},[389,8311,2359],{"class":399},[389,8313,415],{"class":407},[2616,8315,8317],{"id":8316},"_2단계-내가-만든-커스텀-svg-파일-배치","② 2단계: 내가 만든 커스텀 SVG 파일 배치",[213,8319,8320,8321,8324,8325,8328],{},"메뉴 아이콘으로 사용하고 싶은 나만의 로고나 심볼 이미지를 피그마(Figma)나 일러스트레이터에서 ",[1641,8322,8323],{},"순수 SVG 포맷","으로 추출했다. 그 후 레이어에 지정된 로컬 경로(",[253,8326,8327],{},"app\u002Fassets\u002Ficons\u002F",") 안에 원하는 아이콘 명칭으로 파일을 투하했다.",[1649,8330,8331],{},[1652,8332,8333,8334,907,8337],{},"파일 저장 예시: ",[253,8335,8336],{},"app\u002Fassets\u002Ficons\u002Fbrolog-logo.svg",[253,8338,8339],{},"app\u002Fassets\u002Ficons\u002Frocket-fire.svg",[2616,8341,8343,8344,8346],{"id":8342},"_3단계-navigationyml에서-내-아이콘-호출하기","③ 3단계: ",[253,8345,7969],{},"에서 내 아이콘 호출하기",[213,8348,8349,8350,8353,8354,8356],{},"이제 일반 외부 오픈소스 아이콘을 불러 쓰던 방식과 100% 동일하게 ",[253,8351,8352],{},"설정한접두사(prefix):내가만든파일명"," 구조로 ",[253,8355,7969],{},"에 선언해 주기만 하면 끝이다.",[245,8358,8360],{"className":3985,"code":8359,"language":3988,"meta":178,"style":178},"# navigation.yml (최종 최적화 형태)\n- title: \"브랜드 소개\"\n  path: \u002Fbrand\n  icon: \"custom:brolog-logo\" # 👈 내가 만든 brolog-logo.svg가 시스템 아이콘처럼 완벽 매핑!\n- title: \"출시 가이드\"\n  path: \u002Flaunch\n  icon: \"custom:rocket-fire\" # 👈 직접 디자인한 전용 론칭 그래픽 SVG 즉시 적용\n- title: \"일반 설정\"\n  path: \u002Fsettings\n  icon: \"lucide:settings\"   # 👈 기존 글로벌 오픈소스 아이콘과도 아무런 충돌 없이 혼용 가능\n",[253,8361,8362,8367,8382,8390,8406,8421,8430,8446,8461,8470],{"__ignoreMap":178},[389,8363,8364],{"class":391,"line":392},[389,8365,8366],{"class":3108},"# navigation.yml (최종 최적화 형태)\n",[389,8368,8369,8371,8373,8375,8377,8380],{"class":391,"line":186},[389,8370,8005],{"class":399},[389,8372,8008],{"class":2323},[389,8374,400],{"class":399},[389,8376,3605],{"class":399},[389,8378,8379],{"class":936},"브랜드 소개",[389,8381,3611],{"class":399},[389,8383,8384,8386,8388],{"class":391,"line":2333},[389,8385,8022],{"class":2323},[389,8387,400],{"class":399},[389,8389,8068],{"class":936},[389,8391,8392,8394,8396,8398,8401,8403],{"class":391,"line":2344},[389,8393,8032],{"class":2323},[389,8395,400],{"class":399},[389,8397,3605],{"class":399},[389,8399,8400],{"class":936},"custom:brolog-logo",[389,8402,1239],{"class":399},[389,8404,8405],{"class":3108}," # 👈 내가 만든 brolog-logo.svg가 시스템 아이콘처럼 완벽 매핑!\n",[389,8407,8408,8410,8412,8414,8416,8419],{"class":391,"line":2356},[389,8409,8005],{"class":399},[389,8411,8008],{"class":2323},[389,8413,400],{"class":399},[389,8415,3605],{"class":399},[389,8417,8418],{"class":936},"출시 가이드",[389,8420,3611],{"class":399},[389,8422,8423,8425,8427],{"class":391,"line":2364},[389,8424,8022],{"class":2323},[389,8426,400],{"class":399},[389,8428,8429],{"class":936}," \u002Flaunch\n",[389,8431,8432,8434,8436,8438,8441,8443],{"class":391,"line":2374},[389,8433,8032],{"class":2323},[389,8435,400],{"class":399},[389,8437,3605],{"class":399},[389,8439,8440],{"class":936},"custom:rocket-fire",[389,8442,1239],{"class":399},[389,8444,8445],{"class":3108}," # 👈 직접 디자인한 전용 론칭 그래픽 SVG 즉시 적용\n",[389,8447,8448,8450,8452,8454,8456,8459],{"class":391,"line":2379},[389,8449,8005],{"class":399},[389,8451,8008],{"class":2323},[389,8453,400],{"class":399},[389,8455,3605],{"class":399},[389,8457,8458],{"class":936},"일반 설정",[389,8460,3611],{"class":399},[389,8462,8463,8465,8467],{"class":391,"line":2389},[389,8464,8022],{"class":2323},[389,8466,400],{"class":399},[389,8468,8469],{"class":936}," \u002Fsettings\n",[389,8471,8472,8474,8476,8478,8481,8483],{"class":391,"line":2400},[389,8473,8032],{"class":2323},[389,8475,400],{"class":399},[389,8477,3605],{"class":399},[389,8479,8480],{"class":936},"lucide:settings",[389,8482,1239],{"class":399},[389,8484,8485],{"class":3108},"   # 👈 기존 글로벌 오픈소스 아이콘과도 아무런 충돌 없이 혼용 가능\n",[232,8487],{},[481,8489,8491],{"id":8490},"_4-경험을-통해-얻은-인사이트-및-결론","🧠 4. 경험을 통해 얻은 인사이트 및 결론",[1649,8493,8494,8500,8510],{},[1652,8495,8496,8499],{},[1641,8497,8498],{},"Nuxt Content 컴포넌트 무결성 유지:"," 네비게이션 링크를 렌더링하는 Nuxt Content 모듈 내부의 복잡한 UI 소스 코드를 단 한 줄도 수정하거나 커스텀 오버라이딩하지 않고, 순수하게 설정과 정적 파일 배치만으로 원하는 커스텀 비주얼을 완벽히 녹여냈다.",[1652,8501,8502,8505,8506,8509],{},[1641,8503,8504],{},"디자인 시스템과의 융합:"," 일반 PNG\u002FJPG 이미지를 썼을 때 발생하는 해상도 깨짐이나 크기 조절의 번거로움이 사라졌다. 내가 만든 이미지가 정식 벡터(SVG) 아이콘으로 취급되면서 기존 UI의 다크모드 대응이나 폰트 크기(",[253,8507,8508],{},"w-4 h-4"," 등) 규칙에 유연하게 동기화되는 높은 완성도를 보여주었다.",[1652,8511,8512],{},"Nuxt 4 멀티 레이어 기반의 프로젝트에서 전용 에셋을 서비스 전체에 가볍고 효율적으로 전파하고자 할 때, 일반 이미지를 아이콘 컬렉션으로 승격시키는 이 방식은 결합도를 낮추는 가장 영리한 해법임을 실무적으로 경험했다.",[851,8514,8515],{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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":178,"searchDepth":186,"depth":186,"links":8517},[8518,8519,8523,8529],{"id":7942,"depth":186,"text":7943},{"id":8117,"depth":186,"text":8520,"children":8521},"🛠️ 2. 해결 전략: 내가 만든 이미지를 customCollections로 규격화",[8522],{"id":8137,"depth":2333,"text":8138},{"id":8154,"depth":186,"text":8155,"children":8524},[8525,8526,8527],{"id":8158,"depth":2333,"text":8159},{"id":8316,"depth":2333,"text":8317},{"id":8342,"depth":2333,"text":8528},"③ 3단계: navigation.yml에서 내 아이콘 호출하기",{"id":8490,"depth":186,"text":8491},"Nuxt Content v3의 navigation.yml 제한을 극복하고, customCollections를 활용해 직접 제작한 커스텀 SVG 이미지를 메뉴 아이콘으로 바인딩한 개발 경험 공유",{"tags":8532,"image":8536},[43,8533,8534,8535,5935],"Nuxt-Content","Nuxt-Icon","Custom","\u002Fimages\u002Fbro-thumbnail.png",{"icon":26},"\u002Fko\u002Fproject\u002Fbrolog\u002Fcustom-icon",{"title":8540,"description":8541},"Nuxt Content 커스텀 아이콘 적용 방법","Nuxt Icon customCollections를 활용해 직접 제작한 SVG 아이콘을 navigation 메뉴에 적용하는 과정","ko\u002F2.project\u002F1.brolog\u002F3.custom-icon","XZmcHdQ8GMbcFAZHHvzDEpCzZ4Gzp9yMBrlwpPOj52c",{"id":8545,"title":139,"body":8546,"date":7139,"description":8780,"extension":188,"links":863,"meta":8781,"navigation":8784,"path":8785,"seo":8786,"stem":8789,"__hash__":8790},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F4.nuxt-auto-import.md",{"type":180,"value":8547,"toc":8766},[8548],[204,8549,8550,8554,8560,8571,8586,8592,8594,8598,8604,8611,8617,8630,8634,8643,8646,8648,8652,8655,8659,8682,8686,8696,8714,8718,8724,8726,8742,8744],{":spotlight":206},[481,8551,8553],{"id":8552},"_1-명시적-import의-생략-편리함-뒤에-숨은-궁금증","🧐 1. 명시적 import의 생략, 편리함 뒤에 숨은 궁금증",[213,8555,8556,8557,8559],{},"현재 ",[253,8558,7961],{}," 프로젝트를 Nuxt 4 기반으로 개발하면서 한 가지 독특한 개발 경험을 하게 되었다.",[213,8561,8562,8563,8566,8567,8570],{},"순수 Vue 3 컴포지션 API 환경에서 컴포넌트를 작성할 때는 파일 상단에 ",[253,8564,8565],{},"import { ref, onMounted } from 'vue'","와 같은 명시적 임포트 구문을 선언하는 것이 필수적이다. 또한 내가 직접 만든 커스텀 컴포넌트나 유틸리티 함수들을 가져다 쓸 때도 상대 경로를 일일이 계산해가며 ",[253,8568,8569],{},"import"," 코드를 채워 넣어야 했다.",[213,8572,8573,8574,8576,8577,5963,8579,8582,8583,8585],{},"하지만 Nuxt 4 환경에서는 이러한 반복적인 ",[253,8575,8569],{}," 선언을 단 한 줄도 적지 않았음에도 불구하고, ",[253,8578,6048],{},[253,8580,8581],{},"onMounted"," 같은 내장 API는 물론, ",[253,8584,5456],{}," 같은 프레임워크 컴포저블과 내가 따로 만든 독립 컴포넌트들까지 에러 없이 화면에 정상적으로 렌더링되었다.",[213,8587,8588,8589,8591],{},"VS Code 같은 에디터에서도 빨간 줄(타입 에러)을 띄우지 않고 깔끔하게 자동완성을 지원했다.\n",[6390,8590],{},"\"우연히 경로가 맞아서 우회 적용되는 것인가, 아니면 어딘가에 전역 객체로 등록되어 있는 것인가?\" 라는 의문이 생겼고, 이 편리한 기능의 밑바탕에 깔린 **Nuxt 4 Auto-imports(자동 임포트)**의 동작 원리를 찾아보게 되었다.",[232,8593],{},[481,8595,8597],{"id":8596},"️-2-핵심-메커니즘-빌드-타임의-스캔과-가상-타입-맵-생성","🛠️ 2. 핵심 메커니즘: 빌드 타임의 스캔과 가상 타입 맵 생성",[213,8599,8600,8601,8603],{},"이 메커니즘은 브라우저의 글로벌 ",[253,8602,6040],{}," 객체에 변수들을 무식하게 얹어두는 방식이 아니다. 그랬다면 사이드 이펙트가 발생하거나 타입스크립트 시스템이 붕괴했을 것이다.",[213,8605,8606,8607,8610],{},"Nuxt 4는 개발자가 코드를 작성하는 시점과 최종 빌드(또는 개발 서버 구동) 시점을 분리하여 백그라운드에서 ",[1641,8608,8609],{},"가상 링킹(Virtual Linking)"," 작업을 수행한다.",[2616,8612,8614,8615,5529],{"id":8613},"nuxt-디렉토리-내부의-가상-타입-선언-dts","① .nuxt 디렉토리 내부의 가상 타입 선언 (",[253,8616,6231],{},[213,8618,6010,8619,8621,8622,8624,8625,6022,8627,8629],{},[253,8620,6013],{},")를 켜거나 ",[253,8623,6017],{},"를 실행하면, Nuxt 4 엔진은 프로젝트 내부와 하위 레이어의 구조를 전체적으로 스캔한다.\n그 후 ",[253,8626,6021],{},[253,8628,6025],{}," 같은 가상 타입 정의 파일을 실시간으로 생성한다. 이 지도 덕분에 에디터는 명시적인 import 코드가 없어도 \"이 함수와 컴포넌트가 어느 패키지의 어느 경로에서 출발한 녀석인지\" 정확히 인지하게 된다.",[2616,8631,8633],{"id":8632},"컴파일-타임의-동적-주입-및-트리-쉐이킹tree-shaking","② 컴파일 타임의 동적 주입 및 트리 쉐이킹(Tree-shaking)",[213,8635,8636,8637,8639,8640,4902],{},"실제 코드가 빌드되어 프로덕션 레벨로 나갈 때는 Nuxt 가상 컴파일러가 바닐라 자바스크립트로 코드를 변환하면서 생략되었던 ",[253,8638,8569],{}," 구문들을 ",[1641,8641,8642],{},"원래 있어야 할 자리에 알아서 매핑하여 주입",[213,8644,8645],{},"이를 통해 개발자는 매번 수동으로 임포트하는 피로감에서 벗어나고, 런타임 환경에서는 사용하지 않는 자원을 걷어내는 트리 쉐이킹과 코드 스플리팅 효율을 그대로 챙길 수 있게 된다.",[232,8647],{},[481,8649,8651],{"id":8650},"_3-자동-인지-시스템의-세-가지-대상-범위","📦 3. 자동 인지 시스템의 세 가지 대상 범위",[213,8653,8654],{},"Nuxt 4 엔진이 자동으로 인식해서 링킹 인프라에 태워주는 대상은 크게 세 가지 영역으로 나뉜다.",[2616,8656,8658],{"id":8657},"vue-3-및-nuxt-core-api","① Vue 3 및 Nuxt Core API",[213,8660,8661,8662,907,8664,907,8666,8669,8670,907,8672,8675,8676,907,8678,8681],{},"가장 기본적으로 접하는 반응형 API(",[253,8663,6048],{},[253,8665,6111],{},[253,8667,8668],{},"watch",")와 생명주기 훅(",[253,8671,8581],{},[253,8673,8674],{},"nextTick","), 그리고 Nuxt 전용 핵심 컴포저블(",[253,8677,5456],{},[253,8679,8680],{},"useRoute",") 등이 여기에 해당한다.",[2616,8683,8685],{"id":8684},"components-폴더-안의-파일-규칙","② components\u002F 폴더 안의 파일 규칙",[213,8687,8688,8689,8691,8692,8695],{},"따로 만든 커스텀 컴포넌트가 별도 임포트 없이 작동하는 이유다. Nuxt 4는 ",[253,8690,5966],{}," 디렉토리(혹은 레이어의 컴포넌트 경로) 내부에 ",[253,8693,8694],{},".vue"," 파일을 생성하면 구조와 파일명을 조합해 태그로 자동 등록한다.",[1649,8697,8698,8706],{},[1652,8699,8700,6136,8703,8705],{},[253,8701,8702],{},"components\u002FCustomButton.vue",[253,8704,6051],{},"으로 즉시 사용",[1652,8707,8708,8710,8711,8713],{},[253,8709,6135],{}," ➡️ 폴더명이 접두사로 결합하여 ",[253,8712,6139],{},"로 즉시 사용",[2616,8715,8717],{"id":8716},"확장-모듈module의-에셋-등록","③ 확장 모듈(Module)의 에셋 등록",[213,8719,8720,8721,8723],{},"Nuxt Content v3의 ",[253,8722,5596],{}," 같은 신형 데이터 API 역시, 모듈 내부에서 \"이 함수를 개발자가 명시적 임포트 없이 사용할 수 있도록 Nuxt의 오토 임포트 명단에 등록해달라\"고 인프라에 사전 선언해 두었기 때문에 결합하여 사용할 수 있는 것이다.",[232,8725],{},[8727,8728,8729,8732,8734,8735,8737,8738,8741],"tip",{},[1641,8730,8731],{},"더 알아보기",[6390,8733],{},"\nNuxt 4 컴파일러가 추상 구문 트리(AST)를 분석하고, 가상 타입 정의(",[253,8736,6231],{},")를 생성하여 트리 쉐이킹을 유지하는 구체적인 내부 아키텍처와 상세 개념은 ",[2143,8739,61],{"href":8740},"\u002Fnote\u002Fnuxt4\u002Fnuxt-auto-import"," 문서에서 더욱 자세히 확인할 수 있습니다.",[481,8743,8491],{"id":8490},[1649,8745,8746,8752,8763],{},[1652,8747,8748,8751],{},[1641,8749,8750],{},"생산성 극대화:"," 파일마다 수십 줄씩 차지하던 의미 없는 import 상단 영역이 제거되면서, 순수 비즈니스 로직과 화면 설계에만 몰입할 수 있는 쾌적한 DX(개발자 경험)를 확보했다.",[1652,8753,8754,8757,8758,907,8760,8762],{},[1641,8755,8756],{},"디렉토리 규칙의 중요성:"," 이 모든 마법 같은 편리함은 프레임워크가 지정한 디렉토리 명명 규칙(",[253,8759,5966],{},[253,8761,5962],{},")을 정확히 준수했을 때만 보장된다. 규칙을 벗어난 커스텀 경로 파일들은 오토 임포트 대상에서 누락되므로 구조적 약속을 잘 따르는 것이 핵심이다.",[1652,8764,8765],{},"단순한 편의 기능인 줄 알았던 명시적 임포트 생략 뒤에는, 타입 안정성을 유지하면서 빌드 효율을 정밀하게 제어하는 Nuxt 4 전용 컴파일러 엔진의 영리한 설계가 숨어있음을 이론적·실무적으로 깊이 이해하게 되었다.",{"title":178,"searchDepth":186,"depth":186,"links":8767},[8768,8769,8774,8779],{"id":8552,"depth":186,"text":8553},{"id":8596,"depth":186,"text":8597,"children":8770},[8771,8773],{"id":8613,"depth":2333,"text":8772},"① .nuxt 디렉토리 내부의 가상 타입 선언 (.d.ts)",{"id":8632,"depth":2333,"text":8633},{"id":8650,"depth":186,"text":8651,"children":8775},[8776,8777,8778],{"id":8657,"depth":2333,"text":8658},{"id":8684,"depth":2333,"text":8685},{"id":8716,"depth":2333,"text":8717},{"id":8490,"depth":186,"text":8491},"Nuxt 4에서 Vue API, Composable, Components를 별도의 import 없이 사용할 수 있는 Auto-imports 시스템의 동작 방식과 빌드 과정 이해",{"tags":8782,"image":8536},[43,6253,8783,5935],"Auto-Imports",{"icon":26},"\u002Fko\u002Fproject\u002Fbrolog\u002Fnuxt-auto-import",{"title":8787,"description":8788},"Nuxt 4 Auto-imports 동작 원리 | 자동 import 시스템 분석","Nuxt 4 Auto-imports가 Vue API와 Composable, Components를 자동으로 인식하고 처리하는 방식과 내부 동작 원리를 정리합니다.","ko\u002F2.project\u002F1.brolog\u002F4.nuxt-auto-import","ExhkmigFMFpeaY5j6J5NY0LOwSTa32_oWk6xMlnvrUs",{"id":8792,"title":173,"body":8793,"date":9342,"description":9343,"extension":188,"links":863,"meta":9344,"navigation":9353,"path":9354,"seo":9355,"stem":9358,"__hash__":9359},"docs_ko\u002Fko\u002F3.trouble-shooting\u002F1.brolog\u002F1.nuxt-icon-restricted-network-troubleshooting.md",{"type":180,"value":8794,"toc":9328},[8795,9325],[204,8796,8797,8801,8808,8812,8839,8843,8852,8870,8872,8876,8893,8897,8907,8952,8960,8966,8973,9135,9142,9145,9250,9252,9256],{":spotlight":206},[481,8798,8800],{"id":8799},"_1-현상-및-근본-원인-분석","🔍 1. 현상 및 근본 원인 분석",[213,8802,8803,8804,8807],{},"네트워크 아웃바운드 통신이 제한되거나 **SSL 복호화 장비(자체 서명 인증서 주입)**가 개입하는 특수 네트워크 환경에서, Nuxt 개발 서버 구동 중 터미널 콘솔에 대량의 아이콘 페치 실패 경고(",[253,8805,8806],{},"failed to load icon",")가 지속적으로 발생하는 현상이 나타난다.",[2616,8809,8811],{"id":8810},"에러-로그-형태","🚨 에러 로그 형태",[245,8813,8815],{"className":923,"code":8814,"filename":925,"language":926,"meta":178,"style":178},"WARN  [Icon] failed to load icon lucide:search\nWARN  [Icon] failed to load icon simple-icons:nuxt\nWARN  [Icon] failed to load icon fluent-emoji-flat:light-bulb\n",[253,8816,8817,8825,8832],{"__ignoreMap":178},[389,8818,8819,8822],{"class":391,"line":392},[389,8820,8821],{"class":395},"WARN",[389,8823,8824],{"class":407},"  [Icon] failed to load icon lucide:search\n",[389,8826,8827,8829],{"class":391,"line":186},[389,8828,8821],{"class":395},[389,8830,8831],{"class":407},"  [Icon] failed to load icon simple-icons:nuxt\n",[389,8833,8834,8836],{"class":391,"line":2333},[389,8835,8821],{"class":395},[389,8837,8838],{"class":407},"  [Icon] failed to load icon fluent-emoji-flat:light-bulb\n",[2616,8840,8842],{"id":8841},"발생-메커니즘","💡 발생 메커니즘",[213,8844,8845,8847,8848,8851],{},[253,8846,8094],{}," 모듈은 기본적으로 온디맨드(On-Demand) 방식의 원격 조회를 시도한다. 화면 렌더링 시점에 외부 오픈소스 아이콘 API 서버(",[253,8849,8850],{},"https:\u002F\u002Fapi.iconify.design",")로 실시간 요청을 날리는데, 이때 런타임 환경에 따라 이중적인 결과가 발생한다.",[1649,8853,8854,8860],{},[1652,8855,8856,8859],{},[1641,8857,8858],{},"브라우저(Client) 단:"," 로컬 PC 브라우저 검증망을 통과하여 인터넷 릴레이 통신이 성공하므로 화면에는 아이콘이 정상 출력된다.",[1652,8861,8862,8865,8866,8869],{},[1641,8863,8864],{},"Node.js 서버(SSR) 단:"," Nuxt 내부 서버 엔진(Nitro\u002FVite) 레이어에서는 보안 장비가 주입한 자체 서명 인증서 체인을 신뢰할 수 없거나 아웃바운드가 막혀 통신을 거부(",[253,8867,8868],{},"fetch failed",")한다. 결과적으로 화면에는 뜨지만 서버 터미널에는 에러 폭탄이 찍히는 괴리가 발생한다.",[232,8871],{},[481,8873,8875],{"id":8874},"️-2-아키텍처적-해결-방안-full-localizing-layer-merge","🛠️ 2. 아키텍처적 해결 방안 (Full Localizing & Layer Merge)",[213,8877,8878,8879,8882,8883,8885,8886,8892],{},"보안 정책을 우회하기 위해 프로젝트 구동 방식을 ",[1641,8880,8881],{},"외부 의존성이 전무한 완전한 오프라인 로컬(Standalone) 모드","로 전환한다. 특히 ",[253,8884,7961],{}," 프로젝트와 같이 ",[1641,8887,8888,8889,8891],{},"Nuxt 멀티 레이어(",[253,8890,6167],{},") 아키텍처","를 사용하는 경우, 설정 파일 간의 병합(Merge) 특성을 고려하여 명확한 타겟팅 설정을 적용해야 한다.",[2616,8894,8896],{"id":8895},"_1단계-메인-프로젝트-루트에-오프라인-패키지-설치","① 1단계: 메인 프로젝트 루트에 오프라인 패키지 설치",[213,8898,8899,8900,8903,8904,8906],{},"원격 API 서버를 찌르지 않도록 현재 감지된 모든 아이콘 컬렉션을 개발 의존성(",[253,8901,8902],{},"devDependencies",")으로 로컬에 다운로드한다. 명령어는 반드시 ",[253,8905,7961],{}," 프로젝트의 메인 루트 디렉토리에서 실행해야 한다.",[245,8908,8910],{"className":923,"code":8909,"filename":925,"language":926,"meta":178,"style":178},"npm i -D @iconify-json\u002Flucide @iconify-json\u002Fsimple-icons @iconify-json\u002Ffluent-emoji-flat @iconify-json\u002Flogos @iconify-json\u002Fmdi @iconify-json\u002Fskill-icons @iconify-json\u002Funjs @iconify-json\u002Fstreamline-plump @iconify-json\u002Ficon-park-solid @iconify-json\u002Femojione\n",[253,8911,8912],{"__ignoreMap":178},[389,8913,8914,8916,8919,8922,8925,8928,8931,8934,8937,8940,8943,8946,8949],{"class":391,"line":392},[389,8915,3501],{"class":395},[389,8917,8918],{"class":936}," i",[389,8920,8921],{"class":936}," -D",[389,8923,8924],{"class":936}," @iconify-json\u002Flucide",[389,8926,8927],{"class":936}," @iconify-json\u002Fsimple-icons",[389,8929,8930],{"class":936}," @iconify-json\u002Ffluent-emoji-flat",[389,8932,8933],{"class":936}," @iconify-json\u002Flogos",[389,8935,8936],{"class":936}," @iconify-json\u002Fmdi",[389,8938,8939],{"class":936}," @iconify-json\u002Fskill-icons",[389,8941,8942],{"class":936}," @iconify-json\u002Funjs",[389,8944,8945],{"class":936}," @iconify-json\u002Fstreamline-plump",[389,8947,8948],{"class":936}," @iconify-json\u002Ficon-park-solid",[389,8950,8951],{"class":936}," @iconify-json\u002Femojione\n",[2125,8953,8954],{},[213,8955,8956,8959],{},[1641,8957,8958],{},"📦 리소스 오버헤드 검증:"," 아이콘 팩을 로컬에 받아도 실제 용량은 수 MB 수준으로 미비하다. 빌드 시점에는 **트리쉐이킹(Tree-shaking)**이 엄격하게 작동하여 코드 상에서 실제 호출한 아이콘(SVG 데이터)만 최종 프로덕션 번들에 인라인 주입되므로 전체 런타임 성능과 빌드 용량 최적화에 훨씬 유리하다.",[2616,8961,8963,8964,5529],{"id":8962},"_2단계-하위-레이어-설정-조정-layernuxtconfigts","② 2단계: 하위 레이어 설정 조정 (",[253,8965,8128],{},[213,8967,8968,8969,8972],{},"하위 레이어 설정 내부에 원격 조회를 강제하는 ",[253,8970,8971],{},"provider: 'iconify'"," 속성이 남아있으면 메인 설정과 충돌을 일으킨다. 이를 내부 엔진 우선 방식으로 변경하고 로컬 번들 가이드를 주입한다.",[245,8974,8976],{"className":5463,"code":8975,"filename":8128,"language":5466,"meta":178,"style":178},"\u002F\u002F layer\u002Fnuxt.config.ts\nexport default defineNuxtConfig({\n  icon: {\n    customCollections: [\n      {\n        prefix: 'custom',\n        dir: resolve('.\u002Fapp\u002Fassets\u002Ficons'),\n      },\n    ],\n    clientBundle: {\n      scan: true,\n      includeCustomCollections: true,\n    },\n    \u002F\u002F ⚠️ 원격 API를 강제 조회하는 기존 'iconify'를 주석처리하여 로컬 우선 유도\n    \u002F\u002Fprovider: 'iconify', \n    serverBundle: 'local',\n    fallbackToApi: false\n  },\n})\n",[253,8977,8978,8982,8994,9002,9010,9014,9028,9048,9052,9058,9066,9076,9086,9090,9095,9100,9116,9125,9129],{"__ignoreMap":178},[389,8979,8980],{"class":391,"line":392},[389,8981,8172],{"class":3108},[389,8983,8984,8986,8988,8990,8992],{"class":391,"line":186},[389,8985,1231],{"class":2526},[389,8987,2730],{"class":2526},[389,8989,2733],{"class":403},[389,8991,408],{"class":407},[389,8993,2341],{"class":399},[389,8995,8996,8998,9000],{"class":391,"line":2333},[389,8997,8032],{"class":2323},[389,8999,400],{"class":399},[389,9001,2751],{"class":399},[389,9003,9004,9006,9008],{"class":391,"line":2344},[389,9005,8197],{"class":2323},[389,9007,400],{"class":399},[389,9009,6910],{"class":407},[389,9011,9012],{"class":391,"line":2356},[389,9013,8206],{"class":399},[389,9015,9016,9018,9020,9022,9024,9026],{"class":391,"line":2364},[389,9017,8211],{"class":2323},[389,9019,400],{"class":399},[389,9021,4005],{"class":399},[389,9023,8218],{"class":936},[389,9025,2535],{"class":399},[389,9027,6450],{"class":399},[389,9029,9030,9032,9034,9036,9038,9040,9042,9044,9046],{"class":391,"line":2374},[389,9031,8230],{"class":2323},[389,9033,400],{"class":399},[389,9035,8235],{"class":403},[389,9037,408],{"class":407},[389,9039,2535],{"class":399},[389,9041,8242],{"class":936},[389,9043,2535],{"class":399},[389,9045,5529],{"class":407},[389,9047,6450],{"class":399},[389,9049,9050],{"class":391,"line":2379},[389,9051,8256],{"class":399},[389,9053,9054,9056],{"class":391,"line":2389},[389,9055,8261],{"class":407},[389,9057,6450],{"class":399},[389,9059,9060,9062,9064],{"class":391,"line":2400},[389,9061,8268],{"class":2323},[389,9063,400],{"class":399},[389,9065,2751],{"class":399},[389,9067,9068,9070,9072,9074],{"class":391,"line":2406},[389,9069,8277],{"class":2323},[389,9071,400],{"class":399},[389,9073,8282],{"class":2352},[389,9075,6450],{"class":399},[389,9077,9078,9080,9082,9084],{"class":391,"line":2416},[389,9079,8289],{"class":2323},[389,9081,400],{"class":399},[389,9083,8282],{"class":2352},[389,9085,6450],{"class":399},[389,9087,9088],{"class":391,"line":2594},[389,9089,3134],{"class":399},[389,9091,9092],{"class":391,"line":2599},[389,9093,9094],{"class":3108},"    \u002F\u002F ⚠️ 원격 API를 강제 조회하는 기존 'iconify'를 주석처리하여 로컬 우선 유도\n",[389,9096,9097],{"class":391,"line":3171},[389,9098,9099],{"class":3108},"    \u002F\u002Fprovider: 'iconify', \n",[389,9101,9102,9105,9107,9109,9112,9114],{"class":391,"line":3176},[389,9103,9104],{"class":2323},"    serverBundle",[389,9106,400],{"class":399},[389,9108,4005],{"class":399},[389,9110,9111],{"class":936},"local",[389,9113,2535],{"class":399},[389,9115,6450],{"class":399},[389,9117,9118,9121,9123],{"class":391,"line":3182},[389,9119,9120],{"class":2323},"    fallbackToApi",[389,9122,400],{"class":399},[389,9124,2353],{"class":2352},[389,9126,9127],{"class":391,"line":3196},[389,9128,8307],{"class":399},[389,9130,9131,9133],{"class":391,"line":3206},[389,9132,2359],{"class":399},[389,9134,415],{"class":407},[2616,9136,9138,9139,5529],{"id":9137},"_3단계-메인-서비스-설정-덮어쓰기-docsnuxtconfigts","③ 3단계: 메인 서비스 설정 덮어쓰기 (",[253,9140,9141],{},"docs\u002Fnuxt.config.ts",[213,9143,9144],{},"상속받은 하위 레이어 설정을 메인 컨텍스트에서 최종 확정 지으며 외부 통신 폴백을 완벽히 차단한다.",[245,9146,9148],{"className":5463,"code":9147,"filename":9141,"language":5466,"meta":178,"style":178},"\u002F\u002F docs\u002Fnuxt.config.ts\nexport default defineNuxtConfig({\n  extends: [\n    '..\u002Flayer' \u002F\u002F 하위 레이어 상속 경로\n  ],\n\n  icon: {\n    \u002F\u002F 멀티 레이어 병합 시에도 외부 통신을 차단하도록 고정 쐐기 선언\n    serverBundle: 'local', \u002F\u002F 1단계에서 설치한 node_modules 리소스 강제 매핑\n    fallbackToApi: false   \u002F\u002F 어떠한 상황에서도 외부 원격 API 서버 조회를 금지\n  }\n})\n",[253,9149,9150,9155,9167,9176,9187,9194,9198,9206,9211,9228,9240,9244],{"__ignoreMap":178},[389,9151,9152],{"class":391,"line":392},[389,9153,9154],{"class":3108},"\u002F\u002F docs\u002Fnuxt.config.ts\n",[389,9156,9157,9159,9161,9163,9165],{"class":391,"line":186},[389,9158,1231],{"class":2526},[389,9160,2730],{"class":2526},[389,9162,2733],{"class":403},[389,9164,408],{"class":407},[389,9166,2341],{"class":399},[389,9168,9169,9172,9174],{"class":391,"line":2333},[389,9170,9171],{"class":2323},"  extends",[389,9173,400],{"class":399},[389,9175,6910],{"class":407},[389,9177,9178,9180,9182,9184],{"class":391,"line":2344},[389,9179,2760],{"class":399},[389,9181,3629],{"class":936},[389,9183,2535],{"class":399},[389,9185,9186],{"class":3108}," \u002F\u002F 하위 레이어 상속 경로\n",[389,9188,9189,9192],{"class":391,"line":2356},[389,9190,9191],{"class":407},"  ]",[389,9193,6450],{"class":399},[389,9195,9196],{"class":391,"line":2364},[389,9197,2315],{"emptyLinePlaceholder":190},[389,9199,9200,9202,9204],{"class":391,"line":2374},[389,9201,8032],{"class":2323},[389,9203,400],{"class":399},[389,9205,2751],{"class":399},[389,9207,9208],{"class":391,"line":2379},[389,9209,9210],{"class":3108},"    \u002F\u002F 멀티 레이어 병합 시에도 외부 통신을 차단하도록 고정 쐐기 선언\n",[389,9212,9213,9215,9217,9219,9221,9223,9225],{"class":391,"line":2389},[389,9214,9104],{"class":2323},[389,9216,400],{"class":399},[389,9218,4005],{"class":399},[389,9220,9111],{"class":936},[389,9222,2535],{"class":399},[389,9224,5503],{"class":399},[389,9226,9227],{"class":3108}," \u002F\u002F 1단계에서 설치한 node_modules 리소스 강제 매핑\n",[389,9229,9230,9232,9234,9237],{"class":391,"line":2400},[389,9231,9120],{"class":2323},[389,9233,400],{"class":399},[389,9235,9236],{"class":2352}," false",[389,9238,9239],{"class":3108},"   \u002F\u002F 어떠한 상황에서도 외부 원격 API 서버 조회를 금지\n",[389,9241,9242],{"class":391,"line":2406},[389,9243,2793],{"class":399},[389,9245,9246,9248],{"class":391,"line":2416},[389,9247,2359],{"class":399},[389,9249,415],{"class":407},[232,9251],{},[481,9253,9255],{"id":9254},"_3-최적화-적용-결과-비교","📊 3. 최적화 적용 결과 비교",[588,9257,9258,9271],{},[591,9259,9260],{},[594,9261,9262,9265,9268],{},[597,9263,9264],{"align":4836},"비교 지표",[597,9266,9267],{"align":4836},"적용 전 상태",[597,9269,9270],{"align":4836},"최적화 후 상태 (Standalone)",[604,9272,9273,9286,9299,9312],{},[594,9274,9275,9280,9283],{},[609,9276,9277],{"align":4836},[1641,9278,9279],{},"외부 네트워크 의존도",[609,9281,9282],{"align":4836},"원격 CDN 및 API 가동 상태에 종속",[609,9284,9285],{"align":4836},"100% 로컬 인프라 독립 가동",[594,9287,9288,9293,9296],{},[609,9289,9290],{"align":4836},[1641,9291,9292],{},"터미널 콘솔 가독성",[609,9294,9295],{"align":4836},"무수한 네트워크 경고 및 에러 폭탄 발생",[609,9297,9298],{"align":4836},"불필요한 로그 오버헤드 제로 (Clean)",[594,9300,9301,9306,9309],{},[609,9302,9303],{"align":4836},[1641,9304,9305],{},"멀티 레이어 호환성",[609,9307,9308],{"align":4836},"상속 설정 꼬임으로 인한 지속적인 폴백 현상",[609,9310,9311],{"align":4836},"계층형 프로바이더 통일로 깔끔한 병합 완성",[594,9313,9314,9319,9322],{},[609,9315,9316],{"align":4836},[1641,9317,9318],{},"아이콘 렌더링 속도",[609,9320,9321],{"align":4836},"클라이언트 사이드 재요청으로 깜빡임 현상 유발",[609,9323,9324],{"align":4836},"서버 단계 로컬 주입으로 실시간 매싱 완성",[851,9326,9327],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":178,"searchDepth":186,"depth":186,"links":9329},[9330,9334,9341],{"id":8799,"depth":186,"text":8800,"children":9331},[9332,9333],{"id":8810,"depth":2333,"text":8811},{"id":8841,"depth":2333,"text":8842},{"id":8874,"depth":186,"text":8875,"children":9335},[9336,9337,9339],{"id":8895,"depth":2333,"text":8896},{"id":8962,"depth":2333,"text":9338},"② 2단계: 하위 레이어 설정 조정 (layer\u002Fnuxt.config.ts)",{"id":9137,"depth":2333,"text":9340},"③ 3단계: 메인 서비스 설정 덮어쓰기 (docs\u002Fnuxt.config.ts)",{"id":9254,"depth":186,"text":9255},"2026-07-09","제한된 네트워크 환경에서 발생한 Nuxt Icon 모듈 오류 원인을 분석하고 Custom Collection을 활용해 로컬 아이콘 환경을 구성한 과정 정리",{"tags":9345,"image":4331,"imageDark":9351,"imageClass":9352},[9346,9347,9348,9349,9350],"Nuxt","Nuxt UI","Nuxt Layer","Troubleshooting","Offline","\u002Flogo\u002Flogo-dark.png","object-contain",{"icon":26},"\u002Fko\u002Ftrouble-shooting\u002Fbrolog\u002Fnuxt-icon-restricted-network-troubleshooting",{"title":9356,"description":9357},"Nuxt Icon 오류 해결 방법 | Custom Collection과 로컬 아이콘 구성","외부 네트워크 제한 환경에서 발생하는 Nuxt Icon 오류 원인과 @nuxt\u002Ficon Custom Collection을 활용한 로컬 아이콘 구성 방법을 설명합니다.","ko\u002F3.trouble-shooting\u002F1.brolog\u002F1.nuxt-icon-restricted-network-troubleshooting","xX-F1HsC76PIjIyf2trD9pgBVK5rhVoFUqvEh3uw6-Q",{"id":9361,"title":53,"body":9362,"date":10130,"description":10131,"extension":188,"links":863,"meta":10132,"navigation":10136,"path":10137,"seo":10138,"stem":10141,"__hash__":10142},"docs_ko\u002Fko\u002F1.note\u002F2.nuxt4\u002F2.nuxt-use_async_data.md",{"type":180,"value":9363,"toc":10118},[9364,10115],[204,9365,9366,9370,9376,9378,9382,9392,9419,9421,9425,9430,9527,9568,9570,9574,9583,9590,9607,9621,9629,9639,9801,9803,9807,9819,10069,10071,10075,10081],{":spotlight":206},[208,9367,9369],{"id":9368},"서버에서-한-번만-useasyncdata-데이터-페칭의-본질","서버에서 한 번만, useAsyncData 데이터 페칭의 본질",[213,9371,9372,9373,9375],{},"Nuxt 4 환경에서 블로그 포스트나 API 데이터를 가져올 때 가장 핵심이 되는 컴포저블은 단연 ",[253,9374,5456],{},"다. 일반적인 Vue 3 Single Page Application(SPA)의 데이터 페칭 방식을 Nuxt(SSR)에 그대로 대입하면 중복 호출과 화면 깜빡임이라는 치명적인 성능 저하를 마주하게 된다. 이를 우회하고 우아한 데이터 파이프라인을 구축하는 아키텍처적 방법을 알아보자.",[232,9377],{},[481,9379,9381],{"id":9380},"_1-ssr의-고질적-문제-중복-호출double-fetching과-하이드레이션","1. SSR의 고질적 문제: 중복 호출(Double Fetching)과 하이드레이션",[213,9383,9384,9385,9387,9388,9391],{},"기존 Vue 3 + Vite(SPA) 환경에서는 컴포넌트가 마운트된 후(",[253,9386,8581],{},") 브라우저가 직접 API를 호출하는 것이 정석이었다. 그러나 서버 사이드 렌더링(SSR) 환경인 Nuxt에서는 코드가 ",[1641,9389,9390],{},"서버에서 한 번, 브라우저에서 한 번, 총 두 번 실행","되는 특성을 가진다.",[1649,9393,9394,9404],{},[1652,9395,9396,9403],{},[1641,9397,9398,9399,9402],{},"일반 ",[253,9400,9401],{},"$fetch()","의 한계:"," 서버가 페이지를 빌드할 때 API를 호출하고, 브라우저가 HTML을 넘겨받은 뒤 똑같은 API를 또 호출한다. 불필요한 네트워크 오버헤드가 발생할 뿐만 아니라, 서버 데이터와 클라이언트 데이터의 동기화 타이밍 차이로 인해 하이드레이션 미스매치(Hydration Mismatch) 에러가 발생한다.",[1652,9405,9406,9411,9412,9415,9416,4902],{},[1641,9407,9408,9410],{},[253,9409,5456],{},"의 해결책:"," 서버에서 데이터를 최초로 가져오면, 그 결과물을 ",[253,9413,9414],{},"payload","라는 내장 임시 저장소에 공유 상태(State)로 박제하여 브라우저로 함께 토스한다. 브라우저는 네트워킹을 생략하고 이 저장소에서 데이터를 즉시 꺼내 재사용하므로 ",[1641,9417,9418],{},"네트워크 요청을 단 1번으로 최적화",[232,9420],{},[481,9422,9424],{"id":9423},"_2-useasyncdata-구조-및-핵심-반환-객체","2. useAsyncData 구조 및 핵심 반환 객체",[213,9426,9427,9429],{},[253,9428,5456],{},"는 단순한 데이터 호출 래퍼(Wrapper)를 넘어, 요청의 수명 주기와 로딩 상태를 선언적으로 관리할 수 있는 반응형 객체들을 반환한다.",[245,9431,9433],{"className":382,"code":9432,"filename":5465,"language":385,"meta":178,"style":178},"const { data, status, error, refresh } = await useAsyncData(\n        'main-posts-feed',                      \u002F\u002F 1. 애플리케이션 전역 고유 키 (Unique Key)\n        () => queryCollection('docs_ko').all(), \u002F\u002F 2. 비동기 데이터를 반환하는 콜백 함수\n        options                                 \u002F\u002F 3. 성능 및 동작 제어를 위한 옵션 객체\n)\n",[253,9434,9435,9469,9483,9515,9523],{"__ignoreMap":178},[389,9436,9437,9439,9441,9443,9445,9448,9450,9453,9455,9458,9460,9462,9464,9466],{"class":391,"line":392},[389,9438,455],{"class":454},[389,9440,2515],{"class":399},[389,9442,5477],{"class":407},[389,9444,5503],{"class":399},[389,9446,9447],{"class":407}," status",[389,9449,5503],{"class":399},[389,9451,9452],{"class":407}," error",[389,9454,5503],{"class":399},[389,9456,9457],{"class":407}," refresh ",[389,9459,2359],{"class":399},[389,9461,2523],{"class":399},[389,9463,2527],{"class":2526},[389,9465,5491],{"class":403},[389,9467,9468],{"class":407},"(\n",[389,9470,9471,9474,9476,9478,9480],{"class":391,"line":186},[389,9472,9473],{"class":399},"        '",[389,9475,6972],{"class":936},[389,9477,2535],{"class":399},[389,9479,5503],{"class":399},[389,9481,9482],{"class":3108},"                      \u002F\u002F 1. 애플리케이션 전역 고유 키 (Unique Key)\n",[389,9484,9485,9488,9490,9493,9495,9497,9499,9501,9503,9505,9507,9510,9512],{"class":391,"line":2333},[389,9486,9487],{"class":399},"        ()",[389,9489,5509],{"class":454},[389,9491,9492],{"class":403}," queryCollection",[389,9494,408],{"class":407},[389,9496,2535],{"class":399},[389,9498,5524],{"class":936},[389,9500,2535],{"class":399},[389,9502,5529],{"class":407},[389,9504,5532],{"class":399},[389,9506,5559],{"class":403},[389,9508,9509],{"class":407},"()",[389,9511,5503],{"class":399},[389,9513,9514],{"class":3108}," \u002F\u002F 2. 비동기 데이터를 반환하는 콜백 함수\n",[389,9516,9517,9520],{"class":391,"line":2344},[389,9518,9519],{"class":407},"        options                                 ",[389,9521,9522],{"class":3108},"\u002F\u002F 3. 성능 및 동작 제어를 위한 옵션 객체\n",[389,9524,9525],{"class":391,"line":2356},[389,9526,415],{"class":407},[1649,9528,9529,9540,9560],{},[1652,9530,9531,9535,9536,9539],{},[1641,9532,9533],{},[253,9534,6708],{},": 비동기 함수가 최종 반환한 데이터가 담긴 Vue 3 ",[253,9537,9538],{},"Ref"," 객체다.",[1652,9541,9542,9547,9548,907,9551,907,9554,907,9557,5529],{},[1641,9543,9544],{},[253,9545,9546],{},"status",": 현재 데이터 페칭의 상태를 실시간 문자열로 제공한다. (",[253,9549,9550],{},"'idle'",[253,9552,9553],{},"'pending'",[253,9555,9556],{},"'success'",[253,9558,9559],{},"'error'",[1652,9561,9562,9567],{},[1641,9563,9564],{},[253,9565,9566],{},"refresh",": 페이지 전체를 새로고침하지 않고, 특정 이벤트(예: 버튼 클릭) 시점에 데이터를 수동으로 다시 불러와서 UI를 갱신하는 고차 함수다.",[232,9569],{},[481,9571,9573],{"id":9572},"_3-실무-아키텍처를-위한-고급-옵션-options-파이프라인","3. 실무 아키텍처를 위한 고급 옵션 (Options) 파이프라인",[213,9575,9576,9578,9579,9582],{},[253,9577,5456],{},"의 세 번째 인자인 ",[253,9580,9581],{},"options"," 객체를 활용하면 프론트엔드 레벨에서 서빙되는 데이터 구조를 극적으로 경량화할 수 있다.",[2616,9584,6000,9586,9589],{"id":9585},"default-패턴을-이용한-초기-런타임-에러-방지",[253,9587,9588],{},"default"," 패턴을 이용한 초기 런타임 에러 방지",[213,9591,9592,9593,9596,9597,9599,9600,9603,9604,9606],{},"비동기 데이터가 도착하기 전, ",[253,9594,9595],{},"data.value","는 기본적으로 ",[253,9598,7229],{}," 상태다. 이때 템플릿이나 computed에서 배열 메서드(",[253,9601,9602],{},".map()",")를 호출하면 에러가 터진다. ",[253,9605,9588],{}," 옵션으로 초기 상태를 보장하는 것이 안전하다.",[2125,9608,9609],{},[213,9610,9611,9614,9616,9617,9620],{},[1641,9612,9613],{},"💡 데이터 안정성 확보",[253,9615,9588],{}," 옵션을 배열 구조(",[253,9618,9619],{},"() => []",")로 정의하면 v-for 루프나 가공 함수가 데이터 로딩 중에도 크래시 없이 안전하게 실행된다.",[2616,9622,9624,9625,9628],{"id":9623},"transform을-통한-네트워크-페이로드-경량화","② ",[253,9626,9627],{},"transform","을 통한 네트워크 페이로드 경량화",[213,9630,9631,9632,9635,9636,9638],{},"Content 모듈이나 외부 CMS에서 넘겨주는 원본 데이터 중, ",[1641,9633,9634],{},"현재 컴포넌트 렌더링에 꼭 필요한 필드만 필터링하여 보관","할 수 있다. 브라우저가 전달받는 ",[253,9637,9414],{}," 메모리 크기가 줄어들어 전체적인 성능과 메모리 효율이 극대화된다.",[245,9640,9642],{"className":382,"code":9641,"filename":5465,"language":385,"meta":178,"style":178},"\u002F\u002F 본문(body) 내용 등 무거운 필드를 제외하고 목록용 데이터만 슬라이싱하는 예시\nconst { data: postsSummary } = await useAsyncData('posts-summary', () => queryCollection('docs').all(), {\n  transform: (rawPosts) => rawPosts.map(post => ({\n    title: post.title,\n    description: post.description,\n    to: post._path\n  }))\n})\n",[253,9643,9644,9649,9707,9742,9757,9773,9787,9795],{"__ignoreMap":178},[389,9645,9646],{"class":391,"line":392},[389,9647,9648],{"class":3108},"\u002F\u002F 본문(body) 내용 등 무거운 필드를 제외하고 목록용 데이터만 슬라이싱하는 예시\n",[389,9650,9651,9653,9655,9657,9659,9662,9664,9666,9668,9670,9672,9674,9677,9679,9681,9683,9685,9687,9689,9691,9693,9695,9697,9699,9701,9703,9705],{"class":391,"line":186},[389,9652,455],{"class":454},[389,9654,2515],{"class":399},[389,9656,5477],{"class":2323},[389,9658,400],{"class":399},[389,9660,9661],{"class":407}," postsSummary ",[389,9663,2359],{"class":399},[389,9665,2523],{"class":399},[389,9667,2527],{"class":2526},[389,9669,5491],{"class":403},[389,9671,408],{"class":407},[389,9673,2535],{"class":399},[389,9675,9676],{"class":936},"posts-summary",[389,9678,2535],{"class":399},[389,9680,5503],{"class":399},[389,9682,5506],{"class":399},[389,9684,5509],{"class":454},[389,9686,9492],{"class":403},[389,9688,408],{"class":407},[389,9690,2535],{"class":399},[389,9692,1690],{"class":936},[389,9694,2535],{"class":399},[389,9696,5529],{"class":407},[389,9698,5532],{"class":399},[389,9700,5559],{"class":403},[389,9702,9509],{"class":407},[389,9704,5503],{"class":399},[389,9706,2751],{"class":399},[389,9708,9709,9712,9714,9717,9720,9722,9724,9727,9729,9731,9733,9736,9738,9740],{"class":391,"line":2333},[389,9710,9711],{"class":403},"  transform",[389,9713,400],{"class":399},[389,9715,9716],{"class":399}," (",[389,9718,9719],{"class":7336},"rawPosts",[389,9721,5529],{"class":399},[389,9723,5509],{"class":454},[389,9725,9726],{"class":407}," rawPosts",[389,9728,5532],{"class":399},[389,9730,7179],{"class":403},[389,9732,408],{"class":407},[389,9734,9735],{"class":7336},"post",[389,9737,5509],{"class":454},[389,9739,9716],{"class":407},[389,9741,2341],{"class":399},[389,9743,9744,9747,9749,9751,9753,9755],{"class":391,"line":2344},[389,9745,9746],{"class":2323},"    title",[389,9748,400],{"class":399},[389,9750,7475],{"class":407},[389,9752,5532],{"class":399},[389,9754,4000],{"class":407},[389,9756,6450],{"class":399},[389,9758,9759,9762,9764,9766,9768,9771],{"class":391,"line":2356},[389,9760,9761],{"class":2323},"    description",[389,9763,400],{"class":399},[389,9765,7475],{"class":407},[389,9767,5532],{"class":399},[389,9769,9770],{"class":407},"description",[389,9772,6450],{"class":399},[389,9774,9775,9778,9780,9782,9784],{"class":391,"line":2364},[389,9776,9777],{"class":2323},"    to",[389,9779,400],{"class":399},[389,9781,7475],{"class":407},[389,9783,5532],{"class":399},[389,9785,9786],{"class":407},"_path\n",[389,9788,9789,9792],{"class":391,"line":2374},[389,9790,9791],{"class":399},"  }",[389,9793,9794],{"class":407},"))\n",[389,9796,9797,9799],{"class":391,"line":2379},[389,9798,2359],{"class":399},[389,9800,415],{"class":407},[232,9802],{},[481,9804,9806],{"id":9805},"_4-nuxt-4-content-모듈-최적화-실무-패턴","4. Nuxt 4 Content 모듈 최적화 실무 패턴",[213,9808,9809,9810,9812,9813,9815,9816,9818],{},"블로그 메인 피드처럼 정제된 가공 데이터가 필요할 때, 별도의 외부 ",[253,9811,6111],{}," 파이프라인으로 결합도를 높이기보다 ",[253,9814,5456],{}," 콜백 내부에서 ",[253,9817,6285],{}," 후 인라인으로 최종 형태를 빌드하여 반환하는 패턴이 스코프 관리 측면에서 가장 이상적이다.",[245,9820,9822],{"className":382,"code":9821,"filename":5465,"language":385,"meta":178,"style":178},"const { data: formattedPosts } = await useAsyncData('main-posts-feed', async () => {\n  \u002F\u002F 1. 비동기 쿼리를 통해 raw 데이터 배열을 온전히 확보\n  const rawPosts = await queryCollection('docs_ko')\n    .order('date', 'DESC') \u002F\u002F 최신순 정렬\n    .all()\n\n  \u002F\u002F 2. 확보된 순수 배열을 하이드레이션 이전에 즉시 맵핑 및 가공\n  return rawPosts.map(post => ({\n    ...post,\n    image: post.image || '[https:\u002F\u002Fnuxt.com\u002Fassets\u002Fblog\u002Fnuxt-icon\u002Fcover.png](https:\u002F\u002Fnuxt.com\u002Fassets\u002Fblog\u002Fnuxt-icon\u002Fcover.png)',\n    date: formatDate(post.date), \u002F\u002F 날짜 포맷 레이어 적용\n    to: post._path,\n  }))\n}, {\n  default: () => [], \u002F\u002F 초기 렌더링 안정성 확보\n})\n",[253,9823,9824,9864,9869,9892,9919,9927,9931,9936,9957,9966,9991,10016,10031,10037,10044,10063],{"__ignoreMap":178},[389,9825,9826,9828,9830,9832,9834,9837,9839,9841,9843,9845,9847,9849,9851,9853,9855,9858,9860,9862],{"class":391,"line":392},[389,9827,455],{"class":454},[389,9829,2515],{"class":399},[389,9831,5477],{"class":2323},[389,9833,400],{"class":399},[389,9835,9836],{"class":407}," formattedPosts ",[389,9838,2359],{"class":399},[389,9840,2523],{"class":399},[389,9842,2527],{"class":2526},[389,9844,5491],{"class":403},[389,9846,408],{"class":407},[389,9848,2535],{"class":399},[389,9850,6972],{"class":936},[389,9852,2535],{"class":399},[389,9854,5503],{"class":399},[389,9856,9857],{"class":454}," async",[389,9859,5506],{"class":399},[389,9861,5509],{"class":454},[389,9863,2751],{"class":399},[389,9865,9866],{"class":391,"line":186},[389,9867,9868],{"class":3108},"  \u002F\u002F 1. 비동기 쿼리를 통해 raw 데이터 배열을 온전히 확보\n",[389,9870,9871,9874,9876,9878,9880,9882,9884,9886,9888,9890],{"class":391,"line":2333},[389,9872,9873],{"class":454},"  const",[389,9875,9726],{"class":407},[389,9877,2523],{"class":399},[389,9879,2527],{"class":2526},[389,9881,9492],{"class":403},[389,9883,408],{"class":2323},[389,9885,2535],{"class":399},[389,9887,5524],{"class":936},[389,9889,2535],{"class":399},[389,9891,415],{"class":2323},[389,9893,9894,9896,9898,9900,9902,9904,9906,9908,9910,9912,9914,9916],{"class":391,"line":2344},[389,9895,5717],{"class":399},[389,9897,5535],{"class":403},[389,9899,408],{"class":2323},[389,9901,2535],{"class":399},[389,9903,5396],{"class":936},[389,9905,2535],{"class":399},[389,9907,5503],{"class":399},[389,9909,4005],{"class":399},[389,9911,5550],{"class":936},[389,9913,2535],{"class":399},[389,9915,5738],{"class":2323},[389,9917,9918],{"class":3108},"\u002F\u002F 최신순 정렬\n",[389,9920,9921,9923,9925],{"class":391,"line":2356},[389,9922,5717],{"class":399},[389,9924,5559],{"class":403},[389,9926,5562],{"class":2323},[389,9928,9929],{"class":391,"line":2364},[389,9930,2315],{"emptyLinePlaceholder":190},[389,9932,9933],{"class":391,"line":2374},[389,9934,9935],{"class":3108},"  \u002F\u002F 2. 확보된 순수 배열을 하이드레이션 이전에 즉시 맵핑 및 가공\n",[389,9937,9938,9941,9943,9945,9947,9949,9951,9953,9955],{"class":391,"line":2379},[389,9939,9940],{"class":2526},"  return",[389,9942,9726],{"class":407},[389,9944,5532],{"class":399},[389,9946,7179],{"class":403},[389,9948,408],{"class":2323},[389,9950,9735],{"class":7336},[389,9952,5509],{"class":454},[389,9954,9716],{"class":2323},[389,9956,2341],{"class":399},[389,9958,9959,9962,9964],{"class":391,"line":2389},[389,9960,9961],{"class":399},"    ...",[389,9963,9735],{"class":407},[389,9965,6450],{"class":399},[389,9967,9968,9971,9973,9975,9977,9979,9982,9984,9987,9989],{"class":391,"line":2400},[389,9969,9970],{"class":2323},"    image",[389,9972,400],{"class":399},[389,9974,7475],{"class":407},[389,9976,5532],{"class":399},[389,9978,4016],{"class":407},[389,9980,9981],{"class":399}," ||",[389,9983,4005],{"class":399},[389,9985,9986],{"class":936},"[https:\u002F\u002Fnuxt.com\u002Fassets\u002Fblog\u002Fnuxt-icon\u002Fcover.png](https:\u002F\u002Fnuxt.com\u002Fassets\u002Fblog\u002Fnuxt-icon\u002Fcover.png)",[389,9988,2535],{"class":399},[389,9990,6450],{"class":399},[389,9992,9993,9996,9998,10001,10003,10005,10007,10009,10011,10013],{"class":391,"line":2406},[389,9994,9995],{"class":2323},"    date",[389,9997,400],{"class":399},[389,9999,10000],{"class":403}," formatDate",[389,10002,408],{"class":2323},[389,10004,9735],{"class":407},[389,10006,5532],{"class":399},[389,10008,5396],{"class":407},[389,10010,5529],{"class":2323},[389,10012,5503],{"class":399},[389,10014,10015],{"class":3108}," \u002F\u002F 날짜 포맷 레이어 적용\n",[389,10017,10018,10020,10022,10024,10026,10029],{"class":391,"line":2416},[389,10019,9777],{"class":2323},[389,10021,400],{"class":399},[389,10023,7475],{"class":407},[389,10025,5532],{"class":399},[389,10027,10028],{"class":407},"_path",[389,10030,6450],{"class":399},[389,10032,10033,10035],{"class":391,"line":2594},[389,10034,9791],{"class":399},[389,10036,9794],{"class":2323},[389,10038,10039,10042],{"class":391,"line":2599},[389,10040,10041],{"class":399},"},",[389,10043,2751],{"class":399},[389,10045,10046,10049,10051,10053,10055,10058,10060],{"class":391,"line":3171},[389,10047,10048],{"class":403},"  default",[389,10050,400],{"class":399},[389,10052,5506],{"class":399},[389,10054,5509],{"class":454},[389,10056,10057],{"class":407}," []",[389,10059,5503],{"class":399},[389,10061,10062],{"class":3108}," \u002F\u002F 초기 렌더링 안정성 확보\n",[389,10064,10065,10067],{"class":391,"line":3176},[389,10066,2359],{"class":399},[389,10068,415],{"class":407},[232,10070],{},[481,10072,10074],{"id":10073},"_5-결론-및-체크리스트","5. 결론 및 체크리스트",[213,10076,10077,10078,10080],{},"Nuxt 4 아키텍처에서 ",[253,10079,5456],{},"를 다룰 때는 다음 세 가지 규칙을 항상 준수해야 정교한 하이브리드 웹 애플리케이션을 완성할 수 있다.",[10082,10083,10084,10094,10106],"ol",{},[1652,10085,10086,10089,10090,10093],{},[1641,10087,10088],{},"고유 ID 규칙:"," 첫 번째 인자인 ",[253,10091,10092],{},"key","는 서비스 전역 컨텍스트 상에서 단 하나만 존재해야 한다. 키가 중복될 경우 서로 다른 컴포넌트 간에 데이터 오버라이트 현상이 발생한다.",[1652,10095,10096,10099,10100,10102,10103,10105],{},[1641,10097,10098],{},"비동기 타이밍 스코프:"," 콜백 블록 내부에서 맵핑 함수를 쓸 때는 반드시 대상을 ",[253,10101,6285],{},"로 명확하게 평가(Evaluation)하여 비동기 프로미스 객체에 직접 ",[253,10104,9602],{},"을 요청하는 실수를 방지해야 한다.",[1652,10107,10108,10111,10112,10114],{},[1641,10109,10110],{},"미니멀 데이터 유지:"," 블로그 목록 화면에서 굳이 들고 있을 필요가 없는 무거운 마크다운 객체 구조는 ",[253,10113,9627],{}," 또는 인라인 매퍼를 통해 철저히 걷어내어 가벼운 상태를 유지해야 한다.",[851,10116,10117],{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"title":178,"searchDepth":186,"depth":186,"links":10119},[10120,10121,10122,10128,10129],{"id":9380,"depth":186,"text":9381},{"id":9423,"depth":186,"text":9424},{"id":9572,"depth":186,"text":9573,"children":10123},[10124,10126],{"id":9585,"depth":2333,"text":10125},"① default 패턴을 이용한 초기 런타임 에러 방지",{"id":9623,"depth":2333,"text":10127},"② transform을 통한 네트워크 페이로드 경량화",{"id":9805,"depth":186,"text":9806},{"id":10073,"depth":186,"text":10074},"2026-07-08","Nuxt의 useAsyncData가 SSR 환경에서 데이터 중복 요청을 방지하고 Hydration 성능을 개선하는 원리와 활용 방법 정리",{"tags":10133,"image":10135},[43,6253,2095,10134],"WebDev","images\u002Fnuxt-thumbnail2.png",{"icon":26},"\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-use_async_data",{"title":10139,"description":10140},"Nuxt useAsyncData 사용법과 SSR 중복 요청 방지 원리 | SSR 최적화 방법","Nuxt useAsyncData의 데이터 패칭 흐름과 SSR 환경에서 중복 요청을 방지하는 원리, 효율적인 사용 방법을 알아봅니다.","ko\u002F1.note\u002F2.nuxt4\u002F2.nuxt-use_async_data","pps4UxvQ-vRfi0izLE2-krePVjDwxrMJdvG5qS_S49I",{"id":10144,"title":23,"body":10145,"date":11289,"description":11290,"extension":188,"links":863,"meta":11291,"navigation":11297,"path":11298,"seo":11299,"stem":11302,"__hash__":11303},"docs_ko\u002Fko\u002F1.note\u002F0.spring\u002F1.spring-ai\u002F1.spring-ai.md",{"type":180,"value":10146,"toc":11275},[10147,11272],[204,10148,10149,10153,10160,10163,10165,10167,10171,10178,10185,10187,10191,10194,10243,10245,10249,10256,10259,10335,10342,10348,10670,10678,10685,11085,11087,11091,11098,11105,11234,11236,11240,11243],{":spotlight":206},[208,10150,10152],{"id":10151},"왜-spring-ai인가","왜 Spring AI인가?",[4052,10154],{"alt":10155,"className":10156,"src":10159},"Spring AI Official Logo",[6278,10157,9352,10158],"object-fill","animate-pulse","images\u002Fspring-ai-logo.svg",[213,10161,10162],{},"그동안 LLM 연동 및 RAG 시스템 구축 등 AI 애플리케이션 개발 생태계는 Python 중심의 프레임워크(LangChain, LlamaIndex)가 주도해 왔다. 이로 인해 대규모 인프라를 Java 기반으로 운영하던 엔터프라이즈 환경에서는 기술 파편화라는 한계에 부딪혔다. Spring AI는 \"AI 모델을 하나의 추상화된 서비스(POJO)로 다루자\"는 철학 아래, 기존 Spring의 IoC\u002FDI 컨셉을 그대로 계승하여 강력한 자바 AI 파이프라인을 구축할 수 있게 돕는 공식 프로젝트다.",[232,10164],{},[232,10166],{},[481,10168,10170],{"id":10169},"_1-spring-ai의-본질과-psa-철학","1. Spring AI의 본질과 PSA 철학",[213,10172,10173,10174,10177],{},"Spring AI의 가장 핵심적인 강점은 **Portable Service Abstraction(PSA)**에 있다. 과거 Spring이 데이터베이스(JDBC\u002FJPA)나 메시지 큐(AMQP)를 추상화하여 구현체에 종속되지 않는 코드를 작성하게 해주었듯, Spring AI 역시 OpenAI, Anthropic Claude, Google Gemini, 혹은 로컬의 Ollama까지 ",[1641,10175,10176],{},"동일한 인터페이스 레이어","로 제어할 수 있게 추상화한다.",[213,10179,10180,10181,10184],{},"이를 통해 개발자는 비즈니스 로직의 변경 없이 ",[253,10182,10183],{},"application.yml"," 환경 설정만 바꾸는 것으로 AI 모델 공급업체를 언제든지 스위칭할 수 있는 유연성을 확보하게 된다.",[232,10186],{},[481,10188,10190],{"id":10189},"_2-핵심-아키텍처-구성-요소","2. 핵심 아키텍처 구성 요소",[213,10192,10193],{},"Spring AI는 복잡한 AI 워크플로우와 메모리 메커니즘을 추상화하기 위해 다음과 같은 핵심 인터페이스 컴포넌트를 제공한다.",[1649,10195,10196,10212,10235],{},[1652,10197,10198,10207,10208,10211],{},[1641,10199,10200,10203,10204,400],{},[253,10201,10202],{},"ChatModel"," & ",[253,10205,10206],{},"StreamingChatModel"," LLM 인프라와 통신하기 위한 최상위 인터페이스다. 동기식 단순 응답뿐만 아니라 실시간 글자 스트리밍(",[253,10209,10210],{},"Flux"," 반환) 구조를 완벽하게 지원한다.",[1652,10213,10214,10222,10223,10226,10227,10230,10231,10234],{},[1641,10215,10216,10203,10219,400],{},[253,10217,10218],{},"Prompt",[253,10220,10221],{},"Template"," 대화의 컨텍스트를 규정한다. 구조화된 ",[253,10224,10225],{},"SystemMessage",", 사용자의 ",[253,10228,10229],{},"UserMessage",", 모델의 이전 답변인 ",[253,10232,10233],{},"AssistantMessage","를 빌더 패턴으로 조합하여 프롬프트 엔지니어링을 관리한다.",[1652,10236,10237,10242],{},[1641,10238,10239,400],{},[253,10240,10241],{},"VectorStore"," RAG(검색 증강 생성) 아키텍처의 핵심 축이다. 텍스트를 벡터화(Embedding)하여 저장하고 유사도 검색(Similarity Search)을 수행하는 Pgvector, Pinecone, Redis, Milvus 등의 외부 벡터 DB를 동일한 코드로 핸들링한다.",[232,10244],{},[481,10246,10248],{"id":10247},"_3-실무-필수-개발-패턴-및-구현-코드","3. 실무 필수 개발 패턴 및 구현 코드",[2616,10250,10252,10253,5529],{"id":10251},"의존성-관리-및-bom-설정-buildgradle","① 의존성 관리 및 BOM 설정 (",[253,10254,10255],{},"build.gradle",[213,10257,10258],{},"Spring AI는 릴리즈 주기가 활발하므로, 의존성 결합을 방지하기 위해 BOM(Bill of Materials)을 사용해 버전을 통합 관리하는 것이 좋다.",[245,10260,10264],{"className":10261,"code":10262,"filename":10255,"language":10263,"meta":178,"style":178},"language-groovy shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","dependencies {\n    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'\n    implementation 'org.springframework.boot:spring-boot-starter-web'\n}\n\ndependencyManagement {\n    imports {\n        mavenBom \"org.springframework.ai:spring-ai-bom:1.0.0-M1\" \u002F\u002F 사용 시점의 최신 안정 버전 지정\n    }\n}\n","groovy",[253,10265,10266,10271,10283,10294,10298,10302,10307,10312,10327,10331],{"__ignoreMap":178},[389,10267,10268],{"class":391,"line":392},[389,10269,10270],{"class":407},"dependencies {\n",[389,10272,10273,10276,10278,10281],{"class":391,"line":186},[389,10274,10275],{"class":407},"    implementation ",[389,10277,2535],{"class":399},[389,10279,10280],{"class":936},"org.springframework.ai:spring-ai-openai-spring-boot-starter",[389,10282,4011],{"class":399},[389,10284,10285,10287,10289,10292],{"class":391,"line":2333},[389,10286,10275],{"class":407},[389,10288,2535],{"class":399},[389,10290,10291],{"class":936},"org.springframework.boot:spring-boot-starter-web",[389,10293,4011],{"class":399},[389,10295,10296],{"class":391,"line":2344},[389,10297,3620],{"class":407},[389,10299,10300],{"class":391,"line":2356},[389,10301,2315],{"emptyLinePlaceholder":190},[389,10303,10304],{"class":391,"line":2364},[389,10305,10306],{"class":407},"dependencyManagement {\n",[389,10308,10309],{"class":391,"line":2374},[389,10310,10311],{"class":407},"    imports {\n",[389,10313,10314,10317,10319,10322,10324],{"class":391,"line":2379},[389,10315,10316],{"class":407},"        mavenBom ",[389,10318,1239],{"class":399},[389,10320,10321],{"class":936},"org.springframework.ai:spring-ai-bom:1.0.0-M1",[389,10323,1239],{"class":399},[389,10325,10326],{"class":3108}," \u002F\u002F 사용 시점의 최신 안정 버전 지정\n",[389,10328,10329],{"class":391,"line":2389},[389,10330,2784],{"class":407},[389,10332,10333],{"class":391,"line":2400},[389,10334,3620],{"class":407},[2616,10336,10338,10339,10341],{"id":10337},"동기식-대화형-서비스-구조-chatmodel-주입","② 동기식 대화형 서비스 구조 (",[253,10340,10202],{}," 주입)",[213,10343,10344,10345,10347],{},"가장 표준적인 형태의 컴포넌트 모델이다. 별도의 수동 초기화 없이 빈(Bean) 스코프에서 주입된 ",[253,10346,10202],{},"을 통해 텍스트 생성을 즉시 처리한다.",[245,10349,10354],{"className":10350,"code":10351,"filename":10352,"language":10353,"meta":178,"style":178},"language-java shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import org.springframework.ai.chat.model.ChatModel;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class AiChatController {\n\n    private final ChatModel chatModel;\n\n    \u002F\u002F Spring AI 오토구조가 인프라 설정을 읽어 자동으로 주입\n    public AiChatController(ChatModel chatModel) {\n        this.chatModel = chatModel;\n    }\n\n    @GetMapping(\"\u002Fapi\u002Fv1\u002Fai\u002Fgenerate\")\n    public String generate(@RequestParam(value = \"message\", defaultValue = \"오늘 서울 날씨 알려줘\") String message) {\n        return chatModel.call(message);\n    }\n}\n","AiChatController","java",[253,10355,10356,10390,10422,10451,10480,10484,10492,10505,10509,10525,10529,10534,10551,10565,10569,10573,10591,10643,10662,10666],{"__ignoreMap":178},[389,10357,10358,10360,10363,10365,10368,10370,10373,10375,10378,10380,10383,10385,10387],{"class":391,"line":392},[389,10359,8569],{"class":411},[389,10361,10362],{"class":454}," org",[389,10364,5532],{"class":399},[389,10366,10367],{"class":454},"springframework",[389,10369,5532],{"class":399},[389,10371,10372],{"class":454},"ai",[389,10374,5532],{"class":399},[389,10376,10377],{"class":454},"chat",[389,10379,5532],{"class":399},[389,10381,10382],{"class":454},"model",[389,10384,5532],{"class":399},[389,10386,10202],{"class":454},[389,10388,10389],{"class":399},";\n",[389,10391,10392,10394,10396,10398,10400,10402,10405,10407,10410,10412,10415,10417,10420],{"class":391,"line":186},[389,10393,8569],{"class":411},[389,10395,10362],{"class":454},[389,10397,5532],{"class":399},[389,10399,10367],{"class":454},[389,10401,5532],{"class":399},[389,10403,10404],{"class":454},"web",[389,10406,5532],{"class":399},[389,10408,10409],{"class":454},"bind",[389,10411,5532],{"class":399},[389,10413,10414],{"class":454},"annotation",[389,10416,5532],{"class":399},[389,10418,10419],{"class":454},"GetMapping",[389,10421,10389],{"class":399},[389,10423,10424,10426,10428,10430,10432,10434,10436,10438,10440,10442,10444,10446,10449],{"class":391,"line":2333},[389,10425,8569],{"class":411},[389,10427,10362],{"class":454},[389,10429,5532],{"class":399},[389,10431,10367],{"class":454},[389,10433,5532],{"class":399},[389,10435,10404],{"class":454},[389,10437,5532],{"class":399},[389,10439,10409],{"class":454},[389,10441,5532],{"class":399},[389,10443,10414],{"class":454},[389,10445,5532],{"class":399},[389,10447,10448],{"class":454},"RequestParam",[389,10450,10389],{"class":399},[389,10452,10453,10455,10457,10459,10461,10463,10465,10467,10469,10471,10473,10475,10478],{"class":391,"line":2344},[389,10454,8569],{"class":411},[389,10456,10362],{"class":454},[389,10458,5532],{"class":399},[389,10460,10367],{"class":454},[389,10462,5532],{"class":399},[389,10464,10404],{"class":454},[389,10466,5532],{"class":399},[389,10468,10409],{"class":454},[389,10470,5532],{"class":399},[389,10472,10414],{"class":454},[389,10474,5532],{"class":399},[389,10476,10477],{"class":454},"RestController",[389,10479,10389],{"class":399},[389,10481,10482],{"class":391,"line":2356},[389,10483,2315],{"emptyLinePlaceholder":190},[389,10485,10486,10489],{"class":391,"line":2364},[389,10487,10488],{"class":399},"@",[389,10490,10491],{"class":454},"RestController\n",[389,10493,10494,10497,10500,10503],{"class":391,"line":2374},[389,10495,10496],{"class":454},"public",[389,10498,10499],{"class":454}," class",[389,10501,10502],{"class":395}," AiChatController",[389,10504,2751],{"class":399},[389,10506,10507],{"class":391,"line":2379},[389,10508,2315],{"emptyLinePlaceholder":190},[389,10510,10511,10514,10517,10520,10523],{"class":391,"line":2389},[389,10512,10513],{"class":454},"    private",[389,10515,10516],{"class":454}," final",[389,10518,10519],{"class":454}," ChatModel",[389,10521,10522],{"class":407}," chatModel",[389,10524,10389],{"class":399},[389,10526,10527],{"class":391,"line":2400},[389,10528,2315],{"emptyLinePlaceholder":190},[389,10530,10531],{"class":391,"line":2406},[389,10532,10533],{"class":3108},"    \u002F\u002F Spring AI 오토구조가 인프라 설정을 읽어 자동으로 주입\n",[389,10535,10536,10539,10541,10543,10545,10547,10549],{"class":391,"line":2416},[389,10537,10538],{"class":454},"    public",[389,10540,10502],{"class":403},[389,10542,408],{"class":399},[389,10544,10202],{"class":454},[389,10546,10522],{"class":7336},[389,10548,5529],{"class":399},[389,10550,2751],{"class":399},[389,10552,10553,10556,10559,10561,10563],{"class":391,"line":2594},[389,10554,10555],{"class":399},"        this.",[389,10557,10558],{"class":407},"chatModel ",[389,10560,461],{"class":399},[389,10562,10522],{"class":407},[389,10564,10389],{"class":399},[389,10566,10567],{"class":391,"line":2599},[389,10568,2784],{"class":399},[389,10570,10571],{"class":391,"line":3171},[389,10572,2315],{"emptyLinePlaceholder":190},[389,10574,10575,10578,10580,10582,10584,10587,10589],{"class":391,"line":3176},[389,10576,10577],{"class":399},"    @",[389,10579,10419],{"class":454},[389,10581,408],{"class":399},[389,10583,1239],{"class":399},[389,10585,10586],{"class":936},"\u002Fapi\u002Fv1\u002Fai\u002Fgenerate",[389,10588,1239],{"class":399},[389,10590,415],{"class":399},[389,10592,10593,10595,10598,10600,10603,10605,10607,10609,10611,10613,10616,10618,10620,10623,10625,10627,10630,10632,10634,10636,10639,10641],{"class":391,"line":3182},[389,10594,10538],{"class":454},[389,10596,10597],{"class":454}," String",[389,10599,954],{"class":403},[389,10601,10602],{"class":399},"(@",[389,10604,10448],{"class":454},[389,10606,408],{"class":399},[389,10608,7503],{"class":395},[389,10610,2523],{"class":399},[389,10612,3605],{"class":399},[389,10614,10615],{"class":936},"message",[389,10617,1239],{"class":399},[389,10619,5503],{"class":399},[389,10621,10622],{"class":395}," defaultValue",[389,10624,2523],{"class":399},[389,10626,3605],{"class":399},[389,10628,10629],{"class":936},"오늘 서울 날씨 알려줘",[389,10631,1239],{"class":399},[389,10633,5529],{"class":399},[389,10635,10597],{"class":454},[389,10637,10638],{"class":7336}," message",[389,10640,5529],{"class":399},[389,10642,2751],{"class":399},[389,10644,10645,10648,10650,10652,10655,10657,10659],{"class":391,"line":3196},[389,10646,10647],{"class":2526},"        return",[389,10649,10522],{"class":407},[389,10651,5532],{"class":399},[389,10653,10654],{"class":403},"call",[389,10656,408],{"class":399},[389,10658,10615],{"class":407},[389,10660,10661],{"class":399},");\n",[389,10663,10664],{"class":391,"line":3206},[389,10665,2784],{"class":399},[389,10667,10668],{"class":391,"line":3211},[389,10669,3620],{"class":399},[2616,10671,10673,10674,10677],{"id":10672},"구조화된-json-응답-역직렬화-structured-output-패턴","③ 구조화된 JSON 응답 역직렬화 (",[253,10675,10676],{},"Structured Output"," 패턴)",[213,10679,10680,10681,10684],{},"AI가 뱉는 자유 형식의 텍스트 응답을 백엔드 비즈니스 로직 내부에서 유효한 DTO나 객체 구조로 파싱하는 실무 핵심 패턴이다. ",[253,10682,10683],{},"ChatClient","의 Fluent API를 활용하면 매우 직관적으로 구현된다.",[245,10686,10689],{"className":10350,"code":10687,"filename":10688,"language":10353,"meta":178,"style":178},"import org.springframework.ai.chat.client.ChatClient;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport java.util.List;\n\n@RestController\npublic class TechBlogController {\n\n    private final ChatClient chatClient;\n\n    public TechBlogController(ChatClient.Builder builder) {\n        this.chatClient = builder.build();\n    }\n\n    \u002F\u002F 결과 매핑용 불변 구조체 정의\n    public record BlogIdea(String title, String description, List\u003CString> tags) {}\n\n    @GetMapping(\"\u002Fapi\u002Fv1\u002Fai\u002Fblog-ideas\")\n    public List\u003CBlogIdea> getBlogIdeas() {\n        return chatClient.prompt()\n                .user(\"요즘 개발 트렌드 알려줘\")\n                .call()\n                .entity(new ParameterizedTypeReference\u003CList\u003CBlogIdea>>() {}); \n                \u002F\u002F 👈 LLM의 가변 응답 데이터 구조를 List\u003CBlogIdea> 객체 배열로 정밀 역직렬화한다.\n    }\n}\n","TechBlogController",[253,10690,10691,10720,10742,10770,10798,10817,10821,10827,10838,10842,10856,10860,10882,10900,10904,10908,10913,10956,10960,10977,10997,11010,11029,11037,11069,11075,11080],{"__ignoreMap":178},[389,10692,10693,10695,10697,10699,10701,10703,10705,10707,10709,10711,10714,10716,10718],{"class":391,"line":392},[389,10694,8569],{"class":411},[389,10696,10362],{"class":454},[389,10698,5532],{"class":399},[389,10700,10367],{"class":454},[389,10702,5532],{"class":399},[389,10704,10372],{"class":454},[389,10706,5532],{"class":399},[389,10708,10377],{"class":454},[389,10710,5532],{"class":399},[389,10712,10713],{"class":454},"client",[389,10715,5532],{"class":399},[389,10717,10683],{"class":454},[389,10719,10389],{"class":399},[389,10721,10722,10724,10726,10728,10730,10732,10735,10737,10740],{"class":391,"line":186},[389,10723,8569],{"class":411},[389,10725,10362],{"class":454},[389,10727,5532],{"class":399},[389,10729,10367],{"class":454},[389,10731,5532],{"class":399},[389,10733,10734],{"class":454},"core",[389,10736,5532],{"class":399},[389,10738,10739],{"class":454},"ParameterizedTypeReference",[389,10741,10389],{"class":399},[389,10743,10744,10746,10748,10750,10752,10754,10756,10758,10760,10762,10764,10766,10768],{"class":391,"line":2333},[389,10745,8569],{"class":411},[389,10747,10362],{"class":454},[389,10749,5532],{"class":399},[389,10751,10367],{"class":454},[389,10753,5532],{"class":399},[389,10755,10404],{"class":454},[389,10757,5532],{"class":399},[389,10759,10409],{"class":454},[389,10761,5532],{"class":399},[389,10763,10414],{"class":454},[389,10765,5532],{"class":399},[389,10767,10419],{"class":454},[389,10769,10389],{"class":399},[389,10771,10772,10774,10776,10778,10780,10782,10784,10786,10788,10790,10792,10794,10796],{"class":391,"line":2344},[389,10773,8569],{"class":411},[389,10775,10362],{"class":454},[389,10777,5532],{"class":399},[389,10779,10367],{"class":454},[389,10781,5532],{"class":399},[389,10783,10404],{"class":454},[389,10785,5532],{"class":399},[389,10787,10409],{"class":454},[389,10789,5532],{"class":399},[389,10791,10414],{"class":454},[389,10793,5532],{"class":399},[389,10795,10477],{"class":454},[389,10797,10389],{"class":399},[389,10799,10800,10802,10805,10807,10810,10812,10815],{"class":391,"line":2356},[389,10801,8569],{"class":411},[389,10803,10804],{"class":454}," java",[389,10806,5532],{"class":399},[389,10808,10809],{"class":454},"util",[389,10811,5532],{"class":399},[389,10813,10814],{"class":454},"List",[389,10816,10389],{"class":399},[389,10818,10819],{"class":391,"line":2364},[389,10820,2315],{"emptyLinePlaceholder":190},[389,10822,10823,10825],{"class":391,"line":2374},[389,10824,10488],{"class":399},[389,10826,10491],{"class":454},[389,10828,10829,10831,10833,10836],{"class":391,"line":2379},[389,10830,10496],{"class":454},[389,10832,10499],{"class":454},[389,10834,10835],{"class":395}," TechBlogController",[389,10837,2751],{"class":399},[389,10839,10840],{"class":391,"line":2389},[389,10841,2315],{"emptyLinePlaceholder":190},[389,10843,10844,10846,10848,10851,10854],{"class":391,"line":2400},[389,10845,10513],{"class":454},[389,10847,10516],{"class":454},[389,10849,10850],{"class":454}," ChatClient",[389,10852,10853],{"class":407}," chatClient",[389,10855,10389],{"class":399},[389,10857,10858],{"class":391,"line":2406},[389,10859,2315],{"emptyLinePlaceholder":190},[389,10861,10862,10864,10866,10868,10870,10872,10875,10878,10880],{"class":391,"line":2416},[389,10863,10538],{"class":454},[389,10865,10835],{"class":403},[389,10867,408],{"class":399},[389,10869,10683],{"class":454},[389,10871,5532],{"class":399},[389,10873,10874],{"class":454},"Builder",[389,10876,10877],{"class":7336}," builder",[389,10879,5529],{"class":399},[389,10881,2751],{"class":399},[389,10883,10884,10886,10889,10891,10893,10895,10897],{"class":391,"line":2594},[389,10885,10555],{"class":399},[389,10887,10888],{"class":407},"chatClient ",[389,10890,461],{"class":399},[389,10892,10877],{"class":407},[389,10894,5532],{"class":399},[389,10896,906],{"class":403},[389,10898,10899],{"class":399},"();\n",[389,10901,10902],{"class":391,"line":2599},[389,10903,2784],{"class":399},[389,10905,10906],{"class":391,"line":3171},[389,10907,2315],{"emptyLinePlaceholder":190},[389,10909,10910],{"class":391,"line":3176},[389,10911,10912],{"class":3108},"    \u002F\u002F 결과 매핑용 불변 구조체 정의\n",[389,10914,10915,10917,10920,10923,10925,10928,10930,10932,10934,10937,10939,10942,10944,10946,10948,10951,10953],{"class":391,"line":3182},[389,10916,10538],{"class":454},[389,10918,10919],{"class":454}," record",[389,10921,10922],{"class":395}," BlogIdea",[389,10924,408],{"class":399},[389,10926,10927],{"class":454},"String",[389,10929,8008],{"class":407},[389,10931,5503],{"class":399},[389,10933,10597],{"class":454},[389,10935,10936],{"class":407}," description",[389,10938,5503],{"class":399},[389,10940,10941],{"class":454}," List",[389,10943,2320],{"class":399},[389,10945,10927],{"class":407},[389,10947,7830],{"class":399},[389,10949,10950],{"class":407}," tags",[389,10952,5529],{"class":399},[389,10954,10955],{"class":399}," {}\n",[389,10957,10958],{"class":391,"line":3196},[389,10959,2315],{"emptyLinePlaceholder":190},[389,10961,10962,10964,10966,10968,10970,10973,10975],{"class":391,"line":3206},[389,10963,10577],{"class":399},[389,10965,10419],{"class":454},[389,10967,408],{"class":399},[389,10969,1239],{"class":399},[389,10971,10972],{"class":936},"\u002Fapi\u002Fv1\u002Fai\u002Fblog-ideas",[389,10974,1239],{"class":399},[389,10976,415],{"class":399},[389,10978,10979,10981,10983,10985,10988,10990,10993,10995],{"class":391,"line":3211},[389,10980,10538],{"class":454},[389,10982,10941],{"class":454},[389,10984,2320],{"class":399},[389,10986,10987],{"class":454},"BlogIdea",[389,10989,7830],{"class":399},[389,10991,10992],{"class":403}," getBlogIdeas",[389,10994,9509],{"class":399},[389,10996,2751],{"class":399},[389,10998,10999,11001,11003,11005,11008],{"class":391,"line":3216},[389,11000,10647],{"class":2526},[389,11002,10853],{"class":407},[389,11004,5532],{"class":399},[389,11006,11007],{"class":403},"prompt",[389,11009,5562],{"class":399},[389,11011,11012,11015,11018,11020,11022,11025,11027],{"class":391,"line":3221},[389,11013,11014],{"class":399},"                .",[389,11016,11017],{"class":403},"user",[389,11019,408],{"class":399},[389,11021,1239],{"class":399},[389,11023,11024],{"class":936},"요즘 개발 트렌드 알려줘",[389,11026,1239],{"class":399},[389,11028,415],{"class":399},[389,11030,11031,11033,11035],{"class":391,"line":3226},[389,11032,11014],{"class":399},[389,11034,10654],{"class":403},[389,11036,5562],{"class":399},[389,11038,11040,11042,11045,11047,11050,11053,11055,11057,11059,11061,11064,11067],{"class":391,"line":11039},24,[389,11041,11014],{"class":399},[389,11043,11044],{"class":403},"entity",[389,11046,408],{"class":399},[389,11048,11049],{"class":2526},"new",[389,11051,11052],{"class":454}," ParameterizedTypeReference",[389,11054,2320],{"class":399},[389,11056,10814],{"class":454},[389,11058,2320],{"class":399},[389,11060,10987],{"class":454},[389,11062,11063],{"class":399},">>()",[389,11065,11066],{"class":399}," {});",[389,11068,5512],{"class":407},[389,11070,11072],{"class":391,"line":11071},25,[389,11073,11074],{"class":3108},"                \u002F\u002F 👈 LLM의 가변 응답 데이터 구조를 List\u003CBlogIdea> 객체 배열로 정밀 역직렬화한다.\n",[389,11076,11078],{"class":391,"line":11077},26,[389,11079,2784],{"class":399},[389,11081,11083],{"class":391,"line":11082},27,[389,11084,3620],{"class":399},[232,11086],{},[481,11088,11090],{"id":11089},"_4-에이전틱-워크플로우를-위한-function-calling-인터페이스","4. 에이전틱 워크플로우를 위한 Function Calling 인터페이스",[213,11092,11093,11094,11097],{},"Spring AI의 가장 독보적인 기능 중 하나는 LLM이 필요에 따라 실제 시스템의 자바 메서드를 동적으로 호출할 수 있는 ",[1641,11095,11096],{},"Function Calling(도구 사용)"," 인프라다.",[213,11099,11100,11101,11104],{},"\"내 블로그 오늘 총 조회수 알려줘\" 같은 실시간성 질문이 들어오면, LLM이 텍스트 생성을 멈추고 미리 등록해 둔 조회용 자바 함수(",[253,11102,11103],{},"Function\u003CR, T>",")를 명세에 따라 가상으로 호출하여 결합된 데이터를 반환하게 유도할 수 있다.",[245,11106,11109],{"className":10350,"code":11107,"filename":11108,"language":10353,"meta":178,"style":178},"@Configuration\npublic class AiConfig {\n\n    @Bean\n    @Description(\"데이터베이스에서 실시간 특정 날짜의 블로그 방문 통계 수치를 조회합니다.\")\n    public Function\u003CDailyStatusRequest, DailyStatusResponse> fetchBlogMetrics() {\n        return request -> {\n            \u002F\u002F 실제 영속성 레이어(JPA\u002FMyBatis) 엔드포인트 연동 스코프\n            return new DailyStatusResponse(14500); \n        };\n    }\n}\n","AiConfig",[253,11110,11111,11118,11129,11133,11140,11158,11184,11196,11201,11221,11226,11230],{"__ignoreMap":178},[389,11112,11113,11115],{"class":391,"line":392},[389,11114,10488],{"class":399},[389,11116,11117],{"class":454},"Configuration\n",[389,11119,11120,11122,11124,11127],{"class":391,"line":186},[389,11121,10496],{"class":454},[389,11123,10499],{"class":454},[389,11125,11126],{"class":395}," AiConfig",[389,11128,2751],{"class":399},[389,11130,11131],{"class":391,"line":2333},[389,11132,2315],{"emptyLinePlaceholder":190},[389,11134,11135,11137],{"class":391,"line":2344},[389,11136,10577],{"class":399},[389,11138,11139],{"class":454},"Bean\n",[389,11141,11142,11144,11147,11149,11151,11154,11156],{"class":391,"line":2356},[389,11143,10577],{"class":399},[389,11145,11146],{"class":454},"Description",[389,11148,408],{"class":399},[389,11150,1239],{"class":399},[389,11152,11153],{"class":936},"데이터베이스에서 실시간 특정 날짜의 블로그 방문 통계 수치를 조회합니다.",[389,11155,1239],{"class":399},[389,11157,415],{"class":399},[389,11159,11160,11162,11165,11167,11170,11172,11175,11177,11180,11182],{"class":391,"line":2364},[389,11161,10538],{"class":454},[389,11163,11164],{"class":454}," Function",[389,11166,2320],{"class":399},[389,11168,11169],{"class":454},"DailyStatusRequest",[389,11171,5503],{"class":399},[389,11173,11174],{"class":454}," DailyStatusResponse",[389,11176,7830],{"class":399},[389,11178,11179],{"class":403}," fetchBlogMetrics",[389,11181,9509],{"class":399},[389,11183,2751],{"class":399},[389,11185,11186,11188,11191,11194],{"class":391,"line":2374},[389,11187,10647],{"class":2526},[389,11189,11190],{"class":407}," request ",[389,11192,11193],{"class":454},"->",[389,11195,2751],{"class":399},[389,11197,11198],{"class":391,"line":2379},[389,11199,11200],{"class":3108},"            \u002F\u002F 실제 영속성 레이어(JPA\u002FMyBatis) 엔드포인트 연동 스코프\n",[389,11202,11203,11206,11209,11211,11213,11216,11219],{"class":391,"line":2389},[389,11204,11205],{"class":2526},"            return",[389,11207,11208],{"class":2526}," new",[389,11210,11174],{"class":403},[389,11212,408],{"class":399},[389,11214,11215],{"class":411},"14500",[389,11217,11218],{"class":399},");",[389,11220,5512],{"class":407},[389,11222,11223],{"class":391,"line":2400},[389,11224,11225],{"class":399},"        };\n",[389,11227,11228],{"class":391,"line":2406},[389,11229,2784],{"class":399},[389,11231,11232],{"class":391,"line":2416},[389,11233,3620],{"class":399},[232,11235],{},[481,11237,11239],{"id":11238},"_5-결론-및-실무-체크리스트","5. 결론 및 실무 체크리스트",[213,11241,11242],{},"Spring AI를 엔터프라이즈 실무 아키텍처에 안착시키기 위해서는 다음 세 가지 기술적 지향점을 유지해야 한다.",[10082,11244,11245,11254,11266],{},[1652,11246,11247,11250,11251,11253],{},[1641,11248,11249],{},"인프라 결합도 낮추기:"," 비즈니스 서비스 레이어가 특정 벤더(예: OpenAI)의 고유 상속 프롭 객체에 결합하지 않도록, ",[253,11252,10683],{},"나 추상화 클래스를 적극 활용해야 마이그레이션 유연성이 확보된다.",[1652,11255,11256,11259,11260,11262,11263,11265],{},[1641,11257,11258],{},"리액티브 파이프라인 대응:"," 트래픽 오버헤드와 모델 응답 지연에 대응하기 위해, 무거운 단일 블로킹 콜보다는 ",[253,11261,10206],{},"을 이용한 ",[253,11264,10210],{}," 기반의 논블로킹 웹플럭스(WebFlux) 구조를 우선적으로 고려해야 한다.",[1652,11267,11268,11271],{},[1641,11269,11270],{},"데이터 경량 토큰 최적화:"," RAG 구성 시 무조건적인 고차원 임베딩 서치보다는 적절한 크기의 덩어리(Chunk) 슬라이싱 and 메타데이터 필터링을 병행하여 자바 메모리 누수와 API 토큰 비용 낭비를 예방해야 한다.",[851,11273,11274],{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":178,"searchDepth":186,"depth":186,"links":11276},[11277,11278,11279,11287,11288],{"id":10169,"depth":186,"text":10170},{"id":10189,"depth":186,"text":10190},{"id":10247,"depth":186,"text":10248,"children":11280},[11281,11283,11285],{"id":10251,"depth":2333,"text":11282},"① 의존성 관리 및 BOM 설정 (build.gradle)",{"id":10337,"depth":2333,"text":11284},"② 동기식 대화형 서비스 구조 (ChatModel 주입)",{"id":10672,"depth":2333,"text":11286},"③ 구조화된 JSON 응답 역직렬화 (Structured Output 패턴)",{"id":11089,"depth":186,"text":11090},{"id":11238,"depth":186,"text":11239},"2026-07-07","Spring AI가 제공하는 AI 서비스 추상화 구조와 LLM, Vector DB 연동 방식, 실무에서 활용되는 주요 개발 패턴 정리",{"tags":11292,"image":10159,"imageClass":9352},[11293,11294,11295,11296],"Spring AI","Spring Boot 3","LLM","Java",{"icon":26},"\u002Fko\u002Fnote\u002Fspring\u002Fspring-ai\u002Fspring-ai",{"title":11300,"description":11301},"Spring AI 핵심 개념과 LLM 연동 방법 정리","Spring AI의 동작 원리와 ChatModel, VectorStore, Function Calling 기반의 LLM 애플리케이션 구성 방법을 설명합니다.","ko\u002F1.note\u002F0.spring\u002F1.spring-ai\u002F1.spring-ai","NuhuRD4N4Jl7eoqzZpQBXbwVyeDg-z-5aGxsBVA7a_U",{"id":11305,"title":11306,"body":11307,"date":11289,"description":11536,"extension":188,"links":863,"meta":11537,"navigation":11540,"path":11541,"seo":11542,"stem":11545,"__hash__":11546},"docs_ko\u002Fko\u002F1.note\u002F2.nuxt4\u002F1.nuxt-info.md","Nuxt 4를 선택한 이유와 아키텍처 및 실무 가이드",{"type":180,"value":11308,"toc":11525},[11309],[204,11310,11311,11315,11323,11326,11335,11337,11341,11344,11360,11366,11368,11372,11375,11383,11389,11395,11402,11406,11412,11415,11417,11421,11464,11466,11470,11473,11513,11515,11519,11522],{":spotlight":206},[208,11312,11314],{"id":11313},"nuxt-4를-선택한-이유와-아키텍처-정리","📖 Nuxt 4를 선택한 이유와 아키텍처 정리",[2395,11316,11318],{"className":11317},[4049,4050],[4052,11319],{"alt":11320,"height":11321,"src":11322,"width":4317},"nuxt-thumbnail",350,"\u002Fimages\u002Fnuxt-thumbnail.png",[213,11324,11325],{},"Vue.js로 웹 프로젝트를 하다 보면 자연스럽게 마주하게 되는 벽이 있다. 바로 SEO(검색엔진 최적화)와 초기 로딩 속도 문제다. 클라이언트 사이드 렌더링(CSR) 중심의 SPA 구조에서는 대형 프로젝트나 검색 유입이 중요한 서비스를 만들 때 한계가 명확하다.",[213,11327,11328,11329,11331,11332,11334],{},"이 문제를 해결하기 위해 도입한 ",[1641,11330,9346],{},", 그중에서도 최신 버전인 ",[1641,11333,5976],{},"를 직접 다루며 파악한 핵심 구조와 생태계 변화, 그리고 실무에서 느낀 점들을 정리해본다.",[232,11336],{},[481,11338,11340],{"id":11339},"기존-vue-생태계의-갈증과-nuxt의-등장-배경","🔎 기존 Vue 생태계의 갈증과 Nuxt의 등장 배경",[213,11342,11343],{},"초창기 Vue는 화면을 빠르게 그릴 수 있는 훌륭한 UI 라이브러리였다. 하지만 프로젝트를 구성하려면 개발자가 직접 판을 짜야 했다.",[1649,11345,11346,11357],{},[1652,11347,11348,11349,11352,11353,11356],{},"라우터는 ",[253,11350,11351],{},"Vue Router",", 상태 관리는 ",[253,11354,11355],{},"Pinia",", 메타태그 관리는 별도 패키지...",[1652,11358,11359],{},"빌드 최적화와 SSR(서버사이드 렌더링)을 직접 구현하려고 하면 Node.js 서버 세팅부터 시작해 복잡도가 급격히 올라간다.",[213,11361,1239,11362,11365],{},[1641,11363,11364],{},"가장 이상적인 구조를 미리 정의해 둘 테니, 너희는 비즈니스 로직에만 집중해","\"라는 철학으로 나온 프레임워크가 바로 Nuxt다. 라우팅, 데이터 페칭, 빌드 최적화가 하나의 규격 안에 통합되어 있어 개발 생산성을 극대화해 준다.",[232,11367],{},[481,11369,11371],{"id":11370},"nuxt-4로-넘어오면서-무엇이-달라졌을까","🧐 Nuxt 4로 넘어오면서 무엇이 달라졌을까?",[213,11373,11374],{},"최신 Nuxt 4에 이르러서는 프론트엔드와 백엔드의 경계가 더욱 깔끔하게 정돈되었다.",[2616,11376,11378,11379,11382],{"id":11377},"명확해진-단일-앱-레이어-app-디렉토리","📁 ① 명확해진 단일 앱 레이어 (",[253,11380,11381],{},"app\u002F"," 디렉토리)",[213,11384,11385,11386,11388],{},"과거에는 루트 경로에 컴포넌트, 페이지, 레이아웃 등이 섞여 있어 프로젝트가 커지면 구조가 산만해졌다. Nuxt 4에서는 모든 프론트엔드 자산이 ",[253,11387,11381],{}," 폴더 안으로 완벽히 응집된다.",[245,11390,11393],{"className":11391,"code":11392,"language":251,"meta":178},[248],"my-nuxt-project\u002F\n├── app\u002F                  # 💡 프론트엔드 소스코드 집약 (pages, components 등)\n├── server\u002F               # 💡 독립된 백엔드\u002FAPI 레이어 (Nitro 엔진)\n├── nuxt.config.ts\n└── package.json\n",[253,11394,11392],{"__ignoreMap":178},[213,11396,11397,11398,11401],{},"이로 인해 프론트엔드 영역과 서버 영역(",[253,11399,11400],{},"server\u002F",")의 구분이 명확해져 유지보수할 때 정신 건강에 이롭다.",[2616,11403,11405],{"id":11404},"nitro-런타임-엔진","⚡ ② Nitro 런타임 엔진",[213,11407,11408,11409,11411],{},"Nuxt 하부에서 서버를 지탱하는 ",[253,11410,69],{},"는 개발자가 인프라 환경(Node.js, Vercel, AWS Lambda 등)을 크게 신경 쓰지 않도록 추상화되어 있다.",[213,11413,11414],{},"코드 한 줄 수정 없이 에지(Edge) 환경에 배포할 수 있으며, 캐싱(ISR, SWR) 설정도 설정 파일 몇 줄로 제어할 수 있어 인프라 비용과 성능을 동시에 잡을 수 있다.",[232,11416],{},[481,11418,11420],{"id":11419},"개발하면서-체감하는-nuxt의-강력한-무기들","🔥 개발하면서 체감하는 Nuxt의 강력한 무기들",[1649,11422,11423,11440,11448,11454],{},[1652,11424,11425,7881,11428,11431,11432,907,11434,11436,11437,11439],{},[1641,11426,11427],{},"설정보다 관습 (Convention over Configuration):",[253,11429,11430],{},"app\u002Fpages\u002F","에 파일을 만들면 라우팅이 알아서 잡히고, ",[253,11433,6048],{},[253,11435,6111],{}," 같은 Vue 핵심 API나 컴포넌트를 일일이 ",[253,11438,8569],{}," 할 필요가 없다. (Auto-imports)",[1652,11441,11442,11447],{},[1641,11443,11444,11446],{},[253,11445,6114],{},"를 통한 중복 요청 차단 (Payload 직렬화):"," 서버에서 데이터를 가져온 상태(State)를 그대로 브라우저로 넘겨주기 때문에(Hydration), 클라이언트가 켜졌을 때 똑같은 API를 두 번 부르는 상용구 현상을 원천 차단한다.",[1652,11449,11450,11453],{},[1641,11451,11452],{},"엔드투엔드 타입 안전성:"," 백엔드 API 레이어(server\u002Fapi\u002F)의 응답 구조가 바뀌면, 프론트엔드에서 데이터를 호출하는 코드까지 실시간으로 타입 추론이 연동되어 런타임 에러를 획기적으로 줄여준다.",[1652,11455,11456,11463],{},[1641,11457,11458,11459,11462],{},"서버 컴포넌트 (",[253,11460,11461],{},"*.server.vue","):"," 무거운 라이브러리나 마크다운 파서 등을 오직 서버에서만 렌더링하고 브라우저로는 정적 HTML만 보낸다. 덕분에 클라이언트 번들 사이즈를 방어하기 좋다.",[232,11465],{},[481,11467,11469],{"id":11468},"️-실무에서-nuxt를-쓸-때-반드시-알아야-할-주의점","⚠️ 실무에서 Nuxt를 쓸 때 반드시 알아야 할 주의점",[213,11471,11472],{},"막 쓰기에는 편하지만, SPA에만 익숙해져 있다면 몇 가지 삽질 포인트가 있다.",[1649,11474,11475,11498,11504],{},[1652,11476,11477,11486,11487,11490,11491,11493,11494,11497],{},[1641,11478,11479,11480,907,11482,11485],{},"SSR과 브라우저 전역 객체 (",[253,11481,6040],{},[253,11483,11484],{},"localStorage"," 등):","\n서버에서 먼저 HTML을 그릴 때는 저런 객체들이 존재하지 않는다. 따라서 렌더링 단계에서 ",[253,11488,11489],{},"window is not defined"," 에러를 뿜기 십상이다. 브라우저 API는 반드시 ",[253,11492,8581],{}," 훅 내부나 ",[253,11495,11496],{},"import.meta.client"," 체크를 거친 뒤에 사용해야 한다.",[1652,11499,11500,11503],{},[1641,11501,11502],{},"하이드레이션 미스매치 (Hydration Mismatch):","\n서버에서 그린 HTML 결과물과 브라우저가 처음 실행할 때의 결과물이 미묘하게 다르면 콘솔에 경고창이 가득 찬다. (예: 날짜나 랜덤 값을 그대로 렌더링할 때 발생하기 쉽다.)",[1652,11505,11506,11509,11510,11512],{},[1641,11507,11508],{},"무분별한 SSR은 금물:","\n모든 페이지를 다 SSR로 돌릴 필요는 없다. 동적으로 실시간 데이터를 먹일 필요가 없는 페이지는 ",[253,11511,2858],{},"를 통해 SSG나 ISR로 빼는 설계적 고민이 필요하다.",[232,11514],{},[481,11516,11518],{"id":11517},"마치며-프레임워크가-주는-제약-속의-자유","🎯 마치며: 프레임워크가 주는 제약 속의 자유",[213,11520,11521],{},"Nuxt는 처음에는 \"Vue를 조금 더 편하게 쓰게 해주는 껍데기\"처럼 보이지만, 깊게 팔수록 \"웹 애플리케이션을 어떤 구조로 설계하고 배포해야 하는지\"에 대한 선명한 정답을 제시해주는 프레임워크다.",[213,11523,11524],{},"처음에는 강력한 규격(Convention)이 답답하게 느껴질 수도 있지만, 이 규격이 주는 안정감과 Nitro 엔진이 가져다주는 인프라의 유연함을 경험하고 나면, 중대형 웹 서비스를 혼자서도 뚝딱 만들어낼 수 있는 가장 강력한 무기임을 깨닫게 된다.",{"title":178,"searchDepth":186,"depth":186,"links":11526},[11527,11528,11533,11534,11535],{"id":11339,"depth":186,"text":11340},{"id":11370,"depth":186,"text":11371,"children":11529},[11530,11532],{"id":11377,"depth":2333,"text":11531},"📁 ① 명확해진 단일 앱 레이어 (app\u002F 디렉토리)",{"id":11404,"depth":2333,"text":11405},{"id":11419,"depth":186,"text":11420},{"id":11468,"depth":186,"text":11469},{"id":11517,"depth":186,"text":11518},"Vue 생태계에서 Nuxt가 등장한 배경부터 Nuxt 4의 단일 앱 레이어 구조, Auto-imports, SSR 데이터 페칭, 그리고 실무 주의점 정리",{"tags":11538,"image":10135},[9346,4220,11539,2095,1699],"Frontend",{"icon":26},"\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-info",{"title":11543,"description":11544},"개발자를 위한 Nuxt 4 아키텍처 구조 및 실무 적용 가이드","Nuxt 4의 핵심 아키텍처 변화, Nitro 엔진, SSR 렌더링 원리, 그리고 개발 과정에서 알아야 할 주의점과 SEO 최적화 팁","ko\u002F1.note\u002F2.nuxt4\u002F1.nuxt-info","ERo0Pema7is1XyxvelRXj2bv50Ratp4jzrhe0_xsw-4",{"id":11548,"title":75,"body":11549,"date":11289,"description":12060,"extension":188,"links":863,"meta":12061,"navigation":12066,"path":12067,"seo":12068,"stem":12071,"__hash__":12072},"docs_ko\u002Fko\u002F1.note\u002F3.nitro\u002Fnitro.md",{"type":180,"value":11550,"toc":12045},[11551,12042],[204,11552,11553,11557,11563,11566,11568,11572,11578,11592,11599,11602,11604,11608,11611,11625,11628,11630,11634,11641,11645,11648,11652,11662,11666,11680,11682,11686,11690,11697,11701,11708,11847,11849,11853,11860,11907,11921,12011,12013,12015],{":spotlight":206},[208,11554,11556],{"id":11555},"nuxt는-얹었을-뿐-독립형-자바스크립트-was-nitro","\"Nuxt는 얹었을 뿐\" — 독립형 자바스크립트 WAS, Nitro",[213,11558,11559,11560,2250],{},"많은 개발자가 Nitro를 단순히 \"Nuxt 프레임워크의 빌드 도구이자 부속품\" 정도로 인식한다. 그러나 Nitro의 진짜 정체는 현대 웹 생태계의 패러다임 변화(서버리스와 에지 컴퓨팅)에 맞추어 처음부터 완전히 독립적으로 설계된 ",[1641,11561,11562],{},"차세대 고성능 자바스크립트 웹 서버 엔진",[213,11564,11565],{},"Nuxt는 Nitro가 제공하는 강력한 백엔드 인프라 위에서 구동되는 수많은 애플리케이션 중 하나일 뿐이다. 독립된 서버 엔진으로서 Nitro가 가진 아키텍처적 본질과 독보적인 강점이 무엇인지 분석한다.",[232,11567],{},[481,11569,11571],{"id":11570},"_1-아키텍처적-본질-tomcat과-spring의-관계","1. 아키텍처적 본질: Tomcat과 Spring의 관계",[213,11573,11574,11575,5117],{},"Nitro의 본질을 가장 명확하게 이해할 수 있는 비교 대상은 바로 자바(Java) 생태계의 ",[1641,11576,11577],{},"Tomcat(톰캣)과 Spring(스프링)의 관계",[1649,11579,11580,11586],{},[1652,11581,11582,11585],{},[1641,11583,11584],{},"Tomcat(인프라 서버 엔진):"," 네트워크 소켓을 열고, HTTP 요청을 파싱하며, 서블릿 컨테이너를 구동하는 독립적인 WAS(웹 애플리케이션 서버)다.",[1652,11587,11588,11591],{},[1641,11589,11590],{},"Spring Boot(어플리케이션 프레임워크):"," 톰캣이 열어준 서버 환경 위에서 MVC 패턴, IoC\u002FDI 등 비즈니스 로직과 컴포넌트를 제어하는 프레임워크다.",[213,11593,11594,11595,11598],{},"자바스크립트 생태계에서 ",[1641,11596,11597],{},"배포와 인프라 서버 엔진의 역할은 Nitro","가 담당한다. HTTP 요청\u002F응답을 제어하고 가상 런타임 플랫폼을 빌드하는 백엔드 서버 엔진이 Nitro이며, 그 위에서 Vue 컴포넌트를 SSR하고 프론트엔드 상태 관리를 제어하는 프레임워크가 Nuxt다.",[213,11600,11601],{},"Nitro라는 강력한 서버 엔진이 완전히 독립되어 하부 레이어를 받쳐주고 있기에, Nuxt는 거친 백엔드 인프라 설정을 신경 쓰지 않고 풀스택 프레임워크로서의 완성도를 확보할 수 있다.",[232,11603],{},[481,11605,11607],{"id":11606},"_2-독보적인-강점-1-nodejs의-한계를-부수는-에지-퍼스트","2. 독보적인 강점 1: Node.js의 한계를 부수는 '에지 퍼스트'",[213,11609,11610],{},"과거 전통적인 자바스크립트 서버(Express, Koa 등)는 늘 무거운 Node.js 런타임을 서버에 통째로 띄워야 했다. 이는 가상 서버(VPC, EC2) 환경에서는 문제가 없었지만, 현대 클라우드 인프라에서는 두 가지 치명적인 한계에 부딪혔다.",[10082,11612,11613,11619],{},[1652,11614,11615,11618],{},[1641,11616,11617],{},"느린 초기 가동 속도 (Cold Start):"," 서버리스(AWS Lambda 등) 환경에서 요청이 올 때마다 무거운 서버 인프라를 준비하느라 지연이 발생한다.",[1652,11620,11621,11624],{},[1641,11622,11623],{},"에지(Edge) 런타임 구동 불가:"," Cloudflare Workers, Vercel Edge 같은 초경량 글로벌 에지 네트워크는 V8 엔진 기반의 아주 제한된 최소 API만 제공하므로, Node.js 내장 모듈에 의존하는 기존 서버는 아예 실행조차 불가능했다.",[213,11626,11627],{},"Nitro는 이 한계를 깨고 **\"Node.js에 의존하지 않는, 가벼우면서도 어디서나 실행 가능한 Universal 자바스크립트 서버\"**라는 독보적인 강점을 가진다. Node.js 종속성을 파괴하고 가상 런타임을 채택하여 에지 컴퓨팅 시대의 핵심 서버 엔진으로 자리 잡았다.",[232,11629],{},[481,11631,11633],{"id":11632},"_3-독보적인-강점-2-unjs-생태계-기반의-초경량-고성능-기술-군","3. 독보적인 강점 2: Unjs 생태계 기반의 초경량 고성능 기술 군",[213,11635,11636,11637,11640],{},"Nitro가 무거운 레거시 웹 서버들을 압도하고 초경량·고성능을 달성한 비결은 ",[1641,11638,11639],{},"Unjs(Universal JavaScript) 생태계","의 모듈들을 유기적으로 결합한 데 있다.",[2616,11642,11644],{"id":11643},"h3-high-performance-http-framework","⚡ h3 (High-performance HTTP Framework)",[213,11646,11647],{},"Express를 대체하는 Nitro의 미니멀한 심장이다. 고성능 HTTP 라우터로, 트리 셰이킹(Tree-shaking)이 완벽히 지원되어 쓰지 않는 모듈은 빌드 본에서 완전히 제외된다. Node.js 런타임이 없는 순수 V8 에지 환경에서도 완벽히 호환된다.",[2616,11649,11651],{"id":11650},"unstorage-universal-storage-layer","📦 unstorage (Universal Storage Layer)",[213,11653,11654,11655,907,11658,11661],{},"데이터 소스(Redis, 파일 시스템, 메모리, Cloudflare KV 등)가 무엇이든 간에, 소스코드 레벨에서는 항상 동일한 인터페이스(",[253,11656,11657],{},"getItem",[253,11659,11660],{},"setItem",")로 접근할 수 있게 해주는 스토리지 추상화 레이어다. 드라이버 설정 한 줄만 바꾸면 로컬 테스트용 메모리 캐시가 상용 환경의 클라우드 인프라로 즉시 전환된다.",[2616,11663,11665],{"id":11664},"️-rollup-기반의-단일-파일-번들링","🛠️ Rollup 기반의 단일 파일 번들링",[213,11667,11668,11669,11676,11677,11679],{},"Vite의 빌드 엔진이기도 한 Rollup을 사용하여 서버 코드 전체를 단 ",[1641,11670,11671,11672,11675],{},"하나의 최적화된 ",[253,11673,11674],{},".mjs"," 파일","로 압축한다. 수백 메가바이트에 달하는 ",[253,11678,4975],{}," 폴더를 서버에 업로드할 필요 없이, 이 파일 하나만 클라우드에 전송하면 즉시 서버가 가동되므로 가동 속도가 압도적으로 빠르다.",[232,11681],{},[481,11683,11685],{"id":11684},"_4-독보적인-강점-3-제로-컨피그-배포와-하이브리드-제어","4. 독보적인 강점 3: 제로 컨피그 배포와 하이브리드 제어",[2616,11687,11689],{"id":11688},"인프라를-가리지-않는-유니버설-타겟-zero-config-providers","🌐 인프라를 가리지 않는 유니버설 타겟 (Zero-Config Providers)",[213,11691,11692,11693,11696],{},"환경 변수나 설정 파일의 ",[253,11694,11695],{},"preset"," 옵션 하나만 지정하면, Nitro가 빌드 시점에 타겟 인프라(AWS, Vercel, Cloudflare, Netlify 등) 맞춤형 구동 래퍼(Wrapper) 코드를 자동으로 생성한다. 개발자는 인프라 종속적인 코드를 작성할 필요가 없다.",[2616,11698,11700],{"id":11699},"진화된-라우트-규칙-제어-route-rules","🔄 진화된 라우트 규칙 제어 (Route Rules)",[213,11702,11703,11704,11707],{},"Nitro는 페이지 유저의 접근 경로(Route)별로 렌더링 전략을 세부적으로 가로챌 수 있는 강력한 컨트롤 타워다. 정적 생성(SSG), 증분 정적 재생성(ISR), 클라이언트 사이드 렌더링(SPA)은 물론, 특정 API의 응답을 서버 레이어에서 캐싱하는 ",[253,11705,11706],{},"cache"," 옵션까지 코드 몇 줄로 완벽히 제어한다.",[245,11709,11712],{"className":5463,"code":11710,"filename":11711,"language":5466,"meta":178,"style":178},"\u002F\u002F nitro.config.ts (독립형 Nitro 서버 설정 예시)\nexport default defineNitroConfig({\n  routeRules: {\n    \u002F\u002F 빌드 시점에 미리 HTML 파일로 빌드 (SSG)\n    '\u002Fblog\u002F**': { prerender: true },\n    \u002F\u002F 1시간 동안 캐시를 유지하고 백그라운드에서 갱신 (ISR)\n    '\u002Fproducts\u002F**': { isr: 3600 },\n    \u002F\u002F 특정 API의 응답을 서버 레이어에서 10분간 강력 캐싱\n    '\u002Fapi\u002Fv1\u002Fbanners': { cache: { maxAge: 60 * 10 } }\n  }\n})\n","nitro.config.ts",[253,11713,11714,11719,11732,11740,11745,11766,11771,11793,11798,11837,11841],{"__ignoreMap":178},[389,11715,11716],{"class":391,"line":392},[389,11717,11718],{"class":3108},"\u002F\u002F nitro.config.ts (독립형 Nitro 서버 설정 예시)\n",[389,11720,11721,11723,11725,11728,11730],{"class":391,"line":186},[389,11722,1231],{"class":2526},[389,11724,2730],{"class":2526},[389,11726,11727],{"class":403}," defineNitroConfig",[389,11729,408],{"class":407},[389,11731,2341],{"class":399},[389,11733,11734,11736,11738],{"class":391,"line":2333},[389,11735,2746],{"class":2323},[389,11737,400],{"class":399},[389,11739,2751],{"class":399},[389,11741,11742],{"class":391,"line":2344},[389,11743,11744],{"class":3108},"    \u002F\u002F 빌드 시점에 미리 HTML 파일로 빌드 (SSG)\n",[389,11746,11747,11749,11751,11753,11755,11757,11760,11762,11764],{"class":391,"line":2356},[389,11748,2760],{"class":399},[389,11750,2763],{"class":2323},[389,11752,2535],{"class":399},[389,11754,400],{"class":399},[389,11756,2515],{"class":399},[389,11758,11759],{"class":2323}," prerender",[389,11761,400],{"class":399},[389,11763,8282],{"class":2352},[389,11765,7640],{"class":399},[389,11767,11768],{"class":391,"line":2364},[389,11769,11770],{"class":3108},"    \u002F\u002F 1시간 동안 캐시를 유지하고 백그라운드에서 갱신 (ISR)\n",[389,11772,11773,11775,11777,11779,11781,11783,11786,11788,11791],{"class":391,"line":2374},[389,11774,2760],{"class":399},[389,11776,3150],{"class":2323},[389,11778,2535],{"class":399},[389,11780,400],{"class":399},[389,11782,2515],{"class":399},[389,11784,11785],{"class":2323}," isr",[389,11787,400],{"class":399},[389,11789,11790],{"class":411}," 3600",[389,11792,7640],{"class":399},[389,11794,11795],{"class":391,"line":2379},[389,11796,11797],{"class":3108},"    \u002F\u002F 특정 API의 응답을 서버 레이어에서 10분간 강력 캐싱\n",[389,11799,11800,11802,11805,11807,11809,11811,11814,11816,11818,11821,11823,11826,11829,11832,11835],{"class":391,"line":2389},[389,11801,2760],{"class":399},[389,11803,11804],{"class":2323},"\u002Fapi\u002Fv1\u002Fbanners",[389,11806,2535],{"class":399},[389,11808,400],{"class":399},[389,11810,2515],{"class":399},[389,11812,11813],{"class":2323}," cache",[389,11815,400],{"class":399},[389,11817,2515],{"class":399},[389,11819,11820],{"class":2323}," maxAge",[389,11822,400],{"class":399},[389,11824,11825],{"class":411}," 60",[389,11827,11828],{"class":399}," *",[389,11830,11831],{"class":411}," 10",[389,11833,11834],{"class":399}," }",[389,11836,7700],{"class":399},[389,11838,11839],{"class":391,"line":2400},[389,11840,2793],{"class":399},[389,11842,11843,11845],{"class":391,"line":2406},[389,11844,2359],{"class":399},[389,11846,415],{"class":407},[232,11848],{},[481,11850,11852],{"id":11851},"_5-결론-nitro-단독standalone-프레임워크로서의-가치","5. 결론: 'Nitro 단독(Standalone)' 프레임워크로서의 가치",[213,11854,11855,11856,11859],{},"Nitro는 Nuxt라는 프론트엔드 배경 없이도, ",[1641,11857,11858],{},"오직 Nitro만 단독으로 설치해서 초경량 백엔드 API 마이크로서비스(MSA)를 구축","할 때 강력한 가치를 발휘한다.",[245,11861,11863],{"className":923,"code":11862,"filename":925,"language":926,"meta":178,"style":178},"# Nitro 단독 프로젝트 생성 및 실행\nnpx giget@latest nitro my-backend-api\ncd my-backend-api\nnpm install && npm run dev\n",[253,11864,11865,11870,11883,11890],{"__ignoreMap":178},[389,11866,11867],{"class":391,"line":392},[389,11868,11869],{"class":3108},"# Nitro 단독 프로젝트 생성 및 실행\n",[389,11871,11872,11874,11877,11880],{"class":391,"line":186},[389,11873,933],{"class":395},[389,11875,11876],{"class":936}," giget@latest",[389,11878,11879],{"class":936}," nitro",[389,11881,11882],{"class":936}," my-backend-api\n",[389,11884,11885,11888],{"class":391,"line":2333},[389,11886,11887],{"class":403},"cd",[389,11889,11882],{"class":936},[389,11891,11892,11894,11897,11899,11902,11904],{"class":391,"line":2344},[389,11893,3501],{"class":395},[389,11895,11896],{"class":936}," install",[389,11898,946],{"class":399},[389,11900,11901],{"class":395}," npm",[389,11903,1262],{"class":936},[389,11905,11906],{"class":936}," dev\n",[213,11908,11909,11910,11920],{},"단독 프로젝트로 구동할 경우, 프론트엔드 관련 오버헤드가 완전히 제거된 채 ",[1641,11911,11912,11913,907,11916,11919],{},"오직 순수한 파일 기반 API 라우터(",[253,11914,11915],{},"routes\u002F",[253,11917,11918],{},"api\u002F",") 구조만 가지고 작동하는 극도로 가벼운 독립형 백엔드 서버","를 얻게 된다.",[245,11922,11925],{"className":5463,"code":11923,"filename":11924,"language":5466,"meta":178,"style":178},"\u002F\u002F routes\u002Findex.ts (순수 Nitro 단독 API 핸들러)\nexport default defineEventHandler(() => {\n  return {\n    engine: \"Nitro Server Engine\",\n    status: \"Standalone\",\n    benefit: \"Node.js 독립성, 초경량 h3 라우터, 에지 인프라 즉시 배포\"\n  }\n})\n","routes\u002Findex.ts",[253,11926,11927,11932,11949,11955,11971,11987,12001,12005],{"__ignoreMap":178},[389,11928,11929],{"class":391,"line":392},[389,11930,11931],{"class":3108},"\u002F\u002F routes\u002Findex.ts (순수 Nitro 단독 API 핸들러)\n",[389,11933,11934,11936,11938,11941,11943,11945,11947],{"class":391,"line":186},[389,11935,1231],{"class":2526},[389,11937,2730],{"class":2526},[389,11939,11940],{"class":403}," defineEventHandler",[389,11942,408],{"class":407},[389,11944,9509],{"class":399},[389,11946,5509],{"class":454},[389,11948,2751],{"class":399},[389,11950,11951,11953],{"class":391,"line":2333},[389,11952,9940],{"class":2526},[389,11954,2751],{"class":399},[389,11956,11957,11960,11962,11964,11967,11969],{"class":391,"line":2344},[389,11958,11959],{"class":2323},"    engine",[389,11961,400],{"class":399},[389,11963,3605],{"class":399},[389,11965,11966],{"class":936},"Nitro Server Engine",[389,11968,1239],{"class":399},[389,11970,6450],{"class":399},[389,11972,11973,11976,11978,11980,11983,11985],{"class":391,"line":2356},[389,11974,11975],{"class":2323},"    status",[389,11977,400],{"class":399},[389,11979,3605],{"class":399},[389,11981,11982],{"class":936},"Standalone",[389,11984,1239],{"class":399},[389,11986,6450],{"class":399},[389,11988,11989,11992,11994,11996,11999],{"class":391,"line":2364},[389,11990,11991],{"class":2323},"    benefit",[389,11993,400],{"class":399},[389,11995,3605],{"class":399},[389,11997,11998],{"class":936},"Node.js 독립성, 초경량 h3 라우터, 에지 인프라 즉시 배포",[389,12000,3611],{"class":399},[389,12002,12003],{"class":391,"line":2374},[389,12004,2793],{"class":399},[389,12006,12007,12009],{"class":391,"line":2379},[389,12008,2359],{"class":399},[389,12010,415],{"class":407},[232,12012],{},[481,12014,7864],{"id":7863},[1649,12016,12017,12023,12036],{},[1652,12018,12019,12022],{},[1641,12020,12021],{},"본질:"," Nitro는 Nuxt의 하부 인프라를 책임지는 동시에, 언제든 따로 분리해 쓸 수 있는 **자바스크립트\u002F타입스크립트 진영의 독립형 웹 서버 엔진(WAS)**이다.",[1652,12024,12025,12028,12029,12031,12032,12035],{},[1641,12026,12027],{},"강점:"," 무거운 Node.js 환경을 탈피하고 초경량 가상 런타임(",[253,12030,2616],{},")을 채택하여 ",[1641,12033,12034],{},"에지 컴퓨팅 및 서버리스 환경에서 제로에 가까운 Cold Start 속도와 압도적인 렌더링 퍼포먼스","를 보장한다.",[1652,12037,12038,12041],{},[1641,12039,12040],{},"비전:"," 인프라 유연성(Zero-Config)과 강력한 캐싱 라우트 규칙을 제공함으로써, 현대 웹 아키텍처가 요구하는 풀스택\u002F마이크로서비스 서버의 표준을 제시한다.",[851,12043,12044],{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}",{"title":178,"searchDepth":186,"depth":186,"links":12046},[12047,12048,12049,12054,12058,12059],{"id":11570,"depth":186,"text":11571},{"id":11606,"depth":186,"text":11607},{"id":11632,"depth":186,"text":11633,"children":12050},[12051,12052,12053],{"id":11643,"depth":2333,"text":11644},{"id":11650,"depth":2333,"text":11651},{"id":11664,"depth":2333,"text":11665},{"id":11684,"depth":186,"text":11685,"children":12055},[12056,12057],{"id":11688,"depth":2333,"text":11689},{"id":11699,"depth":2333,"text":11700},{"id":11851,"depth":186,"text":11852},{"id":7863,"depth":186,"text":7864},"Nuxt 애플리케이션의 서버 런타임을 담당하는 Nitro의 구조와 서버 라우팅, 배포 추상화, 런타임 동작 방식을 분석",{"tags":12062,"image":12065,"imageClass":9352},[69,12063,12064,1699,7143],"Backend","Server-Engine","images\u002FUnjsNitro.svg",{"icon":26},"\u002Fko\u002Fnote\u002Fnitro\u002Fnitro",{"title":12069,"description":12070},"Nuxt Nitro 아키텍처 분석 | 서버 엔진 구조 이해","Nuxt Nitro가 서버 애플리케이션을 구성하는 방식과 다양한 환경에서 동작하는 서버 엔진 구조를 정리합니다.","ko\u002F1.note\u002F3.nitro\u002Fnitro","daGgOhMFT5iVuV27egyj5a9-CGBGMGABD1M4KBufxJw",{"id":12074,"title":111,"body":12075,"date":11289,"description":12340,"extension":188,"links":863,"meta":12341,"navigation":12343,"path":12344,"seo":12345,"stem":12348,"__hash__":12349},"docs_ko\u002Fko\u002F1.note\u002F4.web\u002F2.rendering-modes.md",{"type":180,"value":12076,"toc":12330},[12077],[204,12078,12079,12083,12087,12111,12115,12132,12136,12153,12157,12174,12178,12288,12292],{":spotlight":206},[481,12080,12082],{"id":12081},"_1-렌더링-모드별-핵심-매커니즘","🔍 1. 렌더링 모드별 핵심 매커니즘",[2616,12084,12086],{"id":12085},"csr-client-side-rendering","CSR (Client-Side Rendering)",[1649,12088,12089,12099,12105],{},[1652,12090,12091,12094,12095,12098],{},[1641,12092,12093],{},"구동 원리:"," 서버는 데이터가 없는 텅 빈 ",[253,12096,12097],{},"\u003Cbody>"," 태그와 자바스크립트(JS) 번들 파일 링크만 포함된 최소한의 HTML을 클라이언트에 내려준다. 브라우저가 이 JS 파일을 다운로드하고 실행하는 시점에 API 요청을 보내 데이터를 받아오고 화면을 동적으로 그려낸다.",[1652,12100,12101,12104],{},[1641,12102,12103],{},"장점:"," 첫 로딩 이후 페이지 이동 시 서버에 새 HTML을 요청하지 않고 필요한 데이터만 바꾸므로 앱처럼 매끄러운 사용자 경험(UX)을 제공한다. 서버 연산 부담이 가장 적다.",[1652,12106,12107,12110],{},[1641,12108,12109],{},"단점:"," 대용량 JS 파일을 모두 다운로드할 때까지 사용자는 흰 화면(Blank Page)을 보게 된다. HTML 뼈대가 비어있어 검색 엔진 봇이 콘텐츠를 긁어가지 못하므로 SEO(검색 엔진 최적화)에 매우 불리하다.",[2616,12112,12114],{"id":12113},"ssr-server-side-rendering","SSR (Server-Side Rendering)",[1649,12116,12117,12122,12127],{},[1652,12118,12119,12121],{},[1641,12120,12093],{}," 사용자가 특정 URL에 접속할 때마다 서버(Node.js 등)가 동적으로 데이터베이스나 API 서버에서 데이터를 긁어온다. 그 후 서버 메모리 상에서 HTML 뼈대에 데이터를 결합하여 완벽히 완성된 HTML 스트림을 빌드한 뒤 브라우저에 던져준다.",[1652,12123,12124,12126],{},[1641,12125,12103],{}," 브라우저가 HTML을 받자마자 화면을 바로 그릴 수 있어 초기 콘텐츠 표시 속도(FCP)가 매우 빠르다. 완성된 HTML을 제공하므로 크롤러 로봇이 페이지를 완벽하게 인식하여 SEO에 최적화된다.",[1652,12128,12129,12131],{},[1641,12130,12109],{}," 사용자가 페이지를 이동하거나 새로고침할 때마다 서버가 매번 실시간으로 화면을 새로 연산해서 빌드하므로, 트래픽이 몰리면 서버 부하가 급격히 증가하고 인프라 유지 비용이 커진다.",[2616,12133,12135],{"id":12134},"ssg-static-site-generation","SSG (Static Site Generation)",[1649,12137,12138,12143,12148],{},[1652,12139,12140,12142],{},[1641,12141,12093],{}," 사용자의 요청 시점이 아니라, 개발자가 코드를 빌드하고 배포하는 시점(Build Time)에 가용한 모든 페이지의 HTML을 미리 정적으로 구워둔다. 배포가 완료되면 서버는 이미 만들어진 HTML static 파일들을 CDN을 통해 유저에게 배달만 한다.",[1652,12144,12145,12147],{},[1641,12146,12103],{}," 서버가 실시간으로 화면을 연산할 필요가 전혀 없으므로 보안성이 극도로 높고 속도가 압도적으로 빠르다. AWS S3나 Netlify, Vercel 같은 정적 호스팅 환경이나 CDN 캐싱을 적극 활용할 수 있어 인프라 비용이 거의 제로에 가깝다.",[1652,12149,12150,12152],{},[1641,12151,12109],{}," 데이터가 변경되면 정적 파일들을 다시 만들기 위해 전체 프로젝트를 '다시 빌드하고 배포'해야 한다. 수만 개의 상품 페이지가 있는 커머스처럼 페이지 수가 너무 많으면 빌드하는 데 수 시간이 걸려 실시간 데이터 반영이 불가능하다.",[2616,12154,12156],{"id":12155},"isr-incremental-static-regeneration","ISR (Incremental Static Regeneration)",[1649,12158,12159,12164,12169],{},[1652,12160,12161,12163],{},[1641,12162,12093],{}," SSG의 치명적인 한계(실시간성 부족)를 극복하기 위해 등장한 하이브리드 방식이다. 전체 페이지를 빌드 시점에 미리 만들어두는 것은 SSG와 같지만, 각 페이지마다 유효 기간(예: 60초)을 설정한다. 유효 기간이 지난 후 유저가 방문하면 서버가 백그라운드에서 해당 페이지를 딱 한 번만 새로 빌드하여 정적 파일을 조용히 업데이트한다.",[1652,12165,12166,12168],{},[1641,12167,12103],{}," SSG의 압도적인 속도와 비용 절감 장점을 그대로 누리면서, 주기적으로 데이터가 자동으로 최신화되므로 대규모 블로그나 트래픽이 높은 뉴스 사이, 상품 목록 페이지에 이상적이다.",[1652,12170,12171,12173],{},[1641,12172,12109],{}," 첫 번째 유효 기간 만료 직후 방문한 사용자는 백그라운드에서 빌드가 도는 동안 '이전 버전(Stale)의 정적 화면'을 보게 되는 데이터 시차가 발생할 수 있다.",[481,12175,12177],{"id":12176},"_2-아키텍처-지표별-비교-매트릭스","📊 2. 아키텍처 지표별 비교 매트릭스",[588,12179,12180,12194],{},[591,12181,12182],{},[594,12183,12184,12186,12188,12190,12192],{},[597,12185,9264],{"align":4836},[597,12187,2084],{"align":4836},[597,12189,2095],{"align":4836},[597,12191,2106],{"align":4836},[597,12193,2117],{"align":4836},[604,12195,12196,12215,12233,12250,12269],{},[594,12197,12198,12203,12206,12209,12212],{},[609,12199,12200],{"align":4836},[1641,12201,12202],{},"HTML 생성 시점",[609,12204,12205],{"align":4836},"브라우저 런타임 (실시간)",[609,12207,12208],{"align":4836},"사용자 요청 시점 (On-Demand)",[609,12210,12211],{"align":4836},"프로젝트 빌드 타임 (사전 생성)",[609,12213,12214],{"align":4836},"빌드 타임 + 주기적 백그라운드 생성",[594,12216,12217,12222,12225,12228,12231],{},[609,12218,12219],{"align":4836},[1641,12220,12221],{},"초기 로딩 속도 (FCP)",[609,12223,12224],{"align":4836},"느림 (JS 로딩 대기)",[609,12226,12227],{"align":4836},"빠름",[609,12229,12230],{"align":4836},"매우 빠름",[609,12232,12230],{"align":4836},[594,12234,12235,12240,12243,12246,12248],{},[609,12236,12237],{"align":4836},[1641,12238,12239],{},"SEO 최적화 수준",[609,12241,12242],{"align":4836},"낮음 (자동 크롤링 제한)",[609,12244,12245],{"align":4836},"매우 높음",[609,12247,12245],{"align":4836},[609,12249,12245],{"align":4836},[594,12251,12252,12257,12260,12263,12266],{},[609,12253,12254],{"align":4836},[1641,12255,12256],{},"실시간 데이터 동기화",[609,12258,12259],{"align":4836},"완벽함 (클라이언트 통신)",[609,12261,12262],{"align":4836},"완벽함 (요청 시점 반영)",[609,12264,12265],{"align":4836},"불가능 (재배포 필요)",[609,12267,12268],{"align":4836},"우수함 (지정 주기 준수)",[594,12270,12271,12276,12279,12282,12285],{},[609,12272,12273],{"align":4836},[1641,12274,12275],{},"인프라 비용 및 부하",[609,12277,12278],{"align":4836},"매우 낮음",[609,12280,12281],{"align":4836},"매우 높음 (컴퓨팅 지속)",[609,12283,12284],{"align":4836},"가장 낮음 (CDN 호스팅)",[609,12286,12287],{"align":4836},"낮음 (간헐적 백그라운드 연산)",[481,12289,12291],{"id":12290},"_3-비즈니스-시나리오별-최적의-선택-가이드","💡 3. 비즈니스 시나리오별 최적의 선택 가이드",[1649,12293,12294,12303,12312,12321],{},[1652,12295,12296,12299,12300,12302],{},[1641,12297,12298],{},"대시보드 \u002F 사내 관리자 페이지 (Admin):"," 외부 검색 엔진에 노출될 필요가 전혀 없고, 사용자와의 잦은 인터랙션 및 실시간 데이터 차트 반영이 핵심이므로 ",[1641,12301,2084],{},"이 가장 적합하다.",[1652,12304,12305,12308,12309,12311],{},[1641,12306,12307],{},"커뮤니티 \u002F 마이페이지 \u002F 실시간 금융 정보:"," 사용자 로그인 상태에 따라 화면이 매번 다르게 바뀌어야 하고, 공유 링크나 보안, 실시간성이 생명인 영역은 ",[1641,12310,2095],{},"이 강제된다.",[1652,12313,12314,12317,12318,12320],{},[1641,12315,12316],{},"공식 가이드 문서 \u002F 기업 소개 \u002F 블로그:"," 데이터가 한 달에 몇 번 바뀔까 말까 하며, 전 세계 유저에게 초고속으로 문서를 뿌려야 하고 SEO가 최우선인 기술 위키나 도큐먼트 사이트는 ",[1641,12319,2106],{},"가 절대적인 정답이다.",[1652,12322,12323,12326,12327,12329],{},[1641,12324,12325],{},"대규모 커머스 상품 카탈로그 \u002F 실시간 뉴스 매체:"," 수천 수만 개의 정적 페이지 속도를 유지하면서 한 시간 혹은 하루 단위로 가격 정보나 기사를 업데이트해야 할 때는 ",[1641,12328,2117],{}," 아키텍처를 도입하는 것이 최선이다.",{"title":178,"searchDepth":186,"depth":186,"links":12331},[12332,12338,12339],{"id":12081,"depth":186,"text":12082,"children":12333},[12334,12335,12336,12337],{"id":12085,"depth":2333,"text":12086},{"id":12113,"depth":2333,"text":12114},{"id":12134,"depth":2333,"text":12135},{"id":12155,"depth":2333,"text":12156},{"id":12176,"depth":186,"text":12177},{"id":12290,"depth":186,"text":12291},"SSR, CSR, SSG, ISR 등 현대 웹 애플리케이션의 주요 렌더링 방식의 동작 원리와 특징, 상황별 선택 기준 정리",{"tags":12342},[3400,11539,2095,2084,2106,2117,10134],{"icon":26},"\u002Fko\u002Fnote\u002Fweb\u002Frendering-modes",{"title":12346,"description":12347},"SSR CSR SSG ISR 차이점과 웹 렌더링 방식 이해","SSR, CSR, SSG, ISR의 동작 원리와 장단점을 비교하고 웹 애플리케이션에서 적절한 렌더링 방식을 선택하는 기준을 설명합니다.","ko\u002F1.note\u002F4.web\u002F2.rendering-modes","mnW-LacOSOaYAJpTuF3N6-ZvwabBKAFFcjVPIcsS_BM",{"id":12351,"title":127,"body":12352,"date":11289,"description":12458,"extension":188,"links":863,"meta":12459,"navigation":12462,"path":12463,"seo":12464,"stem":12466,"__hash__":12467},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F1.outline.md",{"type":180,"value":12353,"toc":12452},[12354],[204,12355,12356,12364,12371,12373,12377,12380,12383,12386,12388,12392,12395,12398,12401,12403,12407,12410,12417,12420,12423,12430,12432,12435,12438,12441,12444,12447,12450],{":spotlight":206},[2395,12357,12360],{"className":12358},[4049,4050,12359],"my-8",[4052,12361],{"alt":12362,"src":4331,"width":12363},"Brolog Logo",250,[2125,12365,12366],{},[213,12367,12368,12370],{},[1641,12369,121],{},"는 개발 과정에서 배우는 내용과 문제 해결 과정을 기록하기 위해 시작한 개인 개발 블로그 프로젝트입니다.",[232,12372],{},[481,12374,12376],{"id":12375},"프로젝트-소개","📖 프로젝트 소개",[213,12378,12379],{},"Brolog는 개발하면서 남기고 싶은 기록들을 한곳에 정리하기 위해 만든 프로젝트다.",[213,12381,12382],{},"공부한 내용을 정리하거나, 기능을 구현하며 겪은 문제를 기록하고, 프로젝트를 진행하면서 했던 고민들을 꾸준히 남기는 것을 목표로 한다.",[213,12384,12385],{},"시간이 지난 뒤에도 다시 참고할 수 있는 개인 개발 아카이브가 되었으면 한다.",[232,12387],{},[481,12389,12391],{"id":12390},"개발-배경","🚀 개발 배경",[213,12393,12394],{},"원래는 LLM과 AI를 활용한 새로운 토이 프로젝트를 먼저 진행할 계획이었다.",[213,12396,12397],{},"하지만 프로젝트를 구상하는 과정에서 학습한 내용과 시행착오를 체계적으로 기록할 공간이 필요하다고 느꼈고, 이후 진행할 프로젝트들의 기반이 될 개인 블로그를 먼저 개발하게 되었다.",[213,12399,12400],{},"앞으로 진행할 다양한 프로젝트의 개발 과정과 기술적인 고민, 문제 해결 경험 역시 이곳에 함께 기록할 예정이다.",[232,12402],{},[481,12404,12406],{"id":12405},"개발-방향","🎯 개발 방향",[213,12408,12409],{},"Brolog는 처음부터 모든 기능을 직접 구현하는 것을 목표로 하지 않았다.",[213,12411,12412,12413,12416],{},"앞으로 진행할 프로젝트에 더 많은 시간을 투자하기 위해, 기본적인 블로그 구조는 ",[1641,12414,12415],{},"Docus Theme","를 활용하여 빠르게 구축했다.",[213,12418,12419],{},"반복적으로 필요한 초기 세팅에 시간을 소비하기보다, 검증된 구조를 기반으로 시작하고 필요한 기능만 직접 구현하는 방향을 선택했다.",[213,12421,12422],{},"프로젝트를 진행하면서 필요한 기능은 점진적으로 추가하고 개선해 나갈 예정이다.",[2125,12424,12425],{},[213,12426,12427],{},[1641,12428,12429],{},"반복되는 초기 세팅보다, 프로젝트 자체에 더 많은 시간을 투자한다.",[232,12431],{},[481,12433,12434],{"id":809},"💭 마무리",[213,12436,12437],{},"Brolog는 완성된 서비스를 만드는 것을 목표로 시작한 프로젝트가 아니다.",[213,12439,12440],{},"앞으로 진행할 다양한 프로젝트를 기록하고, 개발하면서 마주하는 문제와 해결 과정을 정리하기 위한 개인 아카이브에 가깝다.",[213,12442,12443],{},"프로젝트가 하나씩 늘어날수록 자연스럽게 기록도 함께 쌓이고, 시간이 지난 뒤에도 다시 참고할 수 있는 공간이 되었으면 한다.",[213,12445,12446],{},"또한 새로운 프로젝트를 시작할 때마다 반복되는 환경 구성에 시간을 소비하기보다, 이미 구축된 기반 위에서 개발에 더 집중할 수 있는 출발점의 역할도 함께 담당하게 될 것이다.",[213,12448,12449],{},"이곳에 작성되는 모든 글은 실제 프로젝트를 진행하며 경험한 내용을 바탕으로 작성하며, 잘된 결과뿐 아니라 시행착오와 고민했던 과정까지 함께 기록해 나갈 예정이다.",[232,12451],{},{"title":178,"searchDepth":186,"depth":186,"links":12453},[12454,12455,12456,12457],{"id":12375,"depth":186,"text":12376},{"id":12390,"depth":186,"text":12391},{"id":12405,"depth":186,"text":12406},{"id":809,"depth":186,"text":12434},"개발 기록을 위한 개인 블로그 프로젝트의 목적과 방향",{"tags":12460,"image":8536},[12461,9346,4397],"Blog",{"icon":26},"\u002Fko\u002Fproject\u002Fbrolog\u002Foutline",{"title":127,"description":12465},"Nuxt 4와 Nuxt Content 기반으로 개발하는 개인 개발 블로그 프로젝트 소개","ko\u002F2.project\u002F1.brolog\u002F1.outline","mZXjD90rlWMFP-9EcTUJCiZU1QjN5f6PkmvqpqVky3Y",{"id":12469,"title":131,"body":12470,"date":11289,"description":12631,"extension":188,"links":863,"meta":12632,"navigation":12634,"path":12635,"seo":12636,"stem":12637,"__hash__":12638},"docs_ko\u002Fko\u002F2.project\u002F1.brolog\u002F2.structure.md",{"type":180,"value":12471,"toc":12625},[12472],[204,12473,12474,12479,12485,12487,12491,12559,12561,12565,12585,12587,12591,12597,12599,12603,12623],{":spotlight":206},[2395,12475,12477],{"className":12476},[4049,4050,12359],[4052,12478],{"alt":12362,"src":4331,"width":12363},[2125,12480,12481],{},[213,12482,12483,12370],{},[1641,12484,121],{},[232,12486],{},[481,12488,12490],{"id":12489},"_1-프로젝트-정보","📌 1. 프로젝트 정보",[588,12492,12493,12502],{},[591,12494,12495],{},[594,12496,12497,12499],{},[597,12498,599],{},[597,12500,12501],{},"내용",[604,12503,12504,12512,12522,12532,12541,12550],{},[594,12505,12506,12510],{},[609,12507,12508],{},[1641,12509,4397],{},[609,12511,121],{},[594,12513,12514,12519],{},[609,12515,12516],{},[1641,12517,12518],{},"Type",[609,12520,12521],{},"Personal Development Blog",[594,12523,12524,12529],{},[609,12525,12526],{},[1641,12527,12528],{},"Started",[609,12530,12531],{},"2026.07",[594,12533,12534,12539],{},[609,12535,12536],{},[1641,12537,12538],{},"Framework",[609,12540,5976],{},[594,12542,12543,12548],{},[609,12544,12545],{},[1641,12546,12547],{},"Theme",[609,12549,12415],{},[594,12551,12552,12557],{},[609,12553,12554],{},[1641,12555,12556],{},"Content",[609,12558,4601],{},[232,12560],{},[481,12562,12564],{"id":12563},"_2-기술-스택","🛠 2. 기술 스택",[1649,12566,12567,12569,12571,12574,12577,12580,12582],{},[1652,12568,5976],{},[1652,12570,4601],{},[1652,12572,12573],{},"Nuxt UI 4",[1652,12575,12576],{},"Nuxt Image",[1652,12578,12579],{},"Tailwind CSS 4",[1652,12581,12415],{},[1652,12583,12584],{},"MCP Toolkit",[232,12586],{},[481,12588,12590],{"id":12589},"_3-구조","📂 3. 구조",[245,12592,12595],{"className":12593,"code":12594,"language":251,"meta":178},[248],"Brolog\n├── Tech\u002FArchitecture\n├── Projects\n├── Troubleshooting\n└── Archive\n",[253,12596,12594],{"__ignoreMap":178},[232,12598],{},[481,12600,12602],{"id":12601},"_4-앞으로-기록할-내용","📝 4. 앞으로 기록할 내용",[1649,12604,12605,12608,12611,12614,12617,12620],{},[1652,12606,12607],{},"프로젝트 개발 과정",[1652,12609,12610],{},"학습한 기술 및 개념 정리",[1652,12612,12613],{},"오류 및 예외 상황 해결 과정",[1652,12615,12616],{},"기능 구현 방식과 설계 과정",[1652,12618,12619],{},"개발하면서 고민했던 내용",[1652,12621,12622],{},"선택한 기술과 그 이유",[232,12624],{},{"title":178,"searchDepth":186,"depth":186,"links":12626},[12627,12628,12629,12630],{"id":12489,"depth":186,"text":12490},{"id":12563,"depth":186,"text":12564},{"id":12589,"depth":186,"text":12590},{"id":12601,"depth":186,"text":12602},"개발 기록을 위한 개인 블로그 프로젝트의 정보 및 기술스택",{"tags":12633,"image":8536},[12461,9346,4397],{"icon":26},"\u002Fko\u002Fproject\u002Fbrolog\u002Fstructure",{"title":131,"description":12631},"ko\u002F2.project\u002F1.brolog\u002F2.structure","wa-ygYaOig0UZxq9MnGUtUqu9E05U2xyUwdg2M3YR64",[12640],{"title":12641,"path":12642,"stem":12643,"children":12644,"page":27},"Ko","\u002Fko","ko",[12645,12697,12715],{"title":5,"icon":6,"path":12646,"stem":12647,"children":12648,"page":27},"\u002Fko\u002Fnote","ko\u002F1.note",[12649,12658,12664,12673,12678,12683,12692],{"title":11,"icon":12,"path":12650,"stem":12651,"children":12652,"page":27},"\u002Fko\u002Fnote\u002Fspring","ko\u002F1.note\u002F0.spring",[12653],{"title":17,"icon":18,"path":12654,"stem":12655,"children":12656,"page":27},"\u002Fko\u002Fnote\u002Fspring\u002Fspring-ai","ko\u002F1.note\u002F0.spring\u002F1.spring-ai",[12657],{"title":23,"path":11298,"stem":11302,"icon":26},{"title":29,"icon":30,"path":12659,"stem":12660,"children":12661,"page":27},"\u002Fko\u002Fnote\u002Fjavascript","ko\u002F1.note\u002F1.javascript",[12662,12663],{"title":35,"path":7149,"stem":7153,"icon":26},{"title":39,"path":7928,"stem":7932,"icon":26},{"title":43,"icon":44,"path":12665,"stem":12666,"children":12667,"page":27},"\u002Fko\u002Fnote\u002Fnuxt4","ko\u002F1.note\u002F2.nuxt4",[12668,12669,12670,12671,12672],{"title":11306,"path":11541,"stem":11545,"icon":26},{"title":53,"path":10137,"stem":10141,"icon":26},{"title":57,"path":5937,"stem":5941,"icon":26},{"title":61,"path":6256,"stem":6260,"icon":26},{"title":65,"path":3403,"stem":3407,"icon":26},{"title":69,"icon":70,"path":12674,"stem":12675,"children":12676,"page":27},"\u002Fko\u002Fnote\u002Fnitro","ko\u002F1.note\u002F3.nitro",[12677],{"title":75,"path":12067,"stem":12071,"icon":26},{"title":79,"icon":80,"path":12679,"stem":12680,"children":12681,"page":27},"\u002Fko\u002Fnote\u002Fdevops","ko\u002F1.note\u002F4.devops",[12682],{"title":85,"path":5327,"stem":5331,"icon":26},{"title":89,"icon":90,"path":12684,"stem":12685,"children":12686,"page":27},"\u002Fko\u002Fnote\u002Ftooling","ko\u002F1.note\u002F4.tooling",[12687],{"title":95,"icon":96,"path":12688,"stem":12689,"children":12690,"page":27},"\u002Fko\u002Fnote\u002Ftooling\u002Fpackage-manager","ko\u002F1.note\u002F4.tooling\u002F1.package-manager",[12691],{"title":101,"path":5034,"stem":5038,"icon":26},{"title":105,"icon":106,"path":12693,"stem":12694,"children":12695,"page":27},"\u002Fko\u002Fnote\u002Fweb","ko\u002F1.note\u002F4.web",[12696],{"title":111,"path":12344,"stem":12348,"icon":26},{"title":115,"icon":116,"path":12698,"stem":12699,"children":12700,"page":27},"\u002Fko\u002Fproject","ko\u002F2.project",[12701],{"title":121,"icon":122,"path":12702,"stem":12703,"children":12704,"page":27},"\u002Fko\u002Fproject\u002Fbrolog","ko\u002F2.project\u002F1.brolog",[12705,12706,12707,12708,12709,12710,12711,12712,12713,12714],{"title":127,"path":12463,"stem":12466,"icon":26},{"title":159,"path":1609,"stem":1613,"icon":26},{"title":155,"path":873,"stem":877,"icon":26},{"title":131,"path":12635,"stem":12637,"icon":26},{"title":135,"path":8538,"stem":8542,"icon":26},{"title":139,"path":8785,"stem":8789,"icon":26},{"title":143,"path":4603,"stem":4607,"icon":26},{"title":3918,"path":4223,"stem":4227,"icon":4222},{"title":147,"path":3910,"stem":3914,"icon":26},{"title":151,"path":2228,"stem":2232,"icon":26},{"title":163,"icon":164,"path":12716,"stem":12717,"children":12718,"page":27},"\u002Fko\u002Ftrouble-shooting","ko\u002F3.trouble-shooting",[12719],{"title":121,"icon":122,"path":12720,"stem":12721,"children":12722,"page":27},"\u002Fko\u002Ftrouble-shooting\u002Fbrolog","ko\u002F3.trouble-shooting\u002F1.brolog",[12723],{"title":173,"path":9354,"stem":9358,"icon":26},1785297528238]