<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev_flee.log</title>
        <link>https://velog.io/</link>
        <description>바라는 색이 있다면 눈이 멀도록 바라볼 것. 가능한 온몸으로 부서질 것.</description>
        <lastBuildDate>Thu, 02 Apr 2026 07:16:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev_flee.log</title>
            <url>https://velog.velcdn.com/images/heeflee_1310/profile/010660f4-a032-4232-9476-f6baa661332d/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev_flee.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/heeflee_1310" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[02. 스타일링 우선순위 선정]]></title>
            <link>https://velog.io/@heeflee_1310/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94-%EC%A0%84%EB%9E%B5-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EC%84%A0%EC%A0%95</link>
            <guid>https://velog.io/@heeflee_1310/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94-%EC%A0%84%EB%9E%B5-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EC%84%A0%EC%A0%95</guid>
            <pubDate>Thu, 02 Apr 2026 07:16:49 GMT</pubDate>
            <description><![CDATA[<p><em>&#39;런타임 비용을 줄여보자!&#39;</em> 며 패기있게 시작한 스타일링 이원화 전략은 생각보다 더 다뤄야할 요소들이 깊었습니다..</p>
<blockquote>
<p>스타일링 이원화 전략 정리 글 <a href="https://velog.io/@heeflee_1310/05.-Emotion%EA%B3%BC-Tailwind-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94">( ▶이동 )</a></p>
</blockquote>
<p>먼저 <strong>스타일의 우선순위 선정</strong>이 필요했습니다.</p>
<p>이번 글은 스타일링 우선순위 선정을 주제로 아래 목차 순으로 진행해보겠습니다~</p>
<blockquote>
<h3 id="목차">목차</h3>
</blockquote>
<h4 id="1-문제-상황--▶이동-">1. 문제 상황 <a href="https://velog.io/@heeflee_1310/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94-%EC%A0%84%EB%9E%B5-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EC%84%A0%EC%A0%95#1-%EC%99%9C-%EC%8A%A4%ED%83%80%EC%9D%BC-%EC%B6%A9%EB%8F%8C%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%A0%EA%B9%8C">( ▶이동 )</a></h4>
<h4 id="2-css-cascade-layer-개념--▶이동-">2. CSS Cascade Layer 개념 <a href="https://velog.io/@heeflee_1310/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94-%EC%A0%84%EB%9E%B5-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EC%84%A0%EC%A0%95#2-css-cascade-layer%EB%9E%80">( ▶이동 )</a></h4>
<h4 id="3-tailwind-layer-개념--▶이동-">3. Tailwind layer 개념 <a href="https://velog.io/@heeflee_1310/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94-%EC%A0%84%EB%9E%B5-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EC%84%A0%EC%A0%95#3-tailwind-v4%EC%9D%98-layer-%EA%B5%AC%EC%A1%B0">( ▶이동 )</a></h4>
<h4 id="4-해결-전략--emotion-unlayered-설정--▶이동-">4. 해결 전략 : Emotion Unlayered 설정 <a href="https://velog.io/@heeflee_1310/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94-%EC%A0%84%EB%9E%B5-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EC%84%A0%EC%A0%95#4-%ED%95%B4%EA%B2%B0-%EC%A0%84%EB%9E%B5--emotion-unlayered-%EC%84%A4%EC%A0%95">( ▶이동 )</a></h4>
<h4 id="5-환경별-세팅--▶이동-">5. 환경별 세팅 <a href="https://velog.io/@heeflee_1310/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94-%EC%A0%84%EB%9E%B5-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EC%84%A0%EC%A0%95#5-%ED%99%98%EA%B2%BD%EB%B3%84-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C">( ▶이동 )</a></h4>
<h4 id="6-활용하기-위해-알아야-하는-개념들--▶이동-">6. 활용하기 위해 알아야 하는 개념들 <a href="https://velog.io/@heeflee_1310/%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94-%EC%A0%84%EB%9E%B5-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EC%84%A0%EC%A0%95#6-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94-%EA%B0%9C%EB%85%90%EB%93%A4">( ▶이동 )</a></h4>
<h1 id="0-intro">0. Intro</h1>
<p><code>Tailwind CSS</code>(유틸리티 클래스)와 <code>Emotion</code>(CSS-in-JS)를 함께 사용하면 아래와 같은 장점들을 가져갈 수 있다.</p>
<ul>
<li><code>Tailwind</code> : 빠른 레이아웃 구현, 간격, 반응형 처리</li>
<li><code>Emotion</code> : 컴포넌트 단위 스타일 캡슐화, 디자인 토큰 기반 스타일링, 상태 기반 동적 스타일링</li>
</ul>
<p>하지만, 두 라이브러리를 함께 사용할 때 <strong>CSS 우선 순위 충돌 문제</strong>가 발생할 수 있다.
발생 원인과 <strong>CSS Cascade Layer</strong>를 활용한 해결 방법을 정리해본다.</p>
<hr>
<h1 id="1-왜-스타일-충돌이-발생할까">1. 왜 스타일 충돌이 발생할까?</h1>
<h2 id="1-1-css-기본-우선-순위-규칙">1-1. CSS 기본 우선 순위 규칙</h2>
<p><strong><code>CSS</code></strong>는 아래와 같은 세 가지 기준으로 우선 순위를 정한다.</p>
<ol>
<li><strong>Specificity</strong> : 선택자 구체성(<code>#id</code> &gt; <code>.class</code> &gt; <code>tag</code>)</li>
<li><strong>Source Order</strong> : 나중에 선언된 스타일이 우선</li>
<li><strong><code>!important</code></strong> : 모든 규칙을 무시하고 적용</li>
</ol>
<h2 id="1-2-tailwind와-emotion-충돌-지점">1-2. Tailwind와 Emotion 충돌 지점</h2>
<p>먼저 각각 생성 스타일을 살펴보겠다.</p>
<p><code>Emotion</code> 생성 스타일</p>
<pre><code>.css-abc123 { padding: 16px }</code></pre><p><code>Tailwind</code> 생성 스타일</p>
<pre><code>.p-4 { padding: 1rem }</code></pre><p>두 스타일 모드 클래스 선택자(<code>.</code>로 시작) 1개이므로 <strong>Specificity</strong>가 동일하다. 
이 경우 <strong>Source Order</strong>(선언 순서)로 우선순위가 결정된다.</p>
<p>문제는 이 경우에 발생한다.
<strong>빌드 환경 A</strong> : <code>Tailwind</code>가 나중에 삽입 → <strong>Tailwind 승</strong>
<strong>빌드 환경 B</strong> : <code>Emotion</code>이 나중에 삽입 → <strong>Emotion 승</strong></p>
<p>동일한 출력물도 <code>빌드 환경</code>, <code>번들러 설정</code>, <code>청크 분할 방식</code>에 따라 CSS 삽입 순서가 달라지므로 결과를 예측할 수 없다.</p>
<blockquote>
<p><em>참고</em></p>
</blockquote>
<ul>
<li><strong>빌드 환경</strong> : <code>React</code>, <code>Next.js</code> 등</li>
<li><strong>번들러 설정</strong> : <code>Vite</code>, <code>Webpack</code>, <code>Turbopack</code>  등</li>
<li><strong>청크 분할이란?</strong> : 하나의 큰 번들을 <strong>여러 개의 작은 파일(chunk)로 나누는 것</strong></li>
</ul>
<hr>
<h1 id="2-css-cascade-layer란">2. CSS Cascade Layer란?</h1>
<h2 id="2-1-layer-개념">2-1. @layer 개념</h2>
<p><strong>CSS Cascading and Inheritance Level 5</strong> 스펙에서 도입된 <strong>우선순위 그룹화 메커니즘</strong>이다.</p>
<pre><code>@layer base, components, utilities;

@layer base {
    /* 가장 낮은 우선 순위 */
}

@layer utilities {
    /* 가장 높은 우선 순위 */
}
</code></pre><h2 id="2-2-layer-핵심-규칙">2-2. @layer 핵심 규칙</h2>
<h3 id="1-선언-순서--우선-순위">1. 선언 순서 === 우선 순위</h3>
<p><code>@layer A, B, C</code>에서 C가 우선 순위가 가장 높다. → <strong>Source Order</strong></p>
<h3 id="2-layer--specificity">2. Layer &gt; Specificity</h3>
<p>상위 layer의 <code>.class</code>가 하위 layer의 <code>#id</code>를 이김 (<code>#id</code> &gt; <code>.class</code> &gt; <code>tag</code> )</p>
<h3 id="3-unlayered--layered">3. Unlayered &gt; Layered</h3>
<p><code>@layer</code> 밖의 스타일이 모든 layer보다 우선</p>
<h3 id="2-3-시각화">2-3. 시각화</h3>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/47c2a243-13f6-40bc-8109-77d80154e2a7/image.png" alt=""></p>
<h3 id="정리핵심">정리(핵심)</h3>
<h4 id="layer를-사용하면-css-삽입-순서와-무관하게-우선순위가-고정된다">Layer를 사용하면 CSS 삽입 순서와 무관하게 우선순위가 고정된다!</h4>
<hr>
<h1 id="3-tailwind-v4의-layer-구조">3. Tailwind v4의 Layer 구조</h1>
<h2 id="3-1-tailwind-기본-layer-구조">3-1. Tailwind 기본 layer 구조</h2>
<p><code>layer</code>는 다음과 같이 4개의 layer로 구성된다.</p>
<ul>
<li><strong><code>theme</code></strong> : CSS 변수, 테마 토큰</li>
<li><strong><code>base</code></strong> : Preflight(브라우저 초기화)</li>
<li><strong><code>components</code></strong> : <code>@apply</code> 기반 컴포넌트 스타일</li>
<li><strong><code>utilities</code></strong> : 유틸리티 클래스(<code>p-4</code>, <code>mt-2</code> 등)</li>
</ul>
<h2 id="3-2-preflight란">3-2. Preflight란?</h2>
<p><code>Tailwind</code>에 내장된 <strong>브라우저 초기화 스타일</strong>이다.
modern-normalize를 기반으로 하며, <strong><code>base</code></strong> layer에 포함된다.</p>
<h3 id="주요-초기화-내용">주요 초기화 내용</h3>
<ul>
<li><strong>margin/padding 제거</strong> : 모든 요소의 기본 여백 초기화</li>
<li><strong>border 리셋</strong> : border: 0 solid;로 통일</li>
<li><strong>heading 스타일 제거</strong> : h1~h6의 font-size, font-weight 상속</li>
<li><strong>list 스타일 제거</strong> : ul, ol의 bullet/number 제거</li>
<li><strong>img block 처리</strong> : 이미지를 display:block으로 설정</li>
</ul>
<p>초기화 내용이 좀 익숙하였다.</p>
<blockquote>
<p>*<em>그렇다. <code>reset.css</code>와 중복된다. *</em></p>
</blockquote>
<h2 id="3-3-tailwind-v4-import-문법">3-3. Tailwind v4 import 문법</h2>
<p><code>Tailwind</code> <strong>v4</strong>에서는 간결한 <code>import</code> 문법을 사용한다</p>
<pre><code>@layer theme, base, components, utilities;

@import &#39;tailwindcss&#39;;
</code></pre><p>이 한 줄로 <code>theme</code>, <code>preflight</code>, <code>utilities</code>가 모두 포함된다.</p>
<p>만약 <strong>개별 <code>import</code></strong>가 필요한 경우라면 아래와 같이 진행한다.</p>
<pre><code>@layer theme, base, components, utilities

@import &#39;tailwindcss/theme.css&#39; layer(theme);
@import &#39;tailwindcss/preflight.css&#39; layer(base);
@import &#39;tailwindcss/utilities.css&#39; layer(utilities)</code></pre><hr>
<h1 id="4-해결-전략--emotion-unlayered-설정">4. 해결 전략 : Emotion Unlayered 설정</h1>
<h2 id="4-1-전략-선택-이유">4-1. 전략 선택 이유</h2>
<p><code>Emotion</code>에서 <code>@layer</code>를 직접 지원하는 방법을 검토했다.<em>(with 클로드)</em></p>
<ol>
<li>stylis 플러그인 커스텀 : Emotion 출력을 @layer로 감싸기 → 실용성 낮음. 불안정</li>
<li>CSS 후처리 : 런타임에서 style 태그 수정 → 실용성 낮음. 성능 이슈(런타임 비용 증가)</li>
<li><strong>Unlayered로 유지</strong> : Emotion을 layer 밖에 두기 → 실용성 높음. 안정적</li>
</ol>
<p><code>Emotion</code> v11이 현재 <code>@layer</code>를 네이티브로 지원하지 않는다.
커스텀 플러그인이나 후처리 방식은 복잡도가 높고, 안정성이 떨어진다.</p>
<blockquote>
<h2 id="결론">결론</h2>
<p>Emotion을 Unlayered로 두는 전략이 가장 실용적이다.</p>
</blockquote>
<h2 id="4-2-최종-우선-순위">4-2. 최종 우선 순위</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/c27493cc-927b-4a03-a8f1-a877021b69d9/image.png" alt="">
CSS 스펙 규칙상 
<strong>Unlayered 스타일은 모든 Layered 스타일보다 우선한다.</strong></p>
<h4 id="따라서-emotion-스타일이-항상-tailwind보다-높은-우선순위를-가진다">따라서 Emotion 스타일이 항상 Tailwind보다 높은 우선순위를 가진다.</h4>
<p>Tailwind로 레이아웃 → Emotion으로 컴포넌트 스타일링 순서가 자연스럽게 보장된다.</p>
<h2 id="4-3-설정-방법">4-3. 설정 방법</h2>
<h3 id="1-globalscss">1. globals.css</h3>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/ebb16797-8ef8-4971-b6b3-aceb8df7a360/image.png" alt=""></p>
<h3 id="2-resetcss-삭제">2. reset.css 삭제</h3>
<p><strong><code>Tailwind</code> Preflight</strong>가 브라우저 초기화를 담당한다.
그러므로 기존 <code>reset.css</code>는 삭제한다. 
중복 선언을 방지하고 유지보수를 단순화 할 수 있다.</p>
<h2 id="4-4-유틸리티-오버라이드가-필요한-경우">4-4. 유틸리티 오버라이드가 필요한 경우</h2>
<p>현재 전략은 <code>Emotion</code>이 <code>Tailwind</code> <code>utilities</code>보다 우선순위가 높다.
만약 특정 상황에서 <strong><code>Tailwind 유틸리티</code></strong>로 <strong><code>Emotion 스타일</code></strong>을 덮어써야 한다면 <code>!</code> 접두사를 이용한다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/aed2c44a-4de4-483b-9f98-59b8cb555a43/image.png" alt=""></p>
<blockquote>
<p><em>참고</em>
<code>!</code>접두사는 해당 유틸리티에 <code>!important</code>를 추가한다.
남용은 하지말고..꼭 필요한 경우에만 사용하자..!</p>
</blockquote>
<hr>
<h1 id="5-환경별-설정-가이드">5. 환경별 설정 가이드</h1>
<p>현재 나는 동시에 참여 중인 <code>React</code>, <code>Next.js</code>에서 모두 이원화 전략을 사용하고 있다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/f9bc14c0-ccfe-4be2-8086-890fc1194693/image.png" alt="">
맞다..그래서 2번의 세팅을 진행했다..<em>( 주여.. )</em></p>
<h2 id="5-1-react--vite">5-1. React + Vite</h2>
<h3 id="1-globalscss-1">1. globals.css</h3>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/06c751ed-8956-4942-ba82-ebfaab0953b1/image.png" alt=""></p>
<h3 id="2-maintsx">2. <code>main.tsx</code></h3>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/eb20b65b-7314-48f5-8bd5-6bf02cde9e24/image.png" alt="">
<code>globals.css</code> <code>import</code>만 하면 끝!!</p>
<h2 id="5-2-nextjsapp-router">5-2. Next.js(App Router)</h2>
<p><code>Next.js</code>에서 <code>App Router</code>에서 <code>Emotion</code>을 사용할 때는 <strong>SSR 대응</strong>을 위한 추가 설정이 필요하다.</p>
<h3 id="1-appglobalscss">1. <code>app/globals.css</code></h3>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/06c751ed-8956-4942-ba82-ebfaab0953b1/image.png" alt=""></p>
<p>React와 동일하게 globals.css를 설정한다.</p>
<h3 id="2-appemotioncacheprovidertsx">2. <code>app/EmotionCacheProvider.tsx</code></h3>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/5e1f7e29-92d8-4816-8561-4de2c0f7875b/image.png" alt=""></p>
<blockquote>
<p><em>참고</em>
Emotion Hydration 해결 <a href="https://velog.io/@heeflee_1310/04.-Next.js%EC%97%90%EC%84%9C-Emotion-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0#5-hydration-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0">( ▶이동 )</a>
Emotion HTML 삽입 <a href="https://velog.io/@heeflee_1310/04.-Next.js%EC%97%90%EC%84%9C-Emotion-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0#5-3-app-router--useserverinsertedhtml%EB%A1%9C-%EC%8A%A4%ED%83%80%EC%9D%BC-%EB%8F%99%EA%B8%B0%ED%99%94">( ▶이동 )</a></p>
</blockquote>
<h3 id="3-applayouttsx">3. <code>app/layout.tsx</code></h3>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/93570ffa-96fa-466a-bbdb-b60101da4ca5/image.png" alt=""></p>
<h2 id="5-3-공통-확인-사항">5-3. 공통 확인 사항</h2>
<ul>
<li><strong><code>@layer 선언 위치</code></strong> : CSS 파일 최상단에 위치하는 지</li>
<li><strong><code>빌드 테스트</code></strong> : dev/prod 환경에서 동일하게 동작하는지</li>
<li><strong><code>브라우저 확인</code></strong> : 개발자 도구에서 Computed 탭에서 적용된 스타일 확인</li>
</ul>
<hr>
<h1 id="6-알아야-하는-개념들">6. 알아야 하는 개념들</h1>
<h2 id="6-1-css">6-1. CSS</h2>
<h3 id="cascade-layerlayer">Cascade Layer(@layer)</h3>
<ul>
<li>CSS 우선순위를 그룹 단위로 관리하는 메커니즘</li>
<li>삽입 순서와 무관하게 선언된 순서대로 우선순위 설정</li>
<li>나중에 선언된 layer가 높은 우선순위</li>
</ul>
<h3 id="specificity-vs-source-order">Specificity vs Source Order</h3>
<ul>
<li><strong>Specificity</strong> : 선택자의 구체성(<code>#id</code> &gt; <code>.class</code> &gt; <code>tag</code>)</li>
<li><strong>Source Order</strong> : 동일 Specificity인 경우 나중에 선언된 스타일 우선</li>
<li><strong><code>@layer</code></strong> 사용 시 Layer 우선순위가 <strong>Specificity</strong>보다 먼저 적용됨</li>
</ul>
<h3 id="unlayered-vs-layered">Unlayered vs Layered</h3>
<ul>
<li><strong>Unlayered</strong> : <code>@layer</code> <strong>밖</strong>에 선언된 스타일</li>
<li><strong>Layered</strong> : <code>@layer</code> <strong>안</strong>에 선언된 스타일<blockquote>
<p>우선 순위 : <strong>Unlayered</strong> &gt; <strong>Layered</strong></p>
</blockquote>
</li>
</ul>
<h3 id="우선-순위-정리">우선 순위 정리</h3>
<p><strong><code>Unlayered</code></strong> &gt; <strong><code>Layered</code></strong> ( <strong>Layer</strong> &gt; <strong>Source Order</strong> &gt; *<em>Specificity(<code>#id</code> &gt; <code>.class</code> &gt; <code>tag</code>) ) *</em> </p>
<h2 id="6-2-tailwind">6-2. Tailwind</h2>
<h3 id="preflight">Preflight</h3>
<ul>
<li><strong>tailwind 내장 브라우저 초기화 스타일</strong></li>
<li><strong>modern-normalize</strong> 기반</li>
<li>base <strong>layer</strong>에 포함</li>
</ul>
<h3 id="layer-구조">layer 구조</h3>
<ul>
<li><strong>theme</strong> : CSS 변수, 테마 토큰</li>
<li><strong>base</strong> : Preflight, 기본 스타일</li>
<li><strong>components</strong> : <code>@apply</code> 기반 컴포넌트</li>
<li><strong>utilities</strong> : 유틸리티 클래스<blockquote>
<h4 id="우선순위">우선순위</h4>
<p>theme → base → components → utilities</p>
</blockquote>
</li>
</ul>
<h3 id="v4-import-문법">v4 import 문법</h3>
<ul>
<li><code>@import &quot;tailwindcss&quot;;</code> 한줄로 전체 <code>import</code></li>
<li>개별 <code>import</code> 시 <code>layer( )</code>함수로 맵핑<blockquote>
<h4 id="여기서-주의해야할-점은">여기서 주의해야할 점은,</h4>
<p>위에서 언급한 <strong>Preflight</strong>는 <strong>layer</strong> 개념으로 <code>&quot;tailwindcss&quot;</code>에 내장되어 있다.<br/>
그래서 <code>@layer</code>를 사용할 경우 <strong>무조건<code>@layer</code>를 최상단에 선언</strong>한 뒤, <code>@import &quot;tailwindcss&quot;;</code>를 위치시켜야 한다.<br/>
<code>@layer</code> 선언 전에 <code>@import &quot;tailwindcss&quot;;</code>를 진행한다면,이후 <code>@layer</code> 선언이 기존 구조를 덮어쓰거나 무시될 수 있다.</p>
</blockquote>
</li>
</ul>
<h4 id="tailwind-우선순위-선정-단계는-다음과-같다">Tailwind 우선순위 선정 단계는 다음과 같다.</h4>
<ol>
<li><strong><code>@layer</code> 선언</strong> : (theme→ base → components → utilities 순으로 렌더) 스타일시트 처리 시점에서 layer 우선순위 구조 확정</li>
<li><strong><code>@import &quot;tailwindcss&quot;;</code></strong> : 이미 선언된 layer 안에 스타일 배치</li>
<li><strong><code>@config(optional)</code></strong> : Tailwind 설정(tailwind.config.ts) 설정 적용</li>
</ol>
<blockquote>
<p><em>참고</em>
<strong>Tailwind v4</strong>부터는 <code>tailwind.config.ts</code>를 사용하지 않는다.
다만, 나는 스타일링 이원화에 따른 중앙 관리형 타이포시스템 사용을 위해 추가로 설정했다.</p>
</blockquote>
<h2 id="6-3-emotion-관련">6-3. Emotion 관련</h2>
<h3 id="런타임-스타일-생성">런타임 스타일 생성</h3>
<ul>
<li>컴포넌트 렌더링 시점에 <code>&lt;style&gt;</code> 태그 동적 삽입</li>
<li>빌드타임에 CSS 파일을 생성하지 않음</li>
</ul>
<h3 id="cacheprovider와-createcache">cacheProvider와 createCache</h3>
<ul>
<li><strong>Emotion 스타일 캐싱 관리</strong> : className 캐싱-중복 선언 방지를 위해</li>
<li><strong>key</strong> : 생성되는 클래스명 접두사(prefix)</li>
<li><strong>prepend</strong> : <code>&lt;style&gt;</code>태그 삽입 위치<ul>
<li>true : head 최상단</li>
</ul>
</li>
</ul>
<h3 id="prepend-옵션-주의">prepend 옵션 주의</h3>
<ul>
<li><strong><code>prepend : true</code></strong> → head 최상단에 삽입 → 우선순위 낮아짐.</li>
<li><code>@layer</code> 사용 시 prepend 설정은 의미가 없음. layer 우선순위가 결정되기 때문에.</li>
</ul>
<h2 id="6-4-nextjs-app-router">6-4. Next.js (App Router)</h2>
<h3 id="ssr과-css-삽입">SSR과 CSS 삽입</h3>
<ul>
<li>서버에서 HTML 렌더링 시 스타일도 함께 추출</li>
<li>클라이언트 하이드레이션 후에도 스타일 유지 필요</li>
</ul>
<h3 id="emotion-cache-패턴">Emotion Cache 패턴</h3>
<ul>
<li><code>useServerInsertedHTML</code>로 SSR시 스타일 삽입</li>
<li><code>CacheProvider</code>로 클라이언트 스타일 관리</li>
</ul>
<hr>
<h1 id="7-마무리">7. 마무리</h1>
<h2 id="7-1-핵심-요약">7-1. 핵심 요약</h2>
<ul>
<li><strong>문제 상황</strong> : <code>Tailwind</code> + <code>Emotion</code> 동일 속성 충돌, 빌드마다 출력되는 결과 다름</li>
<li><strong>원인</strong> : CSS 삽입 순서에 따른 <strong>Source Order</strong> 의존</li>
<li><strong>해결</strong> : <strong>CSS Cascade Layer</strong>로 명시적 우선순위 설정</li>
<li><strong>전략</strong> : <code>Emotion</code>을 Unlayered로 유지(최상위 우선순위)</li>
<li><strong>오버라이드</strong> : 필요 시 <code>!</code>접두사 사용</li>
</ul>
<h2 id="7-2-체크리스트">7-2. 체크리스트</h2>
<ol>
<li><code>globals.css</code> 에 <code>@layer</code> 선언</li>
<li><code>reset.css</code> 삭제(<strong>tailwind preflight</strong>로 대체)</li>
<li><strong>Next.js</strong> 경우 <strong>EmotionCache</strong> 설정</li>
<li>dev/prod 빌드 테스트</li>
</ol>
<hr>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/aac4712d-939c-4f88-a94b-5f8ca868b384/image.png" alt=""><em>( 이렇게 또 이슈 상황에서 살아남았다..! )</em>
내가 판 무덤같은 이원화 전략이지만, 이왕 선택한 김에 제대로 활용해보고 싶다.
언젠간 만날 클린 코드를 기다리며..글을 마칩니당~</p>
<blockquote>
<p>참고 자료
<a href="https://tailwindcss.com/docs/preflight">Tailwind Preflight 공식 문서</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@layer">MDN @layer</a>
<a href="https://emotion.sh/docs/ssr#when-using-emotionreact">Emotion Server Side Rendering 공식 문서 </a>
<a href="https://www.w3.org/TR/css-cascade-5/#cascade-layers">CSS Cascading and Inheritance Level 5 </a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[03. Next.js에서 자동 저장 구현하기]]></title>
            <link>https://velog.io/@heeflee_1310/03.-Next.js%EC%97%90%EC%84%9C-%EC%9E%90%EB%8F%99-%EC%A0%80%EC%9E%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@heeflee_1310/03.-Next.js%EC%97%90%EC%84%9C-%EC%9E%90%EB%8F%99-%EC%A0%80%EC%9E%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 22 Mar 2026 16:10:58 GMT</pubDate>
            <description><![CDATA[<p>이걸 쓰기 위해 앞에 2개의 글을 작성했답니다..
그냥 깃허브 공개 코드였다면 속시원했을텐데..덕분에 다시 공부했어요~(p)</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/d96223c9-8b1b-4529-ba61-fcc59613ea6f/image.png" alt=""></p>
<h1 id="0-intro">0. Intro</h1>
<h2 id="0-1-자동-저장이란">0-1. 자동 저장이란?</h2>
<blockquote>
<p><strong>사용자가 직접 저장 버튼을 누르지 않아도, 데이터 변경이 감지되면 자동으로 서버에 저장되는 기능</strong></p>
</blockquote>
<p>대표적으로 Google Docs, Notion, Figma처럼 타이핑하는 순간 자동으로 저장되는 기능이 대표적이다.</p>
<h2 id="0-2-왜-필요할까">0-2. 왜 필요할까?</h2>
<p><strong>Next.js 환경</strong>에서 <strong>그래픽 에디터 기능</strong>을 개발하면서 자동 저장의 필요성을 느꼈다.</p>
<p>에디터에서는 아래와 같이 작고 큰 다양한 변경들이 끊임 없이 발생했다.</p>
<ul>
<li><strong>작은 단위 변경</strong> : 요소 1px 이동, 컬러 해시값 변경, 요소 내 컨텐츠 한 글자 변경 등</li>
<li><strong>큰 단위 변경</strong> : 작업물의 한 페이지 삭제 및 추가, n개 이상의 요소의 그룹을 복사 등</li>
</ul>
<p>이 과정에서 브라우저 종료, 새로고침, 네트워크 끊김 등 <strong>예상하지 못한 상황이 발생하면,
작업 내역이 모두 날아갈 수 있다.</strong>
그리고 실제로 기능 테스트를 진행하면서 브라우저 새로고침이 일어나면, 모든 데이터가 날라가는 상황에서 많이 짜증이 나기도 했다..ㅎㅎ
<em>( 디자인과 출신으로서 정말 그냥 죽고 싶은 순간 1위 )</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/3083b358-5bad-41e1-b5e7-00a2739af670/image.png" alt=""></p>
<p>사용자가 매번 저장 버튼을 누르는 것도 불편하고, 깜빡하면 데이터 손실로 이어진다.</p>
<p>자동 저장은 이런 문제를 해결하기 위해 구현해보기로 했다.</p>
<h2 id="0-3-구현-전략--debounce--usemutation-조합">0-3. 구현 전략 : debounce + useMutation 조합</h2>
<p>자동 저장은 아래 두 가지 핵심 전략을 사용했다.</p>
<ul>
<li><strong><code>useMutation</code></strong> : 에디터 내부 작업물 데이터를 서버에 저장하는 요청 담당(<a href="https://velog.io/@heeflee_1310/02.-Tanstack-Query-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0#1-usemutation%EC%9D%B4%EB%9E%80">▶ 이동하기</a>)</li>
<li><strong><code>debounce</code></strong> : 불필요한 네트워크 요청을 줄이는 요청 제어 담당</li>
</ul>
<p>둘을 조합하면 아래와 같은 흐름을 만들 수 있다.</p>
<blockquote>
<ol>
<li>에디터 작업물 변경 </li>
<li><code>debounce</code> 대기</li>
<li>일정 시간 후 <code>useMutation</code> 실행</li>
<li>서버 저장</li>
<li>걱정 없이 행복한 에디터 작업 진행</li>
</ol>
</blockquote>
<hr>
<h1 id="1-debounce란">1. debounce란?</h1>
<h2 id="1-1-debounce-정의">1-1. debounce 정의</h2>
<blockquote>
<p><strong>연속된 이벤트 중 마지막 이벤트만 실행되도록 지연시키는 기법</strong></p>
</blockquote>
<p>만약 타이핑 중이라면, 타이핑을 멈추고(끝나고) <strong>n초 후에 실행</strong>한다고 이해하면 된다.</p>
<h2 id="1-2-자동-저장에서-debounce가-필요한-이유">1-2. 자동 저장에서 debounce가 필요한 이유</h2>
<p>만약, <strong><code>debounce</code></strong> 없이 자동 저장을 구현한다면, 어떤 상황이 발생할 수 있을까?</p>
<h4 id="예시-상황--input-창에-안녕하세요-타이핑">예시 상황 : input 창에 &quot;안녕하세요&quot; 타이핑</h4>
<blockquote>
<p><strong><code>debounce</code> 적용 전 - 총 5번의 API 요청이 일어난다.</strong>
    1. &quot;안&quot; 입력 → API 요청
    2. &quot;녕&quot; 입력 → API 요청
    3. &quot;하&quot; 입력 → API 요청
    4. &quot;세&quot; 입력 → API 요청
    5. &quot;요&quot; 입력 → API 요청</p>
</blockquote>
<blockquote>
<p><strong><code>debounce</code> 적용 후 - 적용 전과 다르게 1번의 API 요청만 발생한다.</strong>
    1. &quot;안녕하세요&quot; 입력 → <strong>1초 대기</strong> → API 요청</p>
</blockquote>
<p>정리하자면, <strong>debounce</strong>를 통해 자동 저장을 구현하면
불필요한 네트워크 요청을 최소화하여 서버 부하를 줄이고, 통신 비용을 낮춰 앱 성능을 높일 수 있다.</p>
<hr>
<h1 id="2-nextjs-app-router-환경-세팅">2. Next.js App Router 환경 세팅</h1>
<h2 id="2-1-queryclientprovider-세팅app-router-기준">2-1. QueryClientProvider 세팅(App Router 기준)</h2>
<p><code>Next.js</code> <code>App Router</code>에서 <code>QueryClientProvider</code>를 사용하려면 별도의 클라이언트 컴포넌트가 필요하다.
<code>QueryClientProvider</code>는 말그대로 <strong>Client Provider</strong>이기에 <code>React Context</code> 기반이기 때문이다.
<strong>즉, 서버 컴포넌트에서는 직접 사용할 수 없다.</strong></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/6f4ad025-5f65-48d6-8e04-e191563ed6da/image.png" alt=""></p>
<p><code>&#39;use client&#39;;</code>를 사용하여 클라이언트 컴포넌트로 선언하여 <code>Provider</code>를 생성한다.</p>
<blockquote>
<p><em>참고</em>
<code>useState</code>로 <code>QueryClient</code>를 생성하는 이유는 매 렌더링마다 새 인스턴스가 생성되는 것을 방지하기 위함이다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/0368b42a-65b6-4d76-b0b9-7a1b6fad8f7b/image.png" alt=""></p>
<p>서버 컴포넌트인 <code>RootLayout</code>에서 <code>Context</code>를 전달할 자식 요소(<code>children</code>)를 생성한 Provider로 감싼다.
이러면 이제 <code>Next.js</code>환경에서 <code>Tanstak Query</code> 세팅이 끝난다.</p>
<blockquote>
<p><em>참고</em>
<a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-react-query-3#reactqueryconfigprovider-and-reactquerycacheprovider-have-both-been-replaced-by-queryclientprovider">Tanstack Query 공식 문서</a></p>
</blockquote>
<h2 id="2-2-api-데이터-구조-설계">2-2. API 데이터 구조 설계</h2>
<p>자동 저장 구현 전, API 요청 <code>body</code>와 <code>Zustand store(CanvasState)</code>를 동일한 JSON 구조로 설계했다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/08103c08-0082-4c3a-b9e7-b74bee7de916/image.png" alt=""></p>
<p>구조를 동일하게 맞추면 별도 변환 없이 <code>mutate(canvasState)</code>으로 바로 넘길 수 있기때문이다.
백엔드와 응답 포맷을 사전에 조율한 덕분에 불필요한 작업 비용을 줄일 수 있었다.</p>
<hr>
<h1 id="3-자동-저장-구현">3. 자동 저장 구현</h1>
<h2 id="3-1-에디터-client-상태-관리zustand">3-1. 에디터 client 상태 관리(Zustand)</h2>
<p>에디터의 클라이언트 상태는 <code>Zustand store</code>로 관리한다.
<code>CanvasState</code>가 최상위 계층이며, 모든 에디터 데이터가 여기에 저장된다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/887b9728-11bd-49eb-8f76-28480a1ca60f/image.png" alt=""></p>
<p>컴포넌트는 아래와 같이 <code>store</code>를 구독한다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/8ba20ea6-d4d1-465b-b959-1317d997ab86/image.png" alt=""></p>
<p><code>canvasState</code>가 변경될 때마다 자동 저장이 트리거 된다.</p>
<h2 id="3-2-usemutation으로-저장-요청-연동">3-2. useMutation으로 저장 요청 연동</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/562a87ae-e138-4df1-80b3-0bed649c4838/image.png" alt=""></p>
<h2 id="3-3-debounce-적용---불필요한-네트워크-요청-최소화">3-3. debounce 적용 - 불필요한 네트워크 요청 최소화</h2>
<p><code>useRef</code>로 타이머를 관리하여 <code>debounce</code>를 구현했다.
<code>useRef</code>를 사용하는 이유는 타이머가 리렌더링 시에도 초기화되지 않고, 
유지되어야 하기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/e228e2ef-4388-48da-a2db-c2cd07f84c37/image.png" alt=""></p>
<blockquote>
<p><em>참고</em>
<code>clearTimeout(timerRef.current)</code>는 
컴포넌트가 언마운트될 때 대기 중인 타이머를 취소한다.
호출하지 않으면 언마운트 후에도 뒤늦게 API 요청이 발생할 수 있다.</p>
</blockquote>
<p>이번 프로젝트에서는 <code>debounce</code>를 <strong>3분</strong>으로 정하였다.</p>
<p>판단 기준은 지극히 개인적이긴하다. <em>(개발자 실격 포인트)</em>
1분으로 설정하면 너무 많은 통신 비용이 발생할 것 같고, 생각보다 1분동안 많은 작업양이 발생하지 않았다.
그렇지만, 5분동안은 생각보다 작업양이 많았다.
만약 자동저장이 안되어서 데이터를 날리더라도 타격이 없고, 통신 비용이 많지 않은 범위는 &#39;3분&#39;이 적당하다고 판단했다.</p>
<p><em>( just one 3 minutes..내 것이 되는 시간..순진한..작동에 기뻐 우는 유저들.. )</em></p>
<h2 id="3-4-저장-상태-ui-표시">3-4. 저장 상태 UI 표시</h2>
<p><code>isPending</code>, <code>isError</code>, <code>isSuccess</code>를 활용하여 저장 상태를 UI에 적용했다.
스타일링은 <code>emotion/styled</code> 기반으로 구현했다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/d23b9048-23c3-4e9a-a28e-bd8c2c03f071/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/e7c7707d-df65-46ad-98c5-9cee66af355e/image.png" alt=""></p>
<h1 id="4-전체-코드">4. 전체 코드</h1>
<p>자동 저장 컴포넌트 코드는 아래와 같다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/466ca490-7216-4bc7-8695-e314ecc6760b/image.png" alt=""></p>
<p>스타일링은 <code>emotion/styled</code>의 <code>css helper</code>를 활용하여 구현했다.(<a href="https://velog.io/@heeflee_1310/02.-emotionstyled-%EA%B8%B0%EC%B4%88#4-1-css-helper%EB%A1%9C-%EB%8F%99%EC%A0%81-%EC%8A%A4%ED%83%80%EC%9D%BC-%EB%B6%84%EB%A6%AC">▶ 이동하기</a>)</p>
<h1 id="5-마무리">5. 마무리..</h1>
<p>이번 글을 정리하면 다음과 같다.</p>
<ul>
<li><strong>debounce</strong>로 연속된 상태 변경 이벤트를 제어하여 불필요한 네트워크 요청을 최소화한다.</li>
<li><code>Next.js</code> <code>App Router</code>에서 <code>QueryClientProvider</code>는 <code>&#39;use client&#39;</code> 컴포넌트로 분리하여 세팅한다.</li>
<li><code>Zustand store(CanvasState)</code>와 API 스키마를 동일한 JSON구조로 설계하면 별도 변환 없이 <code>mutate(canvasState)</code>로 바로 요청할 수 있다.</li>
<li><code>useRef</code> + <code>setTimeout</code> 조합으로 리렌더링 시에도 타이머를 유지하며 <code>debounce</code>를 구현한다.</li>
<li><code>isPending</code> / <code>isError</code> / <code>isSuccess</code>로 저장 상태를 UI에 표시 한다.</li>
</ul>
<p>이번 기회에 <code>Tanstack Query</code>의 공식 문서를 제대로 살펴보게 되었다.
그곳은 노다지다..끝나지 않은 배움의 축복이...눈물이 살짝 고인다(행복의 눈물..ㅎㅎ)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[02. Tanstack Query 데이터 작성하기]]></title>
            <link>https://velog.io/@heeflee_1310/02.-Tanstack-Query-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@heeflee_1310/02.-Tanstack-Query-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 22 Mar 2026 13:01:35 GMT</pubDate>
            <description><![CDATA[<p>1편에서 <strong>useQuery</strong>로 서버 데이터를 <strong>&quot;읽는&quot;</strong> 방법에 대해 설명해봤는데요!
이번 편에서는 서버 데이터를 <strong>&quot;쓰는&quot;</strong> 방법에 대해 알아보겠습니다~
게시글 작성,수정, 삭제처럼 서버에 변경을 요청하는 모든 작업에 참고하시면 됩니다!!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/f92ceb53-4c0a-47df-bcb4-eb7c735c38e3/image.png" alt=""></p>
<h1 id="0-intro">0. Intro</h1>
<p>1편의 이론을 간단히 정리하면 아래와 같습니다.
<a href="https://velog.io/@heeflee_1310/01.-Tanstack-QueryReact-Query%EB%9E%80">▶ 1편 이동하기</a></p>
<ul>
<li><strong><code>useQuery</code></strong> : 서버 데이터를 <strong>&quot;읽을 때&quot;</strong> 사용하는 훅 (<a href="https://velog.io/@heeflee_1310/01.-Tanstack-QueryReact-Query%EB%9E%80#2-usequery-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9%EB%B2%95">▶ 이동하기</a>)</li>
<li><strong><code>QueryKey</code></strong> : 캐시를 식별하는 기준점(<a href="https://velog.io/@heeflee_1310/01.-Tanstack-QueryReact-Query%EB%9E%80#1-2-query-key">▶ 이동하기</a>)</li>
<li><strong><code>staleTime</code></strong>/<strong><code>gcTime</code></strong> : 캐시의 신선도와 수명을 제어하는 옵션(<a href="https://velog.io/@heeflee_1310/01.-Tanstack-QueryReact-Query%EB%9E%80#1-3-staletimegctime">▶ 이동하기</a>)<br/>

</li>
</ul>
<p>이번 편에서는 <strong>데이터를 직접 쓰고(POST), 제어(수정(PUT,PATCH), 삭제(DELETE))하는 방식</strong>에 대해 알아보겠다.</p>
<ul>
<li><strong><code>useMutation</code></strong> : 서버 데이터를 <strong>&quot;쓸 때&quot;</strong> 사용하는 훅 <em>(<a href="https://velog.io/@heeflee_1310/02.-Tanstack-Query-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0#1-usemutation%EC%9D%B4%EB%9E%80">▶ 1. useMutation이란?</a>)</em></li>
<li><strong><code>useQueryClient</code></strong> : 캐시를 직접 제어하는 훅 <em>(<a href="https://velog.io/@heeflee_1310/02.-Tanstack-Query-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0#2-usequeryclient%EB%9E%80">▶ 2. useQueryClient란?</a>)</em></li>
</ul>
<p>추가적으로 아래 요소들도 함께 다뤄보겠다.</p>
<ul>
<li><strong>에러 핸들링</strong> : <code>mutation</code> 실패 시 처리 방법 <em>(<a href="https://velog.io/@heeflee_1310/02.-Tanstack-Query-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0#3-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81">▶ 3. 에러 핸들링</a>)</em></li>
<li><strong>낙관적 업데이트</strong> : 응답을 기다리지 않고 UI를 먼저 바꾸는 패턴 <em>(<a href="https://velog.io/@heeflee_1310/02.-Tanstack-Query-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0#4-usemutation--usequeryclient-%ED%95%A8%EA%BB%98-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%ED%8C%A8%ED%84%B4">▶ 4-1. 낙관적 업데이트</a>)</em></li>
</ul>
<hr>
<h1 id="1-usemutation이란">1. useMutation이란?</h1>
<blockquote>
<p><strong>서버 데이터를 생성(POST), 수정(PUT, PATCH), 삭제(DELETE)할 때 사용하는 훅</strong>
즉, 데이터를 <strong>&quot;쓰는&quot;</strong> 훅이다.</p>
</blockquote>
<h2 id="1-1-usequery와-차이점">1-1. useQuery와 차이점</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/9ac634fa-743f-4ce9-a4c3-9abc057b9ade/image.png" alt=""></p>
<p><code>useQuery</code>는 컴포넌트가 렌더링되면 자동으로 실행된다.
하지만, <strong><code>useMutation</code></strong>은 버튼 클릭, 폼 제출처럼 <strong>&quot;특정 이벤트가 발생했을 때&quot;</strong> 실행된다.</p>
<h2 id="1-2-usemutation-기본-구조">1-2. useMutation 기본 구조</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/8f151dde-87af-4ce8-87ff-4509862f0648/image.png" alt=""></p>
<ul>
<li><strong><code>mutationFn</code></strong> : 실제 API 요청을 담당하는 함수. <code>Promise</code>를 반환하면 된다.</li>
<li><strong><code>onSuccess</code></strong> : 요청이 <strong>성공</strong>했을 때 실행되는 콜백. 응답 데이터(data)를 인자로 받는다.</li>
<li><strong><code>onError</code></strong> : 요청이 <strong>실패</strong>했을 때 실행되는 콜백. 에러 객체(error)를 인자로 받는다.</li>
<li><strong><code>onSettled</code></strong> : 성공/실패 여부와 상관 없이 <strong>항상 실행</strong>되는 콜백. <code>try-catch</code>의 <code>finally</code>와 동일 개념</li>
</ul>
<blockquote>
<p><em>참고</em>
<a href="https://tanstack.com/query/latest/docs/framework/react/guides/mutations#mutation-side-effects">Tanstack Query 공식 문서</a></p>
</blockquote>
<h2 id="1-3-usemutation-반환값">1-3. useMutation 반환값</h2>
<p><code>useMutation</code>이 반환하는 주요 값들은 아래와 같다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/3ae800fa-56a9-4a1e-939f-f759f947783f/image.png" alt=""></p>
<ul>
<li><strong><code>mutate</code></strong> : mutation을 실행하는 함수. 이벤트 핸들러에서 호출한다.</li>
<li><strong><code>isPending</code></strong> : 요청 진행 중 여부 (boolean값)</li>
<li><strong><code>isError</code></strong> : 에러 발생 여부 (boolean값)</li>
<li><strong><code>error</code></strong> : 에러가 발생한 객체</li>
</ul>
<p>반환값 사용 방식은 두 가지이다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/32cb1cdd-6649-4219-a7b0-6373b9d87d68/image.png" alt="">
상황에 맞게 편한 방식을 선택하여 사용하면 된다.</p>
<blockquote>
<p><em>참고</em>
<code>mutate</code>와 <code>mutateAsync</code> 두가지가 있다.</p>
</blockquote>
<ul>
<li><strong><code>mutate</code></strong> : <strong>콜백(onSuccess, onError)</strong> 방식으로 결과 처리</li>
<li><strong><code>mutateAsync</code></strong> : <strong>Promise</strong>를 반환하여 <code>async</code>/<code>await</code>으로 결과 처리<br/>
[Tanstack Query 공식 문서](https://tanstack.com/query/latest/docs/framework/react/guides/mutations#promises)

</li>
</ul>
<p><code>useMutation</code> 선언과 반환값의 전체 코드는 다음과 같다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/dc281f97-b3cc-4a98-b317-296402a02c33/image.png" alt=""></p>
<h2 id="1-4-예제-코드">1-4. 예제 코드</h2>
<p>게시글을 작성하는 <code>POST</code> 요청 예제이다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/23277b90-ed02-43a0-a4ee-f4e5f94d1265/image.png" alt=""><img src="https://velog.velcdn.com/images/heeflee_1310/post/77d7c7f6-5d18-4cf5-a5be-1fbb8ebfd01f/image.png" alt="">
위의 예시코드의 작동 화면이다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/bcfff937-242c-4185-b593-c8d0775a8a73/image.gif" alt=""></p>
<hr>
<h1 id="2-usequeryclient란">2. useQueryClient란?</h1>
<blockquote>
<p><strong>QueryClient 인스턴스에 접근하여 캐시를 직접 제어하는 훅</strong></p>
</blockquote>
<p>지난 글에서 <code>QueryClient</code>는 모든 캐시를 저장하는 핵심 인스턴스라고 설명했다.
<code>useQueryClient</code>는 해당 인스턴스에 접근하여 캐시를 읽거나 수정할 수 있게 해준다.</p>
<blockquote>
<p>use로 시작하니, React의 Hook개념이 베이스인거 잊지말기!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/25cb0e47-23b7-4216-94c8-64f0f08adefc/image.png" alt=""></p>
<p>위와 같은 방식으로 선언하여 사용한다.</p>
<blockquote>
<p><em>참고</em>
<a href="https://tanstack.com/query/latest/docs/framework/react/reference/useQueryClient">Tanstack Query 공식 문서</a></p>
</blockquote>
<h2 id="2-1-invalidatequeries--캐시-무효화">2-1. invalidateQueries : 캐시 무효화</h2>
<blockquote>
<p><strong>특정 Query Key의 캐시를 &quot;오래된 것(stale)&quot;으로 표시하여 자동으로 재요청 유발</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/7f3da05f-e946-47d5-aa82-2af24ad6c6fb/image.png" alt=""></p>
<p><code>mutation</code>으로 데이터를 변경했을 때, 기존 캐시가 최신 데이터를 반영하지 못하는 상황이 발생한다.
<code>invalidateQueries</code>를 호출하면, 해당 캐시를 stale 처리하고, 화면에서 사용 중인 경우 자동으로 재요청하여 최신 데이터로 갱신한다.</p>
<blockquote>
<p><em>참고</em>
<a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation">Tanstack Query 공식 문서</a></p>
</blockquote>
<h2 id="2-2-setquerydata--캐시-직접-수정">2-2. setQueryData : 캐시 직접 수정</h2>
<blockquote>
<p><strong>특정 Query Key의 캐시 데이터를 직접 수정.</strong>
API 재요청 없이 캐시를 업데이트 할 때 사용한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/c7792d65-6cea-4220-885e-b9460683ff4d/image.png" alt=""></p>
<p><code>invalidateQueries</code>는 서버에 재요청하여 최신 데이터를 받아오는 방식이라면,
<code>setQueryData</code>는 서버 요청 없이 캐시만 직접 수정한다.</p>
<p>주로 낙관적 업데이트(4. 낙관적 업데이트)에서 사용된다.</p>
<blockquote>
<p><em>참고</em>
<a href="https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientsetquerydata">Tanstack Query 공식 문서</a></p>
</blockquote>
<h2 id="2-3-예제-코드">2-3. 예제 코드</h2>
<p>게시글 작성 후 목록 캐시를 갱신하는 실제 코드이다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/83819154-7c0c-4450-b8ab-f57e84f3f871/image.png" alt="">
<code>onSuccess</code>에서 <code>invalidateQueries</code>를 호출하면,
게시글 작성 성공 후 자동으로 목록을 다시 불러온다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/68e44ddf-fd36-45ea-b439-0b875e48ef08/image.gif" alt=""></p>
<hr>
<h1 id="3-에러-핸들링">3. 에러 핸들링</h1>
<h2 id="3-1-onerror-콜백">3-1. onError 콜백</h2>
<p><code>mutation</code> 실패 시 <code>onError</code> 콜백에서 에러를 처리할 수 있다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/e0935b2b-fd14-4dc4-96c7-eae3970f45fe/image.png" alt=""></p>
<blockquote>
<p><em>참고</em>
<code>onSettled</code>는 <code>mutation</code> 상태(성공, 실패)와 무관하게 모두 실행된다.
캐시 무효화처럼 결과에 관계없이 <strong>항상 실행해야 하는 로직은 <code>onSettled</code>에 두는 것이 좋다.</strong></p>
</blockquote>
<h2 id="3-2-iserrorerror-반환값-활용">3-2. isError/error 반환값 활용</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/185e28e6-2bb7-4626-8e54-7fe5a4d3ec3c/image.png" alt="">
<code>isError</code>와 <code>error</code>를 활용하면, UI에서 직접 에러 상태를 표시할 수 있다.</p>
<p>위의 예시코드를 구현한 화면이다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/42336105-85ef-4e71-ad26-2f5084948edd/image.gif" alt=""></p>
<h1 id="4-usemutation--usequeryclient-함께-사용하는-패턴">4. useMutation + useQueryClient 함께 사용하는 패턴</h1>
<h2 id="4-1-낙관적-업데이트optimistic-update란">4-1. 낙관적 업데이트(Optimistic Update)란?</h2>
<blockquote>
<p><strong>서버 응답을 기다리지 않고, 요청이 성공할 것이라 &#39;낙관&#39;하고 UI를 먼저 업데이트 하는 패턴</strong></p>
</blockquote>
<p>일반적인 요청 흐름과 비교한다면 다음과 같다.</p>
<h4 id="일반적인-흐름">일반적인 흐름</h4>
<blockquote>
<p>요청 → (대기) → 서버 응답 → UI 업데이트</p>
</blockquote>
<h4 id="낙관적-업데이트-흐름">낙관적 업데이트 흐름</h4>
<blockquote>
<p>요청 → UI 즉시 업데이트 → 서버 응답 → (실패 시 rollback)</p>
</blockquote>
<p>서버 응답을 기다리는 동안 UI가 멈춰있지 않기 때문에 사용자 경험(UX)이 향상된다.
인스타그램의 좋아요 버튼이 대표적인 예시이다.
버튼을 누르는 즉시 하트가 채워지고, 실패 시에만 원래대로 돌아온다.</p>
<h2 id="4-2-onmutate--onerror-rollback-흐름">4-2. onMutate + onError rollback 흐름</h2>
<p>낙관적 업데이트는 <code>onMutate</code>, <code>onError</code>, <code>onSettled</code> 세 콜백을 조합하여 구현한다.</p>
<ul>
<li><strong><code>onMutate</code></strong> : 요청 직전. 현재 캐시를 백업하고, UI를 먼저 업데이트</li>
<li><strong><code>onError</code></strong> : 요청 실패. 백업해둔 캐시로 rollback</li>
<li><strong><code>onSettled</code></strong> : 요청 완료 후. 서버 데이터와 최종 동기화(invalidateQueries)</li>
</ul>
<h2 id="4-3-예제-코드">4-3. 예제 코드</h2>
<p>실제 게시글 스크랩 버튼 기능을 낙관적 업데이트로 구현한 코드이다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/c203f3d0-40ea-4462-af55-c48b271f035b/image.png" alt=""></p>
<blockquote>
<p><em>참고</em>
<strong><code>cancelQueries</code></strong>란? (<code>onMutate</code> 내 <code>await</code> 부분)
진행 중인 <code>refetch</code> 요청을 취소한다.
낙관적 업데이트 직전에 호출하지 않으면, 진행 중이던 <code>refetch</code> 응답이 낙관적으로 업데이트한 캐시를 덮어쓸 수 있다.</p>
</blockquote>
<p>해당 코드를 실제로 구현하면 아래와 같이 작동한다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/50fcda43-5dcf-45a4-93df-857f6b659e9b/image.gif" alt=""></p>
<h1 id="5-마무리">5. 마무리..</h1>
<p>이번 글에서 다룬 내용을 정리하면 아래와 같다.</p>
<ul>
<li><code>useMutation</code> 으로 서버 데이터를 생성, 수정, 삭제한다.</li>
<li><code>onSuccess</code>, <code>onError</code>, <code>onSettled</code> 콜백으로 요청의 성공, 실패, 완료 시점을 처리한다.</li>
<li><code>useQueryClient</code>의 <code>invalidateQueries</code>로 캐시를 무효화하여 목록을 자동 갱신한다.</li>
<li><code>useQueryClient</code>의 <code>setQueryData</code>로 서버 요청 없이 캐시를 직접 수정한다.</li>
<li><code>onMutate</code> + <code>onError</code> rollback 패턴으로 낙관적 업데이트를 구현한다.</li>
</ul>
<p>이제 <strong>Tanstack Query</strong>의 핵심은 파악했다!
<em>( 하지만 배움의 축복은 끝나지 않는다... 공식 문서에 축복들이 잔뜩 있다.. )</em></p>
<p>다음편에서는 지금까지의 이론을 바탕으로 Next.js 환경에서 자동 저장 기능을 직접 구현한 과정을 작성해 볼 예정이다.</p>
<blockquote>
<p><em>참고</em>
<a href="https://tanstack.com/query/latest">▶ 🔗 Tanstack Query 공식 문서</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[01. Tanstack Query(React Query)란?]]></title>
            <link>https://velog.io/@heeflee_1310/01.-Tanstack-QueryReact-Query%EB%9E%80</link>
            <guid>https://velog.io/@heeflee_1310/01.-Tanstack-QueryReact-Query%EB%9E%80</guid>
            <pubDate>Sun, 22 Mar 2026 08:49:06 GMT</pubDate>
            <description><![CDATA[<p>제가 처음 <strong>React</strong>를 접했을 때 가장 낯설던 개념은 <strong>상태(state)</strong>였습니다.
저는 개념이 이해되지 않아 상태 관련된 코드를 직접 짜보며 억지로 이해했는데요..
이 글을 읽는 분들은 저와 같은 가시밭길을 걷지 않길 바라며 자세하게 작성해봤습니다..!
우리 모두 서버 상태관리의 마스터가 되어봐요~!😋
<img src="https://velog.velcdn.com/images/heeflee_1310/post/1cdb3533-b033-4737-9fef-79fcc3dec175/image.png" alt=""></p>
<h1 id="0-intro">0. Intro</h1>
<h2 id="0-1-상태-관리란">0-1. 상태 관리란?</h2>
<p><strong>상태(State)</strong>란, <strong>&quot;컴포넌트가 기억하는 값&quot;</strong>이다.
버튼을 &quot;클릭 했는지&quot;, 모달이 &quot;열려있는지&quot;, 입력창에 &quot;뭘 썼는지&quot; 등 이런 <strong>&quot;값&quot;들을 관리</strong>하는 것이 상태 관리이다.</p>
<p>상태는 크게 두 종류로 나뉜다.
위에서 언급한 것 처럼 UI와 관련된 상태가 있고, 그와 별개로 서버에서 가져오는 데이터가 있다.
그렇다면, 서버 데이터의 상태란 뭘까?</p>
<h2 id="0-2-서버-상태-관리란">0-2. 서버 상태 관리란?</h2>
<p>게시글 목록, 유저 프로필, 상품 정보처럼 <strong>API를 호출해서 받아오는 데이터</strong>가 바로 서버 상태다.
이 데이터를 <strong>&#39;언제 가져오고&#39;</strong>, <strong>&#39;언제 다시 가져오고&#39;</strong>, <strong>&#39;어떻게 저장할지&#39;</strong> 관리하는 것이 서버 상태 관리다.</p>
<h2 id="0-3-클라이언트client-상태-vs-서버server-상태">0-3. 클라이언트(client) 상태 VS 서버(server) 상태</h2>
<p>두 환경의 상태 차이를 표로 정리하자면 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/69287750-b887-4e08-b758-2e37801b4e06/image.png" alt="">
<strong>클라이언트(client) 상태</strong>는 <strong>내가 완전히 제어할 수 있다.</strong></p>
<p>하지만, <strong>서버(server) 상태</strong>는 아래와 같다.</p>
<ul>
<li>제어할 수 없거나, 소유하지 않은 원격 위치에 저장된다.</li>
<li>데이터 가져오기 및 업데이트를 위해 비동기 API가 필요하다.</li>
<li>데이터를 다른 사람이 나의 동의 없이 변경할 수 있다.</li>
<li>어플리케이션에서 최신화되지 않은 데이터가 표시될 수 있다.</li>
</ul>
<p>정리하자면, <strong>서버(server) 상태</strong>는 서버가 언제든 바뀔 수 있고, 내 앱은 그걸 가져다 쓰는 입장이다.
즉, <strong>내가 완전히 제어 &quot;불가능&quot;한 상태이다.</strong></p>
<blockquote>
<p><a href="https://tanstack.com/query/latest/docs/framework/react/overview">서버 상태 정의 참고 문헌</a></p>
</blockquote>
<h2 id="0-4-서버-상태-관리가-어려운-이유">0-4. 서버 상태 관리가 어려운 이유</h2>
<p><code>fetch</code> + <code>useState</code> 만으로 서버 데이터를 다룰때, 주로 아래와 같은 코드를 작성했다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/78ac9e09-5f69-42bc-9f45-fc484756624a/image.png" alt="">
데이터를 하나 가져오는데, state가 3개다. ( 🤢 🤮 )
그리고 이 방식은 아래와 같은 한계들이 있다</p>
<ul>
<li><strong>캐시(Cache)가 없다.</strong> : 페이지를 이동했다 돌아오면, 다시 요청해야한다.</li>
<li><strong>중복 요청을 막기 어렵다.</strong> : 같은 데이터를 여러 컴포넌트에서 각자 요청한다.</li>
<li><strong>데이터가 오래됐는지 모른다.</strong> : 서버가 바뀌어도 내 화면은 그대로다.</li>
<li><strong>보일러플레이트가 많다.</strong> : 매번 로딩/에러 상태(state)를 직접 만든다.</li>
</ul>
<p>위의 한계들을 해결하기 위해 나온 것이 <strong>Tanstack Query(React Query)</strong>이다.</p>
<h1 id="1-tanstack-queryreact-query란">1. Tanstack Query(React Query)란?</h1>
<p>공식 문서의 표현을 가져오면 아래와 같다.</p>
<blockquote>
<p><strong>서버 상태를 fetching, caching, synchronizing, updating하는 라이브러리</strong>
<a href="https://tanstack.com/query/latest/docs/framework/react/overview">참고</a> </p>
</blockquote>
<p>Tanstack Query 라이브러리는 <strong>&quot;서버 데이터를 가져오고, 캐싱하고, 최신 상태로 유지해주는 도구&quot;</strong>이다.</p>
<p>참고로 원래 이름은 <strong>React Query</strong>였다.
React 외의 다양한 프레임워크(Vue, Angular, Svelte 등)를 지원하면서 Tanstack Query로 이름을 바꾸었다.</p>
<p>React 환경에서 사용시 <code>@tanstack/react-query</code>를 설치하여 사용하면 된다.</p>
<blockquote>
<p><em>참고</em>
취준시 JD에 Tanstack Query과 React Query를 혼용해서 작성되는 경우가 많다.
같은 라이브러리이지만, 혹시 모르니 JD 요구 기술 스택에 맞게 서류를 꼼꼼히 수정하자..!</p>
</blockquote>
<p>이제부터 <strong>Tanstack Query</strong> 사용법을 알아보겠다.</p>
<h2 id="1-1-queryclient--queryclientprovider">1-1. QueryClient / QueryClientProvider</h2>
<p><strong>Tanstack Query</strong>를 사용하려면, 먼저 앱 전체를 <code>QueryClientProvider</code>로 감싸야한다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/c477ee2c-de92-48c1-83ef-d15500a971bd/image.png" alt=""></p>
<h4 id="queryclient"><code>QueryClient</code></h4>
<p>캐시를 관리하는 핵심 인스턴스.
모든 쿼리의 데이터가 여기에 저장된다.</p>
<h4 id="queryclientprovider"><code>QueryClientProvider</code></h4>
<p><strong>QueryClient</strong>를 <strong>Context</strong>로 앱 전체에 전달한다.
React의 Context API와 동일한 방식이다.</p>
<blockquote>
<p><em>참고</em>
<strong>Context란?</strong>
React에서 컴포넌트 간 데이터를 공유하기 위한 객체다. 
<code>createContext()</code>로 생성하며, <code>Provider</code>로 감싼 하위 컴포넌트 어디서든 해당 데이터에 접근할 수 있다.<br />
<strong>React의 Context API란?</strong>
React의 컴포넌트 트리 전체에 데이터를 전달하는 기능이다. 
일반적으로 <code>props</code>는 <strong>부모 → 자식</strong>으로만 데이터를 전달할 수 있지만(props drilling 발생), <code>Context</code>를 사용하면 중간 컴포넌트를 거치지 않고 원하는 컴포넌트에 바로 데이터를 전달할 수 있다.</p>
</blockquote>
<h2 id="1-2-query-key">1-2. Query Key</h2>
<blockquote>
<p><strong>캐시의 기준점</strong></p>
</blockquote>
<p><strong>Tanstack Query는 모든 캐시를 Query Key를 기준으로 &quot;식별&quot;한다.</strong>
<strong>Query Key</strong>가 같으면 같은 데이터로 취급한다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/0b2c3629-64c9-438f-be00-426450038f9c/image.png" alt=""></p>
<p><strong>Query Key</strong>는 <strong>&quot;배열 형태&quot;</strong>로 작성한다.
예시 이미지를 보면 키의 형식이 2가지이다.</p>
<h3 id="단순한-키">단순한 키</h3>
<blockquote>
<p><strong>고정된 값이다. 항상 동일한 데이터를 가져올 때 사용한다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/2d05fb47-3880-4594-926b-f73574a378a4/image.png" alt=""></p>
<h3 id="동적인-키">동적인 키</h3>
<blockquote>
<p><strong>변하는 값이 포함된 키이다. 특정 데이터를 하나씩 식별할 때 사용한다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/1fb3a4a3-fa30-411b-a8e2-12c95ba685c0/image.png" alt=""></p>
<p>이미지 속 예시 코드인 <code>[&#39;posts&#39;, 1]</code>과 <code>[&#39;posts&#39;, 2]</code>는 <strong>서로 다른 캐시이다. **
**<code>queryKey</code></strong>는 동일하지만, <strong>배열의 두번째 요소(id)가 다르기 때문이다.</strong></p>
<p>동적인 키가 필요한 이유는 다음과 같다.</p>
<ul>
<li>고정된 값인 게시글 목록(<code>[&#39;posts&#39;]</code>)은 하나지만, </li>
<li>게시글 목록 안 특정 데이터인 게시글 상세(<code>[&#39;posts&#39;, 1]</code>, <code>[&#39;posts&#39;, 2]</code>)는 <strong><code>id</code></strong>마다 데이터가 다르다.</li>
</ul>
<p>즉, <strong><code>id</code></strong>별로 캐시를 따로 관리해야 하므로, 배열에 <strong>유니크한 값인 <code>id</code></strong>를 추가해 키를 구분한다.</p>
<p><strong>React</strong>의 <strong><code>useEffect</code></strong> 의존성 배열과 비슷한 개념이라 생각하면 된다.
<strong><code>postId</code></strong>가 바뀌면 <strong>Query Key</strong>도 바뀌고, 이를 기반으로 <strong>Tanstack Query</strong>가 자동으로 새 데이터를 요청한다. </p>
<p><strong>Query Key</strong> 덕분에 같은 데이터를 여러 컴포넌트에서 요청해도 
<strong>실제 API 호출은 한 번만 일어난다.</strong></p>
<h2 id="1-3-staletimegctime">1-3. staleTime/gcTime</h2>
<blockquote>
<p><strong>데이터를 얼마나 믿을건지</strong></p>
</blockquote>
<p>캐시된 데이터를 언제까지 <strong>&quot;신선하다&quot;</strong>고 볼건지, <strong>&quot;언제 지울 건지&quot;</strong>를 제어하는 두가지 옵션이다.
표로 표현하자면 아래와 같다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/c4a62674-e71b-422e-b737-bdb5ddd6fc89/image.png" alt=""></p>
<blockquote>
<p><em>참고</em>
<strong>gcTime</strong>은 v4까지 <code>cacheTime</code>이라는 이름이었다. v5에서 <code>gcTime</code>으로 변경되었다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/11402e69-5438-45d2-8161-ea3618d758c1/image.png" alt=""></p>
<p><strong>staleTime</strong>를 5분으로 설정하면, 같은 쿼리를 5분 안에 다시 호출해도 API 요청 없이 캐시 데이터를 바로 반환한다.</p>
<p>각 모듈의 작동 흐름은 아래와 같다.</p>
<h4 id="⌛-예시-상황--게시글-목록-페이지에-진입했다가-10분-후-다시-돌아온-경우"><em>⌛ 예시 상황 : 게시글 목록 페이지에 진입했다가, 10분 후 다시 돌아온 경우</em></h4>
<h4 id="1-첫-진입">1. 첫 진입</h4>
<ul>
<li>1-1. <code>queryFn</code> 실행 (API 요청)</li>
<li>1-2. 데이터 캐시에 저장</li>
<li>1-3. <code>staleTime</code> 5분 타이머 시작<br/><h4 id="2-1--staletime--5분-이내-재진입">2-1. [ staleTime ] 5분 이내 재진입</h4>
</li>
<li>API 요청 없음 → 캐시 데이터 그대로 사용 (fresh 상태)<br/><h4 id="2-2--staletime--5분-이후-재진입"><strong>2-2. [ staleTime ] 5분 이후 재진입</strong></h4>
</li>
<li>*<em>⭐ &quot;핵심&quot; 캐시 데이터를 먼저 보여줌 (stale 상태) *</em> </li>
<li>백그라운드에서 <code>queryFn</code> 재실행 (API 재요청) → 새 데이터로 교체
<br/>2-2번은 핵심 동작이다. 
<code>stale</code> 상태여도 빈 화면이 아니라 기존 캐시를 먼저 보여주면서 백그라운드에서 갱신한다.
사용자 입장에선 로딩 없이 바로 데이터가 보이기에 UX가 향상된다.<br/><h4 id="3--gctime--10분-동안-해당-쿼리를-아무도-사용하지-않으면">3. [ gcTime ] 10분 동안 해당 쿼리를 아무도 사용하지 않으면</h4>
</li>
<li><code>gcTime</code> 만료 → 캐시 메모리에서 삭제</li>
<li>다음 진입 시 <code>queryFn</code> 재실행</li>
</ul>
<p>자 이제 기본 작동 원리는 파악이 됐다.
이제 <code>useQuery</code>에 대해 설명해보겠다.</p>
<h1 id="2-usequery-기본-사용법">2. useQuery 기본 사용법</h1>
<h2 id="2-1-usequery란">2-1. useQuery란?</h2>
<blockquote>
<p><strong>Tanstack Query에서 데이터를 가져올 때 사용하는 핵심 훅(Hook)</strong><br/>
<em>참고</em>
<strong>훅(Hook)</strong>이란?</p>
</blockquote>
<ul>
<li>함수 컴포넌트에서 <strong>React의 state, lifecycle</strong> 등을 사용할 수 있게 해주는 함수.</li>
<li><strong>React</strong>의 state, lifecycle을 사용하면 클래스 컴포넌트 없이 함수 컴포넌트만으로 구현이 가능하다. 클래스 컴포넌트는 this, constructor 등 보일러플레이트가 많고 코드가 복잡해지는 단점이 있었지만, 훅의 등장으로 함수 컴포넌트로 동일한 기능을 구현하고, 코드가 간결해졌다.</li>
<li><strong>use로 시작하는 컨벤션</strong>이 있다.(useState, useEffect, useQuery ...)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/d9fb46e1-b059-48b9-bb24-6995c6d88001/image.png" alt=""></p>
<ul>
<li><strong><code>queryKey</code></strong> : 캐시 키.</li>
<li><strong><code>queryFn</code></strong> : 데이터를 가져오는 비동기 함수. fetch, axios 상관 없이 <strong>Promise를 반환하면 된다.</strong></li>
</ul>
<h2 id="2-2-반환값-읽기">2-2. 반환값 읽기</h2>
<p><code>useQuery</code>가 반환하는 주요 값들을 예시 코드로 작성해보았다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/fe9b1ef2-03ae-41a5-acaf-663d56223266/image.png" alt=""></p>
<ul>
<li><strong>data</strong> : 성공적으로 가져온 데이터</li>
<li><strong>isLoading(isPending)</strong> : 최초 로딩 중 여부(boolean값)</li>
<li><strong>isError</strong> : 에러 발생 여부(boolean값)</li>
<li><strong>error</strong> : 발생한 에러 객체</li>
</ul>
<blockquote>
<p><em>참고</em>
v5에서 isLoading이 isPending으로 변경되었다.
두 이름이 혼용되는 글들이 많다!</p>
</blockquote>
<h2 id="2-3-예제-코드">2-3. 예제 코드</h2>
<p>실제로 사용 예정인 &#39;게시글 목록을 불러오는 기능&#39; 예제 코드다.</p>
<blockquote>
<p>게시글 목록 : Feed
게시글 : Post<br/>
<strong>1 Feed : N Post</strong></p>
</blockquote>
<p>먼저 feed의 api 메소드 코드 예시이다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/b8a0812b-ceb4-4a1f-8bd3-641d675d3da8/image.png" alt=""></p>
<p>다음은 api 메소드를 queryFn에 넣은 실제 컴포넌트 코드 예시이다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/2521a887-6e48-4d1d-8799-28ea74ed2e15/image.png" alt=""></p>
<p>위에서 <code>fetch</code> + <code>useState</code> 예시(0-4)보다 코드가 훨씬 짧아지고, <strong>Tanstack Query</strong>가 캐싱과 중복 요청 방지를 알아서 처리해준다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/b9e89196-0893-4cc1-9b48-8e91410c45d8/image.png" alt=""></p>
<h1 id="3-마무리">3. 마무리..</h1>
<p>이번 글에서 다룬 내용을 정리하면 아래와 같다.</p>
<ul>
<li><strong>서버 상태는 클라이언트 상태와 의미가 다르다.</strong> 서버 상태는 서버가 주도권을 갖는 데이터다.</li>
<li><strong>Tanstack Query는 서버 상태를 불러오고, 캐싱하고, 정합성을 유지한다.</strong></li>
<li><code>QueryClient</code>가 캐시를 관리하고, <code>QueryKey</code>가 캐시를 식별한다.</li>
<li><code>staleTime</code>, <code>gcTime</code>으로 <strong>캐시의 신선도와 수명을 제어한다.</strong></li>
<li><code>useQuery</code>로 데이터를 가져오고, <code>data</code>/<code>isLoading</code>/<code>isError</code>로 상태를 다룬다.</li>
</ul>
<blockquote>
<p><em>참고</em>
<strong>정합성이란?</strong>
서버와 클라이언트(혹은 DB) 간의 데이터가 서로 일치하고 모순이 없는 상태를 의미한다.</p>
</blockquote>
<p>이제 <strong>Tanstack Query</strong>를 제대로 활용하기 위해
데이터를 변경할 때 사용하는 useMutation, 캐시를 직접 건드리는 useQueryClient를 다뤄볼 예정이다.</p>
<blockquote>
<p>참고
<a href="https://tanstack.com/query/latest">▶ 🔗 Tanstack Query 공식 문서</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[01. Emotion과 Tailwind, 스타일링 이원화]]></title>
            <link>https://velog.io/@heeflee_1310/05.-Emotion%EA%B3%BC-Tailwind-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94</link>
            <guid>https://velog.io/@heeflee_1310/05.-Emotion%EA%B3%BC-Tailwind-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%9B%90%ED%99%94</guid>
            <pubDate>Thu, 19 Mar 2026 07:40:51 GMT</pubDate>
            <description><![CDATA[<h1 id="intro">Intro</h1>
<p>이전 해커톤 후기 글에 <strong>스타일링 이원화 전략</strong>에 대해 짧게 정리했었다.
<strong>Emotion</strong>은 _상태 변화가 빈번한 컴포넌트_에, <strong>Tailwind</strong>는 _정적 레이아웃 영역_에 사용했다.</p>
<p>당시에는 각자의 장점만 골라쓰자는 관점에서 결정했고, 실제로 개발하면서 나쁘지 않다고 느꼈었다.
그런데 아래와 같은 의문점들이 생기기 시작했다.</p>
<ul>
<li>이 기준이 이론적으로도 맞는 선택일까?</li>
<li>실제로 경계가 모호해지는 순간에 어떻게 해야할까?</li>
<li>어떤 상황에서 이 전략이 잘 작동하고, 어떤 상황에서 삐걱거릴까?</li>
</ul>
<p>이런 의문점들을 해결해보기 위해 조금 더 깊게 접근해보았다.</p>
<h1 id="1-tailwind-vs-emotion-차이">1. Tailwind VS Emotion 차이</h1>
<p>먼저 두 라이브러리가 <strong>&quot;어떤 문제&quot;</strong>를 <strong>&quot;어떻게 해결&quot;</strong>하는지를 비교해본다.</p>
<h2 id="1-1-tailwind">1-1. Tailwind</h2>
<h4 id="빌드-타임-유틸리티-클래스">&quot; 빌드 타임 유틸리티 클래스 &quot;</h4>
<p><strong>Tailwind</strong>는 <strong>미리 정의된 유틸리티 클래스를 HTML에 직접 붙이는 방식</strong>이다</p>
<blockquote>
<p><strong>유틸리티(Utility)</strong>란?
하나의 목적만 수행하는 도구. 단일 기능에 집중된 작은 단위.
복잡한 일을 하는 게 아니라, 한 가지 일을 명확하게 처리하는 것이 목적이다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/b2ee4afd-0652-4b89-952c-5e871fcac564/image.png" alt="">
즉, <strong>Tailwind</strong>에서 <strong>유틸리티 클래스</strong>란 <strong>&#39;CSS 속성 하나&#39; == &#39;클래스 하나&#39;</strong>를 의미한다.</p>
<p><strong>Tailwind</strong>는 스타일을 <strong>빌드 타임</strong>에 처리한다.</p>
<blockquote>
<p><strong>빌드 타임</strong>이란?
<strong>&quot; 컴파일 → 빌드 → 런타임 &quot;</strong></p>
</blockquote>
<ul>
<li><strong>컴파일</strong> : 소스코드를 브라우저가 이해할 수 있는 형태로 변환 (TS → JS, JSX → JS)</li>
<li><strong>빌드</strong> : 컴파일 + 번들링 + 최적화까지 포함한 전체 과정 (<strong><code>npm run build</code></strong>)</li>
<li><strong>런타임</strong> : 브라우저에서 실제로 실행되는 시점</li>
</ul>
<p>브라우저가 페이지를 렌더링할 때 <strong>Tailwind가 하는 일은 없습니다.</strong>
런타임에 스타일을 계산하거나 삽입하지 않는다. </p>
<h4 id="즉-런타임-비용이-없다-tailwind의-가장-큰-장점이다">즉, 런타임 비용이 없다. Tailwind의 가장 큰 장점이다.</h4>
<h2 id="1-2-emotion">1-2. Emotion</h2>
<h4 id="런타임-css-in-js">&quot; 런타임 CSS-in-JS &quot;</h4>
<p><strong>Emotion</strong>은 <strong>JavaScript 안에서 스타일을 작성하고, 런타임에 CSS를 생성해 DOM에 삽입</strong>한다.</p>
<p>props를 받아서 그 <strong>값에 따라 스타일을 동적으로 계산한다.</strong>
이 계산은 컴포넌트가 렌더링될 때마다 발생한다.</p>
<h2 id="1-3-런타임-vs-빌드-타임">1-3. 런타임 VS 빌드 타임</h2>
<p>두 라이브러리를 표로 비교했다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/0f2ff574-1db1-43ac-bdad-cb14a53aad99/image.png" alt=""></p>
<h1 id="2-이원화-전략-기준선">2. 이원화 전략 기준선</h1>
<p>두 라이브러리의 특성을 파악했으니, 이제 어떤 스타일을 누가 담당할지 기준을 잡아봤다.</p>
<h2 id="2-1-tailwind-담당-영역">2-1. Tailwind 담당 영역</h2>
<blockquote>
<p><strong>&quot; 정적 레이아웃 &quot;</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/9affd201-af2e-4d72-a711-5fdf3eab4f51/image.png" alt=""></p>
<p><strong>Tailwind</strong>는 상태와 무관하게 고정된 스타일에 강하다.</p>
<ul>
<li><strong>페이지 레이아웃</strong> : <code>flex</code>, <code>grid</code>, <code>gap</code>, <code>padding</code> 등</li>
<li><strong>반응형 브레이크 포인트</strong> : <code>md:</code>, <code>lg:</code></li>
<li><strong>고정된 색상, 테두리, 그림자</strong></li>
<li><strong>spacing 시스템 기반의 여백</strong></li>
</ul>
<p>이 영역은 <strong>런타임에 바뀔 일이 없다.</strong>
<strong>Tailwind</strong>가 빌드 타임에 처리하고 끝내는게 최적이다.</p>
<h2 id="2-2-emotion-담당-영역상태-기반-동적-스타일링">2-2. Emotion 담당 영역:상태 기반 동적 스타일링</h2>
<blockquote>
<p><strong>&quot; 상태 기반 동적 스타일링 &quot;</strong></p>
</blockquote>
<p><strong>Emotion</strong>은 JS 상태가 스타일에 직접 영향을 주는 영역에서 진가를 발휘한다고 생각한다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/edf74896-bd5a-4ca2-a75c-ef9787bf5a84/image.png" alt=""></p>
<ul>
<li><strong>상태에 따라 색상, 크기, 형태가 달라지는 컴포넌트</strong></li>
<li><strong>디자인 토큰 기반 theme 연동</strong></li>
<li><strong>여러 variant를 객체로 관리하는 UI 컴포넌트</strong></li>
<li>**인터랙션(hover, focus)이 JS 상태와 연동되는 경우</li>
<li>*</li>
</ul>
<h2 id="2-3-경계가-모호한-케이스">2-3. 경계가 모호한 케이스</h2>
<blockquote>
<p><strong>&quot; 어떤 라이브러리로 구현해야할까? &quot;</strong></p>
</blockquote>
<p>실제로 라이브러리 선택이 모호할 때, 나는 아래 기준으로 선정했다.</p>
<h4 id="1-js-변수상태-props가-스타일-값-자체에-직접-영향을-주는가">1. JS 변수(상태, props)가 스타일 값 자체에 직접 영향을 주는가?</h4>
<ul>
<li><strong>YES</strong> → Emotion</li>
<li><strong>No</strong> → Tailwind <em>( 단순히 클래스 2개 토글 수준 )</em><h4 id="2-variant가-3개-이상이거나-앞으로-늘어날-가능성이-있는가">2. variant가 3개 이상이거나 앞으로 늘어날 가능성이 있는가?</h4>
: Tailwind의 조건부 클래스 조합은 variant가 늘어날수록 관리가 어려움 <strong>→ Emotion</strong><h4 id="3-테마-토큰디자인-시스템-변수을-참조해야하는가">3. 테마 토큰(디자인 시스템 변수)을 참조해야하는가?</h4>
</li>
<li><em>→ Emotion*</em><h4 id="4-스타일을-별도-파일로-분리하여-관리하는가">4. 스타일을 별도 파일로 분리하여 관리하는가?</h4>
<em>(ui.tsx / style.ts / type.ts로 나누고 page에서 조합하는 구조)</em></li>
<li><strong>YES</strong> → Emotion <em>(style.ts에서 styled component로 관리 )</em></li>
<li><strong>No</strong> → Tailwind <em>( 마크업 안에서 바로 처리 )</em></li>
</ul>
<p>단순 토글 정도라면 <strong>Tailwind</strong>의 조건부 클래스로 처리해도 충분하다.
복잡도가 올라가는 순간 <strong>Emotion</strong>으로 가져오는 것이 유지보수에 유리하다 생각한다.</p>
<h1 id="3-실제-예시">3. 실제 예시</h1>
<p>실제로 내가 어떻게 사용했는지 코드를 살짝 공개해보겠다.</p>
<h2 id="3-1-tailwind로-레이아웃-작업">3-1. Tailwind로 레이아웃 작업</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/de7389e1-b54e-4b16-8020-01ad4408b9d5/image.png" alt=""></p>
<p>레이아웃 골격은 <strong>Tailwind</strong>로 빠르게 잡았다.
별도의 파일 없이 마크업 안에서 즉시 확인이 가능하다.</p>
<h2 id="3-2-emotion으로-상태-스타일-작업">3-2. Emotion으로 상태 스타일 작업</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/249253c3-d0b7-4570-b4f9-b5f5a72be6d1/image.png" alt="">
variant가 늘어나도 토큰만 추가하면 된다. 
만약 이걸 <strong>Tailwind</strong> 조건부 클래스로 구현했다면, className 문자열 복잡도가 빠르게 증가했을 것이다.</p>
<h2 id="3-3-같은-컴포넌트-안에서-공존">3-3. 같은 컴포넌트 안에서 공존</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/6c224746-e789-49d6-984f-e4b6266414ab/image.png" alt="">
<strong>Tailwind</strong>와 <strong>Emotion</strong>이 같은 파일 안에 있다.
처음에는 조금 어색했지만, 
고정된 것은 className, 달라지는 것은 emotion이라는 규칙을 잡으면 읽기에 어렵지 않다.</p>
<h1 id="4이-전략의-트레이드오프">4.이 전략의 트레이드오프</h1>
<p>저번 프로젝트에서는 빠르고 동적인 스타일 구현에만 초점을 두고 구현했지만, 앞으로는 아래 요소들을 고려하여 선택을 진행하려고 한다.</p>
<h2 id="4-1-번들런타임-비용">4-1. 번들&amp;런타임 비용</h2>
<p><strong>Tailwind</strong>의 가장 큰 장점 중 하나는 <strong>런타임 비용이 0(Zero)</strong>라는 점이다.
반면, <strong>Emotion</strong>은 컴포넌트가 렌더링될 때마다 스타일을 직렬화하는 <strong>비용이 발생</strong>한다.</p>
<p>두 라이브러리를 함께 쓰면 <strong>Tailwind</strong>가 커버하는 영역에서의 런타임 절약은 의미가 있다. 
다만 <strong>Emotion</strong> 쪽의 런타임 비용은 그대로 남는다.</p>
<p>그러므로, 빠른 구현과 반응형 레이아웃이라는 이점만을 챙기고 싶다면 이번 이원화 전략은 좋은 선택이지만, 런타임 비용 최적화가 필요한 상황이라면 필요 없는 전략이다.</p>
<h2 id="4-2-경계가-흐려질-때-생기는-일">4-2. 경계가 흐려질 때 생기는 일</h2>
<p>초반엔 명확한 경계_( 정적 레이아웃 VS 상태 기반 동적 컴포넌트 )_가 존재했지만, 
점점 경계가 모호해지는 순간들이 발생할 수 있다. </p>
<p>예를 들면 아래와 같은 순간들이다.</p>
<pre><code>const Button = styled.button`
  display: flex;        // ← 이건 Tailwind로 가야 하지 않았나?
  align-items: center;  // ← 이것도
  gap: 8px;             // ← 이것도
  background: ${({ isActive }) =&gt; isActive ? &#39;#1BCBA3&#39; : &#39;#e5e7eb&#39;};
`;</code></pre><p><strong>레이아웃 스타일</strong> _( flex, grid, gap, padding )_이 <strong>emotion안으로 침범하는 순간</strong>들이 생긴다.
이러한 순간이 점점 많아진다면, 결국 두 라이브러리의 역할 구분이 섞이고, 이원화의 의미가 없어진다.</p>
<p>해결책으로는 명확한 기준을 문서화하는 것을 추천한다.</p>
<p>나는 아래와 같은 기준을 세우고 작업을 진행했었다.
<strong>뷰포트 조건(반응형)에 따른 스타일 변화는 Tailwind,</strong> 
<strong>JS 상태에 따른 스타일 변화(state, props)는 Emotion</strong>을 선택하였다.</p>
<h2 id="4-3-ssr-환경에서의-주의점nextjs-기준">4-3. SSR 환경에서의 주의점(Next.js 기준)</h2>
<p><strong>Tailwind</strong>는 SSR에서 아무런 추가 설정이 필요없다.
하지만, <strong>Emotion</strong>은 추가 설정이 필요하다. </p>
<p>짧게 설명해보자면,
<strong>Next.js App Router 환경에서 Emotion을 별도 설정 없이 사용한다면 Hydration 불일치 에러가 발생할 수 있다.</strong>
서버에서 생성한 스타일과 클라이언트에서 생성한 스타일이 달라지기 때문이다.
자세한 설명은 <a href="https://velog.io/@heeflee_1310/04.-Next.js%EC%97%90%EC%84%9C-Emotion-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">이전에 작성한 글</a>을 참고하길 바란다.</p>
<p>만약, Next.js에서 이원화 전략을 선택한다면 설정이 제대로 되어있는지 확인해야한다.</p>
<h1 id="5-마치며">5. 마치며..</h1>
<p>이번 글을 작성하며 어떤 프로젝트에서 맞는 전략인가에 대해 깊게 고민해보았다.</p>
<p><strong>Emotion</strong>을 이미 써야할 이유가 있다면(동적 컴포넌트, 디자인 시스템), <strong>Tailwind</strong>를 레이아웃 전담으로 병용하는 건 합리적인 선택이다.
단, 경계를 명확하게 문서화해두고 사용해야한다.</p>
<p>만약 성능이 최우선인 프로젝트라면
<strong>Emotion</strong>의 런타임 비용을 구체적으로 측정한 뒤 판단해봐야 한다.
그리고 신규 팀원이 자주 합류한다면, 스타일링 온보딩 비용을 고려해봐야 할 것 같다.</p>
<p>처음엔 직관적으로 선택한 전략이었는데,
다시 들여다보니 나름 이론적 근거가 있었다.
물론 <strong>&quot;경계를 유지하는 비용&quot;</strong>이라는 약점도 분명하게 존재하지만, 
현재 참여 중인 프로젝트에서는 그 비용보다 동적 스타일의 유지보수 편의성이 더 중요한 고려사항이라 판단되어 다시한번 이원화 전략에 대해 선택했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프랙탈/FSD 파일 구조]]></title>
            <link>https://velog.io/@heeflee_1310/%ED%94%84%EB%9E%99%ED%83%88FSD-%ED%8C%8C%EC%9D%BC-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@heeflee_1310/%ED%94%84%EB%9E%99%ED%83%88FSD-%ED%8C%8C%EC%9D%BC-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Wed, 18 Mar 2026 11:10:00 GMT</pubDate>
            <description><![CDATA[<p>저는 개발자라면 코드만 잘 작성하면 될 줄 알았는데요?
개발 경험을 쌓아갈수록 구조 설계 능력이 중요해지는걸 뼈저리게 느꼈습니다...</p>
<p>특히 프로젝트 폴더구조 설계 능력이 생각 이상으로 더 중요한데요!
개발 전 구조 정책을 잘 설정해두지 않으면, 후반부로 갈수록 더 고생길이 열립니다..
<em>( 알고 싶지 않던...고생길 루트... )</em></p>
<p>거기다 Cursor, Copilot 같은 AI 코딩 도구를 쓰면서 느낀 건, 
<strong>구조가 잡혀있지 않으면 AI한테 좋은 명령도 못 내린다</strong>는 거였어요. 
설계 능력의 중요성이 두 배로 와닿았습니다...</p>
<p>사실 전 FSD를 실제 프로젝트에 도입해봤지만, 제대로 이해하지 못한 채로 프로젝트가 끝난 아쉬운 경험이 있습니다.. 
뇌가 딱 감을 잡기 직전에 흐름이 끊어져서, 날을 잡고 FSD를 다시 제대로 공부하다보니 비교 대상으로 프랙탈 구조도 알게 되었습니다!! 
<em>( 1+1? 나이스~ )</em></p>
<p>공부한 내용을 정리하지 않으면 머리에 남지 않을 것 같아 기록을 남깁니다!</p>
<blockquote>
<p><strong>시작하기 앞서..</strong>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/99a12f12-296a-4adf-8e49-7121f86d7813/image.png" alt="">
갑작스러운 문체 변경은... 이해해 주세요..두뇌 풀가동하면서 쓰느라 그렇답니다..</p>
</blockquote>
<h1 id="intro">Intro</h1>
<p>처음 <code>HTML</code>, <code>CSS</code>, <code>JavaScript</code>로 프로젝트를 처음 시작할 때는 파일 구조를 크게 신경쓰지 않았었다.
그리고 <code>React</code>를 처음 시작했을 때도, <code>public/</code>, <code>src/</code> 정도만 알고, 
그 안에 <code>components/</code>, <code>pages/</code>, <code>utils/</code> 폴더 몇 개 만들어 파일 네이밍 규칙을 지키며 코드 구현에만 열중하는 상황을 1년 넘게 보내왔다.</p>
<p>그런데 창업 프로젝트에 들어가게되며 프로젝트 규모가 점점 커졌고,
이런 상황이 자주 발생했다.</p>
<ul>
<li>&quot;이 컴포넌트, 어느 폴더에 넣어야 하지?&quot;</li>
<li>&quot;<code>components/</code> 폴더에 파일이 너무 많은데?&quot;</li>
<li>&quot;이 유틸함수는 어디서 쓰지? 지금도 쓰는건가?&quot;</li>
</ul>
<p>파일 구조는 단순한 취향 문제가 아니라,
나와 팀(과 ai)이 코드를 읽고, 찾고, 유지하는 속도에 직접적인 영향을 미친다는 것을 느끼게 되었다. </p>
<p>이 글은 먼저 <strong>프랙탈 구조</strong>를 살펴본 뒤, 
한계를 극복하기 위해 등장한 <strong>FSD(Feature-Sliced Design)</strong>로 흘러가보겠다.</p>
<h1 id="1-프랙탈-구조fractal-structure">1. 프랙탈 구조(Fractal Structure)</h1>
<h2 id="1-1-프랙탈fractal이란">1-1. 프랙탈(Fractal)이란?</h2>
<blockquote>
<p><strong>작은 구조가 전체 구조와 닮은 형태로 끝없이 되풀이되는 구조</strong>이다.</p>
</blockquote>
<p>글로 풀어서 작성해본다면,
❶ 자신의 <strong><code>작은 부분</code></strong>에 <strong>자신과 닮은 모습</strong>이 나타나고 
❷ 그 안의 <strong><code>작은 부분</code></strong>에도 <strong>자신과 닮은 모습</strong>이 <strong>무한히 반복</strong>되어 나타나는 구조이다.</p>
<p>이걸 프론트엔드 구조로 다시 작성해본다면,
❶ A폴더의 작은 부분인 <strong><code>하위 폴더 B폴더</code></strong> 안에 <strong>A폴더와 유사한 구조</strong>가 나타나고 
❷ B폴더의 <strong><code>하위 폴더인 C폴더</code></strong>에도 <strong>동일한 구조가 반복</strong>되어 나타나는 형식을 말한다.</p>
<p>마지막으로 정의해보면,
폴더 구조에서 <strong>각 폴더가 동일한 내부 구조를 반복하는 구조이다.</strong></p>
<p>예를 들어, 
최상위 <code>src/</code> 폴더에 <code>components/</code>, <code>hooks/</code>, <code>utils/</code>가 있다면,
<code>feature</code> 폴더 안에도 동일하게 <code>components/</code>, <code>hooks/</code>, <code>utils/</code>가 존재한다.</p>
<h2 id="1-2-프랙탈-구조의-핵심-규칙">1-2. 프랙탈 구조의 핵심 규칙</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/fd3264ef-a571-42aa-90ab-61c06d5048c1/image.png" alt=""></p>
<h3 id="1-기능-단위로-폴더를-나눈다">1. 기능 단위로 폴더를 나눈다.</h3>
<blockquote>
<p>파일 타입(<code>components</code>, <code>hooks</code>)이 아닌, <strong>기능(<code>auth</code>, <code>dashboard</code>)</strong>이 최상위 분류 기준이다.</p>
</blockquote>
<blockquote>
<p><strong>참고</strong>
이미지 속 <code>📁auth/</code>, <code>📁dashboard/</code> 폴더를 보면 <strong>Lv.1</strong>에서 반복된 요소가 <strong>Lv.3</strong>에서 다시 반복되는 걸 확인할 수 있다.<br />
이런 성질을 <strong>자기 유사성(Self-similarity)</strong>이라고 한다.</p>
</blockquote>
<h3 id="2-각-기능-폴더는-독립적이다">2. 각 기능 폴더는 독립적이다.</h3>
<blockquote>
<p> 한 기능을 삭제할 때 해당 폴더만 지우면 된다.</p>
</blockquote>
<h3 id="3-공통-코드는-최상위에-둔다">3. 공통 코드는 최상위에 둔다</h3>
<blockquote>
<p>여러 기능에서 공유되는 코드는<code>src/</code> 바로 아래에 위치한다.</p>
</blockquote>
<h2 id="1-3-프랙탈-구조의-장점">1-3. 프랙탈 구조의 장점</h2>
<h4 id="1-직관적이다">1. 직관적이다.</h4>
<p>기능 이름으로 폴더를 찾으면 되기 때문에 진입장벽이 낮다.
<em>ex. <code>📁auth/</code>, <code>📁dashboard/</code></em></p>
<h4 id="2-로컬-응집성이-높다">2. 로컬 응집성이 높다.</h4>
<p>관련 파일이 한 폴더에 모여 있어 맥락을 파악하기 쉽다.</p>
<p>여기서, <strong>응집성</strong>이란 뭘까?</p>
<blockquote>
<p><strong>응집성(응집도/Cohesion)란?</strong> <br/>
_<strong>&quot; 연관된 코드가 얼마나 한 곳(폴더)에 모여 있는가 &quot;</strong>_를 나타냅니다.
응집도가 높을 수록 관련 코드를 찾기 쉽고, 수정할 때 영향 범위도 예측하기 쉽습니다.<br/>
첨부한 이미지 기반으로 설명하자면, 
로그인 관련 <code>components</code>, <code>hooks</code>, <code>utils</code>이 <code>📁auth/</code>폴더 하나에 모여있는 걸 응집도가 높다고 표현합니다.
<a href="https://frontend-fundamentals.com/code-quality/code/examples/code-directory.html">▶ 참고 링크</a></p>
</blockquote>
<h4 id="3-독립성이-좋다">3. 독립성이 좋다.</h4>
<p>기능 폴더를 통째로 복사하거나 삭제할 수 있다.</p>
<h4 id="4-별도의-학습이-필요-없다">4. 별도의 학습이 필요 없다.</h4>
<p>구조 자체가 설명적이다.</p>
<h2 id="1-4-프랙탈-구조의-단점">1-4. 프랙탈 구조의 단점</h2>
<h4 id="1-기능-간-참조를-구조적으로-제한할-수-없다">1. 기능 간 참조를 구조적으로 제한할 수 없다</h4>
<p><code>📁auth/</code>가 <code>📁dashboard/</code>를 참조해도 구조상 막을 방법이 없다.</p>
<h4 id="2-공통-코드의-경계가-모호하다">2. 공통 코드의 경계가 모호하다.</h4>
<p><em>&quot;이 코드, 공통으로 올려야할지, 기능 폴더에 둬야 할지&quot;</em> 결정하기 어려워진다.</p>
<h4 id="3-규모가-커지면-혼란스러워-진다">3. 규모가 커지면 혼란스러워 진다.</h4>
<p>기능이 20개를 넘어가면 <code>📁features/</code>폴더 자체가 복잡해진다.</p>
<h4 id="4-팀마다-해석이-다르다">4. 팀마다 해석이 다르다.</h4>
<p>명확한 표준이 없어 팀원마다 구조를 다르게 적용할 수 있다.
이를 보완하기 위해 팀 내 파일 구조 컨벤션이 필요하거나, 구조 자체가 규칙을 강제하는 방식을 도입하게 된다.</p>
<h2 id="1-5-프랙탈-구조의-문제-상황">1-5. 프랙탈 구조의 문제 상황</h2>
<p>프랙탈 구조는 소규모 프로젝트에서 잘 작동한다.
그런데 팀이 커지고 기능이 늘어나면 두 가지 문제가 반복적으로 등장한다.</p>
<h4 id="1-의존성-방향이-뒤섞인다">1. 의존성 방향이 뒤섞인다.</h4>
<p>A 기능이 B를 참조하고, B가 다시 A를 참조하는 <strong>순환 참조가 일어나게 된다.</strong></p>
<h4 id="2-코드의-위치-결정이-주관적이다">2. 코드의 위치 결정이 주관적이다.</h4>
<p>개발자마다 파일을 다른 위치에 넣는다..
<em>( 파일 구조 컨벤션이 없다면, 일어나는 아찔한 상황.. )</em></p>
<p>이를 <strong>FSD의 레이어(Layer)개념</strong>으로 해결할 수 있다.</p>
<h1 id="2-fsd-구조feature-sliced-design">2. FSD 구조(Feature-Sliced Design)</h1>
<blockquote>
<p><strong>레이어를 정의</strong>하고, <strong>상위 레이어만 하위 레이어를 참조할 수 있게 강제</strong>한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/14fc3adb-c9b1-453d-8cf3-328813c1cf20/image.png" alt=""></p>
<h2 id="2-1-레이어-설명">2-1. 레이어 설명</h2>
<p>FSD는 코드를 <strong>주로 6개의 레이어(Layers)</strong>로 나눈다.
위로 갈 수록 &quot;더 구체적인 기능&quot;, 아래로 갈수록 &quot;더 범용적인 코드&quot;이다.</p>
<ul>
<li><strong>app</strong> : 앱 초기화, 라우팅 설정, 전역 Provider</li>
<li><strong>pages</strong> : 라우트에 대응하는 페이지 컴포넌트</li>
<li><strong>widgets</strong> : 여러 features를 조합한 독립적인 UI 블록</li>
<li><strong>features</strong> : 사용자 시나리오(행동) 단위의 기능 (로그인, 장바구니 담기 등)</li>
<li><strong>entities</strong> : 비즈니스 도메인 단위(User, Product, Order 등). 데이터 모델</li>
<li><strong>shared</strong> : 재사용 가능한 UI 컴포넌트, 유틸, API 클라이언트</li>
</ul>
<blockquote>
<p><strong><em>참고</em></strong>
사실 레이어는 7개이다.
app 다음에 <code>processes</code>가 있지만, 실제로 잘 사용되지는 않는다.</p>
</blockquote>
<h2 id="2-2-레이어의-참조-규칙">2-2. 레이어의 참조 규칙</h2>
<blockquote>
<p>각 레이어는 자신보다 아래 레이어만 참조할 수 있다.</p>
</blockquote>
<h4 id="app"><code>app</code></h4>
<ul>
<li>참조 가능 : <code>pages</code>, <code>widgets</code>, <code>features</code>, <code>entities</code>, <code>shared</code></li>
<li>참조 불가능 : X<h4 id="pages"><code>pages</code></h4>
</li>
<li>참조 가능 : <code>widgets</code>, <code>features</code>, <code>entities</code>, <code>shared</code></li>
<li>참조 불가능 : <code>app</code><h4 id="widgets"><code>widgets</code></h4>
</li>
<li>참조 가능 : <code>features</code>, <code>entities</code>, <code>shared</code></li>
<li>참조 불가능 : <code>app</code>, <code>pages</code><h4 id="features"><code>features</code></h4>
</li>
<li>참조 가능 : <code>entities</code>, <code>shared</code></li>
<li>참조 불가능 : <code>app</code>, <code>pages</code>, <code>widgets</code><h4 id="entities"><code>entities</code></h4>
</li>
<li>참조 가능 : <code>shared</code></li>
<li>참조 불가능 : <code>app</code>, <code>pages</code>, <code>widgets</code>, <code>features</code><h4 id="shared"><code>shared</code></h4>
</li>
<li>참조 가능 : X</li>
<li>참조 불가능 : <code>app</code>, <code>pages</code>, <code>widgets</code>, <code>features</code>, <code>entities</code>, <code>shared</code></li>
</ul>
<h2 id="2-3-레이어의-핵심-규칙">2-3. 레이어의 핵심 규칙</h2>
<h3 id="1-단방향-의존성unidirectional-dependencies">1. 단방향 의존성(Unidirectional Dependencies)</h3>
<p>레이어 간 참조는 항상 위에서 아래 방향으로만 흐른다.
바로 <strong><code>2-2. 참조 규칙</code></strong> 기반으로만 가능하다.</p>
<h3 id="2-slice와-segment">2. Slice와 Segment</h3>
<p>레이어 안에서는 2가지 단위가 있다.</p>
<ul>
<li><strong>Slice</strong> : 각 레이어 안에서 코드를 나누는 단위</li>
<li><strong>Segment</strong> : <strong>slice</strong> 안에서 역할별로 나누는 단위</li>
</ul>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/f637c4a0-0499-4b1e-97f0-f3efb6b4945b/image.png" alt="">
사진과 같이 단위에 맞게 자르면 된다.
추가적으로 설명을 더 남겨보자면 아래와 같다.</p>
<pre><code>📁features : 레이어
├📁auth : 기능 단위 슬라이스 (Slice)
│   ├ 📁ui : UI 컴포넌트 시그먼트 (Segment)
│   ├ 📁model : 상태, 비즈니스 로직 시그먼트 (Segment)
│   └ 📁api : API 호출 시그먼트 (Segment)</code></pre><p><strong>슬라이스(Slice)</strong>은 프로젝트별 <strong>네이밍 컨벤션</strong>이 필요하다.
Slice 네이밍은 도메인(명사)기준이 표준이므로, 프로젝트 컨벤션에 따라 자유롭게 정하되, 
시그먼트 이름은 <strong>FSD 표준 명칭(ui, model, api, lib, config)</strong>을 따르는 것을 권장한다.</p>
<blockquote>
<p><strong>FSD 사용하는 실제 프로젝트 구조 **
기능 : post 생성 / post 삭제<br/>
📁 **features/</strong>
├── 📁 <strong>post-create/</strong>          : Post 생성
│   ├── 📁 ui/               : 생성에 필요한 UI 컴포넌트
│   ├── 📁 model/            : Post 생성 관련 상태 관리, 비즈니스 로직
│   └── 📁 api/              : Post 생성 요청 (POST)
└── 📁 <strong>post-delete/</strong>          : Post 삭제
│   ├── 📁 ui/               : 삭제에 필요한 UI 컴포넌트
│   ├── 📁 model/            : Post 삭제 관련 상태 관리, 비즈니스 로직
│   └── 📁 api/              : Post 삭제 요청 (DELETE)
📁 <strong>entities/</strong>
└── 📁 post/
│   ├── 📁 ui/               : PostCard, PostPreview 등 순수 UI
│   ├── 📁 model/            : Post 타입 정의
│   └── 📁 api/              : Post 데이터 조회 (GET)</p>
</blockquote>
<h3 id="3-public-apiindexts">3. Public API(index.ts)</h3>
<p>각 slice는 <code>index.ts</code>를 통해 <strong>외부에 노출할 코드를 명시적으로 선언</strong>한다.
외부에서는 slice 내부 파일을 직접 참조하지 않고, <strong>반드시 <code>index.ts</code>를 통해 접근한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/0e9e42bb-dff8-44c9-924b-f5d8d0545359/image.png" alt=""></p>
<h2 id="2-4-fsd-구조의-장점">2-4. FSD 구조의 장점</h2>
<ul>
<li><strong>의존성이 명확하다.</strong> 레이어 규칙 덕분에 순환 참조가 구조적으로 불가능하다.</li>
<li><strong>코드 위치 결정이 객관적이다.</strong> &quot; 이 코드는 어느 레이어인가? &quot; 위치가 결정된다.</li>
<li><strong>대규모 팀에서 강하다.</strong> 여러 팀이 서로 다른 레이어를 맡아 병렬로 작업할 수 있다.</li>
<li><strong>eslint 플러그인으로 자동 강제할 수 있다.</strong> <code>eslint-plugin-fsd</code></li>
</ul>
<h2 id="2-5-fsd-구조의-단점">2-5. FSD 구조의 단점</h2>
<ul>
<li><strong>학습 곡선이 있다.</strong> 6개 레이어와 각 규칙을 이해하는 데 시간이 필요하다.
<em>( 지금의 나에요.. )</em></li>
<li><strong>소규모 프로젝트엔 과하다.</strong> 기능이 5개 미만이면 구조가 오히려 복잡하게 느껴진다.</li>
<li><strong>초기 설정 비용이 있다.</strong> alias 설정, eslint 규칙 등 사전 설정이 필요하다.</li>
<li><strong>widgets 레이어가 모호하게 느껴질 수 있다.</strong> pages와 features의 중간 개념이라 처음엔 구분이 어렵다.</li>
</ul>
<hr>
<h1 id="3-프랙탈-→-fsd-왜-발전했는가">3. 프랙탈 → FSD, 왜 발전했는가??</h1>
<p>두 구조의 철학을 (ai가) 비교한 표를 먼저 봐보자.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/0914a8dc-4ed9-47f5-9fb9-ec9144a6c9c3/image.png" alt=""></p>
<p>각각의 성격은 알겠다. 
이제 어떤 상황에서 어떤 구조를 선택해야 할까?</p>
<h4 id="1-프랙탈-구조가-적합한-경우">1. 프랙탈 구조가 적합한 경우</h4>
<ul>
<li>혼자 개발하거나, 팀이 2~3명인 경우</li>
<li>MVP나 사이드 프로젝트처럼 빠른 초기 개발이 중요한 경우</li>
<li>기능이 10개 이하로 유지될 것으로 예상되는 경우<h4 id="2-fsd가-적합한-경우">2. FSD가 적합한 경우</h4>
</li>
<li>팀이 5명 이상이거나 앞으로 커질 예정인 경우</li>
<li>장기적으로 유지보수할 서비스인 경우</li>
<li>도메인이 복잡하고 기능 간 의존성 관리가 중요한 경우</li>
</ul>
<p>프랙탈이 FSD로 발전했다에서 간혹 프랙탈 구조가 틀렸다고 생각할 수 있지만,
프랙탈 구조가 틀린건 아니다!!</p>
<p>프랙탈은 <strong>&quot;파일을 어디에 둘 것인가&quot;</strong>라는 목적에 집중했고,
FSD는 한 발 더 나아가 <strong>&quot;코드가 서로 어떻게 참조해야 하는가&quot;</strong>라는 목적에도 집중했다.</p>
<hr>
<h1 id="4-마치며">4. 마치며..</h1>
<p>좋은 파일 구조란 &quot;현재 팀과 프로젝트 규모에 맞는 구조&quot;라 생각한다.</p>
<p>소규모로 시작할 때는 프랙탈로 빠르게 시작하고,
규모가 커지는 시점에 FSD 마이그레이션을 검토하는 것이 좋을 것 같다는 판단이다.
<em>( 하지만 전 소규모 플젝도 공부를 위해 FSD를 선택했어요..! 프론트 혼자일때의 장점이랄까요 후후.. )</em></p>
<p>그리고 AI_(Cursor, Claude Code, Codex 등)_를 사용하여 코드를 작성해본 결과, 
<strong>FSD가 AI에게도 좋은 선택</strong>이라 판단이 든다.</p>
<p>AI는 코드를 생성하는 속도는 빠르지만, 구조를 제대로 판단하지는 않는 것으로 보인다.
이전 FSD를 사용한 프로젝트에서 명확한 FSD 구조 설계 없이 진행하였더니, 점점 파일 구조가 붕괴되는 경험을 했었다.</p>
<p>요구사항에 대해 .md를 제대로 설정하지 않고, 너무 모호한 명령을 내려서 그럴수도 있지만, AI는 코드 생성 속도를 빠르게 높이는 것에 중점을 두고, 구조의 붕괴는 따로 요청 혹은 설정이 없다면 신경쓰지 않는다는 걸 알게되었다.</p>
<p>그래서 이번에는 FSD를 사람도, AI도 더 명확하게 이해할 수 있는 <strong>&quot;좌표계&quot;</strong>로 사용해보고자 한다.
<strong>AI</strong>는 <strong>기능 구현과 보일러플레이트</strong>를, 
<strong>사람</strong>은 <strong>설계와 리스크 검토</strong>를 담당하는 방식으로 진행해보고자 한다.
<em>( 제발 구조 붕괴 없이 잘 마무리되길...!!! )</em></p>
<p>결론은,
<strong>AI는 &quot;무엇을 만들지&quot; 를 도와주지만,
&quot;어디에 어떻게 놓을지&quot; 는 여전히 사람과 구조가 결정해야 한다고 생각한다.</strong></p>
<p>AI는 &quot;지금 잘 동작하는 코드&quot; 를 만들고, FSD는 &quot;나중에도 읽히는 구조&quot; 를 만든다.</p>
<blockquote>
<p><strong>참고</strong>
<a href="https://fsd.how/kr/docs/get-started/overview/">▶ 🔗 FSD 공식 문서</a>
<a href="https://frontend-fundamentals.com/code-quality/">▶ 🔗 토스 Frontend Fundamentals 공식 문서</a>
<a href="https://busan.greenart.co.kr/community/greenDesignNews_view?&amp;idx=1538#:~:text=%ED%94%84%EB%9E%99%ED%83%88%EC%9D%B4%EB%9E%80%20%EC%9E%91%EC%9D%80%20%EA%B5%AC%EC%A1%B0%EA%B0%80,%EC%89%BD%EA%B2%8C%20%EB%B0%9C%EA%B2%AC%ED%95%A0%20%EC%88%98%20%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4.">▶ 프랙탈 구조란?</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[04. Next.js에서 Emotion사용하기]]></title>
            <link>https://velog.io/@heeflee_1310/04.-Next.js%EC%97%90%EC%84%9C-Emotion-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@heeflee_1310/04.-Next.js%EC%97%90%EC%84%9C-Emotion-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 17 Mar 2026 09:11:59 GMT</pubDate>
            <description><![CDATA[<p><strong><code>CSR(Client-Side-Rendering)</code></strong> 환경에서 신나게 <code>Emotion</code>을 사용하다가,
<strong>Next.js</strong> <strong><code>SSR(Server-Side-Rendering)</code></strong> 환경에 아무 설정 없이 사용려고 하면 문제가 발생해서 띠용한 경험 있지 않으신가요?
<em>( 맞아요... 제 경험이에요... )</em></p>
<p>대부분은 SSR 환경에서 <strong>CSS-in-JS</strong>가 동작하는 방식이 원인입니다..
(나 자신!) 이제 SSR 환경의 작동 원리를 다시 이해해보자!</p>
<h1 id="ssr-환경에서-emotion의-문제점">SSR 환경에서 Emotion의 문제점</h1>
<h2 id="1-css-in-js의-ssr-동작-원리">1. CSS-in-JS의 SSR 동작 원리</h2>
<p><code>Emotion</code>은 기본적으로 <strong>런타임에 스타일을 생성한다.</strong></p>
<blockquote>
<p><strong>런타임</strong> : 컴파일 후 코드가 실제로 실행되는 순간/환경</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/c54f61f1-e175-4433-b45b-42ea0f92d8c2/image.png" alt=""></p>
<p>컴포넌트가 랜더링될 때 스타일을 계산하고, 해당 스타일을 <code>&lt;style&gt;</code> 태그로 <strong>DOM</strong>에 동적으로 삽입하는 방식이다.</p>
<p><code>CSR</code> 환경에서는 이 방식이 문제가 없다.
<strong>브라우저에서 <code>JS</code>가 실행되면서 스타일도 함께 삽입되기 때문이다.</strong></p>
<p>그런데,
<strong>SSR에선 문제가 발생한다.</strong>
서버에서는 <strong>HTML</strong> 문자열을 만들어 응답하는데, 
기본 설정에는 <code>Emotion</code>이 생성한 스타일이 해당 <strong>HTML</strong>에 포함되지 않는다.
<em>( 당연하다! <code>JS</code>가 실행되기 전이니 포함이 안된다! )</em></p>
<p>그 결과 브라우저는 스타일이 없는 <strong>HTML</strong>을 먼저 받아 화면에 그리고, 이후 <strong>JS</strong>가 실행되며 뒤늦게 스타일이 적용된다.</p>
<hr>
<h2 id="2-critical-css-누락-문제---fouc">2. Critical CSS 누락 문제 - FOUC</h2>
<p>위의 상황은 실제 사용자에게 <strong>&#39;스타일이 없는 날것의 HTML&#39;</strong>이 순간적으로 노출된다.
이를 <strong><code>FOUC(Flash of Unstyled Content)</code></strong>라고 한다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/fa8876bb-f473-4341-8237-99310b98ef56/image.png" alt=""> <em>( 완전...밤티 이미지... )</em></p>
<p>JS 번들이 로드되고 <strong>Hydration</strong>이 완료되기 전까지, 사용자는 레이아웃이 깨진 화면을 보게 된다. 
네트워크가 느릴수록 이 현상은 더 두드러진다.</p>
<p>그런데, <strong>Hydration</strong>? 얘는 또뭘까? </p>
<hr>
<h2 id="3-hydration">3. Hydration</h2>
<p><strong>SSR 환경에서 React가 동작하는 방식</strong>을 이해하려면 <code>Hydration</code> 개념이 필수다.</p>
<h3 id="3-1-hydration-개념-정의">3-1. Hydration 개념 정의</h3>
<p><em>&quot; <strong>React</strong>가 <strong><code>SSR로 생성된 HTML</code></strong>을 재사용하기 위해 <strong><code>서버에서 생성된 DOM</code></strong>과 <strong>클라이언트에서 렌더링한 결과</strong>가 일치하는지 검증하는 과정 &quot;</em></p>
<p><strong>SSR</strong>은 서버에서 완성된 <strong>HTML</strong>을 만들어 브라우저에 전달한다. 
덕분에 사용자는 <strong>JS</strong>가 실행되기 전에도 화면을 볼 수 있다.
그런데, 이 <strong>HTML</strong>은 정적인 문자열일 뿐이다. 버튼을 눌러도 반응이 없고, 상태도 없다.</p>
<p>이후 <strong>JS 번들</strong>이 로드되면 <strong>React</strong>가 개입한다.
<strong>React</strong>는 <strong><code>서버에서 받은 HTML</code></strong>을 버리고 새로 그리는게 아니라, <strong>기존 DOM에 이벤트 핸들러와 상태를 붙여 App으로 만든다.</strong></p>
<p>이 과정을 수화, <strong>Hydration</strong>이라고 한다.</p>
<h3 id="3-2-hydration-성립-조건">3-2. Hydration 성립 조건</h3>
<p><em><strong>서버가 만든 HTML과 클라이언트 React가 렌더링하는 결과가 100% 일치해야 한다.</strong></em></p>
<p><strong>React</strong>는 둘을 비교하여 검증하기 때문이다.</p>
<p>조금이라도 다르면, 
<strong>React</strong>는 경고를 띄우거나, 아예 <strong><code>SSR 결과</code></strong>를 버리고 클라이언트에서 전체를 다시 렌더링한다. </p>
<p>이 전제 조건 때문에 <code>Emotion</code> 같은 <strong>CSS-in-JS</strong>에서 <strong><code>className</code> 불일치 문제</strong>가 발생한다.</p>
<hr>
<h2 id="4-hydration-불일치-문제">4. Hydration 불일치 문제</h2>
<p><strong>Hydration</strong> 불일치는 <strong>FOUC</strong>보다 더 심각한 문제이다.
<strong>FOUC</strong>는 UI가 추후 제대로 작동하지만, <strong>Hydration</strong>은 React가 SSR 결과물을 아예 폐기하고 클라이언트에서 전체를 다시 렌더링하여서 SSR을 선택하는 장점 자체가 사라지는 셈이다!</p>
<h3 id="에러-발생-원인">에러 발생 원인</h3>
<p><code>Emotion</code>은 스타일을 기반으로 <strong><code>className</code> 해시값을 런타임에 생성한다.</strong></p>
<blockquote>
<p><strong>해시값(Hash Value)</strong>
임의의 데이터를 고정된 길이의 문자열로 변환한 결과값
ex. <code>css-abc123</code> 에서 <strong>abc123</strong>이 해시값이다.</p>
</blockquote>
<p>별도의 설정이 없으면 서버와 클라이언트가 각자 독립적인 카운터로 해시를 계산하는데, <strong>렌더링 순서나 타이밍에 따라</strong> 동일한 스타일이더라도 <strong>서로 다른 className이 나올 수 있다.</strong></p>
<pre><code>    Warning: Prop `className` did not match.
    Server: &quot;css-abc123&quot; Client: &quot;css-xyz789&quot;</code></pre><p>이 에러가 콘솔에 출력되면,
(심한 경우)<strong>React</strong>가 <strong><code>SSR 결과물</code></strong>을 버리고 전체를 클라이언트에서 다시 렌더링한다.</p>
<p>원인은 크게 두 가지다.</p>
<h3 id="원인1-캐시-인스턴스-따로-증가">원인1. 캐시 인스턴스 따로 증가</h3>
<blockquote>
<p><strong><em>&quot;서버와 클라이언트가 각자 해시값을 만드는 문제&quot;</em></strong></p>
</blockquote>
<p><code>Emotion</code>은 스타일을 만들 때마다 <strong>고유한 이름표(className)</strong>을 붙인다.
<code>css-1</code>, <code>css-2</code>, <code>css-3</code> 이런식으로, 만들어진 순서대로 번호가 올라간다.</p>
<p>이 <strong>번호를 관리하는 장부</strong>가 바로 <strong>캐시(Cache)</strong>이다.
쉽게 말해 &quot;나는 지금까지 몇 번째 스타일까지 만들었다&quot;를 기억하는 메모장이다.</p>
<p>문제는 SSR 환경에서 캐시(메모장)가 <strong>서버용, 클라이언트용 따로따로 존재한다</strong>는 점이다.</p>
<p>클라이언트에서만 추가로 렌더링 되는 스타일이 끼어들면<em>(ex. &#39;use client&#39; 사용한 컴포넌트 스타일)</em> 번호가 어긋나기 시작한다. 
( 문제 발생 시작!! )</p>
<p>서버는 헤더에 <code>css-1</code>이라고 붙여 <strong><code>HTML</code></strong>을 만들어도, 클라이언트가 헤더에 <code>css-2</code>라고 알고 있는 상황이다. 
<strong>React</strong>가 둘을 비교하면, 에러를 낸다.</p>
<p>정리하면, 
공유된 하나의 캐시(메모장)를 같이 보지 않고, 각자 처음부터 번호를 새로 매기기 떄문에 생기는 문제다.</p>
<p>이를 해결하려면 
<strong>1. *<em>서버에서 만든 메모장의 상태를 
*</em>2.</strong> 클라이언트에 그대로 전달하여
*<em>3. *</em>클라이언트가 이어서 번호를 매기도록 만들어야 한다.</p>
<h3 id="원인2-emotionbabel-plugin-미적용">원인2. @emotion/babel-plugin 미적용</h3>
<blockquote>
<p><code>className</code>이 렌더링 순서에 따라 결정되는 숫자 해시라는 점이 근본 원인이다.</p>
</blockquote>
<p>플러그인 없이 빌드하면 클래스명에 <strong>안정적인 식별자</strong>가 붙지 않아 순서에만 의존하게 된다.
조건부 렌더링이나 동적 스타일이 있을 때 특히 불일치가 잘 발생한다.</p>
<hr>
<h2 id="5-hydration-에러-해결하기">5. Hydration 에러 해결하기</h2>
<p>원인이 두 가지였으니, 해결책도 그에 맞게 맞춰 작성해봅니당~</p>
<h3 id="5-1-emotioncache로-캐시-분리">5-1. <code>@emotion/cache</code>로 캐시 분리</h3>
<p><code>@emotion/cache</code>는 <code>Emotion</code>이 스타일을 저장하고 관리하는 인스턴스다.</p>
<p>기본적으로 전역 단일 인스턴스를 사용하는데,
<strong>SSR 환경</strong>에서는 <strong>요청마다 독립된 캐시 인스턴스를 생성</strong>해야한다.</p>
<p>그렇지 않으면 이전 요청의 스타일이 다음 요청에 <strong>오염되는 문제</strong>가 생긴다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/4a866270-61f0-4790-a03e-1531855c5773/image.png" alt=""></p>
<p>오염을 방지 하기 위해 <code>key</code> 옵션을 사용하면 된다.
<code>key</code>는 생성되는 <code>className</code>의 접두사(prefix)가 된다. (<code>css-abc123</code>에서 <code>css</code> 부분)
이로 인해 요청마다 안정되고 독립된 캐시 인스턴스를 생성할 수 있게 된다.</p>
<p>여러 캐시 인스턴스를 구분해야 하는 경우 <code>key</code>를 다르게 지정하면 된다.
<em>( 좋은 전략 방법! )</em></p>
<blockquote>
<p><strong>추가 설정</strong>
<strong>&quot; <code>prepend: true</code>로 스타일 삽입 순서 제어 하기!! &quot;</strong>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/99e92ef4-0890-468f-9b03-826810cc5b15/image.png" alt=""><code>prepend:true</code>를 설정하면 <code>Emotion</code>이 생성한 <code>&lt;style&gt;</code> 태그를 <code>&lt;head&gt;</code>의 맨 앞에 삽입한다.
이렇게 설정해두면 전역 CSS나 다른 라이브러리 스타일보다 Emotion의 스타일이 먼저 선언되어, 
*<em>나중에 선언된 스타일이 덮어쓸 수있는 구조가 만들어진다. *</em>
스타일 우선순위를 예측 가능하게 유지하는 데 도움이 된다!!!</p>
</blockquote>
<h3 id="5-2-emotionbabel-pluginswc로-클래스명-안정화">5-2. <code>@emotion/babel-plugin</code>/SWC로 클래스명 안정화</h3>
<p>Hydration 불일치의 근본 원인 중 하나는 <code>className</code>이 렌더링 순서에 따라 결정되는 숫자, 해시라는 점이다.</p>
<p>빌드 타임에 <strong>&#39;각 스타일에 컴포넌트 이름 + 파일 경로 기반의 안정적인 레이블&#39;</strong>을 추가한다.</p>
<p>서버와 클라이언트가 <code>동일한 컴포넌트</code>를 렌더링하면 항상 같은 <code>className</code>을 생성하게 되어 Hydration 불일치가 해소된다.</p>
<pre><code>    // 플러그인 없음 — 순서에 의존
    css-1, css-2, css-3 ...

    // 플러그인 적용 후 — 안정적인 식별자
    css-Button-abc123, css-Header-xyz789 ...</code></pre><h4 id="babel을-사용하는-경우">Babel을 사용하는 경우</h4>
<p><code>@emotion/babel-plugin</code>을 사용하면 이 문제를 해결할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/b4b8ae35-5f9b-42b5-ad35-80fd5a366089/image.png" alt=""></p>
<h4 id="nextjs에서-swc-컴파일러를-사용하는-경우"><strong>Next.js</strong>에서 <strong>SWC 컴파일러</strong>를 사용하는 경우</h4>
<p>Babel 대신 <strong><code>next.config.js</code></strong>에서 설정한다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/667fd51c-a17a-404b-a908-50451e829ae0/image.png" alt=""></p>
<blockquote>
<p><strong>SWC 컴파일러란?</strong>
<strong>컴파일러</strong> : 사람이 작성한 코드를 브라우저가 읽을 수 있게 변환해주는 도구<br/>
우리가 작성한 코드(TypeScript,.jsx, .tsx)는 브라우저가 바로 이해하지 못한다. 
브라우저는 <strong>순수한 JavaScript</strong>만 읽을 수 있기 때문에, 작성한 코드를 <strong>브라우저가 읽을 수 있는 JavaScript로 변환하는 과정이 필요하다.</strong> 
변환을 해주는 도구를 <strong>컴파일러</strong> 라고 부른다.
기존에는 이 역할을 <strong>Babel</strong>이 담당했다.
그런데, Babel은 JavaScript로 만들어져 있어 변환 속도가 느리다는 단점이 있다.<br/>
<strong><code>SWC</code></strong>는 이 Babel을 대체하기 위해 만들어진 컴파일러이다.
Rust로 작성되어 있어 Babel보다 훨씬 빠르다.
Next.js 12부터 기본 컴파일러로 채택됬다.<br/>
<code>@emotion/babel-plugin</code>은 말그대로 Babel 전용 플러그인이다.
프로젝트가 SWC를 사용하고 있으면, 이 플러그인을 그대로 쓸 수 없고,
대신 next.config.js에서 SWC용 Emotion 옵션을 따로 설정해줘야 한다.</p>
</blockquote>
<h3 id="5-3-app-router--useserverinsertedhtml로-스타일-동기화">5-3. App Router : useServerInsertedHTML로 스타일 동기화</h3>
<p><strong>App Router</strong>에서는 <code>_document.tsx</code> 방식을 사용할 수 없다.
대신 React 18에서 추가된 <code>useServerInsertedHTML</code> 훅을 활용한다.</p>
<blockquote>
<p><strong><code>useServerInsertedHTML</code></strong>
: 서버 렌더링 중 생성된 스타일을 <strong>HTML</strong>에 직접 삽입할 수 있게 해준다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/13866d9f-b22c-461d-8724-5af12da9adea/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/7f01b245-3ac7-49cf-bd77-c703a021030f/image.png" alt=""></p>
<p>흐름을 정리하면 아래와 같다.</p>
<blockquote>
<ol>
<li>서버 렌더링 시작</li>
<li><code>EmotionCacheProvider</code>가 캐시 인스턴스 생성</li>
<li>컴포넌트 트리 렌더링 ( 스타일이 cache.inserted에 누적 )</li>
<li><code>useServerInsertedHTML</code> 콜백 실행</li>
<li>누적된 스타일을 <code>&lt;style&gt;</code> 태그로 HTML에 삽입</li>
<li>브라우저에 스타일이 포함된 <strong>HTML</strong> 전달</li>
<li>Hydration 시 서버/클라이언트 className 일치</li>
</ol>
</blockquote>
<hr>
<h2 id="6-ssr--emotion-설정-순서">6. SSR + Emotion 설정 순서</h2>
<p>일단 심호흡 한번 하자...
이 글을 정리하며 당시 에러 상황이 생각나서 너무 호흡이 딸렸다..</p>
<p>다시는 이런 아찔상황을 겪지 않기 위해 아래 순서대로 설정을 진행하는 걸 잊지 말자.
<em>( 컨시컨브 편하게 하기 위해 이미지가 아닌 점.. 참고 부탁드립니다..^0^~ )</em></p>
<h3 id="6-1-패키지-설치">6-1. 패키지 설치</h3>
<pre><code>npm install @emotion/react @emotion/styled @emotion/cache @emotion/server</code></pre><h3 id="6-2-컴파일러-설정---swcnextjs-기본">6-2. 컴파일러 설정 - SWC(Next.js 기본)</h3>
<pre><code>// next.config.js
const nextConfig = {
    compiler: {
        emotion:true, // 클래스명 안정화
    },
};

module.exports = nextConfig;</code></pre><h3 id="6-3-캐시-생성-유틸-작성">6-3. 캐시 생성 유틸 작성</h3>
<pre><code>// lib/emotionCache.ts
import createCache from &#39;@emotion/cache&#39;;

export function createEmotionCache() {
    return createCache({ key:&#39;css&#39;, prepend:true });
}</code></pre><p>파일 주소는 임의로 저렇게 적어놨는데, 파일 구조 형식에 맞게 위치하면 됩니다~</p>
<h3 id="6-4-emotioncacheprovider-작성app-router">6-4. EmotionCacheProvider 작성(App Router)</h3>
<pre><code>// lib/EmotionCacheProvider.tsx
&#39;use client&#39;;

import { useServerInsertedHTML } from &#39;next/navigation&#39;;
import { CacheProvider } from &#39;@emotion/react&#39;;
import { createEmotionCache } from &#39;./emotionCache&#39;;
import { useState } from &#39;react&#39;;

export default function EmotionCacheProvider({ children} : { children:React.ReactNode}) {
    const [cache] = useState(()=&gt;{
        const c = createEmotionCache();
        c.compat = true;
        return c;
    });

    useServerInsertedHTML(()=&gt;(
        &lt;style 
            data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(&#39; &#39;)}`}
            dangerouslySetInnerHTML={{__html:Object.values(cache.inserted).join(&#39; &#39;)}}
        /&gt;
    ));

    return &lt;CacheProvider value={cache}&gt;{children}&lt;/CacheProvider&gt;
}</code></pre><h3 id="6-5-layouttsx-적용">6-5. layout.tsx 적용</h3>
<pre><code>// app/layout.tsx
import EmotionCacheProvider from &#39;@/lib/EmotionCacheProvider&#39;;

export default function RootLayout({ children }:{ children: React.ReactNode}) {
    return(
        &lt;html&gt;
            &lt;body&gt;
                &lt;EmotionCacheProvider&gt;{children}&lt;/EmotionCacheProvider&gt;
            &lt;/body&gt;
        &lt;/html&gt;
    );
}</code></pre><p>설정을 완료한다면, 아래와 같은 흐름으로 동작한다.
( 그래도 에러 해결이 안된다면...다시 검토해보자 )</p>
<blockquote>
<ol>
<li>서버 렌더링 시작</li>
<li>EmotionCacheProvider가 캐시 인스턴스 생성</li>
<li>컴포넌트 트리 렌더링 (스타일이 cache.inserted에 누적)</li>
<li>useServerInsertedHTML 콜백 실행</li>
<li>누적된 스타일을 <code>&lt;style&gt;</code> 태그로 HTML에 삽입</li>
<li>브라우저에 스타일이 포함된 HTML 전달</li>
<li>Hydration 시 서버/클라이언트 className 일치</li>
</ol>
</blockquote>
<hr>
<h2 id="7-흔히-겪는-hydration-에러-사례-및-디버깅">7. 흔히 겪는 Hydration 에러 사례 및 디버깅</h2>
<h3 id="7-1-emotionbabel-plugin-미적용">7-1. @emotion/babel-plugin 미적용</h3>
<p>가장 흔한 원인.
플러그인 없이 사용하면, <code>className</code>이 렌더링 순서에 의존하게 되어 조건부 렌더링이나 동적 스타일이 있을 때 불일치가 발생한다.</p>
<blockquote>
<p>→<code>@emotion/babel-plugin</code> or SWC <code>emotion</code> 옵션 적용으로 해결.</p>
</blockquote>
<h3 id="7-2-캐시-인스턴스가-요청-간-공유됨">7-2. 캐시 인스턴스가 요청 간 공유됨</h3>
<p><code>createEmotionCache()</code>를 모듈 최상단에서 한 번만 호출하면,
모든 요청이 같은 캐시를 공유한다.
이전 요청의 스타일이 섞여 들어온다.</p>
<blockquote>
<p>→ 요청마다 새 인스턴스를 생성하도록 수정.</p>
</blockquote>
<h3 id="7-3-app-router에서-useserverinsertedhtml-미적용">7-3. App Router에서 <code>useServerInsertedHTML</code> 미적용</h3>
<p><code>EmotionCacheProvider</code> 없이 사용하면 서버에서 생성된 스타일이 <strong>HTML</strong>에 포함되지 않아 <strong>Hydration</strong> 시 <code>className</code>은 일치해도 스타일이 없는 상태로 시작된다.
→ <code>6. SSR + Emotion</code> 설정 순서를 다시 적용해보자.</p>
<h3 id="디버깅-방법">디버깅 방법</h3>
<ol>
<li>브라우저 콘솔에서 <code>className did not match</code> 에러 확인</li>
<li>서버 응답 HTML(<code>view-source:</code>)에서 <code>&lt;style data-emotion&gt;</code> 태그 존재 여부 확인</li>
<li>React DevTools에서 Hydration 에러 컴포넌트 추적.</li>
</ol>
<hr>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/2b6da294-c705-49c0-b96a-a511a97be1ab/image.png" alt=""></p>
<p>오늘도 긴 글 읽어주셔서 정말 감사드립니다...
방전된 저는 이만..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[03. @emotion/styled 활용한 타이포 시스템 구현]]></title>
            <link>https://velog.io/@heeflee_1310/03.-emotionstyled-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%83%80%EC%9D%B4%ED%8F%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@heeflee_1310/03.-emotionstyled-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%83%80%EC%9D%B4%ED%8F%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 16 Mar 2026 14:37:05 GMT</pubDate>
            <description><![CDATA[<p>저는 디자인 시스템을 사용한 프로젝트를 5번 참여했는데요, 매번 타이포 시스템을 정의할 때마다 같은 과정을 반복했습니다.</p>
<blockquote>
<p><strong>피그마 dev 모드</strong> → <strong>복사</strong> → <strong>붙여넣기</strong> → <strong>또 복사</strong> → <strong>또 붙여넣기...</strong></p>
</blockquote>
<p>타이포그래피 컴포넌트가 20개라면, 이 과정을 20번 반복해야 했습니다.
결과물은 아래와 같은 구조였는데요.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/5af26fe1-b09d-48c0-b0c3-15fab08d7c6d/image.png" alt=""></p>
<blockquote>
<p>(왼쪽) 피그마 코드를 그대로 객체로 정의 / (오른쪽) 컴포넌트 오버라이딩 한 코드</p>
</blockquote>
<p>타이포 객체를 정의해두고, 실제 사용 컴포넌트에서는 <code>color</code> 같은 가변 요소만 <code>styled(컴포넌트)</code> 형식으로 오버라이딩해서 쓰는 방식이었습니다.</p>
<p><strong>ctrl+c, ctrl+v. **
행동 자체는 단순했지만, **유지보수</strong>가 문제였습니다. 
시스템 변경이 생기면 20개의 객체를 전부 찾아서 고쳐야 했고, 코드는 점점 더 지저분해졌습니다.</p>
<h4 id="-좀-더-효율적인-방식이-없을까-"><em>&#39; 좀 더 효율적인 방식이 없을까? &#39;</em></h4>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/7c3ec9c3-5dfe-4025-a7cb-35098653faeb/image.png" alt=""></p>
<p>그래서 직접 설계해봤습니다..!</p>
<blockquote>
<p>이번 구현 방식은 <code>@emotion/styled</code> 이론 지식 기반입니다!
공식 문서를 먼저 읽어 보신 뒤, 기록을 읽어주시면 더욱 이해가 잘 될거에요!
<a href="https://emotion.sh/docs/@emotion/styled"><strong>▶ 🔗 공식 문서 이동 링크</strong></a>
<a href="https://velog.io/@heeflee_1310/02.-emotionstyled-%EA%B8%B0%EC%B4%88"><strong>▶ 🔗 @emotion/styled 정리 글</strong></a></p>
</blockquote>
<h1 id="0-intro">0. intro</h1>
<p>먼저 <strong><em>&#39;공통적으로 사용되는 요소들은 <code>Token</code>으로 정의하고, 이를 기반으로 베이스를 만들어서 활용하자!&#39;</em></strong>로 시작했습니다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/9aa6a555-0819-474f-80c9-676424a9a122/image.png" alt="">
피그마 dev 모드로 전환시 타이포그라피 요소는 위와 같은 형태로 코드가 출력됩니다.
참여한 서비스 내 타이포 시스템은 아래와 같은 특징을 가지고 있었습니다.</p>
<blockquote>
<p><strong>1. 불변 요소(빨간 부분)</strong> : <code>font-family</code>, <code>font-style</code>
<strong>2. 가변 요소</strong> : <code>font-size</code>, <code>font-weight</code>, <code>font-height</code>, <code>color</code></p>
</blockquote>
<p>다음으로 저는 2가지 문제 상황을 해결해보고자 했습니다.</p>
<h2 id="0-1-문자열로-표현된-font-weight를-내부적으로-숫자로-풀어내자">0-1. 문자열로 표현된 font-weight를 내부적으로 숫자로 풀어내자.</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/0fe0ba9a-c328-4693-a898-43ef95ea3e0a/image.png" alt="">
퍼블리싱 작업 중에 간혹 디자인 시스템에 정의되지 않은 타이포 요소를 확인할 수 있었는데요!
그럴 경우 <code>font-weight</code> (초록색 부분)가 숫자값이 아닌 <strong>문자열</strong>로 표현이 되어 해당 시스템 요소의 <code>weight</code> 값이 헷갈리는 경우가 있었습니다.
이런 경우에<strong>_ &#39;리터럴로 작성하여도 내부적으로 자동 변환이 되면 편하겠다.&#39;_</strong> 라는 생각이 들었습니다. </p>
<h2 id="0-2-불변요소들은-외부로-노출되지-않게-표현해보자">0-2. 불변요소들은 외부로 노출되지 않게 표현해보자.</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/5b6e024f-7afc-4faf-8d15-5db0bd24a774/image.png" alt="">피그마의 <code>Variables</code> 표현 방식처럼 <strong><em>&#39; <code>color</code>를 제외한 가변요소(노랑색, 하늘색)만 표현하고, 그 외의 요소는 숨겨보자!&#39;</em></strong> 라는 목표로 삼고 디자인 시스템을 리팩토링했습니다.</p>
<p>이 두 가지 문제를 해결하기 위해 <code>@emotion/styled</code> 기반의 레이어 구조를 설계했습니다.
각 레이어가 다음 레이어의 재료가 되는 구조로, 아래 순서로 구현했어요.</p>
<blockquote>
<p><strong>원시값(Token) → 재사용 조각(Mixins) → 공통 기본값(Base) → 조합기(Builder) → 자동 생성기(Factory)</strong></p>
</blockquote>
<hr>
<h1 id="1-import">1. import</h1>
<blockquote>
<p><code>@emotion/styled</code> 패키지는 내부적으로 <code>@emotion/react</code>를 래핑하고 있어서, 2개의 패키지를 함께 설치 후 사용해주셔야 합니다! 
<a href="https://velog.io/@heeflee_1310/01.-Emotion%EC%9D%B4%EB%9E%80">▶🔗 관련 정리 글</a></p>
</blockquote>
<p>먼저 타이포 시스템 파일 상단에 <code>import</code>문을 작성했습니다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/15e9beab-1722-4968-adbf-6dc025bc282c/image.png" alt=""></p>
<h2 id="1-1-emotionstyled">1-1. @emotion/styled</h2>
<p><code>styled</code> 컴포넌트 자동 생성(<code>7. Factory</code>) 기능에 활용</p>
<h2 id="1-2-emotionreact">1-2. @emotion/react</h2>
<ul>
<li><code>css()</code> : 객체로 스타일을 정의</li>
<li><code>CSSObject</code> : <code>TypeScript</code> 환경에서 <code>Emotion</code>의 자동완성 및 타입 추론</li>
</ul>
<hr>
<h1 id="2-token">2. token</h1>
<p><em>&quot; 폰트 상수 정의 &quot;</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/7a4dc6c5-9b8d-4e85-abc1-3ad7025347e2/image.png" alt=""></p>
<p>시스템 전체에서 사용할 원시값인 <code>Token</code>을 정의했습니다.</p>
<h2 id="2-1-typofont">2-1. $typoFont</h2>
<p><code>$typoFont</code> 객체는 아래 2가지 목적을 가지고 있습니다.</p>
<blockquote>
<p><strong>1. family 정의</strong> : <code>font-family</code>를 한 곳에서 관리합니다.
<strong>2. weight 값 리터럴 맵핑</strong> : 피그마에서는 <code>font-weight</code>를 숫자가 아닌 문자열(Regular, SemiBold 등)으로 표기하는데요, 이를 보고 바로 코드에 적용 가능하기 위해 리터럴 타입으로 매핑했습니다.
( 0-1 문제 해결 !! )</p>
</blockquote>
<h2 id="2-2-lh">2-2. $lh</h2>
<p><code>$lh</code> 함수는 행간을 배수로 입력받아 <code>&#39;%문자열&#39;</code>로 변환해주는 유틸입니다.</p>
<blockquote>
<p><code>$lh(1.4)</code> → <code>&quot;140%&quot;</code> </p>
</blockquote>
<p>2글자 타이핑이라도 줄여보자는 마음으로 작성했는데, 만족스럽습니다!</p>
<h2 id="2-3-etc">2-3. etc</h2>
<blockquote>
<p><strong><code>$</code> prefix</strong>에 대해 <code>Sass</code> <strong><code>$</code> *<em>변수 선언 컨벤션에서 영감을 받아 붙여봤습니다.
문법적으로는 의미가 없지만, 코드를 보면 *</em>스타일 토큰</strong>이라는 것을 바로 인식할 수 있어서 꽤 만족스럽습니다!</p>
</blockquote>
<blockquote>
<p><code>as const</code>를 사용해 리터럴 타입으로 좁혀주었기 때문에, 컴파일 단계에서 잘못된 키 접근을 바로 잡아줍니다!</p>
</blockquote>
<hr>
<h1 id="3-mixins">3. Mixins</h1>
<p><em>&quot; 재사용 스타일 조각 &quot;</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/37966e1c-5fa9-498f-8ba0-dce571de3783/image.png" alt="">여러 곳에서 반복되는 스타일 묶음을 <code>clampLines()</code>함수로 추출하였습니다.</p>
<h2 id="3-1-clamplines">3-1. clampLines</h2>
<p>텍스트를 n줄까지만 보여주고, 초과하면 말줄임표(...)로 자르는 함수</p>
<ul>
<li><code>display: -webkit-box;</code> : 박스를 webkit flex box로 설정</li>
<li><code>-webkit-box-orient: vertical;</code> : 박스 방향을 세로로 설정</li>
<li><code>-webkit-line-clamp: ${n};</code> : n줄까지만 표시</li>
<li><code>overflow: hidden;</code> : n줄 초과 텍스트 숨김</li>
<li><code>text-overflow: ellipsis;</code> : 잘린 부분을 ...으로 표시</li>
</ul>
<p><code>clampLines(2)</code>를 호출하면 2줄 이상은 말줄임표 처리하는 CSS를 반환합니다. 
특정 타이포 컴포넌트에 <code>clamp</code> 옵션이 있을 때 이 믹스인이 자동으로 주입됩니다!
<em>( <code>5.Styled Builder</code> 참고 )</em></p>
<blockquote>
<p><code>clampLines</code>는 현재 구현에서 사용한 예시이며, 
  프로젝트 필요에 따라 믹스인 레이어에 자유롭게 함수를 추가해 확장할 수 있습니다!</p>
</blockquote>
<hr>
<h1 id="4-base">4. Base</h1>
<p><em>&quot; 공통 기본 스타일 &quot;</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/26df2cd1-c679-4d18-a12f-8341ec9c11df/image.png" alt=""></p>
<p>모든 타이포 컴포넌트에 공통으로 깔리는 베이스 스타일입니다.
<code>CSSObject</code> 타입으로 정의하여 <code>Object Style</code>의 <strong>자동완성</strong>과 <strong>타입 검사</strong>가 동작하게 했습니다!
<em>( <code>Object Style</code>의 장점! )</em></p>
<blockquote>
<p><code>BASE_TYPO</code>는 상수임을 명시하기 위해 <strong>UPPER_SNAKE_CASE</strong>로 작성했습니다.</p>
</blockquote>
<ul>
<li><strong>UPPER_SNAKE_CASE</strong> : 수정하지 않을 것임을 명시하는 &#39;네이밍 컨벤션&#39;</li>
</ul>
<h2 id="4-1-fontfamily">4-1. fontFamily</h2>
<p><code>2.Token</code>에서 정의한 토큰<code>$typoFont</code>을 참조합니다.</p>
<h2 id="4-2-폰트-렌더링-속성">4-2. 폰트 렌더링 속성</h2>
<p>폰트 렌더링을 부드럽게 처리하기 위해 아래 속성들을 추가했습니다.</p>
<blockquote>
<p>아래 두 속성은 함께 써야 <strong>Chrome</strong>, <strong>Safari</strong>, *<em>Firefox *</em>환경 모두에서 동일하게 부드러운 폰트 렌더링을 보장할 수 있어요.</p>
</blockquote>
<p><code>WebkitFontSmoothing: &#39;antialiased&#39;</code></p>
<ul>
<li>적용 환경 : macOS + <strong>Chrome/Safari</strong> (<strong>webkit</strong> 계열)</li>
<li>역할 : 폰트 렌더링 방식을 안티앨리어싱으로 설정</li>
<li>효과 : 폰트 외곽선을 부드럽게 처리해서 얇고 선명하게 보임</li>
</ul>
<p><code>MozOsxFontSmoothing: &#39;grayscale&#39;</code></p>
<ul>
<li>적용 환경 : macOS + <strong>Firefox</strong> (Moz 계열)</li>
<li>역할 : <strong>Firefox에서</strong> macOS 폰트 스무딩을 그레이스케일 방식으로 설정</li>
<li>효과 : <strong>webkit</strong>과 동일한 부드러운 렌더링 효과를 <strong>Firefox</strong>에서도 구현 </li>
</ul>
<p>그외 추가적으로 설정하고자 하는 요소들을 붙여줍니다!!</p>
<hr>
<h1 id="5-styled-builder">5. Styled Builder</h1>
<p>_&quot; 스타일 배열 조합기 &quot; _</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/3f12631d-655b-4dc0-a51e-7f4208809d60/image.png" alt=""><code>TypoConfig</code> 객체를 받아 스타일 배열을 반환하는 함수입니다.</p>
<h2 id="5-1-typoconfig">5-1. TypoConfig</h2>
<ul>
<li>필수값 : <code>size(string)</code>, <code>weight(number)</code>, <code>lh(string / line-height)</code></li>
<li>선택값 : <code>clamp(number / line-clamp)</code>, <code>extra(CSSObject)</code></li>
</ul>
<h2 id="5-2-styleof-">5-2. styleOf( )</h2>
<p><code>@emotion/styled</code>는 스타일 인자로 <strong>배열</strong>을 받을 수 있습니다. 
<strong>배열 내부를 순서대로 순회하며 병합해주고, null은 자동으로 무시해요.</strong></p>
<p><strong>객체 방식</strong>이라면 조건부 스타일을 추가할 때마다 <code>spread 연산자(...)</code>가 필요하지만, 
<strong>배열 방식</strong>은 조건부 결과를 요소로 넣기만 하면 <strong><code>Emotion</code>이 알아서 처리해줍니다!!!</strong> 
<em>( 최고다...!!!! )</em>
덕분에 <code>BASE_TYPO</code>, <code>개별 값</code>, <code>clamp</code>, <code>extra</code> 같은 스타일 레이어를 독립적으로 깔끔하게 조합할 수 있었습니다.</p>
<p><code>styleOf</code> 함수 흐름을 정리하자면, 다음과 같습니다.</p>
<blockquote>
<ol>
<li><code>styleOf</code> 함수가 <code>TypoConfig</code> 타입의 객체를 인자로 받아 구조 분해</li>
<li><code>BASE_TYPO</code>(공통 기본값)를 배열 첫 번째 요소로 추가</li>
<li><code>fontSize</code> / <code>fontWeight</code> / <code>lineHeight</code> 개별 값을 객체로 묶어 추가</li>
<li><code>clamp</code>가 있으면<code>clampLines(clamp)</code>, 없으면 <code>null</code> <strong>—</strong> 삼항 연산자로 조건부 추가</li>
<li><code>extra</code>가 있으면 그대로, 없으면 <code>null</code> <strong>—</strong> <code>??</code> 연산자로 조건부 추가</li>
</ol>
</blockquote>
<hr>
<h1 id="6-config-map">6. Config Map</h1>
<p><em>&quot; 타이포 스펙 명세 &quot;</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/0f83ce7c-3dd4-47ea-a88a-1394d9ddc6c1/image.png" alt=""></p>
<p>타입별 스펙을 한곳에 모아 정의하는 명세서입니다!</p>
<h2 id="6-1-typofontweightsb-참조">6-1. <code>$typoFont.weight.sb</code> 참조</h2>
<p>숫자 대신 토큰을 참조해서 의미를 명확하게 했습니다.</p>
<p>피그마에서 &quot;SemiBold&quot;를 확인하고 바로 $typoFont.weight.sb로 
작성할 수 있게 되어 0-1 문제가 해결됐습니다!</p>
<h2 id="6-2-가변-요소만-노출lh-유틸-size-">6-2 가변 요소만 노출(<code>$lh</code> 유틸, size )</h2>
<p>intro에서 세운 목표를 다시 떠올려볼게요.
&quot; color를 제외한 가변요소만 표현하고, 불변요소는 숨기자 &quot;</p>
<p>Config Map을 보면 size, weight, lh — 
타입별로 달라지는 가변요소만 명세되어 있어요.
font-family, font-style 같은 불변요소는 Config Map 어디에도 없습니다.
불변요소는 4. Base의 BASE_TYPO에서 처리했기 때문에
Config Map에서는 신경 쓸 필요가 없거든요!</p>
<p>color 역시 Config Map에 없어요.
사용하는 쪽에서 오버라이딩으로 처리하기 때문이에요.</p>
<p>결과적으로 Config Map은 
딱 &quot;바뀌는 것만&quot; 보이는 명세서가 됐습니다. 0-2 문제 해결!!!</p>
<h2 id="6-3-satisfies-recordstring-typoconfig">6-3. <code>satisfies Record&lt;string, TypoConfig&gt;</code></h2>
<p>각 항목이 <code>5-1. TypoConfig</code> 타입을 만족하는지 컴파일 단계에서 검사하며, 각 키의 정확한 타입 추론을 유지할 수 있습니다.</p>
<h2 id="6-4-extra">6-4. extra</h2>
<p><code>Caption</code>, <code>Button</code> 타입처럼 필요한 개별 속성을 추가할 수 있습니다.</p>
<blockquote>
<p>이미지에는 <code>flexShrink:0</code> 추가함!</p>
</blockquote>
<hr>
<h1 id="7-factory">7. Factory</h1>
<p><em>&quot; styled 컴포넌트 자동 생성 &quot;</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/e0efefc2-3b60-49b9-8f4c-a04f15356128/image.png" alt=""></p>
<p>Config Map을 순회하며 <code>styled.span</code> 컴포넌트를 자동 생성하는 함수입니다.</p>
<p>핵심은 <strong><code>styled.span(styleOf(map[key]))</code></strong> 한 줄이에요.</p>
<blockquote>
<ol>
<li><code>Config Map</code>의 각 키</li>
<li><code>styleOf( )</code>로 스타일 배열 생성</li>
<li><code>styled.span( )</code>에 주입</li>
<li><code>styled</code> 컴포넌트 반환</li>
</ol>
</blockquote>
<p>제네릭 <code>T</code>를 활용하여 반환 타입이 <code>Record&lt;keyof T, StyledSpan&gt;</code>으로 추론됩니다.
즉, <code>T</code>에 없는 키로 접근하면, <code>TypeScript</code>가 바로 에러를 반환해요!</p>
<h2 id="7-1-styledspan">7-1. StyledSpan</h2>
<p><code>type StyledSpan = ReturnType&lt;typeof styled.span&gt;;</code>
<code>styled.span</code>이 반환하는 타입을 추출해서 별도 타입으로 정의한 것입니다.
<code>ReturnType&lt;typeof styled.span&gt;</code>을 직접 쓰면 너무 길고 가독성이 떨어지기 때문에, <code>StyledSpan</code>이라는 이름으로 추출해서 재사용할 수 있게 했습니다.</p>
<blockquote>
<p>📝 용어 정리
<strong>ReturnType<T></strong> : TypeScript 유틸리티 타입으로, 함수 T의 반환 타입을 추출해줍니다.
<code>ReturnType&lt;typeof styled.span&gt;</code> → <code>styled.span()</code>이 반환하는 타입을 그대로 가져옴</p>
</blockquote>
<h2 id="7-2-createtypo">7-2. createTypo</h2>
<pre><code>function createTypo&lt;T extends Record&lt;string, TypoConfig&gt;&gt;(
  map: T
): Record&lt;keyof T, StyledSpan&gt;</code></pre><p>  <code>Config Map</code>을 받아 <code>styled.span</code> 컴포넌트를 자동으로 생성해주는 팩토리 함수입니다.
제네릭 T를 활용한 덕분에 두 가지 이점이 생겨요.</p>
<p><strong>1. 타입 안전성</strong>
T는 <code>Record&lt;string, TypoConfig&gt;</code>를 확장해야 하므로, <code>TypoConfig</code> 형태에 맞지 않는 값을 넘기면 컴파일 단계에서 바로 에러가 발생합니다.</p>
<p><strong>2. 반환 타입 자동 추론</strong>
반환 타입이 <code>Record&lt;keyof T, StyledSpan&gt;</code>으로 추론되기 때문에, T에 없는 키로 접근하면 <code>TypeScript</code>가 바로 잡아줘요.</p>
<pre><code>const T = createTypo(CONFIGS);

T.Headline1  // ✅ 정상
T.존재하지않는키  // ❌ TypeScript 에러</code></pre><p><code>6. Config Map</code>에서 정의한 <code>CONFIGS</code>를 인자로 넘기면,
각 키에 해당하는 <code>styled.span</code> 컴포넌트가 자동으로 생성됩니다.</p>
<hr>
<h1 id="8-build--named-export">8. Build / Named Export</h1>
<p><em>&quot; 외부 노출 &quot;</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/73b029d3-3751-482b-bf66-a02c283d65ea/image.png" alt=""></p>
<p>  마지막으로 <code>createTypo(CONFIGS)</code>를 실행해 <strong>전체 컴포넌트를 생성</strong>하고,
<code>Named Export</code>로 <strong>하나씩 내보냅니다.</strong></p>
<pre><code>import { Headline1, Body2 } from &#39;@/styles/typo&#39;;</code></pre><p>  <code>Named Export</code>를 사용하면 사용하는 쪽에서 <strong>필요한 컴포넌트만 골라서</strong> <code>import</code>할 수 있어요.
  <img src="https://velog.velcdn.com/images/heeflee_1310/post/a29ccdd2-2e4a-4449-8750-bb80f8a2c698/image.png" alt="">
야호!! 사용 방식은 그대로 유지하되, 내부 로직만 변경 성공이에요!!
마치 젠가 가운데 블럭을 뺀 것 같이..내부 로직만 수정 변경한게 너무 짜릿하지 않나요!<del>!</del>!</p>
<hr>
<h1 id="9-적용-예시-및-결과">9. 적용 예시 및 결과</h1>
<blockquote>
<p>프로젝트 보안상 실제 적용 화면 첨부가 어렵지만,
아래는 실제 사용 예시 코드입니다!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/b47fd0fb-ca7f-4652-950e-ea20ccec9661/image.png" alt=""></p>
<blockquote>
<p>(왼쪽) 타이포 컴포넌트를 오버라이딩해서 사용 / (오른쪽) 실제 사용 예시</p>
</blockquote>
<p>기존에 타이포 시스템을 사용하던 방식(export/import, 오버라이딩)은 그대로 유지하되,
타이포 시스템 구축 자체는 훨씬 효율적인 구조로 개선되었습니다!</p>
<h2 id="결과">결과</h2>
<p>이제 시스템 변경이 필요하면, <strong><code>6. Config Map</code></strong>와 <strong><code>8. export 문</code></strong>만 추가/수정하면 됩니다.</p>
<p>실제 커밋 기록을 보면, 1개 파일에서 <strong>304줄이 제거되고 213줄로 재작성</strong>되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/f7d9eb41-52ea-4b92-8898-f2561a48dd55/image.png" alt="">
약 100줄 이상의 코드를 줄이고, 재사용 가능한 구조를 만들 수 있었어요.
<em>( 코드양 약 33% 감소!! )</em></p>
<p>각 레이어별 역할이 분리되어 있어서, 폰트 자체를 변경하더라도
<strong>Token 한 줄만 고치면 전체에 반영됩니다!!</strong></p>
<p>언젠간 지금보다 더 나은 방식으로 발전시킬 수 있겠죠?
야호~ 이렇게 기분좋게 기록 마무리합니다~ ^0^~
<img src="https://velog.velcdn.com/images/heeflee_1310/post/a48e39ef-ff09-4ee2-ac02-cc32c2a98777/image.png" alt=""></p>
<blockquote>
<p><strong>🔗 참고</strong>
Emotion 공식 문서 : <a href="https://emotion.sh/docs/styled">https://emotion.sh/docs/styled</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[02. @emotion/styled 기초]]></title>
            <link>https://velog.io/@heeflee_1310/02.-emotionstyled-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@heeflee_1310/02.-emotionstyled-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Mon, 16 Mar 2026 14:22:54 GMT</pubDate>
            <description><![CDATA[<p>지난 글에서 <code>@emotion/styled</code>가 무엇인지, 어떤 패키지를 어떤 상황에 선택해야하는지를 얘기해봤는데요!!
이번 글은 실제로 <code>@emotion/styled</code>를 어떻게 사용하는지에 대해 집중적으로 다뤄보고자 합니다!!</p>
<blockquote>
<p>이번 글은 <code>@emotion/styled</code> 공식 문서 기반입니다!
기본 개념은 <a href="https://velog.io/@heeflee_1310/01.-Emotion%EC%9D%B4%EB%9E%80">▶ 🔗 1편</a>을 먼저 읽어 보신 뒤 읽어주시면 더욱 이해가 잘 될거에요!<br/>
<a href="https://emotion.sh/docs/styled">▶ 🔗 공식 문서 이동 링크</a></p>
</blockquote>
<h1 id="1-props-기반-동적-스타일링">1. props 기반 동적 스타일링</h1>
<p><code>@emotion/styled</code> 의 강점. 
바로 <strong><code>props</code>를 받아서 스타일을 동적으로 변경할 수 있다</strong>는 점입니다.</p>
<h2 id="1-1-기본-props-사용-방식">1-1. 기본 props 사용 방식</h2>
<p>백틱 안에서 <code>${}</code> 을 사용해 <code>props</code>에 접근이 가능합니다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/3c7ed550-a082-40d8-b059-6bcc4dc861a4/image.png" alt=""><code>${}</code> 안에 함수를 넣으면 <code>props</code>를 인자로 받습니다.
함수가 반환하는 값이 그대로 <strong>CSS</strong> 값으로 적용됩니다!</p>
<p><strong>Object Style</strong> 방식도 동일하게 사용해요.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/cc0bf338-4922-4aa8-ac98-33b603000f40/image.png" alt=""></p>
<h2 id="1-2-typescript-환경에서-props-타입-정의">1-2. TypeScript 환경에서 props 타입 정의</h2>
<p><code>TypeScript</code> 환경에서는 <code>props</code> 타입을 <strong>제네릭</strong>으로 정의해야해요.
저는 이 <strong>제네릭</strong> 부분이 참 헷갈렸는데요!! 
참고용으로 아래 헷갈릴만한 이론을 정리해뒀습니다!</p>
<blockquote>
<p><strong>📝참고용</strong></p>
</blockquote>
<ul>
<li><strong><code>()</code></strong> : props의 값을 받는 곳 <em>( ex. <strong><code>(</code></strong> props <strong><code>)</code></strong> =&gt; ... )</em></li>
<li><strong><code>&lt;&gt;</code></strong> : 타입을 주입하는 제네릭 문법. styled에서는 props의 타입을 주입하는 데 사용<pre><code> _( ex. **```&lt;```**{ active?: boolean }**```&gt;```** )_</code></pre></li>
<li><strong><code>&lt;&gt;</code> 안의 내용</strong> : 주입하는 타입 <em>( ex. <strong><code>{ active?: boolean }</code></strong> )</em></li>
</ul>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/f92898d4-aaf7-4ffe-b115-7683e1c9f334/image.png" alt=""></p>
<p>타입을 정의하면 잘못된 <code>props</code> 전달을 <strong>컴파일 단계에서 바로 잡아줍니다!</strong>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/0e8e0d75-5dff-433f-955c-2cd1f2be7bef/image.png" alt=""></p>
<hr>
<h1 id="2-shouldforwardprop">2. shouldForwardProp</h1>
<p><code>props</code> 기반 스타일링을 하다 보면 아래와 같은 경고를 마주칠 수 있다고 합니다..!
<em>( 흐음..전 만나본 기억은 없어용.. )</em></p>
<blockquote>
<p><code>Warning: Received &#39;true&#39; for a non-boolean attribute &#39;primary&#39;</code><br/>
HTML 표준 속성이 아닌 <strong>커스텀 <code>props</code>가 DOM에 전달되면 발생하는 경고</strong>입니다.
<strong>HTML 표준 속성이 아닌 props</strong>는 <strong>DOM에 전달되면 안되거든요!</strong></p>
</blockquote>
<p>이를 해결하는 것이 바로 <code>shouldForwardProp</code>입니다.</p>
<h2 id="2-1기본-사용법">2-1.기본 사용법</h2>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/aaead069-172d-4f83-821c-2349ae7b56f2/image.png" alt=""></p>
<blockquote>
<p><code>shouldForwardProp</code>이 <code>false</code>를 반환하는 <code>prop</code>은 <strong>DOM</strong>에 전달되지 않고 <strong>스타일링에만 사용됩니다.</strong></p>
</blockquote>
<h2 id="2-2-ispropvalid-활용">2-2. isPropValid 활용</h2>
<p>커스텀 <code>props</code>가 많아질수록 하나씩 제외하는 방식은 번거롭습니다.
<code>@emotion/is-prop-valid</code> 패키지를 활용하면 HTML 표준 속성인지 자동 판별을 해줍니다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/13988dca-6d2a-4f8b-94f0-13d19393d511/image.png" alt="">
<code>isPropValid</code>는 속성들에 대해 아래와 같이 반환합니다.</p>
<ul>
<li>HTML 표준 속성(<code>id</code>, <code>className</code>, <code>onClick</code> 등) : <code>true</code></li>
<li>커스텀 속성(<code>primary</code>, <code>active</code> 등) : <strong><code>false</code></strong></li>
</ul>
<p><strong>특정 커스텀 <code>props</code>만 추가로 제외하고 싶다</strong>면, 아래와 같이 조합이 가능합니다.</p>
<pre><code>shouldForwardProp: (prop) =&gt; isPropValid(prop) &amp;&amp; prop !== &#39;primary&#39;</code></pre><blockquote>
<p>저는 <strong>커스텀 속성</strong>은 <code>Sass</code>에서 영감을 받은 <strong><code>$</code> prefix</strong>를 사용하여 표시하곤 합니다!!
<strong><code>$</code></strong>가 붙은 <code>props</code>는 HTML 표준 속성이 아니라는 게 한눈에 보여서, <code>shouldForwardProp</code> 설정 없이도 의도를 명확하게 전달할 수 있어요.
<br/>단, 실제로 <strong><code>$</code> prefix</strong>만으로는 <strong>DOM</strong> 전달을 막을 수 없고 <code>shouldForwardProp</code>은 여전히 필요해요!!!</p>
</blockquote>
<hr>
<h1 id="3-오버라이딩">3. 오버라이딩</h1>
<h2 id="3-1-styled-로-컴포넌트-확장">3-1. styled( )로 컴포넌트 확장</h2>
<p><code>styled(컴포넌트)</code> 방식으로 기존 스타일을 확장할 수 있습니다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/e18101d5-9ffb-49c6-b090-4e7dbcfad6b8/image.png" alt="">
<code>BaseButton</code>의 <strong>공통 스타일은 유지</strong>하되, <strong>각각 다른 스타일만 추가</strong>했습니다.</p>
<h4 id="변형이-많은-컴포넌트를-만들-때-중복을-줄이는-핵심-패턴이에요"><strong>변형이 많은 컴포넌트를 만들 때 중복을 줄이는 핵심 패턴이에요!!</strong></h4>
<blockquote>
<p>실제로 제가 정말 많이 애용하는 패턴입니다!
디자인 시스템 만들때 좋아요!</p>
</blockquote>
<blockquote>
<p>단, <code>styled(컴포넌트)</code> 방식은 
해당 컴포넌트가 <strong><code>className</code> prop</strong>을 받을 수 있어야 합니다.
<code>Emotion</code>이 내부적으로 생성한 클래스명을 <code>className</code>으로 전달하는 방식이기 때문이에요!</p>
</blockquote>
<h2 id="3-2-as-prop으로-렌더링-태그-변경">3-2. as prop으로 렌더링 태그 변경</h2>
<p>스타일은 그대로 유지하면서 렌더링되는 HTML 태그만 변경하고 싶을 때 <code>as</code> prop을 사용해요.
<em>( 사실 저는 한번도 사용해보지 않았습니다..! )</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/cfaa391e-fead-4efe-99f7-2d6424eb9b0f/image.png" alt=""></p>
<p>아직 실무에서 직접 써볼 기회가 없었는데요,
버튼 컴포넌트를 <code>&lt;a&gt;</code> 태그로 렌더링해야 할 때나,
시맨틱 마크업(의미 있는 HTML 태그 사용)이 중요한 상황에서 유용할 것 같아요.
언젠가 써먹을 날이 오겠죠...?
아무튼 무척 편리한 방식이에요! 천재만재 개발자들!!</p>
<hr>
<h1 id="4-실무에서-자주-사용되는-패턴">4. 실무에서 자주 사용되는 패턴</h1>
<h2 id="4-1-css-helper로-동적-스타일-분리">4-1. css helper로 동적 스타일 분리</h2>
<p><code>props</code> 기반 스타일이 복잡해질수록 <strong>백틱 안이 지저분해집니다.</strong>
<strong><code>css</code> helper</strong>를 활용해 <strong>동적 스타일을 별도 변수로 분리</strong>하면 훨씬 깔끔해져요.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/d354934d-7605-4247-9e7a-07b06d6ff019/image.png" alt="">스타일 변형이 많은 디자인 시스템 컴포넌트에서 자주 사용합니다!!
<em>( 강추 강추 강강추~ )</em></p>
<h2 id="4-2-조건부-스타일링-패턴">4-2. 조건부 스타일링 패턴</h2>
<p>실제로 제가 가장 자주 쓰는 조건부 패턴들입니다!</p>
<h3 id="삼항-연산자">삼항 연산자</h3>
<p>: 2가지 중 <strong>한가지</strong> 선택 시
<img src="https://velog.velcdn.com/images/heeflee_1310/post/91692446-d2f7-4e33-bf1d-bf612b4e6cfd/image.png" alt=""></p>
<h3 id="연산자">&amp;&amp; 연산자</h3>
<p>: 조건이 <code>true</code>인 경우에만, <strong>스타일 블록 전체를 적용</strong>할 때
<img src="https://velog.velcdn.com/images/heeflee_1310/post/56e9624c-3d3a-47ca-8946-bf8f4a9737e1/image.png" alt=""></p>
<h3 id="객체-스프레드">객체 스프레드</h3>
<p>: <strong>Object Style</strong>에서 조건부 속성을 추가할 때
<img src="https://velog.velcdn.com/images/heeflee_1310/post/ef949d98-d1f9-408b-a215-e99abd9693f0/image.png" alt=""></p>
<p>아래 상황에 맞게 패턴을 선택해서 사용하면 된답니다!</p>
<ul>
<li><strong>적용할 스타일이 1~2개</strong> : <code>삼항 연산자</code></li>
<li><strong>여러 속성을 한 번에 적용</strong> : <code>&amp;&amp; 연산자</code> or <code>객체 스프레드</code></li>
</ul>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/6bc7cba0-323e-4620-8c38-9bded8b1403f/image.png" alt="">저는 3가지의 스타일링 패턴 중 <strong>삼항 연산자</strong>와 <strong>&amp;&amp; 연산자</strong>를  <strong><code>css</code> helper</strong>와 함께 자주 사용합니다!</p>
<p><code>Tailwind</code>도 좋지만, 전 여전히 이 매력둥이에게 푹 빠져있답니다..❤️
<img src="https://velog.velcdn.com/images/heeflee_1310/post/1d856dfc-a638-44dd-bf54-0400463deb84/image.png" alt=""></p>
<p>다음 글은 <code>@emotion/styled</code> 기반으로 구현한 <strong>타이포 시스템 구현</strong>으로 돌아올게요!!</p>
<blockquote>
<p><strong>🔗 참고</strong>
Emotion 공식 문서 : <a href="https://emotion.sh/docs/styled">https://emotion.sh/docs/styled</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[01. Emotion이란?]]></title>
            <link>https://velog.io/@heeflee_1310/01.-Emotion%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@heeflee_1310/01.-Emotion%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Mon, 16 Mar 2026 04:49:14 GMT</pubDate>
            <description><![CDATA[<p>저는 굉장한 <code>@emotion/styled</code> 러버인데요?
그동안 개인 노션에만 저의 사랑을 기록해왔더라구요.</p>
<p>풀어봅니다. 저의 사랑.</p>
<h1 id="emotion이란">Emotion이란?</h1>
<p><em><strong>&quot; JavaScript로 CSS를 작성하게 해주는 스타일링 라이브러리 &quot;</strong></em></p>
<p><strong>styled-component</strong>와 <strong>CSS-in-JS</strong> 진영의 양대산맥 중 하나로, <strong>styled-component</strong>에 영감을 받아 개발했다고 공식 문서에서 직접 밝히고 있다. </p>
<p><em>즉, <code>Emotion</code>은 <code>styled-components</code>의 오마주라 생각하면 된다.</em></p>
<p>이번 글은 아래 목차 순으로 작성 예정이다.</p>
<blockquote>
<p><strong>목차</strong></p>
</blockquote>
<ol>
<li>CSS-in-JS 장점</li>
<li>Emotion 작동 원리</li>
<li>Emotion 스타일 작성 방식(2가지)</li>
<li><code>@emotion/css</code> <strong>VS</strong> <code>@emotion/react</code> <strong>VS</strong> <code>@emotion/styled</code> 비교</li>
</ol>
<p>그런데 여기서 의문이 드는 건,
<em>&#39;왜 굳이 CSS 파일 대신 JS로 스타일을 작성할까?&#39;</em> 이다.</p>
<p><strong>CSS-in-JS</strong>의 장점을 보면 이해할 수 있다.</p>
<hr>
<h2 id="1-css-in-js-장점">1. CSS-in-JS 장점</h2>
<p><strong>1. 스코프 격리</strong> : 클래스명 충돌 걱정이 없이 컴포넌트 단위로 스타일 관리 가능
<strong>2. 동적 스타일링</strong> : JS변수, props, state 값을 스타일에 직접 반영 가능
<strong>3. Dead Code 제거</strong> : 컴포넌트 삭제 시, 스타일도 함께 삭제.</p>
<p>이러한 장점에 <code>Emotion</code>은 <em>DX(Developer Experience)</em> 향상을 위해 아래 요소를 추가로 제공한다.</p>
<h3 id="1-1-source-maps">1-1. Source maps</h3>
<p><em>&quot;변환된 코드 - 원본 코드를 연결해주는 지도&quot;</em></p>
<p>빌드 후 브라우저에서 실행되는 코드는 압축/변환되어 있기에, 에러가 발생해도 <strong><em>&quot;어느 파일, 몇 번째 줄에서 발생&quot;</em></strong> 했는지 추적이 어렵다.
이를 해결하는 것이 <strong>Source maps</strong>이다.
개발자 도구를 통해 어떤 컴포넌트의 어느 줄에서 해당 스타일이 바로 왔는지 확인 가능하다. </p>
<h3 id="1-2-labels">1-2. Labels</h3>
<p><code>Emotion</code>이 생성하는 클래스명은 기본적으로 <strong><em>&quot;해시값&quot;</em></strong>(ex.css-abc123)이라 사람이 읽기 어렵다.
<code>@emotion/babel-plugin</code>을 사용하면, 자동으로 컴포넌트/변수이름을 클래스명에 붙여줘서 어떤 스타일인지 바로 파악이 가능하다.</p>
<pre><code>// label이 없을 때
class = &quot;css-abc123&quot;

//label이 있을 때
class = &quot;css-Button-abc123&quot; // Button 컴포넌트의 스타일임을 바로 인식 가능</code></pre><p>직접 <code>label</code> 옵션을 붙일 수도 있다.</p>
<pre><code>const style = css`
    label : this-is-label-example;
    color : hotpink;
`

// class = &quot;css-this-is-label-example-abc123&quot;</code></pre><p>디버깅 시 브라우저 DevTools를 보면 <code>css-this-is-label-example-abc123</code>과 같은 형태를 볼 수 있다.</p>
<h3 id="1-3-testing-utilties">1-3. Testing utilties</h3>
<p><code>@emotion/jest</code> 패키지를 통해 Jest 스냅샷 테스트에서 실제 CSS 스타일이 어떻게 적용됐는지 확인할 수 있다.
일반 Jest 스냅샷은 <strong><em>클래스명(css-abc123)</em></strong> 만 찍히는데, 
Emotion의 테스트 유틸리티를 쓰면 클래스명이 아닌 <strong>실제 스타일 내용이 스냅샷에 포함된다.</strong></p>
<pre><code>// 일반 스냅샷
&lt;button class=&quot;css-abc123&quot;&gt;클릭&lt;/button&gt;

// @emotion/jest 적용 후
.css-abc123 {
    color:hotpink;
    font-size:16px;
}

&lt;button class=&quot;css-abc123&quot;&gt;클릭&lt;/button&gt;</code></pre><p>스타일이 의도치 않게 변경된 경우, 테스트로 바로 잡아낼 수 있어 스타일 회귀 방지에 유용하다.</p>
<p><em>(오호..Jest 사용 시 적용해봐야겠다)</em></p>
<hr>
<h2 id="2-작동-원리">2. 작동 원리</h2>
<p>Emotion은 내부적으로 돌아가는 방식 중 핵심 아래와 같다.</p>
<h4 id="런타임-에-클래스명을-생성-한다">&quot; <em><strong>런타임</strong></em> 에 클래스명을 <em><strong>생성</strong></em> 한다 &quot;</h4>
<blockquote>
<p>📝 용어 정리
<strong>컴파일(Compile)</strong> : 코드를 실행 하기 전 변환하는 과정 <em>( 사람 말을 컴퓨터 말로 번역 )</em>
*<em>런타임(RunTime) *</em>: 코드가 실제로 실행되는 순간/환경 <em>( 번역된 코드가 실제로 돌아가는 순간 )</em></p>
</blockquote>
<p>예시 코드를 <code>@emotion/css</code>로 작성해보았다</p>
<pre><code>import { css } from &#39;@emotion/css&#39;;

const ExampleStyle01 = css`
    color : hotpink;
    font-size : 24px;
`</code></pre><p>해당 예시 코드는 개발자 모드로 확인하면, <code>&#39;css-abc123&#39;</code>과 같은 <strong>&quot;고유&quot;</strong> 클래스명 문자열을 반환한다.</p>
<p><strong>emotion</strong>은 </p>
<ol>
<li>스타일 코드를 받아서 </li>
<li>해시 기반의 고유 클래스명을 생성하고, 해당 CSS 규칙을 </li>
<li><code>&lt;style&gt;</code>태그에 동적으로 주입한다.</li>
</ol>
<p>이 모든 과정은 <strong><code>@emotion/cache</code></strong>가 담당한다.</p>
<hr>
<h2 id="3-emotion-스타일-작성-방식2가지">3. Emotion 스타일 작성 방식(2가지)</h2>
<p><strong>emotion</strong>은 2가지 스타일 작성 방식을 제공한다.</p>
<h3 id="3-1-tagged-template-literal"><strong>3-1. Tagged Template Literal</strong></h3>
<p><em>&quot; CSS 문법 그대로 사용&quot;</em></p>
<pre><code>const ExampleStyle02 = css`
  background-color : hotpink;
  color:white;

  &amp;:hover{
      color:${myColor}; // JS 변수로 사용 가능
  }
  `</code></pre><p>css키워드+백틱(<code>`</code>)을 사용하여 백틱(<code>`</code>) 안에 일반 CSS를 쓰듯 작성한다.
  내부적으로 css 함수가 <strong>Tagged Template Literal</strong>을 <em>파싱</em> 해 처리한다.</p>
<blockquote>
<p>📝 용어 정리
<strong>파싱(parsing)</strong> : 텍스트(문자열)을 읽은 뒤 컴퓨터가 다룰 수 있는 구조로 변환하는 과정.
즉, 글자 덩어리를 의미 있는 구조로 해석하는 것.</p>
</blockquote>
<h3 id="3-2-object-style"><strong>3-2. Object Style</strong></h3>
<p><em>&quot; JS 객체로 스타일 표현 &quot;</em></p>
<p>평소 프로젝트 시 역할별 분리를 좋아하는 나에게는 정말 사랑스러운 표현 방식이다.
이 방식보다는 추후 등장할 <code>@emotion/styled</code>가 정말 압도적이다.
<em>( DX 향상의 길. )</em></p>
<pre><code>  const ExampleStyle03 = css({
      backgroundColor : &#39;hotpink&#39;,
      color : &#39;white&#39;,
      &#39;&amp;:hover&#39;:{
          color:myColor, // JS 변수
      }
  })</code></pre><p>해당 표현 방식은 <strong>CSS 프로퍼티 명이 camelCase로 바뀌는 것</strong>만 주의하면 된다.
또한 TypeScript 환경에서 자동완성이 더 잘된다는 이점이 있다.</p>
<blockquote>
<p><strong>TS에서 자동완성이 더 잘되는 이유</strong><br />
<em>&quot; CSS 속성명이 JS 객체의 키(Key)이기 때문 &quot;</em><br />
그러므로 <strong>1. Tagged Template Literal</strong>의 경우 TS 관점에서 <strong>백틱 안은 그냥 문자열이다.</strong> 오타가 나도 타입 에러가 발생하지 않고, 자동 완성도 되지 않는다.<br />
<strong>2.Object Style</strong>의 경우 <strong>css( )함수</strong>는 인자 타입으로 <strong>CSSObject(React의 CSSProperties 기반)</strong>를 받는다. <br />JS객체의 키로 CSS 속성을 사용하기에 TS가 타입 검사를 할 수 있다. <br />이로 인해 아래와 같은 편리함이 생긴다.</p>
</blockquote>
<ul>
<li><strong>자동 완성</strong> : <code>back</code> 입력 시, <code>backgroundColor</code> 등 후보 목록이 뜸</li>
<li><strong>오타 감지</strong> : 존재하지 않는 속성명에 빨간 줄이 생김</li>
<li><strong>값 타입 검사</strong> : <code>font-size:&#39;abc&#39;</code> 처럼 잘못된 값 타입을 잡아줌<br />
정리하자면,
_" **Template Literal 안은 TypeScript가 '문자열'**로 보고, **Object Style은 TypeScript가 '타입이 있는 객체'**로 본다. "_

</li>
</ul>
<p><a href="https://emotion.sh/docs/@emotion/cache"><code>@emotion/cache</code></a>와 <code>스타일 캐싱</code>에 대해 짧게 먼저 소개를 해보자면,</p>
<p><strong><code>Emotion</code></strong>은 같은 스타일이 중복으로 삽입되지 않도록 내부적으로 캐시를 사용한다.
동일한 스타일 문자열은 동일한 클래스명을 반환하고, 이미 <code>&lt;style&gt;</code>태그에 존재하면 다시 삽입하지 않는다.
성능 최적화가 내장된 셈이다. <em>( 최고다...!!!! )</em></p>
<hr>
<h2 id="4-emotioncss-vs-emotionreact-vs-emotionstyled-비교">4. @emotion/css VS @emotion/react VS @emotion/styled 비교</h2>
<p>그렇다면, 이제 뭘 써야할까? 라고 할땐
<code>React</code> 기반이라면 개인적으로는 <code>@emotion/styled</code>를 추천한다.</p>
<p>3가지 패키지를 표로 비교를 해보자면 다음과 같다.
  <img src="https://velog.velcdn.com/images/heeflee_1310/post/9114f449-9996-4096-af47-a03a2e3fb754/image.png" alt=""></p>
<h3 id="4-1-emotioncss">4-1. @emotion/css</h3>
<p>_&quot; 프레임워크 무관, 가장 심플한 진입점 &quot; _</p>
<pre><code>import { css, cx } from &#39;@emotion/css&#39;

const ExampleStyle04 = css`
    color : hotpink;
`

&lt;div className={ExampleStyle04}&gt; @emotion/css 예시 코드 &lt;/div&gt;</code></pre><ul>
<li>Babel 설정이 전혀 필요 없다. <code>npm install</code> 후 바로 사용 가능</li>
<li><code>css( )</code> 함수가 클래스명 문자열을 반환(핵심!!)</li>
<li>클래스를 합성 시, <code>cx( )</code> 함수를 사용</li>
<li>SSR 적용 시 추가 설정이 필요하고, 테마 시스템이 없다.</li>
</ul>
<blockquote>
<p>참고 : 공식 문서 예시 출력 화면(<a href="https://emotion.sh/docs/@emotion/css">🔗 문서 이동</a>)<img src="https://velog.velcdn.com/images/heeflee_1310/post/babadfa4-671a-4e52-b0c4-b042141b45fc/image.png" alt=""></p>
</blockquote>
<p>이런 경우에 선택하는 걸 추천한다.</p>
<blockquote>
<ol>
<li><strong>React가 아닌 환경(Vue, Svelte, Vanilla JS 등)</strong>에서 Emotion을 사용해야 할 때</li>
<li><strong>Babel 커스터마이징이 불가능한 환경</strong>(CRA 기본 설정 등)에서 <strong>가장 빠르게 시작</strong>하고 싶을 때</li>
</ol>
</blockquote>
<h3 id="4-2-emotionreact">4-2. @emotion/react</h3>
<p>_&quot; <code>React</code> 유저의 메인 선택지 &quot; _</p>
<pre><code>/** @jsxImportSource @emotion/react */
import { css } from &#39;@emotion/react&#39;

const ExampleStyle05 = &#39;hotpink&#39;

&lt;div css={css`color:${ExampleStyle05};`}&gt; @emotion/react 예시 코드 &lt;/div&gt;</code></pre><ul>
<li>HTML 요소에 직접 css prop를 붙일 수 있음. styled API 없이도 컴포넌트에 바로 스타일을 입힐 수 있어서 <strong>보일러플레이트가 줄어든다.</strong><blockquote>
<p>📝 용어 정리</p>
</blockquote>
</li>
<li><em>보일러플레이트(Boilerplate) *</em>
<em>&quot; 기능은 없지만, 꼭 써야하는 의례적인 코드. 매번 반복해서 써야하는 틀에 박힌 코드 덩어리 &quot;</em><br/>
뭔가를 만들기 위해 실제 내용과 상관 없이 항상 작성해야하는 코드. 
다다익손. 
많으면 피로도와 실수가 증가함</li>
</ul>
<p>보일러플레이터가 줄어든다를 더 잘 이해하기 위한 비교 예시 코드</p>
<pre><code>// ========== @emotion/react 코드 ========== 
// 📄 Button.tsx
/** @jsxImportSource @emotion/react */
import { css } from &#39;@emotion/react&#39;

const Button = () =&gt; {
    &lt;button css={css`
        background : pink;
        color : white;
    `}&gt;예시 버튼&lt;/button&gt;
}

//  ===== CSS Module 방식 : 항상 2개의 파일 필요. 반복 작업 증가 ===== 
// 📄 button.module.css
.button {
    background : pink;
    color : white ;
}

// 📄 Button.tsx
import styles from &#39;./button.module.css&#39; // 매번 import 해야함

const Button = () =&gt; {
    &lt;button className={styles.button}&gt;css module 버튼&lt;/button&gt; // className 연결
}</code></pre><ul>
<li><code>@emotion/css</code>와 달리 <code>css( )</code>함수가 클래스명 문자열이 아닌 내부 스타일 객체를 반환. 이 객체는 <code>Emotion</code>이 저레벨에서 해석해 다른 스타일과 조합 가능.</li>
<li>SSR이 별도 설정 없이 지원. <code>ThemeProvider</code>를 통해 테마 시스템 사용 가능.</li>
<li>단, <code>JSX Pragma</code> 또는 <code>Babel</code> 설정이 반드시 필요.</li>
</ul>
<blockquote>
<p>참고 : 공식 문서 예시 출력 화면(<a href="https://emotion.sh/docs/@emotion/react">🔗 문서 이동</a>)<img src="https://velog.velcdn.com/images/heeflee_1310/post/e6c01205-f2c5-4671-bb5b-fe6d7dc59076/image.png" alt=""></p>
</blockquote>
<p>이런 경우에 사용하는 걸 추천한다.</p>
<blockquote>
<ol>
<li><strong>React 프로젝트</strong>에서 <strong>Babel 설정을 커스터마이징 가능</strong></li>
<li>styled 컴포넌트를 매번 만들지 않고 <strong>요소에 직접 스타일을 적용</strong>하고 싶을때</li>
<li><strong>테마와 SSR이 중요한 프로젝트</strong></li>
</ol>
</blockquote>
<h3 id="4-3-emotionstyled">4-3. @emotion/styled</h3>
<p><em>&quot; <code>styled-components</code>에서 갈아타는 사람들의 선택 (마이럽💕)&quot;</em></p>
<p>styled 패키지는 내부적으로 <strong><code>@emotion/react</code>를 래핑한 API</strong>이다.
그래서 <code>@emotion/react</code>의 모든 기능을 포함한다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/68b44ca5-bd10-4f08-8f50-c0ece32789ea/image.png" alt="">래핑 패키지이기에 무조건 <code>@emotion/react</code>가 의존성으로 함께 설치되어야한다.
<em>( 공식 문서 작성된 install 문을 보면 더 와닫는다. )</em></p>
<p><code>styled-components</code>와 API가 거의 동일하여 마이그레이션 비용이 낮다.
<code>props</code> 기반 동적 스타일링이 직관적이다.
<code>&#39;as&#39;</code> prop으로 렌더링 태그를 바꾸거나, <code>shouldForwardProp</code>으로 props 전달을 제어하는 등 고급 기능이 포함되어 있다.</p>
<blockquote>
<p>참고 : 공식 문서 예시 출력 화면(<a href="https://emotion.sh/docs/styled">🔗 문서 이동</a>)<img src="https://velog.velcdn.com/images/heeflee_1310/post/4d0e839d-a02a-4424-8bd4-d2b7e2d95877/image.png" alt=""></p>
</blockquote>
<p>개인적으로 정말 추천한다. 
이런 경우에 사용하는 걸 추천한다.</p>
<blockquote>
<ol>
<li>팀이 <strong>styled-components 스타일 API에 익숙</strong> + <strong>마이그레이션 필요</strong></li>
<li><strong>스타일을 독립된 컴포넌트 단위로 분리</strong>해서 <strong>재사용성을 높이고 싶을 때</strong>.</li>
</ol>
</blockquote>
<h3 id="4-4-비교-결론">4-4. 비교 결론</h3>
<p>실무에서는 <code>@emotion/react</code>와 <code>@emotion/styled</code>를 함께 쓰는경우가 많다고 합니다.
_(클로드 피셜. 아니라면 독자분이 맞아요) _</p>
<p>재사용 컴포넌트는 <code>styled</code>로 만들고, 일회성 스타일 조정은 <code>css</code> <code>prop</code>으로 처리하는 식이다. 
심지어 둘은 내부적으로 같은 캐시를 공유하므로 동시에 사용해도 충돌도 없다..!!</p>
<p>설명을 통해서도 선택하기가 어려우면 아래 단계를 따라 선택하면 된다.</p>
<h4 id="q1-react-프로젝트인가">Q1. React 프로젝트인가?</h4>
<p><strong>NO</strong> : <code>@emotion/css</code>
<strong>YES</strong> : Q2 이동</p>
<h4 id="q2-babel-설정이-가능한가">Q2. Babel 설정이 가능한가?</h4>
<p><strong>NO</strong> : <code>@emotion/css</code> (제한이 있지만 가능)
<strong>YES</strong> : Q3 이동</p>
<h4 id="q3-css-prop-vs-styled-api-중-선호-방식은">Q3. CSS prop VS styled API 중 선호 방식은?</h4>
<p><strong>CSS props</strong>: <code>@emotion/react</code>
<strong>styled API</strong> : <code>@emotion/styled</code></p>
<hr>
<h2 id="결론">결론</h2>
<p><code>Emotion</code>은 
<strong><em>&quot; CSS를 JS답게 사용하고 싶다 &quot;</em> + <em>&quot; 그래도 CSS 문법은 익숙한게 좋다 &quot;</em></strong>
를 만족하기 위해 만들어진 라이브러리다.</p>
<p>3개의 패키지 차이를 알고나면, 프로젝트 성격에 맞게 골라 쓰는게 점점 익숙해질 것이다.
_( 이렇게 쓰고 난 <code>@emotion/styled</code>를 주로 사용한다. 헿~ )
_</p>
<blockquote>
<p>*<em>🔗 참고 *</em>
Emotion 공식 문서 : <a href="https://emotion.sh/docs/introduction">https://emotion.sh/docs/introduction</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[신한 스퀘어브릿지 청년 해커톤 2기] 03. 2주차 ]]></title>
            <link>https://velog.io/@heeflee_1310/%EC%8B%A0%ED%95%9C-%EC%8A%A4%ED%80%98%EC%96%B4%EB%B8%8C%EB%A6%BF%EC%A7%80-%EC%B2%AD%EB%85%84-%ED%95%B4%EC%BB%A4%ED%86%A4-2%EA%B8%B0-03.-2%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@heeflee_1310/%EC%8B%A0%ED%95%9C-%EC%8A%A4%ED%80%98%EC%96%B4%EB%B8%8C%EB%A6%BF%EC%A7%80-%EC%B2%AD%EB%85%84-%ED%95%B4%EC%BB%A4%ED%86%A4-2%EA%B8%B0-03.-2%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Mon, 23 Feb 2026 16:32:05 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/85c497cf-1a14-42f6-98e6-5c2e010a5d4e/image.png" alt=""> 20대 후반의 기억력은 빠르게 사라지기에..빠르게 다시 왔어요!!
2주차 기록입니다!!</p>
<p><strong>2주차 키워드 : 취업 사진 촬영, 멘토링, MVP 완성</strong></p>
<h2 id="1126일차">1/12(6일차)</h2>
<blockquote>
<p>추가 기획 논의 및 개발 세팅 진행, 스타일링 이원화</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/02a58610-d8c7-42ec-ad24-bf9ebc7eb548/image.png" alt="">첫 데일리 스크럼입니다!</p>
<p>저번주 백로그 기반으로 개발 시작 전 마지막으로 다듬는 시간을 가졌습니다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/782d9d36-fb4e-4606-b757-969000495662/image.png" alt=""><em>( 못 알아 보실 것 알아서 블러 처리는 따로 안했어요!헤헿~ )</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/b827e71c-1da0-497a-a0e1-95aee453af77/image.png" alt="">
이 과정에서 추가 기획이 생겼어요..<em>( ..ㅠㅠ )</em>
빠른 배포를 목표로 MVP만 설정했지만, 다시 봐도 살 떨리는 양이긴했어요..</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/46d2575b-e368-49c9-80c0-51446793cfe6/image.png" alt="">저 표정으로 크게 숨 한번 쉬면 진짜 갑자기 몸에 도파민이 싹돌아요. 추천합니다~ ^_^b
오랜만에 달릴 준비하니까 참..보람차구 힘이 납니다..(p)</p>
<p>아무래도 개발자라..개발 모먼트들도 함께 기록해봅니다!</p>
<hr>
<h3 id="📝-개발-기록--스타일링-전략">📝 개발 기록 : 스타일링 전략</h3>
<p>이번 프로젝트에서는 <strong>스타일링 전략</strong>을 조금 색다르게 가져가봤어요!
바로바로 <strong>Emotion + Tailwind</strong> 조합으로 이원화를 했답니다!!</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/d44b8a3a-cb63-4eba-bce9-293cac59f1c4/image.png" alt=""></p>
<p>굳이 이원화를 선택한 이유는..
다양한 상태 변화와 빠른 구현, 두 가지 목표를 달성하기 위해 라이브러리 선정에서 고민이 많았어요.
그러다 각자의 장점만 골라서 쓰면 오히려 목적에 딱 맞겠다는 판단이 들었습니다!
저는 아래 기준으로 라이브러리를 사용했답니다~</p>
<blockquote>
<h4 id="emotion--상태-변화가-빈번하고-인터랙션이-많은-컴포넌트-구현">Emotion : 상태 변화가 빈번하고 인터랙션이 많은 컴포넌트 구현</h4>
</blockquote>
<ul>
<li>props 기반 조건부 스타일링으로 상태에 따른 동적 스타일 처리</li>
<li>디자인 토큰 기반 theme 설정으로 추후 기업 연계 시 확장성 확보</li>
<li>다수의 상태 기반 변형 구조를 객체화된 스타일로 관리해 유지보수 용이<h4 id="tailwind--상태와-무관한-정적-레이아웃-영역-구현">Tailwind : 상태와 무관한 정적 레이아웃 영역 구현</h4>
</li>
<li>정적 영역에서 CSS-in-JS 런타임 오버헤드 제거로 성능 최적화</li>
<li>spacing, flex 정렬 등 반복 스타일을 유틸리티 클래스로 빠르게 구현</li>
<li>별도 스타일 파일 없이 마크업 내에서 즉시 레이아웃 구성 가능</li>
</ul>
<p>이원화 덕분에 동적 UI의 표현력 + 정적 레이아웃의 생산성,
두 마리 토끼를 모두 잡을 수 있었어요~🐇🐇
<img src="https://velog.velcdn.com/images/heeflee_1310/post/9d5364e5-ef1e-4ae8-8b97-806526fc87b6/image.png" alt="">앞으로 반복되는 Tailwind 클래스를 더 효율적으로 재사용하는 방법도 틈틈이 공부해봐야겠습니다!</p>
<p>오늘의 개발 기록 끝!</p>
<hr>
<p>오늘의 점심은 각자 배달시켜먹었슴다!
포케 러버는 포케를 시켜먹었는데요!! 
제가 먹었던 포케 중 가장 양이 많고 맛있는 곳 맛있는 곳을 찾았어요..💗
포케 러버분께 너무 추천해요~
<img src="https://velog.velcdn.com/images/heeflee_1310/post/ba645d0c-5b5b-4204-ad39-e6b41707dbad/image.png" alt=""><em>( 건강하고 돼람직한 메뉴 선택이었다. )</em></p>
<hr>
<h2 id="1137일차">1/13(7일차)</h2>
<blockquote>
<p>취업 사진 촬영 <em>(팀별로 사진 촬영 일정 다름)</em></p>
</blockquote>
<p>오늘의 팀 이벤트는 조금은 널널했어요~
<img src="https://velog.velcdn.com/images/heeflee_1310/post/38c03ce7-ce44-4fe1-8e33-8c61766c7e9e/image.png" alt="">
저희 팀은 취업 사진을 가장 처음 찍게 되었는데요!!
카메라 앞에서 엄청난 뚝딱 로봇이 되는 사람이라 정말 긴장 많이했지만,
촬영팀 스태프분들이 너무나 즐겁게 도와주셔서 활짝 웃는 취업사진을 얻을 수 있었답니다!!
<em>( 돌잔치 체험 덕분에 자연스러운 웃음 GET )</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/67fd0132-f29a-48fc-96e3-26052fc78834/image.png" alt="">기초화장(피부+쉐도우)을 하고 가면, 빠르게 추가 메이크업과 헤어 스타일링을 해주십니다!
장점만 살리는 메이크업을 엄청 빨리 해주셔서 당황스러울 정도로 신기했어요..
헤메 다하는 데 <em>(기억상)</em> 10분..?15분..?걸렸어요..!
<em>( 이때 수정해주신 부분 기억 못하는게 너무 한스러울 뿐. )</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/19998f86-ed66-40e7-9601-7ba531ef0522/image.png" alt=""> 다들 깔쌈한 얼굴로 개발 논의를 마저 진행했답니다~
<em>( 핸섬앤프리티 너디 최고다. ✨ 🤓 💗 )</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/13823906-d19c-4421-bb51-10aaf4ecadbf/image.png" alt="">추억 강박증 환자는 오늘 같은 <em><strong>🎀pretty event🎀,</strong></em> 놓칠 수 없어요.
바로 옆에 있는 포토이즘으로 이동합니다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/3ac12469-625a-4501-ba20-bef4cb65d78d/image.png" alt="">📸 추억을 추억하는 사진 찍기를 도촬당한 사진
<em>( 추추사사.아줌마 의미 없는 별다줄 좋아해요.. )</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/11f23f66-b6c1-420b-b896-9ac98d212192/image.png" alt=""> 팀 컨지 첫 단체사진!!<em>( 최고다 팀컨지! )</em></p>
<p>개인적으로 프로그램 초반에 프로필 사진 촬영해서 좋았습니다!!
더 바쁠때 찍었다면, 프로필 촬영에 신경도 못 쓰구, 프로젝트 흐름이 많이 깨졌을 것 같아요!</p>
<p>이렇게 즐거운 추억 저장 후 다시 본업으로 돌아왔습니다..😇</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/e5ef36d2-9bdf-4020-8941-1d966e9476c4/image.png" alt="">오늘의 데일리 스크럼이에요!
프론트는 백업이 없으니..그동안의 개발 지식을 총동원해
&#39;확장 가능한 구조로 MVP 구현하기&#39;를 목표로 잡고 달렸습니다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/8c8c1d7b-fb37-4646-ad4a-9786b25995ae/image.png" alt="">
그리고 해냈어요.</p>
<hr>
<h2 id="1148일차">1/14(8일차)</h2>
<blockquote>
<p>멘토링</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/d499af6b-af8f-45e5-8bc6-6699341f0f94/image.png" alt=""> 오늘은 두 번째 멘토링이 있는 날이었어요!
개인적으로 아쉬운 점은 
멘토분들이 개발과 디자인 멘토가 아닌 <strong>전략, 마케팅, 투자</strong> 멘토 셨다는 점이에요.</p>
<p>물론 멘토링 시간들이 너무나 도움되는 시간들이었지만, 
파트별 멘토링을 통해 실무에 가까운 역량을 쌓고 싶던 저는 많이 아쉬웠어요..ㅠㅠ</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/022a7c34-bd4c-475d-bbaa-8fac8b300143/image.png" alt="">오늘의 데일리 스크럼입니다~ </p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/788f161a-35c4-447c-812f-d7dcdb8c80ff/image.png" alt="">그동안 에디터 개발만 하다가 오랜만에 카카오 SDK 사용하니까 조금 신이 났어요~~</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/7a1ae31c-4631-44a7-9920-28b33cdaee96/image.png" alt="">우리의 <em>👑Queen_👑, 
매주 금요일 제출인 운영 문서화를 간편하게 작성하기 위해 영수증 정산 서비스를 만들었어요..
덕분에 저희 팀은 운영쪽에서 힘을 빼고 프로젝트에 더 집중할 수 있었답니다!!
_( 진짜 개발자, 퀸수👑 )</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/2eed7af4-738f-4040-83d8-51607dafe325/image.png" alt="">개발 공부를 하면 할수록 느끼는 건데요..
성장 속도보다 의무적으로 알아야하는 지식들이 더 많아지는 것 같아요..특히 요즘엔 더..
아휴! 솔직히 개발 왤케 할게 끝이 없을까요!!증말!!
<em>( 당연함. 기술파트라 그럼. )</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/ec3f5426-aa4e-4798-8f81-a0ac2c1bb595/image.png" alt=""></p>
<hr>
<h2 id="1159일차">1/15(9일차)</h2>
<blockquote>
<p>플로우 개발</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/513e6d00-8688-43d8-95d5-47ae17fa136e/image.png" alt="">오늘은 잔잔한 Day 시작이에요~</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/ecfecdb3-1d7a-419a-ab32-ecc6d037f47d/image.png" alt="">그리고 와글와글 오늘의 데일리 스크럼입니다.
ㅎ..나자신 화이팅!!</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/dbbdeaa6-ddfa-491c-ada8-74acc35bdc04/image.png" alt="">다들 2주차 후반부 오니까 힘든가봐요..
저만 그런줄 알았는데, 모두가 힘들지만 열심히 일하는 거 보고 다시 힘을 얻어요..!!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/d8c5ba4e-3a25-44d1-9c5b-aca83914fd4a/image.png" alt="">빡집중해서 개발하는데, 
저의 늙은 노트북은...벌써 2번의 블루 스크린이 떴어요..
이제 곧 보내줄 때가 됐나봅니다..
<em>( 죽어도 못 보내.. )</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/33e3621a-4731-49c6-9b82-4f0ecc36d1cb/image.png" alt="">깜빡했는데, 신한에서 정말 빠르게 피드백을 잘 반영해주십니다!!
듀얼 모니터, AI 구독료 등 여러 방면으로 최대한 많이 지원해주셨답니다!!
제가 참여했던 대외 활동 중 가장 소통이 잘 되는 프로그램이었어요👍👍</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/57003297-eb81-4df7-9606-7f7023b89543/image.png" alt="">플로우 하나 구현 완료했어요~
<em>( 뿌듯뿌듯~ )</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/5d0b254c-75e6-4c64-b9c5-2b48da6cb755/image.png" alt=""> 이제 영역 전개 구현으로 들어갑니다.
<em>( 팀 컨지를 향한 하트. 받으세요.💟 )</em></p>
<hr>
<h2 id="11610일차">1/16(10일차)</h2>
<blockquote>
<p>긴급 기획 회의 &amp; 서류 제출</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/fa169cda-72e5-4955-b974-ab9ead3db4b2/image.png" alt="">오늘은 이슈 파티 데이에요~🎉
<img src="https://velog.velcdn.com/images/heeflee_1310/post/a44c7203-b229-4b6c-9a9d-dccb929617d6/image.png" alt=""> 저희 팀은 하나의 큰 목표를 위해 3개의 서비스가 단계별로 필요한 상황이었는데, 
그 중 2번째 서비스에서 이슈 상황이 발생했어요!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/d8d1e225-ef09-4eff-9a4d-48176eb78dc7/image.png" alt=""></p>
<p>능력자들이 모인 저희 팀, 회의 한번으로 해결했답니다!!
<em>( 그냥 박수갈채👏👏👏👏👏 )</em></p>
<p>오늘은 제가 사랑니 이슈로 아침 조퇴를 했는데요!
생각보다..출혈이 멈추지 않아서, MVP 구현은 주말 내에 완성하기로 하고 푹 쉬었답니다!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/c15cef02-5052-461b-ba5c-881bcb91b39b/image.png" alt="">내 몸이 내 마음을 이해한 순간. 이렇게 기쁠 수가 없어요..
광대 잔뜩 올라간 상태로 푹 휴식했답니다~
<em>( 하지만 이러고 다음날 응급 내원했어요..🦷🩸🩸 )</em></p>
<p>어느정도 회복 후 다시 작업에 들어갔습니다! 
<img src="https://velog.velcdn.com/images/heeflee_1310/post/49be393c-3f2e-4fdb-8788-9ac488f1fbff/image.png" alt="">1주차 깃허브 이슈는 총 14개였습니다!
6일만에 MVP 구현...이게 정말 될 줄 몰랐어요..!
전에 7일 안에 MVP 구현을 했었는데, 이번에 하루 줄였어요!!
이렇게 인생 업적에 하나 또 추가합니다~ <em>( 룰루랄라~ )</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/5c6d7f56-53ec-4733-8f4c-334cbbb7d386/image.png" alt="">이번 플젝을 통해 새로운 습관이 하나 생겼습니다!
자잘한 이슈 파악이 아니라면,
디버깅을 위한 컴포넌트를 구현해서 화면에서 바로 디버깅을 하는 습관이 생겼어요!
사진은 웹소켓 통신을 바로바로 확인할 수 있게 컴포넌트를 구현한 화면이랍니다~
다음에도 이 방식으로 빠르게 상태 파악하고 릴리즈때 삭제해야겠어요~</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/3637af40-3e9f-495c-91e1-8046ce2282a9/image.png" alt="">2주차 활동은 자존감을 회복할 수 있던 행복하고 뜻깊은 시간들이었습니다!
앞으로 또 얼마나 회복하고 성장할 수 있을지 기대를 한아름 안고,
<strong>2주차 기록 끝!!</strong></p>
<blockquote>
<p><strong>사진 출처</strong>
팀컨지 (진짜 곧) 인플루언서님의 블로그 줍줍..아자스..아이시떼루..</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[신한 스퀘어브릿지 청년 해커톤 2기] 02. OT & 1주차]]></title>
            <link>https://velog.io/@heeflee_1310/%EC%8B%A0%ED%95%9C-%EC%8A%A4%ED%80%98%EC%96%B4%EB%B8%8C%EB%A6%BF%EC%A7%80-2%EA%B8%B0-1%EC%A3%BC%EC%B0%A8-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@heeflee_1310/%EC%8B%A0%ED%95%9C-%EC%8A%A4%ED%80%98%EC%96%B4%EB%B8%8C%EB%A6%BF%EC%A7%80-2%EA%B8%B0-1%EC%A3%BC%EC%B0%A8-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Mon, 23 Feb 2026 07:21:12 GMT</pubDate>
            <description><![CDATA[<p>정신없이 지나갔던 1주차 기록입니다!!
<strong>1주차 키워드 : 팀빌딩, 기업선정</strong></p>
<h2 id="151일차">1/5(1일차)</h2>
<blockquote>
<p>킥오프(OT)</p>
</blockquote>
<p>기본 스케줄은 9:30~17:30까지지만, 오늘은 오후 1시부터 시작했어요!!
6주 동안 이렇게 여유로운 스케줄은 당분간 없겠죠..아좌잣! <em>(나자신)_화이팅..!!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/3a3c4c3b-2ef5-4f20-9eab-2109379c6a7a/image.png" alt="">킥오프 장소에 도착하니까 &#39;이제 진짜 시작이구나&#39; 싶어서 심장이 두근두근 했답니다!!
_( 원래 심장은 두근두근함. 그냥 귀엽게 시작해봄 )</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/3e5734f8-b185-4287-a477-2f06e7f23811/image.png" alt="">1주차는 지하 라운지?에서 활동한다고 합니다! 춥지 않고 참 따뜻했어요!
다만, 노트북 충전이 어려워서 늙은 노트북을 가진 저는 조금 힘들었어요ㅠ_ㅠ</p>
<p>일찍 도착해서 짐 놓고 바로 옆 명동교자로 점심을 먹으러 갔습니당~
<img src="https://velog.velcdn.com/images/heeflee_1310/post/27428a04-9d8b-4497-bb9a-b07d6776a7a0/image.png" alt="">사실 몇번 먹어봐서 이젠 엄청 맛있다?는 잘 모르겠는데, 빨리 나와서 좋았어요~</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/04847c55-98cb-443c-bf6f-71d4b5f292b9/image.png" alt="">이름표를 받고 임시로 원하는 자리에 착석했답니다!</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/b3f6a832-18f0-49f3-8d15-566a5c2bee84/image.png" alt="">간단하게 OT가 끝나고, 레크레이션을 시작했는데요, 
좀 새로운 레크레이션이 많아서 시간이 빨리 갔어요.
<em>( 대문자 I인간에게는 힘들었음...흑흑 )</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/16420edd-aa1e-46a5-ad74-22d1505c4694/image.png" alt="">힘들었지만 짧은 시간 동안 같은 테이블 팀원분들과 가까워졌답니다!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/9612f898-befb-42b1-9ff3-008790cb108e/image.png" alt="">레크레이션에서는 맞은편 팀원의 얼굴을 종이를 보지 않고, 얼굴만 보고 그리는 시간도 가졌는데요?
저는 금손 디자인 팀원분이 그려주셔서 사람 얼굴이 나왔답니다!!
1분?정도 짧은 시간이었는데, 저렇게 디테일하게 그리시다니..역시 금손이에요</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/40b12fbe-a8bb-472d-8965-5f328029ecd9/image.png" alt="">해커톤에 참여하면 신한의 이달의 희망이 될 수 있답니다?
<em>( 일석이조 미쳤다...진짜 미쳤다..아..😫 )</em>
킥오프 때 같은 테이블이었던 팀원분들과 추억을 남기지 못한게 아쉬웠는데, 이렇게 박제되어서 반가웠어요~
<em>( 많은 얘기를 나눠보지는 못했지만, 너무나 좋은 분들이었습니다!! 뒤늦게 몰래 전해보는 속마음이에요..🫶 )</em></p>
<p>이렇게 1일차 킥오프가 끝나고 과제가 생겼어요.
다음날 발표할 <strong>자기 pr 준비하기</strong> 에요..
<img src="https://velog.velcdn.com/images/heeflee_1310/post/4c5272ed-94ce-4ee2-a283-899fa8b32645/image.png" alt="">자기 PR은 팀빌딩에 꼭 필요한 시간이긴 하지만, 항상 부끄럽더군요..
제가 어떤 사람인지, 함께 하고 싶은 팀원상을 <em>(기억상)</em> 3분이라는 짧은 시간 내에 전달하고자 수정을 무한반복 하다보니, 어느덧 새벽 3시에요...</p>
<p>전 여기서부터 느꼈어요.</p>
<h4 id="이-해커톤생각보다-더-바쁘겠다"><em>이 해커톤...생각보다 더 바쁘겠다..</em></h4>
<p><em>( 이런 과제는 사전에 공지해주셨다면, 촉박하지 않게 준비할 수 있었을 것 같아요! )</em></p>
<hr>
<h2 id="162일차">1/6(2일차)</h2>
<blockquote>
<p>자기 PR &amp; 기업 소개 1차</p>
</blockquote>
<p>오늘은 자기 PR 1차날이에요!
총 49명이 발표했고, 백엔드&gt;프론트&gt;디자인 순이었어요!</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/7d972843-cb08-4dca-b8f4-8cccd2fb6452/image.png" alt="">역시나 대단하신 분들 가득이었어요..
개인적으로 대외활동을 하면 할수록 제가 우물 안 개구리였구나를 느낍니다!
오늘도 우물 밖 세상을 맛보았어요!!! 
긍정적 자극 많이 받아요. 아 오늘 도움 많이 된다.</p>
<p>그런데...</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/bb0b28fd-2f0e-4f88-b958-563440fb2190/image.png" alt="">ㄹㅈㄷ개큰 사고...
저는 프론트 협업을 꿈꾸며 들어온건데, 팀당 프론트가 1명이래요...
<em>(프론트 아예 없는 1팀도 있어요..)</em></p>
<p>매니저님 말씀으로는 프백 비율을 맞추기 위해 총점이 높은 지원자분들을 불합격시키는 건  형평성에 맞지 않다고 판단되어 비율 생각하지 않고 총점 순으로 뽑으셨다고 하셨지만...
팀에서 프론트가 나 혼자라니...!!혼자라니...!!!
일단 좋은 팀원들 만나고 싶어서 털린 멘탈은 미래의 나에게 일단 토스해봐요.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/e4d7ceeb-78e5-44c3-a9f2-bbfb11541c51/image.jfif" alt=""> 긴장 가득한 저의 발표... 어떠셨을까요..?
저도 제가 이렇게 많이 떨줄 몰랐어요..다들 좋게 봐주셨기를 기도했답니다..🙏🙏</p>
<p>오늘은 오전부터 시작한 스케줄이라 중간에 점심시간이 있었는데요! 
매니저님이 맘스터치 햄버거를 사주셨어요~ 
<img src="https://velog.velcdn.com/images/heeflee_1310/post/efe8d6d9-e1b7-4ea0-9567-ecf3ebd617e0/image.png" alt="">맛있다! 내일부터는 팀원분들과 먹는데요!!
아직 팀빌딩도 안했지만, 벌써 기대되는 첫 회식?이에요!!!</p>
<p>오후까지 자기 PR을 마무리한 뒤, 팀빌딩을 진행했답니다!
2기 팀빌딩은 개발자는 한팀에 최대 3명, 팀 전체 최대 인원은 5명까지 가능했어요.
2기 기준으로 가장 이상적인 팀 비율은 <strong>백엔드 2 : 프론트 1 : 디자인 2</strong> 이었습니다!</p>
<p>팀빌딩은 뭐 따로 시간을 주시지는 않고, 
그냥 닌자에 빙의해서 자기 pr로 얻을 수 있는 정보로 각자 원하는 분들에게 연락했답니다!
그래서 다들 PR 시간 후반부가 될수록 핸드폰과 노트북으로 바쁘게 연락하셨어요!
<em>( 아닐수도..저만 그런거일수도.. )</em></p>
<p>저희 팀은 조금 빠르게 팀빌딩이 완료되었습니다!<em>( 속도감 호감. )</em>
훌륭한 개발자분들과 디자이너분들이 많았지만, 어떤 분들과 협업핏이 맞을지 자기 PR내내 메모를 남겼다가 자기 PR이 끝나자마자 바로 연락을 드렸어요~💌
<img src="https://velog.velcdn.com/images/heeflee_1310/post/a6af14c8-25e5-41ab-8972-72a2656ec5e6/image.jfif" alt="">조금 빠르게 움직여서 원하던 팀원분들을 다 모셔올 수 있었어요!!
<em>( 사랑해 팀 컨지!🫶 )</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/6b087d8c-3ebc-4173-9553-b3a24fc9f81c/image.png" alt="">우리의 첫 추억...✨ <strong>팀 컨지 빌딩 완료!!!</strong></p>
<hr>
<h2 id="173일차">1/7(3일차)</h2>
<blockquote>
<p>기업 소개 2차</p>
</blockquote>
<p>오늘부터는 팀별로 점심시간을 가지게 되었답니다~<em>( 두근두근 팀 활동!! )</em>
<img src="https://velog.velcdn.com/images/heeflee_1310/post/9bb11d32-1027-4832-b967-365bda367f55/image.png" alt="">오늘의 타임테이블입니다.
총 8개의 기업이 참여하다보니, 기업 소개에 1.5일을 사용할 수 밖에 없었어요..!!
<em>( 지치지?않아?요... )</em></p>
<p>팀 컨지의 첫 회식은
<img src="https://velog.velcdn.com/images/heeflee_1310/post/c63a258a-1db2-495e-ab30-37987b4e543d/image.png" alt="">베트남 음식이었답니다!</p>
<h3 id="꼭-기억하십쇼">꼭 기억하십쇼.</h3>
<h3 id="꾸에흐엉-볶음밥-해물비빔면">꾸에흐엉. 볶음밥. 해물비빔면.</h3>
<p>저에게 명동은 정말 많이 온 곳이지만, 맛집을 발견할 수 없는 곳이었는데요...
꾸에흐엉 진짜 맛있어요...이제라도 알아서 너무 좋아요..</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/0cb337b7-e2fe-4cd4-b155-c76d64beddde/image.png" alt="">팀원분이 근처에 위치한 헤이티가 맛있다고 해서 트라이해봤는데요!!
저 치즈폼 처음 먹어보는데 사랑에 빠졌어요...포도와 치즈의 만남 최고다..</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/61f62694-35e1-4d64-bcf0-fb80df3ccadc/image.png" alt="">밥먹고 아직은 어색한 팀컨지의 모습이 도촬당했답니다! 
아쉽게도 저희 팀원 분 한분이 일정이 있어 같이 도촬당하지 못한게 아쉬워요~</p>
<p>사진을 남기지는 못했지만, 저희 팀은 기획안 작성을 위해 오후 20:30까지 야근?을 하고 집에갔답니다.. 오늘 불참했던 팀원분은 일정을 끝내고 다시 명동으로 출근해서 같이 회의를 진행했어요!!
열정가득걸들...<em>( 성별은 다수결 따름 )</em>
덕분에 저희는 하루 만에 기업 선정과 메인 기획에 대한 큰 틀을 잡을 수 있었답니다!</p>
<hr>
<h2 id="184일차">1/8(4일차)</h2>
<blockquote>
<p>팀빌딩 &amp; 특강</p>
</blockquote>
<p>오늘은 팀별로 연계할 기업에 대해 논의하는 시간과 특강을 들었어요!</p>
<p>오전 오후에 한타임씩 특강을 들었습니다.
특강해주신 두분 다 너무 본받을 점이 많은 멘토분들이셨어요! 
현재의 제가 가진 고민과, 미래의 제가 가질 수 있는 고민들에 대해 많은 인사이트를 얻을 수 있는 특강들이었습니다!
특강 내용을 참고하며 어떤 기업을 선정하는게 좋을지 고민했어요~</p>
<p>오늘 드디어 팀명을 정했습니다!!!
<em>( 개인적으로 팀 활동에서 가장 중요한 부분이라 생각합니다. )</em>
여러 아이디어 중 마음이 움직이는 네이밍이 나오지 않아 고민하던 찰나
나왔어요!!
<strong>&#39;Ctrl+G&#39;</strong>가 나왔어요!!!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/395664ae-2bda-491a-af63-9e1e0fe18d06/image.png" alt=""><strong>&#39;그룹화&#39; 기능 단축키</strong>라는 뜻이 디발자(디자이너+개발자)에게 너무나 매력적인거에요~
그래서 이렇게 <strong>Team.Ctrl+G(컨트롤지)</strong>가 되었답니다!
<em>( 다시 봐도 호감. )</em></p>
<p>그 뒤 기업 선정에 있어서 저희팀은 꽤 다이나믹한 의견 변경이 있었는데요! 
이 과정이 참 즐거웠어요!!</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/824cfc86-6617-4ff8-9f5e-4c27c6b93abe/image.png" alt="">팀 구성안 및 기업 선정 제출 형식과 취업 컨설팅 일정을 공유해주셨어요!!</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/2e74c637-32b2-41c2-a431-2d84710b6841/image.png" alt="">오늘은 명동교자에서 비빔국수를 먹었습니다! 
먹으면서 ㄹㅈㄷ 빌런 팀원 썰 들어서 입도, 귀도 매운 흥미진진한 시간이었어요!!</p>
<hr>
<h2 id="195일차">1/9(5일차)</h2>
<blockquote>
<p>기업 선정과 기획안 작성 및 제출</p>
</blockquote>
<p>기업 선정과 기획안 문서는 팀원 소개와 희망 기업을 작성한 뒤, 해당 기업별 기획안을 제출하는 방식이었습니다.
저희는 어제 1지망 기업의 기획안을 어느정도 진행해서 그런지, 조금은 수월하게 문서를 작성한뒤 제출했어요!</p>
<p>어제 집중해서 기획안을 완성해서 그런지, 오늘은 제출 형식에만 힘을 썼어요!</p>
<p>기획안 제출 후 공정한 자리뽑기를 시작했어요!
진짜 그냥 운에 맡긴 뽑기였어요!
앞으로 6주 동안 보낼 공간이라 다들 도파민과 긴장 그 사이 어딘가의 상태로 화면만 쳐다봤답니다..</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/14a2efd1-8334-4288-9750-cec4ad55abc7/image.png" alt="">두둥탁탁..ㅠㅠ
10팀 중 저희가 마지막 순서로 뽑히게 되어서 선택권이 없었답니다ㅠㅠ</p>
<p>자리가 어떻게 구성되어있는지 미리 전달받은 내용이 없어서 &#39;이미 좋은 자리는 다 나가겠다ㅠㅠ&#39;라는 아쉬움을 가지고 처음으로 활동 구역으로 올라갔습니다!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/f5c8024c-2783-40e2-af42-5c0063e2654d/image.png" alt="">6주 동안 이 쾌적한 공간에서 생활한다니<del>갑자기 취직 한 것 같아서 두근두근했어요</del></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/58c890f5-997e-477c-a98d-3a027a141291/image.png" alt="">그런데 자리가 너무 좋은거에요!!
사실 어느 자리든 다 좋았을 것 같긴한데, 저희 팀원 모두 &quot;넓은 공간 선호&quot;걸들이어서 선택권 없이 배정된 자리가 오히려 좋았습니다!
앞으로 이름표 없이 활동해서, 네트워킹을 위해 종이 명패를 만들어서 각자 자리에 붙여놨답니다!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/1c82e336-0464-4420-b608-ccaae22e5243/image.png" alt=""><em>( like 무한상사..헿~우리팀 벌써부터 재밌다 )</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/8ec8db31-bbc2-4b69-804d-8341eb0312dd/image.png" alt="">팀 구역에 자리를 풀고 저희가 넘..신나버렸는지 게임을 진심으로 해버렸어욥..ㅎㅎ
그래서 매니저님께 &#39;금쪽이&#39;타이틀을 얻었답니다!
<em>( 사실 저희팀 금쪽이 타이틀 좋아했어요..팀 ctrl금쪽이들..ㅎㅎ )</em>
매니저님의 새로운 네이밍 덕분에 닉값하는 팀이 되었답니다! 헿 0_&lt;~</p>
<p>잠시 휴식을 즐겼다가 다시 본업으로 돌아왔어요.</p>
<p>첫 주의 마지막 날은 협업 환경 구성하고, 다음 주 할일을 정했습니다!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/ba576d53-eac0-4cdd-a425-9135b10aa5ca/image.png" alt=""> 팀 노션을 호다닥 만들고, 다들 경력직답게 빠르게 노션 템플릿을 만들었어요!</p>
<p>저희 팀은 이번 프로젝트에서 &#39;스크럼&#39;방식을 진행하기로 했어요!</p>
<p>이말은 뭐냐, 6주 동안 아주 바쁜 일상을 보낸다는 말이었어요...!!!
항상 워터폴에 가까운 방식으로 프로젝트를 진행해왔기에 이번 협업 방식이 무척 기대되었답니다!<em>( 걱정 한스푼 아니 한컵과 함께... )</em></p>
<p>저희 팀이 정한 스크럼 방식은 아래와 같습니다!</p>
<ul>
<li>사용자 니즈에 가까운 산출물을 만들기 위해, 주별 목표를 정하고 매주 MVP 배포를 진행한다.</li>
<li>매일 아침 일과 시작 전 약 5분정도 모든 팀원들과 진행 상황과 이슈를 공유하는 데일리 스크럼을 가진다.</li>
<li>매주 금요일은 다음 주 일정을 위해 스프린트 회의를 진행한다.</li>
</ul>
<p>저희 팀은 노션 캘린더에 각각의 이벤트를 해당 일자 속 페이지를 만들어서 공유했습니다!
각자 다른 취업 패키지 일정(사진 촬영, 자소서 컨설팅, 모의 면접)과 그 외의 이슈(조퇴, 결석 등)를 캘린더에 미리 다 작성한 뒤 큰 계획을 잡기 시작했어요!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/590aeb2d-fde0-4419-a0ee-2bdc845afb02/image.png" alt=""> 짜잔!오늘 해야할 일이랍니다!</p>
<p>현업에 가까운 협업 환경을 위해 메신저로 슬랙을 선택했어요!
빠르게 슬랙도 판 뒤, 규칙 기반 채널들을 생성했답니다~
<img src="https://velog.velcdn.com/images/heeflee_1310/post/68cd8b2b-e8ba-42c9-824d-b8fc2ae7a047/image.png" alt=""></p>
<p>이렇게 협업 환경을 구성 후 이제 본격적인 업무를 시작했습니다!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/df18c1fd-cacd-4291-897e-696bfd459c24/image.png" alt="">스크럼 방식을 선택하였기에 저희는 다음 주 할 일을 오늘 다 구성해 놨어야 했습니다.
약 3시간 동안 논의를 통해 백로그 정의 후 우선순위 기반으로 MVP 선정 후 다음 주 목표를 정하였답니다..!
<em>( 나혼자..프론트...다시봐도 까마득했던 순간이 기억납니다..)</em></p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/74f762a6-a263-47d3-a46a-453ce2b02ed1/image.png" alt="">사실 저는 엄청난 추억 강박증 환자인데요
소소한 재미를 남기고 싶어서 노션에 <strong>daily.log</strong>를 만들었어요!!
그냥 별건 없고 각자 남기고 싶은 아무말 대잔치 공간이었어요~</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/5794827f-ac4d-424c-8757-d5459a10afe3/image.png" alt="">이렇게 본격적이고 재미있는 해커톤이 시작되었답니다!!</p>
<h4 id="쾌조의-스타트-1주차-기록-끝">쾌조의 스타트, 1주차 기록 끝!!</h4>
<blockquote>
<p>** 사진 출처**</p>
<p>신한금융희망재단 블로그에서 추억 몇개 가져왔습니다..아자스..👍
팀컨지 <em>(곧)</em> 인플루언서님의 블로그에서 줍줍해왔습니다..아자스2..👍</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[신한 스퀘어브릿지 청년 해커톤 2기] 01. 합격 후기]]></title>
            <link>https://velog.io/@heeflee_1310/%EC%8B%A0%ED%95%9C-%EC%8A%A4%ED%80%98%EC%96%B4%EB%B8%8C%EB%A6%BF%EC%A7%80-%EC%B2%AD%EB%85%84-%ED%95%B4%EC%BB%A4%ED%86%A4-2%EA%B8%B0-%EC%A7%80%EC%9B%90-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@heeflee_1310/%EC%8B%A0%ED%95%9C-%EC%8A%A4%ED%80%98%EC%96%B4%EB%B8%8C%EB%A6%BF%EC%A7%80-%EC%B2%AD%EB%85%84-%ED%95%B4%EC%BB%A4%ED%86%A4-2%EA%B8%B0-%EC%A7%80%EC%9B%90-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 18 Feb 2026 07:22:09 GMT</pubDate>
            <description><![CDATA[<p><strong>신한 스퀘어브릿지 청년 해커톤 2기</strong> 활동의 A-Z를 기록해보고자 합니다.
적지 않은 대외활동을 해왔지만, 만족도가 이렇게 높았던 프로그램은 없었습니다..🫶
객관적인 척하려 했는데 사심이 너무 많이 들어갔습니다..💗
그냥 소중했던 6주를 기록해봅니다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/722c13a5-c06e-4175-9272-3ee10a1a8bd3/image.png" alt=""></p>
<h2 id="지원-동기">지원 동기</h2>
<p>취준 1시즌을 보내며 성과 없이 열심히만 하는 생활이 반복되다보니, 자기 효능감과 의욕을 잃었다.</p>
<p>의욕을 잃어버린 것에는 <strong>두가지 이유</strong>가 있다 생각했다.</p>
<blockquote>
<ol>
<li>개발 커리어에서 가장 큰 의욕을 얻고 있던 프로젝트가 점점 부담으로 느껴짐</li>
<li>반복되는 탈락으로 인한 자신감 하락</li>
</ol>
</blockquote>
<p>그래서 극약처방인 <strong>&quot;환경 바꾸기&quot;</strong>에 들어갔다.
지금와서 들어갈 수 있는 동아리나, 그런 여건도 되지 않았기에 조금은 막막하던 찰나 <strong>신한 스퀘어브릿지 청년 해커톤</strong>을 알게되었다.</p>
<p>고민하고 있던 도메인 중 핀테크도 포함되어있고, 해커톤 연계 기업으로 참여하는 기업들이 매력적이었기에 현재 필요한 환경 바꾸기에 최적화된 활동이라 생각했다.</p>
<p>약 1주일이 남은 시점에 발견하여 지원서를 급하게 작성하기 시작했다.</p>
<hr>
<h2 id="서류-작성지원서포트폴리오">서류 작성(지원서+포트폴리오)</h2>
<p>총 4개의 문항이 있었다. </p>
<blockquote>
<p><strong>1. 지원 동기
2. 지원 동기와 관련된 경험
3. 나의 보유 스킬 및 역량
4. 해커톤을 통해 이루고 싶은 목표와 기대하는 점</strong></p>
</blockquote>
<p>위의 문항들에 대해 각각 300~1000자 이내로 작성해야 했다.
여기서 나의 고질병인, &quot;좀만더병&quot;이 발생되버렸다.
난 첫번째 문항에서 6일을 써버렸다.</p>
<p>하고싶은 말은 많고, 깔끔하게 나의 지원동기와 내가 어떤 사람인지 표현하고 싶어서 계속 다듬다가 선택사항으로 제출해야하는 포트폴리오도 미쳐 다 다듬지 못했다!!!
<em>( J 실격이다 )</em></p>
<p>취준 시즌에 가장 어려운 점은 나 자신을 스스로 파악하는 것이 어렵다고 생각한다. 
당연하다고 느껴지는게 나의 특징이라 그 당연한걸 캐치하는 게 너무 어려웠다. 
그래서 6일동안 너무 당연하다 생각해서 작성하지 않았던 나의 특징들을 뒤늦게 특징으로 정의한 뒤 고쳐나갔다.
그 작업을 하고 나니, 나머지 세 문항에서 필요한 소스들을 얻어 5시간 만에 나머지 문항 완성이라는 기적이 일어났다.✨
<img src="https://velog.velcdn.com/images/heeflee_1310/post/146fa589-792e-47d1-876a-d45f3634f061/image.png" alt=""><em>( 하지만, 다시는 이러지 않을 것. J 실격 2 )</em></p>
<p>마감 당일 제출을 먼저한 뒤, 포트폴리오 작업을 시작했다.
<em>( 선택 사항인 포트폴리오는 이메일 제출이었다. 선택이지만, 필수입니다✨ )</em></p>
<p>기존에 만들어둔 오리진 포트폴리오가 있어서 추가로 디자인과 내용을 다듬었는데,
제출하고 나서 보니 내용 실수가 2개나 있었다...
아찔했지만, <em>이미 늦음.</em> </p>
<p>그렇게 4일 뒤, 
문자가 왔다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/72afc026-2cfa-45d6-94f6-158c67d50a33/image.jpg" alt=""></p>
<hr>
<h2 id="면접대면">면접(대면)</h2>
<p>면접은 오래 준비하지 못했다.
이유는 정확히 기억나지 않지만, 무언가로 인해 무척 바빠서 전날 겨우 시작한 게 전부였다.
자기소개와 기술 면접 준비만 간신히 챙긴 채 명동으로 향했다.</p>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/3ddf680e-ef9f-4676-a3c0-b45ea50a56b4/image.png" alt="">면접장에 들어가니 출석 체크 후 이름표(스티커)를 받고, 면접 조 뽑기가 진행되었다.
기억상 4번째 조였나... 첫 번째와 마지막은 아니었는데,
갑자기 우리 조가 첫 번째로 면접에 들어간다는 거 아닌가요..????
<img src="https://velog.velcdn.com/images/heeflee_1310/post/ec899fdb-5af1-4bce-8159-b2d0df65769a/image.png" alt=""><em>( 당황했지만, 오히려 좋아. 매도 먼저 맞는 게 낫지. 그래도 좀 당황스러운 건 어쩔 수 없었다 )</em></p>
<p>4인 1팀으로 약 15분간 팀 면접이 진행되었다.
면접 문항은 총 3개였고, 답변에 따라 짧은 꼬리 질문이 몇 개 이어졌다.</p>
<blockquote>
<ol>
<li>자기소개</li>
<li>최근에 관심있게 본 빅테크 기업이나 스타트업</li>
<li>개발하면서 팀으로 어떤 상황이 가장 저하되는 요인인지, 그리고 개발자로서 나의 단점이 무엇인지</li>
</ol>
</blockquote>
<p>개발 파트 지원이라 기술 면접이 있을거라 긴장했는데, 인성 면접 위주여서 한결 편하게 임할 수 있었다.</p>
<p>면접관들은 총 4분이셨다. 
해커톤이 끝난 시점에서 돌아보니, 해커톤 사업 담당 대리님과 팀장님, 그리고 멘토님 2분이셨던 것 같다.</p>
<p>면접 분위기는 무척 좋았다. 
짧지만 따뜻한 아이스브레이킹이 있고, 공격적인 질문은 전혀 없었다.
아쉬웠던 건 면접 시간이 너무 짧아서 &#39;나&#39;라는 사람을 다 보여주지 못한 느낌이었다는 것.
이렇게 짧은 시간 안에 사람을 어떻게 평가할 수 있을까, 라는 의문이 남는 면접이었다.</p>
<p>면접을 나쁘지 않게 보긴 했지만, 크게 임팩트 있는 지원자는 아니었다고 판단되었다.
서류와 짧고 임팩트 없는 면접으로 과연 내가 뽑힐 수 있을까, 솔직히 탈락을 50%쯤 예상하고 있었다.
<img src="https://velog.velcdn.com/images/heeflee_1310/post/8141768e-36a1-47b5-afcf-2ac1bc619df4/image.png" alt=""><em>( 면접 후 정신 없던거 티냈던 사람. 명동에 제 이름 자랑하고 다녔어요~ )</em></p>
<hr>
<h2 id="결과">결과</h2>
<p>그렇게 2주 뒤, 
붙었다..! 최종 합격이다!!!
<img src="https://velog.velcdn.com/images/heeflee_1310/post/b7c15371-eeb3-4933-b51e-82a3d1ec3037/image.png" alt=""></p>
<p>최종 합격 후에는 기한 내에 제출해야 하는 서류가 2가지 있었다.
<em>( 제출하지 않으면 탈락이다. )</em>
프로그램 내 취업 역량 강화를 위한 취업 패키지의 일환으로,
자소서 멘토링과 모의 면접 멘토링에서 사용할 <strong>이력서</strong>와 <strong>자기소개서</strong>였다.</p>
<p>이미 취준 1시즌을 보낸 덕분에 준비된 서류가 있었고, 조금만 다듬어 제출하면 됐기에 크게 어렵지는 않았다.</p>
<p>다만 한 가지 아쉬운 점이 있다면,
자소서 제출 형식이 따로 정해져 있지 않아서 희망 기업 지원 동기를 1,000자 정도만 작성해 제출했는데,
막상 자소서 멘토링을 받고 나니 _여러 문항을 더 작성해서 낼걸 _ 싶은 후회가 남았다.</p>
<p>자세한 이야기는 다음 후기에서.. ✨</p>
<hr>
<p><img src="https://velog.velcdn.com/images/heeflee_1310/post/08daf0cc-1fb5-4e8e-a640-2e9d1bed1147/image.png" alt="">합격이라는 글자에 설레면서도, 앞으로 만날 6주가 살짝 두렵기도 했다.</p>
<p>지난 2년은 고난과 시련을 파도타기하듯 <em>&#39;아 즐겨~🤩&#39;</em> 하는 마인드로 버텼는데,
요즘 들어서는 그냥 파도에 휩쓸리며 <em>&#39;살려주세요..😢&#39;</em> 하면서 겨우겨우 헤쳐나가고 있었다.</p>
<p>이번 합격이, 그 마인드를 다시 장착하는 데 꽤 큰 도움이 됐다. 😎</p>
<p>앞으로 어떤 고난과 시련과 성장이 기다리고 있을지 모르겠지만,</p>
<p><strong>덤벼.</strong></p>
]]></description>
        </item>
    </channel>
</rss>