<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko-KR"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://z9-durun.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://z9-durun.github.io/" rel="alternate" type="text/html" hreflang="ko-KR" /><updated>2026-02-24T16:55:33+09:00</updated><id>https://z9-durun.github.io/feed.xml</id><title type="html">durun.log</title><subtitle>프론트엔드 개발자의 기술 블로그. React, React Native, TypeScript, 테스트 자동화, 웹 분석 등 실무에서 겪은 문제 해결 과정과 개발 노하우를 공유합니다.</subtitle><entry><title type="html">GitHub 블로그 도메인 변경부터 SEO/GEO 전면 개편까지</title><link href="https://z9-durun.github.io/posts/github-blog-seo-geo-overhaul/" rel="alternate" type="text/html" title="GitHub 블로그 도메인 변경부터 SEO/GEO 전면 개편까지" /><published>2026-02-24T00:00:00+09:00</published><updated>2026-02-24T00:00:00+09:00</updated><id>https://z9-durun.github.io/posts/github-blog-seo-geo-overhaul</id><content type="html" xml:base="https://z9-durun.github.io/posts/github-blog-seo-geo-overhaul/"><![CDATA[<h2 id="tldr">TL;DR</h2>

<ul>
  <li>예전 GitHub 블로그 도메인(<code class="language-plaintext highlighter-rouge">ooyuo.github.io</code>)을 삭제했다가 다시 만들려니 Google Search Console에서 꼬여버렸어요</li>
  <li>어차피 신입 때 만든 계정명도 마음에 안 들어서 GitHub 아이디까지 바꿨습니다</li>
  <li>이왕 새로 시작하는 거 SEO/GEO를 제대로 해보자 싶어서, JSON-LD 구조화 데이터부터 IndexNow 자동 제출까지 전면 개편했어요</li>
</ul>

<hr />

<h2 id="문제-인식">문제 인식</h2>

<p>몇 년 전에 <code class="language-plaintext highlighter-rouge">ooyuo.github.io</code>로 GitHub Pages 블로그를 운영하고 있었는데, 어느 순간 티스토리로 갈아탔거든요. 그러면서 GitHub 레포도 자연스럽게 삭제했고, 도메인은 한동안 그냥 방치해뒀습니다.</p>

<p>그러다 다시 GitHub Pages로 돌아오려고 했어요. 같은 레포명으로 블로그를 만들었는데, 여기서 문제가 터집니다. Google Search Console에 이전 도메인이 여전히 남아 있었거든요. 이미 삭제한 속성인데 새 속성으로 등록하려니 소유권 인증부터 꼬이기 시작했어요.</p>

<p>결국 GitHub 아이디 자체를 <code class="language-plaintext highlighter-rouge">ooyuo</code>에서 <code class="language-plaintext highlighter-rouge">z9-durun</code>으로 변경했습니다. 사실 이전 아이디가 신입 때 아무 생각 없이 만든 거라 오래전부터 바꾸고 싶기도 했고요. 도메인이 <code class="language-plaintext highlighter-rouge">z9-durun.github.io</code>로 바뀌면서 Search Console 문제는 해결됐어요.</p>

<p>근데 나중에 알게 된 게 있습니다. Google Search Console에 <strong>‘주소 변경’</strong> 도구가 따로 있었어요. 기존 속성에서 새 속성으로의 이전을 Google에 직접 알려주는 기능인데, 이미 레포를 삭제하고 아이디까지 다 바꾼 뒤였습니다. 알았으면 좀 더 깔끔하게 처리했을 텐데요.</p>

<hr />

<h2 id="해결-방향-설계">해결 방향 설계</h2>

<p>도메인 변경을 하고 나니, 이왕 새로 시작하는 거 SEO를 제대로 해보고 싶더라고요. 이전 회사에서 GA4를 다뤘던 경험이 있어서 웹 분석 자체는 익숙했지만, 블로그에 구조화 데이터(GEO)까지 제대로 적용해 본 적은 없었거든요.</p>

<p><strong>“진짜 SEO/GEO를 제대로 하면 개인 블로그도 검색엔진에 잘 타는 걸까?”</strong></p>

<p>이게 이번 개편의 동기였습니다. 메타태그 몇 개 넣는 수준이 아니라, Schema.org 구조화 데이터를 제대로 설계해서 검색엔진이 내 콘텐츠를 정확하게 이해하도록 만들어보고 싶었어요.</p>

<p>목표로 잡은 건 이렇습니다:</p>

<ol>
  <li><strong>구조화 데이터 레이어</strong> — TechArticle, HowTo, FAQPage 등 콘텐츠 특성에 맞는 Schema.org 타입 적용</li>
  <li><strong>크롤링/인덱싱 최적화</strong> — robots.txt, sitemap, IndexNow로 검색엔진이 빠르게 새 글을 발견하도록</li>
  <li><strong>메타태그 정비</strong> — Open Graph, Twitter Card 이미지 보정, 시맨틱 HTML 수정</li>
  <li><strong>프론트매터 표준화</strong> — 포스트별로 구조화 데이터가 자동 활성화되는 구조</li>
</ol>

<p>전체 구조를 그려보면 이런 모양이에요:</p>

<pre><code class="language-mermaid">graph TB
    subgraph Layer3["🔍 크롤링/인덱싱"]
        robots["robots.txt"]
        sitemap["sitemap.xml"]
        feed["feed.xml"]
        indexnow["IndexNow API"]
    end

    subgraph Layer2["📊 구조화 데이터 (GEO)"]
        tech["TechArticle"]
        howto["HowTo"]
        faqpage["FAQPage"]
        website["WebSite + SearchAction"]
        bread["BreadcrumbList"]
    end

    subgraph Layer1["🏷️ 메타태그 (SEO)"]
        og["Open Graph"]
        twitter["Twitter Card"]
        canonical["Canonical URL"]
        seotag["jekyll-seo-tag"]
    end

    Layer1 --&gt; Browser["브라우저/SNS"]
    Layer2 --&gt; SearchEngine["검색엔진 리치 결과"]
    Layer3 --&gt; Crawler["검색엔진 크롤러"]
</code></pre>

<hr />

<h2 id="구현">구현</h2>

<h3 id="1-_configyml--기반-설정">1. _config.yml — 기반 설정</h3>

<p>도메인 변경과 함께 SEO 기반 설정을 정리했습니다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://z9-durun.github.io"</span>

<span class="na">github</span><span class="pi">:</span>
  <span class="na">username</span><span class="pi">:</span> <span class="s">z9-durun</span>

<span class="na">social</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">chole</span>
  <span class="na">email</span><span class="pi">:</span> <span class="s">z9.durun@gmail.com</span>
  <span class="na">links</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">https://github.com/z9-durun</span>

<span class="na">webmaster_verifications</span><span class="pi">:</span>
  <span class="na">google</span><span class="pi">:</span> <span class="s">YOUR_GOOGLE_VERIFICATION_CODE</span>

<span class="na">analytics</span><span class="pi">:</span>
  <span class="na">google</span><span class="pi">:</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">G-XXXXXXXXXX"</span>

<span class="na">plugins</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">jekyll-sitemap</span>   <span class="c1"># /sitemap.xml 자동 생성</span>
  <span class="pi">-</span> <span class="s">jekyll-feed</span>      <span class="c1"># /feed.xml 자동 생성 (신규 추가)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">jekyll-sitemap</code>은 원래 있었고, <code class="language-plaintext highlighter-rouge">jekyll-feed</code>를 새로 추가했어요. RSS 피드가 검색엔진이 새 콘텐츠를 발견하는 또 다른 경로가 되거든요.</p>

<h3 id="2-robotstxt--크롤러-안내">2. robots.txt — 크롤러 안내</h3>

<p>기존에는 robots.txt가 아예 없었습니다. 새로 만들었어요.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>User-agent: *
Allow: /

Sitemap: https://z9-durun.github.io/sitemap.xml
</pre></td></tr></tbody></table></code></pre></div></div>

<p>별거 없어 보이지만, 크롤러에게 sitemap 위치를 알려주는 역할을 합니다.</p>

<h3 id="3-json-ld-구조화-데이터--가장-공들인-부분">3. JSON-LD 구조화 데이터 — 가장 공들인 부분</h3>

<p>이번 개편에서 제일 시간을 많이 쓴 부분이에요. <code class="language-plaintext highlighter-rouge">_includes/seo-jsonld.html</code>을 만들어서 5가지 Schema.org 타입을 조건부로 출력하도록 설계했습니다.</p>

<p>페이지 타입에 따라 어떤 스키마가 붙는지 정리하면 이래요:</p>

<pre><code class="language-mermaid">flowchart LR
    Page["페이지 진입"]

    Page --&gt; IsPost{포스트?}
    Page --&gt; IsHome{홈?}
    Page --&gt; Always["BreadcrumbList\n(모든 페이지)"]

    IsPost --&gt;|Yes| TA["TechArticle"]
    IsPost --&gt;|Yes| HasHowto{howto 프론트매터?}
    IsPost --&gt;|Yes| HasFaq{faq 프론트매터?}

    HasHowto --&gt;|Yes| HT["HowTo"]
    HasFaq --&gt;|Yes| FAQ["FAQPage"]

    IsHome --&gt;|Yes| WS["WebSite\n+ SearchAction"]
</code></pre>

<p><strong>TechArticle (모든 포스트)</strong></p>

<p>기존에 흔히 쓰이는 <code class="language-plaintext highlighter-rouge">BlogPosting</code> 대신 <code class="language-plaintext highlighter-rouge">TechArticle</code> 타입을 선택했어요. 기술 블로그니까 더 정확한 시그널을 검색엔진에 보낼 수 있죠.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre>{% if page.layout == 'post' %}
<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"application/ld+json"</span><span class="nt">&gt;</span>
<span class="p">{</span>
  <span class="dl">"</span><span class="s2">@context</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://schema.org</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">TechArticle</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">headline</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ page.title | escape }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">description</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ page.description | default: page.excerpt | strip_html | truncate: 160 | escape }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">inLanguage</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ site.lang | default: 'ko-KR' }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">wordCount</span><span class="dl">"</span><span class="p">:</span> <span class="p">{{</span> <span class="nx">page</span><span class="p">.</span><span class="nx">content</span> <span class="o">|</span> <span class="nx">number_of_words</span> <span class="p">}},</span>
  <span class="dl">"</span><span class="s2">articleSection</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ page.categories | join: ', ' }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">author</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Person</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ site.social.name }}</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">jobTitle</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Frontend Developer</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">knowsAbout</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">React</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">React Native</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">TypeScript</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Frontend Development</span><span class="dl">"</span><span class="p">],</span>
    <span class="dl">"</span><span class="s2">sameAs</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span><span class="o">%</span> <span class="k">for</span> <span class="nx">link</span> <span class="k">in</span> <span class="nx">site</span><span class="p">.</span><span class="nx">social</span><span class="p">.</span><span class="nx">links</span> <span class="o">%</span><span class="p">}</span><span class="dl">"</span><span class="s2">{{ link }}</span><span class="dl">"</span><span class="p">{</span><span class="o">%</span> <span class="nx">unless</span> <span class="nx">forloop</span><span class="p">.</span><span class="nx">last</span> <span class="o">%</span><span class="p">},{</span><span class="o">%</span> <span class="nx">endunless</span> <span class="o">%</span><span class="p">}{</span><span class="o">%</span> <span class="nx">endfor</span> <span class="o">%</span><span class="p">}</span>
    <span class="p">]</span>
  <span class="p">},</span>
  <span class="dl">"</span><span class="s2">datePublished</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ page.date | date_to_xmlschema }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">dateModified</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ page.last_modified_at | default: page.date | date_to_xmlschema }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">keywords</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ page.tags | join: ', ' }}</span><span class="dl">"</span>
<span class="p">}</span>
<span class="nt">&lt;/script&gt;</span>
{% endif %}
</pre></td></tr></tbody></table></code></pre></div></div>

<p>여기서 포인트는 <code class="language-plaintext highlighter-rouge">inLanguage</code>, <code class="language-plaintext highlighter-rouge">wordCount</code>, <code class="language-plaintext highlighter-rouge">articleSection</code>, 저자의 <code class="language-plaintext highlighter-rouge">jobTitle</code>과 <code class="language-plaintext highlighter-rouge">knowsAbout</code>까지 채운 거예요. 검색엔진이 콘텐츠의 언어, 분량, 분야, 저자 전문성을 한눈에 파악할 수 있습니다.</p>

<p><strong>HowTo (프론트매터로 제어)</strong></p>

<p>포스트 프론트매터에 <code class="language-plaintext highlighter-rouge">howto:</code> 키를 넣으면 HowTo 구조화 데이터가 알아서 생성돼요.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="na">howto</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Storybook</span><span class="nv"> </span><span class="s">인터랙션</span><span class="nv"> </span><span class="s">테스트를</span><span class="nv"> </span><span class="s">함수형으로</span><span class="nv"> </span><span class="s">설계하는</span><span class="nv"> </span><span class="s">방법"</span>
  <span class="na">time</span><span class="pi">:</span> <span class="s2">"</span><span class="s">PT45M"</span>
  <span class="na">steps</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">TestFlow</span><span class="nv"> </span><span class="s">러너</span><span class="nv"> </span><span class="s">만들기"</span>
      <span class="na">text</span><span class="pi">:</span> <span class="s2">"</span><span class="s">테스트</span><span class="nv"> </span><span class="s">컨텍스트를</span><span class="nv"> </span><span class="s">관리하고</span><span class="nv"> </span><span class="s">스텝을</span><span class="nv"> </span><span class="s">순차</span><span class="nv"> </span><span class="s">실행하는</span><span class="nv"> </span><span class="s">createTestFlow</span><span class="nv"> </span><span class="s">함수를</span><span class="nv"> </span><span class="s">작성합니다."</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">공통</span><span class="nv"> </span><span class="s">인터랙션</span><span class="nv"> </span><span class="s">함수</span><span class="nv"> </span><span class="s">추출"</span>
      <span class="na">text</span><span class="pi">:</span> <span class="s2">"</span><span class="s">자주</span><span class="nv"> </span><span class="s">사용하는</span><span class="nv"> </span><span class="s">인터랙션을</span><span class="nv"> </span><span class="s">재사용</span><span class="nv"> </span><span class="s">가능한</span><span class="nv"> </span><span class="s">함수로</span><span class="nv"> </span><span class="s">분리합니다."</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre>{% if page.howto %}
<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"application/ld+json"</span><span class="nt">&gt;</span>
<span class="p">{</span>
  <span class="dl">"</span><span class="s2">@context</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://schema.org</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">HowTo</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ page.howto.name | default: page.title | escape }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">totalTime</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ page.howto.time | default: 'PT30M' }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">step</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span><span class="o">%</span> <span class="k">for</span> <span class="nx">step</span> <span class="k">in</span> <span class="nx">page</span><span class="p">.</span><span class="nx">howto</span><span class="p">.</span><span class="nx">steps</span> <span class="o">%</span><span class="p">}</span>
    <span class="p">{</span>
      <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">HowToStep</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">position</span><span class="dl">"</span><span class="p">:</span> <span class="p">{{</span> <span class="nx">forloop</span><span class="p">.</span><span class="nx">index</span> <span class="p">}},</span>
      <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ step.name | escape }}</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">text</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ step.text | escape }}</span><span class="dl">"</span>
    <span class="p">}{</span><span class="o">%</span> <span class="nx">unless</span> <span class="nx">forloop</span><span class="p">.</span><span class="nx">last</span> <span class="o">%</span><span class="p">},{</span><span class="o">%</span> <span class="nx">endunless</span> <span class="o">%</span><span class="p">}</span>
    <span class="p">{</span><span class="o">%</span> <span class="nx">endfor</span> <span class="o">%</span><span class="p">}</span>
  <span class="p">]</span>
<span class="p">}</span>
<span class="nt">&lt;/script&gt;</span>
{% endif %}
</pre></td></tr></tbody></table></code></pre></div></div>

<p>이 구조 덕분에 새 포스트를 쓸 때 프론트매터만 채우면 됩니다. 템플릿을 건드릴 필요가 없어요.</p>

<p><strong>FAQPage도 동일한 패턴</strong>인데요, 프론트매터에 <code class="language-plaintext highlighter-rouge">faq:</code> 키를 넣으면 FAQ 리치 결과를 노릴 수 있습니다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="na">faq</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">question</span><span class="pi">:</span> <span class="s2">"</span><span class="s">GitHub</span><span class="nv"> </span><span class="s">Pages</span><span class="nv"> </span><span class="s">도메인</span><span class="nv"> </span><span class="s">변경</span><span class="nv"> </span><span class="s">시</span><span class="nv"> </span><span class="s">Search</span><span class="nv"> </span><span class="s">Console은?"</span>
    <span class="na">answer</span><span class="pi">:</span> <span class="s2">"</span><span class="s">주소</span><span class="nv"> </span><span class="s">변경</span><span class="nv"> </span><span class="s">도구를</span><span class="nv"> </span><span class="s">사용하면</span><span class="nv"> </span><span class="s">됩니다..."</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>WebSite + SearchAction (홈 페이지)</strong></p>

<p>홈 페이지에는 사이트 내 검색 기능을 구조화 데이터로 노출했어요.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre>{% if page.layout == 'home' or page.url == '/' %}
<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"application/ld+json"</span><span class="nt">&gt;</span>
<span class="p">{</span>
  <span class="dl">"</span><span class="s2">@context</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://schema.org</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">WebSite</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ site.title }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">url</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ site.url }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">potentialAction</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SearchAction</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">target</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">EntryPoint</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">urlTemplate</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ site.url }}/search/?q={search_term_string}</span><span class="dl">"</span>
    <span class="p">},</span>
    <span class="dl">"</span><span class="s2">query-input</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">required name=search_term_string</span><span class="dl">"</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="nt">&lt;/script&gt;</span>
{% endif %}
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>BreadcrumbList</strong>는 모든 페이지에 출력돼서, 검색 결과에서 사이트 구조를 보여줍니다.</p>

<h3 id="4-open-graph--메타태그-정비">4. Open Graph / 메타태그 정비</h3>

<p><code class="language-plaintext highlighter-rouge">head.html</code>에서 두 가지를 손봤습니다.</p>

<p>첫째, <code class="language-plaintext highlighter-rouge">viewport</code> 메타태그에서 <code class="language-plaintext highlighter-rouge">user-scalable=no</code>를 제거했어요. 접근성(A11Y)과 Core Web Vitals 모두에 악영향을 주는 설정이거든요.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c">&lt;!-- Before --&gt;</span>
<span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"viewport"</span> <span class="na">content=</span><span class="s">"width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no"</span><span class="nt">&gt;</span>

<span class="c">&lt;!-- After --&gt;</span>
<span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"viewport"</span> <span class="na">content=</span><span class="s">"width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"</span><span class="nt">&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>둘째, 포스트별 이미지가 없을 때 <code class="language-plaintext highlighter-rouge">social_preview_image</code>를 <code class="language-plaintext highlighter-rouge">summary_large_image</code> 카드로 자동 업그레이드하도록 보정했습니다.</p>

<h3 id="5-시맨틱-html-수정">5. 시맨틱 HTML 수정</h3>

<p>홈 페이지의 포스트 카드 제목이 <code class="language-plaintext highlighter-rouge">&lt;h1&gt;</code>으로 되어 있었어요. 페이지당 <code class="language-plaintext highlighter-rouge">&lt;h1&gt;</code>은 하나여야 한다는 원칙이 있잖아요? 그래서 <code class="language-plaintext highlighter-rouge">&lt;h2&gt;</code>로 바꿨습니다.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c">&lt;!-- Before --&gt;</span>
<span class="nt">&lt;h1</span> <span class="na">class=</span><span class="s">"card-title my-2 mt-md-0"</span><span class="nt">&gt;</span>{{ post.title }}<span class="nt">&lt;/h1&gt;</span>

<span class="c">&lt;!-- After --&gt;</span>
<span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"card-title my-2 mt-md-0"</span><span class="nt">&gt;</span>{{ post.title }}<span class="nt">&lt;/h2&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>작은 변경이지만, 검색엔진이 헤딩 계층 구조를 올바르게 해석하는 데 영향을 줍니다.</p>

<h3 id="6-indexnow-자동-제출">6. IndexNow 자동 제출</h3>

<p>배포할 때마다 수동으로 색인 요청하는 건 너무 귀찮잖아요. GitHub Actions 워크플로우에 <code class="language-plaintext highlighter-rouge">indexnow</code> job을 추가했습니다.</p>

<p>전체 배포 파이프라인은 이런 흐름이에요:</p>

<pre><code class="language-mermaid">flowchart LR
    A["git push"] --&gt; B["Jekyll Build"]
    B --&gt; C["HTML 검증"]
    C --&gt; D["GitHub Pages\n배포"]
    D --&gt; E["30초 대기"]
    E --&gt; F["sitemap.xml에서\nURL 추출"]
    F --&gt; G["IndexNow API\n제출"]
    G --&gt; H["Bing/Yandex\n색인 요청"]
</code></pre>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="na">indexnow</span><span class="pi">:</span>
  <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
  <span class="na">needs</span><span class="pi">:</span> <span class="s">deploy</span>
  <span class="na">steps</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Wait for deployment to propagate</span>
      <span class="na">run</span><span class="pi">:</span> <span class="s">sleep </span><span class="m">30</span>

    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Submit URLs to IndexNow</span>
      <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">URLS=$(curl -s "https://z9-durun.github.io/sitemap.xml" \</span>
          <span class="s">| grep -oP '(?&lt;=&lt;loc&gt;)[^&lt;]+' | head -20)</span>

        <span class="s">URL_JSON=$(echo "$URLS" | jq -R -s -c 'split("\n") | map(select(length &gt; 0))')</span>

        <span class="s">curl -X POST "https://api.indexnow.org/IndexNow" \</span>
          <span class="s">-H "Content-Type: application/json; charset=utf-8" \</span>
          <span class="s">-d "{</span>
            <span class="s">\"host\": \"z9-durun.github.io\",</span>
            <span class="s">\"key\": \"fadc0d58c30a4a3ca2cc3625b7761549\",</span>
            <span class="s">\"urlList\": $URL_JSON</span>
          <span class="s">}"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>배포 완료 30초 후에 sitemap에서 URL을 추출해서 IndexNow API에 자동 제출하는 구조예요. Bing, Yandex 등 IndexNow를 지원하는 검색엔진에 바로 색인 요청이 갑니다.</p>

<h3 id="7-포스트-프론트매터-표준화">7. 포스트 프론트매터 표준화</h3>

<p>기존 포스트 전체에 다음 필드들을 추가했어요.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">포스트</span><span class="nv"> </span><span class="s">제목"</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">검색</span><span class="nv"> </span><span class="s">결과</span><span class="nv"> </span><span class="s">스니펫에</span><span class="nv"> </span><span class="s">노출될</span><span class="nv"> </span><span class="s">150자</span><span class="nv"> </span><span class="s">내외</span><span class="nv"> </span><span class="s">설명"</span>
<span class="na">date</span><span class="pi">:</span> <span class="s">2024-12-05</span>
<span class="na">categories</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">Development</span>
  <span class="pi">-</span> <span class="s">Git</span>
<span class="na">tags</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">Git</span>
  <span class="pi">-</span> <span class="s">Migration</span>
<span class="na">image</span><span class="pi">:</span>
  <span class="na">path</span><span class="pi">:</span> <span class="s">/assets/img/posts/2024-12-05/hero.webp</span>
  <span class="na">show</span><span class="pi">:</span> <span class="kc">false</span>
<span class="na">last_modified_at</span><span class="pi">:</span> <span class="s">2024-12-05</span>
<span class="na">howto</span><span class="pi">:</span>                    <span class="c1"># HowTo 구조화 데이터 (해당하는 글만)</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">방법</span><span class="nv"> </span><span class="s">이름"</span>
  <span class="na">time</span><span class="pi">:</span> <span class="s2">"</span><span class="s">PT20M"</span>
  <span class="na">steps</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">단계"</span>
      <span class="na">text</span><span class="pi">:</span> <span class="s2">"</span><span class="s">설명"</span>
<span class="nn">---</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">description</code>은 SEO 메타 설명으로 쓰이고, <code class="language-plaintext highlighter-rouge">last_modified_at</code>은 JSON-LD의 <code class="language-plaintext highlighter-rouge">dateModified</code>에 들어갑니다. <code class="language-plaintext highlighter-rouge">howto</code>가 있으면 HowTo 리치 결과를, <code class="language-plaintext highlighter-rouge">faq</code>가 있으면 FAQ 리치 결과를 자동으로 노리는 구조예요.</p>

<hr />

<h2 id="적용-결과">적용 결과</h2>

<p>개편하고 나서 달라진 점들이에요:</p>

<ul>
  <li><strong>5가지 Schema.org 타입</strong>이 페이지 특성에 맞게 자동으로 출력됩니다. <a href="https://search.google.com/test/rich-results">Google 리치 결과 테스트</a>에서 확인할 수 있어요.</li>
  <li><strong>프론트매터만 채우면</strong> 구조화 데이터가 활성화돼요. 템플릿 건드릴 필요 없이 <code class="language-plaintext highlighter-rouge">howto:</code>, <code class="language-plaintext highlighter-rouge">faq:</code> 키만 추가하면 됩니다.</li>
  <li><strong>배포하면 자동으로</strong> IndexNow에 URL이 제출됩니다. 수동 색인 요청은 이제 안 해도 돼요.</li>
  <li><strong>Open Graph 이미지</strong>가 모든 페이지에서 정상 동작합니다. SNS 공유할 때 의도한 이미지가 노출되고요.</li>
  <li>홈 페이지의 <strong>헤딩 계층 구조</strong>가 정리돼서 크롤러가 페이지 구조를 제대로 해석합니다.</li>
</ul>

<hr />

<h2 id="한계와-트레이드오프">한계와 트레이드오프</h2>

<p><strong>Google Search Console의 sitemap.xml 가져오기 실패.</strong>
새 속성을 등록하고 sitemap을 제출했는데, “가져올 수 없음” 상태가 뜨더라고요. 브라우저에서 직접 <code class="language-plaintext highlighter-rouge">sitemap.xml</code>에 접근하면 정상이고, robots.txt에도 위치가 명시되어 있는데 말이죠. 새 도메인에 대한 Google의 초기 크롤링 지연으로 보이는데, 보통 3~7일 정도 걸린다고 합니다. 일단 기다려보는 수밖에 없어요.</p>

<p><strong>도메인 변경 후 이전 URL의 크롤링 히스토리가 사라집니다.</strong> Search Console의 ‘주소 변경’ 도구를 썼으면 이전 도메인의 검색 순위를 일부 승계할 수 있었을 텐데, 이미 레포를 삭제한 뒤에 알게 됐거든요. 새 도메인에서 처음부터 쌓아가야 합니다.</p>

<p><strong>구조화 데이터가 검색 순위에 영향을 주는지는 솔직히 모르겠어요.</strong> Google은 “구조화 데이터는 랭킹 시그널이 아니다”라고 하는데, 리치 결과로 CTR이 올라가면 결국 도움이 되긴 하겠죠. 얼마나 효과가 있는지는 좀 지켜봐야 알 것 같습니다.</p>

<p><strong>IndexNow는 Google을 지원하지 않습니다.</strong> Bing, Yandex 등만 지원하고 Google은 자체 크롤링 정책을 고수하고 있거든요. Google 색인 속도에는 도움이 안 되지만, Bing 쪽에서는 효과를 볼 수 있을 거예요.</p>

<hr />

<h2 id="마무리">마무리</h2>

<p>도메인 변경이라는 삽질에서 시작했지만, 덕분에 블로그 SEO/GEO 기반을 제대로 잡게 됐습니다. 결국 만들고 싶었던 건 <strong>프론트매터만 채우면 구조화 데이터가 알아서 붙는 구조</strong>예요. 새 글 쓸 때 SEO 때문에 따로 뭘 할 필요가 없습니다.</p>

<p>Search Console 데이터가 쌓이면 실제 검색 유입 변화를 추적해보려고 합니다. 이 정도 수준의 SEO/GEO가 개인 블로그에 실제로 차이를 만드는지, 그 결과도 나중에 글로 남겨볼게요.</p>]]></content><author><name></name></author><category term="Development" /><category term="SEO" /><category term="SEO" /><category term="GEO" /><category term="Google Search Console" /><category term="JSON-LD" /><category term="Schema.org" /><category term="Jekyll" /><category term="GitHub Pages" /><category term="IndexNow" /><category term="Structured Data" /><summary type="html"><![CDATA[GitHub Pages 블로그 도메인 변경 후 Google Search Console 꼬임 해결, JSON-LD 구조화 데이터(TechArticle, HowTo, FAQPage, BreadcrumbList) 구현, IndexNow 자동 제출까지 SEO/GEO 전면 개편 과정.]]></summary></entry><entry><title type="html">Storybook 인터랙션 테스트, 함수형으로 설계하기</title><link href="https://z9-durun.github.io/posts/storybook-interaction-test-automation/" rel="alternate" type="text/html" title="Storybook 인터랙션 테스트, 함수형으로 설계하기" /><published>2025-12-09T00:00:00+09:00</published><updated>2025-12-09T00:00:00+09:00</updated><id>https://z9-durun.github.io/posts/storybook-interaction-test-automation</id><content type="html" xml:base="https://z9-durun.github.io/posts/storybook-interaction-test-automation/"><![CDATA[<h2 id="tldr">TL;DR</h2>

<ul>
  <li>컴포넌트 테스트를 수동으로 하다 보니 UI 변경마다 같은 검증을 반복하게 됐다</li>
  <li>함수형 테스트 스텝과 필드 매핑을 분리해서 <strong>변경 지점을 단일화</strong>했다</li>
  <li>레고 블록처럼 조립하는 구조로 <strong>시나리오 작성 시간을 대폭 줄였다</strong></li>
</ul>

<p><img src="/assets/img/posts/2025-12-09/storybook-interaction-test-automation_9.webp" alt="인터렉션 테스트 자동 실행" /></p>

<hr />

<h2 id="문제-인식">문제 인식</h2>

<p>Storybook으로 컴포넌트를 개발하면서, 매번 같은 검증을 수동으로 반복하고 있었다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>1. 주문자 정보 폼 열기
2. 이름, 이메일, 전화번호 입력
3. 값이 제대로 들어갔는지 확인
4. 체크박스 클릭
5. 체크 상태 확인
...
</pre></td></tr></tbody></table></code></pre></div></div>

<p>문제는 <strong>UI가 바뀔 때마다</strong> 이 과정을 처음부터 다시 해야 한다는 것이었다. placeholder 텍스트 하나 바뀌어도, 버튼 라벨 하나 바뀌어도 전체 플로우를 다시 확인해야 했다.</p>

<p>초기에는 Storybook의 <code class="language-plaintext highlighter-rouge">play</code> 함수에 직접 테스트 코드를 작성했다:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="c1">// 초기 방식: 스토리마다 중복 코드</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">Default</span><span class="p">:</span> <span class="nx">Story</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">play</span><span class="p">:</span> <span class="k">async </span><span class="p">({</span> <span class="nx">canvasElement</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nf">within</span><span class="p">(</span><span class="nx">canvasElement</span><span class="p">)</span>
    <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">userEvent</span><span class="p">.</span><span class="nf">setup</span><span class="p">()</span>

    <span class="kd">const</span> <span class="nx">nameInput</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nf">getByPlaceholderText</span><span class="p">(</span><span class="dl">'</span><span class="s1">이름을 입력해 주세요</span><span class="dl">'</span><span class="p">)</span>
    <span class="k">await</span> <span class="nx">user</span><span class="p">.</span><span class="nf">type</span><span class="p">(</span><span class="nx">nameInput</span><span class="p">,</span> <span class="dl">'</span><span class="s1">홍길동</span><span class="dl">'</span><span class="p">)</span>

    <span class="kd">const</span> <span class="nx">emailInput</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nf">getByPlaceholderText</span><span class="p">(</span><span class="dl">'</span><span class="s1">이메일을 입력해 주세요</span><span class="dl">'</span><span class="p">)</span>
    <span class="k">await</span> <span class="nx">user</span><span class="p">.</span><span class="nf">type</span><span class="p">(</span><span class="nx">emailInput</span><span class="p">,</span> <span class="dl">'</span><span class="s1">test@example.com</span><span class="dl">'</span><span class="p">)</span>

    <span class="c1">// ... 반복되는 코드</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>스토리가 10개, 20개로 늘어나면서 문제가 심각해졌다:</p>

<ul>
  <li><strong>중복</strong>: 같은 셀렉터 로직이 여러 파일에 흩어져 있다</li>
  <li><strong>취약성</strong>: placeholder 하나 바뀌면 관련 스토리 전부 수정해야 한다</li>
  <li><strong>가독성</strong>: 테스트 의도보다 DOM 조작 코드가 더 많다</li>
</ul>

<p><img src="/assets/img/posts/2025-12-09/storybook-interaction-test-automation_1.webp" alt="문제 상황 다이어그램" /></p>

<hr />

<h2 id="해결-방향-설계">해결 방향 설계</h2>

<p>목표를 세 가지로 잡았다:</p>

<ol>
  <li><strong>변경 지점 단일화</strong> - UI 텍스트가 바뀌어도 한 곳만 수정하면 된다</li>
  <li><strong>재사용 가능한 스텝</strong> - 레고 블록처럼 조립해서 시나리오를 만든다</li>
  <li><strong>선언적 테스트</strong> - “무엇을 테스트하는지”가 코드에서 바로 보인다</li>
</ol>

<p>핵심 아이디어는 <strong>함수형 테스트 스텝</strong>이었다. 각 스텝은:</p>
<ul>
  <li>상태를 갖지 않는 순수 함수</li>
  <li>context를 입력받아 작업을 수행</li>
  <li>여러 스텝을 순차적으로 조합 가능</li>
</ul>

<p><img src="/assets/img/posts/2025-12-09/storybook-interaction-test-automation_2.webp" alt="해결 구조 다이어그램" /></p>

<hr />

<h2 id="구현">구현</h2>

<h3 id="1-testflow-러너">1. TestFlow 러너</h3>

<p>테스트 컨텍스트를 관리하고 스텝들을 순차 실행하는 러너를 만들었다:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="c1">// src/shared/testing/test-flow.ts</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">TestFlowContext</span> <span class="p">{</span>
  <span class="nl">canvas</span><span class="p">:</span> <span class="nx">BoundFunctions</span><span class="o">&lt;</span><span class="k">typeof</span> <span class="nx">queries</span><span class="o">&gt;</span>  <span class="c1">// Testing Library 쿼리</span>
  <span class="nx">user</span><span class="p">:</span> <span class="nb">ReturnType</span><span class="o">&lt;</span><span class="k">typeof</span> <span class="nx">userEvent</span><span class="p">.</span><span class="nx">setup</span><span class="o">&gt;</span> <span class="c1">// 유저 이벤트</span>
  <span class="nx">data</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="nx">unknown</span><span class="o">&gt;</span>            <span class="c1">// 스텝 간 데이터 공유</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">type</span> <span class="nx">TestStep</span> <span class="o">=</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">:</span> <span class="nx">TestFlowContext</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nf">createTestFlow</span><span class="p">(</span><span class="nx">canvasElement</span><span class="p">:</span> <span class="nx">HTMLElement</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nf">within</span><span class="p">(</span><span class="nx">canvasElement</span><span class="p">)</span>
  <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">userEvent</span><span class="p">.</span><span class="nf">setup</span><span class="p">({</span> <span class="na">delay</span><span class="p">:</span> <span class="mi">50</span> <span class="p">})</span>
  <span class="kd">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">canvas</span><span class="p">,</span> <span class="nx">user</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{}</span> <span class="p">}</span>

  <span class="k">return</span> <span class="p">{</span>
    <span class="k">async</span> <span class="nf">run</span><span class="p">(...</span><span class="na">steps</span><span class="p">:</span> <span class="nx">TestStep</span><span class="p">[])</span> <span class="p">{</span>
      <span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">step</span> <span class="k">of</span> <span class="nx">steps</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">await</span> <span class="nf">step</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="2-공통-인터랙션-함수">2. 공통 인터랙션 함수</h3>

<p>자주 사용하는 인터랙션을 함수로 추출했다:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
</pre></td><td class="rouge-code"><pre><span class="c1">// src/shared/testing/interactions.ts</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">checkbox</span> <span class="o">=</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nf">click</span><span class="p">(</span><span class="na">ctx</span><span class="p">:</span> <span class="nx">TestFlowContext</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">el</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nf">getByRole</span><span class="p">(</span><span class="dl">'</span><span class="s1">checkbox</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">label</span> <span class="p">})</span>
    <span class="k">await</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nf">click</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span>
  <span class="p">},</span>

  <span class="k">async</span> <span class="nf">verifyChecked</span><span class="p">(</span><span class="na">ctx</span><span class="p">:</span> <span class="nx">TestFlowContext</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">el</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nf">getByRole</span><span class="p">(</span><span class="dl">'</span><span class="s1">checkbox</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">label</span> <span class="p">})</span>
    <span class="k">await</span> <span class="nf">expect</span><span class="p">(</span><span class="nx">el</span><span class="p">).</span><span class="nf">toBeChecked</span><span class="p">()</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nf">click</span><span class="p">(</span><span class="na">ctx</span><span class="p">:</span> <span class="nx">TestFlowContext</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">btn</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nf">getByRole</span><span class="p">(</span><span class="dl">'</span><span class="s1">button</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">label</span> <span class="p">})</span>
    <span class="k">await</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nf">click</span><span class="p">(</span><span class="nx">btn</span><span class="p">)</span>
  <span class="p">},</span>

  <span class="k">async</span> <span class="nf">verifyDisabled</span><span class="p">(</span><span class="na">ctx</span><span class="p">:</span> <span class="nx">TestFlowContext</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">btn</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nf">getByRole</span><span class="p">(</span><span class="dl">'</span><span class="s1">button</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">label</span> <span class="p">})</span>
    <span class="k">await</span> <span class="nf">expect</span><span class="p">(</span><span class="nx">btn</span><span class="p">).</span><span class="nf">toBeDisabled</span><span class="p">()</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">verify</span> <span class="o">=</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nf">textExists</span><span class="p">(</span><span class="na">ctx</span><span class="p">:</span> <span class="nx">TestFlowContext</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">element</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nf">getByText</span><span class="p">(</span><span class="nx">text</span><span class="p">)</span>
    <span class="k">await</span> <span class="nf">expect</span><span class="p">(</span><span class="nx">element</span><span class="p">).</span><span class="nf">toBeInTheDocument</span><span class="p">()</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="3-필드-매핑-분리-핵심">3. 필드 매핑 분리 (핵심)</h3>

<p><strong>UI 텍스트와 테스트 로직을 분리</strong>하는 것이 핵심이었다:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
</pre></td><td class="rouge-code"><pre><span class="c1">// src/features/order/ui/OrdererInfoSection/__stories__/orderer.interactions.ts</span>

<span class="c1">// 1. 필드 매핑 - UI 변경 시 여기만 수정</span>
<span class="kd">const</span> <span class="nx">ORDERER_PLACEHOLDERS</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">이름을 입력해 주세요</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">email</span><span class="p">:</span> <span class="dl">'</span><span class="s1">이메일을 입력해 주세요</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">phone</span><span class="p">:</span> <span class="dl">'</span><span class="s1">숫자만 입력해 주세요</span><span class="dl">'</span><span class="p">,</span>
<span class="p">}</span> <span class="kd">as const</span>

<span class="c1">// 2. 폼 인터랙션 인스턴스 생성</span>
<span class="kd">const</span> <span class="nx">ordererForm</span> <span class="o">=</span> <span class="nf">createFormInteractions</span><span class="p">(</span><span class="nx">ORDERER_PLACEHOLDERS</span><span class="p">,</span> <span class="dl">'</span><span class="s1">placeholder</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// 3. 개별 스텝 함수 - 재사용 가능</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">fillOrdererName</span> <span class="o">=</span> <span class="p">(</span><span class="nx">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">TestStep</span> <span class="o">=&gt;</span> <span class="k">async </span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">ordererForm</span><span class="p">.</span><span class="nf">fill</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">name</span><span class="dl">'</span><span class="p">,</span> <span class="nx">name</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">fillOrdererEmail</span> <span class="o">=</span> <span class="p">(</span><span class="nx">email</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">TestStep</span> <span class="o">=&gt;</span> <span class="k">async </span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">ordererForm</span><span class="p">.</span><span class="nf">fill</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">email</span><span class="dl">'</span><span class="p">,</span> <span class="nx">email</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">verifyOrdererName</span> <span class="o">=</span> <span class="p">(</span><span class="nx">expected</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">TestStep</span> <span class="o">=&gt;</span> <span class="k">async </span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">ordererForm</span><span class="p">.</span><span class="nf">verifyValue</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">name</span><span class="dl">'</span><span class="p">,</span> <span class="nx">expected</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 4. 복합 플로우 - 스텝 조합</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">completeOrdererForm</span> <span class="o">=</span> <span class="p">(</span><span class="nx">data</span><span class="p">:</span> <span class="nx">OrdererData</span><span class="p">):</span> <span class="nx">TestStep</span> <span class="o">=&gt;</span> <span class="k">async </span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">ordererForm</span><span class="p">.</span><span class="nf">fill</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">name</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">name</span><span class="p">)</span>
  <span class="k">await</span> <span class="nx">ordererForm</span><span class="p">.</span><span class="nf">fill</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">email</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">email</span><span class="p">)</span>
  <span class="k">await</span> <span class="nx">ordererForm</span><span class="p">.</span><span class="nf">fill</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">phone</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">phone</span><span class="p">)</span>

  <span class="k">if </span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">saveToProfile</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">await</span> <span class="nx">checkbox</span><span class="p">.</span><span class="nf">check</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">변경사항 회원 정보에 반영</span><span class="dl">'</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>placeholder가 “이름을 입력해 주세요”에서 “주문자 이름”으로 바뀌어도, <code class="language-plaintext highlighter-rouge">ORDERER_PLACEHOLDERS.name</code> 한 줄만 수정하면 된다.</p>

<p><img src="/assets/img/posts/2025-12-09/storybook-interaction-test-automation_4.webp" alt="필드 매핑 코드" /></p>

<h3 id="4-스토리에서-사용">4. 스토리에서 사용</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="c1">// OrdererInfoSection.stories.tsx</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">InteractionDemo</span><span class="p">:</span> <span class="nx">Story</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">play</span><span class="p">:</span> <span class="k">async </span><span class="p">({</span> <span class="nx">canvasElement</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">flow</span> <span class="o">=</span> <span class="nf">createTestFlow</span><span class="p">(</span><span class="nx">canvasElement</span><span class="p">)</span>

    <span class="k">await</span> <span class="nx">flow</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
      <span class="nf">completeOrdererForm</span><span class="p">({</span>
        <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">홍길동</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">email</span><span class="p">:</span> <span class="dl">'</span><span class="s1">test@example.com</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">phone</span><span class="p">:</span> <span class="dl">'</span><span class="s1">01012345678</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">saveToProfile</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="p">}),</span>
      <span class="nf">verifyOrdererFormFilled</span><span class="p">({</span>
        <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">홍길동</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">email</span><span class="p">:</span> <span class="dl">'</span><span class="s1">test@example.com</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">phone</span><span class="p">:</span> <span class="dl">'</span><span class="s1">01012345678</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">saveToProfile</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="p">})</span>
    <span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>테스트 의도가 명확하게 드러난다. “주문자 폼을 채우고, 채워진 값을 검증한다.”</p>

<p><img src="/assets/img/posts/2025-12-09/storybook-interaction-test-automation_9.webp" alt="스토리 실행 화면" /></p>

<h3 id="5-도메인별-인터랙션-모듈">5. 도메인별 인터랙션 모듈</h3>

<p>각 컴포넌트마다 <code class="language-plaintext highlighter-rouge">__stories__/*.interactions.ts</code> 파일을 만들어서 관리한다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre>src/features/order/ui/
├── OrdererInfoSection/
│   ├── __stories__/
│   │   └── orderer.interactions.ts    # 주문자 폼 테스트 스텝
│   ├── OrdererInfoSection.tsx
│   └── OrdererInfoSection.stories.tsx
├── OrderAgreementCard/
│   ├── __stories__/
│   │   └── agreement.interactions.ts  # 동의 체크박스 테스트 스텝
│   └── ...
</pre></td></tr></tbody></table></code></pre></div></div>

<p>동의 체크박스 테스트도 같은 패턴:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="c1">// agreement.interactions.ts</span>

<span class="kd">const</span> <span class="nx">LABELS</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">allAgree</span><span class="p">:</span> <span class="dl">'</span><span class="s1">주문 내용 확인 및 전체 동의</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">purchase</span><span class="p">:</span> <span class="dl">'</span><span class="s1">(필수) 구매조건 확인 및 결제 진행 동의</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">sellerPrivacy</span><span class="p">:</span> <span class="dl">'</span><span class="s1">(필수) 개인정보 판매자 제공 동의</span><span class="dl">'</span><span class="p">,</span>
  <span class="c1">// ...</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">clickAllAgree</span><span class="p">:</span> <span class="nx">TestStep</span> <span class="o">=</span> <span class="k">async </span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">checkbox</span><span class="p">.</span><span class="nf">click</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">LABELS</span><span class="p">.</span><span class="nx">allAgree</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">verifyAllChecked</span><span class="p">:</span> <span class="nx">TestStep</span> <span class="o">=</span> <span class="k">async </span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">checkbox</span><span class="p">.</span><span class="nf">verifyChecked</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">LABELS</span><span class="p">.</span><span class="nx">allAgree</span><span class="p">)</span>
  <span class="k">await</span> <span class="nx">checkbox</span><span class="p">.</span><span class="nf">verifyChecked</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">LABELS</span><span class="p">.</span><span class="nx">purchase</span><span class="p">)</span>
  <span class="k">await</span> <span class="nx">checkbox</span><span class="p">.</span><span class="nf">verifyChecked</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">LABELS</span><span class="p">.</span><span class="nx">sellerPrivacy</span><span class="p">)</span>
  <span class="c1">// ...</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">completeAllAgreements</span><span class="p">:</span> <span class="nx">TestStep</span> <span class="o">=</span> <span class="k">async </span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">checkbox</span><span class="p">.</span><span class="nf">check</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">LABELS</span><span class="p">.</span><span class="nx">allAgree</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><img src="/assets/img/posts/2025-12-09/storybook-interaction-test-automation_5.webp" alt="폴더 구조" /></p>

<hr />

<h2 id="적용-결과">적용 결과</h2>

<h3 id="변경-대응-시간-단축">변경 대응 시간 단축</h3>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>Before</th>
      <th>After</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>placeholder 변경</td>
      <td>관련 스토리 5개 수정</td>
      <td><code class="language-plaintext highlighter-rouge">PLACEHOLDERS</code> 1줄 수정</td>
    </tr>
    <tr>
      <td>버튼 라벨 변경</td>
      <td>관련 스토리 3개 수정</td>
      <td><code class="language-plaintext highlighter-rouge">LABELS</code> 1줄 수정</td>
    </tr>
    <tr>
      <td>새 폼 필드 추가</td>
      <td>각 스토리에 코드 추가</td>
      <td>스텝 함수 1개 추가</td>
    </tr>
  </tbody>
</table>

<h3 id="테스트-가독성-향상">테스트 가독성 향상</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c1">// Before: 의도 파악이 어려움</span>
<span class="kd">const</span> <span class="nx">input</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nf">getByPlaceholderText</span><span class="p">(</span><span class="dl">'</span><span class="s1">이름을 입력해 주세요</span><span class="dl">'</span><span class="p">)</span>
<span class="k">await</span> <span class="nx">user</span><span class="p">.</span><span class="nf">clear</span><span class="p">(</span><span class="nx">input</span><span class="p">)</span>
<span class="k">await</span> <span class="nx">user</span><span class="p">.</span><span class="nf">type</span><span class="p">(</span><span class="nx">input</span><span class="p">,</span> <span class="dl">'</span><span class="s1">홍길동</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">checkbox</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nf">getByRole</span><span class="p">(</span><span class="dl">'</span><span class="s1">checkbox</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="sr">/전체 동의/</span> <span class="p">})</span>
<span class="k">await</span> <span class="nx">user</span><span class="p">.</span><span class="nf">click</span><span class="p">(</span><span class="nx">checkbox</span><span class="p">)</span>
<span class="nf">expect</span><span class="p">(</span><span class="nx">checkbox</span><span class="p">).</span><span class="nf">toBeChecked</span><span class="p">()</span>

<span class="c1">// After: 의도가 명확함</span>
<span class="k">await</span> <span class="nx">flow</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
  <span class="nf">fillOrdererName</span><span class="p">(</span><span class="dl">'</span><span class="s1">홍길동</span><span class="dl">'</span><span class="p">),</span>
  <span class="nx">completeAllAgreements</span><span class="p">,</span>
  <span class="nx">verifyAllChecked</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="시나리오-조합의-유연성">시나리오 조합의 유연성</h3>

<p>여러 컴포넌트에 걸친 통합 테스트도 스텝 조합으로 가능:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">await</span> <span class="nx">flow</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
  <span class="c1">// 주문자 정보 입력</span>
  <span class="nf">completeOrdererForm</span><span class="p">(</span><span class="nx">ordererData</span><span class="p">),</span>

  <span class="c1">// 동의 체크</span>
  <span class="nx">completeAllAgreements</span><span class="p">,</span>

  <span class="c1">// 결제 수단 선택</span>
  <span class="nx">clickFirstCard</span><span class="p">,</span>

  <span class="c1">// 최종 검증</span>
  <span class="nf">verifyOrdererFormFilled</span><span class="p">(</span><span class="nx">ordererData</span><span class="p">),</span>
  <span class="nx">verifyAllChecked</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><img src="/assets/img/posts/2025-12-09/storybook-interaction-test-automation_7.webp" alt="레고 블록 비유 다이어그램" /></p>

<h2 id="한계와-트레이드오프">한계와 트레이드오프</h2>

<h3 id="초기-설정-비용">초기 설정 비용</h3>

<p>컴포넌트마다 <code class="language-plaintext highlighter-rouge">*.interactions.ts</code> 파일을 만들어야 한다. 단순한 컴포넌트에는 오버엔지니어링일 수 있다.</p>

<p><strong>기준</strong>: 스토리가 3개 이상이거나, UI 텍스트가 자주 바뀔 가능성이 있으면 분리한다.</p>

<h3 id="storybook-환경-한정">Storybook 환경 한정</h3>

<p>이 패턴은 Storybook 인터랙션 테스트(<code class="language-plaintext highlighter-rouge">play</code> 함수)에 최적화되어 있다. Playwright나 Cypress 같은 E2E 도구에서는 약간의 어댑터가 필요하다.</p>

<h3 id="비동기-타이밍">비동기 타이밍</h3>

<p>애니메이션이나 API 응답을 기다려야 하는 경우, <code class="language-plaintext highlighter-rouge">wait.forMs()</code>나 <code class="language-plaintext highlighter-rouge">wait.forText()</code> 같은 대기 함수를 적절히 사용해야 한다.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">export</span> <span class="kd">const</span> <span class="nx">wait</span> <span class="o">=</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nf">forText</span><span class="p">(</span><span class="na">ctx</span><span class="p">:</span> <span class="nx">TestFlowContext</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">timeout</span> <span class="o">=</span> <span class="mi">3000</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">await</span> <span class="nf">waitFor</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nf">getByText</span><span class="p">(</span><span class="nx">text</span><span class="p">),</span> <span class="p">{</span> <span class="nx">timeout</span> <span class="p">})</span>
  <span class="p">},</span>

  <span class="k">async</span> <span class="nf">forMs</span><span class="p">(</span><span class="na">ms</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">await</span> <span class="k">new</span> <span class="nc">Promise</span><span class="p">(</span><span class="nx">resolve</span> <span class="o">=&gt;</span> <span class="nf">setTimeout</span><span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">ms</span><span class="p">))</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="추가-기능">추가 기능</h2>

<h3 id="1-타입-안전성">1. 타입 안전성</h3>

<p>필드명 오타를 컴파일 타임에 잡을 수 있다. <code class="language-plaintext highlighter-rouge">createFormInteractions</code>가 제네릭으로 키를 추론하기 때문:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c1">// 필드 타입 정의</span>
<span class="kd">type</span> <span class="nx">OrdererFieldName</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">name</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">email</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">phone</span><span class="dl">'</span>

<span class="kd">const</span> <span class="nx">ORDERER_PLACEHOLDERS</span><span class="p">:</span> <span class="nx">FieldLabelMap</span><span class="o">&lt;</span><span class="nx">OrdererFieldName</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">이름을 입력해 주세요</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">email</span><span class="p">:</span> <span class="dl">'</span><span class="s1">이메일을 입력해 주세요</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">phone</span><span class="p">:</span> <span class="dl">'</span><span class="s1">숫자만 입력해 주세요</span><span class="dl">'</span><span class="p">,</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">ordererForm</span> <span class="o">=</span> <span class="nf">createFormInteractions</span><span class="p">(</span><span class="nx">ORDERER_PLACEHOLDERS</span><span class="p">,</span> <span class="dl">'</span><span class="s1">placeholder</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// 이제 오타를 잡아준다</span>
<span class="nx">ordererForm</span><span class="p">.</span><span class="nf">fill</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">namee</span><span class="dl">'</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span>  <span class="c1">// ❌ 타입 에러: 'namee'는 없음</span>
<span class="nx">ordererForm</span><span class="p">.</span><span class="nf">fill</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">name</span><span class="dl">'</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span>   <span class="c1">// ✅ OK</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">createFormInteractions</code> 구현:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">export</span> <span class="kd">function</span> <span class="nf">createFormInteractions</span><span class="o">&lt;</span><span class="nx">TFieldName</span> <span class="kd">extends</span> <span class="kr">string</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="nx">fieldMap</span><span class="p">:</span> <span class="nx">FieldLabelMap</span><span class="o">&lt;</span><span class="nx">TFieldName</span><span class="o">&gt;</span><span class="p">,</span>  <span class="c1">// Record&lt;TFieldName, string&gt;</span>
  <span class="nx">mode</span><span class="p">:</span> <span class="nx">FindByMode</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">label</span><span class="dl">'</span>
<span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="k">async</span> <span class="nf">fill</span><span class="p">(</span><span class="na">ctx</span><span class="p">:</span> <span class="nx">TestFlowContext</span><span class="p">,</span> <span class="na">fieldName</span><span class="p">:</span> <span class="nx">TFieldName</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// fieldName이 TFieldName으로 제한됨</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="2-디버깅-로그">2. 디버깅 로그</h3>

<p>스텝이 많아지면 어디서 실패했는지 찾기 어렵다. 각 스텝 실행 시 로깅을 추가했다:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="k">async</span> <span class="nf">run</span><span class="p">(...</span><span class="nx">steps</span><span class="p">:</span> <span class="nx">TestStep</span><span class="p">[])</span> <span class="p">{</span>
  <span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">step</span> <span class="k">of</span> <span class="nx">steps</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">stepName</span> <span class="o">=</span> <span class="nx">step</span><span class="p">.</span><span class="nx">name</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">anonymous</span><span class="dl">'</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`[TestFlow] ▶ </span><span class="p">${</span><span class="nx">stepName</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>

    <span class="k">try</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">step</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`[TestFlow] ✓ </span><span class="p">${</span><span class="nx">stepName</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">result</span><span class="p">)</span> <span class="nb">Object</span><span class="p">.</span><span class="nf">assign</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span>
    <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="s2">`[TestFlow] ✗ </span><span class="p">${</span><span class="nx">stepName</span><span class="p">}</span><span class="s2"> failed`</span><span class="p">)</span>
      <span class="k">throw</span> <span class="nx">error</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Storybook Interactions 패널에서 로그가 보이므로, 실패 지점을 빠르게 파악할 수 있다.</p>

<h3 id="3-스텝-간-데이터-공유">3. 스텝 간 데이터 공유</h3>

<p><code class="language-plaintext highlighter-rouge">TestFlowContext</code>의 <code class="language-plaintext highlighter-rouge">data</code> 필드를 활용하면 스텝 간 동적 값을 전달할 수 있다:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="c1">// 주문 생성 후 ID 저장</span>
<span class="kd">const</span> <span class="nx">createOrderAndSaveId</span><span class="p">:</span> <span class="nx">TestStep</span> <span class="o">=</span> <span class="k">async </span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">button</span><span class="p">.</span><span class="nf">click</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">주문하기</span><span class="dl">'</span><span class="p">)</span>
  <span class="k">await</span> <span class="nx">wait</span><span class="p">.</span><span class="nf">forText</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="dl">'</span><span class="s1">주문이 완료되었습니다</span><span class="dl">'</span><span class="p">)</span>

  <span class="kd">const</span> <span class="nx">orderId</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nf">getByTestId</span><span class="p">(</span><span class="dl">'</span><span class="s1">order-id</span><span class="dl">'</span><span class="p">).</span><span class="nx">textContent</span>
  <span class="k">return</span> <span class="p">{</span> <span class="nx">orderId</span> <span class="p">}</span>  <span class="c1">// data에 자동 병합됨</span>
<span class="p">}</span>

<span class="c1">// 저장된 ID로 검증</span>
<span class="kd">const</span> <span class="nx">verifyOrderInList</span><span class="p">:</span> <span class="nx">TestStep</span> <span class="o">=</span> <span class="k">async </span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">orderId</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">data</span> <span class="kd">as </span><span class="p">{</span> <span class="na">orderId</span><span class="p">:</span> <span class="kr">string</span> <span class="p">}</span>
  <span class="k">await</span> <span class="nx">verify</span><span class="p">.</span><span class="nf">textExists</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">orderId</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 조합해서 사용</span>
<span class="k">await</span> <span class="nx">flow</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
  <span class="nx">createOrderAndSaveId</span><span class="p">,</span>
  <span class="nx">navigateToOrderList</span><span class="p">,</span>
  <span class="nx">verifyOrderInList</span>  <span class="c1">// 위에서 저장한 orderId 사용</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="스텝-함수가-객체를-반환하면-contextdata에-병합되는-구조다">스텝 함수가 객체를 반환하면 <code class="language-plaintext highlighter-rouge">context.data</code>에 병합되는 구조다.</h2>

<h2 id="마무리">마무리</h2>

<p>함수형 테스트 스텝 패턴의 핵심은 <strong>관심사의 분리</strong>다:</p>

<ol>
  <li><strong>필드 매핑</strong> (<code class="language-plaintext highlighter-rouge">PLACEHOLDERS</code>, <code class="language-plaintext highlighter-rouge">LABELS</code>) - UI 텍스트 변경 담당</li>
  <li><strong>스텝 함수</strong> (<code class="language-plaintext highlighter-rouge">fill</code>, <code class="language-plaintext highlighter-rouge">click</code>, <code class="language-plaintext highlighter-rouge">verify</code>) - 테스트 로직 담당</li>
  <li><strong>복합 플로우</strong> (<code class="language-plaintext highlighter-rouge">complete*</code>) - 시나리오 조합 담당</li>
</ol>

<p>이렇게 분리하면 각 레이어가 독립적으로 변경 가능하고, 테스트 코드의 의도가 명확해진다.</p>

<p>수동 테스트에 지쳐있다면, 작은 것부터 자동화해보자. 가장 자주 반복하는 검증부터 스텝 함수로 만들어보면, 금방 패턴이 잡힌다.</p>

<p><img src="/assets/img/posts/2025-12-09/storybook-interaction-test-automation_8.webp" alt="3계층 구조 다이어그램" /></p>]]></content><author><name></name></author><category term="Development" /><category term="Testing" /><category term="Storybook" /><category term="Testing" /><category term="Interaction Test" /><category term="Functional Programming" /><category term="React" /><category term="Frontend" /><category term="Test Automation" /><summary type="html"><![CDATA[Storybook 인터랙션 테스트를 함수형으로 설계하여 재사용성을 높이는 방법. 레고 블록처럼 조립하는 테스트 시나리오 작성 패턴.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://z9-durun.github.io/assets/img/posts/2025-12-09/storybook-interaction-test-automation_9.webp" /><media:content medium="image" url="https://z9-durun.github.io/assets/img/posts/2025-12-09/storybook-interaction-test-automation_9.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Channel Talk 업데이트로 인한 Maven Repository 설정 이슈 해결하기</title><link href="https://z9-durun.github.io/posts/channel-talk-maven-repository-issue/" rel="alternate" type="text/html" title="Channel Talk 업데이트로 인한 Maven Repository 설정 이슈 해결하기" /><published>2024-12-05T00:00:00+09:00</published><updated>2024-12-05T00:00:00+09:00</updated><id>https://z9-durun.github.io/posts/channel-talk-maven-repository-issue</id><content type="html" xml:base="https://z9-durun.github.io/posts/channel-talk-maven-repository-issue/"><![CDATA[<h2 id="문제-상황">문제 상황</h2>

<p>최근 팀 프로젝트에서 흥미로운 이슈를 마주했다. 새로운 팀원이 <code class="language-plaintext highlighter-rouge">package.json</code>의 디펜던시를 업데이트하는 과정에서 Channel Talk 버전이 올라갔고, 그 이후부터 프로젝트 실행에 문제가 발생했다.</p>

<p><img src="/assets/img/posts/2024-12-05/2024-12-05-channel-talk-maven-repository-issue_2.webp" alt="Channel Talk 업데이트 후 빌드 오류 화면" /></p>

<h2 id="원인-파악">원인 파악</h2>

<p>문제 해결을 위해 Channel Talk 공식 문서를 확인해보니, v0.9.0부터 설치 방법이 크게 변경되었다는 것을 발견했다. 특히 Maven repository 설정에 중요한 변화가 있었다.</p>

<p>주요 변경사항:</p>

<ul>
  <li>Maven Central 지원이 2025년 8월 1일부터 중단될 예정이다.</li>
  <li>새로운 maven repository(<a href="https://maven.channel.io/">maven.channel.io</a>)를 사용해야 한다.</li>
</ul>

<p><img src="/assets/img/posts/2024-12-05/2024-12-05-channel-talk-maven-repository-issue_3.webp" alt="Channel Talk 공식 문서의 Maven Repository 변경 안내" /></p>

<h2 id="해결-과정">해결 과정</h2>

<p>해결을 위해 <code class="language-plaintext highlighter-rouge">android/build.gradle</code> 파일을 수정해야 했다. 기존의 Maven Central 설정을 새로운 Channel Talk repository 설정으로 업데이트했다.</p>

<p>수정된 내용:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="nx">buildscript</span> <span class="p">{</span>
    <span class="nx">repositories</span> <span class="p">{</span>
        <span class="nf">google</span><span class="p">()</span>
        <span class="nf">mavenCentral</span><span class="p">()</span>
        <span class="nx">maven</span> <span class="p">{</span>
            <span class="nx">url</span> <span class="dl">'</span><span class="s1">&lt;https://maven.channel.io/maven2&gt;</span><span class="dl">'</span>
            <span class="nx">name</span> <span class="dl">'</span><span class="s1">ChannelTalk</span><span class="dl">'</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="nx">allprojects</span> <span class="p">{</span>
    <span class="nx">repositories</span> <span class="p">{</span>
        <span class="c1">// 기존 repository 설정들...</span>
        <span class="nx">maven</span> <span class="p">{</span>
            <span class="nx">url</span> <span class="dl">'</span><span class="s1">&lt;https://maven.channel.io/maven2&gt;</span><span class="dl">'</span>
            <span class="nx">name</span> <span class="dl">'</span><span class="s1">ChannelTalk</span><span class="dl">'</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

</pre></td></tr></tbody></table></code></pre></div></div>

<p><img src="/assets/img/posts/2024-12-05/2024-12-05-channel-talk-maven-repository-issue_4.webp" alt="Gradle 설정 수정 후 정상 빌드 결과" /></p>

<p><code class="language-plaintext highlighter-rouge">devrepo.kakao.com</code>을 참조하고 있는데, 이건 예전 저장소라 삭제했다.</p>

<h2 id="결과">결과</h2>

<p>gradle 설정을 수정한 후, 다음 명령어로 빌드 캐시를 정리하고 프로젝트를 재실행했다:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="nb">cd </span>android
./gradlew clean
<span class="nb">cd</span> ..
npm run android
</pre></td></tr></tbody></table></code></pre></div></div>

<p>이후 프로젝트가 정상적으로 실행되는 것을 확인할 수 있었다.</p>

<h2 id="교훈">교훈</h2>

<p>이번 이슈를 통해 몇 가지 중요한 점을 배웠다:</p>

<ol>
  <li>디펜던시 업그레이드 시 해당 라이브러리의 메이저 버전 변경사항을 반드시 확인해야 한다.</li>
  <li>공식 문서를 꼼꼼히 확인하는 것이 문제 해결의 지름길이다.</li>
  <li>Android 프로젝트에서 Maven repository 설정은 매우 중요한 부분이므로 신중하게 관리해야 한다.</li>
</ol>

<h2 id="참고-자료">참고 자료</h2>

<ul>
  <li>Channel Talk 공식 문서: https://developers.channel.io/docs</li>
  <li>React Native 설정 가이드: https://reactnative.dev/docs/environment-setup</li>
</ul>]]></content><author><name></name></author><category term="Troubleshooting" /><category term="Development" /><category term="React Native" /><category term="Android" /><category term="Channel Talk" /><category term="Maven" /><category term="Dependency Management" /><category term="Gradle" /><category term="Mobile Development" /><category term="Technical Issue" /><summary type="html"><![CDATA[Channel Talk v0.9.0 업데이트로 인한 Maven Repository 설정 변경 이슈 해결 방법. React Native Android 빌드 오류 대응 가이드.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://z9-durun.github.io/assets/img/posts/2024-12-05/2024-12-05-channel-talk-maven-repository-issue_2.webp" /><media:content medium="image" url="https://z9-durun.github.io/assets/img/posts/2024-12-05/2024-12-05-channel-talk-maven-repository-issue_2.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Git Repository 이전하기: 커밋 히스토리 유지와 충돌 해결하기</title><link href="https://z9-durun.github.io/posts/git-repository-migration-with-history/" rel="alternate" type="text/html" title="Git Repository 이전하기: 커밋 히스토리 유지와 충돌 해결하기" /><published>2024-12-05T00:00:00+09:00</published><updated>2024-12-05T00:00:00+09:00</updated><id>https://z9-durun.github.io/posts/git-repository-migration-with-history</id><content type="html" xml:base="https://z9-durun.github.io/posts/git-repository-migration-with-history/"><![CDATA[<h2 id="배경">배경</h2>

<p>최근 팀 내 인프라 개편으로 인해 기존에 관리하던 GitLab 레포지토리의 권한이 인프라팀으로 이관되었다. 이에 따라 기존 레포지토리 A에서 새로운 레포지토리 B로 프로젝트를 이전해야 하는 상황이 발생했다.</p>

<h2 id="문제-상황">문제 상황</h2>

<p>레포지토리를 이전하면서 마주친 까다로운 점은 다음과 같았다:</p>

<p><img src="/assets/img/posts/2024-12-05/2024-12-05-git-repository-migration-with-history.webp" alt="Git 레포지토리 이전 시 히스토리 충돌 상황" /></p>

<ol>
  <li>기존 레포지토리 A의 커밋 히스토리를 모두 유지해야 했다.</li>
  <li>새로운 레포지토리 B에는 이미 누군가가 레포지토리 A의 폴더 내용을 복사해서 푸시해둔 상태였다.</li>
  <li>레포지토리 B에는 이미 몇 가지 수정사항이 추가되어 있었다.</li>
</ol>

<h2 id="해결-과정">해결 과정</h2>

<p>이 문제를 해결하기 위해 다음과 같은 단계로 진행했다.</p>

<h3 id="1-새-레포지토리-준비">1. 새 레포지토리 준비</h3>

<p>먼저 레포지토리 B를 로컬에 클론했다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>git clone <span class="o">[</span>repository-B-url]
<span class="nb">cd</span> <span class="o">[</span>repository-B-directory]
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="2-기존-레포지토리-연결">2. 기존 레포지토리 연결</h3>

<p>레포지토리 A를 리모트로 추가했다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>git remote add old-repo <span class="o">[</span>repository-A-url]
git fetch old-repo
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="3-히스토리-병합">3. 히스토리 병합</h3>

<p>기존 히스토리를 가져오기 위해 임시 브랜치를 만들고 병합을 진행했다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># 레포지토리 A의 히스토리를 새 브랜치로 가져오기</span>
git checkout <span class="nt">-b</span> temp-branch old-repo/main

<span class="c"># 메인 브랜치로 돌아가기</span>
git checkout main

<span class="c"># 두 히스토리 병합하기</span>
git merge temp-branch <span class="nt">--allow-unrelated-histories</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="4-충돌-해결">4. 충돌 해결</h3>

<p>예상대로 많은 파일에서 충돌이 발생했다. 다음 단계로 충돌을 해결했다:</p>

<ol>
  <li>충돌 파일 확인</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>git status
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>각 충돌 파일을 열어 수동으로 해결
    <ul>
      <li><code class="language-plaintext highlighter-rouge">&lt;&lt;&lt;&lt;&lt;&lt;&lt;</code>, <code class="language-plaintext highlighter-rouge">=======</code>, <code class="language-plaintext highlighter-rouge">&gt;&gt;&gt;&gt;&gt;&gt;&gt;</code> 마커로 표시된 부분을 확인</li>
      <li>최신 변경사항을 유지하면서 필요한 히스토리는 보존</li>
    </ul>
  </li>
  <li>충돌 해결 후 커밋</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>git add <span class="nb">.</span>
git commit <span class="nt">-m</span> <span class="s2">"Merge repository A history with existing changes"</span>
git push origin main
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="배운-점">배운 점</h2>

<ol>
  <li><code class="language-plaintext highlighter-rouge">-allow-unrelated-histories</code> 옵션의 중요성
    <ul>
      <li>서로 관련 없는 두 저장소의 히스토리를 병합할 때 필수적인 옵션이었다.</li>
    </ul>
  </li>
  <li>사전 준비의 중요성
    <ul>
      <li>중요 데이터는 반드시 백업해두어야 한다.</li>
      <li>팀원들과의 커뮤니케이션이 필요하다.</li>
    </ul>
  </li>
  <li>체계적인 접근
    <ul>
      <li>단계별로 진행하고 각 단계에서 결과를 확인하는 것이 중요하다.</li>
      <li>문제 발생 시 이전 단계로 돌아갈 수 있도록 준비해야 한다.</li>
    </ul>
  </li>
</ol>

<h2 id="결론">결론</h2>

<p>Git을 사용하다 보면 레포지토리 이전이나 병합 같은 작업은 피할 수 없다. 이런 상황에서 커밋 히스토리를 잃지 않고 안전하게 이전하는 것은 매우 중요하다. 이번 경험을 통해 Git의 고급 기능을 활용하는 방법과 체계적인 문제 해결 과정의 중요성을 다시 한번 배울 수 있었다.</p>]]></content><author><name></name></author><category term="Development" /><category term="Git" /><category term="Git" /><category term="Repository Migration" /><category term="Version Control" /><category term="Git History" /><category term="Merge Conflicts" /><category term="DevOps" /><category term="Git Commands" /><category term="Technical Guide" /><summary type="html"><![CDATA[Git 레포지토리를 이전할 때 커밋 히스토리를 유지하면서 충돌을 해결하는 방법. git remote, fetch, merge 명령어 활용 가이드.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://z9-durun.github.io/assets/img/posts/2024-12-05/2024-12-05-git-repository-migration-with-history.webp" /><media:content medium="image" url="https://z9-durun.github.io/assets/img/posts/2024-12-05/2024-12-05-git-repository-migration-with-history.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">GA4와 GTM 적용기 - 웹뷰 프로젝트의 사용자 분석</title><link href="https://z9-durun.github.io/posts/ga4-gtm-integration-for-webview-project/" rel="alternate" type="text/html" title="GA4와 GTM 적용기 - 웹뷰 프로젝트의 사용자 분석" /><published>2024-12-04T00:00:00+09:00</published><updated>2024-12-04T00:00:00+09:00</updated><id>https://z9-durun.github.io/posts/ga4-gtm-integration-for-webview-project</id><content type="html" xml:base="https://z9-durun.github.io/posts/ga4-gtm-integration-for-webview-project/"><![CDATA[<h2 id="배경">배경</h2>

<p>최근 회사에서 흥미로운 협업 프로젝트를 진행했다. 우리 팀이 개발한 웹 애플리케이션이 파트너사의 네이티브 앱 내 웹뷰로 서비스되는 구조였다. 이 프로젝트는 자동차 구매 플랫폼으로, 사용자가 차량 견적을 내고 신차 상담을 신청할 수 있는 서비스다.</p>

<p>프로젝트가 오픈되고 나서 가장 큰 고민은 “실제로 얼마나 많은 사용자가 우리 서비스를 이용하고 있을까?”였다. 특히 웹뷰 환경이다 보니 일반적인 웹사이트보다 사용자 행동 패턴을 파악하기가 더 까다로웠다. 이 문제를 해결하기 위해 Google Analytics 4(GA4)와 Google Tag Manager(GTM)를 도입하게 됐다.</p>

<h2 id="기술-스택-선정">기술 스택 선정</h2>

<p>React 프로젝트에 GA4를 적용하기 위해 다음과 같은 도구들을 선택했다:</p>

<ul>
  <li><a href="https://www.npmjs.com/package/react-ga">react-ga</a>: Google Analytics와 React를 연동하기 위한 라이브러리</li>
  <li><a href="https://www.npmjs.com/package/react-gtm-module">react-gtm-module</a>: Google Tag Manager 설정을 위한 라이브러리</li>
  <li><a href="https://www.npmjs.com/package/@types/react-gtm-module">@types/react-gtm-module</a>: react-gtm-module의 TypeScript 타입 정의</li>
</ul>

<h2 id="구현-과정">구현 과정</h2>

<h3 id="1-google-analytics-속성-및-데이터-스트림-설정">1. Google Analytics 속성 및 데이터 스트림 설정</h3>

<p>먼저 Google Analytics에서 데이터를 수집하기 위한 기본 설정을 했다:</p>

<ol>
  <li>
    <p>Google Analytics(<a href="https://analytics.google.com/">analytics.google.com</a>)에 접속하여 로그인</p>
  </li>
  <li>
    <p>관리 &gt; 만들기 &gt; 속성 클릭
<img src="/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_1.webp" alt="관리 &gt; 만들기 &gt; 속성 클릭" /></p>
  </li>
  <li>속성 설정:
<img src="/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_3.webp" alt="속성 설정" />
    <ul>
      <li>속성 이름(필수): [속성 이름]</li>
      <li>보고 시간대: “대한민국”</li>
      <li>통화: “KRW-한국 원”</li>
    </ul>
  </li>
  <li>
    <p>비즈니스 세부정보 입력:
<img src="/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_4.webp" alt="비즈니스 세부정보 입력" /></p>
  </li>
  <li>
    <p>플랫폼 선택:
<img src="/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_5.webp" alt="플랫폼 선택" /></p>
  </li>
  <li>
    <p>웹사이트 URL 정보 입력:
<img src="/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_6.webp" alt="웹사이트 URL 정보 입력" /></p>
  </li>
  <li><code class="language-plaintext highlighter-rouge">index.html</code>의 <code class="language-plaintext highlighter-rouge">&lt;head&gt;&lt;/head&gt;</code>안에 아래 코드를 복사해서 삽입:
<img src="/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_7.webp" alt="`index.html`의 `&lt;head&gt;&lt;/head&gt;`안에 아래 코드를 복사해서 삽입" /></li>
</ol>

<p>데이터 스트림을 생성하면 “측정 ID(G-로 시작하는 ID)”가 발급된다. 이 ID는 GA4 구현에 필요한 핵심 정보다.</p>

<h3 id="2-환경-변수-설정">2. 환경 변수 설정</h3>

<p>먼저 개발 환경과 프로덕션 환경에서 각각 다른 GA4 추적 ID를 사용할 수 있도록 환경 변수를 설정했다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>VITE_GA_TRACKING_ID=G-98CRVRP3CW
VITE_GTM_ID=GTM-MM88PWSZ
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="3-typescript-환경-설정">3. TypeScript 환경 설정</h3>

<p>Vite와 TypeScript를 사용하는 환경에서는 환경 변수 타입을 정의해야 한다. 이를 위해 <code class="language-plaintext highlighter-rouge">vite-env.d.ts</code> 파일에 다음과 같은 타입 정의를 추가했다:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c1">/// &lt;reference types="vite/client" /&gt;</span>

<span class="kr">interface</span> <span class="nx">ImportMetaEnv</span> <span class="p">{</span>
  <span class="k">readonly</span> <span class="nx">VITE_GTM_ID</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="kr">interface</span> <span class="nx">ImportMeta</span> <span class="p">{</span>
  <span class="k">readonly</span> <span class="nx">env</span><span class="p">:</span> <span class="nx">ImportMetaEnv</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="4-google-analytics와-gtm-초기-설정">4. Google Analytics와 GTM 초기 설정</h3>

<p>index.html에 GA4와 GTM 스크립트를 모두 추가했다. GA4는 gtag.js를 통해 설정하고, GTM은 별도의 스크립트를 추가했다:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c">&lt;!-- Google tag (gtag.js) --&gt;</span>
<span class="nt">&lt;script </span><span class="na">async</span> <span class="na">src=</span><span class="s">"&lt;https://www.googletagmanager.com/gtag/js?id=[측정 ID]&gt;"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script&gt;</span>
  <span class="nb">window</span><span class="p">.</span><span class="nx">dataLayer</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">dataLayer</span> <span class="o">||</span> <span class="p">[];</span>
  <span class="kd">function</span> <span class="nf">gtag</span><span class="p">()</span> <span class="p">{</span>
    <span class="nx">dataLayer</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">arguments</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="nf">gtag</span><span class="p">(</span><span class="dl">'</span><span class="s1">js</span><span class="dl">'</span><span class="p">,</span> <span class="k">new</span> <span class="nc">Date</span><span class="p">());</span>
  <span class="nf">gtag</span><span class="p">(</span><span class="dl">'</span><span class="s1">config</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">[측정 ID]</span><span class="dl">'</span><span class="p">);</span>
<span class="nt">&lt;/script&gt;</span>


</pre></td></tr></tbody></table></code></pre></div></div>

<p>또한 noscript 태그를 사용해 JavaScript가 비활성화된 환경에서도 GTM이 동작할 수 있도록 했다:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre>
<span class="c">&lt;!-- Google Tag Manager --&gt;</span>
<span class="c">&lt;!-- head 태그안에 추가 --&gt;</span>
<span class="nt">&lt;script&gt;</span>
  <span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="nx">d</span><span class="p">,</span><span class="nx">s</span><span class="p">,</span><span class="nx">l</span><span class="p">,</span><span class="nx">i</span><span class="p">){</span><span class="nx">w</span><span class="p">[</span><span class="nx">l</span><span class="p">]</span><span class="o">=</span><span class="nx">w</span><span class="p">[</span><span class="nx">l</span><span class="p">]</span><span class="o">||</span><span class="p">[];</span><span class="nx">w</span><span class="p">[</span><span class="nx">l</span><span class="p">].</span><span class="nf">push</span><span class="p">({</span><span class="dl">'</span><span class="s1">gtm.start</span><span class="dl">'</span><span class="p">:</span>
  <span class="k">new</span> <span class="nc">Date</span><span class="p">().</span><span class="nf">getTime</span><span class="p">(),</span><span class="na">event</span><span class="p">:</span><span class="dl">'</span><span class="s1">gtm.js</span><span class="dl">'</span><span class="p">});</span><span class="kd">var</span> <span class="nx">f</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nf">getElementsByTagName</span><span class="p">(</span><span class="nx">s</span><span class="p">)[</span><span class="mi">0</span><span class="p">],</span>
  <span class="nx">j</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nf">createElement</span><span class="p">(</span><span class="nx">s</span><span class="p">),</span><span class="nx">dl</span><span class="o">=</span><span class="nx">l</span><span class="o">!=</span><span class="dl">'</span><span class="s1">dataLayer</span><span class="dl">'</span><span class="p">?</span><span class="dl">'</span><span class="s1">&amp;l=</span><span class="dl">'</span><span class="o">+</span><span class="nx">l</span><span class="p">:</span><span class="dl">''</span><span class="p">;</span><span class="nx">j</span><span class="p">.</span><span class="k">async</span><span class="o">=</span><span class="kc">true</span><span class="p">;</span><span class="nx">j</span><span class="p">.</span><span class="nx">src</span><span class="o">=</span>
  <span class="dl">'</span><span class="s1">&lt;https://www.googletagmanager.com/gtm.js?id=</span><span class="dl">'</span><span class="o">+</span><span class="nx">i</span><span class="o">+</span><span class="nx">dl</span><span class="p">;</span><span class="nx">f</span><span class="p">.</span><span class="nx">parentNode</span><span class="p">.</span><span class="nf">insertBefore</span><span class="p">(</span><span class="nx">j</span><span class="p">,</span><span class="nx">f</span><span class="p">)</span><span class="o">&gt;</span><span class="p">;</span>
  <span class="p">})(</span><span class="nb">window</span><span class="p">,</span><span class="nb">document</span><span class="p">,</span><span class="dl">'</span><span class="s1">script</span><span class="dl">'</span><span class="p">,</span><span class="dl">'</span><span class="s1">dataLayer</span><span class="dl">'</span><span class="p">,</span><span class="dl">'</span><span class="s1">[측정 ID]</span><span class="dl">'</span><span class="p">);</span>
<span class="nt">&lt;/script&gt;</span>


<span class="c">&lt;!-- Google Tag Manager (noscript) --&gt;</span>
<span class="c">&lt;!-- 닫는 body 태그 바로 위 추가 --&gt;</span>
<span class="nt">&lt;noscript&gt;</span>
  <span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"&lt;https://www.googletagmanager.com/ns.html?id=[측정 ID]&gt;"</span>
    <span class="na">height=</span><span class="s">"0"</span> <span class="na">width=</span><span class="s">"0"</span> <span class="na">style=</span><span class="s">"display:none;visibility:hidden"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;/iframe&gt;</span>
<span class="nt">&lt;/noscript&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="5-react-애플리케이션에서의-초기화">5. React 애플리케이션에서의 초기화</h3>

<p>애플리케이션의 진입점인 main.tsx에서 GA4와 GTM을 초기화했다:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="c1">// GA 초기화</span>
<span class="kd">const</span> <span class="nx">gaTrackingId</span> <span class="o">=</span> <span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VITE_GA_TRACKING_ID</span><span class="p">;</span>
<span class="nx">ReactGA</span><span class="p">.</span><span class="nf">initialize</span><span class="p">(</span><span class="nx">gaTrackingId</span><span class="p">);</span>

<span class="c1">// 히스토리 설정으로 페이지 추적</span>
<span class="kd">const</span> <span class="nx">history</span> <span class="o">=</span> <span class="nf">createBrowserHistory</span><span class="p">();</span>
<span class="nx">history</span><span class="p">.</span><span class="nf">listen</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">ReactGA</span><span class="p">.</span><span class="nf">set</span><span class="p">({</span> <span class="na">page</span><span class="p">:</span> <span class="nx">response</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">pathname</span> <span class="p">});</span>
  <span class="nx">ReactGA</span><span class="p">.</span><span class="nf">pageview</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">pathname</span><span class="p">);</span>
<span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="6-이벤트-트래킹-구현">6. 이벤트 트래킹 구현</h3>

<p>특히 중요했던 것은 상담 신청 버튼 클릭 이벤트의 추적이었다. GTM에서 태그와 트리거를 만들었다.</p>

<ul>
  <li>
    <p>태그
<img src="/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_8.webp" alt="태그" /></p>
  </li>
  <li>
    <p>트리거
<img src="/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_9.webp" alt="트리거" />
여기서 보이는 트리거의 이름이 아래 유틸리티 함수의 event명이 된다.</p>
  </li>
</ul>

<p>그리고 이 트리거를 위한 별도의 유틸리티 함수를 만들어 관리했다:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">export</span> <span class="kd">const</span> <span class="nx">sendGAEvent</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">TagManager</span><span class="p">.</span><span class="nf">dataLayer</span><span class="p">({</span>
    <span class="na">dataLayer</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">event</span><span class="p">:</span> <span class="dl">'</span><span class="s1">click_counseling_request</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">event_category</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Engagement</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">event_label</span><span class="p">:</span> <span class="dl">'</span><span class="s1">즉시 상담신청</span><span class="dl">'</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">});</span>
<span class="p">};</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="google-analytics-보고서-설정">Google Analytics 보고서 설정</h2>

<p>GA4 설정이 완료되면 Google Analytics 보고서 페이지에서 다양한 데이터를 확인할 수 있다. 주요 보고서 설정은 다음과 같이 했다:</p>

<ol>
  <li>실시간 보고서:
    <ul>
      <li>현재 활성 사용자 수 모니터링</li>
      <li>많이 조회되는 페이지 추적</li>
      <li>사용자 위치 데이터 수집</li>
    </ul>
  </li>
  <li>수명주기 보고서:
    <ul>
      <li>사용자 획득 데이터</li>
      <li>참여도 분석</li>
      <li>수익 창출 지표</li>
      <li>유지 관련 지표</li>
    </ul>
  </li>
  <li>사용자 속성:
    <ul>
      <li>신규/재방문 사용자 구분</li>
      <li>기기 유형별 사용자 분포</li>
      <li>지역별 사용자 분포</li>
    </ul>
  </li>
</ol>

<h2 id="결과-및-인사이트">결과 및 인사이트</h2>

<p>GA4와 GTM 설정을 완료한 후, 다음과 같은 실제 데이터를 수집할 수 있게 됐다:</p>

<ol>
  <li>일별 실제 사용자 수</li>
  <li>페이지별 체류 시간</li>
  <li>상담 신청 전환율</li>
  <li>사용자 이탈률</li>
</ol>

<p>이러한 데이터를 통해 몇 가지 중요한 인사이트를 얻을 수 있었다:</p>

<ul>
  <li>웹뷰를 통한 실제 서비스 사용자가 존재한다는 것을 확인</li>
  <li>상담 신청 프로세스에서의 사용자 행동 패턴 파악</li>
  <li>개선이 필요한 페이지와 기능 식별</li>
</ul>

<h2 id="마치며">마치며</h2>

<p>GA4와 GTM의 도입으로 “데이터에 기반한 의사결정”이 가능해졌다. 특히 웹뷰 환경에서의 사용자 행동을 추적하고 분석할 수 있게 된 것은 큰 성과였다. 이러한 데이터는 앞으로의 서비스 개선과 사용자 경험 최적화에 큰 도움이 될 것으로 기대된다.</p>]]></content><author><name></name></author><category term="Development" /><category term="Web Analytics" /><category term="Google Analytics" /><category term="GA4" /><category term="Google Tag Manager" /><category term="GTM" /><category term="React" /><category term="TypeScript" /><category term="WebView" /><summary type="html"><![CDATA[웹뷰 프로젝트에 GA4와 GTM을 적용하여 사용자 행동을 분석하는 방법. React 환경에서의 구현 가이드와 실전 팁.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://z9-durun.github.io/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_1.webp" /><media:content medium="image" url="https://z9-durun.github.io/assets/img/posts/2024-12-04/ga4-gtm-integration-for-webview-project_1.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">React Native Android namespace 자동화하기</title><link href="https://z9-durun.github.io/posts/rn-android-namespace/" rel="alternate" type="text/html" title="React Native Android namespace 자동화하기" /><published>2024-11-22T00:00:00+09:00</published><updated>2024-11-22T00:00:00+09:00</updated><id>https://z9-durun.github.io/posts/rn-android-namespace</id><content type="html" xml:base="https://z9-durun.github.io/posts/rn-android-namespace/"><![CDATA[<h2 id="문제-상황">문제 상황</h2>

<p>최근 React Native 프로젝트를 업그레이드하는 과정에서 골치 아픈 문제가 발생했다. 안드로이드 빌드 시 각 라이브러리마다 namespace를 일일이 추가해줘야 하는 상황이었다. 특히 <code class="language-plaintext highlighter-rouge">node_modules</code>를 삭제하고 재설치할 때마다 이 작업을 반복해야 했는데, 이는 매우 비효율적이고 시간 낭비였다.</p>

<p>처음에는 Android Studio에서 각 라이브러리의 build.gradle 파일을 열어 수동으로 namespace를 추가했다. 하지만 20개가 넘는 라이브러리에 대해 이 작업을 반복하는 것은 너무 고통스러웠다. 특히 새로운 팀원이 프로젝트를 셋업할 때마다 이런 불편함을 겪어야 한다는 점이 마음에 걸렸다.</p>

<h2 id="해결-과정">해결 과정</h2>

<p>이 문제를 자동화하기 위해 세 가지 주요 작업을 진행했다.</p>

<h3 id="1-packagejson에-postinstall-스크립트-추가">1. package.json에 postinstall 스크립트 추가</h3>

<p>먼저 <code class="language-plaintext highlighter-rouge">npm install</code> 실행 시 자동으로 namespace를 추가하도록 package.json의 scripts에 postinstall을 추가했다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nl">"postinstall"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node scripts/add-namespaces.ts &amp;&amp; patch-package"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="2-gradleproperties에-namespace-정보-추가">2. gradle.properties에 namespace 정보 추가</h3>

<p>안드로이드 빌드 시스템이 참조할 수 있도록 <code class="language-plaintext highlighter-rouge">android/gradle.properties</code> 파일에 각 라이브러리의 namespace를 정의했다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>android.enableNamespaceCheck=true
react-native-gesture-handler.namespace=com.swmansion.gesturehandler
react-native-webview.namespace=com.reactnativecommunity.webview
# ... 기타 라이브러리들의 namespace
</pre></td></tr></tbody></table></code></pre></div></div>

<p>이렇게 하면 프로젝트에서 사용하는 모든 라이브러리의 namespace를 한 곳에서 관리할 수 있다.</p>

<h3 id="3-namespace-자동-추가-스크립트-작성">3. namespace 자동 추가 스크립트 작성</h3>

<p>가장 핵심적인 부분은 <code class="language-plaintext highlighter-rouge">scripts/add-namespaces.ts</code> 파일이다. 이 스크립트는 node_modules 내의 각 React Native 라이브러리의 build.gradle 파일을 찾아서 namespace를 자동으로 추가해준다.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
</pre></td><td class="rouge-code"><pre><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">path</span><span class="dl">'</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">nodeModulesPath</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="dl">'</span><span class="s1">..</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">node_modules</span><span class="dl">'</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">namespaceMap</span> <span class="o">=</span> <span class="p">{</span>
  <span class="dl">'</span><span class="s1">react-native-gesture-handler</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.swmansion.gesturehandler</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-firebase-messaging</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">io.invertase.firebase.messaging</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-kakao-share-link</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.reactnativekakaosharelink</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-get-random-values</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">org.linusu</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-webview</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.reactnativecommunity.webview</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">@react-native-firebase/app</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">io.invertase.firebase</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">@react-native-firebase/dynamic-links</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">io.invertase.firebase.dynamiclinks</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-inappbrowser-reborn</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.proyecto26.inappbrowser</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-safe-area-context</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.th3rdwave.safeareacontext</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-channel-plugin</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.zoyi.channel.rn</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-screens</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.swmansion.rnscreens</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">@react-native-async-storage/async-storage</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.reactnativecommunity.asyncstorage</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">@react-native-community/masked-view</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">org.reactnative.maskedview</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">@react-native-seoul/kakao-login</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.dooboolab.kakaologins</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">@invertase/react-native-apple-authentication</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.RNAppleAuthentication</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-reanimated</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.swmansion.reanimated</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-svg</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.horcrux.svg</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-device-info</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.learnium.RNDeviceInfo</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-push-notification</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.dieam.reactnativepushnotification</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-permissions</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.zoontek.rnpermissions</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">react-native-splash-screen</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">org.devio.rn.splashscreen</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">@react-native-cookies/cookies</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">com.reactnativecommunity.cookies</span><span class="dl">'</span><span class="p">,</span>
<span class="p">};</span>

<span class="kd">function</span> <span class="nf">addNamespaceToGradleFile</span><span class="p">(</span><span class="nx">gradleFilePath</span><span class="p">,</span> <span class="nx">packageName</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">content</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nf">readFileSync</span><span class="p">(</span><span class="nx">gradleFilePath</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">);</span>

    <span class="c1">// 이미 namespace가 있는지 확인</span>
    <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">content</span><span class="p">.</span><span class="nf">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">namespace</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
      <span class="c1">// android { 블록 찾기</span>
      <span class="kd">const</span> <span class="nx">androidBlockRegex</span> <span class="o">=</span> <span class="sr">/android</span><span class="se">\s</span><span class="sr">*{/</span><span class="p">;</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">androidBlockRegex</span><span class="p">.</span><span class="nf">test</span><span class="p">(</span><span class="nx">content</span><span class="p">))</span> <span class="p">{</span>
        <span class="c1">// namespace 추가</span>
        <span class="nx">content</span> <span class="o">=</span> <span class="nx">content</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span>
          <span class="nx">androidBlockRegex</span><span class="p">,</span>
          <span class="s2">`android {\n    namespace "</span><span class="p">${</span><span class="nx">packageName</span><span class="p">}</span><span class="s2">"`</span><span class="p">,</span>
        <span class="p">);</span>

        <span class="nx">fs</span><span class="p">.</span><span class="nf">writeFileSync</span><span class="p">(</span><span class="nx">gradleFilePath</span><span class="p">,</span> <span class="nx">content</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`✅ Added namespace to </span><span class="p">${</span><span class="nx">gradleFilePath</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`ℹ️ Namespace already exists in </span><span class="p">${</span><span class="nx">gradleFilePath</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="s2">`❌ Error processing </span><span class="p">${</span><span class="nx">gradleFilePath</span><span class="p">}</span><span class="s2">:`</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nf">processNodeModules</span><span class="p">()</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">🔍 Starting to process React Native libraries...</span><span class="dl">'</span><span class="p">);</span>

  <span class="c1">// namespaceMap의 각 항목에 대해 처리</span>
  <span class="nb">Object</span><span class="p">.</span><span class="nf">entries</span><span class="p">(</span><span class="nx">namespaceMap</span><span class="p">).</span><span class="nf">forEach</span><span class="p">(([</span><span class="nx">lib</span><span class="p">,</span> <span class="k">namespace</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">androidBuildGradle</span><span class="p">;</span>

    <span class="k">if </span><span class="p">(</span><span class="nx">lib</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">@</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="p">[</span><span class="nx">org</span><span class="p">,</span> <span class="nx">name</span><span class="p">]</span> <span class="o">=</span> <span class="nx">lib</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">);</span>
      <span class="nx">androidBuildGradle</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span>
        <span class="nx">nodeModulesPath</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">@</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">org</span><span class="p">,</span>
        <span class="nx">name</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">android</span><span class="dl">'</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">build.gradle</span><span class="dl">'</span><span class="p">,</span>
      <span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">androidBuildGradle</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span>
        <span class="nx">nodeModulesPath</span><span class="p">,</span>
        <span class="nx">lib</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">android</span><span class="dl">'</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">build.gradle</span><span class="dl">'</span><span class="p">,</span>
      <span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if </span><span class="p">(</span><span class="nx">fs</span><span class="p">.</span><span class="nf">existsSync</span><span class="p">(</span><span class="nx">androidBuildGradle</span><span class="p">))</span> <span class="p">{</span>
      <span class="nf">addNamespaceToGradleFile</span><span class="p">(</span><span class="nx">androidBuildGradle</span><span class="p">,</span> <span class="k">namespace</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`⚠️ Could not find build.gradle for </span><span class="p">${</span><span class="nx">lib</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">});</span>

  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">✨ Finished processing libraries</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// 스크립트 실행</span>
<span class="nf">processNodeModules</span><span class="p">();</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>스크립트의 주요 기능은 다음과 같다:</p>

<ul>
  <li>라이브러리별 namespace 매핑 정보 관리</li>
  <li>@org/package 형태의 패키지도 처리 가능</li>
  <li>이미 namespace가 있는 경우 건너뛰기</li>
  <li>작업 진행 상황을 콘솔에 표시</li>
</ul>

<h3 id="결과">결과</h3>

<p>이제 <code class="language-plaintext highlighter-rouge">npm install</code>을 실행하면 자동으로 다음과 같은 작업이 진행된다:</p>

<ol>
  <li>모든 패키지가 설치됨</li>
  <li>postinstall 스크립트가 실행되어 필요한 라이브러리에 namespace가 추가됨</li>
  <li>patch-package가 실행되어 수정된 내용이 패치로 저장됨</li>
</ol>

<p><img src="/assets/img/posts/2024-11-22/rn-android-namespace.webp" alt="npm install &gt; 출력 결과" /></p>

<p>출력 결과를 보면 어떤 라이브러리에 namespace가 추가되었고, 어떤 것은 이미 namespace가 있어서 건너뛰었는지 확인할 수 있다.</p>

<h2 id="마무리">마무리</h2>

<p>이 자동화 작업으로 개발 환경 설정이 훨씬 수월해졌다. 새로운 팀원이 프로젝트를 셋업할 때도 별도의 수동 작업 없이 <code class="language-plaintext highlighter-rouge">npm install</code> 한 번으로 모든 설정이 완료된다.</p>]]></content><author><name></name></author><category term="Development" /><category term="React Native" /><category term="React Native" /><category term="Android" /><category term="Namespace" /><category term="Automation" /><summary type="html"><![CDATA[React Native 업그레이드 시 Android 라이브러리의 namespace를 자동으로 추가하는 스크립트 구현 방법. postinstall 훅을 활용한 자동화 가이드.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://z9-durun.github.io/assets/img/posts/2024-11-22/rn-android-namespace.webp" /><media:content medium="image" url="https://z9-durun.github.io/assets/img/posts/2024-11-22/rn-android-namespace.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>