<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ujinsimss_.log</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 공부 중.. 💻👩‍🎤</description>
        <lastBuildDate>Wed, 07 May 2025 08:38:22 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ujinsimss_.log</title>
            <url>https://velog.velcdn.com/images/ujinsimss_/profile/7c9c3976-5cba-46a1-ba7e-7a0e55d7f638/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ujinsimss_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ujinsimss_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[TypeScript] API 명세서를 통해 타입 설계하기 ]]></title>
            <link>https://velog.io/@ujinsimss_/API-%EB%AA%85%EC%84%B8%EC%84%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-%ED%83%80%EC%9E%85-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ujinsimss_/API-%EB%AA%85%EC%84%B8%EC%84%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-%ED%83%80%EC%9E%85-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 May 2025 08:38:22 GMT</pubDate>
            <description><![CDATA[<ol>
<li>API 명세읽기</li>
<li>string으로 표현하기, 타입 이름은 해당 분야로 짓기 </li>
<li>string 지양하기 → 구체적인 타입으로 좁히기</li>
<li>공식 명칭에는 상표 붙이기</li>
</ol>
<h1 id="예제-1-가상의-사용자-정보-api">예제 1: 가상의 사용자 정보 API</h1>
<p><strong>API 엔드포인트</strong>: <code>https://api.example.com/users</code></p>
<p><strong>응답 예시</strong>:</p>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;email&quot;: &quot;john.doe@example.com&quot;,
  &quot;address&quot;: {
    &quot;street&quot;: &quot;123 Main St&quot;,
    &quot;city&quot;: &quot;Anytown&quot;,
    &quot;zipcode&quot;: &quot;12345&quot;
  },
  &quot;phone&quot;: &quot;123-456-7890&quot;,
  &quot;website&quot;: &quot;https://example.com&quot;
}</code></pre>
<h3 id="1-string으로-일단-만들기">1. string으로 일단 만들기</h3>
<pre><code class="language-tsx">// Address 타입 정의
interface Address {
  street: string;
  city: string;
  zipcode: string;
}

// User 타입 정의
interface User {
  id: number;
  name: string;
  email: string;
  address: Address;
  phone: string;
  website: string;
}</code></pre>
<p><strong>API 호출 함수</strong></p>
<p><code>fetch</code>를 사용하여 데이터를 호출하고 타입을 적용합니다.</p>
<pre><code class="language-tsx">async function fetchUsers(): Promise&lt;User[]&gt; {
  const response = await fetch(&quot;https://api.example.com/users&quot;);
  if (!response.ok) {
    throw new Error(&quot;Failed to fetch users&quot;);
  }
  return await response.json();
}</code></pre>
<h3 id="2-string을-지양하기">2. string을 지양하기</h3>
<p>: 이메일과 웹사이트URL 전화번호 타입 구체화 하기</p>
<pre><code class="language-tsx">// 이메일 타입 정의
type EmailAddress = `${string}@${string}.${string}`;

// 웹사이트 URL 타입 정의
type WebsiteURL = `http${&quot;s&quot; | &quot;&quot;}://${string}.${string}`;

// 전화번호 타입 정의
type PhoneNumber = `${number}-${number}-${number}`;

// Address 타입 정의
interface Address {
  street: string;
  city: string;
  zipcode: `${number}`; // 우편번호를 숫자로 제한
}

// User 타입 정의
interface User {
  id: number;
  name: string;
  email: EmailAddress; // 엄격한 이메일 형식
  address: Address;
  phone: PhoneNumber; // 간단한 전화번호 형식
  website: WebsiteURL; // 웹사이트 URL 형식
}
</code></pre>
<p><strong>올바른 값</strong></p>
<pre><code class="language-tsx">const user: User = {
  id: 1,
  name: &quot;John Doe&quot;,
  email: &quot;john.doe@example.com&quot;,
  address: {
    street: &quot;123 Main St&quot;,
    city: &quot;Anytown&quot;,
    zipcode: &quot;12345&quot;
  },
  phone: &quot;123-456-7890&quot;,
  website: &quot;https://example.com&quot;
};</code></pre>
<p><strong>잘못된 값 (컴파일 오류 발생)</strong></p>
<pre><code class="language-tsx">const invalidUser: User = {
  id: 2,
  name: &quot;Jane Doe&quot;,
  email: &quot;jane.doe[at]example.com&quot;, // 오류: 이메일 형식이 아님
  address: {
    street: &quot;456 Elm St&quot;,
    city: &quot;Othertown&quot;,
    zipcode: &quot;abcde&quot; // 오류: 숫자 형식이 아님
  },
  phone: &quot;1234567890&quot;, // 오류: 전화번호 형식이 아님
  website: &quot;example.com&quot; // 오류: URL 형식이 아님
};</code></pre>
<h3 id="4-브랜딩하기">4. 브랜딩하기</h3>
<p>: 있으면 하자 !</p>
<h2 id="그렇다면-any와-unknown은-명세서와-타입-설계시-언제-사용하나요-">그렇다면 any와 unknown은 명세서와 타입 설계시 언제 사용하나요 ?</h2>
<p><strong>any, unknown 특징</strong> </p>
<table>
<thead>
<tr>
<th><strong>특징</strong></th>
<th><strong><code>unknown</code></strong></th>
<th><strong><code>any</code></strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>안전성</strong></td>
<td>타입을 사용하기 전에 <strong>검사 필수</strong></td>
<td>모든 작업 허용 (타입 검사 없음)</td>
</tr>
<tr>
<td><strong>타입 검사</strong></td>
<td>엄격 (타입을 좁혀야만 사용 가능)</td>
<td>느슨함 (타입 체크가 무시됨)</td>
</tr>
<tr>
<td><strong>IntelliSense</strong></td>
<td>타입 좁히기 후 사용 가능</td>
<td>IntelliSense 거의 없음</td>
</tr>
<tr>
<td><strong>사용 시기</strong></td>
<td>외부 데이터, 알 수 없는 타입</td>
<td>빠른 프로토타이핑 또는 유연성이 필요할 때</td>
</tr>
</tbody></table>
<h3 id="언제-unknown을-사용할까"><strong>언제 <code>unknown</code>을 사용할까?</strong></h3>
<p>: <code>unknown</code>은 타입 안전성을 유지해야 하지만 데이터의 구체적인 타입을 사전에 알 수 없는 경우에 적합합니다.</p>
<ol>
<li><strong>외부에서 오는 데이터 (</strong>API 응답 구조가 문서화되지 않았거나 동적으로 변할 가능성이 있을 때.)</li>
</ol>
<pre><code class="language-tsx">async function fetchData(url: string): Promise&lt;unknown&gt; {
  const response = await fetch(url);
  return response.json();
}

async function processData() {
  const data: **unknown**= await fetchData(&quot;https://api.example.com/data&quot;);

  // 타입 좁히기 필요
  if (Array.isArray(data)) {
    console.log(`Array of length: ${data.length}`);
  } else if (typeof data === &quot;object&quot; &amp;&amp; data !== null) {
    console.log(`Object with keys: ${Object.keys(data)}`);
  } else {
    console.log(&quot;Unknown data type&quot;);
  }
}</code></pre>
<ul>
<li><strong>장점</strong>: 타입을 검사하기 전까지는 데이터를 조작할 수 없으므로, 실수를 방지할 수 있습니다.</li>
<li><strong>단점</strong>: 추가적인 타입 검사 코드가 필요합니다.</li>
</ul>
<ol start="2">
<li><strong>안전하게 다뤄야 할 데이터가 있을 때</strong></li>
</ol>
<p><code>unknown</code>은 강제로 타입을 좁혀야 하기 때문에 더 안전하게 데이터를 사용할 수 있습니다.</p>
<pre><code class="language-tsx">function processValue(value: unknown) {
  if (typeof value === &quot;string&quot;) {
    console.log(value.toUpperCase()); // 안전
  } else if (typeof value === &quot;number&quot;) {
    console.log(value.toFixed(2)); // 안전
  } else {
    console.log(&quot;Unsupported type&quot;);
  }
}</code></pre>
<hr>
<h3 id="언제-any를-사용할까"><strong>언제 <code>any</code>를 사용할까?</strong></h3>
<p>: <code>any</code>는 타입 안전성을 무시하고 빠르게 코드를 작성해야 할 때 사용합니다. 하지만 남용을 피해야 하며, 가능한 빨리 정확한 타입으로 대체하는 것이 좋습니다.</p>
<ol>
<li><strong>빠른 프로토타이핑</strong></li>
</ol>
<p>개발 초기 단계에서 타입 정의가 아직 명확하지 않거나, 많은 데이터를 테스트해야 할 때 유용합니다.</p>
<pre><code class="language-tsx">function logData(data: any) {
  console.log(data);
}

logData({ id: 1, name: &quot;John Doe&quot; });
logData(&quot;This is a string&quot;);
logData(42);</code></pre>
<ul>
<li><strong>장점</strong>: 유연성.</li>
<li><strong>단점</strong>: 모든 작업이 허용되므로, 타입 오류를 놓칠 가능성이 큽니다.</li>
</ul>
<ol start="2">
<li><strong>타입 검사가 필요 없는 경우</strong></li>
</ol>
<p>특정 로직이 타입에 구애받지 않고 모든 데이터 타입을 다뤄야 할 때 사용합니다.</p>
<pre><code class="language-tsx">function mergeObjects(obj1: any, obj2: any): any {
  return { ...obj1, ...obj2 };
}</code></pre>
<ul>
<li>예: 데이터 직렬화/역직렬화, <code>JSON.parse()</code>, 로깅.</li>
</ul>
<hr>
<h3 id="unknown-vs-any-사용-사례-비교"><code>unknown</code> vs <code>any</code> 사용 사례 비교</h3>
<pre><code class="language-tsx">const jsonData: **any** = JSON.parse(&#39;{&quot;id&quot;: 1, &quot;name&quot;: &quot;John&quot;}&#39;);
console.log(jsonData.toUpperCase()); // 런타임 오류 발생 가능 (TypeScript가 경고하지 않음)

const unknownData: **unknown** = JSON.parse(&#39;{&quot;id&quot;: 1, &quot;name&quot;: &quot;John&quot;}&#39;);
if (typeof unknownData === &quot;string&quot;) {
  console.log(unknownData.toUpperCase()); // 비교적 안전
} else {
  console.log(&quot;Data is not a string&quot;);
}</code></pre>
<ul>
<li><code>any</code>: 런타임 오류 가능성이 있음.</li>
<li><code>unknown</code>: 타입 검사 후 사용 가능, 타입 안전성 증가.</li>
</ul>
<h1 id="예제-2-상표가-있는-api">예제 2: 상표가 있는 API</h1>
<pre><code class="language-tsx">{
  &quot;id&quot;: &quot;12345&quot;,
  &quot;name&quot;: &quot;Apple MacBook Pro&quot;,
  &quot;category&quot;: &quot;laptop&quot;,
  &quot;price&quot;: 2499.99,
  &quot;currency&quot;: &quot;USD&quot;,
  &quot;availability&quot;: {
    &quot;inStock&quot;: true,
    &quot;estimatedDeliveryDays&quot;: 5
  },
  &quot;rating&quot;: {
    &quot;average&quot;: 4.8,
    &quot;reviews&quot;: 1245
  }
}</code></pre>
<h3 id="1-api-명세-읽고-타입-설계-string-중심"><strong>1. API 명세 읽고 타입 설계 (string 중심)</strong></h3>
<p>우선, API 데이터 구조를 단순히 <code>string</code> 및 기타 기본 타입으로 표현합니다.</p>
<pre><code class="language-tsx">interface Product {
  id: string; // 제품 ID
  name: string; // 제품 이름
  category: string; // 제품 카테고리
  price: number; // 제품 가격
  currency: string; // 통화 단위
  availability: {
    inStock: boolean; // 재고 여부
    estimatedDeliveryDays: number; // 예상 배송일
  };
  rating: {
    average: number; // 평균 평점
    reviews: number; // 리뷰 수
  };
}</code></pre>
<h3 id="2-string으로-표현된-타입-이름을-해당-분야로-지정"><strong>2. <code>string</code>으로 표현된 타입 이름을 해당 분야로 지정</strong></h3>
<p>타입 이름을 구체화하여 해당 데이터와 연관된 도메인 용어를 반영합니다.</p>
<pre><code class="language-tsx">type ProductID = string;
type ProductName = string;
type ProductCategory = string;
type Currency = string;

interface Product {
  id: ProductID;
  name: ProductName;
  category: ProductCategory;
  price: number;
  currency: Currency;
  availability: {
    inStock: boolean;
    estimatedDeliveryDays: number;
  };
  rating: {
    average: number;
    reviews: number;
  };
}</code></pre>
<p>이 단계에서는 단순히 <strong>의미를 부여한 별칭</strong>을 도입했습니다. 타입 자체는 여전히 <code>string</code> 기반입니다.</p>
<h3 id="3-string-지양하고-구체적인-타입으로-좁히기"><strong>3. <code>string</code> 지양하고 구체적인 타입으로 좁히기</strong></h3>
<p>이제 더 구체적인 타입으로 좁힙니다. 예를 들어:</p>
<ul>
<li><em><code>Currency</code></em>는 ISO 4217 표준 코드로 한정합니다.</li>
<li><em><code>ProductCategory</code></em>는 정해진 카테고리 값으로 제한합니다.</li>
</ul>
<pre><code class="language-tsx">type ProductID = `${number}`; // ID는 숫자 기반 문자열
type ProductName = string;

type ProductCategory = &quot;laptop&quot; | &quot;smartphone&quot; | &quot;tablet&quot; | &quot;accessory&quot;; // 카테고리 제한

type Currency = &quot;USD&quot; | &quot;EUR&quot; | &quot;JPY&quot;; // ISO 4217 통화 코드

interface Product {
  id: ProductID;
  name: ProductName;
  category: ProductCategory;
  price: number;
  currency: Currency;
  availability: {
    inStock: boolean;
    estimatedDeliveryDays: number;
  };
  rating: {
    average: number;
    reviews: number;
  };
}</code></pre>
<p>이 단계에서는 <strong><code>string</code></strong> 대신 <strong>구체적인 값</strong>으로 제한하여 데이터 구조의 타입 안전성을 향상시켰습니다.</p>
<h3 id="4-공식-명칭에-상표branding-붙이기"><strong>4. 공식 명칭에 상표(branding) 붙이기</strong></h3>
<p>마지막으로, <code>상표</code>를 추가하여 타입의 고유성을 강화합니다. 상표를 추가하면 다른 동일 구조의 타입과 구별할 수 있습니다.</p>
<pre><code class="language-tsx">type ProductID = `${number}` &amp; { _brand: &quot;ProductID&quot; };
type ProductName = string &amp; { _brand: &quot;ProductName&quot; };

type ProductCategory = &quot;laptop&quot; | &quot;smartphone&quot; | &quot;tablet&quot; | &quot;accessory&quot; &amp; { _brand: &quot;ProductCategory&quot; };

type Currency = &quot;USD&quot; | &quot;EUR&quot; | &quot;JPY&quot; &amp; { _brand: &quot;Currency&quot; };

interface Product {
  id: ProductID;
  name: ProductName;
  category: ProductCategory;
  price: number;
  currency: Currency;
  availability: {
    inStock: boolean;
    estimatedDeliveryDays: number;
  };
  rating: {
    average: number;
    reviews: number;
  };
}</code></pre>
<ul>
<li>이제 <code>ProductID</code>나 <code>Currency</code>는 동일한 구조의 다른 <code>string</code>과 구별됩니다.</li>
<li>타입 간 혼동을 방지하고, 잘못된 데이터 사용 가능성을 줄입니다.</li>
</ul>
<pre><code class="language-tsx">const macbook: Product = {
  id: &quot;12345&quot; as ProductID,
  name: &quot;Apple MacBook Pro&quot; as ProductName,
  category: &quot;laptop&quot; as ProductCategory,
  price: 2499.99,
  currency: &quot;USD&quot; as Currency,
  availability: {
    inStock: true,
    estimatedDeliveryDays: 5,
  },
  rating: {
    average: 4.8,
    reviews: 1245,
  },
};</code></pre>
<h3 id="퀴즈-️">퀴즈 ‼️</h3>
<p>아래의 <code>Product</code> 타입은 특정 API 명세를 기반으로 설계되었습니다. 다음 중 <strong>올바르지 않은</strong> 부분은 무엇일까요 </p>
<pre><code class="language-tsx">type ProductID = `${number}` &amp; { _brand: &quot;ProductID&quot; };
type ProductCategory = &quot;laptop&quot; | &quot;smartphone&quot; | &quot;tablet&quot; | &quot;accessory&quot; &amp; { _brand: &quot;ProductCategory&quot; };
type Currency = &quot;USD&quot; | &quot;EUR&quot; | &quot;JPY&quot; &amp; { _brand: &quot;Currency&quot; };

interface Product {
  id: ProductID;
  name: string;
  category: ProductCategory;
  price: number;
  currency: Currency;
  availability: {
    inStock: boolean;
    estimatedDeliveryDays: number;
  };
}</code></pre>
<pre><code class="language-tsx">const productB: Product = {
  id: &quot;102&quot; as ProductID,
  name: &quot;Apple iPad&quot;,
  category: &quot;tablet&quot; as ProductCategory,
  price: 599.99,
  currency: &quot;GBP&quot; as Currency,
  availability: {
    inStock: false,
    estimatedDeliveryDays: 7,
  },
};</code></pre>
<ul>
<li><p>답</p>
<p>  <code>currency</code>가 GBP로 설정되어 있습니다. <code>Currency</code> 타입은 <code>&quot;USD&quot;</code>, <code>&quot;EUR&quot;</code>, <code>&quot;JPY&quot;</code>만 허용합니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 얕은 복사와 깊은 복사, React에서 이 개념을 알아야되는 이유 ]]></title>
            <link>https://velog.io/@ujinsimss_/%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC-React%EC%97%90%EC%84%9C-%EC%9D%B4-%EA%B0%9C%EB%85%90%EC%9D%84-%EC%95%8C%EC%95%84%EC%95%BC%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@ujinsimss_/%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC-React%EC%97%90%EC%84%9C-%EC%9D%B4-%EA%B0%9C%EB%85%90%EC%9D%84-%EC%95%8C%EC%95%84%EC%95%BC%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 06 May 2025 19:08:55 GMT</pubDate>
            <description><![CDATA[<p>React에서 상태 관리와 데이터 변경을 다룰 때, <strong>깊은 복사 (Deep Copy)</strong>와 <strong>얕은 복사 (Shallow Copy)</strong>의 개념은 매우 중요합니다. 이러한 복사 방식은 주로 <strong>불변성 (Immutability)</strong>을 지키는 데 사용되며, React의 성능과 렌더링 효율성을 높이는 데 큰 역할을 합니다.</p>
<h3 id="1-원시-타입과-참조형-타입의-차이점">1. <strong>원시 타입과 참조형 타입의 차이점</strong></h3>
<ul>
<li><p><strong>원시 타입</strong> (Primitive Types): JavaScript의 숫자, 문자열, 불리언, <code>null</code>, <code>undefined</code>와 같은 타입을 포함합니다. 원시 타입은 <strong>값 자체</strong>를 저장하고, 변수에 값을 할당해도 다른 변수에는 영향을 주지 않습니다.</p>
<ul>
<li><p>예시:</p>
<pre><code class="language-jsx">  let a = 10;
  let b = a;
  b = 20;
  console.log(a); // 10
  console.log(b); // 20</code></pre>
<ul>
<li><code>a</code>와 <code>b</code>는 독립적인 값으로, 하나를 변경해도 다른 변수에는 영향을 미치지 않습니다.</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>참조형 타입</strong> (Reference Types): 객체, 배열, 함수 등으로, 변수에는 <strong>참조(메모리 주소)</strong>가 저장됩니다. 참조형 타입은 <strong>주소</strong>를 저장하고 있기 때문에, 하나의 객체를 여러 변수에 할당하면, 하나를 변경할 경우 나머지 변수들도 영향을 받습니다.</p>
<ul>
<li><p>예시:</p>
<pre><code class="language-jsx">  let obj1 = { name: &#39;Alice&#39; };
  let obj2 = obj1;
  obj2.name = &#39;Bob&#39;;
  console.log(obj1.name); // &#39;Bob&#39;
  console.log(obj2.name); // &#39;Bo</code></pre>
<ul>
<li><code>obj1</code>과 <code>obj2</code>는 같은 객체를 참조하고 있기 때문에, 하나를 수정하면 다른 것도 변경됩니다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="2-얕은-복사-shallow-copy">2. <strong>얕은 복사 (Shallow Copy)</strong></h3>
<p>얕은 복사는 객체의 <strong>1단계</strong>만 복사하고, <strong>중첩된 객체</strong>는 여전히 원본 객체와 동일한 <strong>참조</strong>를 공유합니다. 이 말은 중첩된 객체나 배열에 변경이 일어나면 원본 객체에 영향을 미친다는 뜻입니다.</p>
<ul>
<li><p><strong>예시</strong>:</p>
<pre><code class="language-jsx">  const clubInfo = {
    name: &#39;COW Club&#39;,
    members: [
      { name: &#39;John&#39;, fee: 100, part: &#39;Server&#39;, isAttending: true },
      { name: &#39;Sara&#39;, fee: 150, part: &#39;Web&#39;, isAttending: false }
    ]
  };

  const newClubInfo = { ...clubInfo }; // 얕은 복사

  // 중첩된 객체는 여전히 참조를 공유
  newClubInfo.members[0].fee = 200;

  console.log(clubInfo.members[0].fee); // 200
  console.log(newClubInfo.members[0].fee); // 200</code></pre>
<ul>
<li>위 코드에서 <code>newClubInfo</code>는 <code>clubInfo</code>의 얕은 복사본을 만들었지만, <code>members</code> 배열 안의 객체는 여전히 같은 메모리 주소를 참조하고 있습니다. 따라서 <code>newClubInfo.members[0]</code>을 수정하면 <code>clubInfo.members[0]</code>에도 영향을 미칩니다.</li>
</ul>
</li>
</ul>
<h3 id="3-깊은-복사-deep-copy">3. <strong>깊은 복사 (Deep Copy)</strong></h3>
<p>깊은 복사는 객체의 <strong>모든 중첩된 속성</strong>까지 독립적으로 복사하는 방식입니다. 깊은 복사를 사용하면 원본 객체와 복사본 객체가 서로 영향을 미치지 않습니다.</p>
<ul>
<li><p><strong>예시</strong>:</p>
<pre><code class="language-jsx">  const clubInfo = {
    name: &#39;COW Club&#39;,
    members: [
      { name: &#39;John&#39;, fee: 100, part: &#39;Server&#39;, isAttending: true },
      { name: &#39;Sara&#39;, fee: 150, part: &#39;Web&#39;, isAttending: false }
    ]
  };

  const deepCopyClubInfo = JSON.parse(JSON.stringify(clubInfo)); // 깊은 복사

  // 깊은 복사본에서 변경
  deepCopyClubInfo.members[0].fee = 200;

  console.log(clubInfo.members[0].fee); // 100
  console.log(deepCopyClubInfo.members[0].fee); // 200</code></pre>
<ul>
<li><code>JSON.parse(JSON.stringify())</code> 방법은 객체의 깊은 복사를 수행합니다. 이 방식은 객체 내부의 <strong>모든 속성</strong>을 복사하고, 원본 객체와 복사본이 완전히 독립적인 객체로 존재합니다.</li>
</ul>
</li>
</ul>
<h3 id="4-얕은-복사-vs-깊은-복사-어떤-경우에-사용해야-할까">4. <strong>얕은 복사 vs 깊은 복사: 어떤 경우에 사용해야 할까?</strong></h3>
<ul>
<li><strong>얕은 복사</strong>는 <strong>배열이나 객체의 1단계 속성</strong>만 복사할 때 유용합니다. 중첩된 데이터가 없거나, 중첩된 데이터를 수정할 필요가 없는 경우 적합합니다.</li>
<li><strong>깊은 복사</strong>는 객체나 배열이 <strong>중첩된 구조</strong>를 가지고 있고, 중첩된 데이터까지 독립적으로 다뤄야 할 때 사용합니다.</li>
</ul>
<h3 id="5-react에서-불변성immutability과-상태-관리">5. <strong>React에서 불변성(Immutability)과 상태 관리</strong></h3>
<p>React에서는 상태를 직접 수정하지 않고, 새로운 객체를 생성하여 상태를 업데이트해야 합니다. 이 과정에서 <strong>불변성(Immutability)</strong>을 지키는 것이 매우 중요합니다. 불변성을 지키면 React가 상태 변화(예: <code>setState</code>)를 쉽게 추적하고, <strong>효율적인 리렌더링</strong>을 할 수 있습니다.</p>
<ul>
<li><p><strong>불변성을 유지하는 이유</strong>:</p>
<ul>
<li>React는 <strong>참조 비교</strong>를 통해 상태 변경을 감지합니다. 상태를 변경할 때마다 새로운 객체를 생성하면, React는 이전 상태와 새로운 상태를 비교하여 리렌더링을 최적화할 수 있습니다.</li>
<li>불변성을 유지하면 코드가 더 예측 가능하고, 상태 관리가 쉬워집니다.</li>
</ul>
</li>
<li><p><strong>불변성 예시</strong>:</p>
<pre><code class="language-jsx">  const [members, setMembers] = useState([
    { name: &#39;John&#39;, fee: 100, part: &#39;Server&#39;, isAttending: true },
    { name: &#39;Sara&#39;, fee: 150, part: &#39;Web&#39;, isAttending: false }
  ]);

  // 불변성을 유지하면서 상태 업데이트
  const updateFee = (index, newFee) =&gt; {
    const updatedMembers = [...members]; // 얕은 복사
    updatedMembers[index] = { ...updatedMembers[index], fee: newFee }; // 중첩 객체 복사
    setMembers(updatedMembers); // 새로운 배열로 상태 업데이트
  };</code></pre>
</li>
</ul>
<h3 id="퀴즈"><strong>퀴즈</strong></h3>
<ol>
<li><strong>얕은 복사와 깊은 복사에 대한 설명이 올바른 것은?</strong><ol>
<li>얕은 복사는 중첩된 객체까지 독립적으로 복사하고, 깊은 복사는 객체의 1단계만 복사한다.</li>
<li>얕은 복사는 객체의 1단계만 복사하고, 깊은 복사는 중첩된 객체까지 독립적으로 복사한다.</li>
<li>얕은 복사는 객체와 배열을 모두 복사하고, 깊은 복사는 객체만 복사한다.</li>
<li>얕은 복사는 값 타입만 복사하고, 깊은 복사는 참조 타입만 복사한다.</li>
</ol>
</li>
<li><strong>다음 중 깊은 복사를 수행하는 방법으로 올바른 것은?</strong><ol>
<li><code>Object.assign()</code></li>
<li><code>JSON.parse(JSON.stringify())</code></li>
<li><code>...</code> (스프레드 연산자)</li>
<li><code>slice()</code></li>
</ol>
</li>
<li><strong>불변성을 지키지 않았을 때 발생할 수 있는 문제는 무엇인가요?</strong><ol>
<li>React의 리렌더링 최적화가 이루어지지 않거나 예기치 않은 동작이 발생할 수 있다.</li>
<li>상태 업데이트가 불가능해진다.</li>
<li>코드가 더 간결해진다.</li>
<li>상태를 직접 변경할 수 없게 된다.</li>
</ol>
</li>
<li><strong>React에서 상태를 업데이트할 때 얕은 복사를 사용하는 이유는 무엇인가요?</strong><ol>
<li>값 타입만 복사하고, 객체 타입을 무시하기 위해서</li>
<li>중첩된 객체가 변경되지 않기 때문에</li>
<li>상태를 변경할 때 새로운 객체를 생성하여 불변성을 지키기 위해서</li>
<li>React에서 상태를 직접 변경할 수 없기 때문에</li>
</ol>
</li>
<li><strong>스프레드 연산자를 사용한 얕은 복사의 예시로 올바른 코드는?</strong><ol>
<li><code>const newObj = { ...obj };</code></li>
<li><code>const newObj = obj;</code></li>
<li><code>const newObj = Object.assign({}, obj);</code></li>
<li><code>const newObj = JSON.parse(JSON.stringify(obj));</code></li>
</ol>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TODO MVC] 2주차 : 상태관리 및 컴포넌트 개발 (useContext, useReducer )]]></title>
            <link>https://velog.io/@ujinsimss_/TODO-MVC-2%EC%A3%BC%EC%B0%A8-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-%EB%B0%8F-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B0%9C%EB%B0%9C-useContext-useReducer</link>
            <guid>https://velog.io/@ujinsimss_/TODO-MVC-2%EC%A3%BC%EC%B0%A8-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-%EB%B0%8F-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B0%9C%EB%B0%9C-useContext-useReducer</guid>
            <pubDate>Tue, 06 May 2025 07:34:30 GMT</pubDate>
            <description><![CDATA[<p>*<em>일차적으로는 스토리지 기능 없이 상태관리로만 집중하는 개발방식으로 목적을 잡고 개발을 시작했습니다 *</em></p>
<h3 id="컴포넌트에는">컴포넌트에는</h3>
<p>ToDo 내용을 삭제하고 관리하는 부분은 같이 하고 있어요 그래서 그 부분을
useContext와 데이터를 수정하는 부분은 useReducer을 통해서 구현해봤습니다. </p>
<p>TodoContext.tsx</p>
<pre><code class="language-tsx">import { createContext, useReducer } from &quot;react&quot;;

const initialState = {
  todos: [
    { title: &quot;첫번째 투두&quot;, id: 1, isCompleted: false, isEditing: false },
  ],
};

const todoReducer = (state: typeof initialState, action: any) =&gt; {
  switch (action.type) {
    case &quot;ADD_TODO&quot;:
      return { todos: [...state.todos, action.payload] };
    case &quot;REMOVE_TODO&quot;:
      return {
        todos: state.todos.filter((todo) =&gt; todo.id !== action.payload.id),
      };
    default:
      return state;
  }
};

export const TodoStateContext = createContext&lt;typeof initialState | null&gt;(null);
export const TodoDispatchContext = createContext&lt;any&gt;(null);

export const TodoProvider = ({ children }: { children: React.ReactNode }) =&gt; {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  return (
    &lt;TodoStateContext.Provider value={state}&gt;
      &lt;TodoDispatchContext.Provider value={dispatch}&gt;
        {children}
      &lt;/TodoDispatchContext.Provider&gt;
    &lt;/TodoStateContext.Provider&gt;
  );
};
</code></pre>
<h3 id="여기서-생긴-문제는">여기서 생긴 문제는</h3>
<p>한국어만 생성시 마지막 글자가 하나 더 추가된다는 것이였습니다 
밥먹기, 기 처럼.. . .
<img src="https://velog.velcdn.com/images/ujinsimss_/post/db06cbc4-90b7-447e-8ce3-1bdd44f64439/image.png" alt=""></p>
<p>keydown에서 keyup으로 변경해주었씁니다 !</p>
<pre><code class="language-tsx">import { useEffect } from &quot;react&quot;;

function useEnterKey(callback: () =&gt; void) {
  useEffect(() =&gt; {
    const handleKeyUp = (event: KeyboardEvent) =&gt; {
      if (event.key === &quot;Enter&quot;) {
        event.preventDefault();
        callback();
      }
    };

    window.addEventListener(&quot;keyup&quot;, handleKeyUp);

    return () =&gt; {
      window.removeEventListener(&quot;keyup&quot;, handleKeyUp);
    };
  }, [callback]);

  return;
}

export default useEnterKey;

</code></pre>
<p>해당 방법으로 적용했더니 해결되었습니다 
keyup 이벤트를 사용하여 해결되었습니다. keydown이 아닌 <strong>keyup</strong>을 사용하면 사용자가 키를 떼었을 때 동작하므로 중복 입력이 발생하지 않게 되므로 해당 방법으로 해결할 수 있었습니다 </p>
<h3 id="다른-컴포넌트-마저-완성하고-구현을-완료하였습니다">다른 컴포넌트 마저 완성하고 구현을 완료하였습니다.</h3>
<p>todoItem에 있는 delete, toggle
addTodo에 있는 all toggle
filter에 있는 Clear complete 부분은 useReducer과 연결하여 같이 데이터를 다룰 수 있도록 처리하였습니다. </p>
<p>filter에 있는 아이템 갯수와 mode는 state로 처리하였습니다</p>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/3e39def8-a0e0-42ea-9443-7b3d3f84289c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/6b216d3c-32b5-4824-a948-747f8a78118c/image.png" alt="">
지난번에 이런식으로 컴포넌트를 분리하기로 하였씁니다</p>
<p>ui에서는 크게 컴포넌트를 </p>
<ol>
<li>AddTodo.tsx</li>
<li>TodoFilter.tsx,</li>
<li>TodoItem.tsx 3개로 나누었습니다. (1편 참고) </li>
</ol>
<p>현재는 todoList를 context로 (todoContext)
관리하고 todoList의 데이터 수정은 Reducer로 (todoReducer)
시각적인 todoList의 필터는 state로 관리하고있습니다 ( mode )</p>
<p>그래서 전역관리는 </p>
<h2 id="todocontexttsx">todoContext.tsx</h2>
<pre><code class="language-tsx">import { createContext, useReducer } from &quot;react&quot;;
import { todoList } from &quot;../types/todo&quot;;

const initialState: todoList = {
  todos: [],
};

type Action =
  | { type: &quot;ADD_TODO&quot;; payload: { title: string } }
  | { type: &quot;REMOVE_TODO&quot;; payload: { id: number } }
  | { type: &quot;TOGGLE_TODO&quot;; payload: { id: number } }
  | { type: &quot;CLEAR_COMPLETED_TODOS&quot;; payload: null }
  | { type: &quot;ALL_TOGGLE_TODO&quot; };

const todoReducer = (state: todoList, action: Action): todoList =&gt; {
  switch (action.type) {
    case &quot;ADD_TODO&quot;:
      return {
        todos: [
          ...state.todos,
          {
            ...action.payload,
            id: Date.now(),
            isCompleted: false,
            isEditing: false,
          },
        ],
      };
    case &quot;REMOVE_TODO&quot;:
      return {
        todos: state.todos.filter((todo) =&gt; todo.id !== action.payload.id),
      };
    case &quot;TOGGLE_TODO&quot;:
      return {
        todos: state.todos.map((todo) =&gt;
          todo.id === action.payload.id
            ? { ...todo, isCompleted: !todo.isCompleted }
            : todo
        ),
      };
    case &quot;CLEAR_COMPLETED_TODOS&quot;:
      return { todos: state.todos.filter((todo) =&gt; !todo.isCompleted) };
    case &quot;ALL_TOGGLE_TODO&quot;:
      return {
        todos: state.todos.map((todo) =&gt;
          todo.isCompleted
            ? { ...todo, isCompleted: false }
            : { ...todo, isCompleted: true }
        ),
      };
    default:
      return state;
  }
};

export const TodoStateContext = createContext&lt;todoList | null&gt;(null);
export const TodoDispatchContext = createContext&lt;React.Dispatch&lt;Action&gt; | null&gt;(
  null
);

export const TodoProvider = ({ children }: { children: React.ReactNode }) =&gt; {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  return (
    &lt;TodoStateContext.Provider value={state}&gt;
      &lt;TodoDispatchContext.Provider value={dispatch}&gt;
        {children}
      &lt;/TodoDispatchContext.Provider&gt;
    &lt;/TodoStateContext.Provider&gt;
  );
};
</code></pre>
<p>다음과 같이 작성하였습니다. 
Reducer과 Context를 같이 관리하고 있습니다. 
이는 역할이 다르기에 다음주차때 분리할 것을 보입니다.</p>
<p>현재 제가 짠 코드에 대해서 설명을 덧붙히자면
전체 컴포넌트에서 todoList를 다루고있지는 앉지만 전체 컴포넌트에서 데이터 수정을 진행하고 있다는 점에서 todoReducer로 데이터 관리를 전역적으로 관리하는 것으로 구현하였습니다
그리고 mode같은 경우에는 현재 state로 받고있습니다 </p>
<p>App.tsx </p>
<pre><code class="language-tsx">import { useState } from &quot;react&quot;;
import { useContext } from &quot;react&quot;;
import { TodoStateContext, TodoDispatchContext } from &quot;./context/TodoContext&quot;;
import TodoItem from &quot;./components/TodoItem&quot;;
import AddTodo from &quot;./components/AddTodo&quot;;
import TodoFilter from &quot;./components/TodoFilter&quot;;
import { todo, todoFilter } from &quot;./types/todo&quot;;

function App() {
  const { todos } = useContext(TodoStateContext) ?? { todos: [] };
  const dispatch = useContext(TodoDispatchContext);
  const [mode, setMode] = useState&lt;todoFilter&gt;(&quot;All&quot;);

  return (
    &lt;div className=&quot;flex flex-col items-center justify-center h-screen bg-gray-100&quot;&gt;
      &lt;h1 className=&quot;text-4xl py-2&quot;&gt;Todo List&lt;/h1&gt;
      &lt;div&gt;
        &lt;div className=&quot;overflow-y-scroll max-h-96 bg-gray-200 rounded-lg rounded-b-none shadow-lg&quot;&gt;
          &lt;AddTodo dispatch={dispatch} /&gt;
          {todos?.map((todo: todo) =&gt; (
            &lt;TodoItem
              key={todo.id}
              content={todo.title}
              isChecked={todo.isCompleted}
              id={todo.id}
              mode={mode}
            /&gt;
          ))}
        &lt;/div&gt;
        &lt;TodoFilter setMode={setMode} mode={mode} todoLength={todos.length} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<h3 id="mode를-state로-구현한-이유">mode를 state로 구현한 이유?</h3>
<p>todoFilter 에서 선택한 mode를 todoItem에 단순히 뿌려주는 구성이지만
반면 데이터 수정같은 경우에는 서로 상호작용을 하기 때문에 mode는 state로 구현하였습니다</p>
<h2 id="내가-생각하는-앞으로-개선해야될-점">내가 생각하는 앞으로 개선해야될 점</h2>
<ol>
<li><p>현재 상태 관리 방법인 useContext와 useReducer를 사용하고 있지만, 이 방식이 정말 최적인지 고민할 필요가 있습니다. 특히 애플리케이션이 커지거나 복잡해질 때 이 방식이 확장성이나 성능에 문제가 생길 수 있기 때문입니다. 그래서 다른 상태 관리 라이브러리를 써보며 비교하고 성능을 측정하고 싶습니다.</p>
</li>
<li><p>컴포넌트나 함수가 하나의 책임만 가지도록 해야 유지보수나 확장이 용이합니다. 예를 들어, TodoItem 컴포넌트는 단지 아이템을 표시하고 수정하는 역할만 해야 합니다. 상태 관리 로직과 UI 로직을 분리하여, 코드의 책임을 명확히 할 수 있습니다.
현재는 아이콘과 삭제하는 역할도 같이 하고 있기에 이 부분을 개선하는 방안을 고려해보고자 합니다. </p>
</li>
<li><p>컴포넌트가 너무 많으면 관리가 어려워질 수 있고, 너무 적으면 코드가 복잡해집니다. 상태 관리와 UI를 적절히 분리하고, 재사용 가능한 컴포넌트를 만들어야 합니다. 2번 과정을 거치다가 너무 많은 컴포넌트를 남발할 수 있기에 이를 주의하며 최적의 코드를 짜고자합니다 </p>
</li>
</ol>
<p>3주차때 뵙겠습니다 ~~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Todo MVC] 1주차 : 프로젝트 setting⚙️을 해보자 !!]]></title>
            <link>https://velog.io/@ujinsimss_/Todo-MVC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-setting%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@ujinsimss_/Todo-MVC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-setting%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 04 May 2025 14:52:42 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-목적-및-목표">프로젝트 목적 및 목표</h2>
<p>동아리에서 <strong>&quot;우아한 리액트 타입스크립트&quot;</strong> 책을 읽으며  TodoMVC 프로젝트를 진행하기로하였습니다 ! </p>
<p>프로젝트를 시작하기 전, <strong>이 프로젝트를 통해 어떤 걸 얻고 싶은지</strong> 먼저 정리해봤습니다.<br>크게 네 가지 목표 정했는데</p>
<ol>
<li><strong>리액트 훅을 효율적으로 활용하기</strong><ul>
<li>이전에는 훅을 단순히 &quot;이럴 때 쓰는 거구나&quot; 정도로만 이해하고 무조건 사용했던 경향이 있었습니다.</li>
<li>이번에는 <strong>&quot;왜 이 훅을 써야 하는지&quot;</strong>, <strong>&quot;지금 이 구조에서 최선인지&quot;</strong> 등을 고민하며 제대로 활용해보고 싶습니다.</li>
</ul>
</li>
<li><strong>작고 명확한 컴포넌트 설계</strong><ul>
<li>예전에는 컴포넌트를 크고 뭉뚱그려 짰고, “어차피 나 혼자 볼 거니까” 라는 생각도 있었습니다.</li>
<li>하지만 이제는 <strong>하나의 책임만 가지는 컴포넌트 단위</strong>로 분리해, 재사용성과 유지보수성을 고려한 구조를 만들고자 합니다.</li>
</ul>
</li>
<li><strong>명확한 함수/변수 네이밍과 가독성 있는 코드 작성</strong><ul>
<li>함수명이 모호하거나 중복된 역할을 하는 경우가 많았습니다. 예를 들어, handlePrintSomething처럼 명확하지 않은 경우요.</li>
<li>이번엔 <strong>기능을 정확히 설명하는 네이밍</strong>, 그리고 꼭 필요한 주석만을 추가해 직관적인 코드 를 지향합니다.</li>
</ul>
</li>
<li><strong>상태 관리 최적화: useReducer + Context 적용</strong><ul>
<li>props drilling으로 인한 렌더링 이슈를 줄이고, 상태를 더 깔끔하게 관리하기 위해 useReducer와 Context API를 적극 활용해보려 합니다.</li>
</ul>
</li>
</ol>
<blockquote>
<p>추가적으로 프로젝트가 어느 정도 안정화되면<br>Jest와 React Testing Library를 도입해 테스트 코드를 작성하고,<br>변경에 강한 구조를 만들어보고 싶습니다.</p>
</blockquote>
<h2 id="1-프로젝트-구성-선택-왜-vite--tailwind인가">1. 프로젝트 구성 선택: 왜 Vite + Tailwind인가?</h2>
<p>요즘 많이 쓰이는 <strong>Next.js / Vite / Vanilla</strong> 중 고민이 있었지만, 최종적으로는 <strong>Vite + Tailwind</strong> 조합으로 결정했습니다.</p>
<ul>
<li><strong>왜 Vite?</strong><br>SSR보다는 컴포넌트 구조와 훅 사용에 초점을 두고 있어, 빠르고 가벼운 환경의 Vite를 선택했습니다.</li>
<li><strong>왜 Tailwind?</strong><br>스타일링보다는 <strong>기능과 구조 설계에 집중</strong>하고 싶었기 때문에, 빠르게 UI를 구성할 수 있는 Tailwind를 도입했습니다</li>
</ul>
<h3 id="초기-설정-작업">초기 설정 작업</h3>
<ul>
<li>프로젝트 생성: npm create vite@latest</li>
<li>Tailwind 설치 및 설정: npm install -D tailwindcss postcss autoprefixer</li>
</ul>
<hr>
<h2 id="2-코드-스타일-및-품질-도구-설정">2. 코드 스타일 및 품질 도구 설정</h2>
<h3 id="eslint--prettier-설정">Eslint + Prettier 설정</h3>
<p>혼자 하는 프로젝트라고 해도 <strong>코드 가독성</strong>은 결국 미래의 나를 위한 투자라고 생각합니다.<br>나중에 다시 보았을 때 <strong>&quot;이게 뭐였지?&quot;</strong> 싶은 코드를 줄이기 위해,<br>초반부터 <strong>코딩 스타일을 통일하고 일관된 룰을 적용</strong>하기 위해 ESLint와 Prettier를 설정했습니다.</p>
<ul>
<li>코드가 자동으로 정돈되어 시간도 절약되고</li>
<li>협업 시 스타일 충돌 없이 코드 리뷰에 집중할 수 있는 기반도 다질 수 있다고 생각합니다.</li>
</ul>
<h3 id="husky-설정">Husky 설정</h3>
<p>혼자 하는 프로젝트라도 무심코 규칙을 어기면 <strong>프로젝트가 내부적으로 무너질 수 있다</strong>고 생각합니다.<br>그래서 <strong>Git hook을 통해 규칙을 강제</strong>하기 위해 husky를 설정했습니다.</p>
<hr>
<h2 id="3-컴포넌트-설계-및-구조화">3. 컴포넌트 설계 및 구조화</h2>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/2ac76ea2-2653-45df-9d9a-c4abb36b7c14/image.png" alt=""></p>
<p>예전에는 ‘일단 기능부터 만들자’는 생각으로 코드를 짜왔지만,<br>이번 프로젝트에서는 개발에 들어가기 전에 <strong>컴포넌트의 역할을 먼저 설계</strong>하고 구조를 정리해보았습니다.</p>
<h3 id="고민했던-포인트는-다음과-같아요">고민했던 포인트는 다음과 같아요:</h3>
<ul>
<li>어떻게 하면 <strong>각 컴포넌트가 하나의 책임만 가지도록</strong> 나눌 수 있을까?</li>
<li>추후에 재사용하거나 유지보수할 때 <strong>코드의 의도가 쉽게 드러날까?</strong></li>
<li><strong>props 전달</strong>이 너무 깊어지지 않게 설계할 수 있을까?</li>
</ul>
<h3 id="예상되는-컴포넌트-목록">예상되는 컴포넌트 목록:</h3>
<ol>
<li>Button.tsx: 기본 버튼 UI 컴포넌트</li>
<li>Icon.tsx: 아이콘 처리 컴포넌트</li>
<li>TodoInput.tsx: 할 일 추가 입력창</li>
<li>TodoList.tsx: 투두 목록 전체를 보여주는 컴포넌트</li>
<li>TodoItem.tsx: 각각의 투두 항목</li>
<li>TodoFilter.tsx: 전체/완료/미완료 필터링 기능</li>
</ol>
<h3 id="필요한-훅-구성">필요한 훅 구성</h3>
<ol>
<li>useTodoController:<ul>
<li>투두 추가/삭제/수정 같은 <strong>핵심 로직</strong>을 담는 훅</li>
<li>상태 조작은 이 훅에서만 일어나게 분리하는 것이 목표입니다.</li>
</ul>
</li>
<li>useTodoFilter:<ul>
<li>필터에 따라 보여줄 투두를 처리하는 훅</li>
<li>렌더링을 최소화하고, 조건별 필터링을 깔끔하게 분리하고 싶어요.</li>
</ul>
</li>
<li>useEnterClick:<ul>
<li>키보드에서 Enter를 눌렀을 때 동작을 감지하는 <strong>재사용 가능한 유틸성 훅</strong></li>
</ul>
</li>
</ol>
<p>이렇게 미리 컴포넌트와 훅을 나눠 생각해두면,<br>구현을 시작할 때 불필요한 혼란을 줄일 수 있고 <strong>코드의 방향성</strong>도 명확해진다고 느꼈습니다.</p>
<hr>
<h2 id="4-폴더-구조-설계">4. 폴더 구조 설계</h2>
<p>처음에는 FSD 패턴 도입을 고민했지만, 현재 다른 프로젝트에서 적용 중이므로 이번 프로젝트에서는 간단한 기능 중심 구조를 선택했습니다.</p>
<pre><code>├── components
│   └── ... (UI 컴포넌트)
├── hooks
│   └── useTodoController.ts
├── contents
│   └── ... (상수, config 등)
├── assets
│   └── ... (아이콘, 이미지)
├── App.tsx
├── main.tsx</code></pre><hr>
<h2 id="마무리">마무리</h2>
<p>Todo라는 작은 프로젝트이지만, 유지보수성과 설계 원칙을 잘 적용한다면 큰 프로젝트에서도 많은 도움이 될 것이라 믿고, 약간은 과할 수도 있는 목표를 가지고 출발.........</p>
<p>최대한 얻어갈 수 있도록 블로그에 하나하나 남겨보겠습니다 !!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compound Component Pattern으로 Select 컴포넌트 리팩토링하기]]></title>
            <link>https://velog.io/@ujinsimss_/Compount-Component-Pattern%EC%9C%BC%EB%A1%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@ujinsimss_/Compount-Component-Pattern%EC%9C%BC%EB%A1%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</guid>
            <pubDate>Thu, 03 Apr 2025 14:11:57 GMT</pubDate>
            <description><![CDATA[<h3 id="select-컴포넌트를-리팩토링하기-전-고민">Select 컴포넌트를 리팩토링하기 전 고민</h3>
<p>기존에 Select 컴포넌트 코드는 이러합니다. </p>
<pre><code class="language-tsx">&#39;use client&#39;;
import React, { useState } from &#39;react&#39;;
import { Icon } from &#39;../Icon&#39;;

type Props = {
  contents: string[];
  size?: &#39;md&#39; | &#39;lg&#39;;
  placeholder?: string;
};

export function Select({ contents, size, placeholder }: Props) {
  const [selectedContent, setSelectedContent] = useState(placeholder ? placeholder : contents[0]);
  const [openSelect, setOpenSelect] = useState(false);

  return (
    &lt;div
      className={`${openSelect ? &#39;rounded-lg&#39; : &#39;rounded-lg border border-gray-100&#39;} text-b&#39;${
        size === &#39;md&#39; ? &#39;md:min-w-26 w-fit min-w-24&#39; : &#39;md:min-w-66 w-60&#39;
      } cursor-pointer border border-gray-200 bg-white text-start text-base font-semibold text-gray-400 md:text-lg`}
    &gt;
      &lt;div
        className={`${
          openSelect
            ? &#39;border-b-2 border-gray-100 hover:rounded-lg hover:rounded-b-none&#39;
            : &#39;border-0&#39;
        } flex justify-between ${
          size === &#39;md&#39; ? &#39;items-center py-1 pl-3 pr-2 text-sm&#39; : &#39;px-5 py-2 md:py-3&#39;
        } hover:rounded-md hover:bg-gray-50`}
        onClick={() =&gt; setOpenSelect(!openSelect)}
      &gt;
        {selectedContent}

        &lt;Icon
          name=&quot;arrowDown&quot;
          className={`transform transition-transform duration-300 ${
            openSelect ? &#39;rotate-180&#39; : &#39;&#39;
          } ${size === &#39;md&#39; ? &#39;w-5&#39; : &#39;&#39;} }`}
        /&gt;
      &lt;/div&gt;

      {openSelect &amp;&amp; (
        &lt;div className=&quot;flex flex-col&quot;&gt;
          {contents.map((item, key) =&gt; (
            &lt;div
              onClick={() =&gt; {
                setSelectedContent(contents[key]);
                setOpenSelect(!openSelect);
              }}
              className={`cursor-pointer border-gray-100 ${
                size === &#39;md&#39; ? &#39;px-3 py-1 text-sm&#39; : &#39;px-5 py-2 md:py-3&#39;
              } last:border-none hover:bg-gray-50 hover:last:rounded-b-md`}
              key={key}
            &gt;
              {item}
            &lt;/div&gt;
          ))}
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="무언가-한덩이로-크게-span-stylecolor-red묶여져span있는-select-코드">무언가 한덩이로 크게 <span style="color: red;">묶여져</span>있는 Select 코드..</h3>
<p>모든 로직과 스타일이 하나의 컴포넌트 안에 꽉 묶여 있었습니다.
선택된 항목 상태 관리부터 드롭다운 토글, 아이템 렌더링, 스타일링까지… 모든 역할이 한 파일에 들어있는 모습입니다.</p>
<p><code>Select</code> 뿐만 아니라 <code>GroupingComponent</code>도 있는데 <code>groupName</code>만 추가된 형태임에도 새로운 컴포넌트를 만들어야됐습니다 <img src="https://velog.velcdn.com/images/ujinsimss_/post/67c06790-141c-4651-8288-a802d9a70b52/image.png" alt=""></p>
<p>이 경험을 계기로 자연스럽게 이런 질문을 던지게 됐습니다.</p>
<p>Q1. Select 컴포넌트의 ‘책임’은 어디까지여야 할까?
Q2. 조금 다른 UI/동작을 위해 새로운 컴포넌트를 만드는 것이 과연 효율적인가?
Q3. 재사용성과 확장성을 모두 고려한 UI 컴포넌트 설계 기준은 무엇인가?</p>
<p>이 고민은 단순한 리팩토링을 넘어, <strong>&quot;컴포넌트는 기능 단위가 아니라 역할 단위로 쪼개야 한다&quot;</strong>는 깨달음으로 이어졌습니다.</p>
<hr>
<h2 id="리팩토링-방향을-얻은-인사이트">리팩토링 방향을 얻은 인사이트</h2>
<p>Effective Component 강의 <strong>「지속 가능한 성장과 컴포넌트」</strong>를 보고 다음과 같은 인사이트를 얻었습니다.</p>
<h2 id="컴포넌트를-나누는-기준">컴포넌트를 나누는 기준</h2>
<h3 id="1-headless-기반으로-추상화하기">1. Headless 기반으로 추상화하기</h3>
<p>변하는 것과 변하지 않는 것을 나누자.<br>데이터는 같지만 UI는 달라질 수 있다면 <strong>UI와 데이터를 분리</strong>하자.</p>
<ul>
<li><strong>예: 달력</strong><ul>
<li>데이터는 훅으로 관리 (예: <code>useCalendar</code>)</li>
<li>UI는 별도 분리 → Headless 모듈화</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 변화하는 부분은 <code>훅</code>, 보여지는 부분은 <code>컴포넌트</code>로 분리</p>
</blockquote>
<hr>
<h3 id="2-한-가지-역할만-하기-→-composition-pattern">2. 한 가지 역할만 하기 → Composition Pattern</h3>
<p>하나의 역할을 가진 컴포넌트를 여러 개 조합해서 구성하자.</p>
<ul>
<li><strong>예: Select</strong><ul>
<li>선택된 값을 보여주는 <code>Trigger</code></li>
<li>드롭다운 메뉴인 <code>Menu</code></li>
<li>실제 아이템 목록인 <code>Item</code></li>
</ul>
</li>
</ul>
<p>→ 이벤트 흐름: <code>onClick</code> → <code>onChange</code>
<img src="https://velog.velcdn.com/images/ujinsimss_/post/8dd51d0d-9432-46b1-8b43-b3d535538197/image.png" alt=""></p>
<blockquote>
<p> 이렇게 하면 각 컴포넌트는 서로를 몰라도 되고, 독립적으로 재사용 가능</p>
</blockquote>
<h3 id="3-도메인-분리하기">3. 도메인 분리하기</h3>
<p>데이터 주입받는 방식도 컴포넌트처럼 분리해야 한다.</p>
<ul>
<li>도메인을 알지 못하는 컴포넌트  </li>
<li>도메인을 포함하는 컴포넌트</li>
</ul>
<p>이 둘을 분리하면 <strong>외부 의존성 없이도 관리하기 쉬움</strong>
<img src="https://velog.velcdn.com/images/ujinsimss_/post/24764cb9-4323-4ed4-9ed6-cc10d25b9eb3/image.png" alt=""></p>
<p> 예: 인터페이스 정의부터 고민하기</p>
<hr>
<h3 id="해당-영상을-보고-select-컴포넌트의-문제를-명확하게-알-수-있었습니다">해당 영상을 보고 Select 컴포넌트의 문제를 명확하게 알 수 있었습니다.</h3>
<blockquote>
<p>만약 이 Select에 MultiSelect나 검색 기능이 추가된다면, 어디부터 손대야되는지 막막했습니다. 
기능을 더하려 할수록 점점 커지고, 더 복잡해지는 컴포넌트. 확장에는 약하고, 변경에는 민감한 구조라는 걸 느꼈습니다.
때문에 변경에 유연한 컴포넌트 형태로 리팩토링 해야겠다고 생각이 되었습니다. </p>
</blockquote>
<hr>
<h3 id="리팩토링-구조-예시">리팩토링 구조 예시</h3>
<p>영상에서 dropdown컴포넌트를 다음과같이 구조화한 예시를 보여주었습니다 </p>
<ul>
<li><code>DropdownTrigger</code> : 열고 닫는 역할  </li>
<li><code>Menu</code> : 리스트의 wrapper  </li>
<li><code>Item</code> : 실제 선택할 항목  </li>
<li>이벤트 흐름은 <code>onClick</code> → <code>onChange</code></li>
</ul>
<p>변경된 컴포넌트는 <strong>서로의 존재를 몰라도 동작</strong>합니다.
그리고 이러한 구조는 <strong>확장성</strong>과 <strong>재사용성</strong> <strong>유지보수성</strong>을 높입니다 </p>
<hr>
<h2 id="영상에서는-컴포넌를-짜기-위한-2가지의-흐름을-알려줍니다">영상에서는 컴포넌를 짜기 위한 2가지의 흐름을 알려줍니다</h2>
<h3 id="1-인터페이스-먼저-고민하기">1. 인터페이스 먼저 고민하기</h3>
<blockquote>
<p>“이 컴포넌트의 의도는 무엇인가?”<br>“기능보다 표현 방식이 더 중요할 수도 있다.”</p>
</blockquote>
<hr>
<h3 id="2-컴포넌트를-분리하는-이유를-항상-생각하기">2. 컴포넌트를 분리하는 이유를 항상 생각하기</h3>
<ul>
<li>정말 복잡도를 낮추는가?  </li>
<li>정말 재사용 가능한가?</li>
</ul>
<blockquote>
<p>“리팩토링은 기능을 더하는 것이 아니라, 이해를 더하는 과정이다.”</p>
</blockquote>
<hr>
<h2 id="새롭게-select-컴포넌트-리팩토링-방향을-정해보았습니다">새롭게 Select 컴포넌트 리팩토링 방향을 정해보았습니다</h2>
<ol>
<li>역할 단위 분리: 기능을 기준으로 파일 분리를 통해 책임을 명확히 구분</li>
<li>Context 도입: 하위 컴포넌트들이 상태를 공유하도록 하여 prop drilling 제거</li>
<li>재사용성 향상: Option, TriggerButton 등은 다른 Select 변형에도 재활용 가능</li>
<li>Storybook 구성: 유지보수 시 시각적으로 컴포넌트를 빠르게 확인 가능</li>
</ol>
<h4 id="1-폴더구조-directory-structure">1. 폴더구조 (directory structure)</h4>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/0634a504-f586-4e2b-82b1-3a6b5822918b/image.png" alt=""></p>
<p>Select 컴포넌트는 역할별로 책임을 나눠 확장성과 재사용성을 고려한 구조로 설계했습니다.</p>
<ul>
<li><code>index.ts</code>: Select 관련 컴포넌트를 통합 export하는 진입 파일</li>
<li><code>SelectMain.tsx</code>: Select의 루트 컴포넌트로, Context를 제공하고 전체 구조를 제어</li>
<li><code>TriggerButton.tsx</code>: 선택된 값을 보여주고 드롭다운을 열고 닫는 버튼 역할</li>
<li><code>OptionList.tsx</code>: 드롭다운 영역을 구성하며, 내부에 여러 Option을 포함하는 파일</li>
<li><code>Option.tsx</code>: 실제 선택 가능한 항목</li>
<li><code>OptionGroupName.tsx</code>: 옵션을 그룹으로 묶을 때 사용하는 제목 컴포넌트</li>
<li><code>Select.context.tsx</code>: 선택 상태, 열림 여부 등을 관리하는 Context를 정의</li>
<li><code>useSelectMain.ts</code>: Select 내부 로직을 담당하는 커스텀 훅</li>
<li><code>Select.stories.tsx</code>: Storybook용 시각화 테스트 파일</li>
</ul>
<p>각 파일이 명확한 역할을 가지도록 분리함으로써,기능 추가나 유지보수가 훨씬 쉬워졌습니다.</p>
<h4 id="2-상태-관리-흐름">2. 상태 관리 흐름</h4>
<p><code>useSelectMain</code>: 선택 상태 및 열림 여부 등 4가지 상태를 지역 상태로 관리합니다</p>
<blockquote>
<p><code>selected</code>: 현재 선택된 항목
<code>isOpen</code>: 드롭다운 열림 여부
<code>setIsOpen</code>: 드롭다운 열고 닫기 제어
<code>handleSelect</code>: 옵션 선택 핸들러</p>
</blockquote>
<p><code>SelectMain</code>: UI 뼈대를 구성하고, context로 하위 컴포넌트에 상태 전달</p>
<p><code>SelectContext.Provider</code>: context를 통해 하위 컴포넌트로 상태 전파</p>
<p><code>useSelectContext</code>: 하위 컴포넌트들이 context 값을 받아 사용</p>
<h3 id="그림으로-표현하면-이렇습니다">그림으로 표현하면 이렇습니다</h3>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/f59a20d1-467e-42c8-b587-063cdf265876/image.png" alt=""></p>
<p>*<em>구조로 나타내면 이렇게 표현할 수 있습니다 *</em></p>
<pre><code>[useSelectMain]         ← 지역 상태 생성
      │
      ▼
[SelectMain]            ← 상태를 context로 감싸서 하위에 전달
      │
      ▼
[SelectContext.Provider]    ← context 전파
      │
      ▼
[useSelectContext()]    ← 하위 컴포넌트가 context 값 사용
      ├── Option        → onSelect, size
      └── OptionList    → size
      └── TriggerButton    → size,onSelect, selected 
</code></pre><p>그리고 마지막으로는 만들어진 컴포넌트를 조립해서 
<img src="https://velog.velcdn.com/images/ujinsimss_/post/357091db-4af3-4784-b160-c1d5f0481c9c/image.png" alt="">
각각의 컴포넌트처럼 사용할 수 있도록 만들었습니다 </p>
<h1 id="느낀점">느낀점</h1>
<p>이번 리팩토링을 통해,
그동안 <strong>컴포넌트를 나눈다 = 파일/폴더 단위로 나눈다</strong> 라고만 생각했던 저 자신을 돌아보게 되었습니다.</p>
<p>컴포넌트는 단순히 폴더에 따라 분리하는 것이 아니라,
그 안에서도 역할(기능 vs UI) 기반으로 더 깊이 있게 나눌 수 있고,
필요한 기능을 조립해서 사용하는 <strong>‘블록형 사고방식</strong>’이 가능하다는 것을 새롭게 체감했습니다.</p>
<p>하나의 Select 컴포넌트 안에서도</p>
<ul>
<li>상태 관리 (useSelectMain)</li>
<li>전역 상태 공유 (SelectContext)</li>
<li>UI 단위 분리 (Trigger, Option, OptionList, Group) 로 나눌 수 있다는 점에서 역할 중심의 설계 사고방식이 생겼습니다.</li>
</ul>
<p>단순히 쓰기 좋게 만드는 게 아니라,
나중에 내가 다시 볼 때도 유용한 구조로 만드는 것이 진짜 설계라는 걸 느꼈습니다.</p>
<p>*<em>잘 만든 컴포넌트는 나에게 다시 되돌아온다 *</em> 라는 생각이 가장 들었고 
이 구조가 앞으로 제 프로젝트에서도 재사용될 수 있다는 생각에 설계의 중요성을 더 실감하게 됐습니다.</p>
<p>이번 경험을 바탕으로 </p>
<ul>
<li>기능과 UI를 나누는 시선</li>
<li>context와 상태를 언제, 어떻게 공유할지에 대한 판단</li>
<li>조립 가능한 설계 패턴을 염두에 둔 컴포넌트 작성</li>
</ul>
<p>에 더 깊이 고민하며 성장해 나가고 싶습니다.</p>
<h3 id="참고">참고</h3>
<p><a href="https://www.youtube.com/watch?v=fR8tsJ2r7Eg&amp;t=46s">https://www.youtube.com/watch?v=fR8tsJ2r7Eg&amp;t=46s</a> 
<a href="https://velog.io/@aeong98/%EC%BB%B4%ED%8C%8C%EC%9A%B4%EB%93%9C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-Select-%EB%A7%8C%EB%93%A4%EA%B8%B0">https://velog.io/@aeong98/%EC%BB%B4%ED%8C%8C%EC%9A%B4%EB%93%9C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-Select-%EB%A7%8C%EB%93%A4%EA%B8%B0</a>
<a href="https://ui.shadcn.com/docs/components/dropdown-menu">https://ui.shadcn.com/docs/components/dropdown-menu</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Admin 폼지 구현하기  (with Next , 리팩토링 진행하기)]]></title>
            <link>https://velog.io/@ujinsimss_/%EC%A7%80%EC%9B%90%EC%84%9C-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%A7%84%ED%96%89%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ujinsimss_/%EC%A7%80%EC%9B%90%EC%84%9C-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%A7%84%ED%96%89%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 09 Mar 2025 17:57:59 GMT</pubDate>
            <description><![CDATA[<h2 id="띵동-프로젝트--폼지-제작-기능">띵동 프로젝트 – 폼지 제작 기능</h2>
<blockquote>
<h3 id="띵동이란">띵동이란?</h3>
<p><strong>명지대학교</strong> 동아리 <strong>통합 플랫폼</strong>으로,
학생들이 <strong>파편화된 동아리 정보</strong>와 <strong>비효율적인 동아리 업무</strong>를
<strong>일원화하여 제공하는 서비스</strong>입니다.</p>
</blockquote>
<h3 id="기존-방식과-개선점">기존 방식과 개선점</h3>
<p>기존에는 동아리 지원 시, 해당 동아리가 올린 <strong>외부 폼지(구글 폼 등)</strong>로 이동해야 했어요.</p>
<p>💡 <strong>개선된 방식 → 띵동 자체 폼지</strong>를 제작!
이제 <strong>지원하기 버튼</strong>을 누르면 <strong>띵동 내부 폼지</strong>로 이동합니다.</p>
<hr>
<h3 id="폼지-구성">폼지 구성</h3>
<h4 id="✅-기본-질문-공통">✅ 기본 질문 (공통)</h4>
<p>동아리 지원서 특성상, 모든 지원자가 <strong>공통으로 입력해야 하는 항목</strong>이 있어요.</p>
<ul>
<li>이름</li>
<li>학번</li>
<li>학과</li>
<li>전화번호</li>
<li>이메일</li>
</ul>
<h4 id="✅-맞춤형-질문-시트-추가-기능">✅ 맞춤형 질문 (시트 추가 기능)</h4>
<p>모집 분야가 여러 개인 동아리는 <strong>시트를 추가</strong>할 수 있어요.</p>
<blockquote>
<p><strong>예를 들어,</strong>
밴드부 → 보컬, 기타, 드럼 등 각 파트별 추가 질문 가능
사용자가 보컬 선택 → 공통 질문 + 보컬 질문 응답</p>
</blockquote>
<h4 id="✅-질문-유형-5가지">✅ 질문 유형 (5가지)</h4>
<ul>
<li>파일 업로드</li>
<li>체크박스 (다중 선택 가능)</li>
<li>객관식 (단일 선택)</li>
<li>단답형 (300자 이내)</li>
<li>서술형 (1000자 이내)
각 질문은 옵션 및 필수 여부를 설정할 수 있습니다.</li>
</ul>
<hr>
<h3 id="폼지-관리-기능-admin">폼지 관리 기능 (Admin)</h3>
<p>Admin 페이지에서** 지원서의 주요 정보**를 관리할 수 있어요.</p>
<ul>
<li>지원서 제목</li>
<li>모집 기간</li>
<li>지원서 설명</li>
<li>면접 여부 설정</li>
</ul>
<hr>
<h3 id="개발-난이도--복잡도">개발 난이도 &amp; 복잡도</h3>
<p>처음엔 단순히 폼지만 만든다고 생각했는데…
막상 개발하다 보니 <strong>엄청난 복잡도</strong>가 있었습니다. </p>
<p>-&gt; 하나의 컴포넌트에서 모든 상태 관리
<strong>ManageComponent</strong>에서</p>
<ul>
<li>새 지원서 생성</li>
<li>수정 중인 경우</li>
<li>모집 기간 전</li>
<li>모집 기간 중
모두 <strong>하나의 컴포넌트에서 관리</strong>해야 했어요.</li>
</ul>
<p>초기 코드를 복잡도를 고려하지 않고 작성하면…</p>
<ul>
<li>유지보수 어려움</li>
<li>예외 처리 부족
기능 추가 시 구조적으로 수정해야 하는 부분이 많아져서 <strong>확장성을 고려한 설계</strong>가 정말 중요하다는 걸 느꼈습니다. </li>
</ul>
<p>이를 고려하지 않고 코드를 짠 결과 
제 안타까운 <strong>ManageForm</strong>컴포넌트를 소개해보겠습니다. . . ☠️</p>
<pre><code class="language-tsx">const router = useRouter();
  const [{ token }] = useCookies([&#39;token&#39;]);
  const newFormMutation = useNewForm(token);
  const [isEditing, setIsEditing] = useState(false);
  const updateFormMutation = useUpdateForm(setIsEditing);

  const [title, setTitle] = useState(formData?.title ? formData.title : &#39;&#39;);
  const [description, setDescription] = useState(
    formData?.description ? formData.description : &#39;&#39;,
  );</code></pre>
<p>*<em>폼 내부 요소들을 모두 각각 관리함 *</em></p>
<pre><code class="language-tsx">useEffect(() =&gt; {
    if (formData) {
      const updatedFormFields = Object.keys(categorizeFormFields(formData)).map(
        (section) =&gt; ({
          section,
          questions: categorizeFormFields(formData)[section].map((field) =&gt; ({
            question: field.question,
            type: field.type,
            options: field.options || [],
            required: field.required,
            order: field.order,
            section: field.section,
          })),
        }),
      );

      setFormField(updatedFormFields);
    }
  }, [formData]);
</code></pre>
<p>*<em>폼 안에 섹션들을 카테고리화하여 분류함 *</em></p>
<pre><code class="language-tsx">const handleCreateForm = () =&gt; {
    if (!title) {
      toast.error(&#39;지원서 제목을 입력하여주세요. &#39;);
    }

    if (!description || description.length &gt; 255) {
      toast.error(&#39;지원서 설명은 255자 이내로 작성하여주세요.&#39;);
      return;
    }

    const formattedPostData = formatFormData();
    newFormMutation.mutate(formattedPostData);
  };

  const handleUpdateForm = () =&gt; {
    if (!id || !formData) {
      toast.error(&#39;수정할 폼이 존재하지 않습니다.&#39;);
      return;
    }
    if (!title) {
      toast.error(&#39;지원서 제목을 입력하여주세요. &#39;);
    }

    if (!description || description.length &gt; 255) {
      toast.error(&#39;지원서 설명은 255자 이내로 작성하여주세요.&#39;);
      return;
    }

    const formattedPostData = formatFormData();

```tsx
    updateFormMutation.mutate({
      token,
      formId: id,
      formData: formattedPostData,
    });
    setIsEditing(false);
    setIsClosed(true);
  };</code></pre>
<p>*<em>형식을 컴포넌트 내에서 모두 같이 관리함 *</em></p>
<pre><code class="language-tsx">  const formatFormData = (): FormData =&gt; {
    const formatDate = (date: Date | string | null) =&gt; {
      if (!date) return &#39;&#39;;
      if (date instanceof Date) return date.toISOString().split(&#39;T&#39;)[0];
      return new Date(date).toISOString().split(&#39;T&#39;)[0];
    };

    return {
      title: title.trim(),
      description: description.trim() || null,
      startDate: formatDate(recruitPeriod.startDate),
      endDate: formatDate(recruitPeriod.endDate),
      hasInterview: isChecked ?? false,
      sections: sections,
      formFields: formField.flatMap((section) =&gt;
        section.questions.map(
          (question): FormField =&gt; ({
            question: question.question.trim(),
            type: question.type as QuestionType,
            options: question.options || [],
            required: question.required,
            order: question.order,
            section: section.section,
          }),
        ),
      ),
    };
  };
</code></pre>
<p><strong>카테고리화해서 관리하기 때문에 이를 다시 해지해줄 format이 필요하게 됨</strong></p>
<pre><code class="language-tsx">const isPastStartDate = formData?.startDate
    ? new Date(formData.startDate) &lt; new Date()
    : false;

  const [isClosed, setIsClosed] = useState(formData ? true : false);

  const [isChecked, setIsChecked] = useState(formData?.hasInterview);

  function categorizeFormFields(
    formData: FormData | undefined,
  ): CategorizedFields {
    const categorizedFields: CategorizedFields = {};

    (formData?.sections || []).forEach((section) =&gt; {
      categorizedFields[section] = [];
    });

    (formData?.formFields || []).forEach((field) =&gt; {
      if (field.section in categorizedFields) {
        categorizedFields[field.section].push(field);
      }
    });

    return categorizedFields;
  }
</code></pre>
<pre><code class="language-tsx"> const [modiformField, setmodiFormField] = useState(
    Object.keys(categorizeFormFields(formData)).map((section) =&gt; ({
      section,
      questions: categorizeFormFields(formData)[section].map((field) =&gt; ({
        question: field.question,
        type: field.type,
        options: field.options || [&#39;옵션1&#39;],
        required: field.required,
        order: field.order,
        section,
      })),
    })),
  );

  const [focusSection, setFocusSection] = useState(&#39;공통&#39;);
  const [sections, setSections] = useState(
    formData ? formData.sections : [&#39;공통&#39;],
  );
  const [recruitPeriod, setRecruitPeriod] = useState&lt;DateRangeType&gt;({
    startDate: null,
    endDate: null,
  });

  useEffect(() =&gt; {
    if (formData) {
      setRecruitPeriod({
        startDate: formData.startDate ? new Date(formData.startDate) : null,
        endDate: formData.endDate ? new Date(formData.endDate) : null,
      });
    }
  }, [formData]);

  const baseQuestion: FormField[] = [
    {
      question: &#39;&#39;,
      type: &#39;RADIO&#39;,
      options: [&#39;옵션1&#39;],
      required: true,
      order: 1,
      section: &#39;공통&#39;,
    },
  ];

  const [formField, setFormField] = useState&lt;SectionFormField[]&gt;(
    formData
      ? modiformField
      : [
          {
            section: &#39;공통&#39;,
            questions: [
              {
                question: &#39;&#39;,
                type: &#39;RADIO&#39;,
                options: [&#39;옵션1&#39;],
                required: true,
                order: 1,
                section: &#39;공통&#39;,
              },
            ],
          },
        ],
  );

  const handleDateChange = (newValue: DateRangeType | null) =&gt; {
    setRecruitPeriod(newValue ?? { startDate: null, endDate: null });
  };

  const deleteQuestion = (sectionName: string, questionIndex: number) =&gt; {
    setFormField((prev) =&gt;
      prev.map((section) =&gt;
        section.section === sectionName
          ? {
              ...section,
              questions: section.questions
                .filter((_, qIndex) =&gt; qIndex !== questionIndex)
                .map((question, newIndex) =&gt; ({
                  ...question,
                  order: newIndex + 1,
                })),
            }
          : section,
      ),
    );
  };

  const [modalVisible, setModalVisible] = useState(false);
  const [newSectionName, setNewSectionName] = useState(&#39;&#39;);

  const handleOpenModal = () =&gt; {
    setNewSectionName(&#39;&#39;);
    setModalVisible(true);
  };

  const onClickEditButton = () =&gt; {
    setIsEditing(true);
    setIsClosed(false);
  };

  const onClickCancelButton = () =&gt; {
    onReset?.();
  };

  const addQuestion = () =&gt; {
    setFormField((prev) =&gt;
      prev.map((section) =&gt;
        section.section === focusSection
          ? {
              ...section,
              questions: [
                ...section.questions,
                {
                  question: &#39;&#39;,
                  type: &#39;RADIO&#39;,
                  options: [&#39;옵션1&#39;],
                  required: true,
                  order: section.questions.length + 1,
                  section: focusSection,
                },
              ],
            }
          : section,
      ),
    );
  };</code></pre>
<pre><code class="language-tsx">  return (
    &lt;div&gt;
      &lt;Head&gt;
        &lt;title&gt;지원서 템플릿 관리&lt;/title&gt;
      &lt;/Head&gt;

      &lt;div className=&quot;flex items-center justify-between gap-2&quot;&gt;
        &lt;div
          onClick={() =&gt; router.back()}
          className=&quot;flex cursor-pointer items-center gap-1 &quot;
        &gt;
          &lt;Image
            src={arrow_left}
            alt=&quot;navigation&quot;
            className=&quot;mt-7 w-6 md:mt-11 md:w-9&quot;
          /&gt;
          &lt;Heading&gt;지원서 생성&lt;/Heading&gt;
        &lt;/div&gt;
        &lt;FormEditButtons
          formData={formData}
          isEditing={isEditing}
          isClosed={isClosed}
          isPastStartDate={isPastStartDate}
          handleCreateForm={handleCreateForm}
          onClickEditButton={onClickEditButton}
          onClickCancelButton={onClickCancelButton}
          handleUpdateForm={handleUpdateForm}
        /&gt;
      &lt;/div&gt;

      &lt;div className=&quot;flex w-full items-center justify-end gap-2 pt-10 text-lg font-semibold text-gray-500&quot;&gt;
        &lt;div className=&quot;relative flex h-[20px] w-[20px] cursor-pointer items-center justify-center&quot;&gt;
          &lt;Image
            onClick={() =&gt; {
              if (!isClosed) {
                setIsChecked(!isChecked);
              }
            }}
            src={isChecked ? square : emptySquare}
            width={isChecked ? 18 : 22}
            height={isChecked ? 18 : 22}
            className={`object-contain ${
              isClosed ? &#39;cursor-not-allowed opacity-50&#39; : &#39;cursor-pointer&#39;
            }`}
            alt=&quot;checkBox&quot;
          /&gt;
        &lt;/div&gt;
        우리동아리는 면접을 보지 않아요!
      &lt;/div&gt;

      &lt;div className=&quot;flex flex-col gap-4&quot;&gt;
        &lt;div className=&quot;mt-4 flex flex-row flex-wrap gap-3 md:flex-nowrap&quot;&gt;
          &lt;BaseInput
            type=&quot;text&quot;
            placeholder={&#39;지원서 제목을 입력해주세요&#39;}
            value={title}
            onChange={(
              e: React.ChangeEvent&lt;HTMLInputElement | HTMLTextAreaElement&gt;,
            ) =&gt; setTitle(e.target.value)}
            disabled={isClosed}
          /&gt;

          &lt;div className=&quot;w-full rounded-lg border pt-1&quot;&gt;
            &lt;Datepicker
              value={recruitPeriod}
              useRange={false}
              minDate={new Date(new Date().getFullYear(), 0, 1)}
              maxDate={new Date(new Date().getFullYear(), 11, 31)}
              onChange={handleDateChange}
              placeholder=&quot;모집 기간을 설정하세요&quot;
              disabled={isClosed}
            /&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;TextArea
          placeholder=&quot;지원서 설명을 입력해 주세요 (최대 255자 이내)&quot;
          value={description}
          onChange={(
            e: React.ChangeEvent&lt;HTMLTextAreaElement | HTMLInputElement&gt;,
          ) =&gt; setDescription(e.target.value)}
          disabled={isClosed}
        /&gt;
      &lt;/div&gt;

      &lt;div className=&quot;mt-6&quot;&gt;
        &lt;Sections
          addSection={handleOpenModal}
          focusSection={focusSection}
          sections={sections}
          setFocusSection={setFocusSection}
          isClosed={isClosed}
          formField={formField}
          setFormField={setFormField}
          setSections={setSections}
          baseQuestion={baseQuestion}
        /&gt;
        {focusSection == &#39;공통&#39; &amp;&amp; &lt;CommonQuestion disabled={true} /&gt;}

        {formField
          .filter((item) =&gt; item.section === focusSection)
          .map((section) =&gt; (
            &lt;div key={section.section}&gt;
              {section.questions.map((question, qIndex) =&gt; (
                &lt;Question
                  key={`${section.section}-${qIndex}`}
                  index={qIndex}
                  questionData={question}
                  deleteQuestion={() =&gt; deleteQuestion(section.section, qIndex)}
                  setFormField={setFormField}
                  section={section}
                  isClosed={isClosed}
                /&gt;
              ))}
            &lt;/div&gt;
          ))}
      &lt;/div&gt;
      {!isClosed &amp;&amp; (
        &lt;button
          onClick={addQuestion}
          className=&quot;fixed bottom-24 right-[calc(10vw)] flex items-center justify-center rounded-full bg-blue-500 p-1 shadow-lg transition-all duration-200 hover:bg-blue-600 md:right-[calc(5vw)] lg:right-[calc(2vw)]&quot;
        &gt;
          &lt;Image src={AddForm} width={40} height={40} alt=&quot;질문 추가하기&quot; /&gt;
        &lt;/button&gt;
      )}
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="주요-문제점-정리">주요 문제점 정리</h3>
<h4 id="1-과도한-상태state-관리">1. 과도한 상태(state) 관리</h4>
<ul>
<li><strong>isEditing, isClosed, isPastStartDate</strong> 등 권한과 관련된 상태들이 마구잡이로 관리됨</li>
<li><strong>formData, sections, formFields</strong> 등 폼지의 필드들도 각각의 state로 관리됨
결과적으로 수정이 어려운 코드가 되어버림</li>
</ul>
<h4 id="2-불필요한-데이터-변환">2. 불필요한 데이터 변환</h4>
<p>formData를 섹션별로 객체화하여 관리했는데, </p>
<ul>
<li>막상 섹션 추가하는 경우는 거의 없음</li>
<li>게다가 질문도 많아야 10개 이하 </li>
<li><blockquote>
<p><strong>단순 배열로 관리</strong>했으면 더 깔끔했을 것임</p>
</blockquote>
</li>
</ul>
<h4 id="3-검증validation-중복">3. 검증(Validation) 중복</h4>
<ul>
<li>Zod를 사용해 Validation을 관리하고 있음에도,</li>
<li><em>별도로 유효성 검사를 추가*</em>하여 코드가 불필요하게 길어짐</li>
</ul>
<h4 id="4-컴포넌트-응집도-최악">4. 컴포넌트 응집도 최악</h4>
<ul>
<li>ManageForm에서 모든 로직을 떠맡음</li>
<li>심지어 &quot;수정 버튼의 상태&quot;까지도 관리함</li>
<li><em>단일 책임 원칙(SRP)을 완전히 위반함.*</em></li>
</ul>
<h4 id="5-재사용-불가능한-거대한-컴포넌트">5. 재사용 불가능한 거대한 컴포넌트</h4>
<ul>
<li><strong>Sections, Question, FormEditButtons</strong> 등 모든 하위 컴포넌트가 과도한 props를 받음</li>
<li><strong>isEditing, isClosed, isPastStartDate, handleCreateForm</strong> 등 props를 남발하여 유지보수가 힘듦</li>
</ul>
<hr>
<h3 id="왜-이렇게-됐을까">왜 이렇게 됐을까?</h3>
<h4 id="1-수정에-대한-설계를-하지-않음">1. &quot;수정&quot;에 대한 설계를 하지 않음</h4>
<ul>
<li>처음에는 지원서 생성만 고려하여 개발</li>
<li>이후 &quot;수정&quot; 기능 추가 시 무리한 상태 추가 → 코드가 엉망이 됨</li>
</ul>
<h4 id="2-초기에는-모집-기간이-지난-폼지도-수정-가능했음">2. 초기에는 모집 기간이 지난 폼지도 수정 가능했음</h4>
<ul>
<li>이후 &quot;모집 기간이 지난 폼지는 수정 불가&quot; 정책이 추가됨</li>
<li>이 과정에서 상태가 더 복잡해짐</li>
</ul>
<h4 id="3-수정-기능-추가-시-기존-코드에-덧붙이는-방식으로-작성">3. 수정 기능 추가 시 기존 코드에 덧붙이는 방식으로 작성</h4>
<ul>
<li>&quot;기존 코드 유지&quot;하면서 기능 추가</li>
<li>결과적으로 일관성 없는 상태 관리 + 안좋은 가독성</li>
</ul>
<h4 id="4-데이터-구조-설계-실수">4. 데이터 구조 설계 실수</h4>
<ul>
<li>폼지의 필드를 섹션별 객체로 나눠 관리함</li>
<li>하지만 필드가 많아야 10개 이하 → 단순 배열로 관리하는 게 나았음</li>
</ul>
<p>일단 배포가 되었지만.... <strong>코드의 심각성</strong>을 느끼고 
리팩토링에 들어갔습니다. </p>
<hr>
<h3 id="리팩토링">리팩토링</h3>
<h4 id="1-상태-관리-일원화-formstate">1. 상태 관리 일원화 (formState)</h4>
<ul>
<li>기존에는 개별 상태 (title, description 등)를 각각 관리했지만, formState 객체로 통합하여 상태 변경을 일관되게 유지할 수 있음.</li>
<li>useState를 통해 모든 데이터를 한 곳에서 관리하면서도, <strong>특정 필드만 업데이트하는 방식</strong>으로 개선함.</li>
</ul>
<pre><code class="language-tsx">const [formState, setFormState] = useState&lt;FormState&gt;({
    title: formData?.title ?? &#39;&#39;,
    description: formData?.description ?? &#39;&#39;,
    hasInterview: formData?.hasInterview ?? false,
    sections: formData?.sections ?? [&#39;공통&#39;],
    startDate: formData?.startDate ?? null,
    endDate: formData?.endDate ?? null,
    formFields: formData?.formFields ?? [],
  });</code></pre>
<h4 id="2-mode와-isdisabled-iseditableregardlessofperiod-활용">2. mode와 isDisabled, isEditableRegardlessOfPeriod 활용</h4>
<ul>
<li>mode가 <strong>수정(edit)과 뷰(view)</strong>를 명확하게 구분하도록 해 모드 전환이 직관적으로 변경했어요 </li>
</ul>
<p><strong>isDisabled</strong>와 <strong>isEditableRegardlessOfPeriod</strong>를 활용하여 사용자의 입력 가능 여부를 상태로 관리하므로,
폼 입력 필드나 버튼을 제어할 때 중복 로직 없이 깔끔하게 처리할 수 있음.</p>
<pre><code class="language-tsx">
const isPastStartDate = formData?.startDate
    ? new Date(formData.startDate) &lt; new Date()
    : false;

  const [mode, setMode] = useState&lt;&#39;view&#39; | &#39;edit&#39;&gt;(
    formData == undefined ? &#39;edit&#39; : &#39;view&#39;,
  );

  const isDisabled = mode === &#39;view&#39; || isPastStartDate;

  const isEditableRegardlessOfPeriod = mode === &#39;view&#39;;</code></pre>
<h4 id="3-formeditbuttons-컴포넌트로-책임-분리">3. FormEditButtons 컴포넌트로 책임 분리</h4>
<ul>
<li><p>기존에는 <strong>ManageForm에</strong>서 폼 저장 및 수정 로직을 직접 관리했지만, 이를 <strong>FormEditButtons</strong>로 이동하여 관심사 분리 (Separation of Concerns, SoC) 를 적용.</p>
</li>
<li><p><strong>FormEditButtons</strong> 내부에서 <strong>handleCreateForm, handleUpdateForm, onClickEditButton</strong> 등 관련 로직을 모두 처리하도록 구조화.</p>
</li>
</ul>
<p>-&gt; 이렇게 분리하면 ManageForm이 폼 데이터를 렌더링하는 역할에 집중할 수 있어 코드가 더 모듈화됨.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;
import { useCookies } from &#39;react-cookie&#39;;
import { useNewForm } from &#39;@/hooks/api/apply/useNewForm&#39;;
import { useUpdateForm } from &#39;@/hooks/api/apply/useUpdateForm&#39;;
import { useUpdateFormDeadline } from &#39;@/hooks/api/apply/useUpdateFormDeadline&#39;;
import { FormState } from &#39;@/types/form&#39;;

type ModeType = &#39;view&#39; | &#39;edit&#39;;

type Props = {
  formData: FormState | undefined;
  mode: ModeType;
  onReset: () =&gt; void;
  setMode: React.Dispatch&lt;React.SetStateAction&lt;ModeType&gt;&gt;;
  formState: FormState;
  id: number | undefined;
  isPastStartDate: boolean;
};

export default function FormEditButtons({
  isPastStartDate,
  formData,
  mode,
  onReset,
  setMode,
  formState,
  id,
}: Props) {
  const [{ token }] = useCookies([&#39;token&#39;]);
  const newFormMutation = useNewForm(token);
  const updateFormMutation = useUpdateForm(setMode);
  const updateFormDeadlineMutation = useUpdateFormDeadline();

  const onClickEditButton = () =&gt; {
    setMode(&#39;edit&#39;);
  };
  const handleCreateForm = () =&gt; {
    newFormMutation.mutate({
      ...formState,
      startDate: formState.startDate || &#39;&#39;,
      endDate: formState.endDate || &#39;&#39;,
    });
  };
  const onClickCancelButton = () =&gt; {
    setMode(&#39;view&#39;);
    onReset();
  };
  const handleUpdateForm = () =&gt; {
    if (id === undefined) {
      return;
    }
    if (isPastStartDate) {
      updateFormDeadlineMutation.mutate({
        token,
        formId: id,
        endDate: formState.endDate || &#39;&#39;,
      });
    } else {
      updateFormMutation.mutate({
        token,
        formId: id,
        formData: {
          ...formState,
          startDate: formState.startDate || &#39;&#39;,
          endDate: formState.endDate || &#39;&#39;,
        },
      });
    }
  };

  return (
    &lt;div className=&quot;mt-7 flex items-center justify-between gap-2 text-lg&quot;&gt;
      {!formData ? (
        &lt;button
          className=&quot;rounded-xl bg-blue-500 px-4 py-2 font-semibold text-white hover:bg-blue-600&quot;
          onClick={handleCreateForm}
        &gt;
          저장하기
        &lt;/button&gt;
      ) : (
        &lt;&gt;
          {mode == &#39;view&#39; ? (
            &lt;button
              onClick={onClickEditButton}
              className=&quot;cursor-pointer rounded-xl bg-blue-100 px-4 py-2 font-semibold text-blue-500 hover:bg-blue-200&quot;
            &gt;
              수정하기
            &lt;/button&gt;
          ) : (
            &lt;div className=&quot;flex flex-row gap-2&quot;&gt;
              &lt;button
                onClick={onClickCancelButton}
                className=&quot;rounded-xl bg-gray-100 px-3 py-2 font-semibold text-gray-500 hover:bg-gray-200&quot;
              &gt;
                취소
              &lt;/button&gt;
              &lt;button
                onClick={handleUpdateForm}
                className=&quot;rounded-xl bg-blue-500 px-4 py-2 font-semibold text-white hover:bg-blue-400&quot;
              &gt;
                저장하기
              &lt;/button&gt;
            &lt;/div&gt;
          )}
        &lt;/&gt;
      )}
    &lt;/div&gt;
  );
}


</code></pre>
<p>아직도 수정할 부분이 많이 남았지만 일차적으로 <strong>구조적인 수정</strong>이 끝났습니다 
계속해서 리팩토링이 필요해보입니다 </p>
<p>더 개선하고 싶은 부분은..</p>
<ol>
<li><p><strong>상태관리 최적화</strong> -&gt; formState를 useState로 관리하지만, 여러 필드 업데이트를 처리하면서 setState를 연속 호출하는 경우가 많음. 폼지에서 가장 필요한 부분이 아닐까 싶습니다</p>
</li>
<li><p><strong>formFields 업데이트 최적화</strong>
현재 formFields는 배열이라 추가/삭제 시마다 setFormState를 새로 호출하는 방식인데,불변성 유지 코드를 더 간결하게 수정하고 싶습니다 </p>
</li>
<li><p>*<em>불필요한 리렌더링 줄이기 *</em> : 현재 불필요한 리렌더링이 많이 일어나고 있어서 줄이고자 합니다 </p>
</li>
</ol>
<hr>
<h3 id="느낀-점">느낀 점</h3>
<blockquote>
<p>이번 리팩토링을 진행하면서 단순히 기능을 구현하는 것보다 <strong>코드의 구조를 정리</strong>하고 <strong>유지보수성을 높이는 것</strong>이 얼마나 중요한지 다시금 깨닫게 되었습니다.
처음에는 하나씩 기능을 추가하는 방식으로 개발했지만, 점점 코드가 복잡해지면서 유지보수하기 어려워졌고, 새로운 요구사항이 생길 때마다 예상보다 더 많은 수정이 필요하게 되었습니다.
이 과정에서 <strong>개발 초기 설계의 중요성</strong>을 체감했고, <strong>코드가 확장될 때도 유연하게 대응</strong>할 수 있도록 구조를 잡는 것이 필수적이라는 점을 배웠습니다.</p>
</blockquote>
<p>특히,</p>
<ul>
<li><strong>상태를 어떻게 관리</strong>할 것인지</li>
<li><strong>컴포넌트의 역할</strong>을 어떻게 나눌 것인지</li>
<li>어떤 <strong>로직을 어디에서 담당</strong>하도록 할 것인지
이러한 고민을 깊이 해보면서 단순히 &quot;동작하는 코드&quot;가 아닌 <strong>잘 설계된 코드</strong>가 무엇인지 고민하는 계기가 되었습니다.</li>
</ul>
<p>리팩토링을 통해 관심사를 분리하고, 불필요한 상태 관리를 줄이고, 코드의 가독성을 높이며, 유지보수가 쉬운 구조로 개선할 수 있었지만, 여전히 더 다듬을 부분이 남아 있다고 생각합니다.</p>
<p><strong>&quot;기능 구현&quot;을 넘어서 &quot;설계와 유지보수&quot;까지 고려하는 개발자가 되어야겠다</strong>는 목표를 다시 한번 되새기게 된 리팩토링이었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FSD 패턴이란?,  FSD 패턴을 Next App 라우팅에서 사용해보자 ]]></title>
            <link>https://velog.io/@ujinsimss_/FSD-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80-FSD-%ED%8C%A8%ED%84%B4%EC%9D%84-Next-App-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@ujinsimss_/FSD-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80-FSD-%ED%8C%A8%ED%84%B4%EC%9D%84-Next-App-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 09 Feb 2025 08:05:27 GMT</pubDate>
            <description><![CDATA[<p>프로젝트 마이그래이션을 위해 FSD패턴을 구조로 잡고가자고 제안을 주셨습니다 </p>
<h2 id="fsd-패턴fsfp-feature-sliced-design-pattern-이란">FSD 패턴(FSFP, Feature-Sliced Design Pattern) 이란?</h2>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/a97453d3-dd61-4659-bd8f-2412400c1d42/image.png" alt=""></p>
<pre><code>FSD 패턴은 프론트엔드 프로젝트를 계층적으로 구성하여 유지보수성을 높이는 아키텍처 패턴입니다.
기존 폴더 구조보다 역할별 분리를 강조하며, 프로젝트가 커질수록 폴더 구조가 명확해지고, 코드 간 의존성이 줄어드는 장점이 있습니다.</code></pre><h3 id="fsd-패턴의-주요-폴더-구조">FSD 패턴의 주요 폴더 구조</h3>
<blockquote>
<p><strong>app</strong>
전역 설정 담당 (_app , _document 등 로직이 초기화 되는 곳)
Provider, Router, client 같은 컴포넌트들이 slice 된다.
<strong>processes (선택)</strong>
회원가입 등과 같이 여러 단계로 이루어진 프로세스를 처리한다.
특정한 상태를 기반으로 여러 화면 또는 로직이 포함된 작업
<strong>processes/</strong>
├── <strong>registration/</strong>
│   ├──** model/ ** // 회원가입 상태 및 로직 관리
│   ├── <strong>ui/</strong>  // 회원가입 관련 화면 및 컴포넌트
│   └── <strong>index.ts</strong>
└── <strong>checkout/</strong>
<strong>pages</strong> → 메인페이지 URL 라우트에 매핑되는 페이지가 포함된다.
<strong>widgets</strong> (선택) → 헤더, 내용별 컨텐츠
여러 feature를 결합해 특정 화면 단위의 구성 요소를 정의, 페이지에 사용되는 독립적인 UI 컴포넌트
<strong>features</strong> (선택) → 폼, 액션버튼
행동, 비즈니스 가치를 전달하는 사용자 시나리오와 기능
<strong>entities</strong> →
주요 데이터 모델과 그와 관련된 상태/로직, 비즈니스 엔티티
<strong>shared</strong> →공유 컴포넌트</p>
</blockquote>
<p>이제 패턴에 대해 어느정도 이해를 했으니 Next.js에 적용하려고 했다 
근데 .. </p>
<h2 id="nextjs-app-router와-fsd-패턴-충돌-문제">Next.js App Router와 FSD 패턴 충돌 문제</h2>
<pre><code>Next.js 13.4부터 App Router 방식이 도입되면서 
기존 pages/ 라우팅 방식과 app/ 폴더 방식이 혼용될 수 있는 문제가 발생합니다.</code></pre><p>FSD 패턴에는 pages/ 폴더가 존재하는데, Next.js에도 pages/가 존재하기 때문에 폴더 충돌 가능성이 발생한다 
Next.js App Router 방식에서는 app/ 폴더를 루트로 두어야 하지만, FSD 패턴에서도 app/ 폴더가 존재할 수 있어 혼동 가능한 것이다 </p>
<h3 id="해결-방법">해결 방법</h3>
<p>공식문서를 통해서 해결 방법을 찾아보았다 </p>
<blockquote>
<p> Next.js App Router를 루트로 두고 FSD 페이지 구조를 src/ 폴더 내부에 배치
app/ 폴더를 루트에 유지하고,
pages/ 폴더를 FSD 내부에서 관리하는 방식이 권장된다 
<a href="https://feature-sliced.design/docs/guides/tech/with-nextjs#app-router">next.js에서 FSD패턴 가이드</a></p>
</blockquote>
<pre><code>📦 프로젝트 루트
 ├── 📂 app/  (Next.js App Router 전용 폴더)
 │    ├── 📂 src/
 │    │    ├── 📂 pages/   (🚀 FSD 패턴에 맞춘 페이지 구조)
 │    │    ├── 📂 processes/
 │    │    ├── 📂 widgets/
 │    │    ├── 📂 features/
 │    │    ├── 📂 entities/
 │    │    ├── 📂 shared/
 │    │    ├── index.ts
 │    ├── layout.tsx  (Next.js의 App Router 전용)
 │    ├── page.tsx  (Next.js의 기본 페이지)
 │    └── global.css
 ├── 📂 public/
 ├── 📂 styles/
 ├── 📂 components/
 ├── next.config.js
 ├── tsconfig.json
 └── package.json</code></pre><p> 해당 해결책으로 <code>FSD패턴</code>을 적용하기로 했습니다 </p>
<h3 id="그래서-우리는-기존-프로젝트의-마이그래이션으로-fsd패턴을-쓰려고-했는가">그래서 우리는 기존 프로젝트의 마이그래이션으로 FSD패턴을 쓰려고 했는가</h3>
<ol>
<li>프로젝트 유지보수가 쉬워짐
기존 Next.js의 라우팅 방식 (pages/ 중심)에서는 컴포넌트의 역할이 모호해질 수 있음.
FSD 패턴을 적용하면 각 기능과 데이터 모델이 명확하게 분리되어 유지보수가 편리함.</li>
</ol>
<p>-&gt; 기존 구조에서는 하나의 작업에서 여러 폴더 계층을 오가야되는 상황 해결 </p>
<ol start="2">
<li><p>페이지별 독립적인 관리 가능
pages/ 폴더가 혼합되지 않고, 각 기능을 독립적인 feature로 관리할 수 있음.
하나의 페이지에서 필요한 기능만 로드하여 모듈화가 쉬워짐.</p>
</li>
<li><p>의존성 관리가 쉬워짐
상위 폴더에서 하위 폴더만 import 가능하도록 규칙을 강제하면, 순환 의존성 문제를 예방할 수 있음.
ESLint 규칙을 적용하면 코드 규칙을 자동으로 검사할 수 있으니 사용해보면 좋을 듯 !! </p>
</li>
</ol>
<p><strong>ESLint FSD 규칙 적용 예시</strong></p>
<pre><code class="language-tsx">&quot;overrides&quot;: [
  {
    &quot;files&quot;: [&quot;src/**/*.ts&quot;, &quot;src/**/*.tsx&quot;],
    &quot;rules&quot;: {
      &quot;fsd-layer-imports/path-checker&quot;: &quot;error&quot;,
      &quot;fsd-layer-imports/public-api-imports&quot;: &quot;error&quot;
    }
  }
]</code></pre>
<h3 id="우려되는-점--해결책">우려되는 점 &amp; 해결책</h3>
<ol>
<li>우선순위가 꼬일 가능성
문제: 프로젝트에서 어떤 폴더를 기준으로 관리해야 할지 헷갈릴 수 있음.</li>
</ol>
<p>-&gt; app/을 Next.js의 루트로 유지하되, FSD 패턴을 src/ 내부에서 유지하여 혼동을 방지.</p>
<ol start="2">
<li>라우팅이 꼬일 가능성
문제: Next.js의 App Router와 FSD의 pages/ 구조가 섞이면서 라우팅 우선순위 문제가 발생할 수 있음.</li>
</ol>
<p>-&gt; app/ 라우터 내부에서 FSD 패턴을 적용하고,
pages/ 폴더를 루트가 아닌 src/ 내부에 배치하여 충돌을 방지.</p>
<h4 id="위의-이유로-이번-마이그레이션에서는-fsd패턴을-적용하기로-하였습니다">위의 이유로 이번 마이그레이션에서는 FSD패턴을 적용하기로 하였습니다.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[Error: invariant expected app router to be mounted [Route 컴포넌트 개발하기, tailwind 적용] ( with. Story Book )]]></title>
            <link>https://velog.io/@ujinsimss_/Route-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-tailwind-%EC%A0%81%EC%9A%A9-with.-Story-Book</link>
            <guid>https://velog.io/@ujinsimss_/Route-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-tailwind-%EC%A0%81%EC%9A%A9-with.-Story-Book</guid>
            <pubDate>Sun, 09 Feb 2025 07:19:00 GMT</pubDate>
            <description><![CDATA[<p>뒤로가기 소제목 컴포넌트 개발을 하려는 중이다 </p>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/4cfc5109-7b0b-4779-a398-c332bc79ef67/image.png" alt="">
next 앱라우팅 환경이고 useRouter()를 이용한 동적라우팅 방식을 사용하였다.</p>
<p>개발 후 스토리북에 업로드 하려하자 이런 오류가 난다. 🤮</p>
<h2 id="error-invariant-expected-app-router-to-be-mounted">Error: invariant expected app router to be mounted</h2>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/92805437-bde3-47c4-8313-4530c822e576/image.png" alt=""></p>
<h3 id="공식문서에서-제시한-해결책을-알아보자">공식문서에서 제시한 해결책을 알아보자</h3>
<p><a href="https://storybook.js.org/docs/get-started/frameworks/nextjs">https://storybook.js.org/docs/get-started/frameworks/nextjs</a></p>
<blockquote>
<p>Next.js 프로젝트가 app모든 페이지에 디렉토리를 사용하는 경우(즉, 디렉토리가 없는 경우 ), 파일에서 매개 pages변수를 설정하여 모든 스토리에 적용할 수 있습니다. nextjs.appDirectorytrue.storybook/preview.js|ts
<img src="https://velog.velcdn.com/images/ujinsimss_/post/ae671731-6991-475a-b2e7-477d0cb6b437/image.png" alt=""></p>
</blockquote>
<h4 id="sol-⭐️-appdirectorytrue-를-추가하니-해결됐다"><strong>Sol ⭐️ appDirectory:true 를 추가하니 해결됐다.</strong></h4>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/130e1794-32ee-4c05-be97-af68e3574094/image.png" alt=""></p>
<h3 id="storybook에-tailwind가-적용되지-않은-모습이다-">*<em>Storybook에 tailwind가 적용되지 않은 모습이다. *</em></h3>
<h4 id="sol-⭐️-storybookpreviewts에-밑에-해당-코드를-작성-하면-끝이다-">Sol ⭐️ storybook/preview.ts에 밑에 해당 코드를 작성 하면 끝이다 !!!</h4>
<pre><code>import &#39;../src/app/style/globals.css&#39;;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 타입확장]]></title>
            <link>https://velog.io/@ujinsimss_/Type-Script-%ED%83%80%EC%9E%85%ED%99%95%EC%9E%A5</link>
            <guid>https://velog.io/@ujinsimss_/Type-Script-%ED%83%80%EC%9E%85%ED%99%95%EC%9E%A5</guid>
            <pubDate>Tue, 17 Dec 2024 07:52:01 GMT</pubDate>
            <description><![CDATA[<h1 id="타입확장">타입확장</h1>
<blockquote>
<p>타입 확장은 기존 타입을 사용해서 새로운 타입을 정의하는 것을 말한다. 기본적으로 타입스크립트에서는<code>interface</code>, <code>type</code> 키워드를 사용해서 타입을 정의한다. 뿐만 아니라 타입을 받아오는 <code>제네릭</code>과 <code>클래스</code>에서도 타입을 확장하거나 제한할 수 있다</p>
</blockquote>
<h2 id="1-타입type을-확장하거나-제한하는-경우">1. 타입(Type)을 확장하거나 제한하는 경우</h2>
<blockquote>
<p>TypeScript의 type 키워드를 사용하여 정의된 고정된 타입입니다.</p>
</blockquote>
<p>확장 혹은 제한 방식 : <strong>유니온 타입(Union Type)</strong>, <strong>인터섹션 타입(Intersection Type)</strong>, <strong>타입 별칭(Type Alias)</strong> 등을 사용</p>
<h3 id="언제-타입을-사용해야-하나">언제 타입을 사용해야 하나?</h3>
<ul>
<li><strong>고정된 타입 조합</strong>을 만들고 싶을 때 (유니온, 인터섹션).</li>
<li><strong>단순히 속성을 추가하거나 제거</strong>하여 새로운 타입을 만들고 싶을 때.</li>
<li><strong>타입 유틸리티</strong> (<code>Partial</code>, <code>Pick</code>, <code>Omit</code>, <code>Exclude</code> 등)로 타입을 조작하고 싶을 때.</li>
</ul>
<pre><code class="language-tsx">type Animal = {
  name: string;
};

type Dog = Animal &amp; { breed: string }; // 인터섹션 타입으로 Animal을 확장
type Pet = Animal | Dog; // 유니온 타입으로 Animal과 Dog를 조합

// 타입 제한: 특정 속성 제거
type Person = {
  name: string;
  age: number;
  location: string;
};

type PersonWithoutLocation = Omit&lt;Person, &#39;location&#39;&gt;; // location 속성 제거</code></pre>
<h2 id="2-인터페이스interface를-확장하거나-제한하는-경우">2. 인터페이스(Interface)를 확장하거나 제한하는 경우</h2>
<blockquote>
<p>인터페이스는 TypeScript의 컨텍스트 내에서만 존재하는 가상 구조입니다. TypeScript 컴파일러는 <strong>타입 체크 목적으로만 인터페이스를 사용합니다.</strong></p>
</blockquote>
<ul>
<li><strong>인터페이스의 확장방식</strong> : 상속 , 선언적 확장</li>
</ul>
<blockquote>
<p><strong>✚  인터페이스는 추상클래스</strong>로 사용됩니다. 메소드의 형태만 선언해서 인터페이스를 정의하고, 이후에 클래스를 정의할 때 implements 키워드를 사용하면서 이 인터페이스를 지정하면, 이 클래스는 추상함수로 선언된 메소드를 받아들여야 합니다.</p>
</blockquote>
<h3 id="언제-인터페이스를-사용해야-하나">언제 인터페이스를 사용해야 하나?</h3>
<ul>
<li><strong>객체의 구조를 정의</strong>하고 이를 확장해야 할 때.</li>
<li><strong>확장 가능성이 있는 타입</strong>을 정의할 때, 특히 외부 모듈과 협업할때 주로 사용</li>
<li><strong>클래스와 함께 사용</strong>하여 구조를 공유하거나 상속 구조를 만들 때.</li>
</ul>
<pre><code class="language-tsx">interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

const myDog: Dog = {
  name: &quot;Buddy&quot;,
  breed: &quot;Golden Retriever&quot;,
};

// 인터페이스 확장
interface Person {
  name: string;
  age: number;
}

// 선언적 확장: 기존 인터페이스에 속성 추가
interface Person {
  location: string;
}

const person: Person = {
  name: &quot;Alice&quot;,
  age: 30,
  location: &quot;New York&quot;,
};
</code></pre>
<h2 id="3-제네릭generic을-사용하여-확장하거나-제한하는-경우">3. 제네릭(Generic)을 사용하여 확장하거나 제한하는 경우</h2>
<blockquote>
<p>제네릭은 타입을 파라미터화하여 <strong>유연하게 여러 타입을 처리</strong>할 수 있도록 합니다.</p>
</blockquote>
<ul>
<li>제네릭의 타입 확장과 제한 :<code>extends</code> 키워드</li>
</ul>
<h3 id="언제-제네릭을-사용">언제 제네릭을 사용?</h3>
<ul>
<li><strong>다양한 타입을 처리해야 할 때</strong>: 함수, 클래스, 인터페이스 등에서 다형성 제공.</li>
<li><strong>타입을 명확히 제약하거나 확장</strong>해야 할 때 (<code>extends</code>, <code>T extends keyof U</code> 등).</li>
<li><strong>반환 타입과 입력 타입이 <code>동적으로 결정</code></strong>되어야 할 때.</li>
</ul>
<pre><code class="language-tsx">// 제네릭으로 타입을 제한
function getValue&lt;T extends { name: string }&gt;(obj: T): string {
  return obj.name;
}

getValue({ name: &quot;Alice&quot; }); // 정상
// getValue({ age: 30 }); // 오류: &#39;name&#39; 속성이 없기 때문

// 제네릭을 사용한 인터페이스
interface Box&lt;T&gt; {
  value: T;
}

const numberBox: Box&lt;number&gt; = { value: 123 };
const stringBox: Box&lt;string&gt; = { value: &quot;Hello&quot; };</code></pre>
<h2 id="4-클래스-상속-extends을-통한-타입-확장">4. 클래스 상속 (extends)을 통한 타입 확장</h2>
<blockquote>
<p>클래스 상속은 새로운 클래스를 정의할 때 기존 클래스의 속성과 메서드를 재사용하여 확장하는 가장 기본적인 방법</p>
</blockquote>
<h3 id="사용-상황">사용 상황:</h3>
<ul>
<li>기본 클래스의 기능을 확장하거나, 기본 기능을 유지하면서 새로운 기능을 추가해야 할 때.</li>
<li>여러 클래스 간의 공통 기능을 부모 클래스로 추출하여 재사용할 때.</li>
</ul>
<pre><code class="language-tsx">interface Movable {
  move(): void;
}

interface Barkable {
  bark(): void;
}

class Dog implements Movable,Barkable {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  move() {
    console.log(`${this.name} is moving.`);
  }

  bark() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog(&#39;Buddy&#39;);
dog.move(); // Buddy is moving.
dog.bark(); // Buddy barks.
</code></pre>
<h3 id="추상-클래스-abstract를-통한-타입-확장과-제한">추상 클래스 (abstract)를 통한 타입 확장과 제한</h3>
<blockquote>
<p>추상 클래스는 공통된 기능을 가진 클래스들의 기본 형태를 정의하고, 구체적인 구현은 서브클래스에서 하도록 강제하는 방식입니다.</p>
</blockquote>
<h3 id="사용-상황-1">사용 상황:</h3>
<ul>
<li>기본적인 메서드와 속성을 제공하면서, 구체적인 구현은 자식 클래스에 맡기고 싶을 때.</li>
<li>인스턴스화를 허용하지 않으면서, 상속을 <code>통해 공통 기능을 공유</code>해야 할 때.</li>
</ul>
<pre><code class="language-tsx">abstract class Animal {
  abstract makeSound(): void;

  move() {
    console.log(&#39;Moving...&#39;);
  }
}

class Dog extends Animal {
  makeSound() {
    console.log(&#39;Woof!&#39;);
  }
}

const dog = new Dog();
dog.makeSound(); // Woof!
dog.move(); // Moving...</code></pre>
<h2 id="상황으로-생각하기">상황으로 생각하기</h2>
<h3 id="상황">상황</h3>
<p>도서관에서 책을 분류하고 관리하는 시스템을 설계함. </p>
<pre><code class="language-tsx">📚 장르 : 문학, 과학, 역사, 미술 
💝 사은품 종류 : 책갈피, 포스터, 스티커 
💰 결재 상태 : 결재 됨, 안됨</code></pre>
<p><strong>[타입 영역]</strong> </p>
<p>도서관의 책은 다양한 <strong>장르</strong>가 있으며 책마다 <strong>사은품</strong>이 있을 수도 있고 없을 수도 있다</p>
<p><strong>[처리 영역]</strong> </p>
<p>사은품이 있는 경우→ <strong>결제 시 계산 포스기에서 사은품을 선택할 수 있는 옵션</strong>이 나타나야 한다</p>
<p>계산이 완료된 책→ <strong>계산 완료된 상품</strong>으로 표시되어야 하며, 이 정보는 포스 시스템에 반영</p>
<p>사용자는 결제 방법(카드, 현금 등)을 선택할 수 있으며, 결제 완료 후 사은품에 존재 여부에 따라 추가적인 결제 흐름과 마지막엔 가격이 나온다</p>
<p><strong>타입 영역</strong> </p>
<p>장르와 사은품 결재 상태는 특정형태로 유지되어서 타입을 사용</p>
<pre><code class="language-tsx">// 책의 장르를 정의
type Genre = &#39;문학&#39; | &#39;과학&#39; | &#39;역사&#39; | &#39;미술&#39;;

// 사은품 종류를 정의
type GiftType = &#39;책갈피&#39; | &#39;포스터&#39; | &#39;스티커&#39;;

// 결제 상태를 정의
type PaymentStatus = &#39;paid&#39; | &#39;unpaid&#39;;
</code></pre>
<p><strong>인터페이스(Interface) 영역</strong></p>
<p>인터페이스는 책의 구조를 정의하며, 사은품의 유무와 결제 상태와 같은 속성을 추가로 확장하여 사용할 수 있다. </p>
<pre><code class="language-tsx">// 책의 구조 정의 - 사은품과 결제 상태를 포함하여 타입 제한
interface Book {
  title: string;
  genre: Genre;
  price: number;
  gift?: GiftType; // 사은품은 있을 수도, 없을 수도 있음
  status: PaymentStatus;
}

// 결제 처리와 관련된 인터페이스
interface Payable {
  calculatePrice(): number;
  showPaymentOptions(): void;
}</code></pre>
<p><strong>제네릭 클래스 영역</strong> </p>
<p>제네릭을 사용하여 사은품의 유무와 결제 여부를 처리한다. : 제네릭을 통해 다양한 결제 상태와 사은품 여부를 쉽게 확장하거나 조합</p>
<p>추상 클래스는 기본적으로 책과 관련된 메서드를 정의하며, 실제 계산 로직은 서브클래스에서 구현</p>
<pre><code class="language-tsx">// 추상 클래스 정의 - 결제 로직
// payable + Book타입을 확장하는 T
abstract class LibraryItem&lt;T extends Book&gt; implements Payable {
  protected book: T;

  constructor(book: T) {
    this.book = book;
  }

  // 입장 조건: 모든 서브클래스는 이 메서드를 구현해야 함
  abstract calculatePrice(): number;

  showPaymentOptions(): void {
    console.log(&#39;카드와 신용카드로 결제가 가능합니다 &#39;);
  }

  showPaidStatus() {
    if (this.book.status === &#39;paid&#39;) {
      console.log(&#39;결제가 이미 완료됨&#39;);
    } else {
      console.log(&#39;결제 아직 안됨&#39;);
    }
  }
}

// 제네릭 클래스 - 사은품과 결제 여부에 따른 처리
class ProcessBook&lt;T extends Book&gt; extends LibraryItem&lt;T&gt; {
  // 입장 조건: Book 타입을 확장하는 T 타입을 처리
  calculatePrice(): number {
    if (this.book.gift) {  //추상클래스 사용
      console.log(`사은품포함 : ${this.book.gift}`);
      return this.book.price + 5; // 사은품이 있을 경우 추가 비용
    }
    return this.book.price;
  }//사은품에 따른 가격 수정

  displayBookDetails() {
    console.log(`Title: ${this.book.title}, Genre: ${this.book.genre}`);
    this.showPaidStatus();
  }
}

// 특정 책 인스턴스 생성 및 처리
const myBook: Book = {
  title: &#39;노인과바다&#39;,
  genre: &#39;문학&#39;,
  price: 20,
  gift: &#39;책갈피&#39;,
  status: &#39;paid&#39;,
};

//LibraryItem의 생성자 호출: ProcessBook 클래스는 LibraryItem 추상 클래스를 상속받아
//ProcessBook의 생성자는 먼저 LibraryItem의 생성자를 호출하여 this.book을 초기화
//LibraryItem의 생성자에서 전달된 myBook -&gt; this.book에 할당. 
const processedBook = new ProcessBook(myBook);
processedBook.displayBookDetails(); // 책 정보 및 결제 상태 표시
console.log(`Total price: ${processedBook.calculatePrice()}원`); // 총 금액 계산
</code></pre>
<h2 id="🤔-퀴즈">🤔 퀴즈</h2>
<p><strong>타입(Type) 확장 방식</strong>으로 사용되는 것 중 맞는 것은?</p>
<ol>
<li>extends 키워드</li>
<li>유니온 타입</li>
<li>추상 클래스 </li>
</ol>
<ul>
<li>정답<ol>
<li>유니온 타입</li>
</ol>
</li>
</ul>
<p><strong>제네릭(Generic)의 타입에서</strong> <code>extends</code> 키워드의 역할은 무엇인가요?</p>
<ul>
<li><p>정답</p>
<p>  제네릭 타입의 범위를 제한하거나 특정 조건을 부과합니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 타입 공간과 값 공간의 심벌 구분하기]]></title>
            <link>https://velog.io/@ujinsimss_/TypeScript-%ED%83%80%EC%9E%85-%EA%B3%B5%EA%B0%84%EA%B3%BC-%EA%B0%92-%EA%B3%B5%EA%B0%84%EC%9D%98-%EC%8B%AC%EB%B2%8C-%EA%B5%AC%EB%B6%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ujinsimss_/TypeScript-%ED%83%80%EC%9E%85-%EA%B3%B5%EA%B0%84%EA%B3%BC-%EA%B0%92-%EA%B3%B5%EA%B0%84%EC%9D%98-%EC%8B%AC%EB%B2%8C-%EA%B5%AC%EB%B6%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 17 Dec 2024 07:37:19 GMT</pubDate>
            <description><![CDATA[<h3 id="typescript에서-타입과-값의-구분">TypeScript에서 타입과 값의 구분</h3>
<ol>
<li><p><strong>타입 공간</strong></p>
<ul>
<li><p>타입 공간의 심벌은 타입을 정의하고 참조할 때 사용됩니다. 이 공간에서 심벌은 타입을 의미하며, <strong>컴파일 시점에 타입 검사를 위해 사용</strong></p>
<p>  <code>number</code>, <code>string</code>, <code>boolean</code>, <code>User</code> 등</p>
</li>
</ul>
</li>
<li><p><strong>값 공간</strong> </p>
<ul>
<li><p>값 공간의 심벌은 실제 런타임에 사용되는 값입니다. 이 공간의 심벌은 코드가 실행될 때 <strong>실제 메모리에서 사용</strong></p>
<p>   숫자 <code>5</code>, 문자열 <code>&quot;hello&quot;</code>, 객체 <code>{ name: &quot;Alice&quot; }</code> 등</p>
</li>
</ul>
</li>
</ol>
<p> <strong>타입과 값의 구분 코드</strong></p>
<pre><code class="language-tsx">// 타입 공간의 심벌
type User = {
  name: string;
  age: number;
};

// 값 공간의 심벌
const user: User = {
  name: &quot;youjin&quot;,
  age: 21,
};

// 타입 공간의 심벌 사용 예시
function greet(user: User): string {
  return `Hello, ${user.name}`;
}

// 값 공간의 심벌 사용 예시
console.log(greet(user)); // 출력: Hello, youjin
</code></pre>
<p><code>User</code>는 타입 공간에서 사용되며, 데이터의 구조를 정의한다</p>
<p><code>user</code>는 값 공간에서 사용 되는 데이터이다</p>
<p><strong>타입과 값의 구분 코드</strong></p>
<pre><code class="language-tsx">// 타입 공간의 심벌
type User = {
  name: string;
  age: number;
};

// 값 공간의 심벌
const user: User = {
  name: &quot;youjin&quot;,
  age: 21,
};

// 타입 공간의 심벌 사용 예시
function greet(user: User): string {
  return `Hello, ${user.name}`;
}

// 값 공간의 심벌 사용 예시
console.log(greet(user)); // 출력: Hello, youjin
</code></pre>
<p><code>User</code>는 타입 공간에서 사용되며, 데이터의 구조를 정의한다</p>
<p><code>user</code>는 값 공간에서 사용 되는 데이터이다</p>
<p><strong>타입과 값의 구분을 위한 규칙</strong></p>
<pre><code class="language-tsx">type T1 = &#39;string literal&#39;;
type T2 = 123;

const V1 = &#39;string literal&#39;;
const V2 = 123;</code></pre>
<p><code>T1</code>과 <code>T2</code>는 타입을 정의하고, </p>
<p><code>V1</code>과 <code>V2</code>는 실제 값을 저장하는 심벌입니다</p>
<p><strong>typeof 연산자의 타입 공간과 값 공간에서의 사용 차이</strong></p>
<pre><code class="language-tsx">type T1 = typeof V1; // 타입: &#39;string&#39;
type T2 = typeof V2; // 타입: &#39;number&#39;

const v1 = typeof V1; // 값: &quot;string&quot;
const v2 = typeof V2; // 값: &quot;number&quot;</code></pre>
<ul>
<li><code>type T1 = typeof V1;</code>는 타입 공간에서의 사용으로, V1의 타입을 참조합니다.</li>
<li><code>const v1 = typeof V1;</code>는 값 공간에서의 사용으로, V1의 실제 타입을 문자열로 반환합니다.</li>
</ul>
<h3 id="클래스의-타입과-값-구분">클래스의 타입과 값 구분</h3>
<ol>
<li><p><strong>타입 공간에서의 클래스</strong></p>
<ul>
<li><p><strong>타입으로서의 클래스</strong>는 해당 클래스의 인스턴스 타입을 정의합니다. 즉, 클래스를 타입처럼 사용하여 변수의 타입을 지정하거나, 제네릭 타입 제약조건으로 사용할 수 있습니다.</p>
<p>타입 선언, 함수 파라미터의 타입으로 사용.</p>
</li>
</ul>
</li>
<li><p><strong>값 공간에서의 클래스</strong></p>
<ul>
<li><p><strong>값으로서의 클래스</strong>는 실제로 런타임 시점에 존재하는 생성자 함수로, 객체를 생성하거나 <code>instanceof</code> 연산을 통해 인스턴스인지 검사할 때 사용됩니다.</p>
<p><code>new</code> 키워드를 사용해 객체 생성, <code>instanceof</code>로 타입 검사.</p>
</li>
</ul>
</li>
</ol>
<pre><code class="language-tsx">class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

// 타입 공간에서 사용
let personType: Person; // 타입으로 사용: Person 클래스의 인스턴스를 기대함

// 값 공간에서 사용
const personValue = new Person(&quot;Name&quot;); // 값으로 사용: 객체 생성
personValue.greet(); // 출력: Hello, my name is Name
console.log(personValue instanceof Person); // true</code></pre>
<p><strong>타입 공간 (let personType: Person)</strong>: Person은 타입으로 사용되어 personType 변수는 Person 타입의 인스턴스만 가짐</p>
<p><strong>값 공간 (new Person(&quot;Name&quot;))</strong>: Person은 생성자로 사용되어 새로운 객체를 만든다</p>
<pre><code class="language-tsx">// 기본 클래스 Person 정의
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

// Person을 확장하는 Age 클래스
class Age extends Person {
  age: number;
  constructor(name: string, age: number) {
    super(name);
    this.age = age;
  }

  greetWithAge() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

// 타입 공간에서의 사용 예시
let personType: Person; // Person 타입의 인스턴스를 기대
let ageType: Age; // Age 타입의 인스턴스를 기대

// personType은 Person의 인스턴스를 참조
personType = new Person(&quot;심유진&quot;);
personType.greet(); // 출력: Hello, my name is 심유진

// ageType은 Age의 인스턴스를 참조
ageType = new Age(&quot;심슨&quot;, 25);
ageType.greetWithAge(); // 출력: Hello, my name is 심슨 and I am 25 years old.

// personType에 Age 인스턴스를 할당하는 것도 가능 (Age는 Person을 확장하므로)
personType = new Age(&quot;영희&quot;, 30);
personType.greet(); // 출력: Hello, my name is 영희

// 합집합 타입 사용
type PersonOrAge = Person | Age;

let personOrAge: PersonOrAge;

// Person 타입 할당
personOrAge = new Person(&quot;Alice&quot;);
personOrAge.greet(); // 출력: Hello, my name is Alice

// Age 타입 할당
personOrAge = new Age(&quot;Bob&quot;, 40);
if (personOrAge instanceof Age) {
  personOrAge.greetWithAge(); // 출력: Hello, my name is Bob and I am 40 years old.
} else {
  personOrAge.greet(); // 해당 부분은 Age가 아닌 Person 인스턴스인 경우에만 실행
}

// 값 공간에서의 사용
const personValue = new Person(&quot;Tom&quot;); // 값으로 사용: 객체 생성
personValue.greet(); // 출력: Hello, my name is Tom
console.log(personValue instanceof Person); // true

const ageValue = new Age(&quot;Jenny&quot;, 28); // Age 객체 생성
ageValue.greetWithAge(); // 출력: Hello, my name is Jenny and I am 28 years old.
console.log(ageValue instanceof Age); // true
console.log(ageValue instanceof Person); // true (Age는 Person을 확장했기 때문에)
</code></pre>
<h3 id="두-공간타입-값에서-다른-의미를-가지는-코드-패턴">두 공간(타입, 값)에서 다른 의미를 가지는 코드 패턴</h3>
<h3 id="1-this-키워드">1. <strong>this 키워드</strong></h3>
<ul>
<li><strong>값으로 사용</strong>: 현재 객체를 참조</li>
<li><strong>타입으로 사용</strong>: 현재 클래스 타입을 참조하며, 다형성(polymorphic this)으로 사용된다</li>
</ul>
<pre><code class="language-jsx">class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  // 값으로 사용: 현재 객체를 참조
  setName(name: string): this {
    this.name = name; // this는 현재 객체를 참조
    return this;
  }
}

class Dog extends Animal {
  breed: string;

  constructor(name: string, breed: string) {
    super(name);
    this.breed = breed;
  }

  // 타입으로 사용: this 타입을 반환하여 메서드 체인 
  setBreed(breed: string): this {
    this.breed = breed;
    return this;
  }
}

const myDog = new Dog(&quot;Rex&quot;, &quot;Labrador&quot;)
  .setName(&quot;Buddy&quot;) // 메서드 체인
  .setBreed(&quot;Golden Retriever&quot;);

console.log(myDog); // Dog { name: &#39;Buddy&#39;, breed: &#39;Golden Retriever&#39; }

//setName 메서드를 호출하면 Animal 타입이 아닌 Dog 타입의 인스턴스가 반환
//때문에 setName 메서드 호출 후에 setBreed 메서드를 호출 가능</code></pre>
<h3 id="2-와--연산자">2. <strong>&amp;와 | 연산자</strong></h3>
<ul>
<li><strong>값으로 사용 :</strong> 비트 연산자로 AND와 OR 연산을 수행한다</li>
<li><strong>타입으로 사용 :</strong> 타입 시스템에서 인터섹션(&amp;)과 유니온(|) 타입을 정의한다</li>
</ul>
<pre><code class="language-jsx">// 값으로 사용: 비트 연산
const bitwiseAnd = 5 &amp; 3; // 0101 &amp; 0011 = 0001 (1)
const bitwiseOr = 5 | 3;  // 0101 | 0011 = 0111 (7)
console.log(bitwiseAnd, bitwiseOr); // 출력: 1 7

// 타입으로 사용: 인터섹션(&amp;)과 유니온(|)
type Cat = { purrs: boolean };
type Dog = { barks: boolean };
type Pet = Cat &amp; Dog; // 인터섹션 타입: 두 타입의 모든 속성을 가짐

let myPet: Pet = { purrs: true, barks: true }; // Cat과 Dog의 속성을 모두 가짐

type AnimalType = Cat | Dog; // 유니온 타입: Cat 또는 Dog 중 하나의 속성을 가짐

let anotherPet: AnimalType = { purrs: false }; // Cat 타입으로 사용 가능</code></pre>
<h3 id="3-const">3. <strong>const</strong></h3>
<ul>
<li><strong>값으로 사용</strong>: 변수를 선언할 때 사용하며, 값은 변경되지 xx</li>
<li><strong>타입으로 사용 (as const)</strong>: 리터럴 값의 타입을 정확하게 고정시키거나 추론된 타입을 변경</li>
</ul>
<pre><code class="language-jsx">// 값으로 사용: 변경 불가능한 변수 선언
const MAX_COUNT = 100;
// MAX_COUNT = 200; // 오류: 상수는 재할당할 수 없음

// 타입으로 사용: 리터럴 값을 고정하여 추론
const pet = {
  type: &quot;cat&quot;,
  color: &quot;black&quot;,
} as const; // pet의 타입이 { type: &quot;cat&quot;; color: &quot;black&quot;; }으로 고정됨

function describePet(pet: { type: &quot;cat&quot; | &quot;dog&quot;; color: string }) {
  console.log(`This ${pet.type} is ${pet.color}.`);
}

describePet(pet); // &quot;This cat is black.&quot;</code></pre>
<h3 id="4-extends">4. <strong>extends</strong></h3>
<ul>
<li><strong>값으로 사용 (class A extends B)</strong>: 클래스 상속을 통해 B 클래스의 기능을 A 클래스에 확장</li>
<li><strong>타입으로 사용 (interface A extends B)</strong>: 인터페이스 상속을 통해 A 인터페이스가 B의 타입을 확장</li>
<li><strong>제네릭 타입 한정자 (Generic<T extends number>)</strong>: 제네릭 타입에서 <code>T</code>가 특정 타입을 확장하도록 제한</li>
</ul>
<pre><code class="language-jsx">// 값으로 사용: 클래스 상속
class Vehicle {
  speed: number = 0;
  drive() {
    console.log(`Driving at ${this.speed} km/h`);
  }
}

class Car extends Vehicle {
  setSpeed(speed: number) {
    this.speed = speed;
  }
}

const myCar = new Car();
myCar.setSpeed(60);
myCar.drive(); // 출력: Driving at 60 km/h

// 타입으로 사용: 인터페이스 상속
interface Animal {
  legs: number;
}

interface Bird extends Animal {
  canFly: boolean;
}

const sparrow: Bird = { legs: 2, canFly: true };

// 제네릭 타입 한정자로 사용
function logLength&lt;T extends { length: number }&gt;(item: T) {
  console.log(item.length);
}

logLength([1, 2, 3]); // 출력: 3
</code></pre>
<h3 id="5-in">5. <strong>in</strong></h3>
<ul>
<li><strong>값으로 사용 (for (key in object))</strong>: 객체의 속성을 순회할 때 사용한다</li>
<li><strong>타입으로 사용 (매핑된 타입)</strong>: 타입 시스템에서 키의 집합을 기반으로 새로운 타입을 정의할 때 사용한다</li>
</ul>
<pre><code class="language-tsx">// in 연산자의 값과 타입 공간에서의 사용

// 값 공간에서의 사용: 객체 순회
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(key); // &quot;a&quot;, &quot;b&quot;, &quot;c&quot;
}

// 타입 공간에서의 사용: 매핑된 타입
type Keys = &#39;name&#39; | &#39;age&#39;; //&#39;name&#39; 또는 &#39;age&#39;의 문자열 리터럴 타입을 가짐
type User = { [K in Keys]: string }; // User 타입은 { name: string; age: string; }
const user: User = { name: &quot;Alice&quot;, age: &quot;30&quot; };
</code></pre>
<h3 id="퀴즈">퀴즈</h3>
<ol>
<li>다음 코드에서 타입 공간과 값 공간의 심벌을 구분해주세요 </li>
</ol>
<pre><code class="language-jsx">class Car {
  model: string;
  constructor(model: string) {
    this.model = model;
  }
}

type Vehicle = Car; //1

const myCar = new Car(&quot;Tesla&quot;); //2

console.log(myCar instanceof Car); //3</code></pre>
<ul>
<li><p>정답</p>
<p>  <strong>type Vehicle = Car;</strong>:</p>
<ul>
<li><p><strong>타입 공간</strong>: Vehicle은 type 키워드를 사용하여 Car 타입을 별칭으로 지정한 심벌입니다</p>
</li>
<li><p><em>const myCar = new Car(&quot;Tesla&quot;);*</em>:</p>
</li>
<li><p><strong>값 공간</strong>: myCar는 실제로 메모리에 할당되는 Car 클래스의 인스턴스입니다ㅁ</p>
</li>
<li><p><em>console.log(myCar instanceof Car);*</em></p>
</li>
<li><p><strong>값 공간</strong>: instanceof는 값 공간에서만 동작하는 연산자로, myCar가 Car의 인스턴스인지 검사합니다. 여기서 Car는 값 공간에서 생성자 함수로 사용됐습니다</p>
</li>
</ul>
</li>
</ul>
<ol start="2">
<li>instanceof가 타입을 검사하는지 값을 검사하는지 말해보세요 </li>
</ol>
<pre><code class="language-jsx">class Shape {
  sides: number;
  constructor(sides: number) {
    this.sides = sides;
  }
}

const square = new Shape(4);

console.log(square instanceof Shape); 
</code></pre>
<ul>
<li><p>정답</p>
<h3 id="값-공간에서의-검사"><strong>값 공간에서의 검사</strong>:</h3>
<p>  square instanceof Shape는 square가 Shape 클래스의 인스턴스인지 확인한다</p>
<p>  instanceof는 square 객체의 <strong>프로토타입 체인</strong>을 검사하여 Shape의 프로토타입이 포함되어 있는지 확인함</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Typescript] 캐치! 티니핑으로 알아보는 TS 구조적 타이핑     ]]></title>
            <link>https://velog.io/@ujinsimss_/%EC%BA%90%EC%B9%98-%ED%8B%B0%EB%8B%88%ED%95%91%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-TS%EA%B5%AC%EC%A1%B0%EC%A0%81-%ED%83%80%EC%9D%B4%ED%95%91-iwtlc8t3</link>
            <guid>https://velog.io/@ujinsimss_/%EC%BA%90%EC%B9%98-%ED%8B%B0%EB%8B%88%ED%95%91%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-TS%EA%B5%AC%EC%A1%B0%EC%A0%81-%ED%83%80%EC%9D%B4%ED%95%91-iwtlc8t3</guid>
            <pubDate>Wed, 18 Sep 2024 07:48:32 GMT</pubDate>
            <description><![CDATA[<p>구조적 타이핑 : 어 난 티니핑 멤버 아닌데.. </p>
<p><a href="https://toss.tech/article/typescript-type-compatibility%EA%B8%80%EA%B3%BC">https://toss.tech/article/typescript-type-compatibility글과</a> 
Effective 타입스크립트 책을 보고 정리 하였습니다 ! </p>
<hr>
<h2 id="덕-타이핑-duck-typing"><strong>덕 타이핑 (Duck Typing)</strong></h2>
<p>덕 타이핑은 &quot;만약 어떤 것이 오리처럼 걷고, 헤엄치고, 꽥꽥거린다면 그것은 오리일 것이다&quot;라는 철학에서 유래된 개념입니다. 즉, <strong>객체가 어떤 인터페이스나 타입을 명시적으로 구현하지 않더라도, 해당 객체가 요구되는 기능을 모두 구현하고 있다면 그 객체를 사용할 수 있다</strong>는 의미입니다.</p>
<p>타입스크립트에서도, 객체가 특정 속성과 메서드를 가지고 있다면 타입 선언 없이도 그 객체가 예상대로 동작할 수 있습니다. 이것이 타입스크립트의 <strong>구조적 서브타이핑</strong> 방식과 연결됩니다.</p>
<h2 id="핑-타이핑-ping-typing"><strong>핑 타이핑 (Ping Typing)</strong></h2>
<p>덕 타이핑에서 아이디어를 차용해, <strong>어떤 글자 뒤에 &quot;핑&quot;을 붙이면 그 글자가 &lt;&lt;캐치! 티니핑&gt;&gt;의 멤버가 된다는 발상</strong>을 농담처럼 표현해보았습니다</p>
<h2 id="만약-어떤-글자-뒤에-핑이라고-붙이면--캐치-티니핑의-멤버가-된다--→-핑-타이핑--">“만약 어떤 글자 뒤에 핑이라고 붙이면 &lt;&lt; 캐치! 티니핑&gt;&gt;의 멤버가 된다… ? → 핑 타이핑 ? “</h2>
<pre><code class="language-python">function isTiniPing(name: string): boolean {
  return name.endsWith(&#39;핑&#39;);
}

const name = &quot;하츄핑&quot;;
if (isTiniPing(name)) {
  console.log(`${name}은(는) 티니핑 멤버입니다!`);
}</code></pre>
<hr>
<p><strong>🤔 그렇다면 서브타이핑의 종류에는 어떤 것이 있을까요 ??</strong> </p>
<h2 id="서브타이핑의-종류">서브타이핑의 종류</h2>
<p>타입의 계충 구조에서 한 타입이 다른 타입의 부분 집합일 때 발생하는 타입 관계</p>
<h3 id="1-명목적-서브타이핑nominal-subtyping-→-java-c">1. 명목적 서브타이핑(nominal subtyping) → java, C#</h3>
<blockquote>
<p>타입 정의 시에 상속 관계임을 명확히 명시한 경우에만 타입 호환을 허용하는 것입니다.</p>
</blockquote>
<ul>
<li>개발자의 명확한 의도를 반영할 수 있다</li>
<li>오류 발생할 가능성 배제</li>
</ul>
<h3 id="2-구조적-서브타이핑structural-subtpying-→-ts">2. 구조적 서브타이핑(structural subtpying) → TS</h3>
<blockquote>
<p>상속 관계가 명시되어 있지 않더라도 객체의 프로퍼티를 기반으로 사용처에서 사용함에 문제가 없다면 타입 호환을 허용하는 방식</p>
</blockquote>
<ul>
<li>객체의 프로퍼티를 체크해주는 과정 수행</li>
<li>상속관계를 명시해줄 필요가 없음</li>
</ul>
<hr>
<p><strong>🤔 타입스크립트에서 사용하는 구조적 타이핑의 예시를 알아보겠습니다</strong> </p>
<h2 id="구조적-타이핑의-예시">구조적 타이핑의 예시</h2>
<h3 id="-봉인된-타입과-정확한-타입">: 봉인된 타입과 정확한 타입</h3>
<p>자바스크립트와 타입스크립트는 기본적으로 타입 시스템이 <strong>열려있는(open)</strong> 상태</p>
<p>즉, 객체가 타입에 명시된 속성 외에도 추가적인 속성을 가질 수 있으며, 타입 체크는 해당 속성들만 맞는지를 확인합니다</p>
<ul>
<li><strong>해당 속성들만 맞는지만 확인</strong></li>
</ul>
<pre><code class="language-jsx">interface Vector2D {
  x: number;
  y: number;
}

function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

interface NamedVector {
  name: string;
  x: number;
  y: number;
}

//calculateLength 함수에 NamedVector 타입의 객체 v를 전달할 수 있다.
// 왜냐하면, v가 Vector2D에서 요구하는 x와 y 속성을 가지고 있음.
const v: NamedVector = { x: 3, y: 4, name: &quot;zee&quot; };
console.log(calculateLength(v)); // 5
</code></pre>
<ul>
<li><strong>객체가 2개의 속성밖에 못가지지만 , 3개의 속성을 추가하여 무시됨</strong></li>
</ul>
<pre><code class="language-jsx">interface Vector2D {
  x: number;
  y: number;
}

const vector = { x: 3, y: 4, z: 5 }; // z는 추가적인 속성

function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

console.log(calculateLength(vector)); // 5, z는 무시됨
</code></pre>
<ul>
<li><strong>객체가 여러 속성을 가질 수 있지만, 필요한 속성만 이용함</strong></li>
</ul>
<pre><code class="language-jsx">interface Point3D {
  x: number;
  y: number;
  z: number;
}

function calculate2DLength(point: { x: number; y: number }) {
  return Math.sqrt(point.x * point.x + point.y * point.y);
}

const point3D: Point3D = { x: 5, y: 12, z: 7 };

console.log(calculate2DLength(point3D)); // 13
</code></pre>
<blockquote>
<p>type이름이 티니핑인데 속성이 눈코입이면 눈코입 속성을 가진 다른 캐릭터 또한 들어올 수 있게됩니다  때문에 type 속성에는 들어오지만</p>
</blockquote>
<ul>
<li>명확한 경우</li>
<li>애매한 경우</li>
</ul>
<p>가 공존할 수 있게 됩니다 👉 그래서 타입 구분을 통해 이를 해결할 수 있습니다</p>
<hr>
<h2 id="타입-구분-방법">타입 구분 방법</h2>
<h3 id="1-특정-경우에만-해당하는-변수를-타입으로-지정">1. 특정 경우에만 해당하는 변수를 타입으로 지정</h3>
<p>: 캐릭터 속성에서 티니핑 타입을 정의하여 구분할 수 있습니다 </p>
<pre><code class="language-tsx">// 기본 캐릭터 속성 정의
type Character = {
  strength: number;
  agility: number;
  intelligence: number;
};

// 티니핑 타입 정의 (Character에 추가 속성 포함)
type 티니핑 = Character &amp; {
  characterBrand: string;
};

// 티니핑 객체 예시
const character1: 티니핑 = {
  strength: 29,
  agility: 48,
  intelligence: 13,
  characterBrand: &#39;캐치! 티니핑&#39;
};

// 일반 캐릭터와 티니핑 캐릭터 구분
const character2: Character = {
  strength: 25,
  agility: 40,
  intelligence: 15
};

console.log(character1.characterBrand); // &quot;캐치! 티니핑&quot;</code></pre>
<p><strong>그렇다면 타입이 아닌 character2에만 가능한 characterBrand 프로퍼티 추가를 한다면 ?</strong> </p>
<pre><code class="language-jsx">const character2 = Character({
  strength: 12;
  agility: 32;
  intelligence: 434;
  characterBrand: &#39;캐치! 티니핑&#39;
})                                  /** 타임검사 결과 : 오류 있음 */

//다음처럼 함수에 들어온 인자가 fresh하면 해당 함수에서만 사용되고 다른 곳에서 사용되지 않기에 오류로 지정 </code></pre>
<p>TypeScript Type Checker는 구조적 서브타이핑을 기반으로 타입 호환을 판단하되, Freshness에 따라 예외를 둔다고 합니다.</p>
<h3 id="2-branded-type--프로퍼티-추가-">2. Branded type ( 프로퍼티 추가 )</h3>
<p>의도적으로 <code>__brand</code> 와 같은 프로퍼티를 추가시켜, 개발자가 함수의 매개변수로 정의한 타입 외에는 호환이 될 수 없도록 강제하는 기법</p>
<p>2-1 . 타입에 <strong>고유한 식별자</strong>를 부여하는 방식 </p>
<p>: 브랜트 타입에 브랜드 속성을 &#39;캐치! 티니핑’으로 부여하여 티니핑 타입을 정의할 수 있습니다</p>
<pre><code class="language-jsx">type Brand&lt;K, T&gt; = K &amp; { __brand: T };

// 티니핑 타입 정의
type 티니핑 = Brand&lt;{
  strength: number;
  agility: number;
  intelligence: number;
}, &#39;캐치! 티니핑&#39;&gt;;

// 티니핑의 능력치를 계산하는 함수
function calculatePower(character: 티니핑): number {
  return character.strength * 2 + character.agility * 1.5 + character.intelligence * 3;
}

// 정석적인 방법으로 브랜디드 타입 사용
const brandedCharacter2 = {
  strength: 29,
  agility: 48,
  intelligence: 13,
  __brand: &#39;캐치! 티니핑&#39; as const // __brand 속성 추가로 명확하게 타입 지정
};

console.log(calculatePower(brandedCharacter2)); // OK
</code></pre>
<p>2-2. 함수에서의 타입 제한</p>
<p>: 어떤 글자가 들어올 때 핑으로 끝나더라도 타입을 통해 티니핑 캐릭터인지 확인할 수 있습니다</p>
<pre><code class="language-jsx">// Character type 정의
type Character&lt;K, T&gt; = K &amp; { _Character: T };

// 티니핑 타입 정의
type 티니핑 = Character&lt;string, &#39;티니핑&#39;&gt;;

// 마지막 글자가 &#39;핑&#39;으로 끝나는지 체크하는 함수
function is티니핑(name: string): name is 티니핑 {
  return name.endsWith(&#39;핑&#39;);
}

// 티니핑 캐릭터만 허용하는 함수
//매개변수 character타입 외 제한
function introduceCharacter(character: 티니핑) {
  console.log(`안녕하세요, 저는 ${character}입니다!`);
}

// 일반 문자열과 티니핑 문자열 예시
const Character1 = &quot;구조적타이핑&quot;;
const Character2 = &quot;하츄핑&quot; as const;

// 타입 검사
if (is티니핑(tinipingCharacter)) {
  introduceCharacter(tinipingCharacter); // OK
} else {
  console.log(`${tinipingCharacter}는 티니핑이 아닙니다.`);
}

if (is티니핑(normalCharacter)) {
  introduceCharacter(normalCharacter); // 타입 오류 발생, normalCharacter는 티니핑이 아님
} else {
  console.log(`${normalCharacter}는 티니핑이 아닙니다.`);
}

//result 
안녕하세요, 저는 하츄핑입니다!
구조적타이핑는 티니핑이 아닙니다.</code></pre>
<h3 id="3-index-signature를-이용한-타입-호환성">3. Index Signature를 이용한 타입 호환성</h3>
<p>객체가 고정된 속성 외에도 임의의 속성을 가질 수 있음을 타입스크립트에게 알려주는 방법</p>
<p><strong>🤔 티니핑의 종류에 대해서 알아보자면..</strong></p>
<p>티니핑에서 가장 우수한 티니핑으로 로열 티니핑 6종이 있다고 합니다 </p>
<p>뿐만 아니라 <strong>레전드 티니핑</strong> , <strong>일반 티니핑</strong>,  <strong>빌런 티니핑</strong> 등 여러 종류가 있습니다 ! </p>
<p><a href="https://namu.wiki/w/%ED%8B%B0%EB%8B%88%ED%95%91#s-3.4">https://namu.wiki/w/티니핑#s-3.4</a></p>
<p><img src="https://velog.velcdn.com/images/ujinsimss_/post/768be6b7-f65d-4e48-b272-79b3ff7e250b/image.png" alt=""></p>
<p>해당 속성을 옵션 속성을 통해 추가로 알려줄 수 있습니다 </p>
<p>그럼 다음과 같이 기본 타입을 확장할 수 있습니다</p>
<pre><code class="language-jsx">// 기본 티니핑 타입 정의
interface 티니핑 {
  name: string;
  strength: number;
  agility: number;
  intelligence: number;
  [key: string]: number | string | boolean | undefined; // 추가적인 속성 허용
  type: string;  // 티니핑 종류 (일반, 레전드, 빌런 등)
}

// 레전드 티니핑 타입 정의 (티니핑을 확장)
interface 레전드티니핑 extends 티니핑 {
  legendaryPower: string;  // 레전드 티니핑만의 특별한 능력
}

// 빌런 티니핑 타입 정의 (티니핑을 확장)
interface 빌런티니핑 extends 티니핑 {
  evilPower: string;   // 빌런 티니핑만의 특별한 능력
  invertedTrianglePattern: boolean; 
  // 빌런 티니핑은 뿌뿌핑, 트러핑을 제외하면 눈밑에 역삼각형 무늬가 있음
}

// 일반 티니핑 타입 정의 (특별한 속성 없이 기본 티니핑 속성만 가짐)
type 일반티니핑 = 티니핑;

// 일반 티니핑 객체
const 일반TiniPing: 일반티니핑 = {
  name: &quot;키키핑&quot;,
  strength: 30,
  agility: 35,
  intelligence: 40,
  type: &quot;일반&quot;,
};

// 레전드 티니핑 객체
const 레전드TiniPing: 레전드티니핑 = {
  name: &quot;행운핑&quot;,
  strength: 80,
  agility: 70,
  intelligence: 90,
  type: &quot;레전드&quot;,
  legendaryPower: &quot;행운전달🍀&quot;, // 레전드 티니핑만의 특별한 능력
};

// 빌런 티니핑 객체
const 빌런TiniPing: 빌런티니핑 = {
  name: &quot;악동핑&quot;,
  strength: 60,
  agility: 55,
  intelligence: 65,
  type: &quot;빌런&quot;,
  evilPower: &quot;번개발사⚡️&quot;, // 빌런 티니핑만의 특별한 능력
  invertedTrianglePattern: true, // 역삼각형 무늬 여부
};

// 티니핑의 능력치를 계산하는 함수
function calculatePower(tiniPing: 티니핑): number {
  return tiniPing.strength * 2 + tiniPing.agility * 1.5 + tiniPing.intelligence * 3;
}

// 티니핑을 소개하는 함수
function introduceTiniPing(tiniPing: 티니핑) {
  console.log(`안녕하세요, 저는 ${tiniPing.name}입니다. 저는 ${tiniPing.type} 티니핑이에요!`);

  if (&#39;legendaryPower&#39; in tiniPing) {
    console.log(`저의 전설적인 능력은 ${tiniPing.legendaryPower}입니다!`);
  }

  if (&#39;evilPower&#39; in tiniPing) {
    console.log(`저의 악당 능력은 ${tiniPing.evilPower}입니다!`);
    if ((tiniPing as 빌런티니핑).invertedTrianglePattern) {
      console.log(&quot;저는 역삼각형 무늬를 가진 빌런 티니핑입니다!&quot;);
    }
  }

  console.log(`능력치 합계: ${calculatePower(tiniPing)}`);
}

// 티니핑 객체 출력
introduceTiniPing(일반TiniPing);   // 일반 티니핑 소개
introduceTiniPing(레전드TiniPing); // 레전드 티니핑 소개
introduceTiniPing(빌런TiniPing);   // 빌런 티니핑 소개

//strength: number; agility: number; intelligence: number;는 임의로 지정한 것 입니다 ! 
</code></pre>
<h2 id="정리-🧹"><strong>정리 🧹</strong></h2>
<table>
<thead>
<tr>
<th>방식</th>
<th>구분 방법</th>
<th>사용 목적</th>
</tr>
</thead>
<tbody><tr>
<td><strong>변수 타입 지정</strong></td>
<td>타입 이름</td>
<td>같은 구조의 데이터를 <strong>명확한 상황</strong>에 맞게 구분할 때 사용</td>
</tr>
<tr>
<td><strong>Branded Type</strong></td>
<td>고유한 식별자(__brand)</td>
<td>같은 구조라도 <strong>엄격한 타입 구분</strong>이 필요할 때 사용</td>
</tr>
<tr>
<td><strong>Index Signature</strong></td>
<td>속성의 타입만 제한</td>
<td>객체의 속성 이름을 <strong>동적으로 확장</strong>할 때 사용</td>
</tr>
</tbody></table>
<p><strong><em>📚 핑으로만 끝난다고 혹은 모든 티니핑이 일반 티니핑은 아니듯이 타입 구분의 필요성을 느꼈습니다!!</em></strong> </p>
<hr>
<h3 id="퀴즈-1-구조적-서브타이핑"><strong>퀴즈 1: 구조적 서브타이핑</strong></h3>
<p>다음 코드에서 <code>calculateLength</code> 함수에 <code>NamedVector</code> 타입의 객체를 전달할 수 있는 이유는 무엇일까요?</p>
<pre><code class="language-jsx">interface Vector2D {
  x: number;
  y: number;
}

function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

interface NamedVector {
  name: string;
  x: number;
  y: number;
}

const v: NamedVector = { name: &quot;zee&quot;, x: 3, y: 4 };
console.log(calculateLength(v)); // 결과: 5
</code></pre>
<ul>
<li><p>정답</p>
<p>  <code>Vector2D</code>와 <code>NamedVector</code>는 명시적으로 상속 관계에 있고</p>
<p>  <code>NamedVector</code>는 <code>Vector2D</code>에서 요구하는 모든 속성(<code>x</code>, <code>y</code>)을 가지고 있기 때문</p>
<p>  <code>TypeScript</code>는 항상 모든 객체를 호환 가능하게 처리하기 때문</p>
<p>  <code>NamedVector</code>의 구조가 더 복잡하므로 자동으로 호환됨</p>
</li>
</ul>
<h3 id="퀴즈-2-명목적-서브타이핑"><strong>퀴즈 2: 명목적 서브타이핑</strong></h3>
<p>다음 언어 중 <strong>명목적 서브타이핑</strong>(Nominal Subtyping)을 사용하는 언어는 무엇일까요?</p>
<ol>
<li>TypeScript</li>
<li>JavaScript</li>
<li>Java</li>
<li>Python</li>
</ol>
<ul>
<li>정답<ol>
<li>Java는 명목적 서브타이핑을 사용하는 언어로, 상속 관계가 명확히 명시된 경우에만 타입 호환을 허용합니다</li>
</ol>
</li>
</ul>
<h3 id="퀴즈-3-브랜딩된-타입"><strong>퀴즈 3: 브랜딩된 타입</strong></h3>
<p>다음 코드를 실행할 때 타입 오류가 발생하는 이유는 무엇일까요?</p>
<pre><code class="language-jsx">type Brand&lt;K, T&gt; = K &amp; { __brand: T };

type Food = Brand&lt;{
  protein: number;
  carbohydrates: number;
  fat: number;
}, &#39;Food&#39;&gt;;

function calcCalory(food: Food) {
  return food.protein * 4 + food.carbohydrates * 4 + food.fat * 9;
}

const burger = {
  protein: 100,
  carbohydrates: 100,
  fat: 100,
  burgerBrand: &#39;버거킹&#39;
};

calcCalory(burger); // 오류 발생
</code></pre>
<ul>
<li><p>정답</p>
<p>  <code>Food</code> 타입과 <code>burger</code> 타입이 구조적으로 일치하지 않기 때문에</p>
<ul>
<li><code>burger</code>에 <code>__brand: &#39;Food&#39;</code>가 없기 때문에</li>
<li><code>calcCalory</code> 함수는 매개변수로 <code>burgerBrand</code>를 허용하지 않기 때문에</li>
</ul>
</li>
</ul>
<h3 id="퀴즈-4-index-signature">퀴즈 4: Index Signature</h3>
<p><strong>FlexibleFood 인터페이스가 Food와 다른 점은 무엇일까요?</strong></p>
<pre><code class="language-jsx">interface Food {
  protein: number;
  carbohydrates: number;
  fat: number;
}

interface FlexibleFood {
  protein: number;
  carbohydrates: number;
  fat: number;
  [key: string]: number | string; // 추가 속성 허용
}</code></pre>
<ul>
<li><p>정답</p>
<p>  <code>Food</code>는 고정된 속성만 가질 수 있다</p>
<p>  <code>FlexibleFood</code>는 <code>Food</code>와 달리 추가적인 속성을 허용한다</p>
<p>  <code>FlexibleFood</code>는 타입 호환성 문제를 발생시킨다</p>
<p>  <code>FlexibleFood</code>는 <code>Food</code>를 상속한 타입이다</p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>