<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Mee-</title>
        <link>https://velog.io/</link>
        <description>Mee-</description>
        <lastBuildDate>Mon, 11 Aug 2025 07:37:58 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Mee-</title>
            <url>https://velog.velcdn.com/images/mini_suyo/profile/8949c963-09a0-4504-86e2-6ff79e4d14bf/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Mee-. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mini_suyo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[ComitChu 개발기] Vite에서 로컬은 되는데 배포하면 이미지가 안 보이는 문제와 해결 방법]]></title>
            <link>https://velog.io/@mini_suyo/ComitChu-%EA%B0%9C%EB%B0%9C%EA%B8%B0-Vite%EC%97%90%EC%84%9C-%EB%A1%9C%EC%BB%AC%EC%9D%80-%EB%90%98%EB%8A%94%EB%8D%B0-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%A9%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EC%95%88-%EB%B3%B4%EC%9D%B4%EB%8A%94-%EB%AC%B8%EC%A0%9C%EC%99%80-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@mini_suyo/ComitChu-%EA%B0%9C%EB%B0%9C%EA%B8%B0-Vite%EC%97%90%EC%84%9C-%EB%A1%9C%EC%BB%AC%EC%9D%80-%EB%90%98%EB%8A%94%EB%8D%B0-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%A9%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EC%95%88-%EB%B3%B4%EC%9D%B4%EB%8A%94-%EB%AC%B8%EC%A0%9C%EC%99%80-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 11 Aug 2025 07:37:58 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-상황">1. 문제 상황</h2>
<p>React + Vite 프로젝트를 개발하던 중, 다음과 같이 이미지 경로를 설정했다.</p>
<pre><code class="language-tsx">// ChuViewer.tsx (초기 코드)
const chuImagePath = `/src/assets/images/chu/happy/${mainChu.lang}.png`;
const backgroundImagePath = `/src/assets/images/backgrounds/${mainChu.background}.png`;</code></pre>
<ul>
<li><strong>개발 서버(localhost)</strong>에서는 이미지가 정상 표시됐다.</li>
<li><strong>프로덕션 빌드(배포)</strong>에서는 이미지가 표시되지 않고 404가 발생했다.</li>
</ul>
<p>로컬에서는 됐지만, 배포 환경에서 경로가 깨지는 현상이 있었다.</p>
<hr>
<h2 id="2-원인-분석">2. 원인 분석</h2>
<p>Vite 개발 서버는 <code>/src/...</code> 경로를 <strong>파일 시스템 기준으로 직접 매핑</strong>해줬다. 그래서 개발 중에는 위 코드가 동작했다.
그러나 <strong>빌드 시</strong> Vite는 <code>src/assets</code>의 파일을 <code>dist/assets</code>로 복사하면서 <strong>해시 파일명</strong>으로 변환했다.</p>
<p>예시:</p>
<pre><code>happy/en.png → happy/en.4a6c21ef.png</code></pre><p>코드에 남아 있던 <code>/src/...</code> 경로는 빌드 후 실제 파일 위치와 일치하지 않았고, 그 결과 배포 환경에서 자산을 찾지 못했다.</p>
<hr>
<h2 id="3-올바른-해결-방법--new-url-importmetaurl">3. 올바른 해결 방법 – <code>new URL(..., import.meta.url)</code></h2>
<p>동적으로 구성되는 자산 경로에는 Vite가 빌드 타임에 추적·치환할 수 있는 문법을 사용했다.
<code>new URL(path, import.meta.url).href</code>를 사용하니 빌드 시 해시가 반영된 실제 경로로 자동 변환됐다.</p>
<pre><code class="language-tsx">// 수정된 ChuViewer.tsx
const chuImagePath = new URL(
  `../../assets/images/chu/happy/${mainChu.lang}.png`,
  import.meta.url
).href;

const backgroundImagePath = new URL(
  `../../assets/images/backgrounds/${mainChu.background}.png`,
  import.meta.url
).href;</code></pre>
<p>이렇게 수정하니:</p>
<ul>
<li>개발 서버에서는 원본 경로로 정상 표시됐다.</li>
<li>프로덕션 빌드에서는 해시가 포함된 파일 경로로 자동 치환되어 문제없이 표시됐다.</li>
</ul>
<hr>
<h2 id="4-또-다른-방법--importmetaglob로-폴더-일괄-매핑">4. 또 다른 방법 – <code>import.meta.glob</code>로 폴더 일괄 매핑</h2>
<p>여러 개의 이미지를 조건에 따라 불러와야 하는 경우, <code>import.meta.glob</code>를 사용하면 폴더 전체를 객체 형태로 매핑할 수 있었다.
이 방법은 반복적인 <code>new URL</code> 작성 없이도 모든 이미지를 한 번에 불러올 수 있었다.</p>
<pre><code class="language-tsx">// chu 폴더 내 모든 이미지 매핑
const chuImages = import.meta.glob(&#39;../../assets/images/chu/happy/*.png&#39;, {
  eager: true,
  import: &#39;default&#39;,
});

// backgrounds 폴더 내 모든 이미지 매핑
const backgroundImages = import.meta.glob(&#39;../../assets/images/backgrounds/*.png&#39;, {
  eager: true,
  import: &#39;default&#39;,
});

// 사용 예시
const chuImagePath =
  chuImages[`../../assets/images/chu/happy/${mainChu.lang}.png`];
const backgroundImagePath =
  backgroundImages[`../../assets/images/backgrounds/${mainChu.background}.png`];</code></pre>
<ul>
<li><code>eager: true</code>로 즉시 로드되도록 했다.</li>
<li><code>import: &#39;default&#39;</code>로 URL 문자열만 가져오도록 했다.</li>
<li>빌드 시 해시가 반영된 실제 경로로 자동 치환되어, 로컬과 배포 환경 모두에서 안전하게 동작했다.</li>
</ul>
<hr>
<h2 id="5-선택-가이드--new-url-vs-importmetaglob">5. 선택 가이드 – <code>new URL</code> vs <code>import.meta.glob</code></h2>
<table>
<thead>
<tr>
<th>구분</th>
<th><code>new URL(..., import.meta.url)</code></th>
<th><code>import.meta.glob</code></th>
</tr>
</thead>
<tbody><tr>
<td>쓰임새</td>
<td>개별 파일 경로를 동적으로 구성할 때</td>
<td>폴더 전체를 일괄 매핑해 키로 접근할 때</td>
</tr>
<tr>
<td>코드 양</td>
<td>파일마다 1줄씩 선언</td>
<td>폴더당 1번 선언 후 재사용</td>
</tr>
<tr>
<td>타입 안전성</td>
<td>문자열 경로 조합 중심</td>
<td>매핑된 키 집합이 고정돼 비교적 안전</td>
</tr>
<tr>
<td>빌드 추적</td>
<td>명시적 URL로 확실히 추적됨</td>
<td>글로브 패턴으로 폴더 단위 추적</td>
</tr>
<tr>
<td>런타임 비용</td>
<td>매우 낮음</td>
<td><code>eager</code> 사용 시 초기에 메모리 로드 증가 가능</td>
</tr>
<tr>
<td>지연 로드</td>
<td>기본 없음</td>
<td><code>eager: false</code>로 분할/지연 로드 가능(동적 import)</td>
</tr>
<tr>
<td>유지보수</td>
<td>소량 자산에 단순</td>
<td>다수 자산, 스킨/테마처럼 목록 접근에 유리</td>
</tr>
</tbody></table>
<h3 id="권장-기준">권장 기준</h3>
<ul>
<li><strong>파일 수가 적고, 경로가 간단히 결정</strong>된다면 → <code>new URL</code>이 가장 직관적이다.</li>
<li><strong>스킨/로케일/테마처럼 “목록에서 선택”</strong>하는 패턴이라면 → <code>import.meta.glob</code>가 깔끔하다.</li>
<li><strong>번들 크기/초기 로드 최적화</strong>가 중요하다면 → <code>import.meta.glob</code>에서 <code>eager: false</code>로 코드 스플리팅을 활용한다.</li>
</ul>
<pre><code class="language-tsx">// 지연 로드(코드 스플리팅) 예시
const chuImages = import.meta.glob(&#39;../../assets/images/chu/happy/*.png&#39;); // eager 미사용

async function getChuImage(lang: string) {
  const loader = chuImages[`../../assets/images/chu/happy/${lang}.png`];
  if (!loader) return null;
  const mod = await loader();           // 동적 import
  return mod.default as string;         // 해시 반영된 URL
}</code></pre>
<hr>
<h2 id="마무리">마무리</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/2f23b9ca-63e4-4014-a0e2-f5f145a296cf/image.png" alt=""></p>
</blockquote>
<ul>
<li><code>/src/...</code> 직접 경로 표기는 개발 서버에서만 우연히 동작했다.</li>
<li>빌드 후에는 자산 파일명이 해시로 변경되므로, 빌드 타임에 치환 가능한 방식이 필요했다.</li>
<li><strong>동적 이미지 경로</strong> → <code>new URL(..., import.meta.url).href</code></li>
<li><strong>여러 이미지 일괄 로드</strong> → <code>import.meta.glob</code></li>
<li><strong>지연 로드/코드 스플리팅</strong>이 필요하면 <code>import.meta.glob</code>에서 <code>eager</code>를 생략해 동적 import로 처리했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ComitChu 개발기] TypeScript로 빈 객체를 표현하는 방법]]></title>
            <link>https://velog.io/@mini_suyo/TypeScript%EB%A1%9C-%EB%B9%88-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%ED%91%9C%ED%98%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@mini_suyo/TypeScript%EB%A1%9C-%EB%B9%88-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%ED%91%9C%ED%98%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 04 Aug 2025 07:42:07 GMT</pubDate>
            <description><![CDATA[<p>TypeScript에서 빈 객체를 타입으로 표현할 때 가장 흔히 사용되는 표기는 <code>{}</code>이다.
그러나 <code>{}</code>는 겉보기와 달리, 실제로는 <strong>빈 객체를 정확히 표현하는 타입이 아니다.</strong></p>
<p>이 글에서는 <code>{}</code>와 <code>Record&lt;string, never&gt;</code>의 차이점을 정리하고,
정확하게 빈 객체를 표현하고자 할 때 어떤 방식이 더 적절한지를 설명한다.</p>
<hr>
<h2 id="1-는-모든-객체를-허용한다">1. <code>{}</code>는 모든 객체를 허용한다</h2>
<pre><code class="language-ts">const obj: {} = { a: 123 }; // 허용됨</code></pre>
<p>TypeScript에서 <code>{}</code>는 <strong>모든 non-nullish 객체 타입</strong>을 허용하는 타입이다.</p>
<pre><code class="language-ts">const a: {} = { foo: &#39;bar&#39; }; // 허용
const b: {} = [];             // 허용
const c: {} = () =&gt; {};       // 허용
const d: {} = null;           // 오류
const e: {} = undefined;      // 오류</code></pre>
<p>즉, <code>{}</code>는 빈 객체를 의미하지 않는다.
배열, 함수, 키가 존재하는 객체까지 모두 허용한다.</p>
<p>따라서 &quot;빈 객체만 허용하고 싶다&quot;는 의도를 전달하기에는 부족하다.</p>
<hr>
<h2 id="2-recordstring-never가-진짜-빈-객체를-표현한다">2. <code>Record&lt;string, never&gt;</code>가 진짜 빈 객체를 표현한다</h2>
<pre><code class="language-ts">type EmptyObject = Record&lt;string, never&gt;;</code></pre>
<p>이 타입은 <strong>모든 문자열 키에 대해 값이 존재할 수 없는 객체</strong>를 뜻한다.
즉, <strong>아무 키도 가질 수 없는 객체</strong>만 허용된다.</p>
<pre><code class="language-ts">const a: Record&lt;string, never&gt; = {};         // 허용
const b: Record&lt;string, never&gt; = { foo: 1 }; // 오류</code></pre>
<h3 id="왜-never인가">왜 <code>never</code>인가?</h3>
<ul>
<li><code>Record&lt;K, V&gt;</code>는 &quot;K 키에 대해 V 값을 가지는 객체&quot;를 의미한다.</li>
<li><code>never</code>는 절대로 존재할 수 없는 타입이다.</li>
<li>따라서 <code>Record&lt;string, never&gt;</code>는 &quot;어떤 키도 존재할 수 없음&quot;을 의미하며,
<strong>완전히 비어 있는 객체만 통과시킨다.</strong></li>
</ul>
<hr>
<h2 id="3-어떤-상황에서-사용하나">3. 어떤 상황에서 사용하나?</h2>
<h3 id="1-빈-객체를-명확히-제한할-때">1) 빈 객체를 명확히 제한할 때</h3>
<pre><code class="language-ts">function acceptsOnlyEmpty(obj: Record&lt;string, never&gt;) {
  // 어떤 키도 존재해서는 안 된다
}

acceptsOnlyEmpty({});           // 허용
acceptsOnlyEmpty({ foo: &#39;bar&#39; }); // 오류</code></pre>
<h3 id="2-조건부-타입에서-남는-필드가-없음을-표현할-때">2) 조건부 타입에서 &quot;남는 필드가 없음&quot;을 표현할 때</h3>
<pre><code class="language-ts">type FilterString&lt;T&gt; = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type A = { name: string; age: number };
type B = FilterString&lt;A&gt;; // { name: string }

type C = { id: number };
type D = FilterString&lt;C&gt;; // {} ≡ Record&lt;string, never&gt;</code></pre>
<hr>
<h2 id="4-와-recordstring-never-비교">4. <code>{}</code>와 <code>Record&lt;string, never&gt;</code> 비교</h2>
<table>
<thead>
<tr>
<th>표현</th>
<th>의미</th>
<th>키 허용 여부</th>
</tr>
</thead>
<tbody><tr>
<td><code>{}</code></td>
<td>모든 객체 허용</td>
<td>키가 있어도 허용</td>
</tr>
<tr>
<td><code>Record&lt;string, never&gt;</code></td>
<td>키가 없어야 함</td>
<td>키가 하나라도 있으면 오류</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-recordstring-unknown과-recordstring-any의-차이">5. <code>Record&lt;string, unknown&gt;</code>과 <code>Record&lt;string, any&gt;</code>의 차이</h2>
<p>이 두 타입은 키가 존재해도 되며, 값의 타입 해석 방식이 다르다.</p>
<pre><code class="language-ts">type A = Record&lt;string, unknown&gt;; // 값 타입은 unknown → 사용 전 검사 필요
type B = Record&lt;string, any&gt;;     // 값 타입은 any → 검사 없이 통과</code></pre>
<table>
<thead>
<tr>
<th>타입</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><code>Record&lt;string, never&gt;</code></td>
<td>키가 없어야 함</td>
</tr>
<tr>
<td><code>Record&lt;string, unknown&gt;</code></td>
<td>키는 자유, 값은 타입 검사 필요</td>
</tr>
<tr>
<td><code>Record&lt;string, any&gt;</code></td>
<td>키도 자유, 값도 제한 없음</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-구조적-타입-호환-는-왜-recordstring-never에-할당-가능한가">6. 구조적 타입 호환: <code>{}</code>는 왜 <code>Record&lt;string, never&gt;</code>에 할당 가능한가?</h2>
<pre><code class="language-ts">type Result = {} extends Record&lt;string, never&gt; ? true : false;
// 결과는 true</code></pre>
<p>이는 TypeScript의 구조적 타입 시스템 때문이며,
<strong>빈 객체는 &quot;어떤 키도 없는 객체&quot;로 간주되므로</strong>
<code>Record&lt;string, never&gt;</code>와 호환된다.</p>
<p>즉, <code>{}</code>는 타입으로는 느슨하지만,
값으로는 실제로 빈 객체일 경우 <code>Record&lt;string, never&gt;</code>와 호환된다.</p>
<hr>
<h2 id="7-실전-예시">7. 실전 예시</h2>
<h3 id="상태-초기화에서-사용">상태 초기화에서 사용</h3>
<pre><code class="language-ts">interface StringMap extends Record&lt;string, string&gt; {}

const cleared: Record&lt;string, never&gt; = {}; // 모든 키 제거 후 상태 초기화</code></pre>
<h3 id="api-응답에서-빈-객체-검사">API 응답에서 빈 객체 검사</h3>
<pre><code class="language-ts">type ApiResponse&lt;T&gt; = { data: T; error?: string };

function isEmpty&lt;T&gt;(res: ApiResponse&lt;T&gt;): res is ApiResponse&lt;Record&lt;string, never&gt;&gt; {
  return Object.keys(res.data).length === 0;
}</code></pre>
<hr>
<h2 id="8-정리">8. 정리</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천 타입</th>
</tr>
</thead>
<tbody><tr>
<td>모든 객체 (배열, 함수 포함) 허용</td>
<td><code>{}</code></td>
</tr>
<tr>
<td>진짜 빈 객체만 허용</td>
<td><code>Record&lt;string, never&gt;</code></td>
</tr>
<tr>
<td>키는 자유, 값은 안전하게 처리</td>
<td><code>Record&lt;string, unknown&gt;</code></td>
</tr>
<tr>
<td>키도 자유, 값도 자유롭게 처리</td>
<td><code>Record&lt;string, any&gt;</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="마무리">마무리</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/7f230131-73b6-453d-8c31-568aed8a2ef3/image.png" alt="">
TypeScript에서 빈 객체를 정확히 표현하고자 할 때는 <code>{}</code> 대신
<strong><code>Record&lt;string, never&gt;</code>를 사용하는 것이 가장 안전한 방법</strong>이다.</p>
</blockquote>
<ul>
<li><code>{}</code>는 실제로는 빈 객체가 아니며, 의도를 정확히 전달하지 못한다.</li>
<li><code>Record&lt;string, never&gt;</code>는 타입 수준에서 <strong>&quot;정말 아무 키도 없어야 한다&quot;</strong>는 것을 보장한다.</li>
<li>조건부 타입, 상태 초기화, API 응답 등 다양한 실전 상황에서 활용할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ComitChu 개발기] 비슷하지만 완전히 다르다 – TypeScript 선언문 심층 비교]]></title>
            <link>https://velog.io/@mini_suyo/ComitChu-%EA%B0%9C%EB%B0%9C%EA%B8%B0-%EB%B9%84%EC%8A%B7%ED%95%98%EC%A7%80%EB%A7%8C-%EC%99%84%EC%A0%84%ED%9E%88-%EB%8B%A4%EB%A5%B4%EB%8B%A4-TypeScript-%EC%84%A0%EC%96%B8%EB%AC%B8-%EC%8B%AC%EC%B8%B5-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@mini_suyo/ComitChu-%EA%B0%9C%EB%B0%9C%EA%B8%B0-%EB%B9%84%EC%8A%B7%ED%95%98%EC%A7%80%EB%A7%8C-%EC%99%84%EC%A0%84%ED%9E%88-%EB%8B%A4%EB%A5%B4%EB%8B%A4-TypeScript-%EC%84%A0%EC%96%B8%EB%AC%B8-%EC%8B%AC%EC%B8%B5-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Fri, 01 Aug 2025 04:56:45 GMT</pubDate>
            <description><![CDATA[<p>TypeScript는 개발자에게 강력한 타입 시스템을 제공하지만, 겉으로 보기에 비슷한 여러 선언문들 때문에 혼동을 느끼기 쉽다. 특히 React와 같은 프레임워크를 사용하다 보면, 이러한 미묘한 차이를 제대로 이해하지 못해 런타임 오류, 타입 누락, 심지어는 불필요한 번들링 이슈를 겪기도 한다.</p>
<p>이 글에서는 혼동하기 쉬운 TypeScript의 핵심 선언문들을 <strong>코드 예시와 실제 동작 결과</strong>를 중심으로 비교하고, 각각의 선언이 가지는 실용적인 의미를 깊이 있게 분석한다.</p>
<hr>
<h2 id="1-interface-vs-type">1. <code>interface</code> vs <code>type</code></h2>
<p><code>interface</code>와 <code>type</code>은 객체의 형태를 정의할 때 가장 많이 사용되는 문법이다. 둘은 많은 경우에 비슷하게 동작하지만, 몇 가지 결정적인 차이점을 가지고 있다.</p>
<table>
<thead>
<tr>
<th align="center">구분</th>
<th align="center"><code>interface</code></th>
<th align="center"><code>type</code></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>확장 방법</strong></td>
<td align="center"><code>extends</code> 키워드 사용</td>
<td align="center"><code>&amp;</code> (intersection) 사용</td>
</tr>
<tr>
<td align="center"><strong>선언 병합</strong></td>
<td align="center">가능 (Declaration Merging)</td>
<td align="center">불가능</td>
</tr>
<tr>
<td align="center"><strong>표현력</strong></td>
<td align="center">객체 구조 중심</td>
<td align="center">유니언, 튜플, 조건부 타입 등 다양한 표현 가능</td>
</tr>
</tbody></table>
<p><strong>코드 예시 1: 확장과 병합</strong></p>
<pre><code class="language-typescript">// 인터페이스 확장 (extends)
interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}
// 최종 Dog 타입: { name: string; breed: string }

// 타입 별칭 확장 (&amp;)
type AnimalT = { name: string };
type DogT = AnimalT &amp; { breed: string };
// 최종 DogT 타입: { name: string; breed: string }</code></pre>
<p>두 방식 모두 객체를 확장하는 데 사용되지만, <code>interface</code>의 가장 강력한 기능은 <strong>선언 병합(Declaration Merging)</strong>이다. 동일한 이름의 <code>interface</code>를 여러 번 선언하면 TypeScript가 이를 하나로 합쳐준다.</p>
<pre><code class="language-typescript">// 선언 병합 예시
interface Config {
  url: string;
}
interface Config {
  timeout: number;
}
// 최종 Config 타입: { url: string; timeout: number }
// -&gt; 이 기능은 라이브러리에서 타입 확장을 위해 유용하게 사용된다.

type ConfigT = { url: string };
// type ConfigT = { timeout: number }; // 오류 발생: 중복 선언 불가</code></pre>
<p><strong>코드 예시 2: 다양한 표현력</strong>
<code>type</code>은 인터페이스로 표현하기 어려운 복합적인 타입들을 조합할 수 있다는 장점이 있다.</p>
<pre><code class="language-typescript">// 유니언 타입
type Theme = &#39;light&#39; | &#39;dark&#39;;

// 튜플 타입
type Coords = [number, number];

// 제네릭과 조건부 타입
type FilteredArray&lt;T&gt; = T extends (infer U)[] ? U : T;
type StringArray = FilteredArray&lt;string[]&gt;; // string</code></pre>
<hr>
<h2 id="2-import-vs-import-type">2. <code>import</code> vs <code>import type</code></h2>
<p>이 둘의 가장 큰 차이점은 <strong>컴파일 이후 JavaScript 코드에 남는지 여부</strong>이다. <code>import</code>는 값과 타입을 모두 가져올 수 있어 런타임에 필요한 모듈을 불러올 때 사용하며, <code>import type</code>은 순수하게 타입 정보만 불러오기 때문에 컴파일 시점에 완전히 제거된다.</p>
<p><strong>코드 예시</strong></p>
<pre><code class="language-typescript">// types.ts
export type User = { name: string; age: number; };

// utils.ts
export function getUser(name: string) {
  return { name, age: 30 };
}

// main.ts
import { getUser } from &quot;./utils&quot;; // 런타임에 필요한 &#39;값&#39;
import type { User } from &quot;./types&quot;; // 런타임에 필요 없는 &#39;타입&#39;

const user: User = getUser(&quot;Alice&quot;);
console.log(user);</code></pre>
<p><strong>결과 분석</strong>
<code>getUser</code> 함수는 실제로 코드를 실행하는 데 필요한 <strong>값(value)</strong>이다. 따라서 <code>import { getUser }</code> 문은 컴파일된 JavaScript 결과물에 그대로 남아 있게 된다.</p>
<p>반면, <code>User</code>는 단순히 <code>user</code> 변수의 타입을 정의하기 위한 <strong>타입(type)</strong> 정보일 뿐이다. TypeScript는 타입 체크를 마친 후 이 정보를 더 이상 필요로 하지 않기 때문에, <code>import type { User }</code> 문은 컴파일 시점에 완전히 사라진다.</p>
<pre><code class="language-javascript">// 컴파일된 JS 결과 (main.js)
import { getUser } from &quot;./utils&quot;; // getUser는 그대로 남음
const user = getUser(&quot;Alice&quot;); // User 타입 정보는 사라짐
console.log(user);</code></pre>
<p><code>import type</code>을 사용하면 런타임에 불필요한 코드를 제거하여 번들 크기를 최적화하고, 타입 전용 모듈을 명확히 구분해 코드 가독성을 높일 수 있다.</p>
<hr>
<h2 id="3-export-vs-export-type">3. <code>export</code> vs <code>export type</code></h2>
<p>이 두 선언 역시 <code>import</code>와 마찬가지로 <strong>컴파일 이후 JS 결과물에 남는지 여부</strong>가 다르다. <code>export</code>는 값 또는 타입을 내보내며, <code>export type</code>은 오직 타입만 내보낸다.</p>
<p><strong>코드 예시</strong></p>
<pre><code class="language-typescript">// types.ts
export type User = { name: string };
export const VERSION = &quot;1.0.0&quot;;

// main.ts
import type { User } from &quot;./types&quot;;
import { VERSION } from &quot;./types&quot;;</code></pre>
<p><strong>결과 분석</strong>
위 코드를 컴파일하면 <code>VERSION</code>은 JavaScript 코드에 남아있지만, <code>User</code> 타입은 완전히 사라진다.</p>
<pre><code class="language-javascript">// 컴파일된 JS 결과 (main.js)
import { VERSION } from &quot;./types&quot;;</code></pre>
<p><code>export type</code>은 특히 타입만 내보내는 라이브러리나 모듈에서 불필요한 번들링을 막고, 의존성 관계를 깔끔하게 유지하는 데 효과적이다.</p>
<hr>
<h2 id="4-enum-vs-const-enum-vs-as-const">4. <code>enum</code> vs <code>const enum</code> vs <code>as const</code></h2>
<p>상수를 정의할 때 가장 많이 혼동되는 3가지 방식이다. 핵심은 <strong>런타임에 객체가 생성되는지</strong>와 <strong>타입 리터럴로 고정되는지</strong>이다.</p>
<table>
<thead>
<tr>
<th align="center">구분</th>
<th align="center"><code>enum</code></th>
<th align="center"><code>const enum</code></th>
<th align="center"><code>as const</code></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>런타임 객체</strong></td>
<td align="center">존재함 (객체 생성)</td>
<td align="center">존재하지 않음</td>
<td align="center">존재하지 않음</td>
</tr>
<tr>
<td align="center"><strong>JS 결과물</strong></td>
<td align="center">객체 코드가 남음</td>
<td align="center">값만 인라인 처리됨</td>
<td align="center">값만 인라인 처리됨</td>
</tr>
<tr>
<td align="center"><strong>주요 용도</strong></td>
<td align="center">논리적 그룹의 상수</td>
<td align="center">경량화된 상수</td>
<td align="center">타입 리터럴로 고정</td>
</tr>
</tbody></table>
<p><strong>코드 예시 + 결과</strong></p>
<pre><code class="language-typescript">// enum.ts
export enum Status {
  LOADING = &quot;loading&quot;,
  DONE = &quot;done&quot;,
}

export const enum FastStatus { // const enum은 런타임 객체 미생성
  LOADING = &quot;loading&quot;,
  DONE = &quot;done&quot;,
}

export const statusArray = [&quot;loading&quot;, &quot;done&quot;] as const; // 타입 리터럴로 고정
export type StatusLiteral = typeof statusArray[number]; // &quot;loading&quot; | &quot;done&quot;</code></pre>
<p>위 코드를 컴파일하면, <code>enum Status</code>는 런타임에 사용할 수 있도록 객체 코드가 생성된다.</p>
<pre><code class="language-javascript">// 컴파일된 JS 결과 (enum.js)
var Status;
(function (Status) {
  Status[&quot;LOADING&quot;] = &quot;loading&quot;;
  Status[&quot;DONE&quot;] = &quot;done&quot;;
})(Status || (Status = {}));</code></pre>
<p>반면, <code>const enum</code>과 <code>as const</code>는 런타임에 아무것도 남기지 않는다. 사용되는 지점에 해당 값만 그대로 삽입(inline)된다.</p>
<pre><code class="language-typescript">// 사용 예시
console.log(Status.LOADING);
console.log(FastStatus.DONE); // const enum은 값만 삽입됨
const myStatus: StatusLiteral = &quot;loading&quot;;</code></pre>
<p><code>const enum</code>은 성능 최적화에 유리하며, <code>as const</code>는 값 자체를 타입으로 고정시켜 더욱 엄격한 타입 체크를 가능하게 한다.</p>
<hr>
<h2 id="5-declare-키워드">5. <code>declare</code> 키워드</h2>
<p><code>declare</code>는 TypeScript에게 <strong>&quot;이런 형태의 모듈/변수가 어딘가에 존재하니, 타입만 믿고 사용해도 돼&quot;</strong>라고 알려주는 역할을 한다. 실제 구현 코드가 아니기 때문에 JavaScript로 컴파일되지 않는다.</p>
<p>주로 타입스크립트가 기본적으로 인식하지 못하는 모듈이나 전역 변수에 대한 타입을 정의할 때 사용된다.</p>
<p><strong>예시: CSS 모듈 / SVG 임포트</strong>
React 프로젝트에서 CSS 모듈이나 SVG 파일을 <code>import</code> 할 때 타입 오류가 발생하는 경우가 많다. 이때 <code>declare</code>를 사용해 해당 파일의 타입을 선언해 주면 된다.</p>
<pre><code class="language-typescript">// global.d.ts
declare module &quot;*.module.css&quot; {
  const classes: { [key: string]: string };
  export default classes;
}

declare module &quot;*.svg&quot; {
  const content: string;
  export default content;
}</code></pre>
<p><strong>사용 예시</strong></p>
<pre><code class="language-typescript">// Button.module.css
.button {
  background-color: blue;
}

// Button.tsx
import styles from &quot;./Button.module.css&quot;;
import Icon from &quot;./icon.svg&quot;;

// 이제 .css와 .svg 파일을 import해도 타입 오류가 발생하지 않는다.
// styles는 string key를 가진 객체로, Icon은 string으로 인식된다.
console.log(styles.button); // 예: &quot;_button_xyz123&quot;
console.log(Icon);          // 예: &quot;/static/media/icon.svg&quot;</code></pre>
<p><code>declare</code> 덕분에 TypeScript는 <code>.css</code>나 <code>.svg</code> 파일의 존재와 타입을 인식하고, 개발자는 자동완성 같은 편의 기능을 온전히 누릴 수 있게 된다.</p>
<hr>
<h2 id="6-실용적인-선택-기준-가이드">6. 실용적인 선택 기준 가이드</h2>
<p>지금까지 살펴본 내용을 바탕으로, 실제 프로젝트에서 어떤 선언을 선택해야 할지 고민될 때 참고할 수 있는 가이드를 제시한다.</p>
<h4 id="언제-interface를-쓰고-언제-type을-쓸까"><strong>언제 <code>interface</code>를 쓰고 언제 <code>type</code>을 쓸까?</strong></h4>
<ul>
<li><strong><code>interface</code></strong>:<ul>
<li><strong>객체 구조</strong>를 정의할 때</li>
<li><strong>컴포넌트의 Props</strong> 타입을 정의할 때 (가독성 좋음)</li>
<li><strong>확장</strong>이 자주 필요한 경우 (e.g., <code>extends</code>를 통한 상속)</li>
<li><strong>선언 병합</strong> 기능이 필요한 경우 (라이브러리 타입 선언 등)</li>
</ul>
</li>
<li><strong><code>type</code></strong>:<ul>
<li><code>interface</code>로 표현하기 어려운 <strong>복합 타입</strong> (유니언, 튜플 등)을 만들 때</li>
<li>간단한 <strong>타입 별칭</strong>을 만들 때</li>
<li><code>as const</code>를 통해 <strong>타입 리터럴</strong>을 정의할 때</li>
</ul>
</li>
</ul>
<h4 id="import-vs-import-type"><strong><code>import</code> vs <code>import type</code></strong></h4>
<ul>
<li>가져올 대상이 <strong>타입만</strong>이라면 <code>import type</code>을 명시적으로 사용한다.</li>
<li>번들 최적화가 중요하거나, <strong>순환 참조</strong> 우려가 있는 경우에도 <code>import type</code>을 권장한다.</li>
</ul>
<h4 id="export-vs-export-type"><strong><code>export</code> vs <code>export type</code></strong></h4>
<ul>
<li><strong>값과 타입</strong>을 모두 외부에 공개해야 할 때는 <code>export</code>를 사용한다.</li>
<li><strong>타입 정보만</strong> 외부에 공개하고 싶을 때는 <code>export type</code>을 사용한다.</li>
<li>라이브러리를 제작할 때 이 둘을 명확히 구분하면 의존성 관리가 훨씬 수월해진다.</li>
</ul>
<hr>
<h3 id="마무리">마무리</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/84a619c9-ada2-4798-954c-210d55365ce4/image.png" alt="">
이번 글에서 다룬 TypeScript의 선언문들은 겉으로 보기엔 유사하지만, 실제 동작 방식과 목적에 있어 큰 차이를 보인다. 각 선언이 가지는 고유한 특성을 명확히 이해하고 적재적소에 활용한다면, 코드의 가독성은 물론 성능과 유지보수성까지 크게 향상시킬 수 있을 것이다. 이 내용이 여러분의 TypeScript 여정에 실질적인 도움이 되기를 바란다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ComitChu 개발기] GitHub 소셜 로그인 구현]]></title>
            <link>https://velog.io/@mini_suyo/ComitChu-%EA%B0%9C%EB%B0%9C%EA%B8%B0-GitHub-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@mini_suyo/ComitChu-%EA%B0%9C%EB%B0%9C%EA%B8%B0-GitHub-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Wed, 30 Jul 2025 10:03:18 GMT</pubDate>
            <description><![CDATA[<p>ComitChu 프로젝트에서는 GitHub 기반의 소셜 로그인을 OAuth 2.0 방식으로 구현했다.React + Spring Boot 기반 SPA 구조이며, 로그인 흐름은  Authorization Code Grant 방식에 따라 구성했다.
이 글에서는 실제 프론트 코드 기반으로, 로그인 버튼 클릭부터 인증 처리까지의 과정을 정리한다.</p>
<hr>
<h2 id="1-로그인-버튼-클릭-→-github으로-이동">1. 로그인 버튼 클릭 → GitHub으로 이동</h2>
<pre><code class="language-tsx">const handleGitHubLogin = () =&gt; {
  window.location.href = &quot;https://www.comitchu.shop/oauth2/authorization/github&quot;;
};</code></pre>
<ul>
<li>버튼 클릭 시 백엔드가 미리 설정한 GitHub 인증 주소로 이동한다.</li>
<li>이 URL은 Spring Security가 자동으로 생성한 OAuth 인증 엔드포인트다.</li>
</ul>
<hr>
<h2 id="2-github-인증-완료-→-백엔드로-code-전달">2. GitHub 인증 완료 → 백엔드로 <code>code</code> 전달</h2>
<p>GitHub에서 로그인과 권한 허용을 완료하면 아래 URL로 리다이렉트된다.</p>
<pre><code>https://www.comitchu.shop/login/oauth2/code/github?code=abcdef123</code></pre><ul>
<li>이때 전달되는 code는 백엔드가 access token으로 교환할 임시 인증 코드다.</li>
</ul>
<hr>
<h2 id="3-spring-security가-인증-자동-처리">3. Spring Security가 인증 자동 처리</h2>
<p>Spring Security는 아래 과정을 자동으로 처리한다:</p>
<ol>
<li>URL에서 code 값을 추출한다.</li>
<li><code>application.yml</code>에 설정된 <code>client_id</code>, <code>client_secret</code>과 함께 GitHub에 access token을 요청한다.</li>
<li>받은 access token으로 사용자 정보를 요청한다.</li>
<li>GitHub 사용자 정보를 기반으로 <code>OAuth2User</code> 객체를 생성한다.</li>
<li>설정한 <code>OAuthSuccessHandler</code>로 전달해 후처리를 진행한다.</li>
</ol>
<pre><code class="language-java">.oauth2Login(oauth -&gt; 
    oauth.successHandler(oAuthSuccessHandler)
)</code></pre>
<hr>
<h2 id="4-oauthsuccesshandler에서-jwt-발급-및-리다이렉트">4. OAuthSuccessHandler에서 JWT 발급 및 리다이렉트</h2>
<pre><code class="language-java">response.addCookie(jwtCookie);
response.sendRedirect(&quot;https://www.comitchu.shop&quot;);</code></pre>
<ul>
<li><code>OAuthSuccessHandler.java</code>에서 JWT를 생성하고, <code>HttpOnly</code> 쿠키로 브라우저에 전달한다.</li>
<li>이후 사용자는 프론트엔드의 <code>ComitChu 랜딩페이지</code>로 리다이렉트된다.</li>
<li>JWT는 JavaScript에서 접근할 수 없도록 설정되어 있어 보안성이 높다.</li>
</ul>
<hr>
<h2 id="5-프론트엔드는-로그인-상태를-어떻게-유지하는가">5. 프론트엔드는 로그인 상태를 어떻게 유지하는가?</h2>
<p>GitHub 로그인 후, 백엔드는 JWT를 <code>HttpOnly</code> 쿠키로 발급하고, 사용자를 comitchu 페이지로 리다이렉트시킨다.
이때 프론트엔드는 JWT를 직접 다루지 않고, <strong>상태를 확인하기 위한 API 호출</strong>을 통해 로그인 여부를 판단한다.</p>
<p>아래는 프론트엔드 상태 관리를 담당하는 코드 구성이다.</p>
<hr>
<h3 id="5-1-axios-클라이언트-설정">5-1. Axios 클라이언트 설정</h3>
<pre><code class="language-ts">import axios from &quot;axios&quot;;

const apiClient = axios.create({
  baseURL: &quot;/api&quot;, // 백엔드 프록시 경로
  withCredentials: true, // 쿠키 포함 필수 설정
});

export default apiClient;</code></pre>
<ul>
<li><code>withCredentials: true</code> 설정은 백엔드에서 발급한 <strong>HttpOnly 쿠키를 포함한 요청</strong>을 보낼 때 반드시 필요하다.</li>
<li>이 설정이 없으면 브라우저가 쿠키를 백엔드에 포함하지 않는다 → 인증 실패</li>
</ul>
<hr>
<h3 id="5-2-zustand를-활용한-사용자-상태-관리">5-2. Zustand를 활용한 사용자 상태 관리</h3>
<pre><code class="language-ts">import { create } from &quot;zustand&quot;;
import apiClient from &quot;../api&quot;;

interface User {
  userName: string;
  avatarUrl: string;
  pet?: Pet;
}


interface UserState {
  user: User | null;
  isLoggedIn: boolean;
  fetchUser: () =&gt; Promise&lt;void&gt;;
  logout: () =&gt; Promise&lt;void&gt;;
}

const useUserStore = create&lt;UserState&gt;((set) =&gt; ({
  user: null,
  isLoggedIn: false,
  fetchUser: async () =&gt; {
    try {
      const response = await apiClient.get(&quot;/user/me&quot;);
      if (response.data.success) {
        const { userName, avatarUrl } = response.data.data;
        set({ user: { userName, avatarUrl }, isLoggedIn: true });
      } else {
        set({ user: null, isLoggedIn: false });
      }
    } catch (error) {
      // 대충 에러 처리
    }
  },
  logout: async () =&gt; {
    try {
      await apiClient.post(&quot;/user/logout&quot;);
      set({ user: null, isLoggedIn: false });
    } catch (error) {
      // 대충 에러 처리
    }
  },
}));</code></pre>
<ul>
<li><code>fetchUser</code> 함수는 <code>/api/user/me</code> 엔드포인트를 호출해 <strong>현재 로그인된 사용자 정보를 불러온다</strong>.</li>
<li>백엔드는 <code>HttpOnly</code> 쿠키를 통해 사용자를 식별하며, 성공 시 사용자 객체를 반환한다.</li>
<li>응답 결과에 따라 <code>user</code>, <code>isLoggedIn</code> 상태를 전역에서 관리할 수 있다.</li>
<li><code>logout</code> 역시 쿠키 기반 인증 구조를 유지하며 로그아웃 처리 후 상태를 초기화한다.</li>
</ul>
<hr>
<h3 id="5-3-최초-진입-시-사용자-정보-조회">5-3. 최초 진입 시 사용자 정보 조회</h3>
<pre><code class="language-tsx">// src/App.tsx
function App() {
  const { fetchUser } = useUserStore();

  useEffect(() =&gt; {
    fetchUser();
  }, [fetchUser]);</code></pre>
<ul>
<li>App 컴포넌트 마운트 시 <code>fetchUser()</code>를 호출한다.</li>
<li>이 시점에서 쿠키가 브라우저에 존재한다면 자동으로 전송되어 인증이 완료되고, 사용자 정보가 상태에 저장된다.</li>
<li>즉, <strong>앱 전체가 시작될 때 로그인 여부를 판별하고 초기화</strong>한다.</li>
</ul>
<hr>
<h3 id="5-4-로그인-여부에-따라-라우팅-분기">5-4. 로그인 여부에 따라 라우팅 분기</h3>
<pre><code class="language-tsx">&lt;Routes&gt;
  &lt;Route path=&quot;/&quot; element={&lt;Landing /&gt;} /&gt;
  &lt;Route element={&lt;ProtectedRoute /&gt;}&gt;
    &lt;Route path=&quot;/dashboard&quot; element={&lt;Dashboard /&gt;} /&gt;
    &lt;Route path=&quot;/setting&quot; element={&lt;Setting /&gt;} /&gt;
  &lt;/Route&gt;
  &lt;Route path=&quot;*&quot; element={&lt;Error /&gt;} /&gt;
&lt;/Routes&gt;</code></pre>
<ul>
<li><code>/dashboard</code>, <code>/setting</code>과 같은 민감한 경로는 <code>ProtectedRoute</code>로 감싼다.</li>
<li><code>ProtectedRoute</code>에서는 <code>userStore</code>의 <code>isLoggedIn</code> 값을 확인해 인증되지 않은 사용자는 접근을 차단한다.</li>
</ul>
<hr>
<h3 id="5-5-실제-흐름-요약">5-5. 실제 흐름 요약</h3>
<ol>
<li>사용자가 GitHub 로그인 → 백엔드가 JWT를 HttpOnly 쿠키에 저장 후  comitchu 사이트로 리다이렉트</li>
<li>App이 로드되며 <code>fetchUser()</code> 호출 → <code>/api/user/me</code> 요청으로 사용자 정보 확인</li>
<li>상태 저장 → <code>isLoggedIn: true</code>일 경우 대시보드 진입 허용</li>
<li>로그아웃 시 쿠키 삭제 및 상태 초기화 → 자동 로그아웃</li>
</ol>
<hr>
<p>이렇게 ComitChu 프로젝트는 로그인 후 상태를 직접 다루지 않고, <strong>JWT 기반 쿠키 인증과 API 응답 결과만으로</strong> 로그인 여부를 관리하는 구조다.
상태 관리 라이브러리(Zustand), axios 설정, 초기 렌더링 타이밍, 라우팅 보호까지 모두 자연스럽게 연결되어 있어 유지보수가 쉬운 구조다.</p>
<hr>
<h2 id="6-이-구조를-선택한-이유">6. 이 구조를 선택한 이유</h2>
<ol>
<li><p><strong>보안성 확보</strong></p>
<ul>
<li>client_secret을 프론트에 노출하지 않음</li>
<li>JWT를 HttpOnly 쿠키에 저장하여 XSS에 안전</li>
</ul>
</li>
<li><p><strong>구현 간소화</strong></p>
<ul>
<li>Spring Security의 자동 인증 처리 기능을 활용</li>
</ul>
</li>
<li><p><strong>SPA에 적합한 흐름</strong></p>
<ul>
<li>인증 후 프론트로 리다이렉트하여 SPA 환경을 유지</li>
</ul>
</li>
</ol>
<hr>
<h2 id="7-다른-방식과-비교">7. 다른 방식과 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>ComitChu 방식 (백엔드 중심)</th>
<th>프론트 처리 방식</th>
</tr>
</thead>
<tbody><tr>
<td>redirect_uri</td>
<td>백엔드 주소</td>
<td>프론트 주소</td>
</tr>
<tr>
<td>인증 처리 주체</td>
<td>Spring Security</td>
<td>프론트 → API</td>
</tr>
<tr>
<td>JWT 저장 위치</td>
<td>HttpOnly 쿠키</td>
<td>다양함</td>
</tr>
<tr>
<td>보안성</td>
<td>높음</td>
<td>다소 취약 (XSS 가능)</td>
</tr>
<tr>
<td>구조</td>
<td>자동화 활용</td>
<td>유연한 라우팅 처리 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-마무리">8. 마무리</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/c9e68c90-a236-4584-98bc-a852ba2f0b02/image.png" alt="">
ComitChu 프로젝트는 GitHub 로그인을 <strong>React 기반 SPA 구조에서 보안성과 유지보수성을 모두 고려해</strong> 구현했다. 프론트는 로그인 요청만 보내고, 인증의 핵심 로직은 전적으로 백엔드(Spring Security)가 담당한다. 특히 이번 구현 방식은 기존에 경험했던 다른 소셜 로그인 방식들과 확연히 달랐다.
지금까지는 프론트에서 session이나 localStorage에 access token과 refresh token을 저장하거나, OAuth 인증 후 code를 프론트에서 받아 다시 백엔드로 전달하는 구조를 사용해왔다. 하지만 이번에는 <strong>redirect URI부터 access token 요청, 사용자 정보 조회, JWT 발급까지 모든 과정을 백엔드가 직접 처리하는 구조</strong>를 선택했다.
이번 기회에 이 방식을 적용해보니 무척이나 편했다. 그리고 무엇보다, 이런 구조를 함께 고민하고 구현한 멋진 팀원들과 협업하는 일은 역시 좋은 일이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[CORS, 도대체 왜 막는 건데?]]></title>
            <link>https://velog.io/@mini_suyo/CORS-%EB%8F%84%EB%8C%80%EC%B2%B4-%EC%99%9C-%EB%A7%89%EB%8A%94-%EA%B1%B4%EB%8D%B0</link>
            <guid>https://velog.io/@mini_suyo/CORS-%EB%8F%84%EB%8C%80%EC%B2%B4-%EC%99%9C-%EB%A7%89%EB%8A%94-%EA%B1%B4%EB%8D%B0</guid>
            <pubDate>Tue, 29 Jul 2025 10:48:44 GMT</pubDate>
            <description><![CDATA[<p><strong>– 프론트엔드 개발자의 입장에서 이해하는 CORS 이슈와 해결법</strong></p>
<p>웹 개발을 하다 보면, 다음과 같은 에러를 종종 마주친다.
프론트에선 API 호출도 했고, 백엔드에서도 응답을 보내줬는데…</p>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/9b58db91-5428-4f29-8b22-f11bb8690ea8/image.png" alt=""></p>
<p><strong>“아니, 응답은 있는데 왜 못 쓰게 하는 건데?”</strong>
처음엔 너무 억울하다. 그래서 이번 글에서는 <strong>프론트엔드 입장에서 CORS가 왜 발생하는지, 어떻게 해결해야 하는지</strong>를 정리한다.</p>
<hr>
<h2 id="1-cors란">1. CORS란?</h2>
<p><strong>CORS (Cross-Origin Resource Sharing)</strong>: 
브라우저가 출처(origin)가 다른 리소스에 접근할 때 허용 여부를 판단하는 보안 정책이다.</p>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/912d1041-65e2-4d1b-b9a9-e8ff6f277deb/image.png" alt=""></p>
<ul>
<li><p>1.1 <code>출처(origin)</code>이란
<code>프로토콜 + 도메인 + 포트 번호</code> 세 가지를 조합한 것이다.
예를 들어:</p>
<ul>
<li><code>http://localhost:3000</code> → 프론트 서버</li>
<li><code>http://localhost:8080</code> → 백엔드 서버
는 서로 다른 출처다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="2-브라우저가-cors를-강제하는-이유">2. 브라우저가 CORS를 강제하는 이유</h2>
<ul>
<li><p>2.1 보안상의 이유
악성 스크립트가 다른 도메인의 API에 함부로 요청을 보내는 것을 막기 위해</p>
</li>
<li><p><em>브라우저가 먼저 요청을 &#39;차단&#39;*</em>하는 구조다.</p>
</li>
<li><p>2.2 브라우저만 강제
Postman이나 curl 같은 툴에선 아무런 문제가 없다.</p>
</li>
</ul>
<hr>
<h2 id="3-프론트엔드-개발할-때-겪는-흔한-상황">3. 프론트엔드 개발할 때 겪는 흔한 상황</h2>
<h3 id="31-상황-예시">3.1 상황 예시</h3>
<pre><code class="language-ts">// React에서 fetch로 API 호출
fetch(&quot;http://localhost:8080/api/data&quot;)
  .then((res) =&gt; res.json())
  .then((data) =&gt; console.log(data));</code></pre>
<h3 id="32-결과">3.2 결과</h3>
<p><strong>백엔드에서 응답도 잘 오는데... 에러가 발생한다!</strong></p>
<pre><code>Access to fetch at &#39;http://localhost:8080/api/data&#39; from origin &#39;http://localhost:3000&#39; has been blocked by CORS policy</code></pre><hr>
<h2 id="4-어떻게-해결할-수-있을까">4. 어떻게 해결할 수 있을까?</h2>
<h3 id="41-백엔드에서-cors-허용-설정하기-정석">4.1 백엔드에서 CORS 허용 설정하기 (정석)</h3>
<p>백엔드가 클라이언트의 출처를 <strong>허용한다고 명시해줘야 한다.</strong></p>
<h4 id="411-spring-boot-예시">4.1.1 Spring Boot 예시</h4>
<pre><code class="language-java">@CrossOrigin(origins = &quot;http://localhost:3000&quot;)
@GetMapping(&quot;/api/data&quot;)
public Data getData() {
    return new Data();
}</code></pre>
<h4 id="412-nodejs-express-예시">4.1.2 Node.js (Express) 예시</h4>
<pre><code class="language-js">app.use(
  cors({
    origin: &quot;http://localhost:3000&quot;,
  })
);</code></pre>
<hr>
<h3 id="42-프론트에서-프록시-설정하기-개발-환경에서-자주-사용">4.2 프론트에서 프록시 설정하기 (개발 환경에서 자주 사용)</h3>
<p>CRA (React Create App)에서는 <code>package.json</code>에 프록시 설정 가능</p>
<pre><code class="language-json">&quot;proxy&quot;: &quot;http://localhost:8080&quot;</code></pre>
<ul>
<li><p>4.2.1 프론트 서버가 먼저 받고 백엔드로 전달하기 때문에
CORS를 우회할 수 있다.</p>
</li>
<li><p>4.2.2 주의: 이 방식은 <strong>개발 환경 전용</strong>이다.
배포 시엔 사용할 수 없다.</p>
</li>
</ul>
<hr>
<h2 id="5-preflight-요청이란">5. Preflight 요청이란?</h2>
<ul>
<li>5.1 <code>Content-Type: application/json</code> 같이 민감한 헤더가 포함되면
브라우저는 본 요청 전에 **OPTIONS 메서드로 사전 검사 요청(Preflight)**을 보낸다.</li>
</ul>
<pre><code class="language-http">OPTIONS /api/data HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST</code></pre>
<ul>
<li>5.2 백엔드는 이 요청에 제대로 응답해줘야 한다.
안 해주면 역시나 CORS 에러 발생!</li>
</ul>
<hr>
<h2 id="6-마무리하며">6. 마무리하며</h2>
<p>CORS는 <strong>막는 게 목적이 아니라, 허용을 명시하라는 시스템</strong>이다.
프론트 입장에서는 “막혀서 짜증나”지만, <strong>브라우저가 사용자를 지켜주는 장치</strong>라는 걸 이해하고,
<strong>백엔드와 협의해서</strong> CORS 설정을 조율해나가는 게 현실적인 접근이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ComitChu 개발기] 버튼과 입력창을 원자화하다 – 재사용 가능한 UI 컴포넌트 설계기]]></title>
            <link>https://velog.io/@mini_suyo/ComitChu-%EA%B0%9C%EB%B0%9C%EA%B8%B0-%EB%B2%84%ED%8A%BC%EA%B3%BC-%EC%9E%85%EB%A0%A5%EC%B0%BD%EC%9D%84-%EC%9B%90%EC%9E%90%ED%99%94%ED%95%98%EB%8B%A4-%EC%9E%AC%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5%ED%95%9C-UI-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84%EA%B8%B0</link>
            <guid>https://velog.io/@mini_suyo/ComitChu-%EA%B0%9C%EB%B0%9C%EA%B8%B0-%EB%B2%84%ED%8A%BC%EA%B3%BC-%EC%9E%85%EB%A0%A5%EC%B0%BD%EC%9D%84-%EC%9B%90%EC%9E%90%ED%99%94%ED%95%98%EB%8B%A4-%EC%9E%AC%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5%ED%95%9C-UI-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84%EA%B8%B0</guid>
            <pubDate>Tue, 22 Jul 2025 08:53:29 GMT</pubDate>
            <description><![CDATA[<p><strong>ComitChu</strong>는 GitHub 커밋 활동을 기반으로 펫이 진화하는 재미있는 웹 서비스다. 서비스가 점점 커지면서 <strong>공통 UI 요소를 효율적으로 관리하는 구조의 필요성</strong>을 절감했고, 이를 해결하기 위해 <strong>버튼과 입력창을 컴포넌트화(원자화)</strong> 하는 작업을 진행했다.</p>
<hr>
<h2 id="리팩토링이-필요한-이유">리팩토링이 필요한 이유</h2>
<p>초기 코드베이스에는 다음과 같은 문제가 있었다:</p>
<ul>
<li>버튼, 인풋 등의 <strong>기본 요소에 대한 전역 스타일</strong>이 <code>index.css</code>에 혼재</li>
<li><strong>여러 컴포넌트에서 중복된 UI 정의</strong></li>
<li>특정 버튼 색상 하나 바꾸기 위해 <strong>여러 CSS 파일에 흩어진 코드 수정</strong>이 필요함</li>
</ul>
<p>디자인 일관성과 유지보수성을 위해 <strong>공통 UI 컴포넌트 정리 및 원자화 작업</strong>이 필요했다.</p>
<hr>
<h2 id="원자적-디자인atomic-design-접근">원자적 디자인(Atomic Design) 접근</h2>
<p>UI 컴포넌트를 설계하면서 Brad Frost의 <a href="https://bradfrost.com/blog/post/atomic-web-design/">Atomic Design</a> 개념을 도입했다. 이 방법론은 UI를 구성하는 요소들을 <strong>화학 구조처럼 작은 단위부터 큰 단위로 계층화</strong>하여 조직적으로 설계할 수 있도록 돕는다.</p>
<h3 id="atomic-design-계층-및-comitchu의-실제-예시">Atomic Design 계층 및 ComitChu의 실제 예시</h3>
<table>
<thead>
<tr>
<th>계층</th>
<th>설명</th>
<th>ComitChu에서의 예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Atoms</strong></td>
<td>가장 기본적인 UI 요소, 더 이상 분해 불가</td>
<td><code>Button</code>, <code>Input</code>, <code>Label</code>, <code>Icon</code> 등</td>
</tr>
<tr>
<td><strong>Molecules</strong></td>
<td>여러 원자가 결합한 작은 단위 기능</td>
<td>검색폼 = <code>Input</code> + <code>Button</code>, 유저정보 표시 = <code>Avatar</code> + <code>Username</code></td>
</tr>
<tr>
<td><strong>Organisms</strong></td>
<td>여러 분자 및 원자가 모인 UI 블록</td>
<td><code>Header</code> = 로고 + 유저정보 + 로그아웃 버튼</td>
</tr>
<tr>
<td><strong>Templates</strong></td>
<td>Organism을 배치한 레이아웃 구조</td>
<td>대시보드 템플릿 레이아웃</td>
</tr>
<tr>
<td><strong>Pages</strong></td>
<td>실제 콘텐츠가 채워진 화면</td>
<td><code>/dashboard</code>, <code>/setting</code> 등 페이지들</td>
</tr>
</tbody></table>
<p>Atoms 단계에서 만든 <code>Button</code>과 <code>Input</code> 컴포넌트는 이후 Molecule과 Organism 단계의 기반이 되며, 전체 UI의 일관성을 지키는 핵심 요소다.</p>
<hr>
<h2 id="1-button-컴포넌트-구조">1. Button 컴포넌트 구조</h2>
<h3 id="📁-구조">📁 구조</h3>
<pre><code>src/
└─ components/
   └─ common/
      ├─ Button.tsx
      └─ Button.module.css</code></pre><h3 id="✅-핵심-변경-사항">✅ 핵심 변경 사항</h3>
<ul>
<li><code>variant</code> prop을 통해 스타일 분기 처리 (예: <code>primary</code>, <code>danger</code>)</li>
<li>공통 스타일은 <code>Button.module.css</code>에 모듈화</li>
<li>기존의 <code>&lt;button&gt;</code> 태그를 전부 <code>Button</code> 컴포넌트로 교체</li>
</ul>
<h3 id="✅-buttontsx-코드">✅ <code>Button.tsx</code> 코드</h3>
<pre><code class="language-tsx">import styles from &quot;./Button.module.css&quot;;

interface ButtonProps extends React.ButtonHTMLAttributes&lt;HTMLButtonElement&gt; {
  children: React.ReactNode;
  variant?: &#39;primary&#39; | &#39;danger&#39;;
}

const Button: React.FC&lt;ButtonProps&gt; = ({
  children,
  variant = &#39;primary&#39;,
  className,
  ...props
}) =&gt; {
  const buttonClassName = `${styles.button} ${styles[variant]} ${className || &#39;&#39;}`;
  return (
    &lt;button className={buttonClassName} {...props}&gt;
      {children}
    &lt;/button&gt;
  );
};

export default Button;</code></pre>
<h3 id="✅-스타일-예시-buttonmodulecss">✅ 스타일 예시 (<code>Button.module.css</code>)</h3>
<pre><code class="language-css">.button {
  padding: 0.5rem 1rem;
  border-radius: 6px;
  font-weight: 600;
  border: none;
  cursor: pointer;
}

.primary {
  background-color: #4a90e2;
  color: white;
}

.danger {
  background-color: #e74c3c;
}
.danger:hover {
  background-color: #c0392b;
}</code></pre>
<h3 id="✅-사용-예시">✅ 사용 예시</h3>
<pre><code class="language-tsx">&lt;Button&gt;저장&lt;/Button&gt;
&lt;Button variant=&quot;danger&quot;&gt;삭제&lt;/Button&gt;</code></pre>
<hr>
<h2 id="2-input-컴포넌트-구조">2. Input 컴포넌트 구조</h2>
<h3 id="📁-구조-1">📁 구조</h3>
<pre><code>src/
└─ components/
   └─ common/
      ├─ Input.tsx
      └─ Input.module.css</code></pre><h3 id="✅-핵심-변경-사항-1">✅ 핵심 변경 사항</h3>
<ul>
<li>input 태그를 감싼 <code>Input</code> 컴포넌트 생성</li>
<li>필요한 경우 label, error 처리 등 확장 가능하도록 설계</li>
</ul>
<h3 id="✅-inputtsx-코드">✅ <code>Input.tsx</code> 코드</h3>
<pre><code class="language-tsx">import styles from &quot;./Input.module.css&quot;;

const Input = ({ className = &quot;&quot;, ...props }: React.InputHTMLAttributes&lt;HTMLInputElement&gt;) =&gt; {
  return &lt;input className={`${styles.input} ${className}`} {...props} /&gt;;
};

export default Input;</code></pre>
<h3 id="✅-스타일-예시">✅ 스타일 예시</h3>
<pre><code class="language-css">.input {
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 1rem;
}</code></pre>
<hr>
<h2 id="3-로그아웃-버튼-커스터마이징-사례">3. 로그아웃 버튼 커스터마이징 사례</h2>
<p><code>Header.tsx</code>에 있던 로그아웃 버튼에 <code>variant=&quot;danger&quot;</code>를 적용하여 스타일을 분리했다. 이로써 <strong>색상 커스터마이징이 단순해졌고</strong>, <strong>로직과 스타일 간 결합도 낮아졌다</strong>.</p>
<pre><code class="language-tsx">&lt;Button onClick={handleLogout} variant=&quot;danger&quot;&gt;
  Logout
&lt;/Button&gt;</code></pre>
<hr>
<h2 id="개선-효과-요약">개선 효과 요약</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>개선 전</th>
<th>개선 후</th>
</tr>
</thead>
<tbody><tr>
<td><strong>재사용성</strong></td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td><strong>유지보수성</strong></td>
<td>여러 파일 수정 필요</td>
<td>컴포넌트 단위 수정 가능</td>
</tr>
<tr>
<td><strong>스타일 일관성</strong></td>
<td>흐트러짐</td>
<td>공통 컴포넌트로 통일</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>낮음</td>
<td>variant 등으로 분기 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="마무리">마무리</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/dfada26e-4d25-4798-98fb-3e225141ab14/image.png" alt="">
UI의 기초 단위를 공통 컴포넌트로 정리하면서, 구조적 일관성과 유지보수성이 크게 향상되었다. 단순한 스타일링 분리처럼 보일 수 있지만, 실제로는 전체 프론트엔드 아키텍처의 기반을 정리하는 핵심 작업이었다. 프로젝트가 커질수록 이런 구조는 점점 더 큰 힘을 발휘하게 된다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 아이콘 사용 완전 정복: 컴포넌트화와 관리]]></title>
            <link>https://velog.io/@mini_suyo/React-%EC%95%84%EC%9D%B4%EC%BD%98-%EC%82%AC%EC%9A%A9-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%ED%99%94%EC%99%80-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@mini_suyo/React-%EC%95%84%EC%9D%B4%EC%BD%98-%EC%82%AC%EC%9A%A9-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%ED%99%94%EC%99%80-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Mon, 21 Jul 2025 08:44:02 GMT</pubDate>
            <description><![CDATA[<p>1부에서는 React 프로젝트에서 아이콘을 사용하는 다양한 방법을 다뤘다.
(SVG 인라인 삽입, SVG 파일 import, <code>react-icons</code> 설치 방식 등)</p>
<p>이번 2부에서는 복사한 SVG 아이콘을 실전에서 <strong>재사용 가능하게 컴포넌트화</strong>하고,
<strong>props로 제어 가능한 구조로 관리</strong>하는 방법까지 정리한다.</p>
<hr>
<h2 id="1-왜-아이콘을-컴포넌트로-만들까">1. 왜 아이콘을 컴포넌트로 만들까?</h2>
<p>처음엔 <code>&lt;svg&gt;</code>를 그냥 복사해 쓰는 게 편하다.
하지만 아래와 같은 문제가 생긴다.</p>
<ul>
<li>같은 아이콘이 여러 곳에 중복</li>
<li>크기나 색상 변경 시 일일이 수정</li>
<li>프로젝트가 커질수록 관리 어려움</li>
</ul>
<blockquote>
<p>SVG를 <strong>컴포넌트화 + props 기반으로 제어</strong>하면 이 문제를 해결할 수 있다.</p>
</blockquote>
<hr>
<h2 id="2-기본-패턴-svg를-컴포넌트로-만들기">2. 기본 패턴: SVG를 컴포넌트로 만들기</h2>
<pre><code class="language-jsx">// components/icons/HomeIcon.jsx
const HomeIcon = ({ size = 20, color = &quot;#000&quot; }) =&gt; (
  &lt;svg
    width={size}
    height={size}
    viewBox=&quot;0 0 1024 1024&quot;
    fill={color}
    xmlns=&quot;http://www.w3.org/2000/svg&quot;
  &gt;
    &lt;path d=&quot;M946.5 505L534.6 93.4a31.93 31.93 0 0 0-45.2 0L77.5 505...&quot; /&gt;
  &lt;/svg&gt;
);

export default HomeIcon;</code></pre>
<pre><code class="language-jsx">// 사용 예시
&lt;HomeIcon size={24} color=&quot;#333&quot; /&gt;</code></pre>
<hr>
<h2 id="3-실전-예시-정렬-아이콘-ascdesc">3. 실전 예시: 정렬 아이콘 (asc/desc)</h2>
<div style="display: flex; gap: 10%;">
  <img src="https://velog.velcdn.com/images/mini_suyo/post/83c3c066-19fb-46b1-b587-0993564e63f3/image.png" style="width: 30%;" />
  <img src="https://velog.velcdn.com/images/mini_suyo/post/392d4e9c-1c22-4012-a559-d767fe1603a7/image.png" style="width: 30%;" />
</div>

<h3 id="direction-props로-상태-제어">direction props로 상태 제어</h3>
<p>정렬 방향에 따라 아이콘을 달리 보여주는 UI라면,
<code>direction</code>이라는 props로 제어하면 깔끔하다.</p>
<hr>
<h3 id="컴포넌트-예시">컴포넌트 예시</h3>
<pre><code class="language-jsx">// components/icons/SortIcon.jsx
const SortIcon = ({ direction = &quot;asc&quot;, size = 20, color = &quot;#000&quot; }) =&gt; {
  if (direction === &quot;desc&quot;) {
    return (
      &lt;svg
        width={size}
        height={size}
        viewBox=&quot;0 0 512 512&quot;
        fill={color}
        xmlns=&quot;http://www.w3.org/2000/svg&quot;
      &gt;
        &lt;path d=&quot;M304 416h-64a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM16 160h48v304a16 16 0 0 0 16 16h32a16 16 0 0 0 16-16V160h48c14.21 0 21.38-17.24 11.31-27.31l-80-96a16 16 0 0 0-22.62 0l-80 96C-5.35 142.74 1.77 160 16 160zm416 0H240a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h192a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm-64 128H240a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM496 32H240a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h256a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z&quot; /&gt;
      &lt;/svg&gt;
    );
  }

  return (
    &lt;svg
      width={size}
      height={size}
      viewBox=&quot;0 0 512 512&quot;
      fill={color}
      xmlns=&quot;http://www.w3.org/2000/svg&quot;
    &gt;
      &lt;path d=&quot;M304 416h-64a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm-128-64h-48V48a16 16 0 0 0-16-16H80a16 16 0 0 0-16 16v304H16c-14.19 0-21.37 17.24-11.29 27.31l80 96a16 16 0 0 0 22.62 0l80-96C197.35 369.26 190.22 352 176 352zm256-192H240a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h192a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm-64 128H240a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM496 32H240a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h256a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z&quot; /&gt;
    &lt;/svg&gt;
  );
};

export default SortIcon;</code></pre>
<hr>
<h3 id="사용-예시">사용 예시</h3>
<pre><code class="language-jsx">import SortIcon from &quot;@/components/icons/SortIcon&quot;;

&lt;SortIcon direction=&quot;asc&quot; /&gt;
&lt;SortIcon direction=&quot;desc&quot; /&gt;</code></pre>
<hr>
<h2 id="4-간단한-정렬-버튼-컴포넌트">4. 간단한 정렬 버튼 컴포넌트</h2>
<p>단순히 아이콘만 사용하는 것이 아니라, 보통은 아래와 같은 형태로 쓰이게 된다.</p>
<pre><code class="language-jsx">import { useState } from &quot;react&quot;;
import { SortIcon } from &quot;@/components/icons&quot;;

const SortButton = () =&gt; {
  const [direction, setDirection] = useState(&quot;asc&quot;);

  const toggleDirection = () =&gt; {
    setDirection((prev) =&gt; (prev === &quot;asc&quot; ? &quot;desc&quot; : &quot;asc&quot;));
  };

  return (
    &lt;button onClick={toggleDirection}&gt;
      {direction === &quot;asc&quot; ? &quot;오름차순&quot; : &quot;내림차순&quot;}
      &lt;SortIcon direction={direction} /&gt;
    &lt;/button&gt;
  );
};

export default SortButton;</code></pre>
<hr>
<h2 id="5-아이콘-폴더-구조와-통합-관리">5. 아이콘 폴더 구조와 통합 관리</h2>
<pre><code>src/
└── components/
    └── icons/
        ├── HomeIcon.jsx
        ├── SortIcon.jsx
        └── index.js</code></pre><pre><code class="language-js">// icons/index.js
export { default as HomeIcon } from &#39;./HomeIcon&#39;;
export { default as SortIcon } from &#39;./SortIcon&#39;;</code></pre>
<pre><code class="language-jsx">// 사용
import { SortIcon } from &quot;@/components/icons&quot;;

&lt;SortIcon direction=&quot;desc&quot; /&gt;</code></pre>
<hr>
<h2 id="마무리">마무리</h2>
<ul>
<li>SVG도 결국 하나의 컴포넌트다.</li>
<li>복붙하는 방식에서 벗어나, props로 상태를 제어하고 관리 가능한 구조로 만들어야 유지보수가 편하다.</li>
<li>실무에서는 <code>direction</code>, <code>active</code>, <code>variant</code> 같은 props 분기로 <strong>동적 표현이 가능한 아이콘</strong>이 필요하다.</li>
</ul>
<blockquote>
<p>복사한 SVG를 그대로 쓰지 말고, 명확한 구조와 확장성을 갖춘 컴포넌트로 다뤄보자.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 아이콘 사용 완전 정복: React에서 아이콘 사용하는 방법]]></title>
            <link>https://velog.io/@mini_suyo/React-%EC%95%84%EC%9D%B4%EC%BD%98-%EC%82%AC%EC%9A%A9-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-React%EC%97%90%EC%84%9C-%EC%95%84%EC%9D%B4%EC%BD%98-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@mini_suyo/React-%EC%95%84%EC%9D%B4%EC%BD%98-%EC%82%AC%EC%9A%A9-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-React%EC%97%90%EC%84%9C-%EC%95%84%EC%9D%B4%EC%BD%98-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 20 Jul 2025 10:09:44 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/0a3aec55-d805-4bec-95c1-efd09a56f90a/image.png" alt="">
React 프로젝트를 진행할 때 아이콘은 사용자 인터페이스의 완성도를 높여주는 중요한 구성 요소다. 버튼, 입력창, 네비게이션 등 다양한 곳에서 기능을 직관적으로 전달하기 위해 아이콘을 자주 활용하게 된다.</p>
<p>이 글에서는 <strong>React에서 아이콘을 사용하는 주요 방법 4가지</strong>를 소개하며, 특히 <code>react-icons</code> 사이트를 활용해 <strong>라이브러리를 설치하지 않고 SVG를 직접 복사해 사용하는 팁</strong>도 함께 다룬다.</p>
<blockquote>
<p>이번 글은 2부로 이어지며, SVG를 재사용 가능한 컴포넌트로 구성하는 방법은 다음 글에서 자세히 설명할 예정이다.</p>
</blockquote>
<hr>
<h2 id="1-svg-코드-직접-삽입">1. SVG 코드 직접 삽입</h2>
<p>가장 기본적인 방식으로, SVG 코드를 JSX 내부에 직접 작성하여 사용하는 방법이다.</p>
<pre><code class="language-jsx">const CheckIcon = () =&gt; (
  &lt;svg
    width=&quot;20&quot;
    height=&quot;20&quot;
    viewBox=&quot;0 0 24 24&quot;
    fill=&quot;none&quot;
    stroke=&quot;green&quot;
    strokeWidth=&quot;2&quot;
    strokeLinecap=&quot;round&quot;
    strokeLinejoin=&quot;round&quot;
  &gt;
    &lt;path d=&quot;M5 13l4 4L19 7&quot; /&gt;
  &lt;/svg&gt;
);</code></pre>
<h3 id="✔-장점">✔ 장점</h3>
<ul>
<li>외부 라이브러리에 의존하지 않는다.</li>
<li>크기, 색상 등을 자유롭게 커스터마이징할 수 있다.</li>
<li>빌드 용량이 매우 작다.</li>
</ul>
<h3 id="❌-단점">❌ 단점</h3>
<ul>
<li>아이콘이 많아질수록 코드가 중복되고 복잡해진다.</li>
<li>컴포넌트 재사용을 고려하지 않으면 유지보수가 어렵다.</li>
</ul>
<hr>
<h2 id="2-svg-파일을-import-해서-사용">2. SVG 파일을 import 해서 사용</h2>
<p>SVG 파일을 컴포넌트처럼 import하여 사용하는 방식이다. Create React App에서는 기본적으로 지원하며, Vite 등의 환경에서는 설정이 필요할 수 있다.</p>
<pre><code class="language-jsx">// CheckIcon.svg
// &lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;...&quot; /&gt;&lt;/svg&gt;

import { ReactComponent as CheckIcon } from &#39;./CheckIcon.svg&#39;;

const Button = () =&gt; (
  &lt;button&gt;
    &lt;CheckIcon width=&quot;16&quot; height=&quot;16&quot; /&gt; 저장
  &lt;/button&gt;
);</code></pre>
<h3 id="✔-장점-1">✔ 장점</h3>
<ul>
<li>컴포넌트처럼 재사용할 수 있다.</li>
<li>props로 크기나 색상 등을 제어할 수 있다.</li>
</ul>
<h3 id="❌-단점-1">❌ 단점</h3>
<ul>
<li>SVG 파일을 별도로 관리해야 한다.</li>
<li>빌드 환경에 따라 import 방식이 다를 수 있다.</li>
</ul>
<hr>
<h2 id="3-아이콘-라이브러리-사용-react-icons">3. 아이콘 라이브러리 사용 (<code>react-icons</code>)</h2>
<p>가장 널리 사용되는 방식으로, 다양한 아이콘 세트를 하나의 라이브러리로 통합한 <code>react-icons</code>를 설치하여 사용하는 방법이다.</p>
<pre><code class="language-bash">npm install react-icons</code></pre>
<pre><code class="language-jsx">import { FiSearch } from &#39;react-icons/fi&#39;;

const SearchBar = () =&gt; (
  &lt;div&gt;
    &lt;FiSearch size={18} color=&quot;#555&quot; /&gt;
    &lt;input type=&quot;text&quot; placeholder=&quot;검색&quot; /&gt;
  &lt;/div&gt;
);</code></pre>
<h3 id="✔-장점-2">✔ 장점</h3>
<ul>
<li>Feather, FontAwesome, Material 등 다양한 아이콘을 지원한다.</li>
<li>props로 크기와 색상 등을 손쉽게 제어할 수 있다.</li>
<li>필요한 아이콘만 import하면 트리쉐이킹이 가능하다.</li>
</ul>
<h3 id="❌-단점-2">❌ 단점</h3>
<ul>
<li>외부 패키지를 설치해야 하므로 번들 크기가 증가할 수 있다.</li>
<li>단순한 아이콘 몇 개만 사용할 경우 과할 수 있다.</li>
</ul>
<hr>
<h2 id="4-react-icons-사이트에서-svg-복사해서-사용">4. react-icons 사이트에서 SVG 복사해서 사용</h2>
<p><code>react-icons</code> 공식 사이트(<a href="https://react-icons.github.io/react-icons/">react-icons.github.io</a>)에서는 다양한 아이콘을 미리 확인할 수 있으며, 원하는 아이콘을 <strong>개발자 도구를 통해 SVG 코드만 복사해서 사용하는 방식</strong>도 가능하다. 이 방법은 패키지를 설치하지 않고도 다양한 아이콘을 활용할 수 있다는 장점이 있다.</p>
<hr>
<h3 id="복사-방법-간단-튜토리얼">복사 방법 (간단 튜토리얼)</h3>
<ol>
<li><a href="https://react-icons.github.io/react-icons/">react-icons.github.io</a>에 접속한다.</li>
<li>원하는 아이콘을 클릭한 뒤, 아이콘 위에서 마우스 오른쪽 클릭 → **“검사(Inspect)”**를 누른다.</li>
<li><code>&lt;svg&gt;...&lt;/svg&gt;</code> 코드를 복사한다.</li>
<li>JSX에 붙여넣고 HTML 속성을 JSX에 맞게 변환한다.</li>
</ol>
<hr>
<h3 id="jsx-변환-시-주의할-점">JSX 변환 시 주의할 점</h3>
<table>
<thead>
<tr>
<th>HTML 속성</th>
<th>JSX 속성</th>
</tr>
</thead>
<tbody><tr>
<td><code>stroke-width</code></td>
<td><code>strokeWidth</code></td>
</tr>
<tr>
<td><code>fill-rule</code></td>
<td><code>fillRule</code></td>
</tr>
<tr>
<td><code>class</code></td>
<td><code>className</code></td>
</tr>
</tbody></table>
<hr>
<h3 id="예시">예시</h3>
<pre><code class="language-jsx">const HomeIcon = () =&gt; (
  &lt;svg
    width=&quot;20&quot;
    height=&quot;20&quot;
    viewBox=&quot;0 0 24 24&quot;
    fill=&quot;none&quot;
    stroke=&quot;currentColor&quot;
    strokeWidth=&quot;2&quot;
    strokeLinecap=&quot;round&quot;
    strokeLinejoin=&quot;round&quot;
  &gt;
    &lt;path d=&quot;M3 9l9-7 9 7&quot; /&gt;
    &lt;path d=&quot;M9 22V12h6v10&quot; /&gt;
  &lt;/svg&gt;
);</code></pre>
<hr>
<h3 id="✔-장점-3">✔ 장점</h3>
<ul>
<li>라이브러리를 설치하지 않고도 다양한 아이콘을 사용할 수 있다.</li>
<li>매우 가볍고 유연하다.</li>
<li>필요한 아이콘만 선택하여 사용할 수 있다.</li>
</ul>
<h3 id="❌-단점-3">❌ 단점</h3>
<ul>
<li>JSX 문법에 맞게 속성을 수동으로 변환해야 한다.</li>
<li>동일한 SVG를 여러 곳에서 사용할 경우 중복 코드가 발생할 수 있다.</li>
</ul>
<hr>
<blockquote>
<p>react-icons 사이트에서 원하는 아이콘을 개발자 도구로 복사해 <code>&lt;svg&gt;</code> 형태로 직접 사용하는 방식은, 라이브러리 설치 없이도 아이콘을 유연하게 활용할 수 있는 좋은 방법이다. 특히 단일 아이콘만 필요한 경우나, 프로젝트 용량을 최소화하고 싶을 때 매우 유용하다. JSX 문법 변환만 잘 처리하면 문제없이 사용할 수 있다.</p>
</blockquote>
<hr>
<h2 id="다음-글-예고-svg-아이콘-컴포넌트화">다음 글 예고: SVG 아이콘 컴포넌트화</h2>
<p>이번 글에서는 SVG 아이콘을 사용하는 주요 방식들을 소개했다.
다음 글에서는 SVG를 <strong>재사용 가능한 React 컴포넌트로 만들고</strong>,
<strong>props로 크기·색상 등을 제어하는 방법</strong>,
<strong>여러 아이콘을 하나의 폴더에서 관리하는 전략</strong>까지 자세히 다룰 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서 다국어 서비스 지원하기 (react-i18next)]]></title>
            <link>https://velog.io/@mini_suyo/React%EC%97%90%EC%84%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B0-react-i18next</link>
            <guid>https://velog.io/@mini_suyo/React%EC%97%90%EC%84%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B0-react-i18next</guid>
            <pubDate>Thu, 17 Jul 2025 11:20:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/4d8ec953-110f-44f9-8f27-0bdd8210a368/image.png" alt=""></p>
<h1 id="react-프로젝트에-i18n-적용하기-react-i18next">React 프로젝트에 i18n 적용하기 (react-i18next)</h1>
<p>다국어를 지원하는 웹 서비스를 만들기 위해 i18n을 적용했다.
React 기반 프로젝트에서 <code>react-i18next</code>를 이용해 간단하게 다국어 지원을 설정할 수 있다.
이번 글에서는 국제화(i18n)의 개념부터 적용 과정까지 차근차근 정리한다.</p>
<hr>
<h2 id="0-i18n이란">0. i18n이란?</h2>
<p><strong>i18n</strong>은 <strong>Internationalization</strong>의 약어다.
영어 단어 <em>Internationalization</em>에서 <code>i</code>로 시작해 <code>n</code>으로 끝나는 사이에 18개의 문자가 들어있기 때문에 <code>i18n</code>이라고 부른다.</p>
<blockquote>
<p>웹 서비스에서 i18n은 사용자의 <strong>언어, 국가, 지역 환경</strong>에 맞게 텍스트나 날짜, 숫자 포맷 등을 다국어로 지원하는 기능을 말한다.</p>
</blockquote>
<p>예를 들어,</p>
<ul>
<li><code>&quot;환영합니다&quot;</code> → 한국어</li>
<li><code>&quot;Welcome&quot;</code> → 영어</li>
<li><code>&quot;欢迎&quot;</code> → 중국어
이런 식으로 텍스트를 사용자 언어에 맞게 바꿔주는 것이다.</li>
</ul>
<hr>
<h2 id="1-왜-react-i18next를-선택했는가">1. 왜 react-i18next를 선택했는가?</h2>
<p>i18n을 React에서 적용할 수 있는 라이브러리는 몇 가지가 있다. 그 중 <code>react-i18next</code>를 선택한 이유는 다음과 같다:</p>
<table>
<thead>
<tr>
<th>이유</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>공식 지원</td>
<td><code>i18next</code>의 공식 React 바인딩</td>
</tr>
<tr>
<td>문서가 풍부하고 안정적</td>
<td>사용자가 많고 레퍼런스가 많음</td>
</tr>
<tr>
<td>간단한 API</td>
<td><code>useTranslation()</code> 훅만으로도 충분히 사용 가능</td>
</tr>
<tr>
<td>브라우저 언어 감지 지원</td>
<td>자동 언어 설정을 쉽게 구현 가능</td>
</tr>
<tr>
<td>Lazy loading, namespace 등 확장성 높음</td>
<td>필요 시 세분화된 설정도 가능</td>
</tr>
</tbody></table>
<p>특히, 기존 React 프로젝트에 부담 없이 적용할 수 있고 컴포넌트 단위로 번역 키를 관리할 수 있어서 개발 및 유지보수가 용이하다.</p>
<hr>
<h2 id="2-패키지-설치">2. 패키지 설치</h2>
<pre><code class="language-bash">npm install i18next react-i18next i18next-browser-languagedetector</code></pre>
<table>
<thead>
<tr>
<th>패키지</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>i18next</code></td>
<td>국제화 핵심 라이브러리</td>
</tr>
<tr>
<td><code>react-i18next</code></td>
<td>React 연동을 위한 바인딩</td>
</tr>
<tr>
<td><code>i18next-browser-languagedetector</code></td>
<td>브라우저 언어 자동 감지 기능 제공</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-i18n-설정-파일-만들기">3. i18n 설정 파일 만들기</h2>
<p><code>src/i18n.js</code> 파일을 생성하고 다음처럼 설정을 작성한다.</p>
<pre><code class="language-js">import i18n from &#39;i18next&#39;;
import { initReactI18next } from &#39;react-i18next&#39;;
import LanguageDetector from &#39;i18next-browser-languagedetector&#39;;

import translationEN from &#39;./locales/en/translation.json&#39;;
import translationKO from &#39;./locales/ko/translation.json&#39;;

const resources = {
  en: { translation: translationEN },
  ko: { translation: translationKO },
};

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: &#39;ko&#39;,
    interpolation: {
      escapeValue: false,
    },
  });

export default i18n;</code></pre>
<hr>
<h2 id="4-main-파일에-i18n-연결">4. main 파일에 i18n 연결</h2>
<p><code>main.jsx</code> 또는 <code>index.js</code>에서 i18n을 import해 초기화되도록 한다.</p>
<pre><code class="language-js">import &#39;./i18n&#39;;</code></pre>
<hr>
<h2 id="5-번역-리소스-생성">5. 번역 리소스 생성</h2>
<pre><code>src/
├── locales/
│   ├── en/
│   │   └── translation.json
│   └── ko/
│       └── translation.json</code></pre><p><strong>locales/ko/translation.json</strong></p>
<pre><code class="language-json">{
  &quot;welcome&quot;: &quot;환영합니다!&quot;,
  &quot;subtitle&quot;: &quot;이건 한글이에요&quot;
}</code></pre>
<p><strong>locales/en/translation.json</strong></p>
<pre><code class="language-json">{
  &quot;welcome&quot;: &quot;Welcome!&quot;,
  &quot;subtitle&quot;: &quot;This is English&quot;
}</code></pre>
<hr>
<h2 id="6-컴포넌트에서-사용하기">6. 컴포넌트에서 사용하기</h2>
<pre><code class="language-jsx">import { useTranslation } from &#39;react-i18next&#39;;

const Home = () =&gt; {
  const { t } = useTranslation();

  return (
    &lt;div&gt;
      &lt;h1&gt;{t(&#39;welcome&#39;)}&lt;/h1&gt;
      &lt;p&gt;{t(&#39;subtitle&#39;)}&lt;/p&gt;
    &lt;/div&gt;
  );
};</code></pre>
<hr>
<h2 id="7-자주-사용하는-번역-키-작성-팁">7. 자주 사용하는 번역 키 작성 팁</h2>
<p>i18n을 적용하다 보면 번역 키를 어떻게 구성하고 유지할지에 대한 고민이 생긴다.
특히 프로젝트 규모가 커질수록 번역 키 관리가 중요해진다.</p>
<hr>
<h3 id="1-기능화면-단위로-네임스페이스-분리하거나-접두사-사용하기">1. <strong>기능/화면 단위로 네임스페이스 분리하거나 접두사 사용하기</strong></h3>
<p>예: <code>landing.title</code>, <code>dashboard.subtitle</code>, <code>profile.editButton</code></p>
<pre><code class="language-json">{
  &quot;landing&quot;: {
    &quot;title&quot;: &quot;여기에 오신 것을 환영합니다!&quot;
  },
  &quot;dashboard&quot;: {
    &quot;greeting&quot;: &quot;안녕하세요, {{username}}님&quot;
  }
}</code></pre>
<hr>
<h3 id="2-컴포넌트와-11-대응되는-구조">2. <strong>컴포넌트와 1:1 대응되는 구조</strong></h3>
<p>각 컴포넌트별로 필요한 텍스트만 독립적으로 관리할 수 있어서 유지보수가 편하다.</p>
<p>예: <code>BadgePreview.empty</code>, <code>CommitChart.tooltip</code>, <code>LanguageToggle.label</code></p>
<hr>
<h3 id="3-공통-키는-따로-빼기">3. <strong>공통 키는 따로 빼기</strong></h3>
<p>프로젝트 전반에서 반복되는 텍스트(버튼, 상태 메시지 등)는 공통으로 분리해서 관리하는 것이 좋다.</p>
<pre><code class="language-json">{
  &quot;button&quot;: {
    &quot;ok&quot;: &quot;확인&quot;,
    &quot;cancel&quot;: &quot;취소&quot;
  },
  &quot;common&quot;: {
    &quot;loading&quot;: &quot;로딩 중...&quot;
  }
}</code></pre>
<hr>
<h3 id="4-변수를-포함한-메시지도-지원">4. <strong>변수를 포함한 메시지도 지원</strong></h3>
<p>i18next는 <code>{{ }}</code> 구문으로 변수를 받을 수 있다.</p>
<pre><code class="language-json">{
  &quot;dashboard.greeting&quot;: &quot;안녕하세요, {{username}}님&quot;
}</code></pre>
<pre><code class="language-js">t(&#39;dashboard.greeting&#39;, { username: &#39;민승용&#39; });</code></pre>
<hr>
<h3 id="5-언어-전환-버튼-만들기">5. <strong>언어 전환 버튼 만들기</strong></h3>
<p>사용자가 직접 언어를 바꿀 수 있도록 전환 버튼을 제공할 수 있다.</p>
<pre><code class="language-jsx">import { useTranslation } from &#39;react-i18next&#39;;

const LanguageToggle = () =&gt; {
  const { i18n, t } = useTranslation();

  const toggleLanguage = () =&gt; {
    const nextLang = i18n.language === &#39;ko&#39; ? &#39;en&#39; : &#39;ko&#39;;
    i18n.changeLanguage(nextLang);
  };

  return (
    &lt;button onClick={toggleLanguage}&gt;
      {t(&#39;button.changeLanguage&#39;)} ({i18n.language})
    &lt;/button&gt;
  );
};</code></pre>
<hr>
<h3 id="결론">결론</h3>
<ul>
<li>키는 <strong>의미 중심</strong>, <strong>계층적 구조</strong>로 작성한다.</li>
<li>공통 키와 화면별 키를 <strong>분리</strong>해 관리한다.</li>
<li>번역 키 작성 습관이 나중에 <strong>협업과 유지보수 효율성</strong>에 큰 영향을 준다.</li>
</ul>
<p>더 많은 언어가 추가되거나 서버와 연동해야 하는 상황이라면
namespace 분리, lazy loading 등 고급 설정도 고려할 수 있다.
기초적인 구조를 처음에 잘 잡아두는 게 중요하다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p><code>react-i18next</code>를 사용하면 React 프로젝트에서 간단한 설정만으로 국제화를 적용할 수 있다.
전역 설정, 컴포넌트 단위 사용, 언어 전환까지 한 번에 구성할 수 있고, 이후 lazy loading이나 서버 연동 등도 쉽게 확장 가능하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[실행은 되는데 빨간 줄? CSS 모듈과 타입 선언의 관계]]></title>
            <link>https://velog.io/@mini_suyo/%EC%8B%A4%ED%96%89%EC%9D%80-%EB%90%98%EB%8A%94%EB%8D%B0-%EB%B9%A8%EA%B0%84-%EC%A4%84-CSS-%EB%AA%A8%EB%93%88%EA%B3%BC-%ED%83%80%EC%9E%85-%EC%84%A0%EC%96%B8%EC%9D%98-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@mini_suyo/%EC%8B%A4%ED%96%89%EC%9D%80-%EB%90%98%EB%8A%94%EB%8D%B0-%EB%B9%A8%EA%B0%84-%EC%A4%84-CSS-%EB%AA%A8%EB%93%88%EA%B3%BC-%ED%83%80%EC%9E%85-%EC%84%A0%EC%96%B8%EC%9D%98-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Tue, 15 Jul 2025 06:45:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/2d3e5e75-eeff-4db1-8677-c34c69fd1f9a/image.png" alt=""></p>
<h1 id="typescript에서-css-modules-사용할-때-dts-파일이-필요한-이유">TypeScript에서 CSS Modules 사용할 때 .d.ts 파일이 필요한 이유</h1>
<blockquote>
<p>&quot;분명 CSS는 적용됐는데 빨간 줄이 계속 뜬다… 왜 그럴까?&quot;</p>
</blockquote>
<p>React 프로젝트를 TypeScript로 개발하다 보면, CSS Modules를 사용할 때 이런 경험을 한 번쯤 하게 된다.
스타일은 잘 적용되는데, VSCode에서는 계속 빨간 밑줄이 생기고 빌드 시 타입 오류가 발생하는 현상이다.</p>
<p>이 글에서는 이 문제의 원인과 해결 방법, 그리고 <code>.d.ts</code> 파일이 필요한 이유를 정리한다.</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<p>예를 들어, 다음과 같이 CSS Modules를 사용하는 코드가 있다고 가정한다.</p>
<pre><code class="language-tsx">import styles from &#39;./MyComponent.module.css&#39;

const MyComponent = () =&gt; {
  return &lt;div className={styles.container}&gt;Hello&lt;/div&gt;
}</code></pre>
<p>이 코드는 실제로 스타일이 잘 적용되고 화면에도 정상적으로 나타난다.
하지만 TypeScript는 아래와 같은 오류를 발생시킨다.</p>
<pre><code>Cannot find module &#39;./MyComponent.module.css&#39; or its corresponding type declarations.</code></pre><blockquote>
<p>CSS 파일을 import했지만, TypeScript는 이 파일의 타입이 어떤 것인지 알 수 없기 때문에 오류가 발생하는 것이다.</p>
</blockquote>
<hr>
<h2 id="typescript는-css-타입을-알지-못한다">TypeScript는 CSS 타입을 알지 못한다</h2>
<p>TypeScript는 기본적으로 <code>.css</code> 파일의 구조를 이해하지 못한다.
자바스크립트에서는 넘어가는 부분이지만, TypeScript는 **&quot;이 파일이 어떤 값을 export하는지&quot;**를 명확하게 알아야 한다.</p>
<hr>
<h2 id="해결-방법-dts-타입-선언-추가">해결 방법: <code>.d.ts</code> 타입 선언 추가</h2>
<p>해결 방법은 간단하다. 프로젝트에 아래와 같은 타입 선언 파일을 하나 추가하면 된다.</p>
<pre><code>src/
└── types/
    └── index.d.ts</code></pre><pre><code class="language-ts">// src/types/index.d.ts
declare module &#39;*.module.css&#39; {
  const classes: { [key: string]: string }
  export default classes
}</code></pre>
<p>이렇게 선언해주면 TypeScript는 <code>.module.css</code> 파일을 import할 때
<code>styles.someClass</code> 형태로 사용할 수 있다는 것을 알게 된다.</p>
<hr>
<h2 id="꼭-필요한가">꼭 필요한가?</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>실행에는 영향 없음</td>
<td>CSS는 그대로 잘 적용된다.</td>
</tr>
<tr>
<td>타입 검사에는 영향 있음</td>
<td>TypeScript와 VSCode에서 오류 없이 개발하려면 필요하다.</td>
</tr>
<tr>
<td>자동완성 및 정적 분석</td>
<td>CSS 클래스 이름도 자동완성으로 확인할 수 있다.</td>
</tr>
</tbody></table>
<hr>
<h2 id="정리">정리</h2>
<ul>
<li>CSS Modules를 TypeScript와 함께 사용할 때는 <code>.d.ts</code> 타입 선언이 필요하다.</li>
<li>실행에는 영향이 없지만, 타입 오류를 방지하고 개발 효율을 높이기 위해 반드시 추가하는 것이 좋다.</li>
<li>선언 파일의 위치는 <code>src/global.d.ts</code>, <code>src/types/index.d.ts</code> 등으로 자유롭게 구성할 수 있다.</li>
</ul>
<hr>
<h2 id="마무리">마무리</h2>
<p>TypeScript는 엄격한 타입 시스템을 통해 안정적인 개발을 지원하지만, 이렇게 명시적인 설정이 필요한 경우도 존재한다.
한 줄의 타입 선언으로 코드 품질을 높일 수 있다면, 굳이 마다할 이유는 없다.</p>
<blockquote>
<p>팀 프로젝트라면, 이 설정이 누락된 상태에서 전 팀원이 모두 동일한 타입 오류를 마주할 수도 있다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Hook 완전 정복] Step 4: customHook]]></title>
            <link>https://velog.io/@mini_suyo/React-Hook-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Step-4-customHook</link>
            <guid>https://velog.io/@mini_suyo/React-Hook-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Step-4-customHook</guid>
            <pubDate>Fri, 20 Jun 2025 09:01:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/4b4a674f-b9e7-40c2-b964-91b61cd95f02/image.png" alt=""></p>
<p>React 애플리케이션을 만들다 보면, 여러 컴포넌트에서 <strong>비슷한 로직이 반복</strong>되는 경우가 많다.
예를 들어 입력 값 제어, 타이머 관리, 로컬 스토리지 접근, API 상태 관리 등은 다양한 컴포넌트에서 공유되는 공통된 패턴이다.</p>
<p>React는 이러한 로직을 <strong>커스텀 Hook(Custom Hook)</strong>으로 추상화할 수 있도록 지원한다.
이번 글에서는 커스텀 Hook의 개념부터 작성법, 실전 예제까지 알아본다.</p>
<hr>
<h2 id="커스텀-hook이란">커스텀 Hook이란?</h2>
<p>커스텀 Hook은 <strong>React의 기본 Hook(<code>useState</code>, <code>useEffect</code>, 등)을 사용해 만든, 재사용 가능한 함수</strong>다.
이 함수는 반드시 <code>use</code>로 시작하는 이름을 가지며, 내부에서 하나 이상의 Hook을 사용할 수 있다.</p>
<pre><code class="language-jsx">function useCustomFeature() {
  const [value, setValue] = useState(0);
  // 기타 로직 ...
  return [value, setValue];
}</code></pre>
<p>커스텀 Hook은 <strong>UI가 아닌 로직만을 담당</strong>하며, <strong>함수형 컴포넌트처럼 동작하지만 JSX를 반환하지 않는다.</strong></p>
<hr>
<h2 id="커스텀-hook을-사용하는-이유">커스텀 Hook을 사용하는 이유</h2>
<ul>
<li><strong>로직의 재사용성</strong> 향상</li>
<li><strong>컴포넌트 코드 분리와 가독성 향상</strong></li>
<li>팀원 간 <strong>기능 추상화 및 일관성 유지</strong></li>
<li><strong>Hook의 조합</strong>을 통해 더 복잡한 기능도 단순화 가능</li>
</ul>
<hr>
<h2 id="기본-예제-입력-값-관리-훅-만들기">기본 예제: 입력 값 관리 훅 만들기</h2>
<p>여러 입력 필드에서 <code>useState</code>를 반복하는 대신, 커스텀 훅으로 추상화할 수 있다.</p>
<pre><code class="language-jsx">// useInput.js
import { useState } from &#39;react&#39;;

function useInput(initialValue = &quot;&quot;) {
  const [value, setValue] = useState(initialValue);
  const onChange = (e) =&gt; setValue(e.target.value);
  const reset = () =&gt; setValue(&quot;&quot;);
  return { value, onChange, reset };
}

export default useInput;</code></pre>
<p>사용 예시:</p>
<pre><code class="language-jsx">import useInput from &#39;./useInput&#39;;

function LoginForm() {
  const username = useInput();
  const password = useInput();

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    console.log(&quot;입력값:&quot;, username.value, password.value);
    username.reset();
    password.reset();
  };

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      &lt;input {...username} placeholder=&quot;아이디&quot; /&gt;
      &lt;input {...password} type=&quot;password&quot; placeholder=&quot;비밀번호&quot; /&gt;
      &lt;button&gt;로그인&lt;/button&gt;
    &lt;/form&gt;
  );
}</code></pre>
<hr>
<h2 id="커스텀-hook-작성-원칙">커스텀 Hook 작성 원칙</h2>
<h3 id="이름은-use로-시작해야-한다">이름은 <code>use</code>로 시작해야 한다</h3>
<p>→ React는 이 네이밍 규칙을 기반으로 Hook을 식별하고 동작 순서를 관리한다.</p>
<h3 id="다른-hook과-마찬가지로-최상위-레벨에서만-호출해야-한다">다른 Hook과 마찬가지로 <strong>최상위 레벨에서만 호출해야 한다</strong></h3>
<p>→ 조건문, 루프, 중첩 함수 내에서 호출하면 안 된다.</p>
<h3 id="jsx를-반환하지-않는다"><strong>JSX를 반환하지 않는다</strong></h3>
<p>→ 커스텀 Hook은 <strong>로직만 담당하는 함수</strong>이며, UI는 일반 컴포넌트에서 처리한다.</p>
<hr>
<h2 id="실무-예제">실무 예제</h2>
<h3 id="api-상태-관리-hook">API 상태 관리 Hook</h3>
<pre><code class="language-jsx">import { useEffect, useState } from &#39;react&#39;;

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() =&gt; {
    let ignore = false;
    setLoading(true);
    fetch(url)
      .then((res) =&gt; res.json())
      .then((result) =&gt; {
        if (!ignore) {
          setData(result);
          setLoading(false);
        }
      });

    return () =&gt; {
      ignore = true;
    };
  }, [url]);

  return { data, loading };
}</code></pre>
<p>사용 예시:</p>
<pre><code class="language-jsx">function PostList() {
  const { data: posts, loading } = useFetch(&quot;https://jsonplaceholder.typicode.com/posts&quot;);

  if (loading) return &lt;p&gt;로딩 중...&lt;/p&gt;;

  return (
    &lt;ul&gt;
      {posts.map(post =&gt; (
        &lt;li key={post.id}&gt;{post.title}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}</code></pre>
<hr>
<h3 id="로컬-스토리지-hook">로컬 스토리지 Hook</h3>
<pre><code class="language-jsx">import { useState, useEffect } from &#39;react&#39;;

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() =&gt; {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() =&gt; {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}</code></pre>
<p>사용 예시:</p>
<pre><code class="language-jsx">function ThemeToggle() {
  const [darkMode, setDarkMode] = useLocalStorage(&quot;darkMode&quot;, false);

  return (
    &lt;button onClick={() =&gt; setDarkMode(!darkMode)}&gt;
      {darkMode ? &quot;라이트 모드&quot; : &quot;다크 모드&quot;}
    &lt;/button&gt;
  );
}</code></pre>
<hr>
<h2 id="핵심-정리">핵심 정리</h2>
<ol>
<li>커스텀 Hook은 <strong>Hook 기반 로직을 재사용 가능한 함수로 추상화한 것</strong>이다.</li>
<li>이름은 <code>use</code>로 시작해야 하며, JSX를 반환하지 않는다.</li>
<li>여러 Hook들을 조합하여 복잡한 동작을 하나의 커스텀 Hook으로 묶을 수 있다.</li>
<li>반복적인 상태 관리, API 처리, 이벤트 등록 등 <strong>비즈니스 로직을 재사용성 있게 구성</strong>할 수 있다.</li>
<li>커스텀 Hook은 <strong>로직의 분리</strong>와 <strong>컴포넌트 단순화</strong>에 핵심적인 역할을 한다.</li>
</ol>
<hr>
<h2 id="마무리">마무리</h2>
<p>React에서 커스텀 Hook은 로직을 모듈화하고 팀 개발 효율을 높이기 위한 가장 강력한 도구 중 하나다. 복잡한 컴포넌트일수록 로직을 커스텀 Hook으로 분리하면 <strong>코드가 훨씬 읽기 쉬워지고</strong>, <strong>유지보수성이 향상</strong>된다.
앞으로는 하나의 기능을 구현할 때마다 &quot;이걸 커스텀 Hook으로 만들 수 있을까?&quot;를 고민해보자. 작은 반복도 훅으로 추상화하는 습관이 점점 더 탄탄한 리액트 실력을 만들어줄 것이다.</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://ko.react.dev/learn/reusing-logic-with-custom-hooks">React 공식 문서: 로직 재사용을 위한 커스텀 Hook</a></li>
<li><a href="https://ko.legacy.reactjs.org/docs/hooks-custom.html">React 레거시 문서: 커스텀 Hook 만들기</a></li>
<li>[React 완벽 가이드 2025 with React Router &amp; Redux -Udemy]
(<a href="https://www.udemy.com/course/best-react">https://www.udemy.com/course/best-react</a>)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Hook 완전 정복] Step 3: useRef 기초와 활용]]></title>
            <link>https://velog.io/@mini_suyo/React-Hook-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Step-3-useRe-%EA%B8%B0%EC%B4%88%EC%99%80-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@mini_suyo/React-Hook-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Step-3-useRe-%EA%B8%B0%EC%B4%88%EC%99%80-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Tue, 17 Jun 2025 09:11:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/41bcc85b-7c5d-4f61-a65b-40c7689fd198/image.png" alt="">
<code>useState</code>와 <code>useEffect</code>는 렌더링 흐름을 중심으로 상태와 사이드 이펙트를 관리한다. 하지만 <strong>렌더링과 무관하게 값을 기억하거나, DOM을 직접 조작해야 하는 순간</strong>도 있다. 이럴 때 React는 <code>useRef</code>라는 훅을 제공한다.</p>
<p>이번 글에서는 <code>useRef</code>의 기본 개념부터, 실무에서 활용할 수 있는 심화 패턴까지 정리해보았다.</p>
<hr>
<h2 id="useref란">useRef란?</h2>
<p><code>useRef</code>는 다음 두 가지 역할을 수행하는 Hook이다.</p>
<ol>
<li>렌더링과 상관없이 값을 저장하는 변수처럼 사용</li>
<li>DOM 요소를 직접 참조하기 위한 수단</li>
</ol>
<pre><code class="language-jsx">const ref = useRef(initialValue);</code></pre>
<ul>
<li><code>ref.current</code>에 저장한 값은 컴포넌트가 리렌더링돼도 유지된다.</li>
<li><code>ref.current</code>를 변경해도 컴포넌트는 리렌더링되지 않는다.</li>
</ul>
<hr>
<h2 id="기본-사용법">기본 사용법</h2>
<h3 id="dom-참조-예제-input-포커스-제어">DOM 참조 예제: input 포커스 제어</h3>
<pre><code class="language-jsx">import { useRef } from &#39;react&#39;;

function FocusInput() {
  const inputRef = useRef(null);

  const handleClick = () =&gt; {
    inputRef.current.focus();
  };

  return (
    &lt;div&gt;
      &lt;input ref={inputRef} placeholder=&quot;클릭 시 포커스됨&quot; /&gt;
      &lt;button onClick={handleClick}&gt;포커스&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<hr>
<h3 id="값-추적-예제-렌더링-횟수-카운팅">값 추적 예제: 렌더링 횟수 카운팅</h3>
<pre><code class="language-jsx">import { useRef, useState } from &#39;react&#39;;

function RenderCounter() {
  const renderCount = useRef(1);
  const [value, setValue] = useState(&quot;&quot;);

  renderCount.current += 1;

  return (
    &lt;div&gt;
      &lt;input value={value} onChange={(e) =&gt; setValue(e.target.value)} /&gt;
      &lt;p&gt;렌더링 횟수: {renderCount.current}&lt;/p&gt;
    &lt;/div&gt;
  );
}</code></pre>
<hr>
<h2 id="이전-값-기억하기">이전 값 기억하기</h2>
<pre><code class="language-jsx">import { useEffect, useRef, useState } from &#39;react&#39;;

function PreviousValueTracker() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(count);

  useEffect(() =&gt; {
    prevCountRef.current = count;
  }, [count]);

  return (
    &lt;div&gt;
      &lt;p&gt;현재 값: {count}&lt;/p&gt;
      &lt;p&gt;이전 값: {prevCountRef.current}&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;+1&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<hr>
<h2 id="useref-vs-usestate-차이">useRef vs useState 차이</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>useRef</th>
<th>useState</th>
</tr>
</thead>
<tbody><tr>
<td>값 변경 시 렌더링</td>
<td>아니오</td>
<td>예</td>
</tr>
<tr>
<td>값 유지</td>
<td>렌더링 사이에 유지됨</td>
<td>유지됨</td>
</tr>
<tr>
<td>주요 용도</td>
<td>값 추적, DOM 접근</td>
<td>UI 상태 관리</td>
</tr>
<tr>
<td>불변성 관리</td>
<td>필요 없음</td>
<td>필요함</td>
</tr>
</tbody></table>
<hr>
<h2 id="실무-활용-예제-심화">실무 활용 예제 (심화)</h2>
<h3 id="스크롤-위치-추적">스크롤 위치 추적</h3>
<pre><code class="language-jsx">import { useEffect, useRef, useState } from &#39;react&#39;;

function ScrollTracker() {
  const scrollY = useRef(0);
  const [message, setMessage] = useState(&quot;&quot;);

  useEffect(() =&gt; {
    const handleScroll = () =&gt; {
      scrollY.current = window.scrollY;
      if (scrollY.current &gt; 300) {
        setMessage(&quot;스크롤이 300px 이상입니다!&quot;);
      } else {
        setMessage(&quot;&quot;);
      }
    };

    window.addEventListener(&quot;scroll&quot;, handleScroll);
    return () =&gt; window.removeEventListener(&quot;scroll&quot;, handleScroll);
  }, []);

  return &lt;p&gt;{message}&lt;/p&gt;;
}</code></pre>
<hr>
<h3 id="디바운싱-타이머-제어">디바운싱 타이머 제어</h3>
<pre><code class="language-jsx">import { useRef, useState } from &#39;react&#39;;

function DebouncedInput() {
  const [text, setText] = useState(&quot;&quot;);
  const timerRef = useRef(null);

  const handleChange = (e) =&gt; {
    const value = e.target.value;
    clearTimeout(timerRef.current);
    timerRef.current = setTimeout(() =&gt; {
      console.log(&quot;서버 전송: &quot;, value);
    }, 500);
    setText(value);
  };

  return (
    &lt;input value={text} onChange={handleChange} placeholder=&quot;입력 후 잠시 멈춰보세요&quot; /&gt;
  );
}</code></pre>
<hr>
<h3 id="intersectionobserver-활용">IntersectionObserver 활용</h3>
<pre><code class="language-jsx">import { useEffect, useRef, useState } from &#39;react&#39;;

function LazyBox() {
  const boxRef = useRef(null);
  const [visible, setVisible] = useState(false);

  useEffect(() =&gt; {
    const observer = new IntersectionObserver(([entry]) =&gt; {
      setVisible(entry.isIntersecting);
    });

    if (boxRef.current) {
      observer.observe(boxRef.current);
    }

    return () =&gt; observer.disconnect();
  }, []);

  return (
    &lt;div
      ref={boxRef}
      style={{
        marginTop: &quot;500px&quot;,
        height: &quot;200px&quot;,
        background: visible ? &quot;skyblue&quot; : &quot;gray&quot;,
        transition: &quot;background 0.3s&quot;,
      }}
    &gt;
      {visible ? &quot;보이고 있어요!&quot; : &quot;아직 안 보여요&quot;}
    &lt;/div&gt;
  );
}</code></pre>
<hr>
<h2 id="핵심-정리">핵심 정리</h2>
<ol>
<li><code>useRef()</code>는 렌더링과 무관한 값을 저장할 수 있다.</li>
<li><code>.current</code> 값을 변경해도 리렌더링은 발생하지 않는다.</li>
<li>DOM 요소에 접근하거나 이전 값을 추적하는 데 매우 유용하다.</li>
<li>디바운싱, IntersectionObserver, 외부 API 타이머 등을 다룰 때 필수 도구다.</li>
<li>화면에 표시할 값은 <code>useState</code>, 내부에서만 추적할 값은 <code>useRef</code>로 구분해서 사용한다.</li>
</ol>
<hr>
<h2 id="마무리">마무리</h2>
<p><code>useRef</code>는 React 함수형 컴포넌트에서 <strong>렌더링 없이 값을 보존하거나</strong>, <strong>DOM 요소를 직접 조작</strong>해야 할 때 유용한 도구다.
UI 상태는 <code>useState</code>, 내부 로직이나 참조 추적은 <code>useRef</code>로 구분하면 컴포넌트를 더 명확하고 효율적으로 관리할 수 있다.</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://ko.react.dev/reference/react/useRef">React 공식 문서: useRef</a></li>
<li>[React 완벽 가이드 2025 with React Router &amp; Redux -Udemy]
(<a href="https://www.udemy.com/course/best-react">https://www.udemy.com/course/best-react</a>)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Hook 완전 정복] Step 2: useEffect 기초와 활용]]></title>
            <link>https://velog.io/@mini_suyo/React-Hook-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Step-2-useEffect-%EA%B8%B0%EC%B4%88%EC%99%80-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@mini_suyo/React-Hook-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Step-2-useEffect-%EA%B8%B0%EC%B4%88%EC%99%80-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Fri, 13 Jun 2025 10:01:19 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/c1b7884a-2235-4f5d-8a59-18c32bbdab77/image.png" alt="">
React 함수형 컴포넌트는 깔끔하지만, UI 외의 작업(예: API 호출, 이벤트 등록, 타이머 설정 등)을 어디서 처리해야 할까? 클래스 컴포넌트에서는 <code>componentDidMount</code>, <code>componentDidUpdate</code>, <code>componentWillUnmount</code> 등을 사용했지만, 함수형 컴포넌트에서는 <strong><code>useEffect</code> Hook</strong>으로 이를 모두 처리할 수 있다.
이 글에서는 <code>useEffect</code>의 기본 개념부터 동작 원리, 그리고 실무에서 자주 마주치는 패턴까지 차근차근 알아본다.</p>
<hr>
<h2 id="useeffect란">useEffect란?</h2>
<p><code>useEffect</code>는 컴포넌트가 렌더링된 이후에 <strong>부수 효과</strong> 를 수행할 수 있게 해주는 Hook이다.
React는 렌더링 과정에서 DOM을 그리는 데 집중하고, 외부 작업(API 호출, 로깅 등)은 <code>useEffect</code>에 맡긴다.</p>
<pre><code class="language-jsx">useEffect(() =&gt; {
  // 이곳에 실행할 부수 효과 로직을 작성한다
}, [dependencies]);</code></pre>
<ul>
<li>첫 번째 인자: <strong>Effect 함수</strong> – 부수 효과를 실행하는 함수</li>
<li>두 번째 인자: <strong>의존성 배열 (dependency array)</strong> – 언제 Effect를 다시 실행할지 결정</li>
</ul>
<hr>
<h2 id="언제-쓰나">언제 쓰나?</h2>
<ul>
<li>데이터 요청 (fetch API, axios)</li>
<li>이벤트 리스너 등록 및 해제</li>
<li>타이머 설정 (<code>setTimeout</code>, <code>setInterval</code>)</li>
<li>콘솔 로깅, 문서 제목 변경 등 외부 변경 작업</li>
</ul>
<hr>
<h2 id="기본-예제-마운트-시-api-호출">기본 예제: 마운트 시 API 호출</h2>
<pre><code class="language-jsx">import { useEffect, useState } from &#39;react&#39;;

function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() =&gt; {
    fetch(&quot;https://jsonplaceholder.typicode.com/users&quot;)
      .then(res =&gt; res.json())
      .then(data =&gt; setUsers(data));
  }, []); // 빈 배열: 컴포넌트가 처음 마운트될 때 한 번만 실행

  return (
    &lt;ul&gt;
      {users.map(user =&gt; (
        &lt;li key={user.id}&gt;{user.name}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}</code></pre>
<ul>
<li>의존성 배열이 <code>[]</code>이기 때문에 <strong>마운트 시점</strong>에만 한 번 실행된다.</li>
<li><code>setUsers</code>로 상태를 업데이트하면, 컴포넌트는 재렌더링된다.</li>
</ul>
<hr>
<h2 id="의존성-배열에-따른-실행-시점">의존성 배열에 따른 실행 시점</h2>
<table>
<thead>
<tr>
<th>의존성 배열</th>
<th>동작 설명</th>
</tr>
</thead>
<tbody><tr>
<td>없음</td>
<td><strong>렌더링될 때마다</strong> 실행됨</td>
</tr>
<tr>
<td><code>[]</code></td>
<td><strong>처음 마운트될 때 한 번만</strong> 실행됨</td>
</tr>
<tr>
<td><code>[count]</code></td>
<td><code>count</code> 값이 바뀔 때마다 실행됨</td>
</tr>
</tbody></table>
<pre><code class="language-jsx">// 의존성 배열이 없는 경우: 매 렌더링마다 실행됨
useEffect(() =&gt; {
  console.log(&quot;렌더링마다 실행됨&quot;);
});

// 특정 상태에 의존하는 경우
useEffect(() =&gt; {
  console.log(&quot;count가 변경될 때만 실행됨&quot;);
}, [count]);</code></pre>
<hr>
<h2 id="정리clean-up이-필요한-경우-언마운트-처리">정리(clean-up)이 필요한 경우: 언마운트 처리</h2>
<p><code>useEffect</code>는 <strong>컴포넌트가 언마운트되거나, 다음 Effect가 실행되기 전</strong> 정리 작업을 할 수 있도록 <strong>함수를 반환</strong>할 수 있다.</p>
<pre><code class="language-jsx">useEffect(() =&gt; {
  const id = setInterval(() =&gt; {
    console.log(&quot;타이머 실행 중...&quot;);
  }, 1000);

  return () =&gt; {
    clearInterval(id); // 컴포넌트가 사라질 때 타이머 해제
    console.log(&quot;타이머 정리됨&quot;);
  };
}, []);</code></pre>
<p>정리(clean-up)는 메모리 누수나 이벤트 중복을 방지하는 데 중요하다.</p>
<hr>
<h2 id="의존성-배열-누락에-주의">의존성 배열 누락에 주의</h2>
<p>의존성 배열을 잘못 작성하면 다음과 같은 문제가 생길 수 있다:</p>
<ol>
<li><strong>의존성이 빠진 경우</strong>: 최신 상태를 참조하지 못함</li>
<li><strong>불필요한 재실행</strong>: 바뀌지 않는 값까지 포함하면 성능 낭비</li>
</ol>
<p>React 공식 문서는 ESLint 규칙(<code>react-hooks/exhaustive-deps</code>)을 사용하여 자동으로 경고를 주도록 권장한다.</p>
<pre><code class="language-jsx">useEffect(() =&gt; {
  doSomething(value); // value는 의존성 배열에 포함되어야 함
}, []); // ❌ value 누락</code></pre>
<hr>
<h2 id="핵심-정리">핵심 정리</h2>
<ol>
<li><code>useEffect</code>는 <strong>렌더링 이후 부수 효과(사이드 이펙트)</strong> 를 실행하는 Hook이다.</li>
<li>두 번째 인자인 <strong>의존성 배열</strong>을 통해 실행 조건을 제어할 수 있다.</li>
<li><code>return</code> 문을 통해 정리(clean-up) 작업을 정의할 수 있다.</li>
<li>상태나 props가 변경될 때 해당 값들을 의존성 배열에 명시해야 한다.</li>
<li>과도한 <code>useEffect</code> 사용은 오히려 코드를 복잡하게 만들 수 있으므로, <strong>정확한 시점에만 효과를 적용하는 게 핵심</strong>이다.</li>
</ol>
<hr>
<h2 id="마무리">마무리</h2>
<p>React에서 <code>useEffect</code>는 상태 변화에 따른 외부 작업을 다루기 위한 <strong>사이드 이펙트 전용 영역</strong>이다.
함수형 컴포넌트에서는 필수적인 개념이며, 상태와 동기화되는 동작(API 요청, 타이머, 구독 등)을 처리할 때 유용하게 사용된다.</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li>React 공식 문서 - useEffect</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Hook 완전 정복] Step 1: useState 기초와 활용]]></title>
            <link>https://velog.io/@mini_suyo/React-Hook-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Step-1-useState-%EA%B8%B0%EC%B4%88%EC%99%80-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@mini_suyo/React-Hook-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Step-1-useState-%EA%B8%B0%EC%B4%88%EC%99%80-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Wed, 11 Jun 2025 10:49:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/64f5cb17-3caa-43bf-93fd-9743b5f7024f/image.png" alt="">React는 클래스형 컴포넌트에서 함수형 컴포넌트 중심으로 구조가 변화하면서, <strong>Hook</strong>이라는 개념을 도입했다. 그중에서도 <code>useState</code>는 가장 기초적이면서도 자주 사용되는 Hook으로, 컴포넌트 내부에 <strong>상태(state)를 선언하고 조작</strong>할 수 있게 해준다.</p>
<p>이 글에서는 공식 문서의 내용을 바탕으로, <code>useState</code>의 기본적인 사용법과 동작 원리를 정리해보았다.</p>
<hr>
<h2 id="hook이란">Hook이란?</h2>
<p>Hook은 함수형 컴포넌트에서 <strong>상태 값 관리</strong> 및 <strong>생명주기 처리</strong> 같은 React 고유 기능을 사용할 수 있도록 도와주는 함수이다.
Hook 이전에는 상태 관리나 라이프사이클 처리를 위해 클래스형 컴포넌트를 사용해야 했지만, Hook을 통해 더 간결하고 선언적인 코드 작성이 가능해졌다.</p>
<hr>
<h2 id="hook-사용-규칙">Hook 사용 규칙</h2>
<p>공식 문서에 따르면, Hook은 다음과 같은 두 가지 규칙을 반드시 따라야 한다.</p>
<h3 id="1-항상-컴포넌트-함수의-최상단에서-호출해야-한다">1. 항상 컴포넌트 함수의 최상단에서 호출해야 한다</h3>
<p>Hook은 컴포넌트가 렌더링될 때 <strong>호출 순서를 기준으로 상태를 연결</strong>하므로, 조건문이나 반복문, 중첩 함수 내부에서는 사용할 수 없다.</p>
<pre><code class="language-jsx">// ❌ 잘못된 예시
function MyComponent() {
  if (someCondition) {
    const [value, setValue] = useState(0); // 조건문 내부 호출 ❌
  }
}

// ✅ 올바른 예시
function MyComponent() {
  const [value, setValue] = useState(0); // 항상 최상단에서 호출
}</code></pre>
<h3 id="2-오직-react-함수형-컴포넌트-또는-커스텀-hook에서만-사용해야-한다">2. 오직 React 함수형 컴포넌트 또는 커스텀 Hook에서만 사용해야 한다</h3>
<p>Hook은 일반 함수나 클래스 컴포넌트에서는 사용할 수 없다.
React의 렌더링 흐름과 상태 관리 시스템은 함수형 컴포넌트 기반으로 동작하기 때문에, 다음과 같은 코드에서는 오류가 발생한다.</p>
<pre><code class="language-jsx">// ❌ 잘못된 예시 1: 일반 함수에서 Hook 사용 (에러 발생)
function notAComponent() {
  const [value, setValue] = useState(0); // 오류: 일반 함수에서는 Hook 사용 불가
  return value;
}

// ❌ 잘못된 예시 2: 클래스 컴포넌트에서 Hook 사용 (에러 발생)
class MyComponent extends React.Component {
  render() {
    const [count, setCount] = useState(0); // 오류: 클래스에서는 Hook 사용 불가
    return &lt;div&gt;{count}&lt;/div&gt;;
  }
}

// ✅ 올바른 예시 1: 함수형 컴포넌트에서 Hook 사용
function FunctionalComponent() {
  const [count, setCount] = useState(0);
  return (
    &lt;div&gt;
      &lt;p&gt;카운트: {count}&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;+1&lt;/button&gt;
    &lt;/div&gt;
  );
}

// ✅ 올바른 예시 2: 커스텀 Hook 내부에서 사용
function useCounter(initialValue) {
  const [count, setCount] = useState(initialValue);
  return [count, setCount];
}

function CustomHookComponent() {
  const [count, setCount] = useCounter(0);
  return (
    &lt;div&gt;
      &lt;p&gt;커스텀 훅 카운트: {count}&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;+1&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<hr>
<h2 id="usestate란">useState란?</h2>
<p><code>useState</code>는 컴포넌트에 상태를 선언하고 해당 값을 변경할 수 있는 <strong>가장 기본적인 Hook</strong>이다.
호출 시에는 배열을 반환하며, 구조 분해 할당으로 <code>상태값</code>과 <code>상태를 갱신하는 함수</code>를 각각 받아 사용할 수 있다.</p>
<pre><code class="language-jsx">const [state, setState] = useState(initialValue);</code></pre>
<ul>
<li><code>state</code>: 현재 상태 값</li>
<li><code>setState</code>: 상태를 변경하는 함수</li>
<li><code>initialValue</code>: 상태의 초기값</li>
</ul>
<hr>
<h2 id="기본-예제">기본 예제</h2>
<pre><code class="language-jsx">import { useState } from &#39;react&#39;;

function Counter() {
  const [count, setCount] = useState(0); // 초기값 0

  return (
    &lt;div&gt;
      &lt;p&gt;현재 카운트: {count}&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;+1&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="설명">설명</h3>
<ul>
<li><code>useState(0)</code>은 초기값을 0으로 설정하며, <code>count</code>는 현재 상태 값을, <code>setCount</code>는 그 값을 갱신하는 함수이다.</li>
<li>버튼 클릭 시 <code>setCount(count + 1)</code>을 호출하면, React는 컴포넌트를 다시 렌더링하며 <code>count</code>의 변경된 값을 반영한다.</li>
</ul>
<hr>
<h2 id="상태-변경은-비동기적이다">상태 변경은 비동기적이다</h2>
<p><code>setState</code> 함수는 즉시 상태를 바꾸는 것이 아니라, <strong>React가 다음 렌더링에서 새로운 값을 반영하도록 예약</strong>하는 구조로 동작한다.</p>
<pre><code class="language-jsx">setCount(count + 1);
console.log(count); // 여전히 이전 값일 수 있다!</code></pre>
<p>따라서 이전 상태를 기반으로 새로운 상태를 계산할 때는 다음과 같이 함수형 업데이트를 사용하는 것이 안전하다.</p>
<pre><code class="language-jsx">setCount(prevCount =&gt; prevCount + 1);</code></pre>
<hr>
<h2 id="상태는-어떤-타입이든-가능하다">상태는 어떤 타입이든 가능하다</h2>
<p>초기값으로는 숫자뿐 아니라 문자열, 배열, 객체 등 <strong>모든 JavaScript 값</strong>을 사용할 수 있다.</p>
<pre><code class="language-jsx">const [name, setName] = useState(&quot;Alice&quot;);
const [todos, setTodos] = useState([&quot;공부하기&quot;, &quot;운동하기&quot;]);
const [user, setUser] = useState({ name: &quot;민수&quot;, age: 26 });</code></pre>
<p>객체나 배열은 불변성을 유지한 채로 업데이트하는 것이 좋다:</p>
<pre><code class="language-jsx">setUser(prev =&gt; ({ ...prev, age: prev.age + 1 }));</code></pre>
<hr>
<h2 id="계산-비용이-큰-초기값은-함수로-전달하자">계산 비용이 큰 초기값은 함수로 전달하자</h2>
<pre><code class="language-jsx">const [data, setData] = useState(() =&gt; heavyComputation());</code></pre>
<p>이렇게 함수로 전달하면, <code>heavyComputation()</code>은 <strong>처음 렌더링 시에만 실행</strong>된다. 매 렌더링마다 호출되는 것을 방지할 수 있다.</p>
<hr>
<p>좋습니다! 글의 마무리 부분에 들어갈 수 있는 <strong><code>useState</code> 핵심 개념 요약</strong>을 아래와 같이 정리해드릴게요. 시리즈형 블로그의 흐름을 유지하면서, 독자가 다시 한 번 중요한 내용을 짚고 넘어갈 수 있도록 구성했습니다.</p>
<hr>
<h2 id="핵심-정리">핵심 정리</h2>
<p><code>useState</code>를 제대로 이해하기 위해 기억해야 할 핵심 포인트는 다음과 같다:</p>
<ol>
<li><p><strong>컴포넌트 내부 상태를 선언할 수 있게 해주는 Hook이다.</strong>
→ 상태 값과 상태 변경 함수를 <code>[state, setState]</code> 형태로 제공한다.</p>
</li>
<li><p><strong>초기값은 숫자, 문자열, 배열, 객체 등 모든 JS 값이 가능하다.</strong>
→ 계산 비용이 클 경우 함수로 전달하여 최초 1회만 실행할 수도 있다.</p>
</li>
<li><p><strong>상태 변경은 비동기적이다.</strong>
→ <code>setState</code> 이후 바로 값을 읽으면 이전 값일 수 있다.
→ 이전 상태 기반 업데이트는 함수형으로 처리하는 것이 안전하다.</p>
<pre><code class="language-js">setCount(prev =&gt; prev + 1);</code></pre>
</li>
<li><p><strong>Hook은 컴포넌트 최상단에서만 사용해야 하며, 조건문이나 반복문, 일반 함수 내부에서는 사용할 수 없다.</strong></p>
</li>
<li><p><strong>Hook은 함수형 컴포넌트 또는 커스텀 Hook에서만 사용할 수 있다.</strong>
→ 클래스 컴포넌트나 일반 함수에서 사용 시 오류가 발생한다.</p>
</li>
</ol>
<hr>
<h2 id="마무리">마무리</h2>
<p><code>useState</code>는 가장 단순하지만 가장 널리 사용되는 Hook으로, 상태 기반 UI를 구현하는 데 있어 출발점이 되는 개념이다.
Hook의 규칙과 <code>useState</code>의 작동 방식을 정확히 이해하는 것이, 이후 다룰 <code>useEffect</code>, <code>useRef</code> 등 더 복잡한 Hook들을 배우는 데 튼튼한 기반이 된다.</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://ko.react.dev/reference/react/useState">React 공식 문서 - useState</a></li>
<li>[React 완벽 가이드 2025 with React Router &amp; Redux -Udemy]
(<a href="https://www.udemy.com/course/best-react">https://www.udemy.com/course/best-react</a>)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 루트 구조]]></title>
            <link>https://velog.io/@mini_suyo/Next.js-%EB%A3%A8%ED%8A%B8-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@mini_suyo/Next.js-%EB%A3%A8%ED%8A%B8-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Fri, 02 May 2025 00:52:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/be9875a8-8391-4762-a502-852016785b51/image.png" alt=""></p>
<h2 id="nextjs-루트-구조-완벽-정리-app-router-기준">Next.js 루트 구조 완벽 정리 (App Router 기준)</h2>
<p>Next.js를 처음 접하거나, 최신 버전(App Router)을 사용하려고 할 때 프로젝트 루트 구조를 이해하는 건 정말 중요합니다.<br>이번 글에서는 <strong>Next.js 기본 디렉터리 구조</strong>를 <strong>App Router 기준</strong>으로 체계적으로 설명합니다.</p>
<hr>
<h3 id="1-최상단-구조">1. 최상단 구조</h3>
<p>Next.js 프로젝트를 생성하면 기본적으로 다음과 같은 루트 구조를 갖습니다.</p>
<pre><code>my-next-app/
├── app/
├── public/
├── node_modules/
├── .next/
├── package.json
├── next.config.js
├── tsconfig.json / jsconfig.json
└── README.md</code></pre><h4 id="주요-폴더와-파일-설명">주요 폴더와 파일 설명</h4>
<table>
<thead>
<tr>
<th align="left">이름</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>app/</code></td>
<td align="left"><strong>App Router 기반</strong> 라우팅 및 페이지 구성 폴더</td>
</tr>
<tr>
<td align="left"><code>public/</code></td>
<td align="left">정적 파일(이미지, 폰트 등)을 넣는 곳</td>
</tr>
<tr>
<td align="left"><code>node_modules/</code></td>
<td align="left">프로젝트 의존성 모듈</td>
</tr>
<tr>
<td align="left"><code>.next/</code></td>
<td align="left">Next.js가 빌드할 때 생성하는 폴더 (자동 관리)</td>
</tr>
<tr>
<td align="left"><code>package.json</code></td>
<td align="left">프로젝트 설정과 의존성 관리</td>
</tr>
<tr>
<td align="left"><code>next.config.js</code></td>
<td align="left">Next.js 전용 설정 파일</td>
</tr>
<tr>
<td align="left"><code>tsconfig.json</code></td>
<td align="left">(TypeScript 사용 시) 타입스크립트 설정 파일</td>
</tr>
<tr>
<td align="left"><code>README.md</code></td>
<td align="left">프로젝트 설명 파일</td>
</tr>
</tbody></table>
<hr>
<h3 id="2-app-폴더---새로운-app-router의-핵심">2. <code>app/</code> 폴더 - 새로운 App Router의 핵심</h3>
<p>Next.js 13부터 새롭게 추가된 <code>app/</code> 폴더는 <strong>파일 기반 라우팅</strong>의 중심입니다.<br>이 안에 페이지, 레이아웃, 템플릿 등을 구성합니다.</p>
<p>예시:</p>
<pre><code>app/
├── layout.tsx
├── page.tsx
├── globals.css
├── about/
│   ├── page.tsx
│   └── layout.tsx
├── blog/
│   ├── [slug]/
│   │   └── page.tsx
│   └── page.tsx
└── api/
    └── hello/
        └── route.ts</code></pre><h4 id="app-폴더-주요-규칙"><code>app/</code> 폴더 주요 규칙</h4>
<table>
<thead>
<tr>
<th align="left">파일/폴더</th>
<th align="left">역할</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>page.tsx</code></td>
<td align="left">하나의 페이지(=URL 경로)로 렌더링</td>
</tr>
<tr>
<td align="left"><code>layout.tsx</code></td>
<td align="left">페이지에 공통으로 적용할 레이아웃</td>
</tr>
<tr>
<td align="left"><code>template.tsx</code></td>
<td align="left">매번 새로운 인스턴스를 렌더링하는 레이아웃</td>
</tr>
<tr>
<td align="left"><code>error.tsx</code></td>
<td align="left">해당 라우트 에러 처리</td>
</tr>
<tr>
<td align="left"><code>loading.tsx</code></td>
<td align="left">해당 라우트 로딩 상태 처리</td>
</tr>
<tr>
<td align="left"><code>not-found.tsx</code></td>
<td align="left">404 페이지</td>
</tr>
<tr>
<td align="left"><code>api/</code></td>
<td align="left">API 라우트를 만드는 폴더 (<code>route.ts</code> 필수)</td>
</tr>
</tbody></table>
<hr>
<h3 id="3-public-폴더">3. <code>public/</code> 폴더</h3>
<ul>
<li>이 폴더 안에 넣은 파일은 <strong>/ 경로 기준</strong>으로 접근할 수 있습니다.</li>
<li>예: <code>/public/images/logo.png</code> → <code>https://yourdomain.com/images/logo.png</code></li>
</ul>
<p><strong>주의:</strong> <code>next/image</code>처럼 최적화된 이미지 컴포넌트를 사용할 경우에는 import 방식으로 가져와야 하고,<br>정적 파일만 <code>public/</code>에 두어야 합니다.</p>
<hr>
<h3 id="4-프로젝트-설정-파일">4. 프로젝트 설정 파일</h3>
<table>
<thead>
<tr>
<th align="left">파일명</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>next.config.js</code></td>
<td align="left">Next.js 커스터마이징 설정 (리다이렉트, 이미지 도메인, 플러그인 등)</td>
</tr>
<tr>
<td align="left"><code>package.json</code></td>
<td align="left">npm/yarn 관리, 스크립트, 의존성 명시</td>
</tr>
<tr>
<td align="left"><code>tsconfig.json</code></td>
<td align="left">TypeScript 설정 (JS 사용시 <code>jsconfig.json</code>)</td>
</tr>
</tbody></table>
<hr>
<h3 id="5-빌드-및-런타임-폴더">5. 빌드 및 런타임 폴더</h3>
<table>
<thead>
<tr>
<th align="left">폴더</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>.next/</code></td>
<td align="left">빌드 시 자동 생성, 서버/클라이언트 번들 파일 저장</td>
</tr>
<tr>
<td align="left"><code>node_modules/</code></td>
<td align="left">설치된 모든 외부 패키지 저장소</td>
</tr>
</tbody></table>
<p><strong>TIP:</strong> <code>.next/</code>와 <code>node_modules/</code>는 직접 수정하지 않습니다. (Git에도 보통 제외합니다.)</p>
<hr>
<h3 id="6-추가로-알면-좋은-것">6. 추가로 알면 좋은 것</h3>
<ul>
<li><strong><code>middleware.ts</code></strong> : 요청(Request) 가로채기 및 처리 (예: 인증 리다이렉트)</li>
<li><strong><code>env</code> 파일</strong> : <code>.env.local</code>, <code>.env.production</code> 등으로 환경변수 관리</li>
<li><strong><code>components/</code> 폴더</strong> : UI 컴포넌트 모듈화</li>
<li><strong><code>lib/</code>, <code>hooks/</code>, <code>utils/</code> 폴더</strong> : 재사용 가능한 함수 및 로직 정리</li>
</ul>
<hr>
<h2 id="pages-router-vs-app-router">Pages Router vs App Router</h2>
<p>Next.js 13부터 <strong>App Router</strong>가 등장했지만, 기존 <strong>Pages Router</strong>도 여전히 사용 가능합니다.<br>둘의 차이를 간단히 비교해봅니다.</p>
<table>
<thead>
<tr>
<th align="left">항목</th>
<th align="left">Pages Router (<code>pages/</code>)</th>
<th align="left">App Router (<code>app/</code>)</th>
</tr>
</thead>
<tbody><tr>
<td align="left">폴더명</td>
<td align="left"><code>pages/</code></td>
<td align="left"><code>app/</code></td>
</tr>
<tr>
<td align="left">라우팅 방식</td>
<td align="left">파일 기반 라우팅</td>
<td align="left">파일 기반 + 컴포넌트 기반 레이아웃</td>
</tr>
<tr>
<td align="left">레이아웃</td>
<td align="left"><code>_app.js</code>, <code>_document.js</code></td>
<td align="left"><code>layout.tsx</code>, <code>template.tsx</code> (라우트별 레이아웃 지원)</td>
</tr>
<tr>
<td align="left">데이터 패칭</td>
<td align="left"><code>getServerSideProps</code>, <code>getStaticProps</code></td>
<td align="left"><code>fetch()</code>, <code>useEffect</code>, 서버 컴포넌트(Server Components)</td>
</tr>
<tr>
<td align="left">클라이언트 컴포넌트 표시</td>
<td align="left">기본 클라이언트 컴포넌트</td>
<td align="left"><code>use client</code> 지시어 필요</td>
</tr>
<tr>
<td align="left">서버 컴포넌트</td>
<td align="left">지원 안 함</td>
<td align="left">기본적으로 서버 컴포넌트 지원</td>
</tr>
<tr>
<td align="left">사용 시기</td>
<td align="left">빠르게 만들거나, 레거시 프로젝트 유지</td>
<td align="left">새로운 프로젝트, 서버 컴포넌트 적극 사용 시</td>
</tr>
<tr>
<td align="left">학습 난이도</td>
<td align="left">쉬움</td>
<td align="left">약간 복잡하지만 확장성 높음</td>
</tr>
</tbody></table>
<p>✅ 정리:  </p>
<ul>
<li>빠르게 MVP를 만들거나 익숙한 구조를 원하면 <strong>Pages Router</strong>  </li>
<li>서버 컴포넌트, 고성능 최적화를 활용하고 싶다면 <strong>App Router</strong></li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>Next.js의 루트 구조를 정확히 이해하면,<br><strong>대규모 프로젝트도 깔끔하게 관리</strong>할 수 있습니다.</p>
<p>특히 App Router를 쓴다면, <strong>&quot;페이지 중심&quot;</strong> 이 아닌 <strong>&quot;레이아웃 중심&quot;</strong> 설계에 익숙해지는 게 정말 중요합니다.<br>지금부터 천천히 구조를 잡아가보세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 개발자 포트폴리오]]></title>
            <link>https://velog.io/@mini_suyo/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4</link>
            <guid>https://velog.io/@mini_suyo/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4</guid>
            <pubDate>Mon, 24 Feb 2025 01:33:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/360c2356-0325-4014-8a60-e3339236f640/image.png" alt=""></p>
<h2 id="포트폴리오가-중요한-이유">포트폴리오가 중요한 이유</h2>
<p>프론트엔드 개발자는 사용자가 직접 접하는 UI/UX를 구현하는 역할을 담당한다. 따라서 기술 역량뿐만 아니라 디자인 감각, 사용자 중심의 사고, 최신 트렌드 반영 능력 등을 증명하는 것이 중요하다. 이를 효과적으로 보여줄 수 있는 것이 바로 포트폴리오다.</p>
<hr>
<h2 id="포트폴리오-웹사이트의-필수-항목">포트폴리오 웹사이트의 필수 항목</h2>
<p><strong>1. 사이트명</strong>
웹사이트 제목은 보통 본인의 이름이나 닉네임을 사용한다. 이는 웹사이트 URL 자체가 개인의 브랜드를 드러낼 수 있는 중요한 요소이기 때문이다.</p>
<p><strong>2. 자기소개</strong>
자신이 다룰 수 있는 기술과 경험 수준을 간결하게 정리한다. 타이포그래피나 간단한 캐릭터 디자인을 활용하면 더욱 인상적인 소개가 될 수 있다.</p>
<p><strong>3. 프로젝트</strong>
프로젝트를 소개할 때는 목표, 기간, 주요 기능을 구체적으로 작성해야 한다. 프로젝트 흐름도나 회고를 추가하면 전문성이 더욱 돋보인다.</p>
<p><strong>3-1. 프로젝트 소개</strong>
프로젝트의 목적, 주요 내용, 진행 기간을 상세히 기록한다. 주요 기능은 간결하면서도 핵심을 전달할 수 있도록 정리한다.</p>
<p><strong>3-2. 기술 스택</strong>
프로젝트에서 사용한 기술 스택을 단순히 나열하는 것이 아니라, 해당 기술을 선택한 이유와 적용 방법까지 설명하는 것이 중요하다.</p>
<p><strong>3-3. 기여도</strong>
프로젝트에서 본인의 기여도를 퍼센트나 그래프로 시각적으로 표현하면 이해하기 쉽다. 코딩뿐만 아니라 버그 수정, 기능 개선, 디자인 조율 등 다양한 기여 내용을 포함하는 것이 좋다.</p>
<hr>
<h2 id="포트폴리오-웹사이트-작성-팁">포트폴리오 웹사이트 작성 팁</h2>
<p><strong>1. 성장 가능성을 어필한다</strong>
프론트엔드 개발자의 역할은 데이터 가공, 캐싱, 라우터 관리 등으로 확장되고 있다. 따라서 원하는 직무와 커리어 방향을 고민하고, 이를 포트폴리오에 반영해야 한다.</p>
<p><strong>2. 개발에 대한 열정을 보여준다</strong>
자신이 관심 있는 주제로 서비스를 만들어 보고, 그 과정을 포트폴리오에 기록하는 것이 좋다. 과거에 만든 프로젝트를 리팩터링하여 개선하는 것도 하나의 방법이다.</p>
<p><strong>3. 성능 최적화 경험</strong>
프론트엔드 개발자로서 웹사이트 성능을 최적화한 경험을 추가하면 좋다. 예를 들어, 이미지 최적화, 코드 스플리팅, 지연 로딩 등의 기법을 적용한 사례를 설명하면 경쟁력을 높일 수 있다. 포트폴리오는 단순한 프로젝트 나열이 아니라, 개발자로서의 강점과 성장 가능성을 보여주는 중요한 도구다. 차별화된 포트폴리오를 만들어 취업 시장에서 경쟁력을 갖추도록 하자.</p>
<p><strong>4. 고민의 흔적을 남긴다</strong>
좋은 코드를 만들기 위해 거친 과정을 보여주는 것이 중요하다. 문제를 해결하기 위해 어떤 방법을 시도했는지, 코드의 개선 과정을 어떻게 진행했는지를 기록하면 채용 담당자에게 긍정적인 인상을 줄 수 있다.</p>
<p><strong>5. 커뮤니케이션 능력을 강조한다</strong>
포트폴리오는 채용 담당자의 시선을 사로잡아야 한다. 다양한 경로로 공유하고 피드백을 반영하면 점점 더 완성도를 높일 수 있다. 또한, 협업 경험과 소통 능력을 강조하면 프론트엔드 개발자로서의 강점을 더욱 부각할 수 있다.</p>
<hr>
<h2 id="포트폴리오-웹사이트-작성-시-유의할-점">포트폴리오 웹사이트 작성 시 유의할 점</h2>
<p><strong>1. 깔끔하고 직관적인 디자인을 유지한다</strong>
메인 페이지는 첫인상을 좌우하는 핵심 요소다. 불필요한 애니메이션을 자제하고, 핵심 정보를 직관적으로 배치해야 한다.</p>
<p><strong>2. 반응형 디자인 구현 여부</strong>
포트폴리오 웹사이트는 다양한 기기에서 최적화되어야 한다. 반응형 디자인을 적용하고 이를 강조하면 가산점이 될 수 있다.</p>
<p><strong>3. 접근성(A11Y) 고려</strong></p>
<p>웹 접근성을 준수한 개발 경험을 강조하면 더욱 전문성이 돋보인다. 예를 들어, WAI-ARIA 속성을 활용하거나 키보드 네비게이션을 지원한 사례를 포함하면 좋다.</p>
<p><strong>4. 무료 서브도메인 사용을 피한다</strong>
무료 도메인은 신뢰도가 낮고, 악성코드에 노출될 가능성이 크며, 도메인이 회수될 위험도 있다. 따라서 커스텀 도메인을 구매해 연결하는 것이 좋다.</p>
<p><strong>5. 라이브 프로젝트 링크를 추가한다</strong>
프로젝트의 실체를 보여줄 수 있는 라이브 링크를 반드시 포함해야 한다. 화면으로 보여줄 수 없는 프로젝트라면 깃허브에 코드를 업로드하고 README 파일을 꼼꼼하게 작성하는 것도 좋은 방법이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서 중복호출방지]]></title>
            <link>https://velog.io/@mini_suyo/React%EC%97%90%EC%84%9C-%EC%A4%91%EB%B3%B5%ED%98%B8%EC%B6%9C%EB%B0%A9%EC%A7%80</link>
            <guid>https://velog.io/@mini_suyo/React%EC%97%90%EC%84%9C-%EC%A4%91%EB%B3%B5%ED%98%B8%EC%B6%9C%EB%B0%A9%EC%A7%80</guid>
            <pubDate>Mon, 17 Feb 2025 00:57:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/a086b917-a4f8-48eb-ba7b-d711f5c3510c/image.png" alt=""></p>
<h2 id="개요">개요</h2>
<p>프로젝트 진행중에 중복클릭이 가능하여 같은 폼을 여러번 보내는 문제가 발생했다. 개발자도구에서 network throttling을 3g로 걸어주니 서버에 호출을 받기까지 시간이 2초정도 걸리더라...
이렇게 개발자가 의도하지 않은 동작을 막기위해 isLoading 메서드를 사용했다.</p>
<h2 id="isloading의-문제">isLoading의 문제</h2>
<pre><code class="language-jsx">const [isLoading, setIsLoading] = useState(false);

const handleConfirmSubmit = async () =&gt; {
    if (isLoading) {
      return;
    }

    setIsLoading(true);
    try {
      const sushiData = {
        title,
        content,
        maxAnswers,
        category,
        sushiType,
      };
      console.log(&quot;등록된 내용:&quot;, sushiData);
      const response = await dispatch(createSushi(sushiData));
      const { success, data, error } = response.payload;
      const { token } = data;
      const shareUrl = `share/${token}`;
      setShareUrl(shareUrl);
      console.log(&quot;공유 URL:&quot;, shareUrl);
      setShowModal(false);
      setShowCompleteModal(true);
    } finally {
      setIsLoading(false);
    }
  };</code></pre>
<p>이런식으로 isLoading 메서드를 useState를 통해 걸어줄 수 있다. 이런 방식은 다음과 같은 문제점을 가지고 있다.</p>
<ol>
<li>경쟁 상태(Race Condition): 여러 요청이 동시에 발생할 경우, isLoading 상태가 정확하게 반영되지 않을 수 있다.</li>
<li>상태 업데이트 지연: React의 상태 업데이트는 비동기적이므로, isLoading 상태가 변경되기 전에 여러 번의 클릭이 발생할 수 있다.</li>
<li>컴포넌트 언마운트 문제: 요청 중에 컴포넌트가 언마운트되면 isLoading 상태가 적절히 초기화되지 않을 수 있습니다.</li>
</ol>
<p>이러한 문제들을 방지하기 위해 useRef를 사용하는 것이 권장된다.</p>
<h2 id="useref를-사용하는-코드">useRef를 사용하는 코드</h2>
<pre><code class="language-jsx">const dispatch = useDispatch();
const isSubmittingRef = useRef(false);
const reRender = useCallback(() =&gt; {}, []);

const handleConfirmSubmit = async () =&gt; {
  if (isSubmittingRef.current) {
    return;
  }

  isSubmittingRef.current = true;
  reRender();

  try {
    const sushiData = {
      title,
      content,
      maxAnswers,
      category,
      sushiType,
    };
    const response = await dispatch(createSushi(sushiData));
    const { success, data, error } = response.payload;
    const { token } = data;
    const shareUrl = `share/${token}`;
    setShareUrl(shareUrl);
    setShowModal(false);
    setShowCompleteModal(true);
  } finally {
    isSubmittingRef.current = false;
    reRender();
  }
};</code></pre>
<p>useRef를 사용하는 방식의 장점</p>
<ol>
<li>즉각적인 값 업데이트: ref는 동기적으로 업데이트되어 경쟁 상태를 방지한다.</li>
<li>리렌더링 독립성: ref 값의 변경은 리렌더링을 트리거하지 않아 성능상 이점이 있다.</li>
<li>컴포넌트 생명주기 독립성: 컴포넌트가 언마운트되어도 ref 값이 유지된다.</li>
</ol>
<h2 id="결론">결론</h2>
<blockquote>
<p>Claude와의 대화를 통해 useState를 통해 중복클릭 방지를 하는 방안을 적용했다. 그러다가 혹시 다른 문제가 발생하지 않을까? 하고 구글링을 시작했다. 그러다가 <a href="https://happysisyphe.tistory.com/72">https://happysisyphe.tistory.com/72</a>
사이트를 발견하고 useRef를 사용하는 방법을 적용했다. 또한 더욱 안정적인 방법 두가지 또한 알게 되었다.
AI에게만 의존하지말고 구글링도 계속해서 더 실력있는 개발자가 되자!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React의 Portals]]></title>
            <link>https://velog.io/@mini_suyo/React%EC%9D%98-Portals</link>
            <guid>https://velog.io/@mini_suyo/React%EC%9D%98-Portals</guid>
            <pubDate>Sun, 02 Feb 2025 07:08:13 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/4908956b-76b1-439d-af87-dd9303571590/image.png" alt=""></p>
<h2 id="portal이란">Portal이란?</h2>
<p>React의 Portal은 특정 컴포넌트를 부모 컴포넌트의 DOM 계층 구조가 아니라, 지정된 DOM 노드에 렌더링할 수 있도록 도와주는 기능이다. 기본적으로 React는 컴포넌트 트리 구조를 따라 렌더링되지만, Portal을 사용하면 부모 컴포넌트의 DOM 구조를 벗어나 원하는 위치에 컴포넌트를 마운트할 수 있다.</p>
<hr>

<h3 id="portals를-사용하는-이유">Portals를 사용하는 이유</h3>
<ol>
<li><strong>DOM 계층 구조를 벗어나 렌더링</strong></li>
</ol>
<ul>
<li>React의 컴포넌트 트리와 관계없이 특정한 DOM 요소에 렌더링할 수 있다.</li>
<li>예를 들어, 모달, 드롭다운, 툴팁 같은 UI 요소를 body 태그 바로 아래에 렌더링하여 스타일과 동작을 안정적으로 유지할 수 있다.</li>
</ul>
<ol start="2">
<li><strong>CSS z-index 문제 해결</strong></li>
</ol>
<ul>
<li>부모 요소의 overflow: hidden 또는 z-index 속성으로 인해 모달이나 드롭다운이 잘리는 문제를 방지할 수 있다.</li>
</ul>
<ol start="3">
<li><strong>이벤트 버블링의 개선</strong></li>
</ol>
<ul>
<li>Portal을 사용해도 이벤트는 React의 이벤트 시스템을 그대로 따르지만, 부모 컴포넌트와 겹치는 이벤트(예: onClick 이벤트로 모달이 닫히는 문제)를 효과적으로 관리할 수 있다.</li>
</ul>
<hr>

<h3 id="portal이-권장되는-상황">Portal이 권장되는 상황</h3>
<ol>
<li><strong>모달(Modal)</strong></li>
</ol>
<ul>
<li>모달이 부모 컴포넌트 내에서 렌더링되면 overflow: hidden 같은 스타일에 의해 가려질 수 있다.</li>
<li>Portal을 사용하여 document.body 아래에 렌더링하면 문제를 방지할 수 있다.</li>
</ul>
<ol start="2">
<li><strong>드롭다운 메뉴 (Dropdown Menu)</strong></li>
</ol>
<ul>
<li>부모 요소의 overflow: hidden 때문에 드롭다운이 잘리는 문제를 해결할 수 있다.</li>
</ul>
<ol start="3">
<li><strong>툴팁 (Tooltip)</strong></li>
</ol>
<ul>
<li>특정 요소에 툴팁을 표시할 때 부모 요소의 overflow 속성에 영향을 받지 않도록 별도의 DOM 요소에 렌더링한다.</li>
</ul>
<p>-&gt; Portal은 특정 UI 요소를 부모 컴포넌트의 DOM 구조를 벗어나 별도의 DOM 요소에 렌더링하고 싶을 때 유용하다. 때문에 모달, 드롭다운, 툴팁과 같이 <strong>부모 요소의 스타일이나 z-index 속성의 영향을 받지 않아야 하는 UI 컴포넌트</strong>에 권장된다.</p>
<hr>

<h3 id="portal-사용법">Portal 사용법</h3>
<p>우선 완성된 코드를 먼저 봐보자!</p>
<p><strong>Modal.jsx</strong></p>
<pre><code class="language-jsx">import React from &quot;react&quot;;
import { createPortal } from &quot;react-dom&quot;;

const Modal = ({ isOpen, onClose, children }) =&gt; {
  if (!isOpen) return null; // 모달이 닫혀 있으면 렌더링하지 않음

  return createPortal(
    &lt;div className=&quot;modal-overlay&quot; onClick={onClose}&gt;
      &lt;div className=&quot;modal-content&quot; onClick={(e) =&gt; e.stopPropagation()}&gt;
        {children}
        &lt;button onClick={onClose}&gt;닫기&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;,
    document.getElementById(&quot;modal-root&quot;) // 특정 DOM 요소에 렌더링
  );
};

export default Modal;</code></pre>
<p><strong>App.jsx</strong></p>
<pre><code class="language-jsx">import React, { useState } from &quot;react&quot;;
import Modal from &quot;./Modal&quot;;

const App = () =&gt; {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    &lt;div&gt;
      &lt;h1&gt;React Portal 모달 예제&lt;/h1&gt;
      &lt;button onClick={() =&gt; setIsModalOpen(true)}&gt;모달 열기&lt;/button&gt;

      &lt;Modal isOpen={isModalOpen} onClose={() =&gt; setIsModalOpen(false)}&gt;
        &lt;h2&gt;안녕하세요!&lt;/h2&gt;
        &lt;p&gt;이것은 Portal을 사용한 모달입니다.&lt;/p&gt;
      &lt;/Modal&gt;
    &lt;/div&gt;
  );
};

export default App;</code></pre>
<p><strong>index.html</strong></p>
<pre><code class="language-html">&lt;body&gt;
  &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
  &lt;div id=&quot;modal-root&quot;&gt;&lt;/div&gt; &lt;!-- Portal을 위한 추가적인 DOM 노드 --&gt;
&lt;/body&gt;</code></pre>
<h4 id="modaljsx---portal을-통한-모달-렌더링">Modal.jsx - Portal을 통한 모달 렌더링</h4>
<ol>
<li><p>우선 react-dom에서 createPortal을 import한다. </p>
<pre><code class="language-jsx">import React from &quot;react&quot;;
import { createPortal } from &quot;react-dom&quot;;</code></pre>
<p>createPortal의 역할은 이 컴포넌트에 렌더링이 될 HTML 코드를 DOM내에 다른곳으로 옮기는 것이다.</p>
</li>
<li><p>그렇게 하기 위해 전체 코드를 감싸주고 createPortal을 이용하여 보내준다. createPortal은 두가지 인수를 받는데 첫번째 인수는 JSX코드이며, 두번째 인수는 HTML요소이다.</p>
<pre><code class="language-jsx">return createPortal(
 &lt;div className=&quot;modal-overlay&quot; onClick={onClose}&gt;
   &lt;div className=&quot;modal-content&quot; onClick={(e) =&gt; e.stopPropagation()}&gt;
     {children}
     &lt;button onClick={onClose}&gt;닫기&lt;/button&gt;
   &lt;/div&gt;
 &lt;/div&gt;
);</code></pre>
</li>
<li><p>두번째 인수는 기본 브라우저 API로 선택해야한다. <strong>document.getElementById</strong>를 사용하면 된다.</p>
<pre><code class="language-jsx">return createPortal(
&lt;div className=&quot;modal-overlay&quot; onClick={onClose}&gt;
  &lt;div className=&quot;modal-content&quot; onClick={(e) =&gt; e.stopPropagation()}&gt;
    {children}
    &lt;button onClick={onClose}&gt;닫기&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;,
document.getElementById(&quot;modal-root&quot;) // 특정 DOM 요소에 렌더링
);</code></pre>
<blockquote>
<p>위 코드는 메인파일이나 인덱스 파일에서 보던것과 비슷하다!</p>
</blockquote>
</li>
</ol>
<p><strong>main.jsx</strong></p>
<pre><code class="language-jsx">import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import App from &quot;./App.jsx&quot;;
createRoot(document.getElementById(&quot;root&quot;)).render(
  &lt;StrictMode&gt;
    &lt;App /&gt;
  &lt;/StrictMode&gt;
);</code></pre>
<p>여기에 createRoot 메소드가 있는데 이것 또한 HTML파일의 요소가 필요하며, 여기에 리액트 앱이 렌더링된다. 이와 같이  createPortal을 사용함으로써 똑같은 리액트 앱의 출력 위치를 다르게 설정한다.</p>
<h4 id="indexhtml---portal을-위한-dom-요소-추가">index.html - Portal을 위한 DOM 요소 추가</h4>
<ol start="4">
<li>Portal을 위한 추가적인 DOM 노드를 추가하자.<pre><code class="language-html">&lt;body&gt;
&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
&lt;div id=&quot;modal-root&quot;&gt;&lt;/div&gt; &lt;!-- Portal을 위한 추가적인 DOM 노드 --&gt;
&lt;/body&gt;</code></pre>
</li>
</ol>
<h4 id="appjs---모달-상태-관리">App.js - 모달 상태 관리</h4>
<ol start="5">
<li>useState를 사용하여 모달 열림/닫힘 상태를 관리한다.<pre><code class="language-jsx">const [isModalOpen, setIsModalOpen] = useState(false);</code></pre>
</li>
<li>버튼을 클릭하면 isModalOpen을 true로 설정하여 모달을 연다.<pre><code class="language-jsx">&lt;button onClick={() =&gt; setIsModalOpen(true)}&gt;모달 열기&lt;/button&gt;
</code></pre>
</li>
</ol>
<pre><code>7. isOpen props를 통해 모달을 열지 결정하고, 
onClose를 호출하면 setIsModalOpen(false)가 실행되어 모달이 닫힌다.
```jsx
&lt;Modal isOpen={isModalOpen} onClose={() =&gt; setIsModalOpen(false)}&gt;</code></pre><blockquote>
<p>프로젝트 진행중에 모달을 여러개 사용해야 할 일이 있었다. 구현과정에서 조금씩 복잡해지고 css적용에 문제가 생겼다... 그러다가 검색을 통해 Portal을 알게 되었다! 아니 이런 대단한 녀석이 있을줄이야.... 이미 만들어놓은 컴포넌트에 Portal을 나중에 적용하려니까 일이 두배가 되어버렸어! 이래서 지식이 중요하구나 싶었다... 열심히해서 두번 일하지 않는 chill한 개발자가 되어보자!
<img src="https://velog.velcdn.com/images/mini_suyo/post/99b284b0-c95f-44aa-b174-025a977ad374/image.png" alt="">(이런.. chill chill치 못하게 두 번 일해버렸네요....)</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 공부 이것저것...]]></title>
            <link>https://velog.io/@mini_suyo/React-%EA%B3%B5%EB%B6%80-%EC%9D%B4%EA%B2%83%EC%A0%80%EA%B2%83</link>
            <guid>https://velog.io/@mini_suyo/React-%EA%B3%B5%EB%B6%80-%EC%9D%B4%EA%B2%83%EC%A0%80%EA%B2%83</guid>
            <pubDate>Sun, 19 Jan 2025 16:03:35 GMT</pubDate>
            <description><![CDATA[<h2 id="1-중요한-데이터는-state에-담자">1. 중요한 데이터는 state에 담자</h2>
<pre><code class="language-javascript">function App(){

  let [글제목, b] = useState(&#39;남자 코트 추천&#39;);
  let posts = &#39;강남 우동 맛집&#39;;
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;div className=&quot;black-nav&quot;&gt;
        &lt;div&gt;개발 blog&lt;/div&gt;
      &lt;/div&gt;
      &lt;div className=&quot;list&quot;&gt;
        &lt;h4&gt;{ 글제목 }&lt;/h4&gt;
        &lt;p&gt;2월 17일 발행&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>위 코드에서 useState를 통해 데이터를 받아와서 <strong>&#39;글제목&#39;</strong>에 값을 넣는다. 이런 식으로 변수를 사용하지 않고 state를 사용해서 자료를 저장할 수 있다.</p>
<p><strong>let [글제목, b] = useState(&#39;남자 코트 추천&#39;)에서 b가 뭐지??</strong>
useState()를 쓰면 그 자리에 [데이터1, 데이터2] 이렇게 생긴 이상한 array가 남는다. 데이터1 자리엔 &#39;남자 코트 추천&#39;같은 자료가 들어있고, 데이터2 자리엔 state변경을 도와주는 함수가 들어있다. 그 데이터들을 각각 변수로 빼고 싶으면 <strong>let [글제목, b] = useState(&#39;남자 코트 추천&#39;)</strong>이러면 된다. </p>
<blockquote>
<p><strong>변수가 아니라 state에 데이터를 저장해서 쓰는이유는 뭘까?</strong>
변수와는 다르게 state는 변동사항이 생기면 state쓰는 html도 자동으로 리렌더링 해준다. 또한 UI 기능 개발이 매우 편리해지고, 사이트가 부드럽게 작동하도록 도와준다.</p>
</blockquote>
<p>하지만 로고나 잘 변화하지 않는 데이터들은 state로 만들어서 데이터바인딩을 할 필요가 없다. state는 상품명, 글제목, 가격 이런것 처럼 자주 변할 것 같은 데이터들을 저장하는 것이 좋다.</p>
<hr>
<h2 id="2-onclick을-사용해서-이벤트-만들기">2. onClick을 사용해서 이벤트 만들기</h2>
<p>버튼을 누르면 좋아요 갯수가 1씩 증가하는 기능을 만들자.</p>
<ol>
<li><p>먼저 state를 만들어 데이터바인딩하자.</p>
<pre><code class="language-javascript">function App(){

let [따봉] = useState(0);
return (
  &lt;h4&gt; { 글제목[0] } &lt;span&gt;👍&lt;/span&gt; { 따봉 }&lt;/h4&gt;
)
}</code></pre>
</li>
<li><p><strong>onClick={ }</strong> 함수를 이용해서 이벤트를 발생 시키자.</p>
<pre><code class="language-javascript">function App(){

function testFunction(){
 console.log(1)
}
return (
 // JSX에서 onClick 사용시 유의점
 // 1. Click이 대문자이다.
 // 2. {} 중괄호 사용한다.
 // 3. 그냥 코드가 아니라 함수를 넣어야 잘 동작한다.
 &lt;div onClick={testFunction}&gt; 안녕하세요 &lt;/div&gt;
)
}</code></pre>
<p>function을 따로 빼서 사용하기 싫다면 onClick안에 함수를 넣을 수도 있다.</p>
<pre><code class="language-javascript">&lt;div onClick={ function(){ 실행할코드 } }&gt; 
&lt;div onClick={ () =&gt; { 실행할코드 } }&gt;</code></pre>
</li>
</ol>
<hr>
<h2 id="⭐-3-state-변경하기">⭐ 3. state 변경하기</h2>
<p>state 변경함수를 사용해서 state를 변경해야 한다.</p>
<pre><code class="language-javascript">function App(){

  let [ 따봉 ] = useState(0);
  return (
    &lt;h4&gt; { 글제목[0] } &lt;span onClick={ ()=&gt;{ 따봉 = 따봉 + 1 } } &gt;👍&lt;/span&gt; { 따봉 }&lt;/h4&gt;
  )
}</code></pre>
<p>위와 같이 { 따봉 = 따봉 + 1 }은 사용 불가!</p>
<blockquote>
<p><strong>state 변경함수</strong></p>
</blockquote>
<pre><code class="language-javascript">let [ state, setState ] = useState(0); </code></pre>
<p>위 코드에서 setState부분이 state 변경함수 이다.
setState(1)을 작성하면 state는 1이 되고, setState(100)을 작성하면 state는 100이 된다.</p>
<hr>
<h2 id="4-arrayobject-자료에서-state-수정">4. array/object 자료에서 state 수정</h2>
<pre><code class="language-javascript">function App(){

  let [글제목, 글제목변경] = useState( [&#39;남자코트 추천&#39;, &#39;강남 우동맛집&#39;, &#39;파이썬 독학&#39;] );  

  return (
    &lt;button onClick={ ()=&gt;{ 
      글제목변경([&#39;여자코트 추천&#39;, &#39;강남 우동맛집&#39;, &#39;파이썬 독학&#39;])
    } }&gt; 수정버튼 &lt;/button&gt;
  )
}</code></pre>
<p>위와 같은 코드로 작성해도 작동은한다. 하지만 확장성이 떨어진다. 때문에 아래와 같은 코드가 추천된다.</p>
<pre><code class="language-javascript">function App(){

  let [글제목, 글제목변경] = useState( [&#39;남자코트 추천&#39;, &#39;강남 우동맛집&#39;, &#39;파이썬 독학&#39;] );  

  return (
    &lt;button onClick={ ()=&gt;{ 
        let copy = [...글제목];
        copy[0] = &quot;여자코트 추천&quot;;
        글제목변경(copy);
    } }&gt; 수정버튼 &lt;/button&gt;
  )
}</code></pre>
<hr>
<h2 id="5-state-변경함수-동작원리">5. state 변경함수 동작원리</h2>
<p>state 변경함수를 쓸 때 기존 <strong>&#39;기존state === 신규state&#39;</strong> 와 같이 먼저 검사해보자. 만약 같다면 state 변경을 해주지 않는다.
<strong>&#39;let copy = state&#39;</strong> 도 마찬가지로 copy와 state가 같아서 변경을 안해준 것이다. copy와 state안의 자료가 다른데 왜 같다고 하는것일까? 그 이유는 다음과 같다.
<img src="https://velog.velcdn.com/images/mini_suyo/post/45cf388c-c8b2-4614-a361-3a3a49633845/image.png" alt=""> 자바스크립트로 arr를 하나 만들어 보자. [1, 2, 3]은 ram에 저장이되고 arr에는 그 자료가 어디에 있는지를 알 수 있는 경로만 가지고 있다. 때문에 aray/object 자료를 복사를 하면 문제가 생긴다.</p>
<pre><code class="language-javascript">let data1 = [1,2,3];
let data2 = data1;</code></pre>
<p>위 코드에서 문제를 확인할 수 있다. data1과 data2는 [1,2,3]값을 저장하는게 아니라, 경로를 가지고 있을 뿐이다. 때문에 data1과 data2는 같은 값을 공유하게 된다. 이러한 이유로 data1을 변경하면 data2도 자동으로 변경된다.
때문에 array/object 자료를 복사하려면 <strong>[...state]</strong> 같은 방식을 사용해야 한다.</p>
<pre><code class="language-javascript">let copy = [...글제목];
copy[0] = &#39;여자코트 추천&#39;;
글제목변경(copy)</code></pre>
<blockquote>
<p><strong>... 이 뭘까?</strong>
spread operator라고 하는 문법으로 array/object 자료형 왼쪽에 붙일 수 있고, <strong>괄호를 벗겨주세요</strong> 라는 뜻이다.
<strong>...[1,2,3]</strong>은 <strong>1,2,3</strong>와 같다. 때문에 ...[]을 사용하면 <strong>괄호를 벗기고 다시 array를 만들어주세요</strong> 라는 말이 된다. 이렇게 복사를 하면 다른 경로를 가리키는 완전히 독립적인 array 복사본을 생성해 줄 수 있다</p>
</blockquote>
<hr>
<h2 id="6-html코드에서의-유의점">6. HTML코드에서의 유의점</h2>
<pre><code class="language-javascript">return(
  &lt;div&gt;&lt;/div&gt;
  &lt;div&gt;&lt;/div&gt;
)</code></pre>
<p>위 코드처럼 return ()안에 두개의 html 태그를 나란히 적을수 없고 하나의 태그로 시작해서 하나의 태그로 끝나야한다.</p>
<pre><code class="language-javascript">return(
  &lt;div&gt;
    &lt;div&gt;&lt;/div&gt;
    &lt;div&gt;&lt;/div&gt;
  &lt;/div&gt;
)</code></pre>
<p>다음과 같이 하나의 div로 감싸야 한다. 의미없는 div를 쓰기 싫다면 fragment 문법인 &lt;&gt;&lt;/&gt;로 감싸도 된다.</p>
<pre><code class="language-javascript">return(
  &lt;&gt;
    &lt;div&gt;&lt;/div&gt;
    &lt;div&gt;&lt;/div&gt;
  &lt;/&gt;
)</code></pre>
<hr>
<h2 id="7-복잡한-html을-한-단어로-치환할-수-있는-component-문법">7. 복잡한 html을 한 단어로 치환할 수 있는 Component 문법</h2>
<p>React는 긴 HTML을 한 단어로 깔끔하게 치환해서 넣을 수 있는 문법을 제공한다. 이를 Component라고 한다.</p>
<pre><code class="language-javascript">function App (){
  return (
    &lt;div&gt;
      (생략)
      &lt;Modal&gt;&lt;/Modal&gt;
    &lt;/div&gt;
  )
}

function Modal(){
  return (
    &lt;div className=&quot;modal&quot;&gt;
      &lt;h4&gt;제목&lt;/h4&gt;
      &lt;p&gt;날짜&lt;/p&gt;
      &lt;p&gt;상세내용&lt;/p&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>위와 같이 HTML을 한 단어로 줄일 수 있다. 줄이는 방법은 다음과 같다.</p>
<ol>
<li>function을 이용해서 함수를 하나 만들어주고 작명한다.</li>
<li>그 함수 안에 return () 안에 축약을 원하는 HTML을 담으면 된다.</li>
<li>그럼 원하는 곳에서 &lt;함수명&gt;&lt;/함수명&gt; 사용하면 아까 축약한 HTML이 등장한다.</li>
</ol>
<blockquote>
<p><strong>Component 만들 때 주의점</strong> </p>
</blockquote>
<ol>
<li>component 작명할 땐 영어대문자로 보통 작명한다.</li>
<li>return () 안엔 html 태그들이 평행하게 여러개 들어갈 수 없습니다.</li>
<li>function App(){} 내부에서 만들면 안된다. component 안에 component 를 만들진 않는다. </li>
<li>&lt;컴포넌트&gt;&lt;/컴포넌트&gt; 이렇게 써도 되고 &lt;컴포넌트/&gt; 이렇게 써도 된다.</li>
</ol>
<blockquote>
<p><strong>어떤 HTML들을 Component 만드는게 좋을까?</strong>
관습적으로 컴포넌트화 하는 경우는 다음과 같다.</p>
</blockquote>
<ul>
<li>사이트에 반복해서 출현하는 HTML 덩어리들은 Component로 만들면 좋다.</li>
<li>내용이 매우 자주 변경될 것 같은 HTML 부분을 잘라서 Component로 만들면 좋다.</li>
<li>다른 페이지를 만들고 싶다면 그 페이지의 HTML 내용을 하나의 Component로 만드는게 좋다.</li>
<li>또는 다른 팀원과 협업할 때 웹페이지를 Component 단위로 나눠서 작업을 분배하기도 한다.</li>
</ul>
<hr>
<h2 id="8-동적인-ui-만드는-step">8. 동적인 UI 만드는 step</h2>
<ol>
<li>html css로 미리 UI 디자인을 마치기</li>
<li>UI의 현재 상태를 state로 저장해두기</li>
<li>state에 따라서 UI가 어떻게 보일지 조건문 등으로 작성하기</li>
</ol>
<p>모달창 기능을 만들면서 실습해보자!</p>
<h3 id="1-html-css로-미리-디자인">1. html css로 미리 디자인</h3>
<pre><code class="language-javascript">.modal {
  margin-top: 20px;
  padding: 20px;
  background: #eee;
  text-align: left;
}</code></pre>
<h3 id="2-ui의-현재-상태를-state로-저장">2. UI의 현재 상태를 state로 저장</h3>
<pre><code class="language-javascript">let [modal, setModal] = useState(false);</code></pre>
<p>모달창은 <strong>열림/닫힘</strong> 두개의 상태밖에 없기 때문에 boolean 타입으로 작성을 해보겠다.</p>
<h3 id="3-state에-따라서-ui가-어떻게-보일지-작성">3. state에 따라서 UI가 어떻게 보일지 작성</h3>
<pre><code class="language-javascript">function App (){

  let [modal, setModal] = useState(false);
  return (
    &lt;div className=&quot;app&quot;&gt;
      (생략)
      {
        modal == true ? &lt;Modal/&gt; : null
      }
    &lt;/div&gt;
  )
}</code></pre>
<hr>
<h2 id="9-많은-div들을-반복문으로-줄이고-싶을때-map을-사용">9. 많은 div들을 반복문으로 줄이고 싶을때 map을 사용</h2>
<p>html이 반복적으로 출현하면 반복문을 이용해서 똑같은 html을 생성할 수 있다. for반복문은 JSX 중괄호 안에서 사용할 수 없어서 map()을 대신 사용한다.
모드 array자료 우측에 map() 함수를 붙일 수 있다.</p>
<h4 id="1-array에-들어있는-자료-갯수만큼-그-안에-있는-코드를-반복-실행한다">1. array에 들어있는 자료 갯수만큼 그 안에 있는 코드를 반복 실행한다.</h4>
<pre><code class="language-javascript">var arr = [2,3,4];
arr.map(function(){
  console.log(1)
});</code></pre>
<p>위 코드에서 arr의 자료 갯수가 세개 이므로 console.log(1)을 세번 실행한다.</p>
<h4 id="2-콜백함수에-파라미터를-아무렇게나-작명하면-그-파라미터는-array안에-있던-모든-자료를-하나씩-출력해준다">2. 콜백함수에 파라미터를 아무렇게나 작명하면 그 파라미터는 array안에 있던 모든 자료를 하나씩 출력해준다.</h4>
<blockquote>
<p><strong>콜백함수란?</strong>
다른 함수의 인자로써 넘겨진 후 특정 이벤트에 의해 호출되는 함수이다.</p>
</blockquote>
<pre><code class="language-javascript">var arr = [2,3,4];
arr.map(function(a){
  console.log(a)
});</code></pre>
<p>위 코드에서는 arr의 자료인 [2, 3, 4]가 &#39;a&#39;로 들어가서 세번 실행된다. 실체로 콘솔창에서 2,3,4가 출력된다.</p>
<h4 id="3-return에-값을-적으면-array로-담아주고-map-쓴-자리에-넘겨준다">3. return에 값을 적으면 array로 담아주고, map() 쓴 자리에 넘겨준다.</h4>
<pre><code class="language-javascript">var arr = [2,3,4];
var newArray = arr.map(function(a){
  return a * 10
});
console.log(newArray)</code></pre>
<p>&#39;a&#39;에 arr의 자료인 [2, 3, 4]가 들어가고, return을 통해 newArray에 [20, 30, 40]이 들어가게 된다.</p>
<p>단순하게 반복문처럼 map() 을 사용할 수도 있다.</p>
<pre><code class="language-javascript">function App (){
  return (
    &lt;div&gt;
      { 
        [1,2,3].map(function(){
          return ( &lt;div&gt;안녕&lt;/div&gt; )
        }) 
      }
    &lt;/div&gt;
  )
}</code></pre>
<p>위 코드는 세번 반복되어서 &#39;안녕&#39;이라는 글이 세번 출력된다.</p>
<hr>
<h2 id="10-props의-활용">10. props의 활용</h2>
<p>자식 컴포넌트가 부모 컴포넌트의 state를 사용하고 싶을 때는 props를 활용한다.
modal 내부에 title state를 넣어보자.</p>
<pre><code class="language-javascript">function App (){
  let [title, setTitle] = useState([&#39;남자코트 추천&#39;, &#39;강남 우동맛집&#39;, &#39;파이썬독학&#39;]);
  return (
    &lt;div&gt;
      &lt;Modal&gt;&lt;/Modal&gt;
    &lt;/div&gt;
  )
}

function Modal(){
  return (
    &lt;div className=&quot;modal&quot;&gt;
      &lt;h4&gt;{ title[0] }&lt;/h4&gt;
      &lt;p&gt;날짜&lt;/p&gt;
      &lt;p&gt;상세내용&lt;/p&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>위와 같은 코드는 실행되지 않는다. title 변수가 function App()에 있기 때문이다. 
<img src="https://velog.velcdn.com/images/mini_suyo/post/2b116faa-943b-47dd-815c-e2d31b528f98/image.png" alt=""> 위 그림과 같이 부모 컴포넌트 state를 자식 컴포넌트로 전송해 줄 수 있다. 그리고 이렇게 전송할 때 props라는 문법을 사용한다. 전송하는 방법은 다음과 같다.</p>
<ol>
<li>자식컴포넌트를 불러오는 곳에서 <strong>&lt;자식컴포넌트 작명={state이름}/&gt;</strong> 작성</li>
<li>자식컴포넌트 만드는 function으로 가서 props라는 파라미터 등록 후 props라는 파라미터 등록 후 <strong>props.</strong> 작명 사용
위와 같은 절차에 따라 코드를 작성한 예시는 다음과 같다.<pre><code class="language-javascript">function App (){
let [title, setTitle] = useState([&#39;남자코트 추천&#39;, &#39;강남 우동맛집&#39;, &#39;파이썬독학&#39;]);
return (
 &lt;div&gt;
   &lt;Modal title={title}&gt;&lt;/Modal&gt;
 &lt;/div&gt;
)
}
</code></pre>
</li>
</ol>
<p>function Modal(props){
  return (
    <div className="modal">
      <h4>{ props.title[0] }</h4>
      <p>날짜</p>
      <p>상세내용</p>
    </div>
  )
}</p>
<pre><code></code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[React 기초문법]]></title>
            <link>https://velog.io/@mini_suyo/React-%EA%B8%B0%EC%B4%88%EB%AC%B8%EB%B2%95</link>
            <guid>https://velog.io/@mini_suyo/React-%EA%B8%B0%EC%B4%88%EB%AC%B8%EB%B2%95</guid>
            <pubDate>Mon, 13 Jan 2025 04:59:33 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mini_suyo/post/94c859f4-f6f8-4324-aea4-53c78a4a2ba3/image.png" alt=""></p>
<h2 id="1-html의-class사용">1. HTML의 class사용</h2>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/a4433ab0-c7d6-41c6-b08a-0e97e6c7d392/image.png" alt="">
다음과 같이 스타일을 주기 위한 class명을 넣을 때 className을 사용해야한다. 왜냐하면 jsx파일에서 class는 선언으로 쓰이기 때문이다.</p>
<h2 id="2-변수를-html에-넣기">2. 변수를 HTML에 넣기</h2>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/f375af48-a294-45b7-b4e9-2540e9faf2dd/image.png" alt="">
다음과 같이 중괄호{}를 통해 변수를 넣어줄 수 있다. href, id, className, src등 여러가지 html 속성들에도 가능하다.
<img src="https://velog.velcdn.com/images/mini_suyo/post/0e464129-0d24-48a9-99c0-c7eb77992b02/image.png" alt="">
실제로 id와 h4태그 안에 post값이 제대로 들어가 있는 모습을 확인 할 수 있다. 이러한 작업을 <strong>데이터바인딩</strong>이라고 한다.</p>
<h2 id="3-html에-style속성-넣기">3. HTML에 style속성 넣기</h2>
<p><img src="https://velog.velcdn.com/images/mini_suyo/post/c640ba73-9612-48ce-a655-df144eb850fa/image.png" alt="">
JSX 상에서는 style속성을 넣을때 <strong>style=&quot;&quot;</strong>가 아니라 <strong>style={}</strong>로 넣어야한다. 또한 font-size처럼 속성명에 -기호를 쓸 수 없다. 때문에 camelCase방식으로 작성해야한다.</p>
]]></description>
        </item>
    </channel>
</rss>