<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>innes.log</title>
        <link>https://velog.io/</link>
        <description>무서운 속도로 흡수하는 스펀지 개발자 🧽</description>
        <lastBuildDate>Tue, 19 Aug 2025 06:26:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>innes.log</title>
            <url>https://velog.velcdn.com/images/innes_kwak/profile/a47f5a41-6648-419f-937e-9a34d5e938af/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. innes.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/innes_kwak" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[자료구조] Set 자료구조 알아보기]]></title>
            <link>https://velog.io/@innes_kwak/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Set-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@innes_kwak/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Set-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 19 Aug 2025 06:26:27 GMT</pubDate>
            <description><![CDATA[<h3 id="1-set-자료구조란-무엇인가"><strong>1. <code>set</code> 자료구조란 무엇인가</strong></h3>
<p><code>set</code>은 수학의 집합 개념을 기반으로 하는 자료구조입니다. 가장 중요한 특징은 <strong>중복된 요소를 허용하지 않는다</strong>는 점입니다. <code>{}</code> 중괄호를 사용하여 선언하며, 키(Key)와 값(Value)의 쌍으로 이루어진 딕셔너리와는 구분됩니다.</p>
<pre><code class="language-python"># set은 중복을 허용하지 않습니다.
unique_set = {1, 2, 2, 3, 4}
print(unique_set)
# 결과: {1, 2, 3, 4}</code></pre>
<p>참고: 빈 set은 my_set = set()으로 선언해야 합니다. {}는 빈 딕셔너리로 인식됩니다.</p>
<hr>
<h3 id="2-set을-사용해야-하는-이유-성능"><strong>2. <code>set</code>을 사용해야 하는 이유: 성능</strong></h3>
<p><code>set</code>은 <strong>시간 효율성</strong> 측면에서 리스트보다 우수한 성능을 제공합니다. 이는 <code>set</code>이 내부적으로 <strong>해시 테이블</strong>을 사용하여 데이터를 관리하기 때문입니다.</p>
<ul>
<li><strong>리스트</strong>: 특정 요소의 존재 여부를 확인하기 위해 첫 번째 요소부터 순차적으로 탐색합니다. (시간 복잡도: O(N))</li>
<li><strong><code>set</code></strong>: 해시 테이블을 통해 요소의 위치를 즉시 찾아냅니다. (시간 복잡도: O(1))</li>
</ul>
<p>따라서 <code>in</code> 연산을 통한 데이터 존재 여부 확인이 빈번하게 발생하는 알고리즘 문제에서 <code>set</code>은 압도적인 속도를 보여줍니다.</p>
<hr>
<h3 id="3-set-자료구조의-주요-활용법"><strong>3. <code>set</code> 자료구조의 주요 활용법</strong></h3>
<p><code>set</code>은 주로 데이터의 <strong>중복을 제거</strong>하거나, <strong>빠른 검색</strong>이 필요할 때 활용됩니다. 다음은 <code>set</code>의 주요 사용 예시입니다.</p>
<h4 id="a-리스트의-중복-제거"><strong>A. 리스트의 중복 제거</strong></h4>
<p>가장 보편적인 활용법입니다. 리스트를 <code>set</code>으로 변환하기만 하면 중복이 자동으로 제거됩니다.</p>
<pre><code class="language-python">my_list = [1, 2, 3, 2, 4, 1]
# 리스트를 set으로 변환하여 중복 제거
unique_elements = set(my_list)
print(unique_elements)
# 결과: {1, 2, 3, 4}</code></pre>
<h4 id="b-요소-추가-및-삭제">B. 요소 추가 및 삭제</h4>
<p>set에 요소를 추가하거나 삭제할 때는 add()와 remove() 메서드를 사용합니다.</p>
<pre><code class="language-python">my_set = {1, 2, 3}
my_set.add(4)
print(my_set)
# 결과: {1, 2, 3, 4}

my_set.remove(1)
print(my_set)
# 결과: {2, 3, 4}</code></pre>
<h4 id="c-정렬된-리스트로-변환">C. 정렬된 리스트로 변환</h4>
<p>set은 순서가 없기 때문에 정렬이 필요한 경우, sorted() 함수를 사용하여 list로 변환해야 합니다.</p>
<pre><code class="language-python">unique_elements = {1, 3, 2}
# sorted() 함수는 정렬된 리스트를 반환합니다.
sorted_list = sorted(list(unique_elements))
print(sorted_list)
# 결과: [1, 2, 3]</code></pre>
<hr>
<h3 id="결론"><strong>결론</strong></h3>
<p><code>set</code>은 코딩 테스트에서 중복 제거 및 빠른 검색을 위한 핵심 자료구조입니다. 리스트와 딕셔너리처럼 자유롭게 다룰 수 있도록 연습한다면, 복잡한 문제도 효율적으로 해결하는 통찰력을 기를 수 있을 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Side Project] Expo Web 500 에러 해결기: Babel 플러그인 호환성 문제]]></title>
            <link>https://velog.io/@innes_kwak/Side-Project-Expo-Web-500-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EA%B8%B0-Babel-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%ED%98%B8%ED%99%98%EC%84%B1-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@innes_kwak/Side-Project-Expo-Web-500-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EA%B8%B0-Babel-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%ED%98%B8%ED%99%98%EC%84%B1-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Mon, 18 Aug 2025 15:43:49 GMT</pubDate>
            <description><![CDATA[<h1 id="🚨-expo-web-500-에러-해결기-babel-플러그인-호환성-문제">🚨 Expo Web 500 에러 해결기: Babel 플러그인 호환성 문제</h1>
<blockquote>
<p><strong>TL;DR</strong>: Expo Web에서 발생하는 500 에러는 <code>react-native-reanimated/plugin</code>과 Expo SDK 53 간의 호환성 문제였습니다. Babel 플러그인을 제거하여 해결했습니다.</p>
</blockquote>
<h2 id="📱-문제-상황">📱 문제 상황</h2>
<p>React Native 모노레포(Turborepo)에서 Expo 앱을 개발 중이었습니다. 앱은 모바일에서는 정상 작동했지만, <strong>Expo Web에서 500 에러</strong>가 계속 발생했습니다.</p>
<h3 id="🔍-에러-메시지">🔍 에러 메시지</h3>
<pre><code>[Reanimated] Seems like you are using a Babel plugin `react-native-reanimated/plugin`.
It was moved to `react-native-worklets` package. Please use `react-native-worklets/plugin` instead.

ERROR index.ts: [BABEL] .plugins is not a valid Plugin property</code></pre><h3 id="🌐-실패하는-요청">🌐 실패하는 요청</h3>
<pre><code>...bundle?platform=web&amp;...transform.routerRoot=app... returns 500 and JSON MIME type</code></pre><h2 id="🕵️-문제-진단-과정">🕵️ 문제 진단 과정</h2>
<h3 id="1단계-일반적인-원인들-확인">1단계: 일반적인 원인들 확인</h3>
<p>처음에는 다음과 같은 일반적인 문제들을 의심했습니다:</p>
<ul>
<li>expo-router 설정 문제</li>
<li>Metro 번들러 설정 문제</li>
<li>모노레포 구조 문제</li>
<li>웹 번들러 설정 문제</li>
</ul>
<h3 id="2단계-설정-파일-분석">2단계: 설정 파일 분석</h3>
<p>백업된 코드와 현재 코드를 비교 분석한 결과:</p>
<table>
<thead>
<tr>
<th>설정 파일</th>
<th>문제 상태</th>
<th>정상 상태</th>
</tr>
</thead>
<tbody><tr>
<td><code>babel.config.js</code></td>
<td><code>plugins: [&#39;nativewind/babel&#39;, &#39;react-native-reanimated/plugin&#39;]</code></td>
<td><code>plugins: []</code></td>
</tr>
<tr>
<td><code>package.json</code></td>
<td>복잡한 의존성 (nativewind, reanimated, tailwind)</td>
<td>간단한 기본 의존성</td>
</tr>
<tr>
<td><code>metro.config.js</code></td>
<td>모노레포 설정 있음</td>
<td>없음</td>
</tr>
<tr>
<td><code>app.json</code></td>
<td><code>&quot;bundler&quot;: &quot;webpack&quot;</code></td>
<td>기본 웹 설정</td>
</tr>
</tbody></table>
<h3 id="3단계-진짜-원인-발견">3단계: 진짜 원인 발견</h3>
<p><strong>에러의 진짜 원인은 <code>react-native-reanimated/plugin</code>이었습니다!</strong></p>
<h2 id="🎯-문제의-핵심">🎯 문제의 핵심</h2>
<h3 id="babel-플러그인-호환성-문제">Babel 플러그인 호환성 문제</h3>
<ul>
<li><strong>Reanimated v3.17.4</strong>와 <strong>Expo SDK 53</strong> 간의 호환성 문제</li>
<li>에러 메시지의 <code>react-native-worklets</code> 가이드는 <strong>잘못된 정보</strong>였음</li>
<li>Reanimated v3.x에서는 여전히 <code>react-native-reanimated/plugin</code> 사용해야 함</li>
</ul>
<h3 id="잘못된-가이드의-함정">잘못된 가이드의 함정</h3>
<pre><code>[Reanimated] Seems like you are using a Babel plugin `react-native-reanimated/plugin`.
It was moved to `react-native-worklets` package.</code></pre><p>이 메시지는 <strong>오해의 소지</strong>가 있었습니다. 실제로는:</p>
<ul>
<li>Reanimated v4+에서만 <code>react-native-worklets/plugin</code> 사용</li>
<li>Reanimated v3.x에서는 기존 플러그인 유지</li>
<li>하지만 Expo SDK 53과의 호환성 문제 존재</li>
</ul>
<h2 id="🔧-해결-방법">🔧 해결 방법</h2>
<h3 id="방법-1-babel-플러그인-제거-추천">방법 1: Babel 플러그인 제거 (추천)</h3>
<pre><code class="language-javascript">// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: [&#39;babel-preset-expo&#39;],
    plugins: [], // Reanimated 플러그인 제거
  };
};</code></pre>
<h3 id="방법-2-reanimated-버전-업그레이드">방법 2: Reanimated 버전 업그레이드</h3>
<pre><code class="language-bash"># Reanimated v4+로 업그레이드
npx expo install react-native-reanimated@^4.0.0

# babel.config.js 수정
plugins: [
  &#39;nativewind/babel&#39;,
  &#39;react-native-worklets/plugin&#39;, // v4+용
]</code></pre>
<h3 id="방법-3-expo-sdk-다운그레이드">방법 3: Expo SDK 다운그레이드</h3>
<pre><code class="language-bash"># Reanimated v3와 호환되는 Expo SDK로 다운그레이드
npx expo install expo@~52.0.0</code></pre>
<h2 id="📚-교훈-및-인사이트">📚 교훈 및 인사이트</h2>
<h3 id="1-에러-메시지-신중하게-해석하기">1. 에러 메시지 신중하게 해석하기</h3>
<ul>
<li>공식 에러 메시지도 항상 정확하지 않을 수 있음</li>
<li>버전별로 다른 해결책이 필요할 수 있음</li>
<li><strong>컨텍스트를 고려한 분석</strong>이 중요</li>
</ul>
<h3 id="2-단계별-문제-해결의-중요성">2. 단계별 문제 해결의 중요성</h3>
<ul>
<li>복잡한 문제는 <strong>하나씩 제거</strong>하며 접근</li>
<li>설정 파일들을 <strong>하나씩 비교</strong>하며 분석</li>
<li><strong>백업과 비교</strong>를 통한 정확한 진단</li>
</ul>
<h3 id="3-모노레포에서의-주의사항">3. 모노레포에서의 주의사항</h3>
<ul>
<li>각 앱의 설정이 독립적으로 작동해야 함</li>
<li>공유 패키지와의 의존성 관리 중요</li>
<li><strong>캐시 정리</strong>가 문제 해결의 핵심</li>
</ul>
<h2 id="🚀-예방-방법">🚀 예방 방법</h2>
<h3 id="1-의존성-버전-관리">1. 의존성 버전 관리</h3>
<pre><code class="language-json">{
  &quot;dependencies&quot;: {
    &quot;expo&quot;: &quot;~53.0.20&quot;,
    &quot;react-native-reanimated&quot;: &quot;~4.0.0&quot; // 최신 버전 사용
  }
}</code></pre>
<h3 id="2-babel-설정-최소화">2. Babel 설정 최소화</h3>
<pre><code class="language-javascript">// 필요한 플러그인만 사용
plugins: [
  &#39;nativewind/babel&#39;, // NativeWind 사용 시에만
  // Reanimated는 필요할 때만 추가
];</code></pre>
<h3 id="3-정기적인-업데이트">3. 정기적인 업데이트</h3>
<pre><code class="language-bash"># 정기적으로 의존성 업데이트
npx expo install --fix
npm update</code></pre>
<h2 id="🔍-디버깅-체크리스트">🔍 디버깅 체크리스트</h2>
<p>500 에러가 발생할 때 다음을 확인하세요:</p>
<ul>
<li><input disabled="" type="checkbox"> Babel 플러그인 설정 확인</li>
<li><input disabled="" type="checkbox"> Reanimated 버전과 Expo SDK 호환성 확인</li>
<li><input disabled="" type="checkbox"> Metro 캐시 정리 (<code>npx expo start -c</code>)</li>
<li><input disabled="" type="checkbox"> node_modules 재설치</li>
<li><input disabled="" type="checkbox"> 설정 파일들 비교 분석</li>
<li><input disabled="" type="checkbox"> 백업과 현재 상태 비교</li>
</ul>
<h2 id="💡-결론">💡 결론</h2>
<p>이번 트러블슈팅을 통해 <strong>Babel 플러그인 호환성</strong>이 Expo Web 에러의 주요 원인임을 확인했습니다.</p>
<p><strong>핵심 교훈</strong>:</p>
<ol>
<li>에러 메시지를 맹신하지 말고 컨텍스트를 고려하라</li>
<li>복잡한 문제는 단계별로 접근하라</li>
<li>백업과 비교를 통한 정확한 진단이 중요하다</li>
<li>불필요한 Babel 플러그인은 제거하는 것이 안전하다</li>
</ol>
<p>이 경험이 비슷한 문제를 겪는 개발자들에게 도움이 되길 바랍니다! 🎉</p>
<hr>
<blockquote>
<p>바이브코딩으로 초기 세팅할 때의 꿀팁! 초기 세팅에서 계속 에러가 난다면, 해당 라이브러리의 공식 문서를 첨부하면서 공식문서 검토 후 다시 작업 진행하도록 시키면 훨씬 정확도가 높아진다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Side Project] React Native 찐초보의 UI 실행 방법 알아보기!]]></title>
            <link>https://velog.io/@innes_kwak/Side-Project-React-Native-%EC%B0%90%EC%B4%88%EB%B3%B4%EC%9D%98-UI-%EC%8B%A4%ED%96%89-%EB%B0%A9%EB%B2%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@innes_kwak/Side-Project-React-Native-%EC%B0%90%EC%B4%88%EB%B3%B4%EC%9D%98-UI-%EC%8B%A4%ED%96%89-%EB%B0%A9%EB%B2%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 18 Aug 2025 14:05:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>React 웹만 하다가 React Native 해보니까 환경부터가 완전 다른 세상이구만...?! 신기하다...!!</p>
</blockquote>
<h1 id="✅-react-native-실행-정리">✅ React Native 실행 정리</h1>
<h3 id="1-🌐-웹에서-확인">1. 🌐 웹에서 확인</h3>
<ul>
<li><p><code>npm run web</code></p>
<p>  → 바로 브라우저에서 확인 (CRA처럼 동작)</p>
</li>
<li><p><code>npx expo start --tunnel</code> 후 DevTools에서 <strong>Run in web browser</strong> 버튼 클릭</p>
<p>  → 이것도 가능하지만 그냥 <code>npm run web</code>이 더 직관적임</p>
</li>
</ul>
<hr>
<h3 id="2-📱-폰에서-확인-expo-go">2. 📱 폰에서 확인 (Expo Go)</h3>
<ul>
<li><p><code>npm expo start</code></p>
<p>  → 같은 와이파이 환경에서 QR 찍으면 됨 (하지만 가끔 연결 문제 발생 가능)</p>
</li>
<li><p><code>npx expo start --tunnel</code></p>
<p>  → 네트워크 문제 있을 때 확실히 연결됨 (QR 찍고 바로 들어갈 수 있음)</p>
</li>
</ul>
<hr>
<h3 id="3-💻-ios-시뮬레이터에서-확인-xcode-필요">3. 💻 iOS 시뮬레이터에서 확인 (Xcode 필요)</h3>
<ul>
<li><p>expo 서버(<code>npm expo start</code>) 열려 있는 터미널에서 <code>i</code> 입력</p>
<p>  → iOS 시뮬레이터 실행됨</p>
</li>
<li><p><code>npm expo run:ios</code></p>
<p>  → 네이티브 빌드해서 시뮬레이터 실행 (조금 무거움, Expo Go 없이 직접 빌드)</p>
</li>
</ul>
<hr>
<h3 id="4-💻-android-시뮬레이터에서-확인-android-studio-필요">4. 💻 Android 시뮬레이터에서 확인 (Android Studio 필요)</h3>
<ul>
<li>Android Studio는 IDE 역할도 한다!</li>
</ul>
<hr>
<h3 id="📌-결론">📌 결론</h3>
<ul>
<li><strong>웹 확인</strong> → <code>npm run web</code></li>
<li><strong>폰에서 확인</strong> → <code>npx expo start --tunnel</code> (이게 가장 안정적)</li>
<li><strong>시뮬레이터에서 확인</strong> → 서버 켠 터미널에서 <code>i</code></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] GitLab Pages에서 React Router 새로고침 시 404 에러 해결기]]></title>
            <link>https://velog.io/@innes_kwak/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-GitLab-Pages%EC%97%90%EC%84%9C-React-Router-%EC%83%88%EB%A1%9C%EA%B3%A0%EC%B9%A8-%EC%8B%9C-404-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EA%B8%B0</link>
            <guid>https://velog.io/@innes_kwak/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-GitLab-Pages%EC%97%90%EC%84%9C-React-Router-%EC%83%88%EB%A1%9C%EA%B3%A0%EC%B9%A8-%EC%8B%9C-404-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EA%B8%B0</guid>
            <pubDate>Tue, 05 Aug 2025 09:41:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>회사 프로젝트를 진행하면서 <strong>React + Tailwind 프로젝트</strong>를 <strong>GitLab Pages</strong>에 배포했는데,<br>라우팅과 관련된 문제가 발생했다. 로컬 개발 서버에서는 문제가 없었지만, <strong>배포된 페이지에서 새로고침 시 404 에러</strong>가 뜨는 상황이었다.<br>이 글에서는 내가 겪은 문제 상황과 원인 분석, 해결책 비교, 그리고 최종적으로 <strong>HashRouter로 임시 해결</strong>한 과정을 정리한다.</p>
</blockquote>
<hr>
<h2 id="1-문제-상황">1. 문제 상황</h2>
<ul>
<li>React 프로젝트를 GitLab Pages로 배포</li>
<li>라우팅에 <code>BrowserRouter</code> 사용</li>
<li>로컬 개발 서버에서는 라우팅 및 새로고침 모두 정상 동작</li>
<li><strong>문제</strong>: GitLab Pages 배포 후 특정 URL 직접 접근 또는 새로고침 시 <strong>GitLab 기본 404 페이지가 표시됨</strong></li>
</ul>
<h3 id="증상">증상</h3>
<ul>
<li><code>/dashboard</code> 같은 URL을 직접 입력하면 GitLab의 404 UI 표시</li>
<li>페이지 내에서 클릭으로 이동하면 UI 정상 작동</li>
<li>이동 후 새로고침하면 다시 메인 페이지(<code>/</code>)로 리다이렉트됨</li>
<li>URL은 바뀌지만 React Router에서 페이지를 렌더링하지 못함</li>
</ul>
<hr>
<h2 id="2-원인-분석">2. 원인 분석</h2>
<ul>
<li><strong>GitLab Pages는 정적 호스팅</strong>으로 동작</li>
<li>서버 측 라우팅 설정 불가 → 존재하지 않는 경로 요청 시 404 반환</li>
<li>React Router의 <code>BrowserRouter</code>는 HTML5 History API를 사용</li>
<li>따라서 새로고침 시 서버에 직접 요청 → 서버는 해당 경로의 정적 파일 없음 → 404 발생<blockquote>
<p><code>dist</code> 폴더를 확인해보자. <code>index.html</code> 하나만 있는걸 봐도 알 수 있다. gitlab pages는 정적 호스팅이기 때문에 링크를 전달해서 요청하면 배포주소는 index.html하나만 바라보고 있어서 &#39;전달받은 링크같은 그런 파일은 없는데..?&#39;라고 생각하고 404를 뱉어버리는 것이다.</p>
</blockquote>
</li>
</ul>
<hr>
<h2 id="3-시도한-해결-방법들">3. 시도한 해결 방법들</h2>
<h3 id="방법-1-404html--sessionstorage-복원">방법 1: 404.html + sessionStorage 복원</h3>
<ul>
<li>404.html에서 현재 경로를 <code>sessionStorage</code>에 저장</li>
<li><code>/</code>로 리다이렉트 후 App.tsx에서 <code>sessionStorage</code> 값을 읽어 원래 경로로 복원</li>
</ul>
<h4 id="장점">장점</h4>
<ul>
<li>URL 깔끔하게 유지 (<code>/dashboard</code> 그대로 사용)</li>
<li>React Router 그대로 사용 가능</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>존재하지 않는 URL도 메인 페이지로 리다이렉트 → <strong>SEO 문제</strong></li>
<li>GitLab Pages의 404 응답이 항상 200으로 변환 → 에러 구분 불가</li>
<li>sessionStorage 복원 로직이 복잡하고 유지보수 어려움</li>
</ul>
<hr>
<h3 id="방법-2-404html-→-indexhtml-유사-내용으로-처리-sessionstorage-사용-안-함">방법 2: 404.html → index.html 유사 내용으로 처리 (sessionStorage 사용 안 함)</h3>
<ul>
<li><code>404.html</code>을 <code>index.html</code>과 동일한 React 앱으로 구성</li>
<li>사용자가 잘못된 URL을 직접 입력하면 404.html이 로드되고, React Router가 즉시 실행되어 해당 경로를 해석</li>
<li>존재하지 않는 경로는 <code>NotFoundPage</code>로 처리, 존재하는 경로는 정상 라우팅</li>
</ul>
<h4 id="장점-1">장점</h4>
<ul>
<li>sessionStorage 없이 자연스럽게 동작</li>
<li>클릭/새로고침/직접 접근 모두 정상 작동</li>
</ul>
<h4 id="단점-1">단점</h4>
<ul>
<li>잘못된 URL도 200 상태로 서빙되어 SEO 문제</li>
<li>GitLab Pages 특성상 서버 측 404 구분 불가</li>
</ul>
<hr>
<h3 id="방법-3-gitlab-pages-ci-수정-모든-경로-indexhtml로-fallback">방법 3: GitLab Pages CI 수정 (모든 경로 index.html로 fallback)</h3>
<ul>
<li>GitLab CI 설정에서 모든 요청을 index.html로 리다이렉트하도록 설정</li>
<li>404.html 활용 없이 서버 레벨에서 우회</li>
</ul>
<h4 id="장점-2">장점</h4>
<ul>
<li>URL 깔끔 유지</li>
<li>별도 복원 로직 불필요</li>
</ul>
<h4 id="단점-2">단점</h4>
<ul>
<li>GitLab Pages는 커스텀 리라이트 규칙을 지원하지 않음 (불가능에 가까움)</li>
<li>결국 404.html 방식과 유사한 효과만 기대 가능</li>
</ul>
<hr>
<h3 id="방법-4-hashrouter-사용">방법 4: HashRouter 사용</h3>
<ul>
<li>React Router에서 <code>BrowserRouter</code> → <code>HashRouter</code>로 변경</li>
<li>URL이 <code>/#/dashboard</code> 형태로 변경됨</li>
<li>서버는 <code>#</code> 이후 경로를 무시하므로 항상 <code>index.html</code> 반환</li>
<li>클라이언트 라우팅은 해시 기반으로 정상 작동</li>
</ul>
<h4 id="장점-3">장점</h4>
<ul>
<li><strong>가장 단순하고 빠른 해결책</strong></li>
<li>GitLab Pages 특성과 100% 호환</li>
<li>별도 서버 설정, 404.html 필요 없음</li>
<li>새로고침, 직접 접근 모두 문제 없이 동작</li>
</ul>
<h4 id="단점-3">단점</h4>
<ul>
<li>URL에 <code>#</code> 포함 → SEO에 불리</li>
<li>HTTP 상태 코드와 무관하게 항상 200 응답</li>
</ul>
<hr>
<h2 id="4-최종-선택-hashrouter">4. 최종 선택: HashRouter</h2>
<h3 id="선택-이유">선택 이유</h3>
<ul>
<li><strong>빠른 배포 필요</strong><ul>
<li>회사 프로젝트이고 GitLab Group 비공개 레포</li>
<li>Vercel/Netlify는 그룹 비공개 레포 무료 연결 불가</li>
<li>유료 서비스 사용 불가한 상황</li>
</ul>
</li>
<li><strong>임시 배포</strong><ul>
<li>최종적으로는 AWS 등 서버 기반 배포로 이관 예정</li>
<li>SEO 문제는 이후 서버 배포 시 BrowserRouter로 복원하며 해결 가능</li>
</ul>
</li>
<li><strong>적용 난이도 최소</strong><ul>
<li>BrowserRouter → HashRouter 변경만으로 문제 해결</li>
<li>404.html 및 복원 로직 제거 가능</li>
</ul>
</li>
</ul>
<hr>
<h2 id="5-적용-방법">5. 적용 방법</h2>
<h3 id="1-maintsx-수정">1) main.tsx 수정</h3>
<pre><code class="language-tsx">import { HashRouter } from &#39;react-router-dom&#39;;

ReactDOM.createRoot(document.getElementById(&#39;root&#39;)!).render(
  &lt;React.StrictMode&gt;
    &lt;HashRouter&gt;
      &lt;App /&gt;
    &lt;/HashRouter&gt;
  &lt;/React.StrictMode&gt;
);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[React + Tailwind CSS 다크모드 구현 시 발생하는 문제들과 해결방법]]></title>
            <link>https://velog.io/@innes_kwak/React-Tailwind-CSS-%EB%8B%A4%ED%81%AC%EB%AA%A8%EB%93%9C-%EA%B5%AC%ED%98%84-%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C%EB%93%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@innes_kwak/React-Tailwind-CSS-%EB%8B%A4%ED%81%AC%EB%AA%A8%EB%93%9C-%EA%B5%AC%ED%98%84-%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C%EB%93%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Fri, 18 Jul 2025 07:14:19 GMT</pubDate>
            <description><![CDATA[<ol>
<li>Tailwind 설정에서 직접 색상 클래스 누락</li>
</ol>
<pre><code class="language-js">// ❌ 문제: bg-bg, text-text 등의 클래스가 정의되지 않음
colors: {
  primary: &quot;var(--color-primary)&quot;,
  secondary: &quot;var(--color-secondary)&quot;,
  // bg-bg, text-text 등이 없어서 클래스가 작동하지 않음
}

// ✅ 해결: 직접적인 색상 클래스들 추가
colors: {
  // 직접 색상 클래스들
  bg: &quot;var(--color-bg)&quot;,
  &quot;bg-card&quot;: &quot;var(--color-bg-card)&quot;,
  &quot;bg-input&quot;: &quot;var(--color-bg-input)&quot;,
  &quot;bg-alt&quot;: &quot;var(--color-bg-alt)&quot;,
  text: &quot;var(--color-text)&quot;,
  &quot;text-muted&quot;: &quot;var(--color-text-muted)&quot;,
  border: &quot;var(--color-border)&quot;,
  &quot;border-divider&quot;: &quot;var(--color-border-divider)&quot;,

  // 기존 색상들
  primary: &quot;var(--color-primary)&quot;,
  secondary: &quot;var(--color-secondary)&quot;,
  // ... 기타 색상들
}</code></pre>
<p>(styles/themes/dark, light/variables.css 에서 컬러 변수를 선언하고 있음)
ex)</p>
<pre><code class="language-css">:root[data-theme=&quot;dark&quot;] {
  /* Background Colors */
  --color-bg: #0a0a0a;
  --color-bg-card: #111111;
  --color-bg-input: #1a1a1a;
  --color-bg-alt: #1f1f1f;

  /* Border Colors */
  --color-border: #222222;
  --color-border-divider: #333333;
  ...</code></pre>
<ol start="2">
<li>CSS 변수 로딩 순서 문제</li>
</ol>
<pre><code class="language-css">/* ❌ 문제: Tailwind가 CSS 변수보다 먼저 로드됨 */
@tailwind base;
@tailwind components;
@tailwind utilities;
@import &#39;../themes/dark/variables.css&#39;;

/* ✅ 해결: CSS 변수를 먼저 import */
@import &#39;../themes/dark/variables.css&#39;;
@import &#39;../themes/light/variables.css&#39;;
@tailwind base;
@tailwind components;
@tailwind utilities;</code></pre>
<ol start="3">
<li>테마 전환 시 CSS 변수 적용 지연</li>
</ol>
<pre><code class="language-js">// ❌ 문제: CSS 변수가 즉시 적용되지 않음
setTheme(theme) {
  const root = document.documentElement;
  root.setAttribute(&#39;data-theme&#39;, theme);
  // reflow가 없어서 CSS 변수 적용이 지연됨
}

// ✅ 해결: 강제 reflow 추가
setTheme(theme) {
  const root = document.documentElement;
  root.setAttribute(&#39;data-theme&#39;, theme);
  root.offsetHeight; // 강제 reflow로 즉시 적용
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js와 React(Vite)에서 Tailwind CSS, @layer, @apply 사용 시 발생하는 차이점과 디자인 시스템 적용 문제]]></title>
            <link>https://velog.io/@innes_kwak/Next.js%EC%99%80-ReactVite%EC%97%90%EC%84%9C-Tailwind-CSS-layer-apply-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%B0%A8%EC%9D%B4%EC%A0%90%EA%B3%BC-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%81%EC%9A%A9-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@innes_kwak/Next.js%EC%99%80-ReactVite%EC%97%90%EC%84%9C-Tailwind-CSS-layer-apply-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%B0%A8%EC%9D%B4%EC%A0%90%EA%B3%BC-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%81%EC%9A%A9-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Wed, 16 Jul 2025 07:11:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>최근에 Next.js로 개발된 디자인 시스템을 React(Vite) 프로젝트로 옮기는 과정에서 예상치 못한 문제가 발생했습니다. Next.js에서는 잘 동작하던 Tailwind CSS 기반의 커스텀 유틸리티와 @layer, @apply 조합이 Vite 기반 React 프로젝트에서는 제대로 적용되지 않는 현상이었습니다. 이 글에서는 그 원인과 해결 과정을 정리했습니다.</p>
</blockquote>
<h1 id="문제-상황">문제 상황</h1>
<p>Next.js 프로젝트(fofood-origin)에서 디자인 시스템을 개발할 때,</p>
<ul>
<li><p><code>globals.css</code>(또는 index.css)에</p>
<pre><code class="language-css">@layer utilities {
  .text-body-2-medium {
    @apply font-figtree text-base font-medium leading-none tracking-[-0.04em];
  }
}</code></pre>
<p>와 같이 커스텀 유틸리티 클래스를 정의하고,</p>
</li>
<li><p>버튼 등 컴포넌트 스타일을</p>
<pre><code class="language-css">.btn-yellow-solid {
  @apply text-body-2-medium ...;
}</code></pre>
<p>처럼 @apply로 재사용하는 방식이 잘 동작했습니다.</p>
</li>
</ul>
<p>이 디자인 시스템을 Vite 기반의 React 프로젝트(design-test)로 그대로 가져와 적용했을 때,
아래와 같은 에러가 발생했습니다.</p>
<pre><code>Error: Cannot apply unknown utility class `text-body-2-medium`.</code></pre><hr>
<h1 id="원인-분석">원인 분석</h1>
<h3 id="1-nextjs와-vitereact의-tailwind-빌드-파이프라인-차이">1. Next.js와 Vite(React)의 Tailwind 빌드 파이프라인 차이</h3>
<ul>
<li><p>Next.js는 Tailwind와 PostCSS, 글로벌 CSS를 통합적으로 처리하면서,
<code>@layer utilities</code>에서 정의한 커스텀 유틸리티를 <code>@apply</code>로 사용할 때도
“일반 CSS 클래스”로 인식하여 에러 없이 빌드가 진행되는 경우가 많았습니다.</p>
</li>
<li><p>Vite(React)는 Tailwind의 JIT 엔진이 모든 CSS를 직접 처리합니다.
이때 <code>@apply</code> 안에 들어가는 클래스는 반드시 Tailwind가 “자신이 생성한 유틸리티”여야 하며,
그렇지 않으면 빌드 에러가 발생합니다.</p>
</li>
</ul>
<h3 id="2-tailwind의-공식-정책">2. Tailwind의 공식 정책</h3>
<p>Tailwind 공식 문서에서는</p>
<blockquote>
<p>“@apply는 Tailwind가 생성한 유틸리티 클래스에만 사용할 수 있습니다.
@layer에서 정의한 커스텀 유틸리티는 @apply로 사용할 수 없습니다.”</p>
</blockquote>
<p>라고 명시하고 있습니다.</p>
<p>즉, Next.js에서만 “운 좋게” 동작했던 것이고,
Vite(React)에서는 공식 정책대로 엄격하게 동작한 것입니다.</p>
<hr>
<h1 id="해결-과정">해결 과정</h1>
<h3 id="1-tailwindconfigjs의-content-경로-확인">1. tailwind.config.js의 content 경로 확인</h3>
<p>src 디렉토리 전체가 포함되어야 Tailwind가 유틸리티를 생성합니다.</p>
<h3 id="2-커스텀-유틸리티를-themeextend에-등록">2. 커스텀 유틸리티를 theme.extend에 등록</h3>
<p>fontSize, colors, fontFamily 등 필요한 값을 tailwind.config.js에 추가했습니다.</p>
<h3 id="3-apply에서-커스텀-유틸리티-사용-금지">3. @apply에서 커스텀 유틸리티 사용 금지</h3>
<p>@apply에서는 반드시 Tailwind 기본 유틸리티만 사용하도록 코드를 수정했습니다.</p>
<p>예를 들어,</p>
<pre><code class="language-css">   .btn-yellow-solid {
     @apply font-figtree text-base font-medium ...;
   }</code></pre>
<p>처럼 직접 유틸리티 조합을 사용했습니다.</p>
<hr>
<h1 id="결론">결론</h1>
<ul>
<li><p>Next.js와 Vite(React)에서 Tailwind CSS의 @layer, @apply, 커스텀 유틸리티 처리 방식이 다릅니다.</p>
</li>
<li><p>Next.js에서는 PostCSS 처리 순서 등으로 인해 커스텀 유틸리티를 @apply로 써도 동작할 수 있지만,
Vite(React)에서는 반드시 Tailwind가 생성한 유틸리티만 @apply에서 사용할 수 있습니다.</p>
</li>
<li><p>디자인 시스템을 여러 환경에서 재사용하려면, Tailwind의 공식 정책에 맞게 커스텀 유틸리티를 관리하고,
@apply에서는 기본 유틸리티만 사용하는 것이 안전합니다.</p>
</li>
</ul>
<p>이 경험을 통해,
프로젝트 환경에 따라 Tailwind CSS의 동작 방식이 달라질 수 있음을 알게 되었고,
공식 문서의 정책을 항상 준수하는 것이 중요하다는 점을 다시 한 번 깨달았습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Design system] 디자인 시스템 제작하기]]></title>
            <link>https://velog.io/@innes_kwak/Design-system-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@innes_kwak/Design-system-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 30 Jun 2025 05:19:36 GMT</pubDate>
            <description><![CDATA[<h1 id="🎨-디자인-시스템-제대로-만들기-토큰-기반-중앙-관리부터-tailwind-연동까지">🎨 디자인 시스템 제대로 만들기: 토큰 기반 중앙 관리부터 Tailwind 연동까지</h1>
<p>프로젝트를 진행하다 보면 디자인이 계속 바뀌는데, 그때마다 색상, 폰트, 간격 등을 일일이 수정하는 일이 번거로웠다.</p>
<p>브랜드 컬러 하나만 바꿔도 앱 전체가 한 번에 바뀌고, 버튼의 둥글기를 바꾸면 전부 반영되며, 폰트를 바꿔도 전역에서 따라오도록 만들고 싶었다.</p>
<p>이걸 해결하기 위해 디자인 시스템을 중앙 관리하는 구조를 설계했고, 그 과정을 정리해보았다.</p>
<hr>
<h2 id="✅-1️⃣-디자인-토큰-객체-만들기">✅ 1️⃣ 디자인 토큰 객체 만들기</h2>
<p>먼저 <strong>디자인 토큰(Design Token)</strong>이 뭔지 간단히 설명하면,</p>
<p>디자인의 결정값을 의미 있는 이름으로 변수화해서 코드로 관리하는 방식이다.</p>
<p>예를 들어 브랜드 메인 색을 그냥 #FF4D8D로 박아 넣는 대신:</p>
<pre><code class="language-ts">export const Colors = {
  brandPrimary: &#39;#FF4D8D&#39;,
};</code></pre>
<p>이렇게 변수로 관리하면 이 변수만 바꿨을 때 전역에서 색이 모두 바뀌도록 만들 수 있었다.</p>
<h3 id="✅-장점">✅ 장점</h3>
<p>중앙 관리가 가능했다. 한 군데서 정의하면 앱 전체에 반영됐다.</p>
<p>의미가 명확했다. brandPrimary라는 이름만 봐도 쓰임새를 알 수 있었다.</p>
<p>팀 작업에서 일관성을 유지할 수 있었다.</p>
<h3 id="✅-단점">✅ 단점</h3>
<p>런타임 변수라서 CSS 클래스에서 직접 못 썼다.</p>
<p>인라인 스타일이나 styled-components 같은 데서는 바로 쓸 수 있었지만</p>
<p>Tailwind 유틸리티 클래스에서는 적용이 안 됐다.</p>
<p>실수로 하드코딩된 색상값을 넣을 가능성이 있었다.</p>
<p>코드리뷰나 규칙으로 강제해야 했다.</p>
<hr>
<h2 id="✅-2️⃣-단점-보완-tailwind-config에-연동하기">✅ 2️⃣ 단점 보완: Tailwind Config에 연동하기</h2>
<p>이 방식의 가장 큰 문제는</p>
<p>“변수 기반으로 중앙 관리하면 좋은데, Tailwind 클래스에서는 못 쓰잖아?”</p>
<p>라는 부분이었다.</p>
<p>이를 해결하기 위해 Tailwind의 tailwind.config.js에서 토큰 객체를 불러와서 확장하도록 만들었다.</p>
<pre><code class="language-ts">// tailwind.config.js
const { Colors } = require(&#39;./design-system/tokens/colors&#39;);

module.exports = {
  theme: {
    extend: {
      colors: Colors,
    },
  },
};</code></pre>
<p>이렇게 하면:</p>
<p>Tailwind가 Colors 객체를 읽어서</p>
<p>bg-brandPrimary, text-brandPrimary 같은 유틸리티 클래스를 자동으로 생성하고</p>
<p>CSS 빌드 시 포함하게 만들었다.</p>
<h3 id="✅-런타임-이슈가-해결되는-이유">✅ 런타임 이슈가 해결되는 이유</h3>
<p>Tailwind는 빌드타임에 config를 읽고 → CSS 클래스를 생성한다.</p>
<p>JS 변수를 config에 넣으면 → 변수 기반으로 클래스를 미리 생성한다.</p>
<p>빌드된 CSS에는 단순히 클래스 이름과 색상값만 들어간다.</p>
<p>브라우저가 읽을 때는 변수 참조가 필요 없다.</p>
<p>결국</p>
<p>“JS 변수 기반 중앙 관리하면서도, Tailwind 클래스화 → 퍼포먼스 최적화 + 일관성 유지”</p>
<p>를 달성할 수 있었다.</p>
<hr>
<h2 id="👩🏻💻-심화--tailwind-plugin-api-사용하기">👩🏻‍💻 심화 : tailwind plugin api 사용하기</h2>
<p>보통은 tailwind.config.js의 extend.colors에 넣으면 bg-primary, text-primary 같은 클래스가 생긴다. 이 정도면 색상을 유틸리티로 쓰기에 충분하다.</p>
<h3 id="문제-ring-color-outline-color는-자동-생성이-안-된다">문제: ring-color, outline-color는 자동 생성이 안 된다</h3>
<p>그런데 디자인 시스템 명세가 이렇게 오면 곤란하다.</p>
<blockquote>
<p>버튼 border는 primary, ring-color도 primary, outline-color도 primary로 맞춰주세요.</p>
</blockquote>
<p>Tailwind가 기본적으로 ring-primary나 outline-primary는 만들어주지 않는다.
bg, text, border까지만 지원하고 ring-color는 따로 지정해줘야 한다.</p>
<p>이렇게 되면 다음 문제를 만나게 된다.</p>
<p>✅ colors에는 primary가 정의되어 있는데
❌ ring-primary 클래스는 존재하지 않는다.</p>
<h3 id="해결책-plugin-api로-커스텀-유틸리티-생성">해결책: Plugin API로 커스텀 유틸리티 생성</h3>
<p>이 문제를 해결하기 위해 Tailwind의 <code>Plugin API</code>를 활용했다.</p>
<p>Tailwind의 plugin 함수에서 addUtilities를 쓰면, 원하는 규칙을 따라 유틸리티 클래스를 추가할 수 있다.</p>
<p>아래는 디자인 토큰의 색상을 border, ring-color 유틸리티로 변환해주는 플러그인 예시 코드다.</p>
<pre><code class="language-ts">import plugin from &#39;tailwindcss/plugin&#39;;
import { colors } from &#39;./design-system/tokens/colors.js&#39;;

export default {
  theme: {
    extend: {
      colors,
    },
  },
  plugins: [
    plugin(({ addUtilities }) =&gt; {
      const colorUtilities = Object.entries(colors).reduce((acc, [name, value]) =&gt; {
        acc[`.border-${name}`] = { borderColor: value };
        acc[`.ring-${name}`] = { &#39;--tw-ring-color&#39;: value };
        return acc;
      }, {});

      addUtilities(colorUtilities);
    }),
  ],
}
</code></pre>
<h4 id="이-코드가-하는-일">이 코드가 하는 일</h4>
<p>colors.js에 아래처럼 디자인 시스템 토큰을 정의했다고 가정했다.</p>
<pre><code class="language-ts">export const colors = {
  primary: &#39;#4F46E5&#39;,
  secondary: &#39;#7C3AED&#39;,
  error: &#39;#EF4444&#39;,
};
</code></pre>
<p>Tailwind의 기본 확장으로는 이런 클래스만 만들어진다.</p>
<p>✅ bg-primary
✅ text-primary
✅ border-primary</p>
<p>하지만 ring-color 유틸리티는 없다.</p>
<p>플러그인 코드가 실행되면:</p>
<p>✅ .border-primary → border-color: #4F46E5
✅ .ring-primary → --tw-ring-color: #4F46E5</p>
<p>이런 규칙을 자동으로 생성해준다.</p>
<h4 id="결과적으로-얻은-것">결과적으로 얻은 것</h4>
<p>디자인 토큰을 바꾸면 Tailwind 유틸리티가 한 번에 바뀐다.
ring, border 등 모든 상태에서 디자인 시스템 색상을 일관되게 사용할 수 있게 됐다.</p>
<p>이제 버튼을 이렇게 쓰면 된다.</p>
<pre><code class="language-ts">&lt;button class=&quot;border-primary ring-primary&quot;&gt;
  Confirm
&lt;/button&gt;
</code></pre>
<p>✅ 디자인 시스템의 색상이 수정되면 버튼의 border와 ring도 전부 바뀐다.</p>
<h4 id="한-단계-더-확장하기">한 단계 더 확장하기</h4>
<p>원한다면 outline, focus-visible 같은 상태까지 지원할 수도 있다.
예를 들어 아래처럼 규칙을 추가하면 된다.</p>
<pre><code class="language-ts">acc[`.outline-${name}`] = { outlineColor: value };
acc[`.focus-visible-${name}`] = { outlineColor: value };
</code></pre>
<p>이렇게 하면:</p>
<p>✅ .outline-primary
✅ .focus-visible-primary</p>
<p>클래스도 자동 생성된다.</p>
<blockquote>
<p>Tailwind CSS는 기본적으로 bg, text, border 색상 유틸리티만 토큰을 반영한다.
하지만 디자인 시스템에서는 ring, outline 같은 다양한 상태색상도 통일해야 할 때가 많다.</p>
</blockquote>
<p>이걸 해결하기 위해 Plugin API를 써서 addUtilities로 규칙을 생성할 수 있다.
이 방법으로 디자인 토큰 변경이 Tailwind 전역 유틸리티 클래스에 일괄 반영되도록 만들 수 있다.</p>
<blockquote>
</blockquote>
<p>디자인 시스템을 코드화하고, 색상 토큰의 일관된 사용을 강제하고 싶다면 Tailwind Plugin 커스텀화를 고려해볼 만하다.</p>
<hr>
<h2 id="✅-3️⃣-폴더-구조-예시">✅ 3️⃣ 폴더 구조 예시</h2>
<p>디자인 시스템은 아예 별도 폴더로 관리하도록 설계했다.</p>
<pre><code class="language-css">design-system/
  ├── tokens/
  │     ├── colors.ts
  │     ├── typography.ts
  │     ├── spacing.ts
  │     └── ...
  ├── themes/
  │     └── light.ts
  ├── components/
  │     ├── Button.tsx
  │     └── ...
  └── index.ts
</code></pre>
<hr>
<p>✅ tokens: 디자인 결정값 (색상, 간격, 폰트) → 싱글 소스 오브 트루스로 관리했다.
✅ themes: tokens을 조합해 테마를 정의했다.
✅ components: 반드시 tokens/테마에서 값을 가져와서 스타일을 적용하도록 했다.</p>
<hr>
<h2 id="✅-4️⃣-싱글-소스-오브-트루스single-source-of-truth">✅ 4️⃣ 싱글 소스 오브 트루스(Single Source of Truth)</h2>
<p>이게 디자인 시스템 설계에서 진짜 핵심이었다.</p>
<p>“디자인 결정값을 한 군데서만 관리하고, 거기가 바뀌면 앱 전체가 바뀌도록 한다.”</p>
<p>색상 팔레트가 바뀌면 → Colors.ts만 수정했다.</p>
<p>폰트 크기를 바꾸면 → Typography.ts만 수정했다.</p>
<p>Spacing 단위를 변경하면 → Spacing.ts만 수정했다.</p>
<p>결국 토큰이 디자인의 &quot;진실의 원천(=Single Source of Truth)&quot;이 되었다.</p>
<p>✅ 다른 말로는
디자인 토큰 저장소</p>
<p>디자인 시스템 변수</p>
<p>디자인 정의 계층</p>
<p>어떻게 부르든 중요한 건 → 중앙에서만 관리한다는 점이었다.</p>
<hr>
<h2 id="✅-5️⃣-결과적으로-얻은-이점">✅ 5️⃣ 결과적으로 얻은 이점</h2>
<p>✔ 브랜드 리디자인이 필요하면 → 토큰 파일만 수정하면 됐다.
✔ 팀원들이 각자 색상을 새로 정의하지 못하게 강제할 수 있었다.
✔ 다크모드 / 라이트모드 테마 전환이 훨씬 쉽도록 설계할 수 있었다.
✔ Tailwind 유틸리티를 그대로 써서 → 퍼포먼스 최적화, Purge도 가능했다.</p>
<hr>
<h2 id="✅-6️⃣-최종-정리">✅ 6️⃣ 최종 정리</h2>
<p>내가 권장하는 방식은:</p>
<p>✅ 디자인 토큰을 TypeScript 객체로 관리했다.
✅ design-system 폴더에서 모든 디자인 결정을 중앙 관리했다.
✅ tailwind.config.js에서 이 토큰을 import해 → Tailwind 클래스 생성까지 자동화했다.
✅ 앱 컴포넌트에서는 클래스와 변수 둘 다 자유롭게 활용했다.</p>
<p>결국</p>
<p>&quot;변수 기반 중앙 관리&quot;와 &quot;Tailwind의 퍼포먼스와 편의성&quot;을 둘 다 잡을 수 있었다.</p>
<hr>
<h2 id="✅-마무리">✅ 마무리</h2>
<p>이 구조를 만들면서 나도 처음에는</p>
<p>그냥 colors.ts 하나 만들어서 변수만 쓰면 될 줄 알았다 → Tailwind에서 못 썼다.</p>
<p>tailwind.config.js만 쓰자 → JS 코드에서 못 썼다.</p>
<p>둘 다 유지보수는 어떻게 하지?</p>
<p>이런 고민을 거쳤다.</p>
<p>결국 “토큰 → Tailwind config 연동” 이 가장 깔끔하고 표준적인 해법이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[i18n] i18n 영어 번역시 트러블슈팅]]></title>
            <link>https://velog.io/@innes_kwak/i18n-i18n-%EC%98%81%EC%96%B4-%EB%B2%88%EC%97%AD%EC%8B%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@innes_kwak/i18n-i18n-%EC%98%81%EC%96%B4-%EB%B2%88%EC%97%AD%EC%8B%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</guid>
            <pubDate>Thu, 26 Jun 2025 09:17:44 GMT</pubDate>
            <description><![CDATA[<h1 id="🛠️-i18n-영어-번역시-트러블슈팅">🛠️ i18n 영어 번역시 트러블슈팅</h1>
<h2 id="키값이-그대로-노출되는-문제-완벽-해결법">키값이 그대로 노출되는 문제 완벽 해결법</h2>
<p>다국어 웹 서비스를 개발하다 보면, 영어 번역을 추가할 때 번역 키값이 그대로 화면에 노출되는 황당한 경험을 하게 됩니다. 예를 들어,
t(&#39;leads.dashboard.period&#39;)를 호출했는데 화면에 leads.dashboard.period가 그대로 뜨는 현상입니다.
이 글에서는 왜 이런 일이 발생하는지,
한 번만 세팅하면 앞으로 다시는 이런 문제가 생기지 않게 만드는 방법을
실전 코드와 함께 정리합니다.</p>
<hr>
<h3 id="1-문제의-원인">1. 문제의 원인</h3>
<p>i18next(react-i18next)에서 번역 키가 번역 파일에 없을 때,
기본적으로 키값 자체를 반환합니다.</p>
<p>영어 번역 파일(en/translation.json)에 &quot;leads.dashboard.period&quot;: &quot;Period&quot;가 없으면
t(&#39;leads.dashboard.period&#39;) 호출 시
화면에 leads.dashboard.period가 그대로 노출됨</p>
<p>이 현상은 번역 파일 관리가 조금만 소홀해도 자주 발생합니다.</p>
<h3 id="2-근본적인-해결책">2. 근본적인 해결책</h3>
<p>(1) i18n 설정에서 키값 노출 방지 옵션 추가
i18n 초기화 시 아래 옵션을 추가하세요.</p>
<pre><code class="language-js">i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: &#39;en&#39;,
    lng: &#39;ko&#39;,
    returnNull: false,         // 키가 없을 때 null 반환 방지
    returnEmptyString: false,  // 키가 없을 때 빈 문자열 반환 방지
    saveMissing: process.env.NODE_ENV === &#39;development&#39;, // 개발환경에서 누락 키 추적
    // ...기타 설정...
  })</code></pre>
<p>returnNull: false, returnEmptyString: false
→ 키가 없을 때 null/빈문자열이 아닌 fallback 언어(예: 한국어)로 자동 대체
→ 키값이 그대로 노출되는 현상 방지
saveMissing: true
→ 개발 환경에서 누락된 키를 자동으로 기록(콘솔, 서버, 파일 등)
→ 번역 누락을 빠르게 추적 가능</p>
<p>(2) 번역 파일 자동 동기화(권장)
모든 언어의 번역 파일에 동일한 키가 항상 존재하도록
자동화 스크립트(i18next-scanner, custom node script 등)로 관리
새 키가 추가되면 모든 언어 파일에 자동으로 추가(미번역은 빈 문자열 또는 &quot;TODO&quot; 등으로 표시)
CI(빌드) 단계에서 누락된 키가 있으면 에러로 처리</p>
<h3 id="3-실전-적용-예시">3. 실전 적용 예시</h3>
<ul>
<li><p>i18n.ts 설정 예시</p>
<pre><code class="language-js">i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
  resources,
  fallbackLng: &#39;en&#39;,
  lng: &#39;ko&#39;,
  returnNull: false,
  returnEmptyString: false,
  saveMissing: process.env.NODE_ENV === &#39;development&#39;,
  // ...기타 설정...
})</code></pre>
</li>
<li><p>번역 파일 예시 (en/translation.json)</p>
<pre><code class="language-js">{
&quot;leads&quot;: {
  &quot;dashboard&quot;: {
    &quot;period&quot;: &quot;Period&quot;
  }
}
}</code></pre>
</li>
</ul>
<h3 id="4-앞으로의-번역-관리-팁">4. 앞으로의 번역 관리 팁</h3>
<p>번역 파일에 키가 없을 때 화면에 키값이 그대로 뜨는 문제는 위 설정만 하면 더 이상 발생하지 않습니다.
개발 환경에서는 누락된 키를 자동으로 추적할 수 있어, 번역 누락도 빠르게 보완할 수 있습니다.
번역 파일만 잘 관리하면, 추가 작업 없이 안전하게 다국어 지원이 가능합니다.</p>
<h3 id="5-결론">5. 결론</h3>
<p>i18n의 옵션 몇 줄만 추가해도,
영어 번역 추가 시 키값이 그대로 노출되는 문제를 완벽하게 예방할 수 있습니다.
한 번만 세팅해두면,
앞으로는 번역 파일만 잘 관리하면 되고,
키값 노출로 인한 당황스러운 상황이 반복되는 걸 방지할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[gitlab pages] gitlab pages로 배포하기]]></title>
            <link>https://velog.io/@innes_kwak/gitlab-pages-gitlab-pages%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@innes_kwak/gitlab-pages-gitlab-pages%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 26 Jun 2025 04:57:13 GMT</pubDate>
            <description><![CDATA[<h1 id="gitlab-pages로-react-프로젝트-배포하기">GitLab Pages로 React 프로젝트 배포하기</h1>
<h2 id="🎯-배포의-필요성">🎯 배포의 필요성</h2>
<p>React + Vite로 개발한 mpi-skeleton 프로젝트를 GitLab Pages로 배포하기로 결정했습니다. GitHub Pages가 아닌 GitLab Pages를 선택한 이유는 프로젝트가 GitLab에서 관리되고 있었기 때문입니다.</p>
<p>또한 처음엔 vercel로 배포를 시도했으나, 회사 vercel 계정이 아직 무료 플랜었는데 gitlab 프로젝트는 그룹 내에 비공개 프로젝트로 위치해있어 무료버전으로는 배포가 불가능했습니다.</p>
<p>따라서 gitlab의 장점인 CI/CD 편리성을 활용해보고자 gitlab pages를 선택했습니다.</p>
<hr>
<h2 id="🛠️-초기-설정과-첫-번째-시도">🛠️ 초기 설정과 첫 번째 시도</h2>
<blockquote>
<p>커스텀 도메인을 사용하지 않는 기본 배포 과정을 정리하였습니다.</p>
</blockquote>
<ol>
<li>GitLab CI/CD 설정 파일 생성
프로젝트 루트에 <code>.gitlab-ci.yml</code> 파일을 생성했습니다:<pre><code class="language-yaml">image: node:20.19.1
// node:latest 로 해도 되지만, 혹시 모를 node 버전 충돌을 막고자 특정 버전을 지정함
</code></pre>
</li>
</ol>
<p>cache:
  paths:
    - node_modules/</p>
<p>pages:
  stage: deploy
  script:
    - npm ci
    - npm run build
    - cp -r dist/* public/
  artifacts:
    paths:
      - public
  only:
    - main</p>
<pre><code>
2. Vite 설정 수정
`vite.config.ts` 파일 수정
```ts
import { defineConfig } from &#39;vite&#39;
import react from &#39;@vitejs/plugin-react&#39;
import path from &quot;path&quot;;

export default defineConfig({
  base : &#39;/&#39;,
  plugins: [react()],
  resolve: {
    alias: {
      &quot;@&quot;: &quot;/src&quot;,
    },
  },
  build: {
    outDir: path.resolve(import.meta.dirname, &quot;dist&quot;),
    emptyOutDir: true,
  },
})</code></pre><h3 id="추가---커스텀-도메인-사용하는-경우">추가 - 커스텀 도메인 사용하는 경우</h3>
<ol>
<li><p>.gitlab-ci.yml 파일 맨 아래에 variables 추가</p>
<pre><code class="language-yml">variables:
CUSTOM_DOMAIN: &quot;&lt;커스텀 도메인&gt;&quot; </code></pre>
</li>
<li><p>public 폴더 내 CNAME 파일 추가
CNAME 파일 안에는 커스텀 도메인만 작성해둡니다.</p>
</li>
<li><p>public &gt; .well-known &gt; acme-challenge 폴더 생성
폴더 안에 .gitkeep 파일 생성 및 아래 내용 작성</p>
<pre><code> # 이 파일은 디렉토리 구조를 유지하기 위한 것입니다.
 # Let&#39;s Encrypt와 같은 SSL 인증서 발급 서비스가 acme-challenge 파일을 이 디렉토리에 저장합니다. </code></pre></li>
</ol>
<hr>
<h2 id="🚨-트러블-슈팅">🚨 트러블 슈팅</h2>
<h3 id="1-첫-번째-실패-typescript-컴파일-에러">1. 첫 번째 실패: TypeScript 컴파일 에러</h3>
<pre><code class="language-bash">src/App.tsx(46,7): error TS6133: &#39;LOARequestsPage&#39; is declared but its value is never read.
src/components/mpi/ClaimsUnderReviewTable.tsx(195,36): error TS7053: Element implicitly has an &#39;any&#39; type...</code></pre>
<ul>
<li>문제 : 사용되지 않는 변수 및 함수들, type 에러 등의 문제</li>
<li>해결 과정:
사용되지 않는 import들 제거
STATUS_CONFIG에 누락된 상태들 추가 (approved, rejected, cancelled)
타입 안전성을 위한 null 체크 구현 등</li>
</ul>
<h3 id="2-두-번째-실패-public-폴더-없음-에러">2. 두 번째 실패: public 폴더 없음 에러</h3>
<pre><code class="language-bash">cp: target &#39;public/&#39;: No such file or directory</code></pre>
<ul>
<li>문제 : 로컬에서는 분명 public 폴더가 있는데 build만 하면 public폴더가 없다고 한다. public폴더 내부에 아무 파일도 없어서 생긴 문제인 것 같았습니다.</li>
<li>해결책: <code>.gitlab-ci.yml</code>에 폴더 생성 명령어 추가<pre><code class="language-yml">script:
- npm ci
- npm run build
- mkdir -p public  # 추가
- cp -r dist/* public/</code></pre>
</li>
</ul>
<h3 id="3-세-번째-실패-배포-사이트-최초-접속시-뜨는-redirecting-메시지">3. 세 번째 실패: 배포 사이트 최초 접속시 뜨는 Redirecting 메시지</h3>
<p>배포는 성공했지만 브라우저에서 접속 시 &#39;Redirecting : ...&#39; 라는 불필요한 리다이렉팅 화면이 떴습니다. </p>
<ul>
<li>문제 : 배포 사이트의 접근 권한이 설정되어 있었기 때문!</li>
<li>해결 : 
⭐️ 마지막엔 Setting &gt; General &gt; Visibility, project features, permissions &gt; Pages 에서 배포 사이트 접근 권한을 전체공개로 바꿔줘야함!
(기본 : only project members)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/innes_kwak/post/8266ca38-8a1f-4f06-9652-49142fe74704/image.png" alt=""></p>
<p>-&gt; Redirecting 링크가 뜨고나서 사이트 접속되는걸 막고, 모두가 배포 사이트에 접속 가능하도록 설정하는 과정입니다.</p>
<hr>
<h2 id="🎉-배포-성공">🎉 배포 성공!!</h2>
<p>배포에 성공했다면, 배포 주소를 확인해봅시다.
배포 링크는 Deploy &gt; Pages에서 확인이 가능합니다.</p>
<p>main 브랜치를 배포했기 때문에, main 브랜치에 push혹은 merge를 하면 바로 재배포가 실행됩니다.</p>
<h2 id="👍-총-정리">👍 총 정리</h2>
<ul>
<li>1단계: .gitlab-ci.yml 생성</li>
<li>2단계: Vite 설정 최적화</li>
<li>3단계: 로컬에서 오류 등 미리 해결</li>
<li>4단계: 배포 실행</li>
<li>5단계: 배포 확인
GitLab 프로젝트 → CI/CD → Pipelines에서 진행 상황 확인</li>
</ul>
<h2 id="💡-핵심-포인트-및-주의사항">💡 핵심 포인트 및 주의사항</h2>
<ul>
<li>.gitlab-ci.yml은 반드시 커밋: CI/CD가 작동하려면 저장소에 포함되어야 함</li>
</ul>
<h3 id="🔧-트러블슈팅-팁">🔧 트러블슈팅 팁</h3>
<ul>
<li>404 에러 발생 시: base path 설정 재확인</li>
<li>빌드 실패 시: TypeScript 에러와 lint 에러 우선 해결</li>
<li>파일 경로 에러: public 폴더 생성 여부 확인</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Middleware] 임시 로그인 로직 리팩토링(local storage → httpOnly 쿠키)]]></title>
            <link>https://velog.io/@innes_kwak/Middleware</link>
            <guid>https://velog.io/@innes_kwak/Middleware</guid>
            <pubDate>Sun, 22 Jun 2025 11:10:06 GMT</pubDate>
            <description><![CDATA[<h1 id="임시-로그인-로직-리팩토링-과정-local-storage-→-httponly-쿠키">임시 로그인 로직 리팩토링 과정 (local storage → httpOnly 쿠키)</h1>
<blockquote>
<p>📝 백엔드로부터 로그인 api를 받기 전, 프론트에서 권한 별 페이지 접근 제어를 해야 해서 임시 로그인을 구현하게 되었다.
처음에는 local storage로 임시 키를 넣었다 빼는 로직이 간편하니까 그렇게 진행했는데, 이후 미들웨어를 사용하면서 문제가 생겼다.
아래는 local storage에서 httpOnly 쿠키로 임시 로그인 로직을 리팩토링한 과정을 작성해보았다.</p>
</blockquote>
<h2 id="기술-스택">기술 스택</h2>
<p>Next.js 프로젝트라서 httpOnly 쿠키를 주고 받는 백엔드 api를 임시로 작성할 수 있었다.</p>
<h2 id="리팩토링-과정">리팩토링 과정</h2>
<ol>
<li><p>처음엔 local storage에 값을 넣었다 뺐다 하는게 쉬우니까 그걸로 임시 로그인 로직을 구현해놨었음</p>
</li>
<li><p>사이드바 만들다보니 어쩔수없이(?) 인증 상태별 접근권한 설정이 필요해 구현</p>
<ol>
<li>로그인 상태일때만 사이드바, 각 메뉴별 페이지가 보이도록 만들어야 했음</li>
<li>local storage, zustand로 로그인 상태 판별</li>
<li>‘/’, dashboard, login, layout.tsx, RootPage 에서 각자 로그인 상태 확인해서 리다이렉트 시켜주는 아주 복잡하고 파악안되는 거미줄이 되어버림</li>
<li>→ 미들웨어를 써서 컴포넌트 책임분리 및 가독성 향상 등등등을 노려야겠군!</li>
</ol>
<ul>
<li><p>사이드바 및 기본 인증 시스템 구현 (구 버전) SPRINT5.md</p>
<pre><code class="language-markdown">  # SPRINT 5: 사이드바 및 기본 인증 시스템 구현

  ## 주요 목표

  - Figma 디자인 기반의 사이드바 UI 컴포넌트 개발
  - 임시 로그인/로그아웃 기능 및 전역 상태 관리 시스템 구축
  - 사용자의 인증 상태에 따라 동적으로 라우팅 및 레이아웃을 제어하는 로직 구현

  ---

  ## ✅ 완료된 작업

  ### 1. 사이드바 UI 및 기능 구현

  - **Figma 분석 및 아이콘 에셋 추출**:
    - Figma 디자인을 분석하여 사이드바 레이아웃과 메뉴 구조를 파악했습니다.
    - 메뉴에 필요한 모든 아이콘을 SVG 형식으로 추출하여 `src/assets/svg/sidebar` 디렉토리에 저장했습니다.
    - `svgr` 라이브러리를 설정하여 SVG 아이콘을 React 컴포넌트로 직접 불러와 사용했습니다.

  - **`Sidebar` 컴포넌트 개발 (`src/components/layout/Sidebar.tsx`)**:
    - `usePathname` 훅을 활용하여 현재 URL 경로를 감지하고, 활성화된 메뉴 항목에 동적으로 하이라이트 스타일(배경색 변경)을 적용하는 클라이언트 컴포넌트를 생성했습니다.
    - 메뉴 항목들을 `Main`, `Workflows`, `Other` 등 논리적 그룹으로 나누어 구조화하여 코드의 가독성과 유지보수성을 향상시켰습니다.

  ### 2. 임시 인증 시스템 구축

  - **Zustand를 이용한 전역 상태 관리 (`src/store/useAuthStore.ts`)**:
    - `isLoggedIn` (로그인 여부), `isLoading` (인증 상태 확인 중) 상태와 `login`, `logout`, `checkAuth` 액션을 포함하는 전역 스토어를 생성했습니다.
    - `localStorage`를 사용하여 브라우저를 새로고침해도 로그인 상태가 유지되도록 구현했습니다.

  - **인증 관련 유틸리티 컴포넌트 개발 (`src/components/auth/`)**:
    - `AuthInitializer.tsx`: 앱이 처음 로드될 때 `localStorage`를 확인하여 전역 스토어의 인증 상태를 초기화하는 컴포넌트입니다.
    - `AuthButton.tsx`: 개발 및 테스트 편의를 위해 UI에 영향을 주지 않는 임시 &#39;로그인&#39;/&#39;로그아웃&#39; 토글 버튼을 추가했습니다.

  ### 3. 인증 기반 동적 라우팅 및 레이아웃 적용

  - **인증된 사용자 전용 레이아웃 분리 (`src/app/(authenticated)/`)**:
    - Next.js의 Route Group 기능을 사용하여 URL에 영향을 주지 않는 `(authenticated)` 그룹을 생성했습니다.
    - `(authenticated)/layout.tsx`: 해당 그룹에 속한 페이지들을 위한 공통 레이아웃을 생성했습니다.
      - 이 레이아웃은 사용자가 로그인 상태일 때만 `Sidebar`와 페이지 콘텐츠를 보여줍니다.
      - 로그인되어 있지 않은 사용자가 접근할 경우, 자동으로 로그인 페이지(`/login`)로 리다이렉트 시키는 보호 로직을 구현했습니다.
    - `(authenticated)/dashboard/page.tsx`: 로그인 후 가장 먼저 보게 될 대시보드 페이지를 생성하여 보호된 라우트 그룹 내에 배치했습니다.

  - **페이지별 접근 제어 로직 구현**:
    - **루트 페이지 (`/`)**: 사용자의 인증 상태를 확인하여 `/dashboard` 또는 `/login`으로 자동 리다이렉트 시키는 역할로 변경했습니다.
    - **로그인 페이지 (`/login`)**: 이미 로그인한 사용자가 접근할 경우, 불필요한 로그인 과정을 생략하고 즉시 `/dashboard`로 이동시키는 로직을 추가했습니다.

  ## ⚙️ 기술 스택 변경 및 추가

  - **`zustand`**: 전역 상태 관리를 위해 도입
  - **`clsx`**: 조건부 스타일링의 가독성 향상을 위해 사용

  ## 💭 회고

  이번 스프린트를 통해 어드민 시스템의 핵심 기능인 사이드바와 인증 기반의 페이지 접근 제어 시스템의 기반을 마련했습니다. 특히 Zustand와 Next.js의 Route Group을 활용하여 효율적이고 확장 가능한 구조를 설계하는 데 집중했습니다. 현재 인증은 `localStorage`를 이용한 임시 방식이므로, 다음 스프린트에서는 실제 백엔드 API와 연동하여 안전한 인증 시스템을 구축하는 작업이 필요합니다. </code></pre>
</li>
</ul>
</li>
<li><p>그래서 미들웨어로 접근권한 관리를 통일하려니까 갑자기 js-cookie인가 그 라이브러리를 설치해야된다그랬음</p>
</li>
<li><p>미들웨어는 서버에서 쓰는건데 local storage는 브라우저에서만 접근이 가능해서 해당 라이브러리로 로그인 확인을 해야된다고 했음</p>
</li>
<li><p>어차피 나중에 로그인 로직 백엔드랑 연결하면서 local storage도 안쓸거고, 아마 httpOnly 쿠키로 refresh token 갱신식으로 전달해주는 로직으로 전달해줄것 같은데, 그럴거면 이 임시 로그인 로직때문에 js-cookie 라이브러리까지 다운받아야된다고? 그건 안되지</p>
</li>
<li><p>그래서 임시 로그인 로직 자체를 바꾸는 방향으로 노선을 틀었음</p>
<ol>
<li>next.js는 백엔드 api 로직 구현도 가능해서 좋다! httpOnly 쿠키에 값 넣었다 뺐다 하는 서버 api들을 만듦</li>
<li>zustand store에서 fetch로 httpOnly 쿠키에 접근하는 api에 연결하여 로그인 상태 확인하는 로직 구현!</li>
</ol>
</li>
<li><p>AuthInitializer.tsx까지 만들어서 앱 처음 로드시 서버 실제 쿠키 상태와 클라이언트의 useAuthStore를 일치시키는 ‘최초 동기화’ 작업 수행하도록 추가</p>
</li>
</ol>
<ul>
<li><p>인증 로직 리팩토링 및 미들웨어 도입 SPRINT6.md</p>
<pre><code>  # SPRINT 6: 인증 시스템 리팩토링 및 미들웨어 도입

  ## 주요 목표

  -   여러 컴포넌트에 흩어져 있던 인증 및 페이지 이동 로직을 **리팩토링**하여 중앙에서 관리
  -   Next.js 미들웨어를 도입하여 요청 단계에서 접근 제어를 처리하는 효율적이고 안전한 구조 구축
  -   `localStorage` 기반의 임시 인증을 `httpOnly` 쿠키 기반으로 변경하여 실제 백엔드 연동에 가까운 구조로 개선

  ---

  ## ✅ 완료된 작업: 인증 로직 리팩토링

  이번 스프린트는 새로운 기능 개발보다는, 기존에 구현했던 임시 인증 시스템의 구조적 문제점을 개선하는 **리팩토링**에 집중했습니다.

  ### 1. 인증 방식 변경: `localStorage`에서 `httpOnly` 쿠키로

  -   **문제점**: 기존 방식은 클라이언트 측 `localStorage`에 의존하여 미들웨어에서 접근이 불가능했고, 실제 프로덕션 인증 방식과 구조적 차이가 컸습니다.
  -   **리팩토링**:
      -   인증 상태를 관리하기 위한 임시 백엔드 API 라우트를 생성했습니다.
          -   `POST /api/auth/login`: 로그인 요청 시 `httpOnly` 속성을 가진 인증 쿠키를 생성합니다.
          -   `POST /api/auth/logout`: 인증 쿠키를 삭제합니다.
          -   `GET /api/auth/status`: 현재 쿠키 유무를 통해 로그인 상태를 반환합니다.
      -   `useAuthStore` (Zustand 스토어)가 `localStorage`를 직접 조작하는 대신, 위 API들을 `fetch`로 호출하도록 로직을 전면 수정했습니다.

  ### 2. 중앙 집중 제어: 미들웨어 도입 (`src/middleware.ts`)

  -   **문제점**: 로그인/로그아웃 상태에 따른 페이지 이동(리다이렉션) 로직이 `(authenticated)/layout.tsx`, `login/page.tsx`, `page.tsx` 등 여러 파일에 흩어져 있어 유지보수가 어려웠습니다.
  -   **리팩토링**:
      -   프로젝트 루트에 `middleware.ts` 파일을 생성하여 모든 접근 제어 로직을 통합했습니다.
      -   미들웨어는 사용자의 모든 페이지 요청을 가로채, 요청에 포함된 `auth-token` 쿠키의 유무를 확인합니다.
      -   인증 상태와 접근하려는 경로(공개/보호)를 비교하여, 서버단에서 직접 적절한 페이지로 리다이렉트 시킵니다.
      -   `matcher` 설정을 통해 API나 정적 파일 등 불필요한 경로에서는 미들웨어가 실행되지 않도록 최적화했습니다.

  ### 3. 컴포넌트 코드 단순화

  -   **문제점**: 여러 컴포넌트들이 인증 상태를 확인하고 페이지를 이동시키는 책임을 함께 가지고 있어 코드가 복잡했습니다.
  -   **리팩토링**:
      -   미들웨어가 모든 접근 제어를 담당하게 되면서, 각 페이지 컴포넌트에 있던 `useEffect` 기반의 리다이렉션 로직을 모두 제거했습니다.
      -   이를 통해 각 컴포넌트는 본래의 목적인 UI 렌더링에만 집중하게 되어, 코드가 훨씬 간결하고 명확해졌습니다.

  ## 🏁 리팩토링 결과 및 기대효과

  -   **유지보수성 향상**: 인증 관련 정책이 변경될 경우, 이제 `middleware.ts` 파일 하나만 수정하면 되므로 관리가 매우 용이해졌습니다.
  -   **구조적 개선**: 실제 프로덕션 환경에서 사용될 `httpOnly` 쿠키 기반 인증과 거의 동일한 아키텍처를 갖추게 되어, 향후 실제 백엔드 API와 연동 시 코드 변경을 최소화할 수 있습니다.
  -   **성능 및 안정성**: 클라이언트 측에서 여러 번 실행되던 리다이렉션 로직이 서버 요청 단계에서 한 번만 실행되므로, 불필요한 렌더링이나 화면 깜빡임 현상이 사라져 사용자 경험이 개선됩니다.

  ---

  ## 📝 아키텍처 변경에 따른 주요 컴포넌트 역할 재정의

  이번 리팩토링으로 인해 기존 컴포넌트들의 역할이 다음과 같이 명확하게 재정의되었습니다.

  -   **`middleware.ts`**: **페이지 접근 제어 담당**
      -   서버 요청 단계에서 쿠키를 검사하여, 허용되지 않은 페이지 접근을 막고 올바른 경로로 리다이렉트 시키는 **&#39;경비원&#39;** 역할을 전담합니다.

  -   **`useAuthStore` (Zustand 스토어)**: **클라이언트 UI 상태 관리 담당**
      -   페이지 이동 로직은 미들웨어에 위임하고, 이제 &#39;로그인&#39;/&#39;로그아웃&#39; 버튼 텍스트 변경이나 사용자 프로필 표시 등 **사용자에게 보여지는 UI를 제어하는 &#39;상태 표시판&#39;** 역할을 합니다.
      -   사용자가 &#39;로그인&#39; 버튼을 클릭했을 때, `/api/auth/login` API를 호출하는 액션을 트리거합니다.

  -   **`AuthInitializer.tsx`**: **서버-클라이언트 상태 동기화 담당**
      -   앱이 처음 로드될 때, `/api/auth/status` API를 호출하여 **서버의 실제 쿠키 상태를 클라이언트의 `useAuthStore` 상태와 일치시키는 &#39;최초 동기화&#39;** 작업을 수행합니다.
      -   이 과정을 통해 사용자가 로그인 상태임에도 불구하고 UI가 잠시 로그아웃 상태로 보이는 등의 깜빡임(flickering) 현상을 방지합니다. </code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인 시스템] typography design system 구축 과정]]></title>
            <link>https://velog.io/@innes_kwak/%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-typography-design-system-%EA%B5%AC%EC%B6%95-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@innes_kwak/%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-typography-design-system-%EA%B5%AC%EC%B6%95-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Fri, 13 Jun 2025 02:04:13 GMT</pubDate>
            <description><![CDATA[<h1 id="typography-디자인-시스템-구축-과정">typography 디자인 시스템 구축 과정</h1>
<blockquote>
<p>💬 타이틀, 서브 텍스트 등과 같이 모든 페이지에서 동일한 글꼴 크기, 색상, 굵기를 사용하고 있음에도 매번 css를 각각 직접 지정해주고 있었다. 공통된 텍스트 css를 매번 새로 지정하고 있는 비효율을 없애고, 동일한 글꼴 크기, 색상, 굵기를 사용하는 곳이라면 한 번에 가져와 사용할 수 있게 만들고 싶어서 typography design system을 구축하게 되었다.</p>
</blockquote>
<h2 id="기술스택">기술스택</h2>
<p>⚙️ next.js, tailwind css, postcss 기술스택에서 타이포그래피 디자인 시스템 구축
    - tailwind만 쓰고 postcss는 결합되어있지 않았다면 @apply로 묶어서 디자인 시스템을 구축하는 방식을 사용하지 못함.
    &gt; <code>@apply</code>는 Tailwind CSS에서 유틸리티 클래스들을 CSS 안에서 재사용 가능하게 해주는 문법     입니다.
  PostCSS가 있어야 @apply가 동작합니다. 
  (<code>@apply</code>는 CSS의 표준 문법이 아니라 Tailwind가 PostCSS 플러그인을 통해 확장한 기능이기 때문입니다.)
   ➡️ 예전에 인턴할 때도 타이포그래피 속성을 묶고싶었는데, 그때도 tailwind를 쓰고있었으나 PostCSS를 결합하지 않고 기본 Tailwind CSS만 사용했기 때문에 @apply 문법 적용이 안돼서 실패한 기억이 있어 이번 프로젝트에서는 처음부터 Tailwind를 PostCSS와 함께 설치했다.</p>
<ol start="2">
<li><p>내가 원했던 것 ex)</p>
<ul>
<li>h1에 해당하는 사이즈, 폰트 굵기, 줄간격 등을 하나의 분류로 묶고 싶었음</li>
<li>반응형 적용할 수 있도록 하고 싶었음 (ex. 같은 h1임에도 모바일에서는 크기 몇, 데스크탑에서는 크기 몇 이런 식으로)</li>
</ul>
</li>
<li><p>내가 적용한 방식</p>
<ul>
<li><p>app&gt; styles&gt; globals.css, typography.css
(globals.css는 기본적으로 root에 있고 layout.tsx에서 import하고 있으므로, styles폴더로 옮길 경우 layout.tsx에서 import 경로 수정 필수!)</p>
</li>
<li><p>globals.css에서 typography.css를 import</p>
<pre><code class="language-jsx">  @import &quot;tailwindcss&quot;;
  @import &quot;tw-animate-css&quot;;

  @import &quot;./typography.css&quot;;</code></pre>
</li>
<li><p>typography.css 작성</p>
<ul>
<li><p>주의 : tailwind css import하는건 globals.css에서만 해야함 (typography.css에서는 @layer base { } 만 작성해야 함</p>
<pre><code>@import &quot;tailwindcss&quot;;
@import &quot;tw-animate-css&quot;;</code></pre></li>
<li><p>예시</p>
<pre><code class="language-css">@layer base {
  /* Headings */
  .h1 {
    @apply text-[32px] font-bold leading-[1.31em];
  }

  .h2 {
    @apply text-[30px] font-bold leading-[1.4em];
  }

  .h3 {
    @apply text-2xl font-semibold leading-tight;
  }
}</code></pre>
</li>
</ul>
</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[다국어 지원 의사결정] i18next와 react-intl 비교]]></title>
            <link>https://velog.io/@innes_kwak/%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-i18next%EC%99%80-react-intl-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@innes_kwak/%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-i18next%EC%99%80-react-intl-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Wed, 30 Apr 2025 07:56:28 GMT</pubDate>
            <description><![CDATA[<h1 id="💬-배경">💬 배경</h1>
<p>필리핀 거래처 사이트를 제작하게 되어 다국어 지원을 알아보게 되었다.
사이트 기본 제공 언어는 영어이지만, 필리핀어(타갈로그어)와 한국어도 함께 지원하면 좋겠다는 생각이 들어 다국어 지원 기능을 알아보게 되었다.</p>
<p>이 프로젝트의 다국어 지원에서 중요한 점으로 아래의 3가지가 있었기에 이 점을 유의하여 비교 및 결정했다.</p>
<ul>
<li>필리핀어(타갈로그어) 지원 여부</li>
<li>사이트 특성 상 통화 포맷 정확도 중요</li>
<li>next.js 기반 적용하기 때문에 플리커 랭귀지(flickering language) 현상 없어야 함</li>
</ul>
<hr>
<h1 id="✨-i18next와-react-intl-비교">✨ i18next와 react-intl 비교</h1>
<p><img src="https://velog.velcdn.com/images/innes_kwak/post/8b828ecd-de5d-4774-8b60-c07f50d8b781/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>🟦 i18next (+ next-i18next)</th>
<th>🟧 react-intl (FormatJS)</th>
</tr>
</thead>
<tbody><tr>
<td>🧩 기본 개념</td>
<td>key-value 기반 번역 관리 (JSON)</td>
<td>메시지 ID 기반 포맷 + ICU 문법</td>
</tr>
<tr>
<td>✅ Next.js 호환성</td>
<td><code>next-i18next</code>로 App Router/SSR 완전 지원</td>
<td>수동 설정 필요. App Router 지원 제한적</td>
</tr>
<tr>
<td>⚙️ 설정 난이도</td>
<td>상대적으로 쉬움 (자동 라우팅, 서버 전송 자동화 등)</td>
<td>복잡 (Provider 직접 설정, 수동 번역 로딩 필요)</td>
</tr>
<tr>
<td>💡 런타임 언어 변경 (클라이언트 전환)</td>
<td><code>i18n.changeLanguage()</code>로 간단</td>
<td>상태 관리 및 Provider 교체 필요</td>
</tr>
<tr>
<td>📁 다국어 리소스 관리</td>
<td>언어별 JSON 파일 사용 (구조화 쉬움)</td>
<td>코드 내 메시지 ID로 직접 관리 (분산됨)</td>
</tr>
<tr>
<td>📉 플리커 방지 (SSR)</td>
<td>✅ <code>serverSideTranslations</code>로 완전 방지 가능</td>
<td>⚠️ 직접 설정 잘못하면 깜빡임 발생 가능</td>
</tr>
<tr>
<td>💬 복잡한 메시지 (성별, 복수형 등)</td>
<td>별도 플러그인 필요</td>
<td>✅ ICU 문법 기본 지원</td>
</tr>
<tr>
<td>💰 통화 / 날짜 / 숫자 포맷</td>
<td><code>Intl.NumberFormat</code>, <code>Intl.DateTimeFormat</code> 직접 사용</td>
<td>✅ 내장 포맷 지원 (통화, 시간, 상대시간 등)</td>
</tr>
<tr>
<td>🌍 번역 SaaS 연동 (Lokalise 등)</td>
<td>연동 잘 됨</td>
<td>제한적, 직접 변환 필요</td>
</tr>
<tr>
<td>🔧 커스터마이징</td>
<td>namespace, fallbackLng 등 유연함</td>
<td>설정 제한적</td>
</tr>
<tr>
<td>🧱 커뮤니티 및 생태계</td>
<td>매우 크고 오래된 생태계</td>
<td>Facebook이 만든 안정적 프로젝트지만 규모는 작음</td>
</tr>
<tr>
<td>📦 패키지 크기</td>
<td>약간 더 무거움</td>
<td>상대적으로 가벼움</td>
</tr>
</tbody></table>
<p>👉🏻 둘 중 i18next가 맘에 들었던 핵심적인 이유</p>
<ul>
<li>App Router/SSR 자동 지원 (추가적인 설정 필요 X)</li>
<li>런타임 언어 변경 용이</li>
<li>플리커 방지 쉬움</li>
<li><blockquote>
<p>next.js의 이점을 잘 살리면서 UX를 해치지 않는 라이브러리로, 도입의 결정적 계기!</p>
</blockquote>
</li>
<li>커뮤니티 활성화가 잘 되어 있음 (다국어 지원 기능을 처음 도입하는 본인에게는 플러스 요인이 됨)</li>
</ul>
<hr>
<h1 id="✅-i18next--next-i18next--intlnumberformat-조합의-장단점">✅ i18next + next-i18next + Intl.NumberFormat 조합의 장단점</h1>
<table>
<thead>
<tr>
<th>항목</th>
<th>장점 👍</th>
<th>단점 👎</th>
</tr>
</thead>
<tbody><tr>
<td>🈂️ 다국어 지원</td>
<td>- JSON 기반 key-value 방식으로 관리 쉬움<br>- 필리핀어(<code>tl</code>), 영어(<code>en</code>), 한국어(<code>ko</code>) 지원에 문제 없음</td>
<td>- JSON 키가 많아지면 관리 복잡해질 수 있음</td>
</tr>
<tr>
<td>🔁 실시간 언어 전환</td>
<td>- 클라이언트에서 <code>i18n.changeLanguage()</code>로 즉시 언어 변경 가능<br>- 자동 쿠키 저장 기능으로 재방문 시 언어 유지</td>
<td>- SSR 시 초기 언어 설정 신경 써야 함 (기본값 정확히 세팅 필요)</td>
</tr>
<tr>
<td>📄 SSR / RSC 호환성</td>
<td>- <code>next-i18next</code>가 <code>getStaticProps</code> / <code>getServerSideProps</code>에서 SSR 최적화 처리<br>- App Router도 공식 지원</td>
<td>- 설정 파일 많아짐 (<code>i18n.js</code>, <code>next-i18next.config.js</code> 등)</td>
</tr>
<tr>
<td>⚠️ Flickering Language (플리커 방지)</td>
<td>✅ 서버사이드에서 언어별 번역 JSON을 미리 주입하기 때문에 <strong>깜빡임 없음</strong></td>
<td>- 잘못된 <code>fallbackLng</code> 설정 시 혼란 발생 가능</td>
</tr>
<tr>
<td>💰 통화 / 숫자 포맷</td>
<td>- <strong><code>Intl.NumberFormat</code></strong> 으로 KRW, USD, PHP 등 정확한 통화 포맷 가능<br>예: <code>₩1,000</code>, <code>$1,000.00</code>, <code>₱1,000</code></td>
<td>- 별도 포맷 함수 만들어야 함 (<code>i18next</code> 기본 기능 아님)</td>
</tr>
<tr>
<td>🛠 커스터마이징</td>
<td>- namespace, fallback, 로딩 전략 등 유연한 설정 가능</td>
<td>- 너무 세세한 설정은 초심자에게 부담될 수 있음</td>
</tr>
<tr>
<td>🌍 SaaS 연동 (Lokalise, Phrase 등)</td>
<td>- i18next와 연동 매우 잘됨 → 다국어 번역 관리 용이</td>
<td>- 사용 안 하면 무관</td>
</tr>
<tr>
<td>📦 생태계 및 유지보수</td>
<td>- 인기 많고 업데이트 활발함<br>- Next.js와 잘 맞음</td>
<td>- 설정 구조가 초반에 약간 번거로움</td>
</tr>
</tbody></table>
<hr>
<h1 id="🧠-결론">🧠 결론</h1>
<p>✅ 다국어(필리핀어 포함) 지원이 안정적이고,</p>
<p>✅ SSR 기반이라 플리커 방지도 자동 해결되고,</p>
<p>✅ 통화 포맷 처리까지 Intl.NumberFormat으로 확실하게 관리 가능</p>
<p>✅ RSC(App Router)와의 궁합도 좋음</p>
<p>따라서, 이번 프로젝트처럼 <code>통화 포맷 + 다국어 라우팅 + SSR 깔끔한 적용</code>이 중요한 경우
👉 <strong>i18next + next-i18next + Intl.NumberFormat</strong> 조합이 가장 현실적이고 안정적인 선택!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스타일링 방식 의사결정] CSS-in-JS와 tailwind css 비교]]></title>
            <link>https://velog.io/@innes_kwak/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EB%B0%A9%EC%8B%9D-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-CSS-in-JS%EC%99%80-tailwind-css-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@innes_kwak/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EB%B0%A9%EC%8B%9D-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-CSS-in-JS%EC%99%80-tailwind-css-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Wed, 30 Apr 2025 07:03:59 GMT</pubDate>
            <description><![CDATA[<h1 id="💬-배경">💬 배경</h1>
<p>프로젝트 들어가기 앞서, next.js를 도입하는 프론트엔드 프로젝트에서 가장 이상적인 스타일링 방법이 무엇인지 고민이 생겼다.
최근 emotion을 쓰고 있는 회사들이 많다고 들어서 이번 기회에 우리 프로젝트에도 도입해볼까 하는 생각에 emotion이 어떤 건지 조사해보니 styled-components같은 css-in-js 스타일링이라는 걸 알게 됐고, 이를 바탕으로 tailwind css와 비교하여 어떤게 더 좋은지 비교해 본 내용을 아래와 같이 정리한다.</p>
<p>(emotion 도입 고민에 박차를 가하게 만든 아래의 npm trends 그래프. styled-components보다 다운로드 수가 높은걸 보고 제대로 비교해봐야겠다는 생각이 들었다.)</p>
<p><a href="https://npmtrends.com/@emotion/react-vs-styled-components-vs-tailwindcss">🔗 npm trends에서 emotion/react, styled-components, tailwindcss 비교</a>
<img src="https://velog.velcdn.com/images/innes_kwak/post/d4ea591d-102d-4d9b-a89d-e0def71ce5fd/image.png" alt=""></p>
<hr>
<h1 id="🧵-css-in-js-vs-tailwind-css-nextjs-기준-비교">🧵 CSS-in-JS vs Tailwind CSS (Next.js 기준 비교)</h1>
<table>
<thead>
<tr>
<th>항목</th>
<th>Emotion / Styled-Components (CSS-in-JS)</th>
<th>Tailwind CSS</th>
</tr>
</thead>
<tbody><tr>
<td>💡 스타일 정의 방식</td>
<td>JavaScript 안에서 <code>styled</code> 또는 <code>css</code>로 정의 (동적)</td>
<td>정적 className 기반 유틸리티 CSS</td>
</tr>
<tr>
<td>⚙️ 실행 시점</td>
<td>런타임에 JS로 스타일 생성 및 삽입</td>
<td>빌드 타임에 class 기준으로 CSS 생성</td>
</tr>
<tr>
<td>⚡ 성능 (렌더링 &amp; hydration)</td>
<td>props 기반 스타일 재계산 발생 → 렌더/하이드레이션 지연 가능성 있음</td>
<td>정적 class 적용 → 렌더/하이드레이션 매우 빠름</td>
</tr>
<tr>
<td>🧠 서버 컴포넌트 (RSC) 호환성</td>
<td>제한적. JS 기반 스타일 계산이 서버 컴포넌트 흐름에 맞지 않음</td>
<td>매우 우수. 정적 CSS는 RSC와 자연스럽게 통합됨</td>
</tr>
<tr>
<td>🧩 런타임 CSS rule 처리</td>
<td>렌더될 때마다 <code>&lt;style&gt;</code> 태그에 CSS rule이 계속 추가됨</td>
<td>사전에 purge된 CSS만 사용됨, 추가 없음</td>
</tr>
<tr>
<td>🧮 번들 크기</td>
<td>추가 JS 코드 포함 (약 20~35KB gzipped)</td>
<td>거의 없음 (purge된 CSS만 사용)</td>
</tr>
<tr>
<td>🔁 중복 스타일 이슈</td>
<td>동일한 스타일이라도 여러 컴포넌트에서 각각 rule 생성 가능</td>
<td>동일 className 재사용됨 (중복 없음)</td>
</tr>
<tr>
<td>🔧 동적 스타일링</td>
<td>매우 유연 (<code>props</code> 기반 조건부 스타일)</td>
<td>class 조합으로 구현 (<code>clsx</code>, <code>classnames</code> 등 필요)</td>
</tr>
<tr>
<td>🧪 디버깅</td>
<td>className이 해시화되어 추적 어려움 (<code>css-abc123</code>)</td>
<td>직관적인 className (<code>bg-blue-500</code>, <code>text-sm</code> 등)</td>
</tr>
<tr>
<td>🎯 개발 경험</td>
<td>컴포넌트 단위 스타일 관리 편리</td>
<td>초반엔 class 조합 복잡해보일 수 있으나 익숙해지면 빠름</td>
</tr>
<tr>
<td>📦 설치 및 설정</td>
<td>별도 설정 필요 (<code>babel-plugin</code>, SSR 지원 등 고려)</td>
<td>PostCSS 기반 설정만으로 간단히 가능</td>
</tr>
</tbody></table>
<ul>
<li><p>스타일 방식:
CSS-in-JS는 JS 안에서 styled나 css 함수를 통해 스타일을 정의하고, Tailwind는 미리 정의된 className을 조합해 사용하는 방식이다.</p>
</li>
<li><p>실행 시점:
CSS-in-JS는 렌더링 중에 스타일을 동적으로 생성하는 반면, Tailwind는 빌드 시 정적 class만 포함되므로 클라이언트에서 추가 연산이 없다.</p>
</li>
<li><p>hydration 성능:
CSS-in-JS는 props 기반 스타일 재계산이 필요해 hydration 속도가 느려질 수 있고, Tailwind는 정적 class로 인해 매우 빠르고 안정적이다.</p>
</li>
<li><p>서버 컴포넌트(RSC) 호환성:
CSS-in-JS는 런타임 기반이라 RSC 흐름과 잘 맞지 않지만, Tailwind는 서버 컴포넌트 구조와 완전히 호환된다.</p>
</li>
<li><p>런타임 스타일 처리:
CSS-in-JS는 렌더링마다 새로운 CSS rule을 <code>&lt;style&gt;</code> 태그에 추가하고, Tailwind는 미리 정리된 class만 사용하므로 성능에 부담이 없다.</p>
</li>
<li><p>번들 크기:
CSS-in-JS는 약 20~35KB(gzipped)의 JS 코드를 추가하고, Tailwind는 purge된 최소한의 CSS만 포함되어 훨씬 가볍다.</p>
</li>
<li><p>중복 스타일:
CSS-in-JS는 같은 스타일도 컴포넌트마다 새롭게 생성되기 쉬우며, Tailwind는 className을 재사용하기 때문에 중복이 없다.</p>
</li>
<li><p>동적 스타일링:
CSS-in-JS는 props 기반으로 유연하게 처리할 수 있고, Tailwind는 조건부 class 조합(clsx, classnames)이 필요하다.</p>
</li>
<li><p>디버깅 편의성:
CSS-in-JS는 해시 class로 인해 디버깅이 어려운 반면, Tailwind는 의미 있는 class명이라 추적이 쉽다.</p>
</li>
<li><p>개발 경험:
CSS-in-JS는 컴포넌트 단위 스타일링이 익숙한 사람에게 편리하지만, Tailwind는 한번 익숙해지면 빠른 개발이 가능하고 유지보수가 편하다.</p>
</li>
<li><p>설정 난이도:
CSS-in-JS는 SSR 지원과 Babel 설정 등 별도 설정이 필요하지만, Tailwind는 PostCSS 기반의 단순한 설정으로 빠르게 도입 가능하다.</p>
</li>
</ul>
<hr>
<h1 id="✍️-결론">✍️ 결론</h1>
<p>Next.js의 가장 큰 장점은 Server-Side Rendering(SSR) 및 <strong>React Server Components(RSC)</strong>를 통해 서버에서 미리 HTML을 렌더링하고 클라이언트에 전달함으로써 최적의 퍼포먼스를 확보하는 것이다.
하지만 Emotion이나 Styled Components처럼 JS 기반 CSS-in-JS는 이러한 구조와 맞지 않는 실행 흐름과 런타임 비용, 그리고 hydration 단계에서의 불리함이 존재한다.</p>
<p>특히 props 기반 동적 스타일링은 렌더링 및 hydration 시점마다 스타일 재계산이 필요하여 브라우저 성능에 부담을 줄 수 있고, <code>&lt;style&gt;</code> 태그에 css rule이 누적되면서 style tree도 무거워질 수 있다.</p>
<p>이에 반해 Tailwind CSS는 정적으로 미리 정의된 className만 사용되기 때문에 hydration 시 연산이 없고, 서버 컴포넌트와 완벽히 호환되며, 번들 크기도 최소화할 수 있어 Next.js의 철학과 구조에 가장 잘 맞는 스타일링 도구라고 판단했다.</p>
<p>👉 따라서, 이번 프로젝트에서는 <code>Tailwind CSS</code>를 스타일링 도구로 선택했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MCP] MCP 연결 트러블슈팅 (TalkToFigma + Claude)]]></title>
            <link>https://velog.io/@innes_kwak/MCP-MCP-%EC%97%B0%EA%B2%B0-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-TalkToFigma-Claude</link>
            <guid>https://velog.io/@innes_kwak/MCP-MCP-%EC%97%B0%EA%B2%B0-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-TalkToFigma-Claude</guid>
            <pubDate>Mon, 28 Apr 2025 01:46:52 GMT</pubDate>
            <description><![CDATA[<h1 id="🛠️-트러블슈팅">🛠️ 트러블슈팅</h1>
<h2 id="문제-1-bun-자체가-설치되어-있지-않아-발생한-에러">문제 1: <code>bun</code> 자체가 설치되어 있지 않아 발생한 에러</h2>
<ul>
<li><strong>문제</strong><br>MCP 서버를 실행할 때 <code>spawn bunx ENOENT</code> 에러 발생 (bun이 아예 설치되지 않은 상태)
<img src="https://velog.velcdn.com/images/innes_kwak/post/d0657cbc-727b-4d24-afe6-2fb0647c9485/image.png" alt=""></li>
</ul>
<ul>
<li><p><strong>원인</strong><br><code>bun</code> 런타임이 시스템에 설치되어 있지 않아서 <code>bunx</code>를 사용할 수 없는 상황</p>
</li>
<li><p><strong>해결</strong><br>터미널에 다음 명령어를 입력하여 <code>bun</code> 설치:</p>
<pre><code class="language-bash">curl -fsSL https://bun.sh/install | bash</code></pre>
</li>
</ul>
<hr>
<h2 id="문제-2-터미널에서-bunx-명령어를-찾을-수-없음">문제 2: 터미널에서 <code>bunx</code> 명령어를 찾을 수 없음</h2>
<ul>
<li><p><strong>문제</strong><br>터미널에서 <code>bunx --version</code> 입력 시 <code>command not found: bunx</code> 에러 발생</p>
</li>
<li><p><strong>원인</strong><br><code>bun</code>은 설치되었지만, 설치 경로(<code>~/.bun/bin</code>)가 터미널의 PATH에 등록되어 있지 않아 <code>bunx</code> 명령어를 인식할 수 없음</p>
</li>
<li><p><strong>해결</strong><br><code>~/.zshrc</code> 파일에 다음을 추가하여 PATH 설정:</p>
<pre><code class="language-bash">export BUN_INSTALL=&quot;$HOME/.bun&quot;
export PATH=&quot;$BUN_INSTALL/bin:$PATH&quot;</code></pre>
<p>이후 적용:</p>
<pre><code class="language-bash">source ~/.zshrc</code></pre>
<p>→ 터미널에서 <code>bunx</code> 명령어 정상 인식 확인</p>
</li>
</ul>
<hr>
<h2 id="문제-3-claude에서-bunx-명령어를-찾을-수-없음-spawn-bunx-enoent-에러">문제 3: Claude에서 <code>bunx</code> 명령어를 찾을 수 없음 (<code>spawn bunx ENOENT</code> 에러)</h2>
<ul>
<li><p><strong>문제</strong><br>Claude를 실행할 때 MCP 서버(TalkToFigma) 연결 시도 중 <code>spawn bunx ENOENT</code> 에러 발생</p>
</li>
<li><p><strong>원인</strong><br>Claude는 자체적으로 독립된 PATH 환경을 사용하기 때문에 터미널에 설정된 PATH(<code>~/.bun/bin</code>)를 인식하지 못함</p>
</li>
<li><p><strong>해결</strong><br><code>claude_desktop_config.json</code> 파일을 수정하여 MCP 서버 실행 명령어를 절대 경로로 지정:</p>
<pre><code class="language-json">&quot;mcpServers&quot;: {
  &quot;TalkToFigma&quot;: {
    &quot;command&quot;: &quot;/Users/innes/.bun/bin/bunx&quot;,
    &quot;args&quot;: [&quot;cursor-talk-to-figma-mcp@latest&quot;, &quot;--server=vps.sonnylab.com&quot;]
  }
}</code></pre>
<p>설정 수정 후 Claude 앱을 완전히 종료하고 재실행하여 정상 연결 확인</p>
</li>
</ul>
<hr>
<h1 id="✨-최종-요약">✨ 최종 요약</h1>
<ul>
<li>bun 설치로 기본 환경 구성</li>
<li>터미널에서 PATH 설정으로 <code>bunx</code> 인식 해결</li>
<li>Claude에서는 명령어 절대 경로 지정으로 MCP 서버 연결 성공</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[github actions] github issues, github projects, PR 자동화를 향한 여정]]></title>
            <link>https://velog.io/@innes_kwak/github-actions-github-issues-github-projects-PR-%EC%9E%90%EB%8F%99%ED%99%94%EB%A5%BC-%ED%96%A5%ED%95%9C-%EC%97%AC%EC%A0%95</link>
            <guid>https://velog.io/@innes_kwak/github-actions-github-issues-github-projects-PR-%EC%9E%90%EB%8F%99%ED%99%94%EB%A5%BC-%ED%96%A5%ED%95%9C-%EC%97%AC%EC%A0%95</guid>
            <pubDate>Fri, 25 Apr 2025 01:13:37 GMT</pubDate>
            <description><![CDATA[<h1 id="💬-배경">💬 배경</h1>
<p>cursor AI와 함께 코드를 작성하는 연습을 하다보니, 알잘딱깔센으로 commit, push를 해주지 않아서 내가 신경써서 해달라고 요청하지 않는 이상 commit, push를 작은 단위 별로 꼼꼼하게 남겨놓는게 쉽지 않았다.</p>
<p>그래서 생각한게, 요즘 cursor ai + mcp 결합이 유행이던데, mcp를 조사해보면 자동화 구현에 도움이 되지 않을까 싶어서 시작하게 된 여정이다.</p>
<p>내가 원하는 기능은 딱 2가지였다.</p>
<ol>
<li>github issue를 내가 issue template 기반, 수기로 작성하면 업로드된 issue를 github projects TODO에 자동 업로드 시키기(이슈번호 포함)</li>
</ol>
<p>1-1. 비슷한 원리로, issue에서 특정 이슈 deleted 됐을때, Todo에서도 삭제하기 (closed는 삭제 X. PR해서 closed된 이슈마저 삭제하면 안되기 때문)</p>
<ol start="2">
<li>PR이 업로드되는 순간 projects의 해당 이슈번호들의 todo들은 in progress 단계로 자동 이동</li>
</ol>
<blockquote>
<p>📌 추가로, 원하는 기능이지만 github actions가 필요한게 아닌, cursor prompt에 명령하는 것 만으로도 구현이 가능했던 기능</p>
</blockquote>
<p>👉🏻 특정 명령어 입력하면 PR 알아서 올려주기!</p>
<blockquote>
</blockquote>
<hr>
<p>(gpt에게 질문했던 프롬프트)
커서와 기능 구현할때 커밋, 푸쉬는 내가 명령해서 수동으로 할거야. 그리고 PR template도 내가 알아서 .github에 넣어둘거야.
근데 내가 PR이 하고싶어지면 커서에게 &#39;@pr closes #20, #30&#39; 이런식으로 이슈번호가 포함된 특정 명령어를 입력하고, 커서가 알아서 해당 이슈들, 코드를 검토해서 PR template에 내용을 알아서 정리해서 PR올리도록 만들고싶어.</p>
<blockquote>
</blockquote>
<hr>
<p>📌 추가 2. 내가 원하는 기능이었지만 기본 workflow에 있는 기능이라서 굳이 action을 만들 필요가 없었던 기능
👉🏻 merge 되는 순간 projects의 in progress에 있던 해당 이슈들은 DONE으로 이동</p>
<ul>
<li>전제 조건<ul>
<li>default branch로의 merge일 것</li>
<li>pr discription 내부에 &#39;closes #이슈번호&#39; 적혀 있을 것</li>
</ul>
</li>
</ul>
<p>위의 3가지 기능을 알아보니 github actions + github api만으로도 구현이 가능한 기능이었지만, 구현하는 과정에서 mcp도 결합할 수 있다 하여 gpt, cursor ai와 함께하는 github 자동화 여정을 시작한다.</p>
<hr>
<h1 id="👩🏻💻-구현-과정">👩🏻‍💻 구현 과정</h1>
<h2 id="1️⃣-issue-업로드-시-projects-todo에-자동-업로드">1️⃣ issue 업로드 시 projects Todo에 자동 업로드</h2>
<blockquote>
<p>✅ “Issue가 생성되면 GitHub Projects의 ‘To Do’ 열로 자동 이동”
이걸 GitHub Actions + GitHub API + MCP 주석을 함께 써서 직접 구현해보자!</p>
</blockquote>
<h3 id="1-사전-확인이-필요한-github-projects-정보">1. 사전 확인이 필요한 GitHub Projects 정보</h3>
<p>(projects v2 (beta)라는 가정 하에)</p>
<p>1) <code>project_id</code> (또는 project_number)
2) column_name 혹은 status name (예: &quot;To Do&quot;)
3) 인증용 GITHUB_TOKEN (자동으로 제공됨)
(GITHUB_TOKEN ❌ secrets.PROJECT_ACCESS_TOKEN ✅ 로 문제 개선 됨)</p>
<p>✅ 1단계: <a href="https://docs.github.com/ko/graphql/overview/explorer">GraphQL Explorer</a> 접속
✅ 2단계: 쿼리문 복사해서 붙여넣기</p>
<pre><code class="language-graphql">query {
  user(login: &quot;innes-k&quot;) {
    projectV2(number: 4) {
      id
      title
      fields(first: 20) {
        nodes {
          ... on ProjectV2Field {
            id
            name
          }
          ... on ProjectV2SingleSelectField {
            id
            name
            options {
              id
              name
            }
          }
        }
      }
    }
  }
}
</code></pre>
<p>✅ 3단계 : 실행하여 아래 3가지 추출
<code>projectV2.id</code>
<code>&quot;Status&quot; 필드의 id</code>
<code>&quot;Todo&quot; 옵션의 id</code></p>
<blockquote>
<p>📌 폴더 관리 계획</p>
</blockquote>
<ul>
<li>github actions 관련 : .github/workflows/[파일명.yml]</li>
<li>cursor, mcp 등 자동화 관련 : root경로/dev-helper/[파일명]</li>
</ul>
<br/>

<h3 id="2-githubworkflows파일명yml-작성">2. .github/workflows/[파일명.yml] 작성</h3>
<p>❌ 1차 시도
-&gt; 개선 필요 : query문 및 GITHUB_TOKEN을 PAT(Project Access Token)로 개선</p>
<pre><code class="language-yml">name: Add Issue to Project Todo

on:
  issues:
    types: [opened]

jobs:
  add-to-project:
    runs-on: ubuntu-latest
    steps:
      - name: Add issue to project
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const query = `
              mutation {
                addProjectV2ItemById(input: {
                  projectId: &quot;PVT_kwHOCNlLx84A3hwL&quot;,
                  contentId: &quot;${{ github.event.issue.node_id }}&quot;
                }) {
                  item {
                    id
                  }
                }
              }
            `;
            const result = await github.graphql(query);

            const itemId = result.addProjectV2ItemById.item.id;

            const setStatus = `
              mutation {
                updateProjectV2ItemFieldValue(input: {
                  projectId: &quot;PVT_kwHOCNlLx84A3hwL&quot;,
                  itemId: &quot;${itemId}&quot;,
                  fieldId: &quot;PVTSSF_lAHOCNlLx84A3hwLzgspl6M&quot;,
                  value: {
                    singleSelectOptionId: &quot;f75ad846&quot;
                  }
                }) {
                  projectV2Item {
                    id
                  }
                }
              }
            `;
            await github.graphql(setStatus);
</code></pre>
<p>✅ 정상 동작</p>
<pre><code class="language-yml">name: Add Issue to Project Todo

on:
  issues:
    types: [opened]

permissions:
  issues: write
  contents: read

jobs:
  add-to-project:
    runs-on: ubuntu-latest
    steps:
      - name: Add issue to project and set status to Todo
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.PROJECT_ACCESS_TOKEN }}
          script: |
            // 1. 이슈를 프로젝트에 추가
            const addToProjectQuery = `
              mutation {
                addProjectV2ItemById(input: {
                  projectId: &quot;PVT_kwHOCNlLx84A3hwL&quot;,
                  contentId: &quot;${{ github.event.issue.node_id }}&quot;
                }) {
                  item {
                    id
                  }
                }
              }
            `;

            const addResult = await github.graphql(addToProjectQuery);
            const itemId = addResult.addProjectV2ItemById.item.id;

            // 2. 항목의 상태를 Todo로 설정
            const setStatusQuery = `
              mutation {
                updateProjectV2ItemFieldValue(input: {
                  projectId: &quot;PVT_kwHOCNlLx84A3hwL&quot;,
                  itemId: &quot;${itemId}&quot;,
                  fieldId: &quot;PVTSSF_lAHOCNlLx84A3hwLzgspl6M&quot;,
                  value: {
                    singleSelectOptionId: &quot;f75ad846&quot;
                  }
                }) {
                  projectV2Item {
                    id
                  }
                }
              }
            `;

            await github.graphql(setStatusQuery);

            // 3. 이슈 제목에 이슈 번호 추가
            const issueNumber = context.issue.number;
            const issueTitle = context.payload.issue.title;

            if (!issueTitle.includes(`#${issueNumber}`)) {
              await github.rest.issues.update({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issueNumber,
                title: `#${issueNumber} ${issueTitle}`
              });
            }
</code></pre>
<br/>

<h3 id="3-commit-push-필수">3. commit, push 필수!</h3>
<p>변경사항이 있다면 push까지 마쳐야 .github를 인식해서 action workflow가 정상적으로 업로드된다.</p>
<br/>

<h2 id="🛠️-run-과정-중-트러블슈팅">🛠️ run 과정 중 트러블슈팅</h2>
<h3 id="🤔-문제">🤔 문제</h3>
<p><code>&quot;Resource not accessible by integration&quot;</code> 오류는 GitHub Action이 GitHub Project에 접근할 수 있는 권한이 부족해서 발생하는 문제</p>
<p><img src="https://velog.velcdn.com/images/innes_kwak/post/45ff117c-098d-43a7-be79-abd0d888d600/image.png" alt=""></p>
<h3 id="🌈-해결">🌈 해결</h3>
<ol>
<li>settings &gt; actions &gt; general &gt; workflow permissions 를 첫번째인 <code>Read and write permissions</code> 로 변경해주기</li>
</ol>
<p><img src="https://velog.velcdn.com/images/innes_kwak/post/03e6164d-0788-4cce-9257-5b287814c5ad/image.png" alt=""></p>
<p>이후 한번 더 push해야 적용됨!</p>
<ul>
<li>GitHub은 워크플로우 설정이 변경된 뒤에 새로 실행된 워크플로우에만 적용해줌</li>
<li>따라서 .github/workflows/issue-to-project.yml 파일에 공백 추가해서 푸시라도 해야 됨</li>
</ul>
<pre><code class="language-bash">git commit --allow-empty -m &quot;trigger workflow again after permission fix&quot;
git push origin main
</code></pre>
<br/>

<ol start="2">
<li>action yml파일 중 github-token을 변경하기</li>
</ol>
<ul>
<li>기존 : <code>github-token: ${{ secrets.GITHUB_TOKEN }}</code></li>
<li>변경 : <code>github-token: ${{ secrets.PROJECT_ACCESS_TOKEN }}</code></li>
</ul>
<br/>

<ol start="3">
<li>PAT(Project Access Token) 발급하기</li>
</ol>
<blockquote>
<p>📝 PROJECT_ACCESS_TOKEN 생성 및 등록 방법</p>
</blockquote>
<p>특정 저장소의 GitHub Actions가 제대로 작동하려면 Project 접근 권한이 있는 Personal Access Token(PAT)이 필요합니다.</p>
<ol>
<li>GitHub 계정 설정으로 이동: 우측 상단 프로필 클릭 &gt; Settings</li>
<li>좌측 메뉴에서 Developer settings &gt; Personal access tokens &gt; Tokens (classic) 클릭</li>
<li>&quot;Generate new token&quot; &gt; &quot;Generate new token (classic)&quot; 클릭</li>
<li>다음 설정으로 토큰 생성:<ul>
<li>Note: cursor-practice-project-access</li>
<li>Expiration: 원하는 기간 선택 (최소 30일 이상 권장)</li>
<li>Scopes:<ul>
<li><code>repo</code> (전체 선택)</li>
<li><code>admin:org</code> &gt; <code>write:org</code> 선택 (Project 접근에 필요)</li>
<li><code>project</code> (전체 선택)</li>
</ul>
</li>
</ul>
</li>
<li>&quot;Generate token&quot; 클릭 후 생성된 토큰 복사 (이 페이지를 벗어나면 다시 확인 불가)</li>
<li>저장소로 돌아와서 Settings &gt; Secrets and variables &gt; Actions 클릭</li>
<li>&quot;New repository secret&quot; 클릭 후 다음 정보 입력:<ul>
<li>Name: PROJECT_ACCESS_TOKEN</li>
<li>Secret: 복사한 토큰 값 붙여넣기</li>
</ul>
</li>
<li>&quot;Add secret&quot; 클릭하여 저장</li>
</ol>
<p>이제 GitHub Actions 워크플로우에서 <code>${{ secrets.PROJECT_ACCESS_TOKEN }}</code>으로 해당 토큰에 접근할 수 있습니다. </p>
<hr>
<h2 id="2️⃣-pr-업로드-시-todo---in-progress-자동-이동">2️⃣ PR 업로드 시 Todo -&gt; in Progress 자동 이동</h2>
<ol>
<li>cursor ai 프롬프트 작성</li>
</ol>
<blockquote>
<p>새로운 action을 만들거야.</p>
</blockquote>
<p>일단 너가 PR을 알아서 올릴 수 있도록 PR방식을 먼저 설명해줄게.
내가 &quot;@pr closes #20 #30&quot; 형식으로 명령어를 작성하면, 지금까지 작성한 코드와 내가 #으로 남겨준 이슈번호를 토대로 검토 후, .github &gt; pull_request_template 파일의 PR template 양식을 바탕으로 현재 브랜치의 작업 내용을 PR로 작성 후,  dev 브랜치로 PR 업로드해줘.</p>
<blockquote>
</blockquote>
<p>그리고 PR이 github에 업로드되는 순간, PR closes 뒤에 적어둔 이슈 번호들이 적힌 projects Todo들을 in progress 단계로 자동으로 이동시키는 actions을 만들어줘.</p>
<blockquote>
</blockquote>
<hr>
<p>** 📌 여기에 추가해야 할 내용**
(이 프롬프트 이후에 개선한 내용)</p>
<blockquote>
</blockquote>
<ol>
<li>PR 제목을 브랜치 명이 아닌, 명령어에 포함되어 있는 이슈들의 제목들과 코드로 구현한 내용을 검토한 후, 모든 내용을 포괄할 수 있는 말로 PR 제목을 선정해줘.<blockquote>
</blockquote>
</li>
<li>PR template 안에 관련 이슈번호 적을때 closes #1 #2 #3 이 형식이 아니라, 
closes #1
closes #2
closes #3
이렇게 적도록 해줘.</li>
</ol>
<ol start="2">
<li>.github/workflows/move-issues-to-progress 문서 자동 작성<pre><code class="language-yml">name: Move Issues to In Progress when PR Created
</code></pre>
</li>
</ol>
<p>on:
  pull_request:
    types: [opened, reopened]</p>
<p>permissions:
  pull-requests: read
  contents: read</p>
<p>jobs:
  move-issues-to-in-progress:
    runs-on: ubuntu-latest
    steps:
      - name: Move related issues to In Progress
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.PROJECT_ACCESS_TOKEN }}
          script: |
            // PR 내용 가져오기
            const prNumber = context.payload.pull_request.number;
            const prBody = context.payload.pull_request.body;</p>
<pre><code>        // PR 설명에서 &quot;closes #숫자&quot; 패턴 찾기 (여러 이슈 번호 지원)
        // 1. 이슈 키워드(closes, fixes 등) 뒤에 오는 첫 번째 이슈 번호 찾기
        const closesPattern = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)/gi;
        const issueNumbers = [];
        let match;

        while ((match = closesPattern.exec(prBody)) !== null) {
          issueNumbers.push(parseInt(match[1]));
        }

        // 2. PR 설명에서 모든 #숫자 형식 찾기
        const hashNumberPattern = /#(\d+)/g;
        const allMatches = [...prBody.matchAll(hashNumberPattern)];

        // 이미 찾은 이슈 번호 외의 새로운 이슈 번호만 추가
        for (const match of allMatches) {
          const num = parseInt(match[1]);
          if (!issueNumbers.includes(num)) {
            issueNumbers.push(num);
          }
        }

        console.log(`Found issue numbers: ${issueNumbers.join(&#39;, &#39;)}`);

        if (issueNumbers.length === 0) {
          console.log(&#39;No related issues found in PR description&#39;);
          return;
        }

        // 1. 프로젝트 아이템 조회
        const findItemsQuery = `
          query {
            user(login: &quot;${{ github.repository_owner }}&quot;) {
              projectV2(number: 4) {
                items(first: 100) {
                  nodes {
                    id
                    content {
                      ... on Issue {
                        id
                        number
                      }
                    }
                    fieldValues(first: 10) {
                      nodes {
                        ... on ProjectV2ItemFieldSingleSelectValue {
                          name
                          field {
                            ... on ProjectV2SingleSelectField {
                              id
                              name
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        `;

        const result = await github.graphql(findItemsQuery);
        const projectItems = result.user.projectV2.items.nodes;

        // 2. 각 이슈에 대해 처리
        for (const issueNumber of issueNumbers) {
          // 2.1 프로젝트에서 해당 이슈를 찾기
          const projectItem = projectItems.find(item =&gt; 
            item.content &amp;&amp; 
            item.content.number === issueNumber
          );

          if (!projectItem) {
            console.log(`Issue #${issueNumber} not found in project`);
            continue;
          }

          // 2.2 현재 상태가 Todo인 경우에만 In Progress로 변경
          const statusField = projectItem.fieldValues.nodes.find(
            node =&gt; node.field &amp;&amp; node.field.name === &#39;Status&#39;
          );

          if (!statusField || statusField.name !== &#39;Todo&#39;) {
            console.log(`Issue #${issueNumber} is not in Todo status`);
            continue;
          }

          // 2.3 상태를 In Progress로 변경
          const updateStatusQuery = `
            mutation {
              updateProjectV2ItemFieldValue(input: {
                projectId: &quot;PVT_kwHOCNlLx84A3hwL&quot;,
                itemId: &quot;${projectItem.id}&quot;,
                fieldId: &quot;PVTSSF_lAHOCNlLx84A3hwLzgspl6M&quot;,
                value: {
                  singleSelectOptionId: &quot;47fc9ee4&quot;
                }
              }) {
                projectV2Item {
                  id
                }
              }
            }
          `;

          await github.graphql(updateStatusQuery);
          console.log(`Successfully moved issue #${issueNumber} to In Progress`);
        } </code></pre><pre><code>- dev-helper/README.md 에 구체적인 내용도 함께 작성해줌
- dev-helper/create-pr.sh 에 PR 자동 생성 해주는 코드 들어 있음
&lt;br/&gt;
3. Github CLI 설치 및 gh auth login

1) [GitHub CLI](https://cli.github.com/manual/installation) 설치
   ```bash
   # macOS (Homebrew 사용)
   brew install gh

   # Windows (Scoop 사용)
   scoop install gh

   # Linux (Debian/Ubuntu)
   sudo apt install gh</code></pre><p>2) GitHub CLI에 로그인</p>
<pre><code class="language-bash">   gh auth login</code></pre>
<ul>
<li>프롬프트에 따라 브라우저에서 인증을 완료하거나 토큰을 입력합니다.</li>
</ul>
<blockquote>
<p>주의 : default branch 를 dev로 바꿔두어야 dev에 PR 올림과 동시에 해당 이슈들이 닫힘.</p>
</blockquote>
<br/>

<h3 id="📌-주의사항">📌 주의사항!</h3>
<ul>
<li>PR template에 closes #이슈번호 적을때는
❌ closes #1 #2 #3
✅
closes #1
closes #2
closes #3</li>
</ul>
<p>이렇게 적어야 PR merge시 이슈들이 전부 제대로 닫힌다.</p>
<ul>
<li>merge 시 이슈 닫히도록 만드는건
default branch로 merge할 때만 가능하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ 인턴 4주차 ] 애증의 이메일 드롭다운 - 리팩토링 변천사]]></title>
            <link>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-4%EC%A3%BC%EC%B0%A8-%EC%95%A0%EC%A6%9D%EC%9D%98-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%93%9C%EB%A1%AD%EB%8B%A4%EC%9A%B4-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EB%B3%80%EC%B2%9C%EC%82%AC</link>
            <guid>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-4%EC%A3%BC%EC%B0%A8-%EC%95%A0%EC%A6%9D%EC%9D%98-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%93%9C%EB%A1%AD%EB%8B%A4%EC%9A%B4-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EB%B3%80%EC%B2%9C%EC%82%AC</guid>
            <pubDate>Sun, 19 Jan 2025 07:43:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/innes_kwak/post/107f081f-1781-4abb-8cf5-d221b37953cd/image.png" alt=""></p>
<h1 id="👩🏻💻-이메일-드롭다운-1차-리팩토링">👩🏻‍💻 이메일 드롭다운 1차 리팩토링</h1>
<h2 id="📝-로직-정리">📝 로직 정리</h2>
<p>✅- 이메일 드롭다운 선택 탭 내용을 이메일 주소에 반영</p>
<ol>
<li><p>드롭다운에서 특정 이메일을 선택하면 : input에 내가 쓴 이메일 + 드롭다운에 선택한 이메일
둘을 합쳐서 formData의 email로 보내고싶어.</p>
</li>
<li><p>드롭다운에서 &#39;직접 입력&#39; 선택시 : input에 내가 쓴 이메일이 그대로 formData로 전달되기</p>
</li>
<li><p>&#39;직접입력&#39; 선택했는데 @랑 .com .net 등의 이메일 형식에 맞는 input값이 들어오지 않았다면
&#39;이메일 형식이 올바르지 않습니다&#39; 라고 에러 메시지를 보여주고싶어.</p>
</li>
</ol>
<p>-&gt;
input type에 email이라고 전달하기
register안에 pattern으로 이메일 형식을 지정해두고, 여기에 맞지 않으면 해당 에러 메시지를 커스텀해서 출력할 수 있다! 우왕…</p>
<pre><code class="language-tsx">// LabelInputSet.tsx 일부

 &lt;input
        id={id}
        autoComplete={id}
        {...register(id, {
          required: `${labelText}을(를) 입력해 주세요.`,
          pattern:
            type === &quot;email&quot; // 이메일 형식 검증
              ? {
                  value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
                  message: &quot;이메일 형식이 올바르지 않습니다.&quot;,
                }
              : undefined,
        })}
        placeholder={placeholder}
        type={type}
        className={styles.input}
        style={{ width }}
      /&gt;</code></pre>
<ul>
<li>유저가 쓴 이메일이랑 드롭다운에 선택된 이메일이랑 합쳐서 or 직접입력 한 이메일을 formData로 제출하는 방법<pre><code class="language-tsx">const { setValue, watch } = useFormContext(); 
// ✅ react-hook-form을 활용하여 이메일 값 관리
const email = watch(&quot;email&quot;); 
// ✅ 현재 입력된 이메일 값 가져오기
…
&lt;EmailDropdown
          email={email}
          setEmail={(updatedEmail) =&gt; 
          setValue(&quot;email&quot;, updatedEmail)}
        /&gt;</code></pre>
setValue를 활용해서 드롭다운에서 선택하면 updateEmail로 수정되도록 하는 로직으로 만들면 됐다!</li>
</ul>
<hr>
<h2 id="🛠️-트러블슈팅">🛠️ 트러블슈팅</h2>
<h3 id="1️⃣-문제--해결">1️⃣ 문제 &amp; 해결</h3>
<ul>
<li><p>🛠️  문제 : 이메일 드롭다운이랑 LabelInputSet.tsx랑 별개의 컴포넌트로 되어있다보니까 유저가 작성한 이메일이랑 드롭다운에 선택된 이메일형식이랑 합치는게 어려웠다.
그래서 둘다 같은 email을 바라보게 하려면 useFormContext가 필요했다.</p>
<p>FindUserInfoProcessContents.tsx에서 CommonForm으로 감싸주고 여기 컴포넌트 안에서 useFormContext 불러와서 setValue하니까 자꾸 setValue가 null이라고 ts에러가 뜨는거야.
에러 : Uncaught TypeError: Cannot destructure property &#39;setValue&#39; of &#39;useFormContext(...)&#39; as it is null.</p>
</li>
<li><p>원인 : useForm()을 호출하는 컴포넌트가 부모에 있어야지 FindUserInfoProcessContents.tsx
여기서 context를 쓸수 있는거였어.
부모에서 provider가 감싸주지 않는거나 마찬가지였던거야.</p>
</li>
<li><p>해결 : CommonForm을 상위 컴포넌트인 FindUserInfoPage.tsx
여기서 감싸주니까 해결됐음!</p>
</li>
</ul>
<h3 id="2️⃣-문제--해결">2️⃣ 문제 &amp; 해결</h3>
<ul>
<li><p>🛠️ 문제 : 이메일 형식이 맞지 않는 경우 toastify처럼 말풍선으로 에러 메시지가 뜸 -&gt; 에러메시지 보여주는 p태그 작성해놓은 곳에서 뜨게 만들고싶다.</p>
</li>
<li><p>해결 
<code>&lt;form&gt;</code>에 noValidate 추가</p>
<form onSubmit={handleSubmit(onSubmit)} noValidate>

<p>🔍 noValidate를 추가하면 브라우저의 기본 검증이 비활성화되고, react-hook-form에서만 오류를 처리할 수 있어!</p>
</li>
</ul>
<h3 id="3️⃣-문제--해결">3️⃣ 문제 &amp; 해결</h3>
<ul>
<li><p>문제 : 이메일 드롭다운 계속 선택하면 이메일 뒤에 @이메일 이 계속 중첩해서 쌓임</p>
</li>
<li><blockquote>
</blockquote>
<pre><code class="language-tsx"> const handleDomainSelect = (domain: string) =&gt; {
   setSelectedDomain(domain);
   setIsDropdownOpen(false);

   if (domain === &quot;직접 입력&quot;) {
     setEmail(email); // 직접 입력 선택 시 기존 이메일 유지
   } else {
     // setEmail(email ? `${email}@${domain}` : &quot;&quot;); 
     // 선택한 도메인과 결합하여 이메일 설정
     // 기존 email에서 &#39;@&#39;가 포함된 경우 앞부분만 추출
     const [localPart] = email.split(&quot;@&quot;);
     setEmail(localPart ? `${localPart}@${domain}` : &quot;&quot;);
   }
 };</code></pre>
</li>
</ul>
<p>-&gt; 이메일 형식이 계속 올바르지 않대
-&gt; 제출 함수 문제인지, novalidate때문인지…
gpt는 useForm({mode:onChange})로 바꾸고
LabelInputSet에서도 </p>
<pre><code class="language-tsx">const inputValue = watch(id); // 입력값 변경 감지</code></pre>
<p>써서 </p>
<pre><code class="language-tsx">{errors[id] &amp;&amp; inputValue &amp;&amp; ( 
  &lt;p className={styles.errorMessage}&gt;{errors[id]?.message}&lt;/p&gt; )}</code></pre>
<p>이렇게 리팩토링할것을 추천해줌
그래서 계속 이것저것 만져봤는데
계속 이메일 형식이 올바르지 않대ㅜㅜ</p>
<p>-&gt; 해결
근데 novalidate 없애고 보니까
내가 계속 이메일주소를 한글로 쓰고있었는데 그게 문제였음….
영어로 쓰도록 정규식이 되어있는거였음….</p>
<p>아직 이메일 형식 유효성 검증해주는 백엔드 api가 안나와서 LabelInputSet에서 pattern으로 임시 검증을 해주고 있었다.
gpt로 이메일 형식 검증 정규식을 가져와서 활용하고 있었는데, 정규식에서 이메일 형식을 어떤 형식으로 걸러내고 있는지에 대한 이해 없이 가져와서 생긴 문제였다...</p>
<hr>
<h1 id="👩🏻💻-emaildropdown-2차-리팩토링---로직-변경">👩🏻‍💻 EmailDropdown 2차 리팩토링 - 로직 변경</h1>
<ul>
<li><p>react hook form에서는 드롭다운과 input을 연결할수있도록
‘Controller’를 지원한다!</p>
<pre><code class="language-tsx">{ Controller ) from react-hook-form
const { control } = useFormContext()</code></pre>
</li>
<li><p>select, option 태그로 리팩토링해보려고 했으나,
css적용이 원활하지 않아서 div 드롭다운으로 그대로 적용</p>
</li>
<li><p>Controller의 render 안에서 드롭다운을 보여주고 있는데,
드롭다운 선택하면 onClick 안에서 field.onChange(domain)으로 해서 
  •    selectedDomain과 동기화하고, field.onChange를 통해 선택된 값을 React Hook Form과 연동하는 방식으로 충분히 구현이 가능합니다.
현재 코드에서는 field.onChange(domain)이 이미 구현되어 있으므로 올바르게 작동할 것입니다. 🐾
(selectedDomain)을 field.value와 동기화해야 합니다. 현재는 field.onChange(domain)을 호출하여 값을 업데이트하고 있으므로, 이 부분은 이미 처리되었습니다.</p>
</li>
<li><p>이메일 형식 검증을 input값으로만 하고 있음</p>
</li>
<li><blockquote>
<p>‘직접 입력’ 선택시에는 이게 맞는데,
드롭다운 항목에서 다른 이메일 형식을 선택한 경우에는 ‘@도메인’ 합친 이메일을 보내서 검증하도록 변경 필요</p>
</blockquote>
</li>
</ul>
<blockquote>
<p>드롭다운 진짜 어렵네…ㅠㅠ</p>
</blockquote>
<ul>
<li><p>드롭다운 선택시
field.onChange(field.value)</p>
</li>
<li><p>드롭다운 기본으로 보여지는 부분 {field.value || “도메인 선택”}</p>
</li>
</ul>
<p>(문제)</p>
<p>❌1. 1차 고민</p>
<ul>
<li>전송 클릭시 setValue로 둘이 합치니까
input값에 @뒷 주소까지 같이 들어와있음</li>
</ul>
<p>-&gt; input defaultValue를 localPart로 지정해두면 되지 않을까?!
(안됨)</p>
<ul>
<li><p>전송 클릭할때마다 계속 @뒷주소가 계속 붙음</p>
</li>
<li><p>이메일주소 써놓고 드롭다운 클릭해도 이메일 형식 바로 검증하지 않음</p>
</li>
</ul>
<p>❌
-&gt; 이메일 형식 검증 주석해버림</p>
<ul>
<li>도메인 선택에 두고 이메일 입력하고 전송해도 전송됨
이메일@
이렇게 전송됨</li>
</ul>
<p>☑️
그래, 딱 필요한것만 해놓자 이메일 형식 검증 이런거 하지말자
딱 지금 해야되는게</p>
<p>이메일 보내는거, 이메일 받는거지</p>
<ol>
<li>이메일 보내는거 - 아이디/비밀번호 찾기(이메일로 찾기), 회원가입</li>
</ol>
<ul>
<li><p>기본!! : 입력한 이메일 주소 + 드롭다운 합쳐서 보내기만 하면 돼</p>
</li>
<li><p>드롭다운 클릭했을때 계속 연달아 붙는거 방지해야해.
(드롭다운 클릭시 value에 @뒷주소 있으면 이번에 선택한 다음 도메인으로 바꿔줘야해)</p>
</li>
<li><p>input에 빈값’ 또는 도메인 선택’에 두고 전송하면 전송 안되게 만들어야돼
(빈값으로 전송하면 undefined@undefiend 되고있음)
(지금은 @ 하나만 붙고있음)</p>
</li>
</ul>
<p>✅- ‘전송’클릭시 드롭다운 항목을 붙이지 말고, 드롭다운 선택시 바로 그냥 setValue해버리자</p>
<ol start="2">
<li>이메일 받는거 - 내 정보 수정</li>
</ol>
<ul>
<li><p>기본!! : 유저정보중 이메일을 가져온다.</p>
</li>
<li><p>@뒷주소가 드롭다운에 있을때 : 드롭다운의 해당 도메인을 선택시켜놓는다, 앞주소는 풀주소 넣어놓기</p>
</li>
<li><p>@뒷주소가 드롭다운에 없을때 : 드롭다운을 ‘직접 입력’으로 선택시켜놓는다,
앞주소는 풀주소</p>
</li>
<li><p>수정 후 제출할때 : 수정된 내용이 있으면 업데이트,
없으면 그대로 둔다.</p>
</li>
</ul>
<blockquote>
<p>와 오늘 하루종일 이거만하네</p>
</blockquote>
<hr>
<h1 id="💗-드디어-이메일-드롭다운-이슈-해결">💗 드디어 이메일 드롭다운 이슈 해결!!</h1>
<p>( 이메일 input에 보여지는 값이랑 useForm에서 관리하는 value랑 달라야하는데… 드롭다운 클릭하면 setValue로 input에 입력했던 값을 아예 바꿔버리니까 input에는 합쳐진 이메일이 보여지고 있었다. )</p>
<p>-&gt; 로직 계획</p>
<p>useForm에
input에 입력한 값을 보내는게 아니라
input에 입력한 value값에 특정 텍스트를 추가한 새로운 newInputValue를 useState로 정해놓고 이 useState값을 useForm에 보낼수 있어?</p>
<p>( 0 ) 구현 예제</p>
<pre><code class="language-tsx">import React, { useState } from &quot;react&quot;;
import { useForm } from &quot;react-hook-form&quot;;

interface FormValues {
  inputValue: string;
}

const ExampleForm: React.FC = () =&gt; {
  const { register, handleSubmit } = useForm&lt;FormValues&gt;();
  const [newInputValue, setNewInputValue] = useState(&quot;&quot;);

  const onSubmit = (data: FormValues) =&gt; {
    const modifiedData = {
      ...data,
      inputValue: newInputValue, // useState로 관리하는 값 전달
    };
    console.log(&quot;제출된 데이터:&quot;, modifiedData);
  };

  const handleInputChange = (event: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const value = event.target.value;
    setNewInputValue(`${value} - 추가 텍스트`); // 입력값에 특정 텍스트 추가
  };

  return (
    &lt;form onSubmit={handleSubmit(onSubmit)}&gt;
      &lt;input
        {...register(&quot;inputValue&quot;)} // useForm으로 등록
        onChange={handleInputChange} // 값 변경 시 useState 업데이트
      /&gt;
      &lt;button type=&quot;submit&quot;&gt;제출&lt;/button&gt;
    &lt;/form&gt;
  );
};

export default ExampleForm;</code></pre>
<h3 id="해결-시도-1">해결 시도 1</h3>
<p>이메일주소 input value 그대로 두고,</p>
<pre><code class="language-tsx">const [ mergedEmail, setMergedEmail ] = useState(“”)</code></pre>
<p>이렇게 지정해둔 mergedEmail을 
onSubmit할때 보내는 방법은 어때?!</p>
 <form onSubmit={handleSubmit(onSubmit)}>

<p>-&gt; 공통컴포넌트 안쓰면서,
input에 onChange 함수 보내주기</p>
<p>or</p>
<h3 id="해결-시도-2">해결 시도 2</h3>
<p>드롭다운 클릭시</p>
<ol>
<li>getValues(“email”)</li>
<li>setMergedEmail( 이메일 + 클릭한 도메인 )</li>
</ol>
<p>이 다음 onSubmit 함수에서</p>
<pre><code class="language-tsx">const modifiedData = { 
...data, inputValue: newInputValue, // useState로 관리하는 값 전달 }; 
console.log(&quot;제출된 데이터:&quot;, modifiedData);
}</code></pre>
<p>이렇게 처리하는거지. 이 modifiedData를 서버로 전달하면 되는거지!!!</p>
<h3 id="해결-시도-3">해결 시도 3</h3>
<p>드롭다운 클릭하는건 얼마든지 계속 클릭해도 되는데,
전송 눌렀을때는 최종 드롭다운항목과 이메일 input value를 mergedEmail로 넣어서 합친 다음,
modifiedData로 만들어서 그걸 전송하는거야!</p>
<pre><code class="language-tsx">// FindUserInfoPage.tsx

  const methods = useForm&lt;FindUserMethodsType&gt;();
  const { watch } = methods;

  const emailInputValue = watch(&quot;email&quot;);
  const emailDomain = watch(&quot;emailDomain&quot;);

  // 인증번호 요청 (임시)
  const handleRequestVerificationCode = (data: any) =&gt; {
    if (!emailDomain) {
      // FIXME - 백엔드 api 참고하여 이메일 형식 에러 처리
      alert(&quot;이메일을 입력해주세요&quot;);
      return;
    }
    if (emailDomain !== &quot;직접 입력&quot;) {
      const modifiedData = {
        ...data,
        email: `${emailInputValue}@${emailDomain}`,
      };
      console.log(&quot;인증번호 요청 1:&quot;, modifiedData);
      setIsCodeSent(true);
      return;
    }
    console.log(&quot;인증번호 요청 2:&quot;, data);
    setIsCodeSent(true);
  };

  // 인증번호 검증 (임시)
  const handleVerifyCode = (formData: any) =&gt; {
    // 이름, 전화번호, 인증번호를 한번에 제출하는 로직
    // -&gt; FIXME : 백엔드 api 참고 후 인증번호만 제출할지 확인 필요
    console.log(&quot;인증번호 검증 및 아이디 찾기:&quot;, formData);

    if (`/userInfo/${url}` === ROUTE_PATHS.FIND_USER_ID) {
      navigate(ROUTE_PATHS.FIND_USER_ID_RESULT);
    } else {
      navigate(ROUTE_PATHS.CHANGE_PASSWORD);
    }
  };

  // form submit 함수 (임시)
  const handleSubmitTest = (formData: any) =&gt; {
    const cleanedFormData = { ...formData };

    if (selectedTab === &quot;email&quot;) {
      delete cleanedFormData.phone; // 이메일 선택 시 phone 필드 제거
    } else if (selectedTab === &quot;phone&quot;) {
      delete cleanedFormData.email; // 휴대폰 선택 시 email 필드 제거
    }

    if (!isCodeSent) {
      handleRequestVerificationCode(cleanedFormData); // 인증번호 요청
    } else {
      handleVerifyCode(cleanedFormData); // 인증번호 검증
    }
  };</code></pre>
<ul>
<li>인증번호 요청시
modifiedData에서 email을 input값 + 드롭다운 항목 도메인 으로 email항목을 바꾼 후</li>
<li><blockquote>
<p>이 data를 보내도록 수정</p>
</blockquote>
</li>
</ul>
<p>드롭다운 항목 클릭할때마다 input값에 도메인 붙게 하면 클릭때마다 계속 주소에 도메인 붙게됨
-&gt; 드롭다운 클릭은 계속 할수 있어도, ‘인증번호 전송’눌렀을때만 마지막 드롭다운 항목이 이메일 주소에 붙어서 최종 전송되도록 로직을 만들면 됐다!</p>
<p>form태그의 handleSubmit 함수에서는
-&gt; ‘인증번호 전송’ 시 함수 실행,
‘완료’시 함수 실행</p>
<blockquote>
<p><strong>📝 최종 정리</strong>
<br/>
이메일 드롭다운에서 계속 해결하지 못하고 있던 부분들 중 가장 어려웠던 부분을 정리해보자.
이메일 input에 값을 입력하고 드롭다운 중 &#39;직접 입력&#39;이 아닌 항목을 선택하고 제출버튼을 누르면 백엔드에 보내는 값은 &#39;이메일 input값&#39; + &#39;이메일 도메인 드롭다운 항목&#39;이 합쳐진 텍스트를 보내되, input창에 보여지는 텍스트는 여전히 이메일 input값만 보이도록 하고 싶었다.
    <br/>
그런데 제출 시 setValue로 이메일+도메인 값을 바꿔버리니까 input텍스트창에 합쳐진 이메일이 그대로 보여졌다. 내가 원한건 그게 아니었는데...
그렇다고 react-hook-form을 사용하는 input 옵션에 defaultValue를 사용한다해도 어쨌든 value 자체가 바뀌는 순간 바뀐 텍스트가 보여버리는걸..
   <br/>
input에 보여지는 텍스트랑 value랑 다르게 만드는 방법은 없으니까 해결 방법이 없다고 생각했다.
 <br/>
사실 form태그의 handleSubmit 함수 원리를 제대로 파악하지 못해서 생긴  문제였던 것 같다. handleSubmit안에 들어가는 함수에 자동으로 인자로 들어오는 data는 백엔드로 보낼때 수정이 아예 불가하다고 착각했다.
   <br/>
<strong>[ 해결 방법 ]</strong></p>
</blockquote>
<ol>
<li>mergedEmail을 변수로 놓고, 제출 버튼 클릭 시 이메일 + 도메인으로 합쳐서 mergedEmail에 set해준다.</li>
<li>handleSubmit으로 보내는 함수 안에서의 data를 바꿔준다.
<code>const modifiedData = {...data, email: mergedEmail}</code> 이런식으로.</li>
<li>화면 렌더링시 가져온 이메일을 useEffect에서 @기준으로 분리하고, 앞의 값만 setValue로 input 텍스트로 넣어버리기!!</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ 인턴 4주차 ] 전체 input을 react-hook-form로 리팩토링]]></title>
            <link>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-4%EC%A3%BC%EC%B0%A8-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%B0%BE%EA%B8%B0-react-hook-form-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%93%9C%EB%A1%AD%EB%8B%A4%EC%9A%B4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-4%EC%A3%BC%EC%B0%A8-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%B0%BE%EA%B8%B0-react-hook-form-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%93%9C%EB%A1%AD%EB%8B%A4%EC%9A%B4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</guid>
            <pubDate>Sun, 19 Jan 2025 07:13:43 GMT</pubDate>
            <description><![CDATA[<h1 id="✨-react-hook-form-리팩토링">✨ react-hook-form 리팩토링</h1>
<h2 id="👩🏻💻-비밀번호-찾기-페이지-리팩토링">👩🏻‍💻 &#39;비밀번호 찾기&#39; 페이지 리팩토링</h2>
<p>자꾸 input에서 autocomplete 경고 메시지가 떴다.
autocomplete 은 자동완성이었다..!
반드시 있어야 하는 것은 아니지만, 최적의 UX와 보안을 위해 추가하는 것이 권장됨. 특히, 다음 경우에는 autocomplete 속성을 명확하게 지정하는 것이 좋아:</p>
<ul>
<li><p>필수적인 경우:
  •    비밀번호 필드 (autocomplete=&quot;current-password&quot; 또는 autocomplete=&quot;new-password&quot;)
  •    로그인 ID, 이메일 필드 (autocomplete=&quot;username&quot;, autocomplete=&quot;email&quot;)</p>
</li>
<li><p>불필요한 경우:
  •    보안이 중요한 필드에서 자동완성을 막고 싶을 때 (autocomplete=&quot;off&quot;)
  •    일반 텍스트 입력 필드에서 자동완성이 필요하지 않을 때</p>
</li>
</ul>
<ul>
<li>autocomplete 이름은 id와 같아야하나? 뭘 지정해줘야하는거지?</li>
<li><blockquote>
<p>브라우저는 autocomplete이름 자체를 가지고 자동완성할 내용들을 분류해서 저장함.
브라우저는 autocomplete 값을 기준으로 저장하고 구분하지, name 값이 같다고 해서 하나로 묶지는 않아! 🚀
자동완성을 정확하게 활용하려면 autocomplete 값을 명확하게 지정하는 것이 중요함! 🚀</p>
</blockquote>
</li>
</ul>
<p>✅ 브라우저는 기본적으로 name 값을 기준으로 자동완성 데이터를 분류하지만,
✅ 자동완성을 적용할 때는 autocomplete 속성이 있으면 이를 우선 적용해.</p>
<ul>
<li>그럼 id랑 name은 꼭 있어야하나?
id는 선택사항
CSS/JS, label 태그와 연결할 때 id를 사용하면 UX가 향상됨.</li>
</ul>
<p>name은 꼭 있어야함
폼 제출 시 (<code>&lt;form&gt;</code> 사용 시): name이 있어야 서버로 데이터를 전송할 수 있음.
브라우저 자동완성 기능: name이 있어야 브라우저가 입력 값을 저장하고 자동완성을 제공할 수 있음.
백엔드와 연동 시: API 요청에서 name 값이 필드 이름으로 사용됨.</p>
<ul>
<li>input에 name넣었더니 에러 : &#39;name&#39;이(가) 두 번 이상 지정되어 이 사용량을 덮어씁니다.ts(2783)</li>
</ul>
<p>-&gt; 
<code>&lt;input&gt;</code> 요소에서 name 속성을 JSX에서 직접 지정(name={id})하면서, 동시에 register(id, ...)을 사용하고 있습니다. react-hook-form의 register 함수는 내부적으로 name을 자동으로 할당하므로, JSX에서 name을 별도로 지정하면 중복 선언 오류가 발생합니다.
해결 방법:
name={id}를 제거하면 됩니다.
✅ auto complete props에 추가하기</p>
<hr>
<h2 id="👩🏻💻-내정보-수정-페이지">👩🏻‍💻 &#39;내정보 수정&#39; 페이지</h2>
<h3 id="📝-로직-정리">📝 로직 정리</h3>
<blockquote>
<p><strong>[ ☑️ 로직 요약 ]</strong>
유저 정보 샘플 데이터 넣어놓기
유저 정보 가져와서 기본값 보여주기
<br/>
<strong>[ ☑️ 로직 디테일 ]</strong></p>
</blockquote>
<ol>
<li>로그인할때 유저정보도 백엔드에서 같이 가져오기</li>
<li>zustand에 저장해두기</li>
<li>내정보 조회, 내정보 수정 - zustand에 있는 정보를 가져오기</li>
<li>수정시 - zustand내용 수정, 백엔드 유저정보 POST(or UPDATE)로 수정
(zustand내용 수정하는 set하는 로직 안에 유저정보 POST하는 api로직도 넣어놓는것도 방법이겠군)</li>
</ol>
<p>-&gt; 백엔드 없이 임시 로직 구현해두려면
일단 샘플 유저데이터를 zustand에만 넣어놓으면 되겠다</p>
<p>✅ 내 정보 수정 페이지에 접근하면 유저의 기존 정보를 input에 default value로 보여주고싶었다.</p>
<p>그런데 react hook form의 default Value를 useForm에서 가져온다해도,
zustand에 있는 값을 최초로 가져온 default value가 캐시에 저장되듯 ‘최초 렌더링 시에만 반영’된다.</p>
<p>그래서 zustand에 있는 유저정보를 변경하더라도 zustand에 있는 업데이트된 값을 default value로 보여주는게 아니라
최초로 가져왔던 값을 그대로 보여준다.</p>
<p>그래서 useEffect를 사용하여 reset() 하는게 필요하다!</p>
<pre><code class="language-tsx">// Zustand에서 가져온 값으로 초기화 (내정보 수정 페이지 처음 진입 시 한 번만 실행) 
  useEffect(() =&gt; { 
  reset({ name, email, phone, companyName, }); 
  }, [name, email, phone, companyName, reset]);</code></pre>
<p>( useFormContext에서 가져오는 정확한 메서드 이름은 getValues 이다. )</p>
<p>getValues 사용 방법은</p>
<pre><code class="language-tsx">  &lt;input defaultValue={getValues(&quot;name&quot;)} /&gt;</code></pre>
<ul>
<li>react hook form의 watch는 input의 name을 기준으로 가져와 조회하는데, register가 name을 자동으로 관리하기 때문에 input에서 직접 name을 관리할 필요 없음!
(register의 첫번째 인자가 name이 된다.)</li>
</ul>
<h3 id="📝-내정보-수정-페이지-미해결-부분">📝 내정보 수정 페이지 미해결 부분</h3>
<p>☑️ zustand에서 가져온 데이터에서 그대로 input에서 수정필요
( input에 보여주고있는 기존 유저데이터를 수정하면 watch 에 감지가 안됨, submit버튼 클릭 후에야 newName이 감지되기 시작함 )</p>
<p>-&gt; ✨ 해결</p>
<ul>
<li>기존 : <pre><code class="language-tsx">const { name, email, phone, companyName, isDocApproved }: UserInfo =
  useUserInfoStore();
</code></pre>
</li>
</ul>
<p><LabelInputSet
            labelText="이메일 주소"
            id="email"
            width="292px"
            defaultValue={email}
          /></p>
<pre><code>이렇게 defaultValue에 zustand에서 가져온 값을 그대로 넣어주고있었음.
-&gt; 현재 watch(&quot;name&quot;)이 기본적으로 zustand에서 받아온 값이 아니라 react-hook-form 내부 상태만 감시하고 있어, 처음에는 값이 비어 있을 가능성이 큼.
-&gt; defaultValue 대신 setValue를 사용하여 초기값을 강제 설정.

- 해결 :
```tsx
  const { getValues, setValue, reset, watch } = useFormContext();
  const newName = watch(&quot;name&quot;);
  console.log(&quot;newName&quot;, newName);

  // 초기값 강제 설정
  useEffect(() =&gt; {
    setValue(&quot;name&quot;, name);
    setValue(&quot;email&quot;, email);
    setValue(&quot;phone&quot;, phone);
    setValue(&quot;companyName&quot;, companyName);
  }, [name, email, phone, companyName, setValue]);

  &lt;LabelInputSet
          labelText=&quot;이름&quot;
          id=&quot;name&quot;
          width=&quot;100%&quot;
          defaultValue={getValues(&quot;name&quot;)}
// defaultValue={name} 도 상관없음 
//(초기값 보여주는것 뿐이라서 zustand값 그대로 보여줘도 됨)
        /&gt;</code></pre><p>-&gt; input에 기존 유저정보를 기본값으로도 보여주고, 
해당 텍스트를 수정하면 watch로 바로 console창에 감지도 된다.</p>
<p>☑️ 제출시 수정된 내용만 zustand 수정</p>
<ul>
<li>문제 : 수정하려면 handleSubmit 함수에서 getValues, setValue가 필요했음. (useForm의 method)</li>
<li><blockquote>
<p>CommonForm 안에서 useForm을 선언해서 methods를 FormProvider에 내려주고있기 때문에</p>
</blockquote>
</li>
</ul>
<p>EditUserInfo.tsx같이 CommonForm을 사용하는 사용처에서는 getValues, setValue를 사용하지 못해 난감했다.</p>
<p>handleSubmit에는 methods가 필요한데, handleSubmit 함수가 있는 컴포넌트 안에서 CommonForm을 사용하고 있으니</p>
<pre><code class="language-tsx">// AccountPage.tsx

const hondleSubmit = ()=&gt;{
// methods 필요
}

&lt;CommonForm&gt;
&lt;EditUserInfo/&gt;
&lt;/CommonForm&gt;</code></pre>
<p>CommonForm 안에 Provider, methods가 다 들어있어서
AccountPage 안에서는 getValues, setValue같은 methods를 쓸수가 없었다!!! 
그럼 handleSubmit함수는 어떻게 작성하라고… ㅜㅜ</p>
<ul>
<li>✨ 해결 : methods를 CommonForm 사용처에서 직접 선언
const methods = useForm()
그리고 methods를 CommonForm props로 전달해버리기!!</li>
<li><blockquote>
<p>CommonForm 안에 있는 EditUserInfoContent.tsx 안에서도 methods, useFormContext 등 사용 가능하다!!!</p>
</blockquote>
</li>
</ul>
<h3 id="📝-commonform-구조-정리">📝 CommonForm 구조 정리</h3>
<p>⭐️ 구조 정리</p>
<pre><code class="language-tsx">// AccountPage.tsx
&lt;EditUserInfo/&gt;

EditUserInfo.tsx
const methods = useForm()
&lt;CommonForm&gt;
&lt;EditUserInfoContent/&gt;
&lt;/CommonForm&gt;</code></pre>
<p>-&gt;</p>
<p>EditUserInfoContent.tsx
useFormContext() 사용 가능</p>
<hr>
<h2 id="👩🏻💻✨-회원가입-페이지">👩🏻‍💻✨ &#39;회원가입&#39; 페이지</h2>
<h3 id="📝-회원가입-페이지-구성">📝 회원가입 페이지 구성</h3>
<ul>
<li><p>회원가입 페이지는 SignupPage 컴포넌트를 기준으로 step1~4 컴포넌트가 동적 라우팅으로 연결되어 있다.</p>
<ul>
<li>step1 : 이름, 이메일</li>
<li>step2 : 아이디, 비밀번호, 비밀번호 확인</li>
<li>step3 : 전화번호</li>
<li>step4 : 회사명, ( 사업자등록증 pdf )</li>
</ul>
</li>
<li><p>step1 ~ 4 모두 url 다름
하지만 입력된 정보들을 한번에 모아서 백엔드 회원가입 api에 제출할것.</p>
</li>
<li><p>방법 : </p>
<pre><code class="language-tsx">const methods = useForm()
</code></pre>
</li>
</ul>
<CommonForm methods={methods}>
<Step1>
<Step2>
…
</CommonForm>
```

<h3 id="💬-질문거리">💬 질문거리</h3>
<ul>
<li>url이 다 다른데도 useForm을 부모에서 관리하면 한데 모을수가 있나?
(다음 페이지로 이동하면 새로고침될텐데 그래도 이전 페이지에서 입력한 input값을 useForm이 들고있는게 맞나?)</li>
</ul>
<p>-&gt; useForm은 컴포넌트 언마운트, 리렌더링시 초기화됨
step1~4는 다른 url이기때문에 페이지 이동하면 CommonForm은 언마운트됨.
-&gt; useForm에 입력된 이전 값은 초기화됨.
-&gt; zustand로 관리 필요 (zustand값은 새로고침하지 않는 이상 리렌더링, 언마운트되어도 상태가 유지되기 때문!)
(새로고침해도 유지하고싶으면 persist와 로컬스토리지를 사용할수있다.)</p>
<ul>
<li><p>api연결은 나중에 하고, 회원가입 데이터들을 한번에 제출하는게 정상 동작하는지만 handleSubmit 실행시 콘솔찍어서 제출데이터를 확인하고싶어.</p>
</li>
<li><p>추후 api연결시 handleSubmit함수에서 회원가입api에 제출하는 api 추가만 하면 되는게 맞는지?</p>
</li>
</ul>
<p>=&gt;</p>
<h3 id="-최종-로직-계획">‼ 최종 로직 계획</h3>
<ol>
<li>zustand에 유저 정보 저장 (stepData, updateStepData)</li>
<li>각 step에서 zustand에 입력값 저장, 다음스텝으로 페이지이동 처리</li>
<li>최종페이지 : zustand에서 stepData가져오기, onSubmit 함수에서 회원가입 api 전달 
(지금은 일단 콘솔만)</li>
</ol>
<p>-&gt; CommonForm을 회원가입페이지에서 감싸는데
최종제출 로직 함수는 step4에서 실행되어야함.</p>
<p>-&gt; CommonForm에서 최종 실행할 함수 작성해서 onSubmit안에 넣어놓기.
step4에서는 useFormContext에서 handleSubmit을 가져와서
최종제출 클릭시 버튼에 handleSubmit을 onClick으로 추가
(원래는 handleSubmit은 콜백함수를 인자로 받는게 필수이지만,
이미 그 처리는 CommonForm에서 하고있기 때문에</p>
<p>step4에서는 그냥 onClick={handleSubmit}만 해도 괜찮음</p>
<p>근데 다른 로직도 클릭시 추가하고 싶다면 그때는
  onClick={handleSubmit(() =&gt; openModal(MODAL_IDS.SIGNUP_COMPLETE))}
이런식으로 handleSubmit안에 콜백함수로 넣어주면 됨</p>
<ul>
<li>각 step에서 useEffect로 setValue해주는게 필요한 이유</li>
<li><blockquote>
<p>이전 페이지로 이동시 이전 step에서 입력했던 데이터가 input에 그대로 들어있어야하기 때문!</p>
</blockquote>
</li>
</ul>
<p>회원가입을 이렇게 step별로 나눠서 하는걸
‘멀티 스텝 폼 Multi-Step Form’ 방식이라고 하는구나 신기하당</p>
<p>반대는 단일 폼 방식(One-Page Form)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ 인턴 4주차 ] import한 zustand store 타입 지정하는 방법, 인증/인가, pdf파일 업로드, progress bar 구현 예습]]></title>
            <link>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-4%EC%A3%BC%EC%B0%A8-import%ED%95%9C-zustand-store-%ED%83%80%EC%9E%85-%EC%A7%80%EC%A0%95%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-pdf%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-progress-bar-%EA%B5%AC%ED%98%84-%EC%98%88%EC%8A%B5</link>
            <guid>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-4%EC%A3%BC%EC%B0%A8-import%ED%95%9C-zustand-store-%ED%83%80%EC%9E%85-%EC%A7%80%EC%A0%95%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-pdf%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-progress-bar-%EA%B5%AC%ED%98%84-%EC%98%88%EC%8A%B5</guid>
            <pubDate>Sun, 19 Jan 2025 06:49:26 GMT</pubDate>
            <description><![CDATA[<h1 id="✨-import한-zustand-store-타입-지정하기">✨ import한 zustand store 타입 지정하기</h1>
<p>1) useModalStore() 의 타입을 지정할때는
❌ useModalStore<CloseModalType>()
zustand의 create 함수를 사용하여 만든 상태 관리 함수이므로 제네릭을 직접 전달하는 방식이 지원되지 않습니다.
Zustand의 create() 함수는 이미 상태 관리 스토어의 타입을 정의하는 방식으로 사용되기 때문에, 추가적인 제네릭 인자를 받을 수 없습니다.</p>
<p>✅ const { closeModal }: CloseModalType = useModalStore();
useModalStore()가 ModalStore 타입을 기반으로 생성되었고, 그 안에서 필요한 속성(closeModal)만 구조 분해 할당 후, 타입을 별도로 지정했기 때문</p>
<hr>
<h1 id="✨-구현할-기능-예습">✨ 구현할 기능 예습</h1>
<h2 id="1-at-rt-access-token-refresh-token">1) AT, RT (access token, refresh token)</h2>
<ul>
<li>RT는 일반적으로 httpOnly 쿠키에 저장 (보안 이슈)</li>
<li>주로 클라이언트가 RT를 직접 다루지 않음</li>
<li>흐름 : AT 유효기간이 만료되면 axios interceptors response.use 사용해서 AT 만료되어 401에러 떴을때 RT에게 자동으로 AT새로 발급해달라고 요청보내기!</li>
<li>RT를 이용해 새로 AT를 발급받는것도 서버를 변경? 하는거랑 같은 맥락이라서 POST 메서드를 쓴다.</li>
</ul>
<h2 id="2-pdf파일-전송-방식">2) pdf파일 전송 방식</h2>
<ul>
<li>formData를 사용해서 업로드</li>
<li>백엔드에서는 pdf파일 자체는 클라우드나 서버 디스크에 저장, 파일이 저장된 경로를 DB에 또 따로 저장</li>
<li>프론트에서는 DB에 저장된 경로를 가져다가 사용하게 됨</li>
</ul>
<h2 id="3-pdf파일-업로드-진행률-보여주는-progress-bar">3) pdf파일 업로드 진행률 보여주는 progress bar</h2>
<ul>
<li><p>axios에 onUploadProgress 옵션이 있음
프론트에서 백엔드로 넘어가는 진행률을 보여줌</p>
</li>
<li><blockquote>
<p>onUploadProgress가 100%이 됐더라도 백엔드에서 db에 제대로 저장됐는지는 알수 없다.</p>
</blockquote>
</li>
<li><p>백엔드에서 db에 제대로 저장됐는지, 저장되고있는 진행률 이런거는 onUploadProgress로는 알수 없다. 그건 백엔드에서 보내줘야됨
(백엔드의 추가 처리 시간이 길어지면, 유저가 &quot;왜 멈췄지?&quot; 또는 &quot;오류인가?&quot;라고 생각할 가능성이 큼.)</p>
</li>
<li><p>해결책 gpt 제시</p>
</li>
<li><blockquote>
<p>유저가 100%라고 보기 전에 모든 작업이 완료되도록, 네트워크 전송과 백엔드 처리를 합쳐 단일 진행률로 표시.
예:
네트워크 전송 = 전체의 70%.
백엔드 처리 = 전체의 30%.</p>
</blockquote>
<pre><code class="language-tsx">const calculateTotalProgress = (uploadProgress: number, backendProgress: number) =&gt; {
 const uploadWeight = 0.7; // 네트워크 전송 비율
 const backendWeight = 0.3; // 백엔드 처리 비율

 return Math.round(uploadProgress * uploadWeight + backendProgress * backendWeight);
};</code></pre>
</li>
<li><p>UI 동작
onUploadProgress로 네트워크 전송률(uploadProgress) 추적.
백엔드의 처리 진행률(backendProgress) API로 가져옴.
위 두 값을 합산해 단일 진행률을 계산 후 표시.</p>
</li>
</ul>
<p>-&gt; 백엔드의 진행률만 보여주는 방법도 있음</p>
<pre><code class="language-tsx">const [progress, setProgress] = useState(0); 
  const [status, setStatus] = useState(&quot;uploading&quot;); 

  // &quot;uploading&quot; | &quot;processing&quot; | &quot;done&quot; // 진행률 확인 
const checkProgress = async () =&gt; { 
  try { const response = await axios.get(&quot;/upload/status&quot;); 
  setProgress(response.data.progress); 
  setStatus(response.data.status); } 
  catch (error) { 
  console.error(&quot;진행 상태를 확인하는 중 에러 발생:&quot;, error); } }; 

  useEffect(() =&gt; { const interval = setInterval(() =&gt; {
  checkProgress(); }, 1000); 
  // 1초마다 진행 상태 확인 return () =&gt; clearInterval(interval); 
  // 컴포넌트 언마운트 시 정리 }, []);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ 인턴 4주차 ] react-hook-form에 대하여, 공통 컴포넌트 제작, 트러블슈팅]]></title>
            <link>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-4%EC%A3%BC%EC%B0%A8-react-hook-form</link>
            <guid>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-4%EC%A3%BC%EC%B0%A8-react-hook-form</guid>
            <pubDate>Tue, 14 Jan 2025 02:13:09 GMT</pubDate>
            <description><![CDATA[<h1 id="✨-react-hook-form">✨ react-hook-form</h1>
<blockquote>
</blockquote>
<p>공식문서 : <a href="https://react-hook-form.com/get-started">https://react-hook-form.com/get-started</a></p>
<hr>
<h2 id="☑️-react-hook-form의-장점">☑️ react-hook-form의 장점</h2>
<blockquote>
<p>요약 : useState onChange 보다 간결한 코드, 성능 최적화 용이, 유지보수 쉬움</p>
</blockquote>
<ul>
<li>useState 없이 input 요소의 ref를 활용하여 상태를 관리
리렌더링 최소화 → 성능 최적화</li>
<li>코드 간결
onChange 없이 register만으로 폼 상태 관리</li>
<li>네이티브 HTML 폼과의 자연스러운 통합
  •    <form> 태그의 onSubmit과 쉽게 연동 가능
  •    브라우저의 기본 폼 기능 (required, pattern 등) 활용 가능</li>
<li>기본적인 클라이언트 유효성 검사 내장
  •    required, minLength, maxLength, pattern 등의 유효성 검사를 기본 제공
  •    Zod, Yup과 같은 스키마 기반 검증 라이브러리와 쉽게 결합 가능</li>
<li>폼 데이터를 JSON 객체로 쉽게 변환 가능
  •    handleSubmit을 사용하면 바로 data 객체로 변환 가능 → 추가 가공 필요 없음</li>
</ul>
<p><code>yarn add react-hook-form</code>
react-hook-form은 TypeScript를 기본적으로 지원하므로, yarn add react-hook-form만 설치하면 바로 사용할 수 있음</p>
<hr>
<h2 id="👩🏻💻-공통-컴포넌트부터-바꿔보자">👩🏻‍💻 공통 컴포넌트부터 바꿔보자</h2>
<h3 id="1-labelinputsettsx-리팩토링">1. LabelInputSet.tsx 리팩토링</h3>
<ul>
<li>공통 컴포넌트는 label, input만 있고, 사용처 컴포넌트에서 form태그를 사용하고있다.
form태그와 input태그가 한 컴포넌트에 있지 않기에 공통컴포넌트에서 useFormContext를 사용하여 부모 form과 연결해주면 된다.</li>
<li><blockquote>
<p>상위 컴포넌트에서 FormProvider {…methods} 로 감싸주는게 필요하다.</p>
</blockquote>
</li>
</ul>
<p>-&gt; provider라고해서 App.tsx에서 한번에 감싸면 안되나?
안됨…</p>
<ul>
<li><p>FormProvider는 각 form마다 별도로 추가해야 해.
즉, App.tsx에서 한 번만 선언하는 것이 아니라, 각각의 폼(LoginPage, SignupPage 등)에 개별적으로 적용해야 해.</p>
</li>
<li><p>각 폼은 서로 다른 useForm() 상태를 가짐
  •    useForm()은 reset, watch, setValue 등의 메서드를 제공하는데, 폼마다 개별적인 상태를 가져야 하기 때문에 각 폼에서 FormProvider를 따로 감싸야 해.
폼 간 데이터 충돌 방지
  •    예를 들어, 로그인 폼과 회원가입 폼이 App.tsx에서 같은 FormProvider를 공유하면, 한 폼에서 입력한 데이터가 다른 폼에서도 공유될 위험이 있음.
폼 제출(handleSubmit)이 독립적으로 동작해야 함
  •    FormProvider는 useForm()에서 생성된 methods를 전달하는 역할이므로, 각 form 컴포넌트에서 handleSubmit을 개별적으로 적용하려면 각 폼에서 useForm()을 초기화하고, 해당 FormProvider로 감싸야 한다.</p>
</li>
<li><p>FormProvider는 App.tsx에서 한 번만 선언하는 것이 아니라,
각각의 form이 있는 컴포넌트(LoginPage, SignupPage 등)에서 개별적으로 추가해야 한다.</p>
</li>
</ul>
<h3 id="2-form태그가-공통컴포넌트-밖에-있어서">2. form태그가 공통컴포넌트 밖에 있어서…</h3>
<p>매번 사용처에서</p>
<pre><code class="language-tsx">const methods = useForm();
&lt;FormProvider {...methods}&gt;
&lt;form&gt;&lt;/form&gt;
&lt;/FormProvider&gt;</code></pre>
<p>이 과정을 거쳐야 했다.</p>
<p>-&gt;
공통컴포넌트에서 FormProvider, useForm을 포함도록 공통form태그를 만들면 반복되는 불필요하고 비효율적인 코드를 간소화할 수 있다!</p>
<h3 id="3-formprovider만-안쓰면-되는거-아냐">3. FormProvider만 안쓰면 되는거 아냐…?</h3>
<p>매 컴포넌트에서 useForm()을 새로 불러와서 쓰면
그럼 form태그 안에 LabelInputSet 쓰도록 하는 로직 그대로 가져갈수 있지 않을까?!</p>
<p>-&gt; LabelInputSet에 props로
{...register(id, { required: <code>${labelText}을(를) 입력해 주세요.</code> })}
이걸 매번 보내줘야되네…</p>
<p>-&gt; 결국 결론!!
CommonForm, LabelInputSet 쓰도록 하자.
반복되는 과정 최대한 공통으로 만들어서
다른사람이 쓸때 최대한 props넘겨줄 내용이 적도록!
그리고 다른사람이 쓸때 구조를 봤을때 공통컴포넌트 쓸때 좀만 생각하면 바로 이해되도록 최대한 간단하고 직관적인 구조로!!</p>
<p>LabelInputSet 안에서 register 하도록 제작,
CommonForm 에서는 useForm, methods, form, FormProvider 등을 갖고 있도록 제작</p>
<p>참고)
CommonForm.tsx, LabelInputSet.tsx 수정사항 참고
메모 사진 참고</p>
<blockquote>
<p>💬 ㅎㅎ... 이후 사용하다보니 문제가 생겨 <code>CommonForm</code> 공통 컴포넌트를 대대적으로 리팩토링 하게 된다.
<br/>
  <strong>🛠️ 문제</strong>
  FormProvider, form, useForm에서 불러오는 methods를 전부 CommonForm 안에서 갖고 있다보니,
정작 CommonForm을 사용하는 컴포넌트에서 특정 methods가 필요한 경우 useForm, methods는 CommonForm 안에 있어서 methods를 불러올 수가 없어 사용할 수 가 없었다.
  <br/>
**  👍 해결**
FormProvider, form은 CommonForm에 그대로 유지하되, useForm을 선언 및 methods 가져오는 위치를 CommonForm 컴포넌트를 사용하는 곳에서 불러오도록 로직을 변경했다.
  이렇게 하면 CommonForm을 사용하는 컴포넌트에서도 methods 중 일부 (ex. watch, getValues, setValue 등)를 사용할 수 있게 된다!!!</p>
</blockquote>
<hr>
<h2 id="👩🏻💻-아이디-찾기-react-hook-form-리팩토링">👩🏻‍💻 &#39;아이디 찾기&#39; react hook form 리팩토링</h2>
<h3 id="🛠️-문제">🛠️ 문제</h3>
<p>인증번호를 받기 위해서는 이름, 전화번호만 먼저 form을 제출해야하는데
인증번호 입력하는 부분으로 바로 넘어가버리고,
거기서 이름, 번호, 인증번호 입력해야 콘솔에 입력값이 출력됐다.</p>
<h3 id="🧩-원인">🧩 원인</h3>
<p>‘인증번호 전송’과 ‘완료’ 버튼이 같은 버튼이라서 함수를 하나만 뒀더니 생긴 문제.</p>
<pre><code class="language-tsx">  const handleGetVerificationCode = () =&gt; {
    // &#39;인증번호 전송&#39; 버튼 클릭 시
    if (!isCodeSent) {
      setIsCodeSent((prev: boolean) =&gt; !prev);
    } else {
      // &#39;완료&#39; 버튼 클릭 시
      // -&gt; 인증실패시 로직 추가 필요
      if (`/userInfo/${url}` === ROUTE_PATHS.FIND_USER_ID) {
        navigate(ROUTE_PATHS.FIND_USER_ID_RESULT);
      } else {
        navigate(ROUTE_PATHS.CHANGE_PASSWORD);
      }
    }
  };</code></pre>
<p>함수 하나에서 인증번호 전송과 완료 두가지를 한번에 처리하고 있었다.</p>
<h3 id="👍-해결">👍 해결</h3>
<p>인증번호 요청 / 인증번호 제출 단계를 분리해서 함수를 작성한다.
submit하는 함수도 code를 받았냐 안받았냐에 따라 다른 동작을 하도록 조건문으로 분리하자.</p>
<p>인증번호 요청, 제출 함수를 2개로 분리하고,
CommonForm에 보낼 함수는 또 하나로 만들어서
그 안에서 조건에 따라 위 2개의 함수를 실행하도록 만들면 된다!</p>
<hr>
<h2 id="👩🏻💻-react-hook-form-예제">👩🏻‍💻 react-hook-form 예제</h2>
<pre><code class="language-tsx">import { SubmitHandler, useForm } from &quot;react-hook-form&quot;;

type Inputs = {
  userId: string;
  password: string;
};

const LoginPage: React.FC = () =&gt; {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
  } = useForm&lt;Inputs&gt;();
  const handleSubmitFunc: SubmitHandler&lt;Inputs&gt; = (data) =&gt; console.log(data);

  console.log(watch(&quot;userId&quot;));

   return (
     &lt;form
            className={styles.testForm}
            onSubmit={handleSubmit(handleSubmitFunc)}
          &gt;
            &lt;label&gt;아이디&lt;/label&gt;
            &lt;input
              {...register(&quot;userId&quot;, { required: &quot;이름을 입력하세요.&quot; })}
            /&gt;
            {errors.userId &amp;&amp; &lt;p&gt;{errors.userId.message}&lt;/p&gt;}
            &lt;label&gt;비밀번호&lt;/label&gt;
            &lt;input
              type=&quot;password&quot;
              {...register(&quot;password&quot;, { required: &quot;비밀번호를 입력하세요.&quot; })}
            /&gt;
            {errors.password &amp;&amp; &lt;p&gt;비밀번호를 입력하세요.&lt;/p&gt;}
            &lt;button type=&quot;submit&quot;&gt;제출&lt;/button&gt;
          &lt;/form&gt;
     // ...</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ 인턴 3주차 ] UX를 위한 고민, 빠른 개발에 대한 고찰]]></title>
            <link>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-3%EC%A3%BC%EC%B0%A8-UX%EB%A5%BC-%EC%9C%84%ED%95%9C-%EA%B3%A0%EB%AF%BC</link>
            <guid>https://velog.io/@innes_kwak/%EC%9D%B8%ED%84%B4-3%EC%A3%BC%EC%B0%A8-UX%EB%A5%BC-%EC%9C%84%ED%95%9C-%EA%B3%A0%EB%AF%BC</guid>
            <pubDate>Sun, 12 Jan 2025 08:36:25 GMT</pubDate>
            <description><![CDATA[<h1 id="✨-ux를-위한-고민">✨ UX를 위한 고민</h1>
<blockquote>
<p>현업 프로젝트를 진행하다보니 어떤 처리가 유저에게 가장 좋은 경험을 제공할 지 점점 더 디테일하게 고민하게 되었다.
누군가는 사소하다고 생각할 수 있는 고민들이지만, 유저는 생각보다 디테일한 부분에서 실망하기 마련이다. (내가 그러니까)
모든 개발자가 그렇지만, 프론트엔드 개발자야 말로 유저와 직접 만나는 역할을 하기 때문에 디테일한 부분들을 챙길 수 있어야 한다.
이런 이유로 고민하게 된 내용들을 아래 간략하게 정리해보았다.</p>
</blockquote>
<p>url 분리가 꼭 필요한가?
동적 라우팅을 사용해도 괜찮은가?
컴포넌트 분리로만 처리해도 괜찮은가?</p>
<p>url을 분리한다면 어디서부터 어디까지 분리해야하나?
어떤 페이지까지는 종속되도록 만들 것인가?</p>
<p>새로고침을 고려할 것인가?
새로고침 했을 때 초기화되어도 괜찮은가? 아니면 진행 중이던 페이지가 유지되어야 하는가?</p>
<p>뒤로가기를 지원할 것인가?</p>
<p>url이 길어져도 괜찮은가?
약어를 써도 괜찮은가? full keyword를 사용하는게 더 좋은가?</p>
<hr>
<h1 id="✨-개발-속도를-높이려면">✨ 개발 속도를 높이려면?</h1>
<p>설계를 미리 많이 하는게 중요한 것 같다.
무작정 코딩부터 먼저 시작하는 것 보다, 컴포넌트와 url path, 공통 컴포넌트로 작성할 수 있는 부분들, 컴포넌트 css 정렬 등의 설계를 깊게 고민하고 시작하는 것이 오히려 이후의 시간을 훨씬 절약해주고, 시행착오를 줄여주는 일이다.</p>
]]></description>
        </item>
    </channel>
</rss>