<?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>Wed, 10 Jun 2026 03:18:28 +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>[Troubleshooting] Maven &amp;quot;was cached in the local repository&amp;quot; &amp;mdash; 폐쇄망 빌드 서버에서 캐시된 실패 처리</title>
      <link>https://bumday.tistory.com/296</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;parent POM을 Nexus에 올렸는데도 빌드가 또 실패했다. 원인은 Maven이 이전에 실패한 해석 결과를 로컬 캐시에 저장해두고 계속 실패로 처리하고 있었기 때문이었다.&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. 증상&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parent POM을 Nexus에 올리고 다시 빌드를 돌렸는데 또 같은 에러가 떴다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;[ERROR] Failed to execute goal on project my-project:
  Could not resolve dependencies ...
    Failure to find com.github.librepdf:openpdf-parent:pom:1.3.30.jaspersoft.2
    in http://nexus.internal:8081/repository/maven-public/
    was cached in the local repository,
    resolution will not be reattempted until the update interval
    of nexus has elapsed or updates are forced&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf-parent&lt;/span&gt;는 분명히 Nexus에 올라가 있는데 왜 여전히 실패하는지 이해가 되지 않았다.&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. 원인 &amp;mdash; Maven의 네가티브 캐시&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven은 한 번 원격 레포에서 아티팩트 다운로드에 실패하면, 실패 정보를 로컬 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&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;이 네가티브 마크 파일이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt;에 남아 있으면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;was cached&lt;/span&gt; 메시지가 뜨면서 설정된 인터벌이 지나기 전까지 재시도하지 않는다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;C:\Users\&amp;lt;agent&amp;gt;\.m2\repository\com\github\librepdf\openpdf-parent\1.3.30.jaspersoft.2\
    openpdf-parent-1.3.30.jaspersoft.2.pom.lastUpdated  &amp;larr; 이게 문제&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt; 파일에는 실패한 시간과 실패한 URL이 담겨 있다. Maven은 이것을 보고 &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;즉, &lt;b&gt;빌드 에이전트가 Nexus 업로드 이전 시점에 이미 캐시를 만들어둔 것&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;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;mdash; -U 플래그로 강제 업데이트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt; (&lt;span style=&quot;background-color: #dddddd;&quot;&gt;--update-snapshots&lt;/span&gt;) 플래그를 추가하면, 캐시된 네가티브 마크를 무시하고 원격 레포를 직접 확인한다. TeamCity Maven Build Step의 &lt;b&gt;Goals&lt;/b&gt;에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt;를 추가했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# 변경 전
clean package -DskipTests

# 변경 후
clean package -DskipTests -U&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt;는 SNAPSHOT 업데이트와 &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;4. 주의사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt;는 빌드마다 원격 레포를 업데이트 확인하므로 &lt;b&gt;빌드 에이전트가 완전한 폐쇄망임을 전제로&lt;/b&gt; 한다. 원격 접근이 전혀 안 되는 환경이라면 아래 선택지를 사용할 수 있다.&lt;/p&gt;
&lt;div&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;/td&gt;
&lt;td&gt;방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.lastUpdated 직접 삭제&lt;/td&gt;
&lt;td&gt;Get-ChildItem -Recurse -Filter *.lastUpdated | Remove-Item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.m2 전체 삭제&lt;/td&gt;
&lt;td&gt;빌드 에이전트의 캐시를 완전히 초기화 후 재시도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;업로드 선행 확인&lt;/td&gt;
&lt;td&gt;빌드 시작 전 Nexus에 정상 업로드되었는지 스크립트로 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&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;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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;빌드 에이전트가 빌드 시도 &amp;rarr; 실패 &amp;rarr; &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt; 생성&lt;/li&gt;
&lt;li&gt;해당 아티팩트를 Nexus에 업로드&lt;/li&gt;
&lt;li&gt;다시 빌드 시도 &amp;rarr; Maven이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt;를 보고 캐시된 실패로 판단 &amp;rarr; 또 실패&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt;를 주면 Maven이 캐시를 무시하고 Nexus를 직접 조회하므로 이 순환 고리를 끊을 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;빌드 #4: clean package -DskipTests -U  &amp;rarr; 성공  &lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폐쇄망에서 Nexus에 파일을 올린 후에도 빌드가 실패한다면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt; 캐시를 먼저 의심한다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt; 하나로 해결된다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/296</guid>
      <comments>https://bumday.tistory.com/296#entry296comment</comments>
      <pubDate>Wed, 3 Jun 2026 10:27:02 +0900</pubDate>
    </item>
    <item>
      <title>[Troubleshooting] Maven parent POM이 Nexus에 없어서 빌드 실패 &amp;mdash; mvn dependency:list의 함정</title>
      <link>https://bumday.tistory.com/295</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;폐쇄망 Nexus에 의존성을 올렸는데도 빌드가 실패했다. 원인은 mvn dependency:list가 parent POM을 출력하지 않는다는 점이었다. 이 함정과 해결 방법을 정리한다.&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. 증상&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt;에서 의존성을 수집해 Nexus에 올리고 폐쇄망 TeamCity 빌드를 돌렸더니 아래 에러가 떴다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;Could not resolve dependencies for project ...
  Could not find artifact com.github.librepdf:openpdf-parent:pom:1.3.30.jaspersoft.2
    in nexus (http://nexus.internal:8081/repository/maven-public/)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf&lt;/span&gt;는 분명히 올렸는데 왜 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf-parent&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;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;mdash; mvn dependency:list의 함정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven 빌드 시 각 아티팩트의 POM을 읽어서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;parent&amp;gt;&lt;/span&gt; 태그를 따라 부모 POM도 함께 다운로드한다. 그런데 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;는 &lt;b&gt;실제 빌드에 사용되는 JAR만&lt;/b&gt; 출력하고, 모델 해석에 필요한 &lt;b&gt;parent POM은 목록에 포함시키지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf&lt;/span&gt; JAR를 올렸더라도, Maven이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf.pom&lt;/span&gt;을 읽어보면 아래와 같다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;xml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;&amp;lt;parent&amp;gt;
  &amp;lt;groupId&amp;gt;com.github.librepdf&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;openpdf-parent&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.3.30.jaspersoft.2&amp;lt;/version&amp;gt;
&amp;lt;/parent&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 parent POM까지 Nexus에 있어야 빌드가 성공한다. 하지만 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;에는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf-parent&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;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;mdash; POM의 parent 체인을 재귀적으로 탐색&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수집 스크립트에 Step 4를 추가했다. 이미 수집된 모든 POM 파일을 파싱해서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;parent&amp;gt;&lt;/span&gt; 요소를 찾고, 해당 parent POM도 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt;에서 복사한다. 복사한 parent POM에서 또 parent를 찾는 식으로 BFS(너비 우선 탐색)로 체인 전체를 추적했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;powershell&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# POM에서 &amp;lt;parent&amp;gt; 정보 추출
function Get-PomParent($pomPath) {
    try {
        $doc = New-Object System.Xml.XmlDocument
        $doc.Load($pomPath)  # [xml] 캐스트 대신 XmlDocument.Load() 사용 (인코딩 안전)
        $node = $doc.DocumentElement.SelectSingleNode(&quot;*[local-name()='parent']&quot;)
        if (-not $node) { return $null }
        $g = $node.SelectSingleNode(&quot;*[local-name()='groupId']&quot;).InnerText.Trim()
        $a = $node.SelectSingleNode(&quot;*[local-name()='artifactId']&quot;).InnerText.Trim()
        $v = $node.SelectSingleNode(&quot;*[local-name()='version']&quot;).InnerText.Trim()
        if ($g -and $a -and $v) { return @($g, $a, $v) }
    } catch {}
    return $null
}

# BFS로 parent 체인 전체 수집
$seenParents = New-Object 'System.Collections.Generic.HashSet[string]'
$parentQueue = New-Object 'System.Collections.Generic.Queue[object]'

# 이미 수집된 POM에서 parent 목록 추출해서 큐에 넣기
$pomFiles = Get-ChildItem -Path $DEST_DIR -Recurse -Filter &quot;*.pom&quot;
foreach ($pf in $pomFiles) {
    $par = Get-PomParent $pf.FullName
    if ($par) {
        $key = &quot;$($par[0]):$($par[1]):$($par[2])&quot;
        if ($seenParents.Add($key)) {
            $parentQueue.Enqueue([PSCustomObject]@{ GroupId=$par[0]; ArtifactId=$par[1]; Version=$par[2] })
        }
    }
}

# BFS 실행
while ($parentQueue.Count -gt 0) {
    $p = $parentQueue.Dequeue()
    $srcPath = Find-M2Path -base &quot;$M2_DIR\$groupPath\$($p.ArtifactId)&quot; -version $p.Version
    if ($srcPath) {
        Copy-Item -Path &quot;$srcPath\*&quot; -Destination $dstDir -Recurse -Force
        Write-Host &quot;[PARENT] $($p.GroupId):$($p.ArtifactId):$($p.Version)&quot;
        # 복사한 POM의 parent도 큐에 추가
        $copiedPom = Get-ChildItem -Path $dstDir -Filter &quot;*.pom&quot; | Select-Object -First 1
        $par = Get-PomParent $copiedPom.FullName
        if ($par -and $seenParents.Add(&quot;$($par[0]):$($par[1]):$($par[2])&quot;)) {
            $parentQueue.Enqueue(...)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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. 삽질 포인트 &amp;mdash; [xml] 캐스트 vs XmlDocument.Load()&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 PowerShell의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;[xml]$doc = Get-Content $pomPath&lt;/span&gt;로 파싱했는데 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Parent POMs copied: 0&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;원인은 두 가지였다.&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;span style=&quot;background-color: #dddddd;&quot;&gt;Get-Content&lt;/span&gt;는 파일 내용을 라인 단위 배열로 반환한다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;[xml]&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;둘째, POM 파일 인코딩이 UTF-8 BOM 없이 저장된 경우 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Get-Content&lt;/span&gt;의 기본 인코딩(UTF-16 LE)과 충돌한다.&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;span style=&quot;background-color: #dddddd;&quot;&gt;XmlDocument.Load()&lt;/span&gt;는 파일 경로를 직접 받아서 XML 선언의 인코딩(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;?xml ... encoding=&quot;UTF-8&quot;?&amp;gt;&lt;/span&gt;)을 자동으로 처리하므로 훨씬 안전하다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;powershell&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# 위험 (인코딩 문제 가능)
[xml]$doc = Get-Content $pomPath

# 안전
$doc = New-Object System.Xml.XmlDocument
$doc.Load($pomPath)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;div&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;/td&gt;
&lt;td&gt;수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JAR 의존성 (COPY)&lt;/td&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;system scope (LOCAL)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parent POM 수집&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 55개의 parent POM이 누락되어 있었다.&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;만 믿으면 parent POM은 반드시 누락된다. JAR 수집 후 POM을 재귀적으로 파싱해서 parent 체인까지 챙겨야 한다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/295</guid>
      <comments>https://bumday.tistory.com/295#entry295comment</comments>
      <pubDate>Wed, 3 Jun 2026 10:22:18 +0900</pubDate>
    </item>
    <item>
      <title>[폐쇄망 CI/CD] Maven 의존성을 Nexus에 자동 업로드하기</title>
      <link>https://bumday.tistory.com/294</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;외부 인터넷이 차단된 폐쇄망 환경에서 Maven 프로젝트의 CI/CD를 구축하면서 겪은 의존성 관리 문제와 해결 과정을 정리한다.&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;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 보안 규정상 외부 인터넷이 막힌 폐쇄망 서버에 Spring Boot 기반 사내 시스템을 배포해야 했다. TeamCity로 CI/CD 파이프라인을 구성했는데, 빌드 서버가 Maven Central에 접근할 수 없으니 의존성을 어디선가 가져와야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방향은 세 단계로 정리됐다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내부망에 &lt;b&gt;Nexus Repository Manager&lt;/b&gt; 구축&lt;/li&gt;
&lt;li&gt;개발자 PC의 로컬 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt; 캐시에서 필요한 의존성만 추려서 Nexus에 업로드&lt;/li&gt;
&lt;li&gt;CI/CD 빌드 서버는 Nexus만 바라보도록 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;settings.xml&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;2. 환경 정보&lt;/b&gt;&lt;/h2&gt;
&lt;div&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;/td&gt;
&lt;td&gt;값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nexus URL&lt;/td&gt;
&lt;td&gt;내부망 Nexus 서버 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;업로드 레포&lt;/td&gt;
&lt;td&gt;hosted 타입 레포 (프로젝트 전용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그룹 레포&lt;/td&gt;
&lt;td&gt;maven-public (업로드 레포 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;의존성 수집 경로&lt;/td&gt;
&lt;td&gt;로컬 임시 수집 디렉터리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;총 의존성&lt;/td&gt;
&lt;td&gt;172개 JAR + 55개 parent POM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD 서버의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;settings.xml&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;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;작업을 두 단계 PowerShell 스크립트로 분리했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;cicd/nexus/
├── 01_collect-mes-deps.ps1   # 의존성 수집 (인터넷 연결된 PC에서 실행)
├── 02_upload-to-nexus.ps1    # Nexus 업로드
└── README.md&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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. Step 1 &amp;mdash; 의존성 수집&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;로 프로젝트 의존성 목록을 추출한 뒤, 각 항목을 로컬 .m2에서 찾아 임시 수집 경로로 복사하는 방식이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;# mvn으로 의존성 목록 추출
mvn dependency:list &quot;-DoutputFile=C:\temp\my-project-deps.txt&quot; -DincludeScope=test -q

# .m2에서 수집 경로로 복사
foreach ($line in $depLines) {
    $parts      = $line.Trim() -split &quot;:&quot;
    $groupId    = $parts[0]
    $artifactId = $parts[1]
    $version    = $parts[-2]
    $srcPath    = Find-M2Path -base &quot;$M2_DIR\$groupPath\$artifactId&quot; -version $version

    if ($srcPath) {
        Copy-Item -Path &quot;$srcPath\*&quot; -Destination $dstPath -Recurse -Force
        Write-Host &quot;COPY ${groupId}:${artifactId}:${version}&quot;
    } else {
        $missed += ... # 나중에 처리
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수집 후에는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;_remote.repositories&lt;/span&gt; 같은 불필요한 메타파일을 제거했다. 이런 파일들이 Nexus에 올라가면 이후 빌드에서 문제가 생길 수 있다.&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;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;는 JAR 아티팩트만 출력하고 &lt;b&gt;parent POM은 누락&lt;/b&gt;한다. 이 때문에 빌드가 실패했고 별도 로직으로 해결했다. (&lt;a href=&quot;https://bumday.tistory.com/294&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;다음 포스팅&lt;/a&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-1. system scope 의존성 처리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;pom.xml&lt;/span&gt;에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;scope&amp;gt;system&amp;lt;/scope&amp;gt;&lt;/span&gt;으로 선언된 로컬 JAR는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt;에 없으므로 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;systemPath&lt;/span&gt;에서 직접 복사했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;powershell&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# pom.xml의 system scope 의존성을 systemPath에서 복사
foreach ($dep in $pom.project.dependencies.dependency) {
    if ($dep.scope -eq &quot;system&quot;) {
        $path = $dep.systemPath -replace '\$\{project\.basedir\}', $PROJECT_ROOT
        Copy-Item -Path $path -Destination $jarDst -Force
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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. Step 2 &amp;mdash; Nexus 업로드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수집 경로의 파일들을 Nexus REST API(PUT)로 업로드했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;powershell&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;$url    = &quot;$NEXUS_URL/repository/maven-my-project-deps/$relativePath&quot;
$result = Invoke-WebRequest -Uri $url -Method PUT -InFile $file.FullName `
    -Headers @{ Authorization = &quot;Basic $cred&quot; } -UseBasicParsing

if ($result.StatusCode -eq 201) {
    Add-Content $DONE_LOG $relativePath  # 완료 기록
    Write-Host &quot;OK  $relativePath&quot; -ForegroundColor Green
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 재시작 내성 기능이다. 완료된 파일 경로를 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;upload-done.log&lt;/span&gt;에 기록해두고, 재실행 시 이미 올라간 파일은 SKIP한다. 네트워크가 불안정해 중간에 끊겨도 이어서 실행할 수 있다.&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. CI/CD 빌드 서버 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TeamCity 빌드 에이전트의 Maven settings.xml에 Nexus 미러 설정만 추가하면 된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;xml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;&amp;lt;settings&amp;gt;
  &amp;lt;mirrors&amp;gt;
    &amp;lt;mirror&amp;gt;
      &amp;lt;id&amp;gt;nexus&amp;lt;/id&amp;gt;
      &amp;lt;mirrorOf&amp;gt;*&amp;lt;/mirrorOf&amp;gt;
      &amp;lt;url&amp;gt;http://nexus.internal:8081/repository/maven-public/&amp;lt;/url&amp;gt;
    &amp;lt;/mirror&amp;gt;
  &amp;lt;/mirrors&amp;gt;
  &amp;lt;servers&amp;gt;
    &amp;lt;server&amp;gt;
      &amp;lt;id&amp;gt;nexus&amp;lt;/id&amp;gt;
      &amp;lt;username&amp;gt;배포용_계정&amp;lt;/username&amp;gt;
      &amp;lt;password&amp;gt;패스워드&amp;lt;/password&amp;gt;
    &amp;lt;/server&amp;gt;
  &amp;lt;/servers&amp;gt;
&amp;lt;/settings&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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. 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;172개 JAR 의존성과 55개 parent POM 수집을 완료했고, 전체 Nexus 업로드에 성공했다. 최종적으로 폐쇄망 TeamCity 빌드까지 통과했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 parent POM 누락 문제와 로그 충돌 문제 두 가지 추가 트러블슈팅이 있었다. 각각 별도 포스팅으로 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;(&lt;span style=&quot;color: #666666;&quot;&gt;parent POM 누락문제 포스팅:&lt;/span&gt; &lt;a href=&quot;https://bumday.tistory.com/294&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bumday.tistory.com/294&lt;/a&gt;)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;(&lt;span style=&quot;color: #666666;&quot;&gt;로그 충돌 문제 포스팅:&lt;/span&gt; &lt;a href=&quot;https://bumday.tistory.com/295&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bumday.tistory.com/295&lt;/a&gt;)&lt;/i&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;폐쇄망 Maven 빌드에서 의존성 문제의 핵심은 JAR 외에 parent POM까지 챙기는 것이다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;만 믿으면 반드시 빌드가 깨진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/294</guid>
      <comments>https://bumday.tistory.com/294#entry294comment</comments>
      <pubDate>Wed, 3 Jun 2026 10:18:10 +0900</pubDate>
    </item>
    <item>
      <title>[Troubleshooting] Spring Boot 2.3 + JEUS 로그 라이브러리 충돌 해결</title>
      <link>https://bumday.tistory.com/293</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 2.3 기반 프로젝트를 JEUS에 배포하는 과정에서 로그 라이브러리 충돌 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특이한 점은 로컬에서 src 폴더를 직접 구동할 때는 아무 문제가 없었다는 것이다. 빌드된 target 폴더를 실행하거나 JEUS에 배포할 때만 문제가 생겼다.&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;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 Smart Tomcat에서 소스(src) 폴더를 직접 지정해 실행하면 정상 동작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 빌드 결과물인 target 폴더를 실행 대상으로 잡거나 운영 서버(JEUS)에 배포하면 Multiple SLF4J bindings 에러와 함께 기동이 실패했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777947480722&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/.../logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/.../log4j-slf4j-impl-2.17.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]&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. 원인 분석&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;src&lt;/span&gt; 모드에서는 IntelliJ가 클래스패스를 관리하며 중복된 라이브러리 중 하나에 임의로 우선순위를 두어 실행한다. 덕분에 충돌이 잠복해 있어도 표면에 드러나지 않는다.&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;반면 Maven이 빌드를 수행하면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;pom.xml&lt;/span&gt;에 정의된 모든 의존성을 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;WEB-INF/lib&lt;/span&gt;으로 물리적으로 복사한다. 이때 의존성 전이(Transitive Dependency)로 끌려온 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;log4j-slf4j-impl&lt;/span&gt;이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;logback&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;JEUS는 클래스패스 내 모든 JAR를 엄격하게 검사하기 때문에 중복 바인딩이 발견되면 기동 자체를 거부한다.&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;/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;방법 1: Provided Scope 활용 (추천)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log4j-slf4j-impl이 어떤 경로로 유입되는지 특정하기 어려울 때 가장 확실한 방법이다. pom.xml에 해당 의존성을 명시하고 배포 시 제외되도록 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777947550126&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.logging.log4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;log4j-slf4j-impl&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.17.1&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법 2: Exclusion 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유입 경로를 확인했다면 상위 라이브러리 설정에서 직접 해당 모듈을 제외한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777947564536&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
    &amp;lt;exclusions&amp;gt;
        &amp;lt;exclusion&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.logging.log4j&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;log4j-slf4j-impl&amp;lt;/artifactId&amp;gt;
        &amp;lt;/exclusion&amp;gt;
    &amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&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;div data-test-render-count=&quot;1&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&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;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;src&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;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;provided&lt;/span&gt; 스코프는 &quot;서버가 제공할 것이니 빌드 결과물에는 넣지 마라&quot;는 의미다. 원치 않는 중복 라이브러리가 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;target&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;빌드 후 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;WEB-INF/lib&lt;/span&gt; 안에 두 라이브러리가 공존하는지 확인하는 습관이 필요하다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-state=&quot;closed&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div data-state=&quot;closed&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/293</guid>
      <comments>https://bumday.tistory.com/293#entry293comment</comments>
      <pubDate>Tue, 5 May 2026 11:20:45 +0900</pubDate>
    </item>
    <item>
      <title>[Maven] 외부망에서 .m2 복사했는데 내부망 빌드 실패할 때</title>
      <link>https://bumday.tistory.com/292</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;외부망에서 빌드를 성공시킨 뒤 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2/repository&lt;/span&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;그런데 내부망에서 빌드를 돌리자 외부 저장소(Central)에 접속을 시도하며 Dependency Resolution 에러가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일은 분명히 있는데, Maven이 계속 외부로 나가려 했다.&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. 원인: _remote.repositories 파일&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;image.png&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYKNmd/dJMcagek4E2/0MNtSGRM4xASJA33pShzN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYKNmd/dJMcagek4E2/0MNtSGRM4xASJA33pShzN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYKNmd/dJMcagek4E2/0MNtSGRM4xASJA33pShzN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYKNmd%2FdJMcagek4E2%2F0MNtSGRM4xASJA33pShzN0%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;1013&quot; height=&quot;375&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven은 라이브러리를 다운로드할 때 해당 파일이 어떤 원격 저장소에서 왔는지를 기록하는 메타데이터 파일을 함께 생성한다.&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;span style=&quot;background-color: #dddddd;&quot;&gt;_remote.repositories&lt;/span&gt; 또는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;_maven.repositories&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;이 파일에는 출처 저장소 정보가 기록되어 있다. 예를 들어 중앙 저장소에서 받은 파일이라면 central이라고 적혀 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 Maven이 로컬에 파일이 있어도 이 메타데이터를 확인한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;central이라고 기록되어 있으면 Maven은 &quot;현재 연결된 저장소에 업데이트된 버전이 있는지&quot; 확인하려고 시도한다.&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;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. 해결: _remote.repositories 파일 일괄 삭제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 저장소 내의 모든 _remote.repositories 파일을 삭제하면 Maven은 출처를 확인하지 않고 로컬에 있는 파일을 그대로 사용하게 된다.&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;Windows (PowerShell 기준)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.m2/repository 경로로 이동한 뒤 아래 명령어를 실행한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1777947130313&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ls -R -Filter &quot;_remote.repositories&quot; | del&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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;Linux / Mac (Terminal 기준)&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1777947150034&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;find . -name &quot;_remote.repositories&quot; -type f -delete&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&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;메타데이터 삭제 이후에도 Maven이 외부 연결을 시도한다면 오프라인 옵션을 함께 사용하는 것이 확실하다.&lt;/p&gt;
&lt;pre id=&quot;code_1777947170735&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mvn clean install -o&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;-o&lt;/span&gt; 옵션은 원격 저장소 체크를 완전히 건너뛰고 로컬 저장소의 파일만 사용하도록 강제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 다룬 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;--offline&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;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;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt; 폴더를 복사하는 것만으로는 충분하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven은 파일 존재 여부뿐 아니라 출처 메타데이터까지 확인하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폐쇄망 환경에서 빌드를 안정적으로 돌리려면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;_remote.repositories&lt;/span&gt; 파일 삭제를 반드시 함께 진행해야 한다.&lt;/p&gt;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/292</guid>
      <comments>https://bumday.tistory.com/292#entry292comment</comments>
      <pubDate>Tue, 5 May 2026 11:15:51 +0900</pubDate>
    </item>
    <item>
      <title>[Maven] clean / package / --offline / -U 실무 정리</title>
      <link>https://bumday.tistory.com/291</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Maven을 쓰다 보면 아래 명령어들을 습관처럼 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777946228814&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mvn clean package
mvn clean package -U
mvn clean package --offline&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;p data-ke-size=&quot;size16&quot;&gt;CI/CD에서 빌드가 실패하거나, 캐시가 꼬이거나, Nexus 이슈로 이어지는 경우가 실제로 있었다.&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. clean &amp;ndash; 빌드 초기화 &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;target/&lt;/span&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;실무에서는 CI 빌드에 거의 무조건 포함한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777946255252&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mvn clean package&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;2. package &amp;ndash; 실제 빌드 수행&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;내부 흐름 compile &amp;rarr; test &amp;rarr; package&amp;nbsp; 순서로 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과물은 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;target/*.jar&lt;/span&gt; 또는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;target/*.war&lt;/span&gt; 형태로 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트라면 여기서 실행 가능한 jar가 만들어진다. 테스트도 함께 실행되기 때문에, 건너뛰고 싶다면 아래 옵션을 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777946303751&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mvn clean package -DskipTests&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. --offline &amp;ndash; 네트워크 차단 모드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;remote repository를 아예 사용하지 않는 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 Maven 저장소만 바라보기 때문에, Nexus에도 접근하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 dependency가 로컬에 없으면 무조건 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 에러 메시지는 아래처럼 뜬다.&lt;/p&gt;
&lt;pre id=&quot;code_1777946322707&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Cannot access ... in offline mode&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;dependency를 미리 받아놓지 않으면 바로 터지는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션은 폐쇄망 환경이거나, dependency preload가 이미 완료된 상태일 때만 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한마디로 &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;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. -U &amp;ndash; 강제 업데이트&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven 캐시를 무시하고 remote repository를 강제로 재조회하는 옵션이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;b&gt;SNAPSHOT 아티팩트&lt;/b&gt;에 효과적이다. Maven은 SNAPSHOT을 일정 주기로만 재조회하는데, -U를 붙이면 주기와 관계없이 강제로 최신 버전을 다시 확인한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 중요하냐면, Maven은 단순 캐시가 아니기 때문이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven은 &quot;없는 dependency도 캐싱한다&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 상황을 예로 들면, Nexus 요청이 실패하면 Maven은 &quot;이거 없음&quot;을 기억한다. 그 이후 빌드에서도 계속 실패한다.&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;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1777946396902&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resolution will not be reattempted&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태가 되면 Nexus가 정상으로 돌아와도 빌드가 계속 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순간 느끼는 것은 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nexus 문제가 아닌데 Maven이 포기해버린 상태라는 것.&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;/p&gt;
&lt;pre id=&quot;code_1777946419178&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mvn clean package -U&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어를 실행하면 기존 캐시를 무시하고, Nexus를 다시 조회하며, metadata도 갱신된다.&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;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;/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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nexus 장애 이후&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;평소 안정적인 빌드&lt;/td&gt;
&lt;td&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;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;Maven의 기본 동작은 로컬 repo를 먼저 확인하고, 있으면 사용, 없으면 Nexus를 조회하는 순서로 진행된다.&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;Nexus에 artifact가 없으면 Maven이 &quot;없음&quot;을 캐싱하고, 이후 Nexus가 복구돼도 계속 실패 상태가 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결은 -U 하나로 끝난다.&lt;/p&gt;
&lt;pre id=&quot;code_1777946490042&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mvn clean package -U&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;6. 실무 기준 추천 전략&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CI/CD (TeamCity 기준)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 세팅 단계에서는 -U를 포함해서 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777946507099&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;clean package -U&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;안정화 이후에는 불필요한 재조회를 줄이기 위해 -U를 제거한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777946516391&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;clean package&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;폐쇄망 + Nexus 환경&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부망에서 dependency preload를 먼저 진행하고, Nexus cache를 확보한 뒤, CI는 Nexus만 바라보도록 구성한다. --offline은 그 이후 선택적으로 사용한다.&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. 추가 팁: Maven 빌드 실패 = Maven 문제가 아닐 수 있다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 실패했다고 해서 항상 Maven 문제는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 에러가 뜨는 경우가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1777946542481&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Deployment problem: Auth fail&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 Maven 이슈가 아니다. 배포 인증 문제, 즉 Nexus / WAS / SSH 쪽을 먼저 확인해야 한다.&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;8. 정리&lt;/b&gt;&lt;/h2&gt;
&lt;div&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;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clean&lt;/td&gt;
&lt;td&gt;이전 빌드 결과 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;package&lt;/td&gt;
&lt;td&gt;컴파일 + 테스트 + 패키징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--offline&lt;/td&gt;
&lt;td&gt;로컬 저장소만 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-U&lt;/td&gt;
&lt;td&gt;캐시 무시 + 강제 재조회&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 옵션은 단순히 명령어가 아니라 Maven의 동작 방식을 이해하고 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt;는 왜 쓰는지 모르고 붙이는 것과 알고 쓰는 것의 차이가 크다.&lt;/p&gt;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/291</guid>
      <comments>https://bumday.tistory.com/291#entry291comment</comments>
      <pubDate>Tue, 5 May 2026 11:02:50 +0900</pubDate>
    </item>
    <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>
  </channel>
</rss>