<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>범데이의 개발노트  </title>
    <link>https://bumday.tistory.com/</link>
    <description>반갑습니다!
개발자 범데이입니다 :)</description>
    <language>ko</language>
    <pubDate>Sat, 11 Apr 2026 07:04:59 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>범데이</managingEditor>
    <image>
      <title>범데이의 개발노트  </title>
      <url>https://tistory1.daumcdn.net/tistory/3827257/attach/45101c7496444dbb80be4293e1d5bb01</url>
      <link>https://bumday.tistory.com</link>
    </image>
    <item>
      <title>[AI 자동화] 웹접근성(WA) 대응 - 3,300개 이미지 Alt 텍스트 생성 자동화</title>
      <link>https://bumday.tistory.com/290</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 문제 배경: &quot;이미지 alt를 다 달아야 한다&quot;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2026년 3월 21일 오후 11_19_22.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bowNra/dJMcagSoAJq/ZdAmQh9u1rx0ylAq2N4mLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bowNra/dJMcagSoAJq/ZdAmQh9u1rx0ylAq2N4mLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bowNra/dJMcagSoAJq/ZdAmQh9u1rx0ylAq2N4mLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbowNra%2FdJMcagSoAJq%2FZdAmQh9u1rx0ylAq2N4mLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;678&quot; height=&quot;1024&quot; data-filename=&quot;ChatGPT Image 2026년 3월 21일 오후 11_19_22.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;431&quot; data-start=&quot;408&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;431&quot; data-start=&quot;408&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;공공기관 웹페이지 고도화 작업을 진행하면서 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;거쳐야 했던 WA 인증(웹 접근성 인증) 과정이 있었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;여기서 요구하는 항목 중 하나가&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;431&quot; data-start=&quot;408&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;ldquo;&lt;b&gt;모든 이미지에 대체 텍스트(alt)를 제공해야 한다&lt;/b&gt;&amp;rdquo;는 것이었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;요구사항만 보면 이미지 설명을 작성하면 되는 단순한 작업처럼 보였다. &lt;/span&gt;&lt;br /&gt;&lt;span&gt;하지만 실제 데이터를 확인해보니 상황이 달랐다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이미지가 포함된 게시글이 약 911개였고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;전체 이미지 수는 약 3,300개였다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 순간 느낀 것은 하나였다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이건 사람이 일일이 처리하면 공수가 크게 발생하는 작업이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 초기 작업 방식 (ChatGPT 기반 수작업)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;772&quot; data-start=&quot;742&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;초기에는 ChatGPT를 활용하여 이미지를 하나씩 처리했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이미지를 복사하여 전달하고 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;해당 이미지에 대한 설명을 생성한 뒤 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;그 결과를 적용하는 방식이었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이미지 수가 적을 때는 문제가 없었지만 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;작업 수량이 늘어나면서 한계가 명확해졌다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;반복 작업으로 인한 피로도가 증가했고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;작업 속도 역시 크게 저하되었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 방식으로는 전체 작업을 처리하기 어렵다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. OpenAI API 결제 및 연동 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1086&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;작업을 진행하면서 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;ChatGPT를 단순히 웹에서 사용하는 것만으로는 한계가 있다는 것을 인지했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;특히 대량의 이미지를 자동으로 처리하기 위해서는 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;AI를 코드에서 직접 호출할 수 있는 구조가 필요했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 과정에서 OpenAI API 사용이 필요했고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;기존 ChatGPT Plus 구독과는 별도로 API 결제가 필요했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;작업 당시에는 일정 금액(약 5,000원 단위)을 설정해두고 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;사용량에 따라 비용이 차감되는 방식으로 사용했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이렇게 API를 사용할 수 있는 환경을 먼저 구성한 이후 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;본격적인 자동화 작업을 진행했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1086&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1086&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1086&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 4. 데이터 구조 분석 (OpenClaw 활용) &lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1351&quot; data-start=&quot;1330&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이후 접근 방식을 변경하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지 개별 처리 대신 전체 구조를 먼저 분석하기로 했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;OpenClaw를 활용하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;대표 홈페이지의 메뉴 구조를 분석하고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;게시판 형태의 메뉴를 식별한 뒤, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;각 게시판의 게시글 목록을 수집했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;그 중 이미지가 포함된 게시글을 추려낸 결과 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;총 911개의 게시글이 확인되었고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지 수는 약 3,300개로 집계되었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;또한 이 과정에서 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;게시글 URL, 이미지 URL, 기존 alt 텍스트를 함께 정리하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1351&quot; data-start=&quot;1330&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1351&quot; data-start=&quot;1330&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1351&quot; data-start=&quot;1330&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 5. 초기 자동화 시도 (Cron Job 기반) &lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_qfy9q1qfy9q1qfy9.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M06T5/dJMcaiWVzZE/G2R5ec6ISh7xhd4G6nbwK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M06T5/dJMcaiWVzZE/G2R5ec6ISh7xhd4G6nbwK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M06T5/dJMcaiWVzZE/G2R5ec6ISh7xhd4G6nbwK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM06T5%2FdJMcaiWVzZE%2FG2R5ec6ISh7xhd4G6nbwK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1376&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_qfy9q1qfy9q1qfy9.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;581&quot; data-start=&quot;545&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;데이터 구조를 정리한 이후 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;alt 텍스트 생성을 자동화하기 위한 시도를 먼저 진행했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;당시에는 Cron Job을 활용하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;일정 주기(약 10분 간격)로 API를 호출하고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지에 대한 대체 텍스트를 생성하도록 구성했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;하지만 실제로 운영해보니 문제가 발생했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;한 번에 많은 이미지(약 200개 단위)를 요청하면 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;결과 품질이 급격히 떨어졌고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지와 전혀 관련 없는 텍스트가 생성되거나 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;유사한 문장이 반복되는 형태가 나타났다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;그래서 요청 단위를 줄여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;20개씩 나누어 처리하는 방식으로 변경했지만, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이 경우 전체 요청 횟수가 크게 증가하면서 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;작업 효율이 떨어지는 문제가 있었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;또한 결과물을 확인해보면 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;웹 접근성 기준에 맞는 alt 텍스트로 보기 어려운 경우가 많았고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;결국 사람이 직접 검증하고 보정하는 과정이 필요했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 과정에서 느낀 점은 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;단순 자동 생성만으로는 품질을 확보하기 어렵고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;추가적인 튜닝이나 검수 구조가 반드시 필요하다는 것이었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;또한 반복 호출 과정에서 토큰 비용 역시 지속적으로 발생하고 있었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이러한 이유로 기존 방식은 유지하기 어렵다고 판단했고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;보다 통제 가능한 구조로 전환하기 위해 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;Cursor 기반의 스크립트 처리 방식으로 변경하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;581&quot; data-start=&quot;545&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;581&quot; data-start=&quot;545&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;77&quot; data-start=&quot;60&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 로컬 데이터 기반 처리 방식 전환&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_ymbxjxymbxjxymbx.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJ6SGz/dJMcagSoA8N/PCk5MU8Jp2nkK5cciWOy10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJ6SGz/dJMcagSoA8N/PCk5MU8Jp2nkK5cciWOy10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJ6SGz/dJMcagSoA8N/PCk5MU8Jp2nkK5cciWOy10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ6SGz%2FdJMcagSoA8N%2FPCk5MU8Jp2nkK5cciWOy10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1376&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_ymbxjxymbxjxymbx.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;395&quot; data-start=&quot;340&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 단계부터는 작업 방식을 완전히 바꿨다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;기존처럼 웹에 직접 접근해서 처리하는 방식이 아니라 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;모든 데이터를 로컬로 내려받아서 처리하는 구조로 전환했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 시점부터는 Cursor 기반으로 작업을 진행했고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;필요한 작업들은 Python 스크립트 형태로 구성하도록 했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;구현은 직접 하나씩 작성하기보다는 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;Cursor에 프롬프트를 입력해서 스크립트를 생성하는 방식으로 진행했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;게시글 HTML을 다운로드하고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지를 모두 내려받고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;게시판 번호와 이미지 번호 기준으로 데이터를 정리하는 작업까지 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;전부 Cursor에게 요청해서 처리했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이후에는 Python 스크립트에서 OpenAI API를 연동하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;ChatGPT에 요청하는 것과 동일한 방식으로 alt 텍스트를 생성하도록 구성했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;게시글 제목과 본문 내용을 함께 전달하고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;로컬에 저장된 이미지를 함께 전달하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지 단독이 아닌 게시글 맥락을 기반으로 alt 텍스트를 생성하도록 했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 과정 역시 프롬프트를 구성하여 요청하고 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;그 결과를 받아 저장하는 방식으로 처리했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;또한 이후 작업에 활용할 수 있도록 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;해당 데이터들을 JSON 형태로 구조화하도록 구성했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이후부터는 웹이 아닌 로컬 데이터를 기준으로 작업하게 되었고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;작업 속도와 안정성이 확실히 개선되기 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;581&quot; data-start=&quot;545&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;581&quot; data-start=&quot;545&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;427&quot; data-start=&quot;402&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 자동 생성 구조 구성&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_w6dqaiw6dqaiw6dq (1).png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL2qol/dJMcahcGkK7/1liBRwvqUMkKZBdp6pkAoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL2qol/dJMcahcGkK7/1liBRwvqUMkKZBdp6pkAoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL2qol/dJMcahcGkK7/1liBRwvqUMkKZBdp6pkAoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL2qol%2FdJMcahcGkK7%2F1liBRwvqUMkKZBdp6pkAoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1376&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_w6dqaiw6dqaiw6dq (1).png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;760&quot; data-start=&quot;693&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로컬 데이터가 준비된 이후에&lt;/span&gt;&lt;span&gt;는 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지에 대한 대체 텍스트를 자동으로 생성하는 단계로 넘어갔다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 과정 역시 Cursor를 활용하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;Python 스크립트 기반으로 구성했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;로컬에 저장된 이미지와 게시글 HTML, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;그리고 게시글 제목 등의 정보를 함께 활용하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지에 대한 설명을 생성하도록 했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;실제 대체 텍스트 생성은 OpenAI API를 호출하는 방식으로 처리했고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;스크립트 내부에서 이미지와 문맥 정보를 함께 전달하도록 구성했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이를 통해 단순 이미지 설명이 아닌 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;게시글 맥락을 반영한 결과를 얻을 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;760&quot; data-start=&quot;693&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;760&quot; data-start=&quot;693&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;781&quot; data-start=&quot;767&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 이미지 유형에 따른 처리 방식 분리&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_8m286m8m286m8m28.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cW3G2w/dJMcabp14Ov/uTqP6D2I1VfeNz50KikmlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cW3G2w/dJMcabp14Ov/uTqP6D2I1VfeNz50KikmlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cW3G2w/dJMcabp14Ov/uTqP6D2I1VfeNz50KikmlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcW3G2w%2FdJMcabp14Ov%2FuTqP6D2I1VfeNz50KikmlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_8m286m8m286m8m28.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;973&quot; data-start=&quot;939&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;초기 생성 결과를 확인해보니 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지마다 결과 품질의 편차가 있었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;원인을 분석해보니 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이미지의 성격이 서로 달랐다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;그래서 이미지를 두 가지 유형으로 구분했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;사진형 이미지는 상황 중심으로 간단하게 설명하고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;정보형 이미지는 이미지 내 텍스트나 내용을 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;누락 없이 설명하도록 처리했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 기준을 적용한 이후 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;대체 텍스트의 품질이 전반적으로 안정되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;973&quot; data-start=&quot;939&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;973&quot; data-start=&quot;939&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;994&quot; data-start=&quot;980&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;9. 병렬 처리 적용&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_mb5b52mb5b52mb5b.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzuD1M/dJMcacPZhbO/KobBNZVNrceoxnvlXy8jaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzuD1M/dJMcacPZhbO/KobBNZVNrceoxnvlXy8jaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzuD1M/dJMcacPZhbO/KobBNZVNrceoxnvlXy8jaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzuD1M%2FdJMcacPZhbO%2FKobBNZVNrceoxnvlXy8jaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;400&quot; data-filename=&quot;Gemini_Generated_Image_mb5b52mb5b52mb5b.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1166&quot; data-start=&quot;1105&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전체 이미지 수가 많다 보니 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;순차 처리 방식으로는 작업 시간이 과도하게 소요되었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이를 해결하기 위해 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;Python 스크립트에 병렬 처리 구조를 적용했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;작업 단위를 나누고 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;동시에 여러 요청을 처리하도록 구성하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;전체 처리 시간을 단축할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1166&quot; data-start=&quot;1105&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1166&quot; data-start=&quot;1105&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1188&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;10. 검수 단계 및 도구 구성 &lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_8tq52g8tq52g8tq5.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFMybx/dJMcaaLpc9C/EZU1pAkwf6uH1YeWvfsG00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFMybx/dJMcaaLpc9C/EZU1pAkwf6uH1YeWvfsG00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFMybx/dJMcaaLpc9C/EZU1pAkwf6uH1YeWvfsG00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFMybx%2FdJMcaaLpc9C%2FEZU1pAkwf6uH1YeWvfsG00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1376&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_8tq52g8tq52g8tq5.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1446&quot; data-start=&quot;1413&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span&gt;자동 생성 이후 결과를 확인해보니 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;일부 문구에서 어색하거나 정보가 부족한 경우가 있었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 부분은 자동화로 해결하기 어렵다고 판단했고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;별도의 검수 과정을 구성했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;다만 3,300개의 이미지를 하나씩 확인하는 방식은 비효율적이었기 때문에 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;검수 역시 도구 형태로 구성했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;Cursor를 활용하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;Python 기반의 간단한 검수 도구를 제작했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이미지와 생성된 대체 텍스트를 동시에 확인할 수 있도록 구성하고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이전/다음 이동이 가능하도록 키 바인딩을 추가했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이를 통해 마우스 조작 없이 빠르게 이미지를 넘기면서 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;대체 텍스트를 확인할 수 있도록 했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;또한 각 이미지에 대해 피드백을 입력할 수 있도록 구성하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;부족한 설명이나 잘못된 내용을 기록하고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이 피드백을 이후 재생성 과정에서 참고할 수 있도록 했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;게시판 번호, 이미지 번호, 게시글 정보까지 함께 확인할 수 있도록 구성하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;실제 원문과 비교하면서 검수가 가능하도록 했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 도구를 활용하여 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;전체 이미지에 대한 검수를 진행했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1446&quot; data-start=&quot;1413&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 캡처 2026-03-12 140146.png&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLohXF/dJMcaadAChg/FTKJ1K4kHTTG4ZAbi7ktnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLohXF/dJMcaadAChg/FTKJ1K4kHTTG4ZAbi7ktnk/img.png&quot; data-alt=&quot;Python으로 구성한 검수 툴. (좌측은 이미지, 우측 상단은 작성된 alt내용, 우측하단은 피드백 입력 구간이다.)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLohXF/dJMcaadAChg/FTKJ1K4kHTTG4ZAbi7ktnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLohXF%2FdJMcaadAChg%2FFTKJ1K4kHTTG4ZAbi7ktnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1560&quot; height=&quot;920&quot; data-filename=&quot;화면 캡처 2026-03-12 140146.png&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Python으로 구성한 검수 툴. (좌측은 이미지, 우측 상단은 작성된 alt내용, 우측하단은 피드백 입력 구간이다.)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1468&quot; data-start=&quot;1453&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;11. &lt;span&gt;최종 작업 완료 및 비용&lt;/span&gt;&lt;br /&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q3H1Z/dJMcafMJVOn/S8RNUZbKY0p4jkVLlwQRWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q3H1Z/dJMcafMJVOn/S8RNUZbKY0p4jkVLlwQRWk/img.png&quot; data-alt=&quot;총 사용된 예산, 토큰, 요청 수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q3H1Z/dJMcafMJVOn/S8RNUZbKY0p4jkVLlwQRWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ3H1Z%2FdJMcafMJVOn%2FS8RNUZbKY0p4jkVLlwQRWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;390&quot; height=&quot;471&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;총 사용된 예산, 토큰, 요청 수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1565&quot; data-start=&quot;1521&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1565&quot; data-start=&quot;1521&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결과적으로 약 3,300개의 이미지에 대해 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;대체 텍스트 생성과 검수를 모두 완료했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;생성은 자동화로 처리했지만, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;최종 품질은 직접 확인하는 방식으로 마무리했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;전체 작업에 소요된 AI 비용은 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;약 32,000원 수준이었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;수작업 대비 작업 시간을 크게 줄일 수 있었고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;대량 반복 작업에 대한 효율적인 처리 구조를 구축할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1565&quot; data-start=&quot;1521&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1565&quot; data-start=&quot;1521&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1589&quot; data-start=&quot;1572&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;12. 정리 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1721&quot; data-start=&quot;1675&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 작업은 단순히 이미지 alt를 작성하는 작업이 아니라 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;대량 반복 작업을 어떻게 구조화할 것인지에 대한 과정이었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;웹 기반 처리에서 로컬 데이터 기반 처리로 전환한 것이 핵심이었고, &lt;/span&gt;&lt;br /&gt;&lt;span&gt;AI와 스크립트를 결합하여 자동화 구조를 만들었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;또한 자동 생성 결과를 그대로 사용하는 것이 아니라 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;검수 단계를 통해 품질을 보완하는 방식으로 진행했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 방식은 향후 유사한 작업에도 &lt;/span&gt;&lt;br /&gt;&lt;span&gt;적용 가능한 구조로 활용할 수 있을 것으로 판단된다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Record/IT Diary</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/290</guid>
      <comments>https://bumday.tistory.com/290#entry290comment</comments>
      <pubDate>Sat, 21 Mar 2026 23:41:50 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble Shooting] 지도 포함 리포트 이미지 삽입 문제 해결</title>
      <link>https://bumday.tistory.com/289</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_vdub4fvdub4fvdub.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6S6D2/dJMcaiCD48M/Pce4rTLbgWjYgVwqqKh0Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6S6D2/dJMcaiCD48M/Pce4rTLbgWjYgVwqqKh0Nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6S6D2/dJMcaiCD48M/Pce4rTLbgWjYgVwqqKh0Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6S6D2%2FdJMcaiCD48M%2FPce4rTLbgWjYgVwqqKh0Nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1376&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_vdub4fvdub4fvdub.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;465&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;최근에 참여했던 프로젝트에서 출장 여비정산 리포트를 개발 과업 중에&lt;/p&gt;
&lt;p data-end=&quot;465&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;주행거리 증빙 화면(지도 포함)&amp;rdquo;을 &lt;b&gt;이미지로 캡쳐해 ClipReport에 삽입&lt;/b&gt;해야 하는 과정이 있었다.&lt;/p&gt;
&lt;p data-end=&quot;481&quot; data-start=&quot;467&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;481&quot; data-start=&quot;467&quot; data-ke-size=&quot;size16&quot;&gt;요구사항은 단순해 보였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;577&quot; data-start=&quot;483&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;514&quot; data-start=&quot;483&quot;&gt;지도 화면이 반드시 포함된 상태로 캡쳐되어야 하고&lt;/li&gt;
&lt;li data-end=&quot;544&quot; data-start=&quot;515&quot;&gt;매번 다른 주행 경로를 동적으로 캡쳐해야 하며&lt;/li&gt;
&lt;li data-end=&quot;577&quot; data-start=&quot;545&quot;&gt;최종 결과는 리포트 안에서 이미지로 출력되어야 했다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;603&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 구현은 예상보다 훨씬 복잡했다.&lt;/p&gt;
&lt;p data-end=&quot;603&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;629&quot; data-start=&quot;605&quot; data-ke-size=&quot;size16&quot;&gt;이번 이슈는 아래와 같은 환경에서 발생했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;794&quot; data-start=&quot;631&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;657&quot; data-start=&quot;631&quot;&gt;&lt;b&gt;Report:&lt;/b&gt; ClipReport&lt;/li&gt;
&lt;li data-end=&quot;681&quot; data-start=&quot;658&quot;&gt;&lt;b&gt;Frontend:&lt;/b&gt; React&lt;/li&gt;
&lt;li data-end=&quot;712&quot; data-start=&quot;682&quot;&gt;&lt;b&gt;Backend:&lt;/b&gt; Spring (Java)&lt;/li&gt;
&lt;li data-end=&quot;763&quot; data-start=&quot;713&quot;&gt;&lt;b&gt;Screenshot:&lt;/b&gt; Playwright (Headless Chromium)&lt;/li&gt;
&lt;li data-end=&quot;794&quot; data-start=&quot;764&quot;&gt;&lt;b&gt;Map SDK:&lt;/b&gt; Kakao Map SDK&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;828&quot; data-start=&quot;796&quot; data-ke-size=&quot;size16&quot;&gt;그리고 문제는 단순히 &amp;ldquo;캡쳐가 안 된다&amp;rdquo; 수준이 아니었다.&lt;/p&gt;
&lt;p data-end=&quot;828&quot; data-start=&quot;796&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;851&quot; data-start=&quot;830&quot; data-ke-size=&quot;size16&quot;&gt;이슈는 크게 세 가지 층으로 나뉘었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;946&quot; data-start=&quot;853&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;871&quot; data-start=&quot;853&quot;&gt;&lt;span&gt;브라우저 렌더링 방식의 한계&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;919&quot; data-start=&quot;872&quot;&gt;서버 OS &amp;harr; Playwright(Chromium) &amp;harr; Java 패키지 호환&lt;/li&gt;
&lt;li data-end=&quot;946&quot; data-start=&quot;920&quot;&gt;지도 SDK 도메인(URL) 매핑 문제&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;998&quot; data-start=&quot;948&quot; data-ke-size=&quot;size16&quot;&gt;그리고 이 복잡한 문제들을 하나로 관통한 키워드는 &lt;b&gt;로깅(Logging)&lt;/b&gt; 이었다.&lt;/p&gt;
&lt;p data-end=&quot;123&quot; data-start=&quot;99&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;p data-end=&quot;123&quot; data-start=&quot;99&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 문제 배경: &amp;ldquo;지도 포함 주행거리 증빙 캡쳐&amp;rdquo; 요구사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 목표는 아래처럼 정리할 수 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 URL(증빙 페이지)을 로드한다&lt;/li&gt;
&lt;li&gt;페이지가 렌더링 완료되면 스크린샷을 찍는다&lt;/li&gt;
&lt;li&gt;생성된 이미지 파일을 ClipReport에서 출력한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말만 보면 쉬운데, &amp;ldquo;지도(카카오맵/타일)&amp;rdquo;이 끼어들면서 난이도가 급상승했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 1차 시도: 프론트(html2canvas) 캡쳐 &amp;mdash; 구조적 한계로 실패 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 React 화면에서 html2canvas 기반으로 캡쳐해서 증빙 이미지를 만들려고 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 시도한 방식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;html2canvas(contentRef.current) 로 화면을 캡쳐&lt;/li&gt;
&lt;li&gt;캡쳐된 이미지를 다운로드 또는 서버 업로드&lt;/li&gt;
&lt;li&gt;리포트에서 해당 이미지 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 문제: &amp;ldquo;보이는 그대로 캡쳐&amp;rdquo;가 아니다&lt;/h3&gt;
&lt;p data-end=&quot;376&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;html2canvas와 같은 라이브러리는&lt;br /&gt;브라우저 화면을 실제로 그대로 캡쳐하는 방식이 아니다.&lt;/p&gt;
&lt;p data-end=&quot;442&quot; data-start=&quot;378&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;442&quot; data-start=&quot;378&quot; data-ke-size=&quot;size16&quot;&gt;canvas 위에 현재 렌더링된 HTML / CSS 구조를 분석해서&lt;br /&gt;&amp;ldquo;&lt;b&gt;비슷하게 보이도록 다시 그리는 방식&lt;/b&gt;&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-end=&quot;442&quot; data-start=&quot;378&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;474&quot; data-start=&quot;444&quot; data-ke-size=&quot;size16&quot;&gt;이 구조적인 특성 때문에 다음과 같은 문제가 발생했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;610&quot; data-start=&quot;476&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;513&quot; data-start=&quot;476&quot;&gt;일부 UI 요소 (특히 input, form 요소)가 틀어짐&lt;/li&gt;
&lt;li data-end=&quot;535&quot; data-start=&quot;514&quot;&gt;스타일이 완벽하게 재현되지 않음&lt;/li&gt;
&lt;li data-end=&quot;578&quot; data-start=&quot;536&quot;&gt;외부 스크립트 기반 요소(지도 SDK 등)가 정상적으로 그려지지 않음&lt;/li&gt;
&lt;li data-end=&quot;610&quot; data-start=&quot;579&quot;&gt;지도 영역 및 타일 이미지가 제대로 표현되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;658&quot; data-start=&quot;612&quot; data-ke-size=&quot;size16&quot;&gt;실제로 여러 캡쳐 라이브러리를 테스트해봤지만&lt;br /&gt;공통적으로 동일한 문제가 발생했다.&lt;/p&gt;
&lt;p data-end=&quot;658&quot; data-start=&quot;612&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-08 210514.png&quot; data-origin-width=&quot;1133&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd1aur/dJMb99ZGjjT/rzk1RR9VLtRdjqfR9q4fGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd1aur/dJMb99ZGjjT/rzk1RR9VLtRdjqfR9q4fGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd1aur/dJMb99ZGjjT/rzk1RR9VLtRdjqfR9q4fGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd1aur%2FdJMb99ZGjjT%2Frzk1RR9VLtRdjqfR9q4fGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1133&quot; height=&quot;643&quot; data-filename=&quot;스크린샷 2026-01-08 210514.png&quot; data-origin-width=&quot;1133&quot; data-origin-height=&quot;643&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;658&quot; data-start=&quot;612&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;666&quot; data-start=&quot;660&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로, &amp;nbsp;&amp;ldquo;&lt;b&gt;프론트에서 보이는 화면을 그대로 캡쳐한다&lt;/b&gt;&amp;rdquo;는 접근 자체가&lt;br /&gt;기술적으로 완전한 재현을 보장하지 못한다.&lt;/p&gt;
&lt;p data-end=&quot;666&quot; data-start=&quot;660&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;774&quot; data-start=&quot;731&quot; data-ke-size=&quot;size16&quot;&gt;특히 지도와 같은 외부 렌더링 요소가 포함되면&lt;br /&gt;정확한 결과를 얻기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론&lt;/b&gt;: &amp;ldquo;프론트에서 보이는 화면을 캡쳐&amp;rdquo; 방식은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도/외부 리소스가 포함되면 구조적으로 실패할 가능성이 높다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 방향을 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 방향 전환: 서버에서 페이지를 직접 요청/렌더링해서 캡쳐하자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 캡쳐 방식의 한계를 확인했기 때문에, &lt;b&gt;서버가 Headless Chromium을 실행해서 캡쳐&lt;/b&gt;하도록 구조를 전환했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 Playwright로 브라우저를 띄우고&lt;/li&gt;
&lt;li&gt;해당 URL에 접속해 렌더링한 뒤&lt;/li&gt;
&lt;li&gt;렌더링 결과를 스크린샷으로 저장한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 &amp;ldquo;보이는 그대로&amp;rdquo;를 캡쳐하기에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 4. 2차 장애: 서버 OS &amp;harr; Playwright 버전/Chromium 호환 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 Playwright를 올리고 실행하는 순간, 다음과 같은 문제가 발생했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chromium이 정상 구동되지 않거나&lt;/li&gt;
&lt;li&gt;OS 지원 경고가 뜨거나&lt;/li&gt;
&lt;li&gt;fallback build 다운로드로 인해 불안정하게 동작했다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 포인트는 &amp;ldquo;코드가 잘못됐다&amp;rdquo;가 아니라:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 OS 환경과 Playwright/Chromium 빌드가 맞지 않으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 자체가 실행되지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 해결: Playwright 버전 다운그레이드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이 문제는 &lt;b&gt;Playwright 버전을 낮추는 방향&lt;/b&gt;으로 해결했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최신 버전은 서버 OS(또는 glibc 등)와 호환이 애매했고&lt;/li&gt;
&lt;li&gt;낮춘 버전에서는 Chromium이 정상 실행되었다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 결과:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지 접속 성공&lt;/li&gt;
&lt;li&gt;스크린샷 생성 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 오면 끝일 줄 알았지만, 진짜 문제는 다음 단계였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 5. 3차 장애: &amp;ldquo;캡쳐는 되는데 지도 SDK가 안 뜬다&amp;rdquo;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chromium이 뜨고 캡쳐도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 결과 이미지에 &lt;b&gt;지도만 안 나온다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지는 정상&lt;/li&gt;
&lt;li&gt;텍스트/레이아웃도 정상&lt;/li&gt;
&lt;li&gt;그런데 지도 영역만 비어 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점부터는 코드 문제가 아니라 &lt;b&gt;환경/네트워크/보안/SDK 정책&lt;/b&gt; 영역으로 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 이때부터 결정적으로 중요해진 것: 로깅(Logging)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터는 감으로 때려맞추면 시간이 10배 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 했던 핵심은:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 서버 네트워크 레벨 검증&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 curl 로 해당 URL 접근 가능한지 확인&lt;/li&gt;
&lt;li&gt;지도 SDK 스크립트 주소 접근 가능한지 확인&lt;/li&gt;
&lt;li&gt;외부망 라우팅/DNS/방화벽 이슈 가능성 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 Playwright 브라우저 로그 출력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 단에서만 보이는 에러가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 로그를 찍었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;console log&lt;/li&gt;
&lt;li&gt;requestfailed&lt;/li&gt;
&lt;li&gt;response status&lt;/li&gt;
&lt;li&gt;network trace&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 통해 결론적으로 파악한 핵심은:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도 SDK는 &amp;ldquo;등록된 URL(도메인)&amp;rdquo;에서만 정상 동작한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 근본 원인: 지도 SDK는 허용된 URL/도메인이 일치해야 로드된다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 서버 캡쳐 환경에서는 다음과 같은 차이가 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원래 SDK 호출 기준 URL이 내부망 기반(172.x.x.x)으로 잡혀 있었고&lt;/li&gt;
&lt;li&gt;로컬/외부에서 접근할 때는 외부망 IP로 호출되었다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도 SDK는 도메인 정책이 강해서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;등록된 URL과 다르면&lt;/li&gt;
&lt;li&gt;SDK가 로드되지 않거나 일부 기능이 차단된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 8. 해결: 지도 SDK가 인식하는 URL을 &amp;ldquo;등록된 주소로 통일&amp;rdquo; (URL 매핑)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 해결책은 단순했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 지도 SDK를 호출할 때도&lt;/li&gt;
&lt;li&gt;SDK에 등록된 URL과 동일한 형태로 인식되도록&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부망 IP &amp;rarr; 외부망 IP 기반 호출로 변경&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;로컬 호출 환경과 동일한 IP/도메인 기준으로 통일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 결과:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 Playwright 환경에서도 지도 SDK 정상 로드&lt;/li&gt;
&lt;li&gt;지도까지 포함된 증빙 화면 캡쳐 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUEM3N/dJMcaaddfNz/U0rd0hBYZ17CveaFdR1UCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUEM3N/dJMcaaddfNz/U0rd0hBYZ17CveaFdR1UCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUEM3N/dJMcaaddfNz/U0rd0hBYZ17CveaFdR1UCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUEM3N%2FdJMcaaddfNz%2FU0rd0hBYZ17CveaFdR1UCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;776&quot; height=&quot;463&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;9. 최종 결론: 이번 이슈의 핵심 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 &amp;ldquo;기술&amp;rdquo;이 여러 층으로 겹쳐져 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) &lt;span&gt;프론트 캡쳐 방식은 구조적 한계가 존재한다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;html2canvas와 같은 방식은 실제 캡쳐가 아니라 재렌더링이기 때문에&lt;/span&gt;&lt;br /&gt;&lt;span&gt;지도/외부 렌더링 요소 포함 시 정확한 결과를 보장하기 어렵다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) 서버 캡쳐는 OS 호환성이 승패를 가른다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;Playwright 최신이 항상 정답이 아니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;서버 OS / glibc / 패키지 호환성이 맞아야 Chromium이 뜬다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 지도 SDK는 &amp;ldquo;URL 정책&amp;rdquo; 때문에 예상치 못하게 실패한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;SDK는 허용 도메인 기반으로 동작한다&lt;/span&gt;&lt;br /&gt;&lt;span&gt;서버에서 띄우면 host/origin이 달라져서 SDK가 막힐 수 있다&lt;/span&gt;&lt;br /&gt;&lt;span&gt;해결은 &amp;ldquo;등록된 URL로 통일(매핑)&amp;rdquo;이다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) 결정타는 로깅이었다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;curl로 네트워크 확인&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Playwright에서 console/network 로깅&lt;/span&gt;&lt;br /&gt;&lt;span&gt;이게 없었으면 &amp;ldquo;지도 안 뜨는 이유&amp;rdquo;는 끝까지 감으로만 맞췄을 것이다&lt;/span&gt;&lt;/p&gt;</description>
      <category>Record/Trubble Shooting</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/289</guid>
      <comments>https://bumday.tistory.com/289#entry289comment</comments>
      <pubDate>Wed, 18 Feb 2026 01:26:54 +0900</pubDate>
    </item>
    <item>
      <title>Viewport(뷰포트) 란?</title>
      <link>https://bumday.tistory.com/288</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹페이지 반응형 작업을 하면서 이런 일이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;293&quot; data-start=&quot;246&quot; data-ke-size=&quot;size16&quot;&gt;미디어쿼리(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@media&lt;/span&gt;)를 분명히 작성했는데&lt;br /&gt;모바일에서 전혀 적용되지 않았다.&lt;/p&gt;
&lt;p data-end=&quot;293&quot; data-start=&quot;246&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;357&quot; data-start=&quot;295&quot; data-ke-size=&quot;size16&quot;&gt;개발자도구(Device Toolbar)에서는 정상처럼 보이는데&lt;br /&gt;실제 모바일에서는 레이아웃이 줄어들지 않았다.&lt;/p&gt;
&lt;p data-end=&quot;357&quot; data-start=&quot;295&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;405&quot; data-start=&quot;359&quot; data-ke-size=&quot;size16&quot;&gt;원인을 확인해보니 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;meta name=&quot;viewport&quot;&amp;gt;&lt;/span&gt; 설정이 빠져 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1771343850643&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;531&quot; data-start=&quot;489&quot; data-ke-size=&quot;size16&quot;&gt;이 한 줄을 추가하자 그동안 먹지 않던 미디어쿼리가 바로 정상 동작했다.&lt;/p&gt;
&lt;p data-end=&quot;547&quot; data-start=&quot;533&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;547&quot; data-start=&quot;533&quot; data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;왜 viewport를 넣어야 반응형이 제대로 작동하는 건지 &lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;547&quot; data-start=&quot;533&quot; data-ke-size=&quot;size16&quot;&gt;그 내용을 정리해보려고 한다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Viewport 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;699&quot; data-start=&quot;615&quot; data-ke-size=&quot;size16&quot;&gt;Viewport는 사용자가 현재 보고 있는 &lt;b&gt;웹 페이지의 보이는 영역&lt;/b&gt;이다.&lt;br /&gt;즉, 브라우저가 레이아웃을 계산할 때 기준으로 삼는 화면 영역이다.&lt;/p&gt;
&lt;p data-end=&quot;699&quot; data-start=&quot;615&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;726&quot; data-start=&quot;701&quot; data-ke-size=&quot;size16&quot;&gt;문제는 모바일 브라우저의 기본 동작 방식이다.&lt;/p&gt;
&lt;p data-end=&quot;726&quot; data-start=&quot;701&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;834&quot; data-start=&quot;728&quot; data-ke-size=&quot;size16&quot;&gt;viewport 설정이 없으면&lt;br /&gt;모바일 브라우저는 과거 PC 웹을 그대로 보여주기 위해&lt;br /&gt;넓은 화면 기준으로 페이지를 먼저 렌더링한 뒤&lt;br /&gt;이를 모바일 화면 크기에 맞게 축소해 보여준다.&lt;/p&gt;
&lt;p data-end=&quot;834&quot; data-start=&quot;728&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;898&quot; data-start=&quot;836&quot; data-ke-size=&quot;size16&quot;&gt;이 상태에서는 CSS가 &lt;b&gt;실제 기기 너비 기준이 아니라, 브라우저가 설정한 넓은 기준값&lt;/b&gt;을 토대로 계산된다.&lt;/p&gt;
&lt;p data-end=&quot;898&quot; data-start=&quot;836&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;898&quot; data-start=&quot;836&quot; data-ke-size=&quot;size16&quot;&gt;그래서 내가 작성한&lt;/p&gt;
&lt;pre id=&quot;code_1771344137615&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@media (max-width: 768px)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;910&quot; data-start=&quot;900&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;979&quot; data-start=&quot;950&quot; data-ke-size=&quot;size16&quot;&gt;같은 코드가 모바일에서 조건을 만족하지 못하게 된다.&lt;/p&gt;
&lt;p data-end=&quot;987&quot; data-start=&quot;981&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1054&quot; data-start=&quot;989&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1007&quot; data-start=&quot;989&quot;&gt;미디어쿼리가 적용되지 않고&lt;/li&gt;
&lt;li data-end=&quot;1026&quot; data-start=&quot;1008&quot;&gt;레이아웃이 줄어들지 않으며&lt;/li&gt;
&lt;li data-end=&quot;1054&quot; data-start=&quot;1027&quot;&gt;반응형이 깨진 것처럼 보이는 현상이 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;910&quot; data-start=&quot;900&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Viewport는 반응형 웹의 출발점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1167&quot; data-start=&quot;1088&quot; data-ke-size=&quot;size16&quot;&gt;반응형 웹이 제대로 작동하려면&lt;br /&gt;미디어쿼리(media query)나 CSS 작성보다 먼저&lt;br /&gt;&lt;b&gt;Viewport 설정이 선행되어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1167&quot; data-start=&quot;1088&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1231&quot; data-start=&quot;1169&quot; data-ke-size=&quot;size16&quot;&gt;Viewport를 지정하면 브라우저는&lt;br /&gt;&amp;ldquo;이 페이지를 기기 화면 크기를 기준으로 렌더링하라&amp;rdquo;는 신호를 받는다.&lt;/p&gt;
&lt;p data-end=&quot;1231&quot; data-start=&quot;1169&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1276&quot; data-start=&quot;1233&quot; data-ke-size=&quot;size16&quot;&gt;그제서야 미디어쿼리는&lt;br /&gt;실제 디바이스 너비를 기준으로 조건을 판단하게 된다.&lt;/p&gt;
&lt;p data-end=&quot;1276&quot; data-start=&quot;1233&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1280&quot; data-start=&quot;1278&quot; data-ke-size=&quot;size16&quot;&gt;즉, Viewport가 없으면 반응형 조건이 어긋나고 Viewport를 선언해야 비로소 반응형이 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기본 Viewport 설정&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1771344192154&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #dddddd;&quot;&gt;width=device-width&lt;/span&gt;: 뷰포트 너비를 기기 화면 너비와 동일하게 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #dddddd;&quot;&gt;initial-scale=1&lt;/span&gt;: 페이지 처음 로딩 시 확대/축소 비율을 1:1로 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 거의 모든 반응형 웹에서 기본값처럼 사용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQ3Rdo/dJMcadOsI4S/csXS0K7TL2sHFjDXyAl4Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQ3Rdo/dJMcadOsI4S/csXS0K7TL2sHFjDXyAl4Kk/img.png&quot; data-alt=&quot;viewport 사용 참고: 한국식품안전관리인증원 대표누리집&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQ3Rdo/dJMcadOsI4S/csXS0K7TL2sHFjDXyAl4Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQ3Rdo%2FdJMcadOsI4S%2FcsXS0K7TL2sHFjDXyAl4Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1211&quot; height=&quot;351&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;351&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;viewport 사용 참고: 한국식품안전관리인증원 대표누리집&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;추가 Viewport Meta 속성들&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 추가 속성들도 활용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #dddddd;&quot;&gt;user-scalable&lt;/span&gt;: 사용자가 확대/축소 허용 여부 지정(yes/no)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #dddddd;&quot;&gt;minimum-scale&lt;/span&gt;: 최소 확대 비율&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #dddddd;&quot;&gt;maximum-scale&lt;/span&gt;: 최대 확대 비율&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예: &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, user-scalable=no&quot;&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;b&gt;user-scalable=no&lt;/b&gt; 는 접근성 측면에서 사용자의 화면 확대를 막기 때문에 주의가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미디어쿼리가 먹지 않는다면 Viewport부터 확인&lt;/li&gt;
&lt;li&gt;모바일 브라우저는 기본적으로 넓은 화면 기준으로 렌더링&lt;/li&gt;
&lt;li&gt;Viewport 선언이 있어야 실제 기기 너비 기준으로 계산&lt;/li&gt;
&lt;li&gt;반응형의 출발점은 CSS가 아니라 Viewport 설정&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>FrontEnd</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/288</guid>
      <comments>https://bumday.tistory.com/288#entry288comment</comments>
      <pubDate>Wed, 18 Feb 2026 01:06:37 +0900</pubDate>
    </item>
    <item>
      <title>웹 UI 기본 내비게이션 용어 완전 정리: GNB, LNB, SNB, FNB</title>
      <link>https://bumday.tistory.com/287</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹 개발하거나 사이트 구조를 공부하다 보면 &lt;b&gt;내비게이션 메뉴 용어(GNB, LNB, SNB, FNB)&lt;/b&gt;를 마주하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자는 물론, 프론트엔드/풀스택 개발자에게도 놓치기 쉬운 UI 기본 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 이 내비게이션 개념들을 한눈에 파악할 수 있게 정리해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 설명을 쉽게 하기 위해, 공공기관 사이트인 한국식품안전관리인증원(&lt;a href=&quot;https://www.haccp.or.kr/main.do&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.haccp.or.kr/main.do&lt;/a&gt;) 웹사이트를 실제 예시로 사용한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사이트는 식품 안전관리 인증 관련 정보를 제공하는 공공기관 홈페이지로, 다양한 내비게이션 구조가 잘 드러나는 대표적인 사례이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;웹 내비게이션 바란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹사이트에서 사용자가 목적한 콘텐츠로 쉽게 이동하도록 돕는 UI 메뉴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 이 메뉴를 통해 서비스 기능/페이지를 탐색하고 웹서비스를 빠르게 이해한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 UI에서 내비게이션은 위치에 따라 구분 명칭이 다르며, 각각의 역할이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. GNB (Global Navigation Bar)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 사이트의 최상단 공통 메뉴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 페이지 어디에서든 항상 보이는 메인 내비게이션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 웹사이트에서 상단에 항상 보이는 메뉴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 서비스 흐름을 보여주는 역할을 하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 &quot;정보공개, 경용공시, 국민소통&quot; 과 같은 전체 메뉴 리스트가 여기에 해당된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1559&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ljabG/dJMcac27oaL/gzee51zLIYKUTx3u4CbNlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ljabG/dJMcac27oaL/gzee51zLIYKUTx3u4CbNlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ljabG/dJMcac27oaL/gzee51zLIYKUTx3u4CbNlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FljabG%2FdJMcac27oaL%2Fgzee51zLIYKUTx3u4CbNlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1559&quot; height=&quot;668&quot; data-origin-width=&quot;1559&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; GNB는 Global이라는 이름처럼 &lt;b&gt;웹 전체를 아우르는 핵심 내비게이션&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. LNB (Local Navigation Bar)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 특정 페이지나 섹션에서 세부 메뉴 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 보통 GNB에서 메뉴를 클릭/호버하면 나타나는 서브 메뉴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LNB는 GNB와 달리 특정 페이지/영역에만 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 GNB에서 메뉴를 선택했을 때, &lt;b&gt;그 아래 보여지는 세부 카테고리&lt;/b&gt;라고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이, &quot;정보공개&quot; 클릭 &amp;rarr; 또 다른 관련 메뉴(정보공개제도/공공데이터/공무국외출장 등) 가 LNB로 나타나는 구조이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;633&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8suau/dJMcaaK1KJG/4Hvcty3LFKB4toDrcfolmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8suau/dJMcaaK1KJG/4Hvcty3LFKB4toDrcfolmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8suau/dJMcaaK1KJG/4Hvcty3LFKB4toDrcfolmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8suau%2FdJMcaaK1KJG%2F4Hvcty3LFKB4toDrcfolmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1546&quot; height=&quot;633&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;633&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. SNB (Side Navigation Bar)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 측면 서브 메뉴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 특정 페이지 내에서 추가적인 메뉴 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 왼쪽 또는 오른쪽에 추가로 붙는 메뉴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 서비스의 &lt;b&gt;콘텐츠 카테고리/필터 영역&lt;/b&gt;으로 활용되며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 내부 상세 탐색을 돕는 구조이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 &quot;정보공개 &amp;gt; 정보공개제도 &amp;gt; 제도안내&quot;&amp;nbsp; 같은 계층 메뉴가 이에 속한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1553&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zixvy/dJMcai3i4ET/IoC3wWiufM5PbyGIIqInA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zixvy/dJMcai3i4ET/IoC3wWiufM5PbyGIIqInA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zixvy/dJMcai3i4ET/IoC3wWiufM5PbyGIIqInA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzixvy%2FdJMcai3i4ET%2FIoC3wWiufM5PbyGIIqInA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1553&quot; height=&quot;684&quot; data-origin-width=&quot;1553&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. FNB (Foot Navigation Bar)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 웹사이트 가장 아래 푸터 영역 메뉴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 홈페이지 전체에서 공통으로 표시되는 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;푸터(Footer) 영역의 내비게이션이 바로 FNB이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 기관 주소, 연락처, 개인정보 처리방침 등 부가 정보와 관련된 링크들이 여기 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1690&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uph8e/dJMcaaYyprk/dgxEtT1YfwopE1IinyUsQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uph8e/dJMcaaYyprk/dgxEtT1YfwopE1IinyUsQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uph8e/dJMcaaYyprk/dgxEtT1YfwopE1IinyUsQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuph8e%2FdJMcaaYyprk%2FdgxEtT1YfwopE1IinyUsQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1690&quot; height=&quot;678&quot; data-origin-width=&quot;1690&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 구조를 이해하려면 내비게이션 구조를 먼저 파악하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI/UX 설계, 웹페이지 구조 분석, 프론트엔드 개발까지 모두 이 개념을 기반으로 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- GNB부터 FNB까지 영역별 용어를 정확히 알고 있으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서비스 설계/협업 커뮤니케이션에서 훨씬 빠르게 이해할 수 있다.&lt;/p&gt;</description>
      <category>FrontEnd</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/287</guid>
      <comments>https://bumday.tistory.com/287#entry287comment</comments>
      <pubDate>Wed, 18 Feb 2026 00:46:04 +0900</pubDate>
    </item>
    <item>
      <title>Unity Android 빌드 실패: Google Mobile Ads 업데이트 후 IL2CPP Internal CLR Error 해결기</title>
      <link>https://bumday.tistory.com/286</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctf8Hg/dJMcajnkETm/XbyxoDsPOsGGueclDlGLG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctf8Hg/dJMcajnkETm/XbyxoDsPOsGGueclDlGLG0/img.png&quot; data-alt=&quot;주기적으로 개발자들을 괴롭히는 SDK 업데이트 권고사항..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctf8Hg/dJMcajnkETm/XbyxoDsPOsGGueclDlGLG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fctf8Hg%2FdJMcajnkETm%2FXbyxoDsPOsGGueclDlGLG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;789&quot; height=&quot;343&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;343&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주기적으로 개발자들을 괴롭히는 SDK 업데이트 권고사항..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 중인 &lt;b&gt;Google Mobile Ads(GMA)&lt;/b&gt; SDK 버전이 sunset에 접어들면서 SDK를 버전업했을 뿐인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unity Android 빌드에서 IL2CPP Internal CLR error (0x80131506) 가 발생하며 빌드가 완전히 막혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 문제의 정체는 GMA 자체가 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 변경 이후 Unity/EDM/Bee 캐시가 꼬인 상태에서 증분 빌드* 가 재사용된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;*증분 빌드: 이전 빌드 결과를 재사용해서, 변경된 부분만 다시 빌드하는 방식&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 초기화 + unitypackage 재임포트 + Force Resolve로 해결하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 문제 발생 배경&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unity 2021.3 LTS 프로젝트&lt;/li&gt;
&lt;li&gt;Android 빌드 대상&lt;/li&gt;
&lt;li&gt;기존에 정상 동작하던 프로젝트&lt;/li&gt;
&lt;li&gt;변경 사항은 단 하나&amp;nbsp; &amp;rarr; Google Mobile Ads SDK 버전 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 발생한 증상&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Android + IL2CPP 빌드 실패&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 도중 아래와 같은 에러가 반복적으로 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;793&quot; data-start=&quot;758&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Incremental Player build failed&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;843&quot; data-start=&quot;794&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Failed to create CoreCLR, HRESULT: 0x8007000E&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;893&quot; data-start=&quot;844&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Fatal error. Internal CLR error. (0x80131506)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1025&quot; data-start=&quot;894&quot;&gt;실패 지점:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1025&quot; data-start=&quot;905&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;956&quot; data-start=&quot;905&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;GoogleMobileAds.Ump.Android-FeaturesChecked.txt&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1002&quot; data-start=&quot;959&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;UnityEngine.*Module-FeaturesChecked.txt&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1025&quot; data-start=&quot;1005&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;il2cpp.exe&lt;/span&gt; 실행 단계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징적인 포인트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1069&quot; data-start=&quot;1043&quot;&gt;Mono로는 Android 빌드 성공&lt;/li&gt;
&lt;li data-end=&quot;1088&quot; data-start=&quot;1070&quot;&gt;IL2CPP에서만 실패&lt;/li&gt;
&lt;li data-end=&quot;1118&quot; data-start=&quot;1089&quot;&gt;캐시 삭제, 재빌드, 재부팅을 반복해도 동일 증상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이 시점에서 Gradle/Android 설정 문제는 아님이 명확해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 해결 방법 (실제로 효과 있었던 절차)&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 프로젝트 캐시 완전 삭제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unity 종료 후, 프로젝트 루트에서 아래 폴더들을 모두 삭제&lt;/p&gt;
&lt;pre id=&quot;code_1767793554446&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Library/
Temp/
obj/
Logs/   (선택)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;Library는 Unity 캐시이므로 삭제해도 안전하며, 재실행 시 자동 재생성된다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) Google Mobile Ads / EDM 관련 폴더 제거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Assets/&lt;/span&gt; 하위에서 다음 폴더들을 삭제:&lt;/p&gt;
&lt;pre id=&quot;code_1767793588623&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Assets/GoogleMobileAds/
Assets/ExternalDependencyManager/
Assets/Plugins/Android/   (GMA 관련 aar/jar가 있다면)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 이전 의존성 산출물을 완전히 제거하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) Unity 재실행 &amp;rarr; GMA 재임포트&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qgymF/dJMcacogkt1/LNvYyszvWf3AVr6Ra48jrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qgymF/dJMcacogkt1/LNvYyszvWf3AVr6Ra48jrk/img.png&quot; data-alt=&quot;Unity 재실행 시 위와같은 팝업이 나오면 'Ignore' 로 진입했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qgymF/dJMcacogkt1/LNvYyszvWf3AVr6Ra48jrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqgymF%2FdJMcacogkt1%2FLNvYyszvWf3AVr6Ra48jrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;397&quot; height=&quot;225&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Unity 재실행 시 위와같은 팝업이 나오면 'Ignore' 로 진입했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1710&quot; data-start=&quot;1666&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Assets &amp;rarr; Import Package &amp;rarr; Custom Package&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1767&quot; data-start=&quot;1711&quot;&gt;최신 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;GoogleMobileAds-x.x.x.unitypackage&lt;/span&gt; 전체 선택 후 Import&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;1009&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHSBXV/dJMcahpAhbw/pzDzAglkVzWPizzu20EYy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHSBXV/dJMcahpAhbw/pzDzAglkVzWPizzu20EYy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHSBXV/dJMcahpAhbw/pzDzAglkVzWPizzu20EYy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHSBXV%2FdJMcahpAhbw%2FpzDzAglkVzWPizzu20EYy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;483&quot; height=&quot;832&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;1009&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) External Dependency Manager 강제 리졸브&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unity 메뉴에서:&lt;/p&gt;
&lt;pre id=&quot;code_1767793647068&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Assets
 &amp;rarr; External Dependency Manager
   &amp;rarr; Android Resolver
     &amp;rarr; Force Resolve&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5) Android 빌드 재시도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2082&quot; data-start=&quot;2051&quot;&gt;Scripting Backend: &lt;b&gt;IL2CPP&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2095&quot; data-start=&quot;2083&quot;&gt;ARM64 only&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 빌드 성공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 근본 원인은 무엇인지?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) GMA 버전업 &amp;rarr; Android 의존성 그래프 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GMA 업데이트 시 함께 바뀌는 것들:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2388&quot; data-start=&quot;2295&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2316&quot; data-start=&quot;2295&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;play-services-ads&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2350&quot; data-start=&quot;2317&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;user-messaging-platform (UMP)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2388&quot; data-start=&quot;2351&quot;&gt;AndroidX / transitive dependency 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2418&quot; data-start=&quot;2390&quot; data-ke-size=&quot;size16&quot;&gt;즉, Gradle 기준 의존성 트리가 달라진다.&lt;/p&gt;
&lt;p data-end=&quot;2418&quot; data-start=&quot;2390&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2418&quot; data-start=&quot;2390&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2418&quot; data-start=&quot;2390&quot; data-ke-size=&quot;size23&quot;&gt;2) EDM 리졸브 결과와 Unity 캐시(Bee/Library)가 불일치&lt;/h3&gt;
&lt;p data-end=&quot;2492&quot; data-start=&quot;2472&quot; data-ke-size=&quot;size16&quot;&gt;Unity는 다음을 강하게 캐싱한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2573&quot; data-start=&quot;2494&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2521&quot; data-start=&quot;2494&quot;&gt;Bee Incremental Build 산출물&lt;/li&gt;
&lt;li data-end=&quot;2548&quot; data-start=&quot;2522&quot;&gt;Library 내부 Feature 체크 파일&lt;/li&gt;
&lt;li data-end=&quot;2573&quot; data-start=&quot;2549&quot;&gt;이전 Android Resolver 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2594&quot; data-start=&quot;2575&quot; data-ke-size=&quot;size16&quot;&gt;이 상태에서 의존성만 바뀌면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2626&quot; data-start=&quot;2595&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2610&quot; data-start=&quot;2595&quot;&gt;일부는 &lt;b&gt;옛 의존성&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2626&quot; data-start=&quot;2611&quot;&gt;일부는 &lt;b&gt;새 의존성&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2650&quot; data-start=&quot;2628&quot; data-ke-size=&quot;size16&quot;&gt;이라는 &lt;b&gt;정합성 붕괴 상태&lt;/b&gt;가 된다.&lt;/p&gt;
&lt;p data-end=&quot;2650&quot; data-start=&quot;2628&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2650&quot; data-start=&quot;2628&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2650&quot; data-start=&quot;2628&quot; data-ke-size=&quot;size23&quot;&gt;3) IL2CPP 단계에서 폭발&lt;/h3&gt;
&lt;p data-end=&quot;2690&quot; data-start=&quot;2682&quot; data-ke-size=&quot;size16&quot;&gt;IL2CPP는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2729&quot; data-start=&quot;2691&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2706&quot; data-start=&quot;2691&quot;&gt;방대한 C++ 코드 생성&lt;/li&gt;
&lt;li data-end=&quot;2729&quot; data-start=&quot;2707&quot;&gt;메타데이터/어셈블리 조합에 매우 민감&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2748&quot; data-start=&quot;2731&quot; data-ke-size=&quot;size16&quot;&gt;입력이 꼬인 상태에서 실행되면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2808&quot; data-start=&quot;2749&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2764&quot; data-start=&quot;2749&quot;&gt;CoreCLR 생성 실패&lt;/li&gt;
&lt;li data-end=&quot;2785&quot; data-start=&quot;2765&quot;&gt;Internal CLR Error&lt;/li&gt;
&lt;li data-end=&quot;2808&quot; data-start=&quot;2786&quot;&gt;Incremental Build 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2839&quot; data-start=&quot;2810&quot; data-ke-size=&quot;size16&quot;&gt;처럼 원인과 전혀 달라 보이는 에러로 터진다.&lt;/p&gt;
&lt;p data-end=&quot;2839&quot; data-start=&quot;2810&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;즉, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;IL2CPP는 &lt;b&gt;범인이 아니라 피해자&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;였다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2839&quot; data-start=&quot;2810&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2839&quot; data-start=&quot;2810&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2839&quot; data-start=&quot;2810&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2839&quot; data-start=&quot;2810&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;5. 해결된 이유&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2942&quot; data-start=&quot;2926&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;캐시 삭제 + 재임포트 + Force Resolve&quot;&lt;/b&gt; 과정이&amp;nbsp;한 일을 정리하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3059&quot; data-start=&quot;2944&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2974&quot; data-start=&quot;2944&quot;&gt;잘못 재사용되던 Bee/Library 캐시 제거&lt;/li&gt;
&lt;li data-end=&quot;2996&quot; data-start=&quot;2975&quot;&gt;이전 unitypackage들의 산출물 제거&lt;/li&gt;
&lt;li data-end=&quot;3029&quot; data-start=&quot;2997&quot;&gt;현재 상태 기준으로 의존성 그래프를 처음부터 재계산&lt;/li&gt;
&lt;li data-end=&quot;3059&quot; data-start=&quot;3030&quot;&gt;Unity&amp;ndash;EDM&amp;ndash;Gradle 간 정합성 복구&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3094&quot; data-start=&quot;3061&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 빌드 파이프라인이 정상 경로로 복구되었다.&lt;/p&gt;
&lt;p data-end=&quot;3094&quot; data-start=&quot;3061&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3094&quot; data-start=&quot;3061&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3094&quot; data-start=&quot;3061&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;3094&quot; data-start=&quot;3061&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 교훈&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;301&quot; data-start=&quot;93&quot; data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 다시 한 번 느낀 점은, 의존성은 결코 가볍게 볼 수 있는 요소가 아니라는 것이다. Java의 Maven, C#의 NuGet, Node.js의 package.json처럼, Unity 역시 다양한 패키지와 라이브러리들이 서로 유기적으로 연결된 상태에서 하나의 프로젝트를 구성하고 있다. Unity 프로젝트라고 해서 이 복잡성에서 예외는 아니었다.&lt;/p&gt;
&lt;p data-end=&quot;301&quot; data-start=&quot;93&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;475&quot; data-start=&quot;303&quot; data-ke-size=&quot;size16&quot;&gt;Google Mobile Ads처럼 외부 SDK 하나를 업데이트하는 작업은 단순한 버전 변경이 아니라, 그 패키지가 의존하고 있는 다른 라이브러리들과 빌드 파이프라인 전반에 영향을 주는 변화가 된다. 겉으로는 작은 수정처럼 보여도, 내부적으로는 의존성 그래프가 달라지면서 예상치 못한 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;475&quot; data-start=&quot;303&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;636&quot; data-start=&quot;477&quot; data-ke-size=&quot;size16&quot;&gt;그래서 패키지 업데이트는 항상 신중해야 하며, 업데이트 이후에는 의존성이 정상적으로 리졸브되고 전체 빌드 흐름이 안정적으로 동작하는지 반드시 확인해야 한다. 이번 이슈는 단순한 에러 해결을 넘어, 의존성을 관리하는 태도와 검증 과정의 중요성을 다시 한 번 상기시켜 준 사례였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Unity</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/286</guid>
      <comments>https://bumday.tistory.com/286#entry286comment</comments>
      <pubDate>Wed, 7 Jan 2026 23:05:14 +0900</pubDate>
    </item>
    <item>
      <title>CSS 우선순위 - 실무에서 바로 쓰는 적용 팁</title>
      <link>https://bumday.tistory.com/285</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;CSS 우선순위를 이해했다고 해서 실전에서 모든 충돌을 해결할 수 있는 건 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 수십~수백 개의 CSS 파일이 섞여 있는 레거시 프로젝트에서는 &quot;어디서 온 스타일인지 찾는 것&quot; 자체가 가장 큰 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스팅에서는 복잡한 실무 환경에서 CSS 우선순위 문제를 관리하고 해결하는 실제 전략만 모아 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(혹시 CSS 우선순위에 대한 기본 개념이 부족하다면, &lt;a href=&quot;https://bumday.tistory.com/284&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 포스팅&lt;/a&gt;을 먼저 보는 것을 추천한다.)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 어디서 적용되는 스타일인지 빠르게 찾는 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 가장 중요한 건 출처 추적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Chrome DevTools &quot;Computed&quot; 패널 활용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1915&quot; data-origin-height=&quot;1027&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/89RrF/dJMcahbKZUF/tQELZfCJYPCkh81V8k07b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/89RrF/dJMcahbKZUF/tQELZfCJYPCkh81V8k07b1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/89RrF/dJMcahbKZUF/tQELZfCJYPCkh81V8k07b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F89RrF%2FdJMcahbKZUF%2FtQELZfCJYPCkh81V8k07b1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1915&quot; height=&quot;1027&quot; data-origin-width=&quot;1915&quot; data-origin-height=&quot;1027&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소를 검사하고 &quot;&lt;b&gt;Computed&lt;/b&gt;&quot; 탭을 보면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 적용된 스타일&lt;/li&gt;
&lt;li&gt;어떤 파일 / 몇 번째 줄에서 왔는지&lt;/li&gt;
&lt;li&gt;우선순위 승패 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 모두 확인 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) DevTools 우측 &quot;Styles&quot; 패널에서 충돌 비교&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지워진(취소선) 스타일은 같은 속성이 다른 규칙에 의해 덮어씌워졌다는 뜻이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDdPjl/dJMcahpix6T/01SwWKgExnAgCHedNrBJy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDdPjl/dJMcahpix6T/01SwWKgExnAgCHedNrBJy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDdPjl/dJMcahpix6T/01SwWKgExnAgCHedNrBJy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDdPjl%2FdJMcahpix6T%2F01SwWKgExnAgCHedNrBJy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1241&quot; height=&quot;463&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 규칙이 이겼는지 즉시 파악 가능&lt;/li&gt;
&lt;li&gt;해당 파일을 직접 찾아 빠르게 조정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) Find in all files 와 브라우저 검사 병행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 class명이 공통적(.btn, .card, .title)일 때 어디에 정의되었는지 파악하기 어려운 경우:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMjs0e/dJMcaioclIC/aD6OaUE2QfRCQCt6dcGnF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMjs0e/dJMcaioclIC/aD6OaUE2QfRCQCt6dcGnF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMjs0e/dJMcaioclIC/aD6OaUE2QfRCQCt6dcGnF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMjs0e%2FdJMcaioclIC%2FaD6OaUE2QfRCQCt6dcGnF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1308&quot; height=&quot;446&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 크롬에서 실제 적용된 파일명/라인 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. IDE에서 해당 파일명을 검색하여 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이 과정을 자동화하면 &quot;스타일의 출처&quot; 를 정확히 알 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 단기 해결책 - override.css + 최소한의 !important&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 통제하기 어려운 대규모 사이트에서는 &lt;b&gt;별도의 override 파일&lt;/b&gt;을 만들어 가장 마지막에 로드하는 것이 효과적이다.&lt;/p&gt;
&lt;pre id=&quot;code_1763776060824&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;override.css&quot;&amp;gt; &amp;lt;!-- 무조건 마지막 --&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 특징:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠르고 안정적&lt;/li&gt;
&lt;li&gt;기존 구조를 건드리지 않음&lt;/li&gt;
&lt;li&gt;페이지별 스타일도 손쉽게 대응 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 반드시 기억해야 할 점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;!important&lt;/span&gt;는 &quot;정말 필요한 경우에만&quot; 사용&lt;/li&gt;
&lt;li&gt;override 파일이 비대해지면 또 다른 레거시가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 중기 해결책 - &quot;명시도 조절(override)&quot; 로 자연스럽게 해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하다면 !important 대신 명시도를 올려서 재정의하는 방식이 훨씬 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제:&lt;/p&gt;
&lt;pre id=&quot;code_1763776164145&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* 기존 */
.btn { padding: 4px; }

/* override */
.container .btn { padding: 8px; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CSS 기본 규칙에 충실&lt;/li&gt;
&lt;li&gt;유지보수성이 훨씬 높음&lt;/li&gt;
&lt;li&gt;나중에 다른 사람이 봐도 이해하기 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. Scoped CSS - 영향 범위를 좁혀 충돌 예방&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 페이지나 특정 컴포넌트만 스타일을 바꿔야 한다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타일의 &quot;범위(scope)&quot; 를 좁히는 것이 가장 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre id=&quot;code_1763776234515&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* 결제 페이지에만 적용 */
#payment-page .btn-primary {
  background-color: #333;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 사이트에 영향을 주지 않음&lt;/li&gt;
&lt;li&gt;의도 명확 &amp;rarr; 협업 시 혼동 없음&lt;/li&gt;
&lt;li&gt;우선순위 충돌 확률 &amp;darr;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. CSS 파일 로드 순서 정리 - 가장 치명적 실수 방지&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타일 충돌의 대부분은 파일 로드 순서 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 명시도를 올려도 앞에서 이미 강한 스타일이 로드되었다면 무용지물이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 다음과 같은 구조를 강력 추천한다.&lt;/p&gt;
&lt;pre id=&quot;code_1763776301026&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;base.css&quot;&amp;gt;       &amp;lt;!-- reset, 공통 --&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;layout.css&quot;&amp;gt;     &amp;lt;!-- 레이아웃 --&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;component.css&quot;&amp;gt;  &amp;lt;!-- 컴포넌트 --&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;page.css&quot;&amp;gt;       &amp;lt;!-- 페이지별 --&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;override.css&quot;&amp;gt;   &amp;lt;!-- 덮어쓰기 전용 --&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;override.css를 항상 마지막&lt;/li&gt;
&lt;li&gt;구조가 명확해지고 충돌이 줄어듦&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 장기 해결책 - CSS 구조 리팩토링(BEM 등)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시에서 벗어나고 싶다면 CSS 클래스 체계를 새로 잡는 것이 최선이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예: BEM&lt;/p&gt;
&lt;pre id=&quot;code_1763776353339&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.header__menu__item--active { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선택자 우선순위 계산이 매우 쉬움&lt;/li&gt;
&lt;li&gt;중복 및 충돌 자체가 줄어듦&lt;/li&gt;
&lt;li&gt;팀 개발에 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS는 결국 &lt;u&gt;&lt;b&gt;구조화된 네이밍 규칙이 유지보수성을 좌우&lt;/b&gt;&lt;/u&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 실무 적용 요약&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1763776425813&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  단기
  - override.css + 최소한의 !important

  중기
  - 명시도 올려 자연스러운 override
  - 페이지 단위 Scoped CSS
  - 파일 로드 순서 재정리

  장기
  - CSS 네이밍 구조 바로잡기(BEM)
  - 중복 제거 및 사용되지 않는 스타일 정리
  - 컴포넌트 기반 CSS로 재편&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS 우선순위 문제는 단순한 기술적 이슈가 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조, 습관, 파일 관리 전략 의 조합으로 해결되는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 프로젝트든 신규 프로젝트든&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위를 정확히 이해하고 적용하면 스타일 충돌은 크게 줄어들고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유지보수 속도와 코드 품질은 비약적으로 향상된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 유지보수나 신규 스타일 작업을 할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;우선순위 &amp;rarr; 범위조절 &amp;rarr; 구조화&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지를 항상 염두에 두고 작성하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>FrontEnd/CSS</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/285</guid>
      <comments>https://bumday.tistory.com/285#entry285comment</comments>
      <pubDate>Sat, 22 Nov 2025 11:06:59 +0900</pubDate>
    </item>
    <item>
      <title>CSS 우선순위 완전 정리 (Specificity &amp;amp; Cascade)</title>
      <link>https://bumday.tistory.com/284</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;HTML에서 어떤 스타일이 최종적으로 적용되는지 헷갈린다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스팅 하나로 모두 정리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. CSS 우선순위란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 요소에 여러 CSS 규칙이 동시에 적용될 경우, 브라우저는 특정 규칙을 선택해 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 어떤 규칙을 우선할지 결정하는 기준이 바로 &lt;b&gt;우선순위(Specificity &amp;amp; Cascade)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS 우선순위는 다음 4가지 요소로 결정된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;중요도(!important)&lt;/li&gt;
&lt;li&gt;명시도 (Specificity)&lt;/li&gt;
&lt;li&gt;소스(origin)와 선언 위치&lt;/li&gt;
&lt;li&gt;선언된 순서 (Order)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요소별로 자세히 정리해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 우선순위 1순위: &lt;span style=&quot;background-color: #dddddd;&quot;&gt;!important&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 속성끼리 충돌할 때, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;!important&lt;/span&gt;는 모든 요소보다 우선한다.&lt;/p&gt;
&lt;pre id=&quot;code_1763774876551&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.box {
  color: red !important;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, !important끼리 충돌하면 &lt;b&gt;명시도 &amp;rarr; 선언 순서&lt;/b&gt; 기준으로 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 명시도(Specificity) 계산법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS는 선택자마다 점수를 부여한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt; 선택자 유형 &lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff; text-align: start;&quot;&gt; 예시&amp;nbsp; &lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt; 점수 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Inline style&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;div style=&quot;&quot;&amp;gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;1000&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ID 선택자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;#title&lt;/td&gt;
&lt;td&gt;&lt;b&gt;100&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Class / 속성 / pseudo-class&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;.box, [type=text], :hover&lt;/td&gt;
&lt;td&gt;&lt;b&gt;10&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;태그 선택자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;div, span&lt;/td&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;전체 선택자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre id=&quot;code_1763774973393&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#header .menu li a:hover&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;#header &amp;rarr; 100&lt;/li&gt;
&lt;li&gt;.menu &amp;rarr;10&lt;/li&gt;
&lt;li&gt;li &amp;rarr; 1&lt;/li&gt;
&lt;li&gt;a &amp;rarr; 1&lt;/li&gt;
&lt;li&gt;:hover &amp;rarr; 10&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;총합 = 122&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 선언된 위치에 따른 우선순위 (Origin)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS는 HTML 내에서 &lt;b&gt;어디서 선언되었는지&lt;/b&gt;에 따라 적용 우선순위가 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본 우선순위(위 &amp;rarr; 아래 = 약함 &amp;rarr;강함)&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저 기본 스타일 (User-Agent)&lt;/li&gt;
&lt;li&gt;외부 CSS 파일 ( &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot;&amp;gt;&lt;/span&gt; )&lt;/li&gt;
&lt;li&gt;HTML 내부 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt; 태그&lt;/li&gt;
&lt;li&gt;inline style (&lt;span style=&quot;background-color: #dddddd;&quot;&gt;style=&quot;&quot;&lt;/span&gt;)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;!important&lt;/span&gt; 선언된 규칙&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. HTML 내 위치에 따른 적용 예시&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 명시도/중요도를 가진 CSS라면 &lt;b&gt;&lt;u&gt;나중에 선언된 스타일이 우선&lt;/u&gt;&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 HTML:&lt;/p&gt;
&lt;pre id=&quot;code_1763775163240&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;head&amp;gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style1.css&quot;&amp;gt; &amp;lt;!-- 먼저 로드됨 --&amp;gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style2.css&quot;&amp;gt; &amp;lt;!-- 나중 로드됨 &amp;rarr; 우선 --&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;late.css&quot;&amp;gt; 
  &amp;lt;!-- body 안에 있어도, head 보다 &quot;나중&quot;이므로 late.css 가 최우선 --&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;pre id=&quot;code_1763775174029&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;style1.css  &amp;lt;  style2.css  &amp;lt;  late.css&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;HTML 구조가 어디 있든지 뒤에서 불러온 CSS가 우선&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt; 태그도 동일 규칙 적용&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1763775228671&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;head&amp;gt;
  &amp;lt;style&amp;gt;
    .box { color: red; }
  &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .box { color: blue; } /* 이게 적용됨 (나중 선언) */
  &amp;lt;/style&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 전체 우선순위 요약표&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1763775250496&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. !important
2. inline style=&quot;&quot;
3. ID 선택자
4. class / pseudo-class / attribute 선택자
5. tag 선택자
6. 전체 선택자 (*)
7. 브라우저 기본 스타일
+ 같은 레벨이면 &amp;ldquo;나중에 선언된 것&amp;rdquo; 승리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS 우선순위는 단순히 암기해두는 개념처럼 보이지만, 실제로는 &lt;b&gt;브라우저가 스타일을 결정하는 전체 흐름(Cascade)&lt;/b&gt;을 이해하는 것이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어디서 선언되었는지, 어떤 선택자를 사용했는지, 그리고 어떤 순서로 로드되었는지에 따라 결과가 달라지기 때문에, 이를 정확히 알고 있으면 예상치 못한 스타일 충돌이나 적용 불가 문제를 훨씬 빠르게 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 여러 CSS 파일이 혼합된 프로젝트나 레거시 구조에서는 이 우선순위 규칙을 이해하는 것만으로도 유지보수 효율이 크게 향상된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 CSS를 작성하거나 수정할 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;b&gt;중요도 &amp;rarr; 명시도 &amp;rarr; 선언 위치 &amp;rarr; 선언 순서&lt;/b&gt;&quot; 라는 기본 원칙을 항상 염두에 두면, 보다 명확하고 예측 가능한 스타일 구조를 유지할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 우선순위 충돌을 해결하는 구체적 전략은 &lt;a href=&quot;https://bumday.tistory.com/285&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;다음 포스팅&lt;/a&gt;으로 자세히 정리하였다.&lt;/p&gt;</description>
      <category>FrontEnd/CSS</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/284</guid>
      <comments>https://bumday.tistory.com/284#entry284comment</comments>
      <pubDate>Sat, 22 Nov 2025 10:41:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Filter와 Interceptor의 차이 정리</title>
      <link>https://bumday.tistory.com/283</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC기반 웹 애플리케이션에서는 &lt;b&gt;요청(Request)&lt;/b&gt;와 &lt;b&gt;응답(Response)&lt;/b&gt;이 Controller에 도달하기 전 후로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 처리가 필요한 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(예: 로그인 세션 확인, 인코딩 설정, 접근 권한 체크, 로깅 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 사용할 수 있는 대표적인 두 가지 기술이 바로 &lt;b&gt;Filter&lt;/b&gt;와 &lt;b&gt;Interceptor&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Filter (javax.servlet.Filter)&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개념&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Servlet 표준 스펙&lt;/b&gt;에서 제공하는 기능이다.&lt;/li&gt;
&lt;li&gt;DispatcherServlet 이전 단계에서 &lt;b&gt;모든 요청을 가로채서 전처리/후처리&lt;/b&gt; 가능.&lt;/li&gt;
&lt;li&gt;Spring MVC에 종속되지 않고, 톰캣/JEUS 등 Servlet 컨테이너 레벨에서 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용 시점&lt;/h3&gt;
&lt;pre id=&quot;code_1761866588687&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Client &amp;rarr; Filter &amp;rarr; DispatcherServlet &amp;rarr; Controller &amp;rarr; View&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;Controller 진입 전&lt;/b&gt;에 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 용도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인코딩 처리(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;CharactorEncodingFilter&lt;/span&gt;)&lt;/li&gt;
&lt;li&gt;인증/권한 체크 (세션/토큰 확인)&lt;/li&gt;
&lt;li&gt;XSS, CORS, 로깅 등 전역적인 요청 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 예시 (web.xml)&lt;/h3&gt;
&lt;pre id=&quot;code_1761866664490&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;encodingFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;org.springframework.web.filter.CharacterEncodingFilter&amp;lt;/filter-class&amp;gt;
    &amp;lt;init-param&amp;gt;
        &amp;lt;param-name&amp;gt;encoding&amp;lt;/param-name&amp;gt;
        &amp;lt;param-value&amp;gt;UTF-8&amp;lt;/param-value&amp;gt;
    &amp;lt;/init-param&amp;gt;
&amp;lt;/filter&amp;gt;

&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;encodingFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/*&amp;lt;/url-pattern&amp;gt;
&amp;lt;/filter-mapping&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 예시&lt;/h3&gt;
&lt;pre id=&quot;code_1761866676055&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AuthCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute(&quot;USER&quot;) == null) {
            ((HttpServletResponse) res).sendRedirect(&quot;/login.do&quot;);
            return;
        }

        chain.doFilter(req, res); // 다음 Filter 또는 DispatcherServlet으로 이동
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Interceptor (org.springframework.web.servlet.HandlerInterceptor)&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개념&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring MVC 전용 요청 가로채기 기능&lt;/li&gt;
&lt;li&gt;DispatcherServlet 이후 단계에서 동작&lt;/li&gt;
&lt;li&gt;Spring Context 내에서 동작함으로, 빈 주입(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Autowired&lt;/span&gt;)이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용 시점&lt;/h3&gt;
&lt;pre id=&quot;code_1761866733892&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Client &amp;rarr; Filter &amp;rarr; DispatcherServlet &amp;rarr; Interceptor &amp;rarr; Controller
                                    &amp;uarr;         &amp;darr;
                            (preHandle)  (postHandle / afterCompletion)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 용도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller 호출 전 사용자 인증 / 권한 검사&lt;/li&gt;
&lt;li&gt;Controller 실행 후 Model 데이터 가공&lt;/li&gt;
&lt;li&gt;View 렌더링 후 로깅, 성능 측정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 예시 (spring-mvc.xml)&lt;/h3&gt;
&lt;pre id=&quot;code_1761866761274&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;mvc:interceptors&amp;gt;
    &amp;lt;mvc:interceptor&amp;gt;
        &amp;lt;mvc:mapping path=&quot;/**&quot;/&amp;gt;
        &amp;lt;bean class=&quot;egovframework.example.common.interceptor.AuthInterceptor&quot;/&amp;gt;
    &amp;lt;/mvc:interceptor&amp;gt;
&amp;lt;/mvc:interceptors&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 예시&lt;/h3&gt;
&lt;pre id=&quot;code_1761866776541&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AuthInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        HttpSession session = request.getSession(false);
        if (session == null || session.getAttribute(&quot;USER&quot;) == null) {
            response.sendRedirect(&quot;/login.do&quot;);
            return false; // Controller 진입 차단
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        // Controller 실행 후 로직
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        // View 렌더링 후 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 주요 차이점 비교&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;구분&amp;nbsp;&lt;/span&gt; &lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #ffffff; text-align: start;&quot;&gt; Filter&amp;nbsp; &lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt; Interceptor &lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;소속&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Servlet (javax.servlet)&lt;/td&gt;
&lt;td&gt;Spring MVC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;적용 위치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DispatcherServlet &lt;b&gt;이전&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DispatcherServlet &lt;b&gt;이후&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;주요 용도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;인코딩, 로깅, 보안, CORS 등 &lt;b&gt;전역 처리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;로그인 세션, 권한, 로직 공통화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;설정 위치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;web.xml&lt;/td&gt;
&lt;td&gt;spring-mvc.xml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DI 지원 여부&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;X (Spring Bean 주입 불가)&lt;/td&gt;
&lt;td&gt;O (@Autowired 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;실행 메서드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;doFilter()&lt;/td&gt;
&lt;td&gt;preHandle(), postHandle(), afterCompletion()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;적용 범위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;애플리케이션 전체 (모든 요청)&lt;/td&gt;
&lt;td&gt;Spring MVC 컨트롤러 영역 한정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 함께 사용하는 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 프로젝트에서는 &lt;b&gt;Filter &amp;rarr; Interceptor 병행 사용&lt;/b&gt;이 일반적이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 50px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;계층&amp;nbsp;&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;역할&amp;nbsp;&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;&lt;b&gt; 예시 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;Filter&lt;/td&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;모든 요청에 대한 전처리 (인코딩, CORS, XSS 등)&lt;/td&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;CharacterEncodingFilter, LoggingFilter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Interceptor&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Controller 진입 전후 처리 (로그인, 권한, 세션 관리 등)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;AuthInterceptor, MenuAccessInterceptor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 흐름도:&lt;/h3&gt;
&lt;pre id=&quot;code_1761866864653&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Client
 &amp;darr;
Filter (인코딩, 로깅)
 &amp;darr;
DispatcherServlet
 &amp;darr;
Interceptor (세션 검사, 공통 데이터 주입)
 &amp;darr;
Controller
 &amp;darr;
Service / DAO
 &amp;darr;
View(JSP)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 정리&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Filter는 Servlet 레벨의 요청 흐름 제어기&lt;/li&gt;
&lt;li&gt;Interceptor는 Spring MVC 레벨의 컨트롤러 전후 후킹 포인트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전역 정책이나 보안/인코딩 은 Filter&lt;/b&gt;로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비즈니스 로직 관련 공통 처리는 Interceptor&lt;/b&gt;로 관리하는 것이 이상적이다.&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/283</guid>
      <comments>https://bumday.tistory.com/283#entry283comment</comments>
      <pubDate>Fri, 31 Oct 2025 08:30:46 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring Framework vs Spring Boot 비교 정리</title>
      <link>https://bumday.tistory.com/282</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 개념과 철학의 차이&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework는 개발자가 직접 조립하는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정파일을 직접 관리하고, 필요한 라이브러리 버전도 일일히 맞춰야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Spring Boot는 &quot;실행 중심 프레임워크&quot; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내장 서버와 자동 설정을 통해 실행만 하면 바로 서비스가 구동된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 80px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt; Spring Framework &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt; Spring Boot &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;핵심 철학&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;개발자가 설정하고 조립&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;설정 자동화 + 실행 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;실행 구조&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;외부 WAS 기반&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;내장 WAS 포함 (Tomcat 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;진입점&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;web.xml &amp;rarr; DispatcherServlet&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;main() &amp;rarr; SpringApplication.run()&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 설정 구조의 진화&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 시절에는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;web.xml&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;root-context.xml&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;servlet-context.xml&lt;/span&gt; 등 다수의 XML을 관리해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Boot에서는 단 하나의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;application.yml&lt;/span&gt; (혹은 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.properties&lt;/span&gt;)로 모든 설정을 통합 관리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bean 등록: XML의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;bean&amp;gt;&lt;/span&gt; 태그 대신 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Configuration&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@SpringBootApplication&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;DB/MyBatis: DataSource, Mapper를 직접 등록하던 시절 &amp;rarr; Starter 의존성 하나로 자동 연결&lt;/li&gt;
&lt;li&gt;환경 분리: &lt;span style=&quot;background-color: #dddddd;&quot;&gt;application-dev.yml&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;application-prod.yml&lt;/span&gt;로 명확하게 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 실행과 배포 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Boot의 가장 큰 변화는 &lt;b&gt;내장 WAS 구조&lt;/b&gt;이다.&lt;br /&gt;기존엔 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.war&lt;/span&gt; 파일을 만들어 Tomcat이나 JEUS에 배포했지만,&lt;br /&gt;Boot에서는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.jar&lt;/span&gt; 하나로 독립 실행이 가능하다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 항목 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; Spring Framework &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; Spring Boot &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빌드 산출물&lt;/td&gt;
&lt;td&gt;.war&lt;/td&gt;
&lt;td&gt;.jar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포 형태&lt;/td&gt;
&lt;td&gt;외부 WAS 필요&lt;/td&gt;
&lt;td&gt;내장 Tomcat으로 단독 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 명령&lt;/td&gt;
&lt;td&gt;WAS 기동 후 WAR 로드&lt;/td&gt;
&lt;td&gt;java -jar 또는 IDE 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설정 위치&lt;/td&gt;
&lt;td&gt;server.xml, context.xml 등&lt;/td&gt;
&lt;td&gt;application.yml에서 통합 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 개발 생산성과 운영 효율&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Boot는 설정보다 &lt;b&gt;즉시 실행과 생산성&lt;/b&gt;에 초점을 맞췄다.&lt;br /&gt;자동 구성, 통합 로깅, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;/actuator&lt;/span&gt; 모니터링 같은 기본 기능이 내장되어 있어 운영환경에서도 바로 쓸 수 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #ffffff; text-align: start;&quot;&gt; 항목&amp;nbsp; &lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt; Spring Framework &lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt; Spring Boot &lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기 설정량&lt;/td&gt;
&lt;td&gt;많음 (XML 다수)&lt;/td&gt;
&lt;td&gt;적음 (자동 설정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;테스트&lt;/td&gt;
&lt;td&gt;복잡한 컨텍스트 로딩&lt;/td&gt;
&lt;td&gt;@SpringBootTest로 간단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로깅&lt;/td&gt;
&lt;td&gt;log4j 직접 구성&lt;/td&gt;
&lt;td&gt;logback 기본 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모니터링&lt;/td&gt;
&lt;td&gt;직접 구현&lt;/td&gt;
&lt;td&gt;/actuator 자동 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포&lt;/td&gt;
&lt;td&gt;WAR 중심&lt;/td&gt;
&lt;td&gt;JAR 중심 (CI/CD 용이)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 디렉터리 구조 예시&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Framework (기존형)&lt;/h3&gt;
&lt;pre id=&quot;code_1761694194363&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/src/main/webapp/WEB-INF/web.xml
/src/main/resources/spring/root-context.xml
/src/main/resources/spring/servlet-context.xml
/src/main/java/.../controller/*.java
/src/main/java/.../service/*.java
/src/main/java/.../dao/*.java&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DispatcherServlet, Context 수동 등록 필요&lt;/li&gt;
&lt;li&gt;외부 WAS(Tomcat, JEUS 등)에 WAR로 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Boot (신형)&lt;/h3&gt;
&lt;pre id=&quot;code_1761694229192&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/src/main/java/com/example/Application.java
/src/main/resources/application.yml
/src/main/java/.../controller/*.java
/src/main/java/.../service/*.java
/src/main/java/.../mapper/*.java&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@SpringBootApplication&lt;/span&gt;으로 자동 스캔&lt;/li&gt;
&lt;li&gt;내장 Tomcat 포함, 별도 WAS 배포 불필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 정리 및 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;352&quot; data-start=&quot;132&quot; data-ke-size=&quot;size16&quot;&gt;Spring Framework와 Spring Boot는 같은 뿌리에서 발전했지만, 추구하는 방향은 다르다.&lt;br /&gt;Spring Framework는 &lt;b&gt;세밀한 제어와 기업형 인프라 호환성&lt;/b&gt;에 강점을 가지며, 대규모 레거시 시스템에 적합하다.&lt;br /&gt;반면 Spring Boot는 &lt;b&gt;빠른 개발과 자동 설정, 단독 실행 환경&lt;/b&gt;에 초점을 맞추어 클라우드나 마이크로서비스, PoC 개발에 유리하다.&lt;/p&gt;
&lt;p data-end=&quot;352&quot; data-start=&quot;132&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;487&quot; data-start=&quot;354&quot; data-ke-size=&quot;size16&quot;&gt;Spring Framework는 설정 중심의 조립형 프레임워크로서 구조를 세밀히 다루려는 개발자에게 적합하고,&lt;br /&gt;Spring Boot는 실행 중심의 통합형 프레임워크로서 빠른 실행과 배포를 중시하는 실무형 개발에 최적화되어 있다.&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/282</guid>
      <comments>https://bumday.tistory.com/282#entry282comment</comments>
      <pubDate>Wed, 29 Oct 2025 08:35:42 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 자주 사용하는 어노테이션 정리 및 예시</title>
      <link>https://bumday.tistory.com/281</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework는 객체를 관리하고 계층 간 의존성을 자동 주입하기 위해 다양한 어노테이션을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 실제 개발 현장에서 자주 사용되는 핵심 어노테이션들을 중심으로 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Component&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Service&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Repository&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Controller&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지는 Spring Bean 등록용 어노테이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Spring이 해당 클래스를 컨테이너에서 관리하도록 등록하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로는 모두 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Component&lt;/span&gt;를 기반으로 하며, 역할에 따라 의미만 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) @Component&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 Bean 등록용 어노테이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Helper, Util, Validator 등 특정 계층에 속하지 않는 일반 클래스를 Bean으로 등록할 때 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1761606317375&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Component;

@Component
public class FileNameHelper {

    public String makeFileName(String original) {
        return System.currentTimeMillis() + &quot;_&quot; + original;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다른 클래스에서 자동 주입하여 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1761606329761&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class FileService {

    private final FileNameHelper fileNameHelper;

    @Autowired
    public FileService(FileNameHelper fileNameHelper) {
        this.fileNameHelper = fileNameHelper;
    }

    public void saveFile(String fileName) {
        String newName = fileNameHelper.makeFileName(fileName);
        System.out.println(&quot;저장 파일명: &quot; + newName);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) &lt;span style=&quot;background-color: #dddddd; color: #000000;&quot; data-token-index=&quot;1&quot;&gt;@Service&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직을 담당하는 계층에 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 걸리거나 여러 DAO를 조합하는 등의 처리를 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드상 기능은 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Component&lt;/span&gt;와 동일하지만, 역할을 명확히 구분하기 위해 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1761606373188&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Service;

@Service
public class MemberService {

    public String getMemberGrade(int point) {
        if (point &amp;gt; 1000) return &quot;VIP&quot;;
        if (point &amp;gt; 500) return &quot;GOLD&quot;;
        return &quot;SILVER&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3)&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Repository&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스와 직접 통신하는 DAO 계층에 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Repository&lt;/span&gt;는 예외 변환 기능이 내장되어 있어, JDBC나 MyBatis, JPA에서 발생한 예외를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 공통 예외(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;DataAccessException&lt;/span&gt;)로 변환해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1761606413204&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Repository;
import java.util.HashMap;
import java.util.Map;

@Repository
public class MemberRepository {

    private static final Map&amp;lt;Integer, String&amp;gt; MEMBER_DB = new HashMap&amp;lt;&amp;gt;();

    static {
        MEMBER_DB.put(1, &quot;홍길동&quot;);
        MEMBER_DB.put(2, &quot;이몽룡&quot;);
    }

    public String findMemberName(int id) {
        return MEMBER_DB.get(id);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Controller&lt;/span&gt; / &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@RestController&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller 계층은 클라이언트의 요청을 받아 처리하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Controller&lt;/span&gt;는 View(JSP, Thymeleaf 등) 를 반환하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@RestController&lt;/span&gt;는 JSON 데이터를 직접 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1761606451095&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MemberViewController {

    @GetMapping(&quot;/member/view&quot;)
    public String showMember(Model model) {
        model.addAttribute(&quot;name&quot;, &quot;홍길동&quot;);
        return &quot;memberView&quot;; // memberView.jsp
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761606459600&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;

@RestController
public class MemberApiController {

    @GetMapping(&quot;/api/member&quot;)
    public Member getMember() {
        return new Member(&quot;홍길동&quot;, &quot;VIP&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761606473685&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Member {
    private String name;
    private String grade;

    public Member(String name, String grade) {
        this.name = name;
        this.grade = grade;
    }

    // getter, setter 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2.&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Autowired&lt;/span&gt;와&amp;nbsp;의존성&amp;nbsp;주입(Dependency&amp;nbsp;Injection)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Autowired&lt;/span&gt;는 Bean을 자동 주입해주는 어노테이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Component&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Service&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Repository&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Controller&lt;/span&gt;로 등록된 Bean을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 곳에 가져다 쓸 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;필드 주입 (비권장)&lt;/h4&gt;
&lt;pre id=&quot;code_1761606529936&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
private MemberService memberService;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하지만 테스트나 유지보수 시 불편하므로 권장되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;생성자 주입 (권장)&lt;/h4&gt;
&lt;pre id=&quot;code_1761606551019&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class OrderService {

    private final MemberService memberService;

    @Autowired
    public OrderService(MemberService memberService) {
        this.memberService = memberService;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring&amp;nbsp;4.3&amp;nbsp;이상부터는&amp;nbsp;생성자가&amp;nbsp;하나뿐이면&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Autowired&lt;/span&gt;를&amp;nbsp;생략할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Lombok과&amp;nbsp;함께&amp;nbsp;사용&amp;nbsp;(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@RequiredArgsConstructor&lt;/span&gt;)&lt;/h4&gt;
&lt;pre id=&quot;code_1761606586142&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class OrderService {

    private final MemberService memberService;

    public void process() {
        System.out.println(memberService.getMemberGrade(700));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3.&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Value&lt;/span&gt;&amp;nbsp;(프로퍼티&amp;nbsp;값&amp;nbsp;주입)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 설정 파일(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;application.properties&lt;/span&gt; 또는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.yml&lt;/span&gt;)에 정의된 값을 클래스에 주입할 수 있다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761606642787&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.properties
app.title=FOO PORTAL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761606650335&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppInfo {

    @Value(&quot;${app.title}&quot;)
    private String title;

    public void printTitle() {
        System.out.println(&quot;애플리케이션 이름: &quot; + title);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@RequestMapping&lt;/span&gt; 계열 (요청 매핑)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서 요청 URL을 메서드에 연결할 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Method별로 축약형 어노테이션이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시&amp;nbsp;1.&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@RequestMapping&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1761606677493&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/member&quot;)
public class MemberController {

    @RequestMapping(&quot;/list&quot;)
    public String listMembers() {
        return &quot;memberList&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시 2. HTTP 메서드별 축약형&lt;/h4&gt;
&lt;pre id=&quot;code_1761606699442&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/member&quot;)
public class MemberRestController {

    @GetMapping(&quot;/{id}&quot;)
    public String getMember(@PathVariable int id) {
        return &quot;회원번호: &quot; + id;
    }

    @PostMapping
    public String addMember(@RequestParam String name) {
        return &quot;등록된 회원: &quot; + name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 변경이 포함된 비즈니스 로직에 트랜잭션을 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 내 예외 발생 시 자동으로 롤백이 수행된다.&lt;/p&gt;
&lt;pre id=&quot;code_1761606721380&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PaymentService {

    @Transactional
    public void pay(Long memberId, int amount) {
        // 1. 잔액 차감
        // 2. 결제 내역 기록
        // 예외 발생 시 모든 작업이 롤백됨
        System.out.println(&quot;회원 &quot; + memberId + &quot;에게 &quot; + amount + &quot;원 결제 처리 완료&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 자주 사용하는 어노테이션은 단순히 &amp;ldquo;문법&amp;rdquo;이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로젝트 구조를 명확히 하고 계층 간 책임을 분리하기 위한 약속&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;@Component&lt;/b&gt;&lt;/span&gt; : 범용 Bean 등록&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Service&lt;/span&gt;&lt;/b&gt; : 비즈니스 로직&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Repository&lt;/span&gt;&lt;/b&gt; : 데이터 접근&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;@Controller&lt;/b&gt;&lt;/span&gt; / &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@RestController&lt;/span&gt;&lt;/b&gt; : 요청 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Autowired&lt;/span&gt;&lt;/b&gt;, &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Value&lt;/span&gt;&lt;/b&gt; : 의존성 및 설정값 주입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt; : 트랜잭션 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 어노테이션은 &amp;ldquo;Spring이 언제, 어떻게 객체를 관리할지&amp;rdquo; 결정하는 핵심 도구이며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념만 정확히 이해해도 대부분의 Spring 프로젝트 구조를 파악할 수 있다.&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/281</guid>
      <comments>https://bumday.tistory.com/281#entry281comment</comments>
      <pubDate>Tue, 28 Oct 2025 08:13:21 +0900</pubDate>
    </item>
  </channel>
</rss>