<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sj_yoon.log</title>
        <link>https://velog.io/</link>
        <description>하나씩 쌓아가는 재미</description>
        <lastBuildDate>Thu, 07 Aug 2025 04:45:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sj_yoon.log</title>
            <url>https://velog.velcdn.com/images/sj_yun/profile/a68829d1-0eb8-42ff-a613-5fe7bb3aaaac/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sj_yoon.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sj_yun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[타입 정의는 열심히 했는데 왜 유지보수는 점점 불편해질까?]]></title>
            <link>https://velog.io/@sj_yun/%ED%83%80%EC%9E%85-%EC%A0%95%EC%9D%98%EB%8A%94-%EC%97%B4%EC%8B%AC%ED%9E%88-%ED%96%88%EB%8A%94%EB%8D%B0-%EC%99%9C-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98%EB%8A%94-%EC%A0%90%EC%A0%90-%EB%B6%88%ED%8E%B8%ED%95%B4%EC%A7%88%EA%B9%8C</link>
            <guid>https://velog.io/@sj_yun/%ED%83%80%EC%9E%85-%EC%A0%95%EC%9D%98%EB%8A%94-%EC%97%B4%EC%8B%AC%ED%9E%88-%ED%96%88%EB%8A%94%EB%8D%B0-%EC%99%9C-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98%EB%8A%94-%EC%A0%90%EC%A0%90-%EB%B6%88%ED%8E%B8%ED%95%B4%EC%A7%88%EA%B9%8C</guid>
            <pubDate>Thu, 07 Aug 2025 04:45:45 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sj_yun/post/bd80f3c6-8568-4030-9a76-b3842519baa2/image.jpg" alt=""></p>
<p>요즘 프론트엔드 개발에서 타입스크립트는 선택이 아닌 기본값이죠 . 새로운 요구사항을 구현할 때마다 자연스럽게 <code>type</code>이나 <code>interface</code>를 정의하고, API 응답에 맞춰 타입을 매핑합니다.</p>
<p>하지만 프로젝트가 커지고 일정에 쫓기기 시작하면, 어느 순간부터 이상한 징후들이 생기기 시작합니다.</p>
<ul>
<li>분명 타입이 있는데도 타입 오류가 납니다.</li>
<li>똑같은 구조의 타입이 여기저기 흩어져 있습니다.</li>
<li>타입 하나 수정했을 뿐인데 여러 파일이 깨집니다.</li>
</ul>
<p>사실 원인은 명확합니다 . 처음엔 단순하게 출발했던 타입 정의가, <strong>모든 케이스를 한 타입으로 처리하려고 하면서 점점 무거워졌기 때문</strong>입니다.</p>
<p>이번 글에서는 코드 예제를 기반으로, <strong>확장 가능한 타입 설계 방식</strong>을 소개하려고 합니다 타입을 &quot;에러 안 나게&quot;가 아니라, <strong>명확하게 나누고 조합</strong>함으로써 유지보수성과 재사용성을 확보하는 방식을 정리했어요.</p>
<h2 id="📌-게시판-구성">📌 게시판 구성</h2>
<p>이해를 돕기 위해 게시판 구성을 위한 가상의 상황을 가정하며 설명하겠습니다. 
처음에는 간단하게 게시글을 보여주는 것부터 시작했죠.</p>
<h3 id="1주차-기본-게시글-목록-구현">1주차: 기본 게시글 목록 구현</h3>
<p>기본 게시글 API 스펙은 다음과 같았습니다:</p>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;첫 번째 게시글&quot;,
  &quot;body&quot;: &quot;게시글 내용입니다.&quot;,
  &quot;userId&quot;: 123
}
</code></pre>
<pre><code class="language-tsx">// types.ts
type Post = {
  id: number;
  title: string;
  body: string;
  userId: number;
};
</code></pre>
<p>요구사항에 맞춰 타입을 정의하고 게시글 목록 컴포넌트도 만들었습니다.</p>
<pre><code class="language-tsx">// PostList.tsx
export const PostList = ({ posts }: { posts: Post[] }) =&gt; {
  return (
    &lt;div&gt;
      {posts.map((post) =&gt; (
        &lt;div key={post.id}&gt;
          &lt;h2&gt;{post.title}&lt;/h2&gt;
          &lt;p&gt;{post.body}&lt;/p&gt;
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
};
</code></pre>
<p><strong>✅ 1주차 결과</strong>: 깔끔하게 구현 완료! 타입 오류도 없고 코드도 간결합니다.</p>
<h3 id="2주차-기획자의-새로운-요구사항">2주차: 기획자의 새로운 요구사항</h3>
<blockquote>
<p>&quot;각 게시글마다 댓글 개수를 보여주면 좋겠어요. 사용자들이 어떤 글이 인기 있는지 알 수 있도록요.&quot;</p>
</blockquote>
<p>요구사항 반영을 위해 게시글 목록 API에서 댓글 개수도 함께 내려주기로 했습니다</p>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;첫 번째 게시글&quot;,
  &quot;body&quot;: &quot;게시글 내용입니다.&quot;,
  &quot;userId&quot;: 123,
  &quot;commentsCount&quot;: 5
}
</code></pre>
<p>간단히 기존 <code>Post</code> 타입에 필드를 추가했습니다</p>
<pre><code class="language-tsx">// ❌ 단순한 해결책
type Post = {
  id: number;
  title: string;
  body: string;
  userId: number;
  commentsCount: number; // ✅ 추가
};
</code></pre>
<p>그리고 컴포넌트도 수정했죠</p>
<pre><code class="language-tsx">// PostList.tsx
export const PostList = ({ posts }: { posts: Post[] }) =&gt; {
  return (
    &lt;div&gt;
      {posts.map((post) =&gt; (
        &lt;div key={post.id}&gt;
          &lt;h2&gt;{post.title}&lt;/h2&gt;
          &lt;p&gt;{post.body}&lt;/p&gt;
          &lt;span&gt;댓글 {post.commentsCount}개&lt;/span&gt; {/* ✅ 추가 */}
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
};
</code></pre>
<p><strong>✅ 2주차 결과</strong>: 문제없이 구현 완료! 타입도 잘 맞고 기획 요구사항도 충족했습니다.</p>
<h3 id="3주차-상세-페이지-개발-시작">3주차: 상세 페이지 개발 시작</h3>
<blockquote>
<p>&quot;이제 게시글을 클릭하면 상세 페이지로 이동하도록 해주세요. 상세 페이지에는 게시글 내용과 실제 댓글들이 보여야 해요.&quot;</p>
</blockquote>
<p>상세 페이지용 API를 따로 추가하고 해당 API는 댓글 정보까지 포함해서 내려줍니다</p>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;첫 번째 게시글&quot;,
  &quot;body&quot;: &quot;게시글 내용입니다.&quot;,
  &quot;userId&quot;: 123,
  &quot;commentsCount&quot;: 2,
  &quot;comments&quot;: [
    {
      &quot;id&quot;: 101,
      &quot;userId&quot;: 456,
      &quot;text&quot;: &quot;좋은 글이네요!&quot;,
      &quot;createdAt&quot;: &quot;2024-01-15T10:30:00Z&quot;
    }
  ]
}
</code></pre>
<h3 id="❌-문제가-되는-접근-방법">❌ 문제가 되는 접근 방법</h3>
<p>기본 API에서 댓글 정보만 추가됐으니 기존 <code>Post</code> 타입에 <code>comments</code>를 추가하려고 시도합니다</p>
<pre><code class="language-tsx">// ❌ 이렇게 하면...
type Post = {
  id: number;
  title: string;
  body: string;
  userId: number;
  commentsCount: number;
  comments: Comment[]; // 추가?
};
</code></pre>
<p>하지만 기존 Post type에 comments 추가 후 PostList로 돌아가보면</p>
<pre><code class="language-tsx">// PostList.tsx - 목록 페이지
export const PostList = ({ posts }: { posts: Post[] }) =&gt; {
  return (
    &lt;div&gt;
      {posts.map((post) =&gt; (
        &lt;div key={post.id}&gt;
          &lt;h2&gt;{post.title}&lt;/h2&gt;
          &lt;p&gt;{post.body}&lt;/p&gt;
          &lt;span&gt;댓글 {post.commentsCount}개&lt;/span&gt;
          {/* ❌ comments 필드는 사용하지 않지만 타입상 필수 */}
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
};</code></pre>
<p>PostList 페이지에서는 <code>comments</code> 데이터가 실제로 필요하지 않고 API에서도 내려주지 않는데, 타입상으로는 필수가 됩니다. 그렇다고 옵셔널(<code>comments?</code>)로 처리하면, 상세 페이지에서 <code>undefined</code> 체크를 계속 해야 하죠.</p>
<h3 id="✅-타입-확장성을-고려한-해결책">✅ 타입 확장성을 고려한 해결책</h3>
<p>기본 타입과 확장 타입을 분리해서 처리합니다:</p>
<pre><code class="language-tsx">// types.ts - 기본 Post 타입정의
export type Post = {
  id: number;
  title: string;
  body: string;
  userId: number;
};

export type Comment = {
  id: number;
  userId: number;
  text: string;
  createdAt: string;
};

// 상세 페이지 전용 확장 타입
export type PostWithLikeCountAndComments = Post &amp; {
  commentsCount: number;
  comments: Comment[];

};
</code></pre>
<pre><code class="language-tsx">// PostDetail.tsx - 상세 페이지 컴포넌트
export const PostDetail = ({ post }: { post: PostWithLikeCountAndComments }) =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;p&gt;{post.body}&lt;/p&gt;
      &lt;div&gt;
        &lt;h3&gt;댓글 ({post.commentsCount}개)&lt;/h3&gt;
        {post.comments.map((comment) =&gt; ( // 타입 보장됨
          &lt;div key={comment.id}&gt;
            &lt;p&gt;{comment.text}&lt;/p&gt;
            &lt;small&gt;{comment.createdAt}&lt;/small&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;
    &lt;/div&gt;
  );
};
</code></pre>
<h3 id="4주차-새로운-요구사항의-등장">4주차: 새로운 요구사항의 등장</h3>
<blockquote>
<p>&quot;메인 페이지에 인기 게시글 섹션을 만들어주세요. 여기서는 제목만 간단하게 보여주면 되고, 댓글 수는 필요 없어요. 대신 좋아요 수를 보여주면 좋겠어요.&quot;</p>
</blockquote>
<p>인기 게시글 API:</p>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;첫 번째 게시글&quot;,
  &quot;body&quot;: &quot;게시글 내용입니다.&quot;,
  &quot;userId&quot;: 123,
  &quot;likesCount&quot;: 15
}
</code></pre>
<h3 id="❌-문제가-되는-접근-방법-1">❌ 문제가 되는 접근 방법</h3>
<p>여기서도 모든 케이스를 하나의 타입으로 처리하려고 시도하려고 한다면? </p>
<pre><code class="language-tsx">// ❌ 모든 케이스를 하나로 처리하려는 시도
type Post = {
  id: number;
  title: string;
  body: string;
  userId: number;
  commentsCount?: number;  // 어떨 때는 있고 어떨 때는 없고...
  comments?: Comment[];    // 상세 페이지에서만 필요
  likesCount?: number;     // 인기 게시글에서만 필요
};</code></pre>
<p>모든 필드가 옵셔널이 되어버리고, 컴포넌트에서는 언제나 <code>undefined</code> 체크를 해야 합니다. 타입을 정의하면서 갖는 장점들이 퇴색되버리죠.</p>
<p>✅ 타입 확장성을 고려한 해결책</p>
<p>각 용도별로 명확한 네이밍과 함께 타입을 정의합니다:</p>
<pre><code class="language-tsx">// types.ts - 각 용도별 확장 타입 정의
export type PostList = Post &amp; {
  commentsCount: number;
};

export type PostWithLikeCount = Post &amp; {
  likesCount: number;
};

export type PostWithLikeCountAndComments = Post &amp; {
  commentsCount: number;
  comments: Comment[];
};
</code></pre>
<pre><code class="language-tsx">// PostList.tsx
export const PostList = ({ posts }: { posts: PostList[] }) =&gt; {
  return (
    &lt;div&gt;
      {posts.map((post) =&gt; (
        &lt;div key={post.id}&gt;
          &lt;h2&gt;{post.title}&lt;/h2&gt;
          &lt;p&gt;{post.body}&lt;/p&gt;
          &lt;span&gt;댓글 {post.commentsCount}개&lt;/span&gt; {
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
};

// PopularPosts.tsx
export const PopularPosts = ({ posts }: { posts: PostWithLikeCount[] }) =&gt; {
  return (
    &lt;div&gt;
      &lt;h2&gt;인기 게시글&lt;/h2&gt;
      {posts.map((post) =&gt; (
        &lt;div key={post.id}&gt;
          &lt;h3&gt;{post.title}&lt;/h3&gt;
          &lt;span&gt;❤️ {post.likesCount}&lt;/span&gt; 
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
};
</code></pre>
<h2 id="📈-결과-타입-안전성과-유지보수성-확보">📈 결과: 타입 안전성과 유지보수성 확보</h2>
<h3 id="before-문제-상황">Before (문제 상황)</h3>
<pre><code class="language-tsx">// ❌ 모든 필드가 옵셔널인 거대한 타입
type Post = {
  id: number;
  title: string;
  body: string;
  userId: number;
  commentsCount?: number;  // 언제 있는지 불분명
  comments?: Comment[];    // 언제 있는지 불분명
  likesCount?: number;     // 언제 있는지 불분명
};

// ❌ 매번 방어 코드 필요
&lt;span&gt;댓글 {post.commentsCount || 0}개&lt;/span&gt;
</code></pre>
<h3 id="after-해결책-적용">After (해결책 적용)</h3>
<pre><code class="language-tsx">// ✅ 목적이 명확한 타입들
export type PostList = Post &amp; { commentsCount: number };
export type PostWithLikeCount = Post &amp; { likesCount: number };
export type PostWithLikeCountAndComments = Post &amp; { commentsCount: number; comments: Comment[] };

// ✅ 타입 안전성 보장
&lt;span&gt;댓글 {post.commentsCount}개&lt;/span&gt; // 항상 존재함이 보장됨
</code></pre>
<h3 id="장점-정리">장점 정리</h3>
<ol>
<li><strong>타입 안전성</strong>: 각 컴포넌트에서 필요한 필드가 항상 존재함을 타입 레벨에서 보장</li>
<li><strong>명확한 의도</strong>: <code>PostWithLikeCount</code>, <code>PostWithLikeCountAndComments</code> 등 이름만 봐도 어디서 사용하는지 알 수 있음</li>
<li><strong>유지보수성</strong>: 새로운 요구사항이 생겨도 기존 타입을 건드리지 않고 새로운 조합 타입만 추가</li>
<li><strong>재사용성</strong>: 기본 <code>Post</code> 타입을 베이스로 다양한 상황에 맞는 확장 타입 생성 가능</li>
<li><strong>런타임 안전성</strong>: <code>undefined</code> 체크나 방어 코드가 불필요</li>
</ol>
<h2 id="📘-마무리-타입도-분리와-조합이-필요하다">📘 마무리: 타입도 분리와 조합이 필요하다</h2>
<p>어찌 보면 당연한 말이죠, 하지만 타입스크립트 사용에 익숙지 않거나 일정에 쫓겨 급박히 개발하다 보면 쉽게 범할 수 있는 실수입니다. 저 역시 “일단 타입부터 맞춰 놓고 나중에 정리하자”는 마음으로, 다양한 케이스를 하나의 거대한 타입에 우겨넣었던 경험이 많습니다.</p>
<p>그렇게 쌓인 타입은 어느새 비대해지고, 변경이 생길 때마다 여기저기서 방어 코드를 쓰게 되며, 결국 타입스크립트의 가장 큰 장점인 ‘<strong>컴파일 타임의 안정성</strong>’을 스스로 무력화시키는 결과를 만들게 되죠.</p>
<p>필요한 데이터만 정확히 받도록 하고, 그 상황에 맞는 타입을 조합해 나가는 것. 처음엔 조금 번거로워 보여도, 결국엔 유지보수가 편하고, 일일이 타입 정의 파일을 확인하는 시간도 줄어들 겁니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vike 한입 해보실래요?]]></title>
            <link>https://velog.io/@sj_yun/Vike-%ED%95%9C%EC%9E%85-%ED%95%B4%EB%B3%B4%EC%8B%A4%EB%9E%98%EC%9A%94</link>
            <guid>https://velog.io/@sj_yun/Vike-%ED%95%9C%EC%9E%85-%ED%95%B4%EB%B3%B4%EC%8B%A4%EB%9E%98%EC%9A%94</guid>
            <pubDate>Sun, 22 Jun 2025 11:31:32 GMT</pubDate>
            <description><![CDATA[<p>요즘 사이드 프로젝트로 블로그를 구성하려고 고민중에 있던 중 발견한 새 기술 스택이 있는데요. 바로 Vike라는 프레임워크입니다. 아직 국내외 커뮤니티를 비롯하여 아직까지 생소한 프레임워크지만, 개발의 유연성과 효율성을 확 끌어올릴 수 있는 흥미로운 요소들을 담고 있다고 생각해서 소개글과 간단한 사용법을 작성해보고자 합니다.</p>
<h1 id="vike란">Vike란?</h1>
<p>Vike는 <strong>UI 프레임워크, 렌더링 전략,서버 환경을 개발자가 원하는 대로 선택할 수 있도록 만들어진, 작지만 확장성 뛰어난 코어 프레임워크</strong>예요.</p>
<h3 id="유연하고-안정적인-아키텍처-flexible-by-design">유연하고 안정적인 아키텍처 (Flexible by Design)</h3>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/bd9f45f1-4bec-434c-9774-9e45b64f2a71/image.png" alt="vike-stack"></p>
<ul>
<li><strong>Vite 기반의 강력한 개발 경험</strong>: Vite를 기반으로 하기 때문에 HMR(Hot Module Replacement)의 빠른 개발 피드백 사이클을 그대로 활용할 수 있다.</li>
<li><strong>기술 스택의 자유로운 조합</strong>: UI 프레임워크(React, Vue, Solid...), 데이터 패칭(REST, GraphQL, RPC...), 렌더링 전략(SPA, SSR, SSG...) 등 특정 기술에 종속되지 않고 프로젝트 요구사항에 맞게 스택을 유연하게 조정할 수 있다.</li>
<li><strong>점진적 도입 가능</strong>: 별도의 레포지토리 분리나 추가적인 인프라 구축 없이 기존 프로젝트에 빠르게 도입할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/8d73b2c1-8d5b-4cf3-bcdd-0512bafe124d/image.png" alt="vike-start">
이러한 Vike의 철학은 <a href="https://vike.dev/new">GET Started 페이지</a>에서도 엿볼 수 있습니다. 다양한 기술 스택에 맞게 입맛대로 프로젝트 생성이 가능하죠.</p>
<h2 id="생태계-강자와의-비교-nextjs와-vike">생태계 강자와의 비교: Next.js와 Vike,</h2>
<h3 id="학습-곡선과-개발자-경험">학습 곡선과 개발자 경험</h3>
<p>SSG와 SSR을 지원하는 프레임워크 시장에서의 선두주자는 단연 Next.js입니다. 특히 React Server Components(RSC) 모델이 도입된 이후 컴포넌트를 서버와 클라이언트에서 매우 세밀하게 분리할 수 있다는 명확한 장점을 제공하죠. 모든 컴포넌트가 기본적으로 서버에서 실행되어 JavaScript 번들에서 제외되므로, 하이드레이션 과정을 최소화하고 클라이언트로 전송되는 JS 양을 줄일 수 있어요.</p>
<p>하지만 이러한 세밀한 제어는 동시에 개발자에게 새로운 고민과 신경 써야 할 지점들을 안겨주기도 합니다. 예를 들어, 단순히 useState 훅 하나를 사용하기 위해 컴포넌트 상단에 &#39;use client&#39;를 명시해야 하고, 이로 인해 해당 컴포넌트 전체가 하이드레이션 대상에 포함될 수 있어요. 또한, dynamic import를 위해 컴포넌트를 별도의 파일로 분리해야 하는 상황이 발생하면서 오히려 개발 복잡도가 증가하는 느낌을 받기도 합니다.</p>
<p>반면 Vike는 전통적인 SSR과 하이드레이션(Hydration) 모델을 기반으로 하여 기존 React 개발 경험을 그대로 활용할 수 있어요. useState, useEffect 같은 익숙한 훅들을 자연스럽게 사용하면서,+Page.tsx와 +data.ts 파일 구조만 이해하면 별다른 러닝커브 없이 빠르게 시작할 수 있습니다. 새로운 멘탈 모델을 학습하는 부담 없이 기존에 알고 있던 React 지식을 바탕으로 서버 사이드 렌더링의 이점을 누릴 수 있다는 점이 매력적입니다.</p>
<h3 id="배포와-인프라-유연성">배포와 인프라 유연성</h3>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/be1b5edc-2585-4201-a118-14d90d29c32e/image.png" alt="NEXTJS"></p>
<p>Next.js는 Vercel과의 깊은 통합으로 배포 경험이 매우 매끄럽긴 하지만, 동시에 Vercel의 인프라에 대한 종속성을 의미하기도 합니다. 물론 다른 플랫폼에도 배포할 수 있지만, 서버 액션이나 일부 고급 기능들은 Vercel에서 최적화되어 있어서 다른 환경에서는 추가 설정이나 우회가 필요한 경우가 빈번합니다. 최근에는 <a href="https://news.hada.io/topic?id=21430">Next.js 15.1+에서 메타데이터 스트리밍 방식 변경으로 인해 Vercel 외 환경에서 SEO 문제가 발생</a>하는 등 플랫폼 종속성이 더욱 심화되는 이슈도 존재하는 상황입니다.</p>
<p>Vike는 개발자의 입맛대로 초기 프로젝트를 구성할 수 있기 때문에 플랫폼 종속성이 훨씬 적다는 장점을 가집니다. 정적 사이트로 빌드하면 Netlify, GitHub Pages, Cloudflare Pages 어디든 배포할 수 있고, SSR이 필요하다면 Node.js가 실행되는 어떤 환경에서든 동작합니다.이런 유연성은 특히 여러 환경을 고려해야 하는 경우나 특별한 요구사항이 있는 프로젝트에서 중요한 고려사항이죠.</p>
<h1 id="vike-주요-기능">Vike 주요 기능</h1>
<h3 id="1-라우팅-전략과-데이터-패칭">1. 라우팅 전략과 데이터 패칭</h3>
<p>Vike는 Next.js와 동일하게 파일 시스템을 기반으로 라우팅을 사용합니다.</p>
<h4 id="1단계-디렉터리-구조-설계">1단계: 디렉터리 구조 설계</h4>
<p>예시로 상품 목록과 개별 상품 상세 페이지를 만든다고 가정하면, 다음과 같은 디렉터리 구조를 만들 수 있어요</p>
<pre><code>src/pages/
└─ product/
  ├─ +data.ts        # 서버에서 데이터 페칭
  ├─ +Page.tsx       # /product 컴포넌트
  └─ @id/            # id 기반 개별 상품 페이지
    ├─ +data.ts      # 서버에서 데이터 페칭
    └─ +Page.tsx     # /product/:id 컴포넌트</code></pre><h4 id="2단계-데이터-페칭과-컴포넌트-구현">2단계: 데이터 페칭과 컴포넌트 구현</h4>
<p>Vike의 데이터 패칭은 서버 사이드 우선 원칙을 따릅니다. <code>+data.ts</code> 파일은 서버에서만 실행되어 페이지에 필요한 데이터를 미리 페칭해 <code>pageContext.data</code>로 반환하고, <code>+Page.tsx</code> 컴포넌트는 <a href="https://vike.dev/useData">useData</a> 훅을 통해 이미 준비된 데이터를 받아와 UI를 렌더링하는 구조예요.</p>
<pre><code class="language-typescript">// src/pages/product/+data.ts
// 서버 사이드에서 실행되는 데이터 페칭
import type { PageContextServer } from &#39;vike/types&#39;;

export interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
}

export async function data(pageContext: PageContextServer): Promise&lt;Product[]&gt; {
  const response = await fetch(`/api/products`);
  const products: Product[] = await response.json();

  return products;
}</code></pre>
<pre><code class="language-typescript">// src/pages/product/+Page.tsx
// 클라이언트에서 렌더링되는 페이지 컴포넌트
import React from &#39;react&#39;;
import { useData } from &#39;vike-react/useData&#39;;
import type { Product } from &#39;./+data&#39;;

export default function ProductPage() {
  const products = useData&lt;Product[]&gt;();

  return (
    &lt;div&gt;
      &lt;h1&gt;상품 목록&lt;/h1&gt;
      {products.map((product) =&gt; (
        &lt;div key={product.id}&gt;
          &lt;h3&gt;{product.name}&lt;/h3&gt;
          &lt;p&gt;{product.price}원&lt;/p&gt;
          &lt;a href={`/product/${product.id}`}&gt;상세보기&lt;/a&gt;
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
}</code></pre>
<p>이런 구조의 장점들을 살펴보면:</p>
<ul>
<li>SSR이 가지는 이점들(보안, 성능, SEO 최적화)을 가집니다.</li>
<li>각 페이지마다 <code>+data.ts</code>와 <code>+Page.tsx가</code> 쌍을 이루어 관심사 분리가 명확히 이루어져요.</li>
<li>데이터 페칭 로직과 UI 로직이 물리적으로 분리되어 있어 유지보수성이 향상됩니다.</li>
<li>컴포넌트에서는 데이터 페칭 로직 없이 순수하게 렌더링에만 집중할 수 있으며 <a href="https://vike.dev/useData">useData()</a> 훅으로 데이터 접근이 매우 간단하고 직관적입니다.</li>
</ul>
<h4 id="동적-라우팅-구현">동적 라우팅 구현</h4>
<p>개별 상품 페이지는 <code>@id</code> 폴더를 사용해 동적 라우팅을 구현합니다:</p>
<pre><code class="language-typescript">// src/pages/product/@id/+data.ts
// 개별 상품 페이지 데이터 페칭

import type { PageContextServer } from &#39;vike/types&#39;;

export interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
}

export async function data(pageContext: PageContextServer): Promise&lt;Product&gt; {
  const productId = pageContext.routeParams.id;

  const response = await fetch(`/api/products/${productId}`);
  const product: Product = await response.json();

  return product;
}</code></pre>
<pre><code class="language-typescript">// src/pages/product/@id/+Page.tsx
// 개별 상품 상세 페이지
import React from &#39;react&#39;;
import { useData } from &#39;vike-react/useData&#39;;
import type { Product } from &#39;./+data&#39;;

export default function ProductDetailPage() {
  const product = useData&lt;Product&gt;();

  return (
    &lt;div&gt;
      &lt;h1&gt;{product.name}&lt;/h1&gt;
      &lt;p&gt;{product.price}원&lt;/p&gt;
      &lt;p&gt;{product.description}&lt;/p&gt;
      &lt;button&gt;장바구니 담기&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li><code>@id</code> 폴더를 통한 동적 라우팅으로 RESTful한 URL 구조를 구현합니다.</li>
<li>Vike에서 제공하는 <a href="https://vike.dev/pageContext">pageContext</a>로 URL 파라미터에 간편하게 접근할 수 있어요.</li>
</ul>
<h3 id="2-정적-렌더링-ssg-static-site-generation">2. 정적 렌더링 (SSG: Static Site Generation)</h3>
<p>SSG(Static Site Generation)는 빌드 시점에 모든 페이지를 미리 정적 HTML 파일로 생성해두는 렌더링 전략입니다. Vike 역시 이 기능을 지원하며, 한 번 생성되면 변경 및 수정이 잦지 않은 블로그나 리포트 포스팅에 특히 유용하죠. 블로그 포스팅 예제를 통해 Vike의 SSG 전략을 알아보겠습니다.</p>
<h4 id="1단계-ssg-설정-및-디렉터리-구조-설계">1단계: SSG 설정 및 디렉터리 구조 설계</h4>
<p><strong>글로벌 설정 (vite.config.ts)</strong>: 프로젝트의 모든 페이지를 기본적으로 SSG로 설정하고 싶다면 Root vite.config.ts 파일의 vike 플러그인 옵션에 prerender: true를 추가해 주세요.</p>
<pre><code class="language-typescript">// vite.config.ts
import { defineConfig } from &#39;vite&#39;;
import react from &#39;@vitejs/plugin-react&#39;;
import vike from &#39;vike/plugin&#39;;

export default defineConfig({
  plugins: [
    react(),
    vike({
      prerender: true, // 모든 페이지를 기본적으로 SSG로 설정
    }),
  ],
});</code></pre>
<p><strong>페이지별 설정 (pages/+config.js 또는 해당 폴더의 +config.js)</strong>: 특정 페이지 경로만 SSG로 만들고 싶을 경우 해당 페이지 폴더 root 경로에 <code>config.js</code> 파일을 생성하여 prerender: true로 설정해줍니다.</p>
<pre><code class="language-javascript">// config.js
export default {
  prerender: true,
};</code></pre>
<p>블로그 SSG를 위한 디렉터리 구조는 다음과 같습니다:</p>
<pre><code>src/pages/
└─ blog/
  ├─ +config.js                     # 이 경로와 하위 경로를 SSG로 설정
  ├─ +Page.tsx                      # 블로그 목록 페이지 (필요하다면)
  ├─ +data.ts                        # 데이터 페칭
  └─ @id/
    ├─ +data.ts                     # 개별 블로그 포스트 데이터 페칭
    └─ +Page.tsx                    # 개별 블로그 포스트 컴포넌트
    └── +onBeforePrerenderStart.ts   # SSG 핵심 요소</code></pre><h4 id="2단계-동적-라우트-처리와-onbeforeprerenderstart-훅-구현">2단계: 동적 라우트 처리와 onBeforePrerenderStart 훅 구현</h4>
<p>이미 생성된 각 개별 블로그 포스팅의 동적 라우트는 URL에 변수가 포함돼요. /blog/1, /blog/2, /blog/3처럼 말이죠. 이런 경우 Vike는 어떤 ID 값들로 페이지를 만들어야 할지 알 수 없기 때문에 <code>onBeforePrerenderStart</code> 훅을 통해 미리 알려줘야 합니다.</p>
<p><strong>onBeforePrerenderStart 훅의 역할</strong></p>
<p><a href="https://vike.dev/onBeforePrerenderStart">onBeforePrerenderStart</a> 훅은 동적 라우트에서 SSG를 구현하는 핵심 요소예요. 이 훅은 빌드 시점에 실행되어 생성할 페이지들의 목록을 Vike에게 알려주는 역할을 합니다. Next.js에서 제공하는 getStaticPaths와 유사하게 동작하죠.</p>
<pre><code class="language-typescript">// pages/blog/@id/+onBeforePrerenderStart.ts
import type { OnBeforePrerenderStartAsync } from &#39;vike/types&#39;;
import {
  fetchAllBlogPosts,
  fetchBlogPostById,
} from &#39;../../../features/blog/blog.api&#39;;
import type { BlogPost } from &#39;../../../features/blog/blog.type&#39;;

export type Data = {
  post: BlogPost;
};

export const onBeforePrerenderStart: OnBeforePrerenderStartAsync&lt;
  Data
&gt; = async () =&gt; {
  const posts = await fetchAllBlogPosts();

  // 각 포스트에 대해 상세 데이터를 가져와 URL과 pageContext 생성
  const blogPages = await Promise.all(
    posts.map(async (post) =&gt; {
      const detailPost = await fetchBlogPostById(post.id);

      return {
        url: `/blog/${post.id}`, // 생성할 정적 URL
        pageContext: {
          data: {
            post: detailPost, // 페칭된 데이터
          },
        },
      };
    })
  );

  return blogPages;
};</code></pre>
<p>이 훅을 통해 각 포스트마다 생성할 URL과 해당 페이지에서 사용할 데이터를 Vike에게 전달합니다.</p>
<p>이제 전체적인 블로그 SSG 구현을 살펴볼게요.</p>
<pre><code class="language-typescript">// pages/blog/index/+data.ts
import type { PageContextServer } from &#39;vike/types&#39;;
import { fetchAllBlogPosts } from &#39;../../../features/blog/blog.api&#39;;
import type { BlogPost } from &#39;../../../features/blog/blog.type&#39;;

export type Data = {
  posts: BlogPost[];
};

export default async function data(
  _pageContext: PageContextServer
): Promise&lt;Data&gt; {
  const posts = await fetchAllBlogPosts();
  return { posts };
}</code></pre>
<pre><code class="language-typescript">// pages/blog/index/+Page.tsx
import { useData } from &#39;vike-react/useData&#39;;
import type { Data } from &#39;./+data&#39;;
import { BlogList } from &#39;../../../features/blog/components/BlogList&#39;;

export default function Page() {
  const { posts } = useData&lt;Data&gt;();

  return &lt;BlogList posts={posts} /&gt;;
}</code></pre>
<pre><code class="language-typescript">// pages/blog/@id/+data.ts
import type { PageContextServer } from &#39;vike/types&#39;;
import { fetchBlogPostById } from &#39;../../../features/blog/blog.api&#39;;
import type { BlogPost } from &#39;../../../features/blog/blog.type&#39;;

export type Data = {
  post: BlogPost;
};

export default async function data(
  pageContext: PageContextServer
): Promise&lt;Data&gt; {
  const { id } = pageContext.routeParams;
  const postId = parseInt(id, 10);
  const post = await fetchBlogPostById(postId);
  return { post };
}</code></pre>
<pre><code class="language-typescript">// pages/blog/@id/+Page.tsx
import { useData } from &#39;vike-react/useData&#39;;
import type { Data } from &#39;./+data&#39;;

export default function Page() {
  const { post } = useData&lt;Data&gt;();

  return (
    &lt;article className=&#39;container mx-auto px-4 py-8 max-w-4xl&#39;&gt;
      &lt;header className=&#39;mb-8&#39;&gt;
        &lt;h1 className=&#39;text-4xl font-bold text-gray-900 leading-tight mb-4&#39;&gt;
          {post.title}
        &lt;/h1&gt;
      &lt;/header&gt;

      &lt;div className=&#39;prose prose-lg max-w-none&#39;&gt;
        &lt;div className=&#39;bg-white rounded-lg shadow-sm border border-gray-200 p-8&#39;&gt;
          &lt;div className=&#39;text-gray-700 leading-relaxed whitespace-pre-wrap&#39;&gt;
            {post.body}
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/article&gt;
  );
}</code></pre>
<p><strong>빌드 과정과 결과물</strong></p>
<p>이제 build 명령어를 실행하면 다음과 같은 과정이 진행됩니다:</p>
<ol>
<li><strong>정적 라우트 처리</strong>: /blog 페이지는 +data.ts를 실행하여 모든 포스트 목록을 가져와 정적 HTML을 생성합니다.</li>
<li><strong>동적 라우트 처리</strong>: +onBeforePrerenderStart.ts가 실행되어 각 포스트 ID별로 상세 페이지를 생성하죠.</li>
</ol>
<p>빌드 결과물은 다음과 같은 구조로 생성돼요:</p>
<pre><code>static/
├── blog/
│   ├── index.html              # 블로그 리스트 페이지
│   ├── 1/index.html            # 포스트 #1 상세 페이지
│   ├── 2/index.html            # 포스트 #2 상세 페이지
│   ├── 3/index.html            # 포스트 #3 상세 페이지
│   └── ... (이외 포스트)        # ...</code></pre><p><strong>주의사항과 팁</strong></p>
<ul>
<li><strong>데이터 업데이트</strong>: 콘텐츠가 변경되면 다시 빌드해야 정적 파일에 반영됩니다.</li>
<li><strong>빌드 시간</strong>: 포스트가 많을수록 빌드 시간이 길어질 수 있어요.</li>
<li><strong>API 호출 최적화</strong>: onBeforePrerenderStart에서 불필요한 API 호출을 줄이도록 주의해야 합니다.</li>
</ul>
<h3 id="3-vike에서-hydration을-진행하는-방법">3. Vike에서 Hydration을 진행하는 방법</h3>
<p>하이드레이션(Hydration)은 서버에서 이미 렌더링된 HTML에 클라이언트 측 JavaScript 코드를 주입하여 사용자와 상호작용할 수 있는 웹 애플리케이션으로 만드는 과정이에요. Vike의 하이드레이션 과정은 크게 두 개의 핵심 훅, <code>onRenderHtml</code>과 <code>onRenderClient</code> 를 통해 이루어집니다.</p>
<h4 id="1단계-서버에서-html-초벌-렌더링-onrenderhtml-훅">1단계: 서버에서 HTML 초벌 렌더링 (onRenderHtml 훅)</h4>
<p>사용자가 Vike 앱에 처음 들어오면, Vike 서버는 <a href="https://vike.dev/onRenderHtml">onRenderHtml</a> 훅을 실행하여 해당 페이지의 컴포넌트들을 HTML 문자열로 변환해요. 이 HTML이 브라우저로 전송되면 사용자는 초기 화면을 빠르게 볼 수 있죠.</p>
<p>먼저 서버 측에서 어떻게 HTML을 생성하는지 살펴보겠습니다:</p>
<pre><code class="language-typescript">// renderer/+onRenderHtml.tsx (React SSR 예시)

import React from &#39;react&#39;;
import ReactDOMServer from &#39;react-dom/server&#39;;
import { PageShell } from &#39;./PageShell&#39;; // 전체 페이지 구조를 담는 컴포넌트
import { escapeInject, dangerouslySkipEscape } from &#39;vike/server&#39;;
import type { OnRenderHtmlHook } from &#39;vike/types&#39;;

const onRenderHtml: OnRenderHtmlHook = async (
  pageContext
): ReturnType&lt;OnRenderHtmlHook&gt; =&gt; {
  const { Page, pageProps } = pageContext; // +Page.tsx 컴포넌트와 데이터

  // 1단계: React 컴포넌트를 HTML 문자열로 렌더링
  const pageHtml = ReactDOMServer.renderToString(
    &lt;PageShell pageContext={pageContext}&gt;
      &lt;Page {...pageProps} /&gt;
    &lt;/PageShell&gt;
  );

  // 2단계: HTML 템플릿에 렌더링된 내용과 필요한 스크립트를 삽입
  const documentHtml = escapeInject`&lt;!DOCTYPE html&gt;
    &lt;html&gt;
      &lt;head&gt;
        &lt;title&gt;Vike App&lt;/title&gt;
        &lt;link rel=&quot;stylesheet&quot; href=&quot;/assets/index.css&quot; /&gt;
        &lt;script type=&quot;module&quot; src=&quot;/assets/entry-client.js&quot;&gt;&lt;/script&gt;
      &lt;/head&gt;
      &lt;body&gt;
        &lt;div id=&quot;react-root&quot;&gt;${dangerouslySkipEscape(pageHtml)}&lt;/div&gt;
      &lt;/body&gt;
    &lt;/html&gt;`;

  return {
    documentHtml,
    pageContext: {
      // 클라이언트로 전달할 추가적인 pageContext 데이터 (선택 사항)
    },
  };
};
export default onRenderHtml;</code></pre>
<p>위 코드에서 ReactDOMServer.renderToString을 통해 React 컴포넌트가 서버에서 HTML 문자열로 변환되고, escapeInject를 사용하여 <code>&lt;div id=&quot;react-root&quot;&gt;</code> 내부에 삽입됩니다. 이 HTML에는 클라이언트 측 JavaScript 번들을 로드하는 <code>&lt;script&gt;</code> 태그도 포함되어 있어요.</p>
<h4 id="2단계-클라이언트-번들-다운로드-후-하이드레이션-onrenderclient-훅">2단계: 클라이언트 번들 다운로드 후 하이드레이션 (onRenderClient 훅)</h4>
<p>브라우저는 서버로부터 받은 HTML을 파싱하는 동안, Vike가 만든 클라이언트 측 JavaScript 번들을 다운로드해요. 이 번들 안에는 페이지를 구성하는 데 필요한 코드와 모든 클라이언트 로직이 포함되어 있습니다. (예시에서는 /assets/entry-client.js가 해당 번들입니다.)</p>
<p>클라이언트 번들이 다운로드되고 실행되면, <a href="https://vike.dev/onRenderClient">onRenderClient</a> 훅이 호출됩니다. 이 훅은 서버에서 렌더링된 HTML 구조와 일치하는 가상 DOM 트리를 구축하고, 이를 실제 DOM에 연결(마운트)하며, 버튼 클릭이나 입력 필드 변경과 같은 사용자 인터랙션에 반응할 수 있도록 이벤트 리스너를 부착합니다. 이 과정이 바로 하이드레이션이에요.</p>
<pre><code class="language-typescript">// renderer/+onRenderClient.tsx (예시: React Hydration)
import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom/client&#39;;
import { PageShell } from &#39;./PageShell&#39;;
import type { OnRenderClientHook } from &#39;vike/types&#39;;

const onRenderClient: OnRenderClientHook = async (
  pageContext
): ReturnType&lt;OnRenderClientHook&gt; =&gt; {
  const { Page, pageProps } = pageContext;
  const container = document.getElementById(&#39;react-root&#39;);

  if (!container) {
    throw new Error(&#39;Root element not found&#39;);
  }

  // 3단계: 클라이언트에서 React 애플리케이션을 하이드레이션
  // 서버에서 렌더링된 HTML에 React 앱을 &#39;부트스트랩&#39;하여 상호작용 가능하게 만듭니다.
  // ReactDOM.hydrateRoot를 사용하면 기존 HTML 구조를 재사용하며 이벤트 리스너를 부착합니다.
  const root = ReactDOM.hydrateRoot(
    container,
    &lt;PageShell pageContext={pageContext}&gt;
      &lt;Page {...pageProps} /&gt;
    &lt;/PageShell&gt;
  );
};
export default onRenderClient;</code></pre>
<p>onRenderClient 훅에서는 <code>ReactDOM.hydrateRoot</code>를 사용하여 서버에서 생성된 HTML을 재활용하면서 클라이언트 측 React 애플리케이션을 마운트하고, 이벤트를 연결하여 페이지를 상호작용 가능한 상태로 만듭니다.</p>
<p><strong>Vike의 Hydration 과정 전체 흐름:</strong></p>
<ol>
<li><strong>서버 렌더링</strong>: 사용자 요청 시 onRenderHtml 훅이 실행되어 React 컴포넌트를 HTML 문자열로 변환</li>
<li><strong>HTML 전송</strong>: 생성된 HTML과 클라이언트 번들 로드 스크립트가 브라우저로 전송</li>
<li><strong>번들 다운로드</strong>: 브라우저가 클라이언트 JavaScript 번들을 다운로드</li>
<li><strong>하이드레이션</strong>: onRenderClient 훅이 실행되어 기존 HTML에 이벤트와 상호작용 기능을 연결</li>
</ol>
<h2 id="마무리하며">마무리하며</h2>
<p>지금까지 Vike의 핵심 개념과 특징들을 빠르게 살펴보았습니다. 직접 사용해 본 결과, Vike는 아직 Next.js와 같은 거대 프레임워크에 비해 생태계 규모가 작고, 공식 문서 외에 참고할 만한 레퍼런스가 부족했습니다. 이 때문에 트러블슈팅을 위해 이슈 탭과 내부 코드를 직접 확인해야 하는 경우도 있었죠. 이러한 관점에서 볼 때, Vike를 선뜻 도입하기에는 부담이 될 수 있다는 점은 분명합니다.</p>
<p>그럼에도 불구하고, Vike가 사용자에게 높은 유연성과 제어권을 제공하며 프로젝트의 특정 요구사항에 맞춰 최적화된 스택을 구축할 수 있게 한다는 점은 분명 매력적인 대안이라고 생각합니다. Vike가 추구하는 방향은 매우 흥미롭기에, 앞으로의 행보가 기대되는 프레임워크입니다.</p>
<p>Reference</p>
<ul>
<li><a href="https://youtu.be/jzjtDC31ZnI?si=F95_pwJ9dL_78dKA">Vike 소개 영상</a></li>
<li><a href="https://vike.dev/">Vike 공식문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TeoConf 3회차 스태프로 참여하며 ]]></title>
            <link>https://velog.io/@sj_yun/TeoConf-3%ED%9A%8C%EC%B0%A8-%EC%8A%A4%ED%83%9C%ED%94%84%EB%A1%9C-%EC%B0%B8%EC%97%AC%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@sj_yun/TeoConf-3%ED%9A%8C%EC%B0%A8-%EC%8A%A4%ED%83%9C%ED%94%84%EB%A1%9C-%EC%B0%B8%EC%97%AC%ED%95%98%EB%A9%B0</guid>
            <pubDate>Wed, 27 Nov 2024 11:42:44 GMT</pubDate>
            <description><![CDATA[<h3 id="테오콘-스태프로-참여하다">테오콘 스태프로 참여하다</h3>
<p>현재 회사에서 프론트엔드 개발자가 나 혼자라, 혼자서 생각하고 결정해야 할 부분들이 있다 보니 가끔은 고립감도 느끼고 &#39;이렇게 해도 괜찮은 건지&#39; 에 대한 불안감도 있었다. 그러면서 자연스럽게** 다른 개발자들은 어떻게 일하고, 어떤 고민을 하고 있을까?** 란 궁금증이 생기기 시작했다.</p>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/30a7b582-55c4-44c4-aa01-9f8c498e127e/image.jpg" alt="테오콘"></p>
<p>그러다 네트워킹의 기회를 알아보고자 다양한 커뮤니티와 이벤트를 찾아보던 중, 블로그글 후기로 테오콘을 알게 되었다. 마침 테오 디스코드에서 테오콘 3회차를 준비 중이라는 글을 보게 되었고, 이번에는 직접 참여하며 의미 있는 경험을 쌓고 싶었다.</p>
<p>하지만 단순히 참가자로 당첨될 자신이 없었고, 한편으로는 내가 행사의 한 부분이 되어 기여하고 싶은 마음도 있었다. 그래서 확실히 참여할 수 있는 방법으로 <strong>스태프 지원</strong>을 결심하게 되었다.</p>
<h3 id="준비과정"><strong>준비과정</strong></h3>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/6e18f470-b6f9-4342-8ab5-e9ee29421ef4/image.jpg" alt="스태프 활동"></p>
<p>스태프로 활동한 후, 2주에 한 번씩 일요일마다 시간이 되는 사람들끼리 자유롭게 피그잼에 모여 회의를 진행하였다. 여기서 스태프들과 함께 다양한 아이디어를 나누고, 이를 구체화하며 기획을 진행했다. 처음에는 막연하게 느껴졌던 것들도 시간이 지날수록 점점 실현 가능성이 높은 아이디어로 다듬어졌다.</p>
<p>피그잼 화면이 포스트잇과 아이디어로 점점 채워질수록, <strong>&#39;구상한 아이디어들이 실제 행사 날 실현될 수 있을까?&#39;</strong>라는 생각에 설렘과 기대가 가득했다.</p>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/04aa6ed1-b95f-4915-b96e-3ef311fd5d36/image.png" alt="오프라인"></p>
<p>오프라인으로도 테오와 스태프들을 만나 준비하는 시간이 있었는데, 이 때 행사 준비 외에도 새로운 사람들의 고민과 생각을 들어 볼 수 있어서 얻어가는것이 많은 시간이었다. 스태프들 모두 열정적으로 참여하는 분들이어서 이야기하며 나 역시 좋은 자극을 얻고 스스로에게 큰 동기부여가 되었다.</p>
<h3 id="소소한-해프닝">소소한(?) 해프닝</h3>
<p>그렇게 준비하다보니, 드디어 대망의 행사날이 다가왔다. 그런데 행사 전날, 명함 제조 업체로 부터 명함 수령이 행사 끝난 다음 날에 가능하다는 답변을 받게 되면서 나를 포함한 모든 스태프가 큰 충격에 빠졌다. 다행히도 다들 열심히 대안을 생각한 덕에 행사 당일 최소한 인당 2장씩이라도 준비하여 드릴 수 있었다. </p>
<p>스태프들이 열심히 준비한 명함을 참가자들에게 나누어주고 서로 교환하지 못한 것은 정말 아쉬웠지만 한편으로는 나중에 돌이켜보면, &quot;이런 해프닝도 있었지&quot; 하고 웃으며 이야기할 수 있는 추억이 되지 않을까 싶다.</p>
<h3 id="행사의-시작">행사의 시작!</h3>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/91c983d7-efab-4117-b874-c93e5912dbe2/image.jpg" alt=""></p>
<p>행사는 MC 루키의 깔끔한 진행과 함께 본격적으로 시작되었다. 그동안 준비했던 아이디어들이 하나둘 현실이 되는 것을 보면서, 개발할 때 보람을 느끼던 순간이 떠올랐다. 내가 상상한 것을 현실로 구현해내는 순간의 성취감. 그 감정을 이번 행사에서도 온전히 느낄 수 있었다.</p>
<p>참가자분들이 우리가 준비한 프로그램을 잘 즐겨주시고 준비한 무대를 통해 많은 사람들이 교류하는 모습을 보니 그동안 준비했던 노력이 헛되지 않았음과 뿌듯함이 밀려왔다.</p>
<h3 id="경험을-기록하고-공유하는-의미">경험을 기록하고 공유하는 의미</h3>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/4c90c456-c1c1-438c-86df-fefd11bded5b/image.JPG" alt=""></p>
<p>스피커분들의 발표를 들으며 공통적으로 인상 깊었던 점은, 각자의 경험을 나누는 방식이었다. 처음엔 &quot;내가 겪은 일들이 과연 다른 사람에게도 의미가 있을까?&quot;라는 의구심이 들 수 있지만, 스피커분들은 오히려 그 <strong>평범한</strong> 순간들 속에서 특별한 인사이트를 끌어내고 계셨다.</p>
<p>이런 경험들을 보며, 기록하고 공유하는 것이 단순한 기록일 뿐 아니라, <strong>모두가 서로의 경험을 통해 조금 더 쉽고 빠르게 성장할 수 있게 해주는 가치 있는 활동</strong>이라는 걸 다시금 느꼈다. 나 역시 앞으로는 개발, 개발 외적으로 마주치는 크고 작은 순간들을 기록하고, 언젠가 누군가에게 도움이 될 수 있는 이야기로 나눌 수 있기를 기대해본다.</p>
<h3 id="마무리하며">마무리하며</h3>
<p align="center">
    <img src="https://velog.velcdn.com/images/sj_yun/post/40b35627-3957-46d3-b0da-057c294d7426/image.jpeg" alt="이미지">
</p>



<p>4개월간의 스태프 활동을 작성하던중 행사 마지막 Q&amp;A 시간에 테오가 한 말이 떠올랐다. <strong>&quot;불안함을 이기기 위해서 무엇이라도 활동 하는 것이 큰 도움이 될 것이다&quot;</strong>  돌이켜보니, 처음에는 고립감과 불안함에서 시작한 작은 용기가 이렇게 값진 경험이 되어 돌아올 줄은 몰랐다. 그저 한 걸음을 내딛었을 뿐인데, 그 발걸음이 이렇게 새로운 경험을 하게 해 줄 줄이야. 이 경험을 함께해준 테오와 스태프들, MC루키 그리고 멋진 경험을 공유해 주신 스피커분들과 참가자분들에게 감사하다는 말씀을 드리며 마무리 하고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[기술은 수단이지 목적이 아닙니다]]></title>
            <link>https://velog.io/@sj_yun/%EA%B8%B0%EC%88%A0%EC%9D%80-%EC%88%98%EB%8B%A8%EC%9D%B4%EC%A7%80-%EB%AA%A9%EC%A0%81%EC%9D%B4-%EC%95%84%EB%8B%99%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@sj_yun/%EA%B8%B0%EC%88%A0%EC%9D%80-%EC%88%98%EB%8B%A8%EC%9D%B4%EC%A7%80-%EB%AA%A9%EC%A0%81%EC%9D%B4-%EC%95%84%EB%8B%99%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Fri, 06 Sep 2024 08:04:40 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>최근 웹 애플리케이션의 복잡도는 그 어느 때보다 빠르게 증가하고 있습니다. 이와 함께 이를 해결하고 개선하기 위한 다양한 기술과 아키텍처가 매일같이 등장하고 있죠. 특히 프론트엔드 분야는 그 변화의 속도가 놀라울 정도로 빠르다고 느껴집니다. 매년 새로운 프레임워크, 상태 관리 라이브러리, 디자인 패턴이 쏟아지면서, 개발자들은 트렌드를 따라가기 위해 끊임없이 공부하고 고민하게 됩니다.</p>
<p>그렇다면, 정말 <strong>모든 새로운 기술</strong>을 고려하고 도입해야 할까요? 이번 포스팅에서는 <strong>기술 선택의 기준</strong>에 대해 이야기하며, 트렌드에만 의존하지 않고 프로젝트에 <strong>정말 필요한 기술을 선택하는 방법</strong>에 대해 고민해보려 합니다.</p>
<ul>
<li>next14(app router)</li>
<li>Feature-Sliced Design (FSD)</li>
<li>Zustand</li>
</ul>
<p>나름대로 요즘 주목받고 있다고 생각되는 기술과 아키텍처를 간단히 나열해봤습니다. 여러분은 기술을 도입하고 사용할 때 어떤 기준을 가지고 선택하시나요? 요즘 핫한 기술이라서, 주변 개발자분들이 많이 사용해서 등 다양한 이유가 있을 수 있습니다.</p>
<p>이러한 이유로 새로운 기술을 접하는 것은 자연스러운 일이지만, 위 이유만으로 기술 스택을 도입하고 사용하는 것은 위험하다고 생각합니다. 최소한 그 기술이 널리 사용되는 이유와 현재 사용하고 있는 기술과 비교했을 때 <strong>어떤 장단점이 있는지와 해당 기술이 가진 한계점에 대해 충분히 고려한 후</strong> 도입해야 된다 생각합니다.</p>
<h3 id="트렌드가-정답은-아니다">트렌드가 정답은 아니다</h3>
<p>사실 당연한 말이지만, 막상 주변에서 여러 사람들이 특정 기술이 좋다고 추천할 때,  나만 그 기술을 쓰지 않으면 뒤처지는 것 같은 느낌이 들기 마련입니다. 그러다 보면 새로운 기술로 바꿀 핑계를 찾기 위해, 지금까지 잘 사용해오던 기술에 괜히 트집을 잡게 되는 경우도 종종 생기죠. 하지만 이럴 때일수록 <strong>문제 해결에 정말 필요한 기술인지, 프로젝트의 요구 사항에 적합한 기술인지</strong>를 판별하는 것이 제일 중요하다고 생각합니다.</p>
<h3 id="나무-대신-숲을-보자">나무 대신 숲을 보자</h3>
<p>새로운 기술은 마치 숲 속에서 자라나는 나무와 같습니다. 나무가 숲에 뿌리를 내리고 성장하듯, 새로운 기술도 기존 기술을 기반으로 발전하고 파생되는 경우가 많습니다. 예를 들어, Zustand는 Redux의 <code>useSelector</code> 훅과 비슷한 역할을 하지만, 컴포넌트 트리에 종속되지 않고 상태를 다루는 것을 개선한 <code>useStore</code> 훅을 사용하여 React 컴포넌트가 아닌 비 UI 로직에서도 상태를 쉽게 사용할 수 있다는 장점을 제공합니다. Jotai는 Recoil과 유사하게 <code>atom</code> 개념을 사용하지만, 이를 더욱 단순화하여 상태 관리의 유연성과 직관성을 높였습니다. 이처럼 새로운 라이브러리들은 기존 기술에 뿌리를 두고 발전하면서, 특정 문제를 해결하거나 개선하는 방향으로 진화합니다.</p>
<p>그렇기에, 기존 기술의 원리와 작동 방식을 충분히 이해하고 있다면, 새 기술도 그 연장선에서 자연스럽게 받아들일 수 있기에 너무 조급하거나 불안해하지 않아도 된다 생각합니다.</p>
<h3 id="기술-선택-기준">기술 선택 기준</h3>
<p>기술을 선택하는 기준은 사람마다, 혹은 상황에 따라 다를 수 있지만, 제가 생각하는 기술 선택 요소는 다음과 같습니다</p>
<p><strong>1. 현재 프로젝트의 문제를 해결할 수 있는 기술인가?</strong>
도입하려는 기술이 실제로 프로젝트에서 직면한 문제를 해결할 수 있는지 신중하게 검토해야 합니다. 기술 자체가 목적이 되어서는 안 되며, 문제 해결과 효율성 향상을 위한 목적으로 접근해야 합니다. 해당 기술이 프로젝트의 문제를 구체적으로 어떻게 해결할 수 있는지, 그리고 기존에 사용 중인 기술보다 더 나은 방법을 제공하는지 고민하는 것이 중요합니다.</p>
<p><strong>2. 팀 프로젝트라면, 팀원들이 쉽게 사용할 수 있는 기술인가?</strong>
혼자 진행하는 프로젝트라면 문제가 없겠지만, 팀 단위로 작업하는 경우라면 팀원들이 새로운 기술을 쉽게 습득하고 적용할 수 있는지 고려해야 합니다. 러닝 커브가 너무 높으면, 학습에 드는 시간과 비용이 커져 프로젝트 일정에 부담을 줄 수 있기에 해당 기술 도입전 팀원들과 충분한 상의를 겨처 도입해야 합니다.</p>
<p><strong>3. 기술의 단점도 충분히 파악했는가?</strong>
모든 기술에는 장점뿐만 아니라 단점도 존재합니다. 도입하려는 기술의 장점만큼이나 한계와 단점을 충분히 이해하고, 그것이 프로젝트에 어떤 영향을 미칠지 어느정도 예측해보면 도입 여부를 보다 확실하게 선택할 수 있습니다.</p>
<h3 id="기술-학습-방법">기술 학습 방법</h3>
<p>기술을 도입하기로 결정했다면, 이제는 그 기술을 학습하기 시작할 텐데요, 공식문서, 블로그, 유튜브, 인터넷 강의 등 학습 방법은 다양하지만, 저는 공식문서가 있다면 항상 공식문서를 우선적으로 참고합니다.</p>
<h4 id="공식문서를-먼저-읽어보자">공식문서를 먼저 읽어보자</h4>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/12747387-841a-4e4a-a8eb-1544ca56ded1/image.jpg" alt=""></p>
<p>기술 도입 시 가장 먼저 공식문서를 참고하는 이유는, 해당 기술을 개발한 개발자들이 직접 작성한 문서이기 때문에 신뢰성과 정확성 면에서 뛰어나기 때문입니다. 그럼에도 불구하고 많은 사람들이 공식 문서를 읽는 것을 주저하는 가장 큰 이유는 언어 장벽에서 오는 거부감이라고 생각합니다. 저 역시 과거에는 개인 블로그나 번역된 자료를 더 선호했지만, 종종 잘못된 정보가 포함된 경우가 많았고, 이를 해결하는 데 오히려 더 많은 시간이 들었던 경험이 있습니다. 시간이 조금 더 걸리더라도 공식문서를 먼저 읽는 것이 결국에는 시간을 절약하는 방법이라 생각합니다.</p>
<h4 id="영어-울렁증이-있어요">영어 울렁증이 있어요…</h4>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/511ba326-d96b-49e9-8e79-a5f9542ba6db/image.jpg" alt=""></p>
<p>영문서가 부담스러운 경우에는, 해당 기술의 공식 문서를 번역한 오픈소스나, 아티클이 있는지를 먼저 검색하거나 <a href="https://www.deepl.com/ko/pro?utm_term=deepl&amp;utm_campaign=KO%7CSearch%7CC%7CBrand%7CKorean%7CExact&amp;utm_source=google&amp;utm_medium=paid&amp;hsa_acc=1083354268&amp;hsa_cam=20838049608&amp;hsa_grp=156042610226&amp;hsa_ad=683813589325&amp;hsa_src=g&amp;hsa_tgt=kwd-370669574285&amp;hsa_kw=deepl&amp;hsa_mt=e&amp;hsa_net=adwords&amp;hsa_ver=3&amp;gad_source=1&amp;gclid=CjwKCAjwreW2BhBhEiwAavLwfK-bfTgzveDt9TV6u1KNfPK2221_CLLRHzV_hP7hrTcf-VcDiyAP7xoCRQUQAvD_BwE">DeepL</a>과 같은 번역 확장 프로그램을 통해 번역에 도움을 받는 방식을 사용하는 것을 추천합니다.  <a href="https://github.com/reactjs/ko.react.dev">React</a>, <a href="https://github.com/luciancah/nextjs-ko">NextJs</a>, <a href="https://github.com/ssi02014/react-query-tutorial">TanstackQuery</a>와 같이 국내에서 많이 이용하는 기술의 경우에는 오픈소스를 통해 번역이 잘 되어있습니다.</p>
<h3 id="맺음말">맺음말</h3>
<p>기술은 빠르게 발전하고 새로운 트렌드는 끊임없이 등장합니다. 트렌드를 따라가는 것도 중요하지만, 궁극적으로는 우리에게 맞는 도구를 선택해 문제를 해결하는 것이 더 중요한 목표입니다. 결국,** 기술은 수단일 뿐이며, 문제 해결을 위한 도구로서 그 본질을 이해하고 사용한다면** 더 현명한 판단을 내릴 수 있을 것이라 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[주니어 프론트엔드 개발자의 2024년 상반기 회고]]></title>
            <link>https://velog.io/@sj_yun/%EC%A3%BC%EB%8B%88%EC%96%B4-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@sj_yun/%EC%A3%BC%EB%8B%88%EC%96%B4-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 22 Jul 2024 13:24:11 GMT</pubDate>
            <description><![CDATA[<h2 id="2024년-상반기를-기록하며">2024년 상반기를 기록하며..</h2>
<p>안녕하세요 프론트엔드 개발자 윤서준입니다. 그동안 블로그 글을 작성할 때는 주로 지식 기록, 트러블슈팅, 구현 내용을 중심으로 다루어왔었지만 올해 상반기는 기술적인 성장만이 아닌 여러 경험을 통해 내적으로 단단해졌음을 느끼는 시간이었습니다.</p>
<p>해당 경험을 글로 기록해두면 좋을것 같아 이번 상반기를 되짚어보며 내적으로 어떤 변화를 겪었는지, 그리고 하반기에는 어떤 방향으로 나아가야 할지 기록해보는 회고를 작성해보겠습니다. </p>
<br/>

<h2 id="1월--2월">1월 ~ 2월</h2>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/6383f734-69d2-411e-8386-2583d82a97e7/image.jpg" alt=""></p>
<p>1월은 작년 11월 말부터 개발을 시작한 <a href="https://github.com/MusicDigging/Mudig_FE">뮤딕</a> 프로젝트의 유저 피드백을 적용하며 마무리했습니다. 처음으로 디자이너와 백엔드 개발자들과 협업한 프로젝트였던 만큼, 기획과 개발 과정에서 많은 변경 사항과 조율이 필요했지만 그 덕분에 완성도 높은 결과물을 만들 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/58eebad0-8393-4c4d-b0e6-86e540722435/image.png" alt=""></p>
<p>뮤딕 프로젝트를 통해 프론트엔드 개발자의 시각에서만 바라보던 저에게 다른 직군과의 협업과 소통을 통해 각 직군이 겪는 고충과 작업 방식을 어느정도 이해할 수 있었고, 배포 이후 약 40여명의 유저 피드백을 통해 놓쳤던 인사이트들을 배포 이후에도 qa를 바탕으로 개선하는 경험을 쌓을 수 있었습니다. </p>
<p>뮤딕을 통해 좋은 사람들도 만나 이야기를 나누고 프로젝트가 끝난 후에도 팀원들과 같이 스터디를 진행하며 늘어지지 않고 지속적으로 공부할 수 있는 값진 경험이었습니다.</p>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/1ab40a0f-eac4-46d3-b9f3-ccc12ba98e68/image.png" alt=""></p>
<p>2월에는 뮤딕 프로젝트를 함께한 팀원들과 스터디를 통해 정보처리기사 시험을 준비했습니다. 약 한 달 정도 시나공 책, 무료 강의, 기출문제를 반복하여 공부한 후 시험에 응시했는데 다행히 기출문제에서 풀었던 문제들이 많이 나와 생각보다 수월하게(?) 합격할 수 있었습니다. </p>
<p>문제는 극악의 합격률을 가진 실기인데, 시간상 1회차 실기를 바로 준비하지 못해 뇌가 리셋된 상태에서 2회차를 준비중입니다..ㅎ</p>
<br/>

<h2 id="3월">3월</h2>
<p>올해 1월부터 3월은 개인적으로 취업 준비를 하면서 가장 힘든 기간이었습니다.</p>
<p>꾸준히 이력서를 제출하며 취업 준비를 했지만 계속되는 서류와 면접 탈락은 점점 자존감과 멘탈을 갉아먹기 시작했고, 불안한 생각들을 떨쳐 내기 쉽지 않았습니다.</p>
<div style="display: flex;  justify-content: center; align-items: center;">
<img src="https://velog.velcdn.com/images/sj_yun/post/7baffe2d-d82d-4b08-a7b6-5139cda56747/image.png" alt="Image" style="margin-bottom: 45px; width: 700px; height: 400px;">
</div>

<p>마침 이때 강지영 아나운서의 유퀴즈 인터뷰를 보게 되었고 인터뷰에서 아나운서가 되기까지의 과정을 이야기하면서 본인도 수없이 낙담하고 좌절했다고 합니다. 그렇지만 힘든 상황이 올 때마다 &quot;버티면 이겨&quot;, &quot;버티면 분명 기회가 올 거야&quot;라는 다짐을 스스로에게 되뇌며 준비했고 결국 아나운서가 될 수 있었다고 합니다. </p>
<p>이 인터뷰는 불안감에 휩싸였던 저에게 큰 위로가 되었고, 저 또한 &quot;버티면 이기는 거야&quot;라는 다짐을 마음에 새기며 다시금 동기부여를 받고 꾸준히 열심히 하다 보면 기회가 올거라 마음을 다잡았습니다. </p>
<p aline='center'>
     <img src="https://velog.velcdn.com/images/sj_yun/post/e02a92cc-3483-485b-855b-bcf9a3e52309/image.png" alt="Image" width="50%">    
</p>

<p text=''>고죠 사토루적 사고</p>
<br/>



<h2 id="4월--현재">4월 ~ 현재</h2>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/0e5166db-af51-4d58-a3fa-df29ae683610/image.png" alt=""></p>
<p>그렇게 꽁꽁 얼어붙은 취업 시장을 열심히 두드리다 드디어 프론트엔드 인턴으로 4월부터 출근하게 되었습니다.</p>
<p>실무를 경험할 수 있다는 생각에 설레기도 했지만, 한편으로는 멍하니 모니터만 바라보다 퇴근하지 않을까 걱정도 되었습니다. 설렘 반 걱정 반으로 첫 출근을 하였고, 첫날은 회사 관련 간단한 온보딩과 소개받은 뒤 점심을 먹고 맥북 세팅을 하다 보니 눈 깜빡할 새 지나갔습니다.</p>
<p>새로운 프로젝트가 진행되는 시점에 합류하게 되어, 이틀 차부터는 개발하고자 하는 서비스의 방향성과 구현 내역을 확인한 뒤 본격적으로 개발을 시작했습니다.</p>
<h2 id="실무를-통해-얻은-것">실무를 통해 얻은 것</h2>
<h4 id="나의-부족한-점">나의 부족한 점</h4>
<p>실무를 통해 얻은 것 중 가장 큰 경험은 제가 무엇이 부족한지를 객관적으로 파악할 수 있었다는 점입니다.</p>
<p>그동안의 개발 과정에서는 프로젝트가 일정 수준 이상으로 커지는 경험을 하지 못했었는데, 실무를 통해 프로젝트가 커지면서 기존의 저는 코드의 확장성과 가독성을 깊게 고려하지 않고 코드를 작성해왔다는 사실을 알게 되었습니다.</p>
<p>이를 통해 무조건 빠르게 구현하는 것이 좋은 방법이 아니라는 것을 깨닫고, 요즘에는 보다 근본적인 아키텍처와 읽기 좋은 코드를 짜는 것을 고민하며 전체적인 개발 시각을 넓히는데 집중하고 있습니다.</p>
<h4 id="배포-환경을-지속적으로-확인하기">배포 환경을 지속적으로 확인하기</h4>
<p>이전에는 하나의 큰 페이지 작업이나 프로젝트를 완료한 후 한 번에 배포하는 방식으로 진행하였으나 실제 업무를 하면서 로컬 환경과 배포 환경 간의 성능 차이를 많이 느끼게 되었습니다.</p>
<p>회사가 스마트팜 관련 도메인을 다루고 있어 실시간 및 누적 기상 정보와 같은 대량의 데이터를 처리하는 일이 빈번한데 로컬 환경에서는 큰 지연 없이 동작하던 기능들이 배포 환경에서는 자주 지연되는 현상을 경험하게 되었고 여러 기능을 한꺼번에 배포하다 보니 문제 발생 지점을 특정하고 디버깅하는데 어려움을 겪었습니다.</p>
<p>이러한 문제를 해결하기 위해, 구현이 완성된 상태에서 배포하자는 욕심을 버리고 구현 단위 기준을 설정하여 기준을 달성할 때마다 즉시 배포하는 방식으로 전환하였습니다. 구현 단위를 소규모로 나누어 작업하면서 개발 집중도가 높아졌고, 배포 환경을 지속적으로 테스트하며 문제가 발생하면 팀원들과 신속하게 공유할 수 있게 되었습니다. 이로 인해 디버깅 시간이 줄어들고 전체적인 개발 효율도 개선할 수 있었습니다.</p>
<h4 id="리프레쉬">리프레쉬</h4>
<p>백수 탈출 기념으로 오랜만에 미뤄뒀던 약속들도 잡고 7월 초엔 태안으로 여행도 다녀왔습니다. 첫 한 달간은 적응하느라 정신없어서 가끔은 약속보다 주말에 좀 쉬고 싶기도 했지만, 간만에 무거운 마음 내려놓고 즐겁게 놀았습니다.</p>
<p>| <img src="https://velog.velcdn.com/images/sj_yun/post/5259f767-569c-4916-b81c-1833e9afcd60/image.jpg" width="100%"> |  | <img src="https://velog.velcdn.com/images/sj_yun/post/94269f52-83d3-4d73-8ccc-a42ded77e758/image.jpg" width="100%"> | <img src="https://velog.velcdn.com/images/sj_yun/post/75bbe5a1-40b8-431c-845e-da0ecf82d6bf/image.jpg" width="100%"> |
|:---:|:---:|:---:|:---:|:---:</p>
<p>(너무 잘먹은 덕분에 3키로가 쪘습니다...ㅋ 운동해야지)</p>
<br/>

<h4 id="오픈-소스-기여">오픈 소스 기여</h4>
<p>상반기 동안 가장 인상 깊었던 활동 중 하나는 처음으로 오픈 소스 프로젝트에 기여한 경험입니다. 그동안 오픈 소스 프로젝트에 참여하고 싶다는 열망이 있었고, 가능하다면 많은 사람들이 사용하는 소스에 기여하고 싶었습니다.</p>
<p>그런 와중에 리액트 공식 문서 번역 오픈 소스 프로젝트를 알게 되었고, <a href="https://ko.react.dev/reference/rules">리액트의 규칙</a> 페이지의 번역 작업을 맡아 진행하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/33643876-ee23-4203-ac5b-e11811ea466a/image.jpg" alt="">
비록 번역한 페이지는 짧은 한 페이지일지라도, 첫 오픈 소스 기여가 리액트 공식 문서라는 점에서 저에게는 특별하고 의미 있는 경험이었습니다.</p>
<br/>

<h2 id="하반기-목표">하반기 목표</h2>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/0d7ccb1e-5024-4c38-b3f9-4c31bec2cdfe/image.png" alt=""></p>
<blockquote>
<ol>
<li>개발 역량 강화
몸소 부딪히면서 아키텍처와 디자인 패턴의 중요성을 깨닫게 된만큼 어떤 상황에서, 어떻게 사용해야 될지에 대한 어느 정도의 감이 잡혔습니다. 이를 확신으로 변화시키기 위해 지속적으로 공부하며, 기존에 진행해왔던 프로젝트들의 코드를 일관적이고 수정 및 확장이 용이한 코드로 리팩토링할 계획입니다.</li>
</ol>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/6851f98c-9e88-4ddf-8dab-af0762b9e59c/image.png" alt=""></p>
<blockquote>
<ol start="2">
<li>5권 이상 독서하기
매년 다짐만 하고 실패하는 목표지만, 올해는 그래도 상반기에 2권 정도 읽었습니다. 하반기에는 리액트 딥다이브와 우아한 타입스크립트를 완독하는 것을 목표로 잡았습니다. ( + 비개발 서적도 틈틈히 읽기)</li>
</ol>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/0ebf60d0-63ac-4e30-93e0-b67e8988c198/image.png" alt=""></p>
<blockquote>
<ol start="3">
<li>해외 여행
스무살 일본 여행 이후 해외 여행을 가지 못했는데 올해는 여유가 좀 생겼으니 올해 가을, 겨울중엔 해외여행을 떠나볼 예정입니다.
(현재는 대만 or 일본 생각중)</li>
</ol>
</blockquote>
<br/>

<h2 id="마무리하며">마무리하며</h2>
<p>유독 올 초 6개월이 빠르게 흘러간듯한 느낌이 있었는데, 이렇게 회고를 작성하고 보니 생각보다 많은 일들이 있었네요. 아직도 개발자로서 가야 할 길이 멀다고 생각하지만, 이번 상반기를 돌아보며 얻은 교훈과 경험들을 바탕으로 하반기에도 지치지 않고 꾸준히 나아가야겠습니다. 이 글을 보시는 모든 분들 또한 하반기에 이루고자 하는 일들 다 잘되기를 바랄게요😄.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] WebSocket 통신하기]]></title>
            <link>https://velog.io/@sj_yun/React-WebSocket-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sj_yun/React-WebSocket-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 14 May 2024 07:12:51 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@sj_yun/Web-Socket%EC%9D%B4%EB%9E%80">전 포스팅</a>에서는 웹소켓의  정의, 등장 배경, 동작방식, 한계점 등을 알아보았고, 이번 포스팅에선 React에서 WebSocket을 어떻게 사용했는지 기록하고자 합니다. </p>
<pre><code class="language-ts">import { useEffect, useRef, useState } from &#39;react&#39;;
import Loader from &#39;./Loader&#39;;

export default function Cctv() { 
  const ws = useRef&lt;WebSocket | null&gt;(null);
  const cctvUrl = &#39;wss://cctv.example.com&#39; // cctv webSocekt Url
  const [cctvData, setCctvData] = useState(&#39;&#39;);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() =&gt; {
    ws.current = new WebSocket(cctvUrl);
    ws.current.onopen = () =&gt; {
      console.log(&#39;WebSocket connection opened.&#39;)
      setIsLoading(false);
    }
    ws.current.onmessage = (event) =&gt; {
      if (event.data) {
        const base64ImageData = &#39;data:image/jpg;base64,&#39; + event.data;
        setCctvData(base64ImageData);
      }
    };
    ws.current.onerror = () =&gt; console.log(&#39;WebSocket Error&#39;);
    ws.current.onclose = () =&gt; {
      console.log(&#39;Websocket connection is closed&#39;);
    };

    return () =&gt; {
      if (ws.current &amp;&amp; ws.current.readyState === 1) {
        ws.current.close();
      }
    };
  }, []);

  return (
    &lt;div className=&#39;cctv-container relative&#39;&gt;
      {isLoading &amp;&amp; &lt;Loader /&gt;} 
      &lt;img
        id=&#39;imageCCTV&#39;
        width=&#39;360&#39;
        height=&#39;220&#39;
        src={cctvData}
        alt=&#39;CCTV&#39;
        className={`cctv-image rounded-lg ${isLoading ? &#39;hidden&#39; : &#39;&#39;}`}
      /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h3 id="웹소켓-설정">웹소켓 설정</h3>
<pre><code>const ws = useRef&lt;WebSocket | null&gt;(null);
</code></pre><ul>
<li>먼저 <code>useRef</code> 훅을 사용하여 컴포넌트 랜더링 시 WebSocket 연결을 감지하고 해당 값을 <code>ws</code>을 통해 지속적으로 감지합니다.
useState가 아닌 useRef를 사용하는 이유는 웹소켓 연결이 생성될 때 컴포넌트를 리렌더링되어도 변경되지 않는 값이기 떄문에 WebSocket 연결을 지속적으로 감지하기에는 useRef를 사용하는 것이 더 적합합니다.</li>
</ul>
<h3 id="웹소켓-연결-활성화">웹소켓 연결 활성화</h3>
<pre><code class="language-js"> useEffect(() =&gt; {
    ws.current = new WebSocket(cctvUrl);
    ws.current.onopen = () =&gt; {
      console.log(&#39;WebSocket connection opened.&#39;)
      setIsLoading(false);
    }
    ws.current.onmessage = (event) =&gt; {
      if (event.data) {
        const base64ImageData = &#39;data:image/jpg;base64,&#39; + event.data;
        setCctvData(base64ImageData);
      }
    };
    ws.current.onerror = () =&gt; console.log(&#39;WebSocket Error&#39;);
    ws.current.onclose = () =&gt; {
      console.log(&#39;Websocket connection is closed&#39;);
    };

    return () =&gt; {
      if (ws.current &amp;&amp; ws.current.readyState === 1) {
        ws.current.close();
      }
    };
  }, []);</code></pre>
<p> <code>useEffect</code>  훅을 사용하여 컴포넌트가 마운트될 때<code>new WebSocket(cctvUrl)</code>을 통해 소켓을 생성한뒤  <code>ws.current</code>로 실시간 연결을 진행합니다.</p>
<p>지난 포스팅에서도 다뤘듯 소켓이 정상적으로 생성되었으므로 아래 4개의 이벤트를 사용할 수 있게 됩니다.</p>
<ul>
<li><code>open</code> – 연결이 성공적으로 되었을 때 발생</li>
<li><code>message</code> – 데이터를 수신하였을 때 발생</li>
<li><code>error</code> – 연결 상 에러가 생겼을 때 발생</li>
<li><code>close</code>  – 연결이 종료되었을 때 발생</li>
</ul>
<p>데이터 수선시 event를 감지해 <code>event.data</code> 값이 있다면 해당 데이터를 <code>base64ImageData</code>에 담고 있습니다.
여기서 중요한 점은 WebSocket을 통해 전송되는 데이터는 주로 ** 텍스트 기반** 입니다. 하지만 CCTV 영상과 같은 이미지 데이터는 ** 이진 형식 ** (컴퓨터 파일)으로 저장되어 있습니다. 이진 데이터를 텍스트 형식으로 전송하기 위해  ** Base64** 인코딩이 필요하므로 &#39;<code>data:image/jpg;base64,</code>를 통해 변환하여 데이터는 URL 형태로 만들어지고, 이를 상태 변수인 cctvData에 업데이트합니다. 이렇게 업데이트된 cctvData는 JSX에서 이미지 태그의 src 속성에 할당되어 화면에 실시간 CCTV 영상이 표시됩니다.</p>
<h3 id="웹소켓-연결-종료">웹소켓 연결 종료</h3>
<pre><code class="language-js">    return () =&gt; {
      if (ws.current &amp;&amp; ws.current.readyState === 1) {
        ws.current.close();
      }
    };</code></pre>
<p> 언마운트 사이클인 clean up을 통해  WebSocket 연결이 활성화되어 있는 경우에만 연결을 종료하도록 하여 컴포넌트의 생명 주기 동안 WebSocket 연결을 안정적으로 관리하고 종료하도록 설정하였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 소켓 (Web Socket)이란?]]></title>
            <link>https://velog.io/@sj_yun/Web-Socket%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@sj_yun/Web-Socket%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Sat, 20 Apr 2024 07:09:48 GMT</pubDate>
            <description><![CDATA[<p>이번에 실무를 경험하게 되면서 실시간으로 CCTV 영상을 화면에 구현하기 위해 웹소켓을 사용하였습니다.웹 소켓의 역할만 두루뭉실하게 알고 있었던 상황인지라 근본적으로 깊게 알아볼 필요가 있다 생각해 포스팅하게 되었습니다.</p>
<h3 id="웹-소켓web-socket">웹 소켓(Web Socket)</h3>
<p>WebSocket은 웹 앱과 서버 간의 지속적인 연결을 제공하는 프로토콜입니다. 이를 통해 서버와 클라이언트 간에 양방향 통신이 가능해집니다. HTTP와는 달리, WebSocket 연결은 한 번 열린 후 계속 유지되므로, 서버나 클라이언트에서 언제든지 데이터를 전송할 수 있다는 것이 특징입니다. 그렇기에 <strong>실시간으로 진행되는 통신에서 적극적으로 사용하고 있습니다.</strong></p>
<h3 id="웹-소켓web-socket-의-등장-배경">웹 소켓(Web Socket) 의 등장 배경</h3>
<p>초기의 인터넷 통신 방식은 주로 <code>HTTP</code> 를 이용한 클라이언트(요청) - 서버(응답) 모델을 통해 진행되었습니다.
즉, 클라이언트가 서버에 <strong>요청(Request)</strong>을 보내고, 서버가 이에 <strong>응답(Response)</strong>하는 통신 방식을 따릅니다.
 
현재 가장 많이 쓰는 기술중 하나이며 대부분의 작업에서는 큰 문제가 없어 보입니다.
하지만, 실시간으로 데이터를 주고받는 데에는  한계점이 발생하게 됩니다.</p>
<p>요청과 응답이 있다는 것은 클라이언트가 서버에게 요청하지 않는 이상 서버는 클라이언트에게 먼저 데이터를 보낼 수 없게 되기 때문에 클라이언트는 항상 새로운 데이터가 있는지 확인을 하기 위해서는 서버에 지속적으로 요청을 보낼 수밖에 없습니다.</p>
<p> 그렇게 되면 트래픽을 불필요하게 증가시키고, 이로 인해 서버의 비용이 증가될 뿐더러 요청과 응답사이의 지연시간이 있기 때문에 실시간 통신의 효율성을 저하시킬 수 있습니다.</p>
<p>이러한 상황을 방지하고 해결하기 위해 사용하는 것이 웹 소켓입니다.</p>
<h3 id="웹-소켓web-socket이란">웹 소켓(Web Socket)이란?</h3>
<p>웹 소켓은 HTML5에 등장 실시간 웹 애플리케이션을 위해 설계된 통신 프로토콜이며, TCP(Transmission Control Protocol)를 기반으로 합니다.
TCP를 기반으로 한 웹 소켓은 신뢰성 있는 데이터 전송을 보장하며, 메시지 경계를 존중하고, 순서가 보장된 양방향 통신을 제공할 수 있습니다.</p>
<p>HTTP와 다르게 클라이언트와 서버 간에 최초 연결이 이루어지면, 이 연결을 통해 양방향 통신을 지속적으로 할 수 있습니다.
즉 전화통화나 채팅같이 양쪽 모두에서 정보를 주고받을 수 있다는 의미입니다.</p>
<p>이때 데이터는 <strong>패킷(packet)</strong> 형태로 전달되며, 전송은 연결 중단과 추가 HTTP 요청 없이 양방향으로 이뤄집니다.</p>
<p>그렇다면 이제 웹소켓을 연결하는 방식에 대해 알아보겠습니다.</p>
<p>연결 자체는 매우 간단합니다. 클라이언트 환경에서는 별도의 패키지 설치 없이 브라우저 자체에서 web Socet Api를 지원하므로 new 웹소켓만 호출하여 사용하면 됩니다.
 이때 <code>ws</code> 라는 특수 프로토콜을 사용합니다</p>
<pre><code class="language-js">const socket = new WebSocket(&quot;wss://example/chat&quot;);</code></pre>
<p>소켓이 정상적으로 생성되면 아래 4개의 이벤트를 사용할 수 있게 됩니다.</p>
<ul>
<li><code>open</code> – 연결이 성공적으로 되었을 때 발생</li>
<li><code>message</code> – 데이터를 수신하였을 때 발생</li>
<li><code>error</code> – 연결 상 에러가 생겼을 때 발생</li>
<li><code>close</code> – 연결이 종료되었을 때 발생</li>
</ul>
<pre><code class="language-js">let socket = new WebSocket(&quot;wss://example/chat&quot;);

socket.onopen = function(e) {
  alert(&quot;[open] 커넥션이 만들어졌습니다.&quot;);
  socket.send(&quot;안녕!&quot;);
};

socket.onmessage = function(event) {
  alert(`[message] 서버로부터 전송받은 데이터: ${event.data}`);
};

socket.onclose = function(event) {
  if (event.wasClean) {
    alert(`[close] 커넥션이 정상적으로 종료되었습니다(code=${event.code} reason=${event.reason})`);
  } else {
    // 예시: 프로세스가 죽거나 네트워크에 장애가 있는 경우
    // event.code가 1006이 됩니다.
    alert(&#39;[close] 커넥션이 죽었습니다.&#39;);
  }
};

socket.onerror = function(error) {
  alert(`[error]`);
};
</code></pre>
<h3 id="웹-소켓web-socket-handshake">웹 소켓(Web Socket) handshake</h3>
<p>방금 위에서 본 코드와 같이 WebSocket(url)을 호출해 소켓을 생성하면 즉시 연결이 시작됩니다.
<code>new WebSocket(&#39;wss://example/chat&#39;)</code> 을 호출해 최초 요청을 전송했다고 가정하고 이때의 요청 헤더를 살펴보겠습니다.</p>
<pre><code>GET /chat
Host: example
Origin: https://example
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13</code></pre><ul>
<li><p><code>Origin</code> : 클라이언트 Origin을 나타냅니다.</p>
</li>
<li><p><code>Connection</code> : 클라이언트 측에서 프로토콜을 바꾸고 싶다는 신호를 보냈다는 것을 나타냅니다. (https ➡️ wss)</p>
</li>
<li><p><code>Upgrade</code> :  클라이언트 측에서 요청한 프로토콜은 &#39;websocket’이라는 것을 의미합니다. (https ➡️ wss)</p>
</li>
<li><p><code>Sec-WebSocket-Key</code> :  보안을 위해 브라우저에서 생성한 키로, 서버가 웹소켓 프로토콜을 지원하는지를 확인하는 데 사용됩니다</p>
</li>
<li><p><code>Sec-WebSocket-Version</code> : 웹소켓 프로토콜 버전을 나타냅니다.</p>
</li>
</ul>
<p>다음으로 서버가 해당 요청을 받으면 웹 소켓 연결을 수락하는 응답을 보냅니다.
해당 응답에는 &#39;101 Switching Protocols&#39; 상태 코드와 함께 응답합니다.</p>
<pre><code>101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBYongNyong24s99EO10UlZ22C2g=
</code></pre><p>이 과정을 통해서 클라이언트와 서버 간 실시간 통신이 가능한 통로가 열리게 되는 것입니다.
 
이렇게 웹 소켓 연결이 성립되면, 클라이언트와 서버 간에 실시간 양방향 통신이 가능해집니다.</p>
<h3 id="웹-소켓web-socket의-한계점">웹 소켓(Web Socket)의 한계점?</h3>
<p>양방향 통신을 하는것에 있어 큰 장점을 가지고 있는 웹소켓은 어떤 단점이 있을까요? </p>
<ul>
<li><p>브라우저 지원 : 웹 소켓은 HTML5 사양의 일부이므로 HTML5를 지원하지 않는 브라우저에서는 사용할 수 없습니다.</p>
</li>
<li><p>서버 비용: 웹소켓은 한번 연결되면 별도의 에러나, 지시가 없는한 지속적인 연결을 유지하므로, 많은 수의 웹소켓 연결을 동시에 관리하는 경우 서버의 부하가 증가할 수 있습니다.</p>
</li>
<li><p>상세한 에러처리: 만약 연결이 끊어졌을 시  웹소켓은 연결이 끊어진 이유에 대해 정확히 알 수 없습니다. 따라서 그에 대한 에러 처리 또한 쉽지 않습니다.</p>
</li>
</ul>
<p>이로서, 실시간 양방향 통신을 쉽게 구현할 수 있도록 해주는 웹 소켓(Web Socket)에 대한 정의, 등장 배경, 동작방식, 한계점에 대해서 알아봤습니다. 다음 포스팅에선 실제로 React와 함께 웹소켓을 어떻게 사용했는지를 기록하고자 합니다. </p>
<p>REFERENCE:<a href="https://ko.javascript.info/websocket#ref-906">https://ko.javascript.info/websocket#ref-906</a>
<a href="https://developer.mozilla.org/ko/docs/Web/API/WebSocket">https://developer.mozilla.org/ko/docs/Web/API/WebSocket</a>
<a href="https://yong-nyong.tistory.com/90#article-2-3--%EC%9B%B9-%EC%86%8C%EC%BC%93(web-socket)%EC%9D%98-%ED%95%9C%EA%B3%84%EC%A0%90">https://yong-nyong.tistory.com/90#article-2-3--%EC%9B%B9-%EC%86%8C%EC%BC%93(web-socket)%EC%9D%98-%ED%95%9C%EA%B3%84%EC%A0%90</a>
<a href="https://velog.io/@sugenius77/%EC%9B%B9-%EC%86%8C%EC%BC%93Web-Socket">https://velog.io/@sugenius77/%EC%9B%B9-%EC%86%8C%EC%BC%93Web-Socket</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React+ Vite 환경에서 .env 환경변수 설정법]]></title>
            <link>https://velog.io/@sj_yun/React-Vite-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-.env-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95%EB%B2%95</link>
            <guid>https://velog.io/@sj_yun/React-Vite-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-.env-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95%EB%B2%95</guid>
            <pubDate>Wed, 10 Apr 2024 06:53:56 GMT</pubDate>
            <description><![CDATA[<p>오늘은 React+ Vite 환경에서 .env 환경변수 설정중 마주친 오류에 관한 글을 작성해보자 합니다.</p>
<h2 id="🧑🏻💻-발생한-문제">🧑🏻‍💻 발생한 문제</h2>
<p>현재 진행 중인 프로젝트에서 특정 정보를 가진 key 값들을 비공개로 관리하기 위해 최상위 루트에서. env 환경 변수를 생성하고 각각의 값들을 설정하였으나 갑자기 <code>Uncaught ReferenceError: process is not defined</code> 에러가 발생하였다 .env 환경 변수 설정 직후 발생한 오류이므로 <code>CreateReactApp</code>으로 세팅했을 때와 vite 환경에서의. env 설정법이 다를 수도 있겠다는 생각이 들어 바로 찾아보니 역시나 Vite 환경에서는 기존 방식과 다르게 환경 변수를 설정해야 됐다. 해당 문제는 기존의 방식과 어떻게 다른지 이해한 뒤 적용하면 손쉽게 해결할 수 있었다.</p>
<h3 id="기존-create-react-app에서-환경변수-사용법">기존 Create React App에서 환경변수 사용법</h3>
<ol>
<li>디렉토리 최상단에 <code>.env</code> 파일 생성</li>
<li>변수명 <code>REACT_APP_</code> 로 시작하는 변수생성</li>
<li>따옴표로 감싸거나 띄어쓰기를 주지 않는다<pre><code class="language-js">REACT_APP_APPLICATION_KEY = 설정하고자 하는 값  </code></pre>
4.필요한 곳에서 사용시<code>process.env.</code>변수명으로 호출<pre><code class="language-js">process.env.REACT_APP_APPLICATION_KEY</code></pre>
</li>
</ol>
<h3 id="vite에서-환경변수-사용법">Vite에서 환경변수 사용법</h3>
<ol>
<li>디렉토리에 .env파일 생성</li>
<li>변수명 <code>VITE_</code> 로 시작하는 변수생성</li>
<li>따옴표를 감싸도 괜찮음<pre><code class="language-js">VITE_APPLICATION_KEY = &quot;설정하고자 하는 값&quot;</code></pre>
</li>
<li>필요한 곳에서 사용시 <code>import.meta.env.</code>변수명으로 호출<pre><code class="language-js">const APPLICATION_KEY = import.meta.env.VITE_APPLICATION_KEY
</code></pre>
</li>
</ol>
<pre><code>

### 정리
- Create React App, Vite 모두 디렉토리 최상단에 `.env` 파일 생성 (깃허브 업로드 시 .env 파일은 올라가면 안되므로 gitignore 설정추가도 잊지말것!)
- 변수명 생성방식과 사용하고자 할때 불러오는 방식이 다르다
- Create React App: 생성: `REACT_APP_` / 호출:  `process.env.`
- Vite: 생성: `VITE_` / 호출: `import.meta.env.`

Reference: https://ko.vitejs.dev/guide/env-and-mode
https://velog.io/@tmdgp0212/TIL230316-using-.env-on-vite
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Tailwind CSS 사용기]]></title>
            <link>https://velog.io/@sj_yun/Tailwind-CSS-%EC%82%AC%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@sj_yun/Tailwind-CSS-%EC%82%AC%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Sun, 07 Apr 2024 06:50:20 GMT</pubDate>
            <description><![CDATA[<p>오늘은 Tailwind CSS를 사용해본 사용 후기에 관한 글을 작성해보고자 한다. 기본 설치과정과 세팅은 따로 다루지 않을 것으며 Tailwind CSS 공식 문서에 친철하게 각 개발 환경에 맞는 설치법을 지원하니 궁금하다면 (<a href="https://tailwindcss.com/docs/installation">링크</a>))를 클릭해 설치하길 권장한다.</p>
<h2 id="tailwind-css란">Tailwind CSS란?</h2>
<p>Tailwind CSS는 <code>Utility-First</code> 컨셉을 가진 CSS 프레임워크.  <code>m-1</code> , <code>flex</code> 와 같이 미리 세팅된 유틸리티 클래스를 활용하는 방식으로 HTML 태그 안에서 스타일링을 할 수 있다. HTML 태그 안에서 CSS를 정의하기 때문에 CSS 파일을 별도로 관리할 필요가 없으며 CSS 변수명 또한 정의할 필요가 없어 기존의 방식보다 훨씬 빠르게 작업할 수 있다는 큰 장점을 가지고 있다. </p>
<p>처음 들었을 땐 위와 같은 장점만 있으면 사용하기 너무 좋은거 아냐? 라고 생각했지만 그 장점이 곧 단점이 될 수도 있다는 사실을 알게 되었다. </p>
<h3 id="tailwind-css-사용을-꺼려했던-이유">Tailwind CSS 사용을 꺼려했던 이유</h3>
<p>개인적으로 HTML 태그 내에서는 깔끔한 코드로 전체적인 구조를 파악하는것을 좋아하기에 아래 사진과 같이 HTML 태그 내에서 작업하는 방식이 편하다 하더라도 가독성을 너무 해치고 보기 불편하다 생각했다. 또한 협업시에 이러한 코드를 보게 될 것을 상상하면 끔찍하다 생각했으며 새로운 유틸리티 클래스 문법에 익숙해지고 이를 조합하여 원하는 스타일을 달성하는 방법을 이해해야 되는것도 진입 장벽중 하나였다.
<img src="https://velog.velcdn.com/images/sj_yun/post/b4693e1a-bf9b-4d22-ba5c-c73bb70be171/image.png" alt=""></p>
<h3 id="그럼에도-불구하고-tailwind-css를-사용하게-된-이유">그럼에도 불구하고 Tailwind CSS를 사용하게 된 이유</h3>
<p>현제 진행중인 프로젝트에서는 전반적인 레이아웃과 어떤 스타일들을 적용해야 될지를 빠르게 테스트 해야 되는 환경이라 언제 바뀔 지 모르는 CSS 네이밍을 정의하며 개발하기에는 비효율적이라 느껴 Tailwind CSS를 사용하기로 결정하였다. </p>
<h3 id="생각보다-괜찮은데">생각보다 괜찮은데??</h3>
<p>그리고 실제로 사용해본  Tailwind CSS는 선입견을 가졌던 것 보다 괜찮았다. 일단 공식문서 자체에 유틸리티 클래스 문법들이 잘 설명이 되있어 기존 CSS 속성들과 유틸리티 클래스 문법들으 어떻게 다른지를 빠르게 파악하고 적용할 수 있었다.</p>
<p>또한 가독성 부분에서도 검색을 해보니 중복되는 속성들을 공통적으로 관리할 수 있다는 사실도 알게 되었다.</p>
<h3 id="layer를-통한-공통-속성-관리">@layer를 통한 공통 속성 관리</h3>
<p>버튼이나 카드 섹션같은  경우 여러 컴포넌트에 같은 스타일을 사용될 가능성이 매우 높다. 중복을 줄이기 위해선 <code>@layer</code> 문과 사용자 정의 CSS를 함께 사용해서 제어가 가능하다 </p>
<pre><code class="language-css">/*공통 CSS 정의 파일 */
@tailwind base;
@tailwind components;
@tailwind utilities;

.btn {
  background-color: skyblue;
  color: #fff;
  padding: 15px;
}</code></pre>
<pre><code class="language-html">&lt;button className=&quot;btn&quot; type=&quot;button&quot;&gt;
  button
&lt;/button&gt;</code></pre>
<pre><code class="language-html">@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .select2-dropdown {
    @apply rounded-b-lg shadow-md;
  }
  .select2-search {
    @apply rounded border border-gray-300;
  }
  .select2-results__group {
    @apply text-lg font-bold text-gray-900;
  }
  /* ... */
}</code></pre>
<h3 id="custom-styles">Custom Styles</h3>
<p>Tailwind CSS 자체에서도 다양한 색상들을 지원하지만 보다 구체적인 서비스를 위해 맞는 색상요소 설정이 가능하다. 
tailwind.config.js의 theme에 추가하여 사용할 수 있다. theme.extend.colors가 아닌 theme.colors에 추가하게 되면 기존 색상을 덮어쓰게 된다.</p>
<pre><code class="language-css">module.exports = {
  theme: {
    extend: {
      colors: {
        primary: &#39;#ffc107&#39;,
        secondary: &#39;#2979ff&#39;,
        success: &#39;00c07f&#39;,
        failure: &#39;ff6562&#39;,
      },
      fontSize: {
        &#39;15px&#39;: &#39;15px&#39;,
      },
    },
  },
};</code></pre>
<pre><code class="language-html">&lt;button type=&quot;button&quot; className=&quot;rounded-md bg-primary text-15px&quot;&gt;
  로그인
&lt;/button&gt;</code></pre>
<p>Tailwind CSS를 사용하다 보니 다른 요소들과 결합하기에도 좋은 프레임워크인것 같다. 실 개발에 앞서 공통 속성 설계만 팀원들과 상의한 사용한다면 팀 프로젝트에서도 충분히 사용 가능하다는 생각이 들었다. </p>
<p>Reference: <a href="https://leesangwondev.vercel.app/tailwind-css-%EB%98%91%EB%98%91%ED%95%98%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">https://leesangwondev.vercel.app/tailwind-css-%EB%98%91%EB%98%91%ED%95%98%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</a>
<a href="https://velog.io/@rmaomina/tailwindCSS-apply-rule">https://velog.io/@rmaomina/tailwindCSS-apply-rule</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[단일 책임의 원칙 적용하기]]></title>
            <link>https://velog.io/@sj_yun/%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84%EC%9D%98-%EC%9B%90%EC%B9%99-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sj_yun/%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84%EC%9D%98-%EC%9B%90%EC%B9%99-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 31 Mar 2024 11:13:51 GMT</pubDate>
            <description><![CDATA[<p>24년도 정보처리기사 필기 시험을 준비하면서, 이름만 들었던 SOlID 원칙의 개념에 대해 공부하게 되었다. 
공부를 하던 도중 실제로 나의 코드에 적용해보면 좋겠다는 생각이 들어, SOlID 중 하나인 단일책임원칙을 적용해보기로 했다.</p>
<h3 id="단일책임원칙single-responsiblity-principle">단일책임원칙(Single Responsiblity Principle)</h3>
<p>단일 책임 원칙(SRP)는 객체는 단 하나의 책임만 가져야 한다는 원칙을 말한다.
여기서 &#39;책임&#39; 이라는 의미는 하나의 &#39;기능 담당&#39;으로 볼 수 있다. 
즉, 하나의 클래스나 함수는 하나의 기능 담당하여 하나의 책임을 수행하는데 집중되어야 있어야 한다는 의미이다.</p>
<pre><code class="language-js">
const setUserInfo = useSetRecoilState(userInfoAtom);
const setSignupInfo = useSetRecoilState(signUpInfoAtom);
const navigate = useNavigate();

const sendCode = async (code: string, social: string) =&gt; {
    try {
      let response;
      if (social === &#39;kakao&#39;) {
        response = await postUserCode(code, &#39;kakao&#39;);
      } else if (social === &#39;google&#39;) {
        response = await postUserCode(code, &#39;google&#39;);
      }
      //가입 이력이 있을 경우
      if (response.message === &#39;로그인 성공&#39;) {
        const { user, token } = response;
        const { id, email, name, image, genre, about, rep_playlist } = user;
        const { access, refresh } = token;
        localStorage.setItem(&#39;token&#39;, access);
        localStorage.setItem(&#39;refreshToken&#39;, refresh);
        setIsLogin(true);
        setUserInfo({
          id,
          email,
          name,
          image,
          genre,
          about,
          rep_playlist,
          token,
        });

        navigate(&#39;/main&#39;);
        //가입 이력이 없고 뮤딕 프로필 설정이 필요한 경우
      } else {
        const email = response.email;
        setSignupInfo({ email, type: &#39;social&#39; });
        navigate(&#39;/setprofile&#39;);
      }
      // console.log(response);
    } catch (error) {
      console.error(&#39;Error&#39;, error);
    }
  };</code></pre>
<p>위의 코드는 sendoCode라는 함수의 매개변수로 들어오는 social의 값이 카카오인지, 구글인지를 확인하여, 해당 소셜로그인의 인가 코드를 post요청으로 전달해주는 함수이다.</p>
<p>허나 위 sendoCode라는 함수에는 추가로 가입 이력이 있는지, 없는지에 따른 동작을 추가로 진행하고 있는데 가입 이력이 있다면 바로 
setUserInfo를 통해 전역 상태관리를 통해 유저정보를 저장하고 메인 페이지로 이동 시켜주고, 가입 이력이 없다면 서비스 이용에 필요한 추가 정보 입력을 위해 프로필 설정 페이지로 이동하는 동작을 추가로 구현하고 있습니다.</p>
<p>이제 위 코드를 단일책임의 원칙을 적용하여 보다 읽기쉬운 코드로 변경해보겠습니다.</p>
<pre><code class="language-js">  const sendCode = async (code: string, social: string) =&gt; {
    try {
      let response;
      if (social === &#39;kakao&#39;) {
        response = await postUserCode(code, &#39;kakao&#39;);
      } else if (social === &#39;google&#39;) {
        response = await postUserCode(code, &#39;google&#39;);
      }
      //가입 이력이 있을 경우
      if (response.message === &#39;로그인 성공&#39;) {
        handleSuccessLogin(response);
        //가입 이력이 없고 뮤딕 프로필 설정이 필요한 경우
      } else {
        handleMoveSignUp(response);
      }
      // console.log(response);
    } catch (error) {
      console.error(&#39;Error&#39;, error);
    }
  };

  const handleSuccessLogin = (response: ILogin) =&gt; {
    const { user, token } = response;
    const { id, email, name, image, genre, about, rep_playlist } = user;
    const { access, refresh } = token;
    localStorage.setItem(&#39;token&#39;, access);
    localStorage.setItem(&#39;refreshToken&#39;, refresh);
    setIsLogin(true);
    setUserInfo({
      id,
      email,
      name,
      image,
      genre,
      about,
      rep_playlist,
      token,
    });
    navigate(&#39;/main&#39;);
  };

  const handleMoveSignUp = (response: ISignup) =&gt; {
    const email = response.email;
    setSignupInfo({ email, type: &#39;social&#39; });
    navigate(&#39;/setprofile&#39;);
  };</code></pre>
<p>위 코드에서 가입상태 여부에 따라 유저 정보와 페이지 이동을 설정하는 코드를 handleSuccessLogin, handleMoveSignUp 함수를 만들어 분리하였습니다.</p>
<p>handleSuccessLogin에서는 로그인 성공시에 필요한 유저정보 저장과 메인 페이지 이동의 기능을 구현하였고,
handleMoveSignUp에서는 추가 프로필 설정에 필요한 기능을 구현하였습니다.</p>
<p>단일책임원칙으로 기반한 함수 분리를 통해 코드들이 보다 가독성이 높아진 것을 확인할 수 있었습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[createBrowserRouter 사용하기]]></title>
            <link>https://velog.io/@sj_yun/createBrowserRouter-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sj_yun/createBrowserRouter-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 19 Mar 2024 12:30:02 GMT</pubDate>
            <description><![CDATA[<p>기존 프로젝트에서 리팩토링 요소들을 고려하던 도중 새롭게 알게된 <code>createBrowserRouter</code> 사용에 관한 포스팅을 작성하고자 합니다.</p>
<h3 id="createbrowserrouter에란">createBrowserRouter에란?</h3>
<p>createBrowserRouter는 <code>React Router v6.4</code>에서 새롭게 에 추가된 방식중 하나로 라우터 객체를 생성하는 데 사용됩니다. 이 함수는 라우터 구성을 ** 객체 형태** 로 선언적으로 만들 수 있게 해주며,이를 BrowserRouter 컴포넌트에 전달하여 라우팅을 설정합니다.</p>
<p>기존의 BrowserRouter와 Routes, Route를 사용하는 방식과 비교하여 createBrowserRouter가 가지는 장점을 살펴보겠습니다.</p>
<h3 id="라우팅-설정의-간결함">라우팅 설정의 간결함</h3>
<pre><code class="language-js">import { BrowserRouter, Routes, Route, Navigate } from &#39;react-router-dom&#39;;
import Layout from &#39;./Layout&#39;;
import Browse from &#39;./pages/Browse&#39;;
import MyList from &#39;./pages/MyList&#39;;
import Search from &#39;./pages/Search&#39;;
import NotFound from &#39;./pages/NotFound&#39;;

const router = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Routes&gt;
        &lt;Route path=&quot;/&quot; element={&lt;Layout /&gt;}&gt;
          &lt;Route index element={&lt;Navigate to=&#39;browse/movie&#39; /&gt;} /&gt;
          &lt;Route path=&quot;browse/:section&quot; element={&lt;Browse /&gt;} /&gt;
          &lt;Route path=&quot;search/:section&quot; element={&lt;Search /&gt;} /&gt;
          &lt;Route path=&quot;mylist&quot; element={&lt;MyList /&gt;} &gt;
            &lt;Route path=&quot;:section&quot; element={&lt;MyList /&gt;} /&gt;
          &lt;/Route&gt;
          &lt;Route path=&quot;*&quot; element={&lt;NotFound /&gt;} /&gt;
        &lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default router;</code></pre>
<p>기존의 BrowserRouter와 Routes, Route를 사용하는 방식은 중첩된 구조로 라우팅을 설정해야 했습니다.</p>
<p> 이로 인해 코드가 길고 복잡해지는 경향이 있었습니다. 반면 createBrowserRouter는 ** JSON 구조로**  라우팅을 설정하여 코드를 간결하게 유지할 수 있습니다. 불필요한 중첩을 줄임으로써 가독성을 향상시키고, 라우팅 설정에 집중할 수 있게 됩니다.</p>
<pre><code class="language-js">import { createBrowserRouter, Navigate } from &#39;react-router-dom&#39;;
import Layout from &#39;./Layout&#39;;
import Browse from &#39;./pages/Browse&#39;;
import MyList from &#39;./pages/MyList&#39;;
import Search from &#39;./pages/Search&#39;;
import NotFound from &#39;./pages/NotFound&#39;;

const router = createBrowserRouter([
  {
    element: &lt;Layout /&gt;,
    children: [
      {
        path: &#39;/&#39;,
        element: &lt;Navigate to=&#39;browse/movie&#39; /&gt;,
      },
      {
        path: &#39;browse/:section&#39;,
        element: &lt;Browse /&gt;,
      },
      {
        path: &#39;search/:section&#39;,
        element: &lt;Search /&gt;,
      },
      {
        path: &#39;mylist&#39;,
        element: &lt;MyList /&gt;,
        children: [
          {
            path: &#39;:section&#39;,
            element: &lt;MyList /&gt;,
          },
        ],
      },
      {
        path: &#39;*&#39;,
        element: &lt;NotFound /&gt;,
      },
    ],
  },
]);

export default router;

</code></pre>
<h3 id="하위-경로-정의-방식">하위 경로 정의 방식</h3>
<p>기존 방식에서는 Route 구성 요소가 서로 중첩되어 하위 경로를 정의합니다. 중첩된 Route의 path는 상위 경로를 기준으로 하위 경로를 지정합니다</p>
<pre><code class="language-js">// 기존 Router 를 사용하여 하위 요소 설정
 &lt;Route path=&quot;mylist&quot; element={&lt;MyList /&gt;} &gt;
    &lt;Route path=&quot;:section&quot; element={&lt;MyList /&gt;} /&gt;
     &lt;/Route&gt;</code></pre>
<p>createBrowserRouter를 사용한 구성은 JSX가 아닌 객체 구조를 사용하여 정의됩니다. children 속성은 상위 경로 <code>/mylist</code> 아래에 하위 경로를 정의하는 데 사용됩니다. 각 하위 개체는 자체 경로와 해당 요소(렌더링할 구성 요소)가 있는 하위 경로를 나타냅니다  </p>
<pre><code class="language-js">
// createBrowserRouter를 사용하여 하위 요소 설정
 {
        path: &#39;search/:section&#39;,
        element: &lt;Search /&gt;,
      },
 {
        path: &#39;mylist&#39;,
        element: &lt;MyList /&gt;,
        children: [
          {
            path: &#39;:section&#39;,
            element: &lt;MyList /&gt;,
          },
        ],
      },</code></pre>
<h3 id="routerprovider-설정-방식">RouterProvider 설정 방식</h3>
<p>우선 기존의 방식처럼 <code>BrowserRouter</code>로 감싸지 않습니다.</p>
<pre><code class="language-js">import { BrowserRouter } from &#39;react-router-dom&#39;

const root = ReactDOM.createRoot(document.getElementById(&#39;root&#39;))
root.render(
  &lt;BrowserRouter&gt; // 최상단 root에서 BrowserRouter로 감싸기
      &lt;App /&gt; 
  &lt;/BrowserRouter&gt;
)</code></pre>
<p><code>RouterProvider</code> 를 이용해서 구성요소들을 전달하고 활성화 합니다.</p>
<pre><code class="language-js">import { RecoilRoot } from &#39;recoil&#39;;
import ReactDOM from &#39;react-dom/client&#39;;
import { RouterProvider } from &#39;react-router-dom&#39;;import router from &#39;./Router&#39;;


const root = ReactDOM.createRoot(
  document.getElementById(&#39;root&#39;) as HTMLElement
);

root.render(
  &lt;RecoilRoot&gt;
      &lt;RouterProvider router={router} /&gt;
  &lt;/RecoilRoot&gt;
);
</code></pre>
<h3 id="요약">요약</h3>
<p>createBrowserRouter도입으로 인한 변화들은 코드의 가독성을 높이고 유지보수를 용이하게 만들어줍니다. 
특히 대규모 애플리케이션에서 정의해야 될 Router의 수가 많을수록 createBrowserRouter 사용으로 더 큰 이점을 얻을 수 있을 것으로 보입니다.</p>
<p>Reference: <a href="https://reactrouter.com/en/main/routers/create-browser-router">https://reactrouter.com/en/main/routers/create-browser-router</a>
<a href="https://velog.io/@adultlee/createBrowserRouter%EB%A5%BC-%ED%86%B5%ED%95%9C-Router%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80">https://velog.io/@adultlee/createBrowserRouter%EB%A5%BC-%ED%86%B5%ED%95%9C-Router%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] useQueries 사용하기]]></title>
            <link>https://velog.io/@sj_yun/Tanstack-Query-useQueries%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@sj_yun/Tanstack-Query-useQueries%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 15 Mar 2024 12:53:53 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 Tanstack Query의 useQueries사용법에 대해 적어보는 시간을 가져보려고 합니다.</p>
<p>들어가기 앞서 useQuery의 사용법을 알고 있다는 가정하에 포스팅을 진행합니다. useQuery의가 궁금하신 분들은 <a href="https://velog.io/@sj_yun/React-Query-useQuery-%EC%82%AC%EC%9A%A9%EB%B2%95">해당 포스팅</a>을 먼저 참고해주세요.</p>
<h2 id="🤔-usequeries는-어떨-때-사용할까">🤔 useQueries는 어떨 때 사용할까?</h2>
<pre><code class="language-js">import React from &#39;react&#39;;
import { useQuery } from &#39;@tanstack/react-query&#39;;

const fetchUser = async (userId) =&gt; {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
  return response.json();
};

const UserProfile = ({ userId }) =&gt; {

  const { data: user, isLoading, error } = useQuery([&#39;user&#39;, userId], () =&gt; fetchUser(userId));

  return (
    &lt;div&gt;
      &lt;h2&gt;User Profile&lt;/h2&gt;
      &lt;p&gt;Name: {user.name}&lt;/p&gt;
      &lt;p&gt;Email: {user.email}&lt;/p&gt;
      &lt;p&gt;Phone: {user.phone}&lt;/p&gt;
    &lt;/div&gt;
  );
};

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;UserProfile userId={1} /&gt;
    &lt;/&gt;
  );
};

export default App;
</code></pre>
<p>위 코드는 useQuery를 사용하여 userId를 api 요청시 전달하여 해당 id에 관한 유저 정보를 불러오는 간단한 예시 코드입니다. 해당 코드는 문제 없이 잘 작동합니다.그렇지만 여기서 userId가 하나가 아닌 여러 개의 userId 값이 존재하는 배열이 전달된 경우 어떻게 useQuery를 수행해야 할까요? </p>
<p>useQuery로도 병렬적인 다건의 API 요청을 수행할 수 있지만, 서로 다른 userId를 100개이상 화면에 보여줘야 한다고 가정하면 코드 길이가 무수히 늘어나고, API 요청이 동적으로 변경되는 것이 아니라 정적인 상태로 한정될 것입니다. 이때, 위 상황을 방지할 수 있는것이 <code>useQueries</code> 입니다.</p>
<h3 id="usequeries-사용법">useQueries 사용법</h3>
<p>useQueries는 Tanstack Query에서 useQuery의 <strong>동적 병렬 쿼리 작업</strong>을 위해 사용됩니다.
그리고 여기서 말하는 동적 병렬 쿼리 작업은 병렬 쿼리 작업을 수행을 하지만 상황에 따라 쿼리 작업이 유동적으로 변하는 것을 의미합니다.</p>
<pre><code class="language-js">import React from &#39;react&#39;;
import { useQuery, useQueries } from &#39;@tanstack/react-query&#39;;

const fetchUser = async (userId) =&gt; {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
  return response.json();
};

const UsersProfile = ({ userIds }) =&gt; {
  // 여러 사용자의 데이터를 병렬로 가져오기 위해 useQueries를 사용
  const userQueries = useQueries(
    userIds.map((userId) =&gt; ({
      queryKey: [&#39;user&#39;, userId],
      queryFn: () =&gt; fetchUser(userId),
    }))
  );

  return (
    &lt;div&gt;
      &lt;h2&gt;Users Profile&lt;/h2&gt;
      {userQueries.map((query, index) =&gt; {
        const user = query.data;
        return (
          &lt;div key={index}&gt;
            &lt;p&gt;Name: {user.name}&lt;/p&gt;
            &lt;p&gt;Email: {user.email}&lt;/p&gt;
            &lt;p&gt;Phone: {user.phone}&lt;/p&gt;
          &lt;/div&gt;
        );
      })}
    &lt;/div&gt;
  );
};

const App = () =&gt; {
  return (
    &lt;div&gt;
      &lt;UsersProfile userIds={[1, 2, 3]} /&gt;
    &lt;/div&gt;
  );
};

export default App;</code></pre>
<p>uUsersProfile 컴포넌트가 props로 받은 userIds 배열을 map 함수를 통해 각각의 id를 queryKey와 queryFn 항목만 지정해서 리턴하면 useQueries 함수가 알아서 동적 병렬 쿼리를 생성하여 보다 효율적으로 구현할 수 있게 도와줍니다.</p>
<h3 id="실제-적용-사례">실제 적용 사례</h3>
<p>추가로 프로젝트에 useQueries를 어떻게 적용했는지 보여드리겠습니다.</p>
<pre><code class="language-js">import { useQueries } from &#39;@tanstack/react-query&#39;;
import { useRecoilValue } from &#39;recoil&#39;;
import styled from &#39;styled-components&#39;;
import { myLangAtom, myMovieAtom, myTvAtom } from &#39;../atom&#39;;
import { getDetails } from &#39;../utils/api&#39;;
import Loader from &#39;../components/Loader&#39;;
import MyListGrid from &#39;../components/MyListGrid&#39;;

function MyList() {
  const { t } = useTranslation();
  const lang = useRecoilValue(myLangAtom);
  const myMovie = useRecoilValue(myMovieAtom);
  const myTv = useRecoilValue(myTvAtom);
  const myMovieQuery = useQueries({
    queries: myMovie.map((movieId) =&gt; {
      return {
        queryKey: [&#39;myMovie&#39;, String(movieId), lang],
        queryFn: () =&gt; getDetails(&#39;movie&#39;, String(movieId), lang),
      };
    }),
  });

  const myTvQuery = useQueries({
    queries: myTv.map((tvId) =&gt; {
      return {
        queryKey: [&#39;myTv&#39;, String(tvId), lang],
        queryFn: () =&gt; getDetails(&#39;tv&#39;, String(tvId), lang),
      };
    }),
  });

  const myMovieData = myMovieQuery?.map((myMovie) =&gt; myMovie.data);
  const myTvData = myTvQuery?.map((myTv) =&gt; myTv.data);

  const isMyMovieLoading = myMovieQuery.some((myMovie) =&gt; myMovie.isLoading);
  const isMyTvLoading = myTvQuery.some((myTv) =&gt; myTv.isLoading);
  const isLoading = isMyMovieLoading || isMyTvLoading;

  if (isLoading) {
    return &lt;Loader /&gt;;
  }

  return (
    &lt;&gt;
      &lt;Wrapper&gt;
        &lt;MyListGrid
          title={t(&#39;mylist.movie&#39;)}
          contents={myMovieData}
          section=&#39;movie&#39;
          altText={t(&#39;mylist.altText&#39;)}
        /&gt;
        &lt;MyListGrid
          title={t(&#39;mylist.tv&#39;)}
          contents={myTvData}
          section=&#39;tv&#39;
          altText={t(&#39;mylist.altText&#39;)}
        /&gt;
      &lt;/Wrapper&gt;
    &lt;/&gt;
  );
}

export default MyList;</code></pre>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/76e6faa8-68a7-48e5-af87-996231f66e93/image.png" alt=""></p>
<p>유저가 보관함에 추가한 각 영화, TV의 작품들의 id를 배열 형태로 받아온 뒤, useQueries를 사용하여 각 영화 및 TV 프로그램에 대한 쿼리를 생성합니다.
<img src="https://velog.velcdn.com/images/sj_yun/post/051de6e2-59d0-444e-bb2c-62e4a1dc8d99/image.png" alt="">
각 쿼리는 getDetails 함수를 호출하여 해당 작품의 세부 정보를 가져옵니다. 이때 쿼리 키에는 해당 작품의 ID와 언어가 포함된 각각의 쿼리를 동적으로 생성해줍니다. 각 쿼리의 로딩 상태를 확인하여 전체 데이터가 로드되었는지를 판단하여 로딩 중에는 각각의 로딩 컴포넌트를 보여주고, 데이터가 로드된 후에는 실제 내용을 표시합니다.</p>
<ul>
<li>보관함 기능
<img src="https://velog.velcdn.com/images/sj_yun/post/c10b5824-e97c-4904-a27e-866780e706cb/image.png" alt=""></li>
</ul>
<p>Reference: </p>
<ul>
<li><a href="https://tanstack.com/query/latest/docs/framework/react/reference/useQueries">https://tanstack.com/query/latest/docs/framework/react/reference/useQueries</a></li>
<li><a href="https://velog.io/@jhjung3/React-Query%EC%9D%98-useQueries-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0-79d6b6k1">https://velog.io/@jhjung3/React-Query%EC%9D%98-useQueries-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0-79d6b6k1</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSR vs SSR 특징 및 차이]]></title>
            <link>https://velog.io/@sj_yun/CSR-vs-SSR-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@sj_yun/CSR-vs-SSR-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Thu, 07 Mar 2024 08:58:50 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 개발과 관련된 질문으로 빠지지 않는 CSR 과 SSR의 차이점, 
실제 서비스 또한 CSR 과 SSR의 차이점과 장단점을 알고 목적에 맞는 서비스를 개발해야 되기 때문에 블로그 글로 확실하게 정리해보고자 합니다. </p>
<h3 id="1-ssrserver-side-rendering">1. SSR(Server Side Rendering)</h3>
<blockquote>
<p><strong>S</strong>erver <strong>S</strong>ide <strong>R</strong>endering의 약자.
서버쪽에서 렌더링 준비를 끝마친 상태로 클라이언트에 전달하는 방식이다.</p>
</blockquote>
<p>아래 사진과 함께 설명을 보겠습니다. </p>
<ol>
<li>서버가 렌더링된 HTML 파일을 보내고 브라우저가 받는다. </li>
<li>브라우저는 받은 HTML 파일을 확인하고 바로 화면에 보여주게 된다.(그러나 Javascript파일이 브라우저에서 실행되기 전이기 때문에 볼 수 만 있고 사이트 자체는 조작 불가능하다.)</li>
<li>서버로부터 Javascript파일을 받고 브라우저에서 실행시킨 뒤 웹과 상호작용이 가능해진다.
<img src="https://velog.velcdn.com/images/sj_yun/post/41cdcee6-2cda-4bf7-8271-df2f0b9a05b9/image.png" alt=""></li>
</ol>
<h4 id="ssr-장점">SSR 장점</h4>
<ul>
<li>첫페이지 로딩속도가 빠르다.</li>
<li>검색엔진 최적화가 가능하다</li>
</ul>
<h4 id="ssr-단점">SSR 단점</h4>
<ul>
<li>초기 로딩 이후 페이지 이동 시 속도가 CSR에 비해 느리다.</li>
<li>깜빡임 이슈 (매번 새로고침 해야하기 때문에)</li>
<li>서버 과부하</li>
</ul>
<p>위의 장단점을 이해하기 위해 예시 코드와 함께 SSR의 동작 원리를 살펴보겠습니다.</p>
<pre><code class="language-index.html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot; /&gt;
    &lt;title&gt;React App&lt;/title&gt;
  &lt;/head&gt;

  &lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;div className=&quot;App&quot;&gt;
      &lt;h1&gt;SSR과 CSR&lt;/h1&gt;
      &lt;h2&gt;SSR 장점&lt;/h2&gt;
      &lt;ul&gt;
        &lt;li style=&quot;font-size: 22px&quot;&gt;첫페이지 로딩속도가 빠르다.&lt;/li&gt;
        &lt;li style=&quot;font-size: 22px&quot;&gt;검색엔진 최적화가 가능하다&lt;/li&gt;
      &lt;/ul&gt;

      &lt;h2&gt;SCR 장점&lt;/h2&gt;
      &lt;li style=&quot;font-size: 22px&quot;&gt;
        새로고침이 발생하지 않아 사용자 경험에 도움을 준다.
      &lt;/li&gt;
      &lt;li style=&quot;font-size: 22px&quot;&gt;
        초기 로딩 이후 빠른 웹사이트 렌더링이 가능하다
      &lt;/li&gt;
    &lt;/div&gt;
  &lt;/body&gt;
  &lt;/html&gt;</code></pre>
<pre><code class="language-js">//server.js
const express = require(&#39;express&#39;);
const app = express();
const port = 3001;
app.listen(port, function () {
  console.log(&#39;server opend&#39;);
});

app.get(&#39;/&#39;, function (req, res) {
  res.sendFile(__dirname + &#39;/index.html&#39;);
});

</code></pre>
<p>위 코드는 node.js와 express를 사용하여 SSR 방식을 구현한 예시입니다. 
서버에서 초기 렌더링에 필요한 HTML 파일을 보내고, 브라우저는는 이를 받아 화면에 표시합니다.
SSR 방식을 사용하면 개발자 도구의 네트워크 탭을 통해 렌더링된 HTML 파일을 확인할 수 있는데 어떻게 보여지는 확인해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/0f08c79e-3814-4c21-9200-85c50a3c8bfa/image.png" alt=""></p>
<p>SSR은 서버로부터 받은 HTML 파일과 내용을 브라우저가 확인하고 그것을 바로 화면에 미리 보여줄 수 있기에 
미리보기에서 조작은 못하지만 확인이 가능하기에 첫페이지 <strong>로딩속도가 빠르며</strong>, 
HTML 파일 안에 포함된 정보를 바탕으로** SEO(검색엔진 최적화)**에 유리하다는 장점을 가지게 되는 것입니다.</p>
<h3 id="2-csrclient-side-rendering">2. CSR(Client Side Rendering)</h3>
<blockquote>
<p><strong>C</strong>lient <strong>S</strong>ide <strong>R</strong>endering의 약자.
SSR과 반대로 브라우저가 렌더링을 맡아서 하는 방식을 의미한다. </p>
</blockquote>
<ol>
<li>서버가  HTML 파일을 보내고 브라우저가 받는다. 
(여기서 핵심은 HTML 파일을 보내주긴 하지만** 빈 HTML 파일**을 보내준다는 점이다)</li>
<li>랜더링할 정보가 없으므로 화면에는 아무것도 보여지지 않는다.</li>
<li>서버로부터 Javascript파일을 받고 브라우저에서 실행시킨 뒤에 랜더링과 상호작용이 가능해진다.
<img src="https://velog.velcdn.com/images/sj_yun/post/ee9a624e-512d-4fb8-87fd-17e46944d165/image.png" alt=""></li>
</ol>
<p>이번에는 <code>Creat-React-app</code> 을 통해 리액트 프로젝트를 생성하고 CSR 환경에서 작동하는 예시 코드를 살펴보겠습니다.</p>
<pre><code class="language-js">import React from &#39;react&#39;;
import { createRoot } from &#39;react-dom/client&#39;;
import App from &#39;./App&#39;;

const container = document.getElementById(&#39;root&#39;);
const root = createRoot(container);
root.render(
    &lt;App /&gt;
);
</code></pre>
<pre><code class="language-js">import React from &#39;react&#39;;

function App() {
  return (
    &lt;&gt;
      &lt;h1&gt;SSR과 CSR&lt;/h1&gt;
      &lt;h2&gt;SSR 장점&lt;/h2&gt;
      &lt;ul&gt;
        &lt;li style={{ fontSize: &#39;22px&#39; }}&gt;첫페이지 로딩속도가 빠르다.&lt;/li&gt;
        &lt;li style={{ fontSize: &#39;22px&#39; }}&gt;검색엔진 최적화가 가능하다&lt;/li&gt;
      &lt;/ul&gt;

      &lt;h2&gt;SCR 장점&lt;/h2&gt;
      &lt;ul&gt;
        &lt;li style={{ fontSize: &#39;22px&#39; }}&gt;
          새로고침이 발생하지 않아 사용자 경험에 도움을 준다.
        &lt;/li&gt;
        &lt;li style={{ fontSize: &#39;22px&#39; }}&gt;
          초기 로딩 이후 빠른 웹사이트 렌더링이 가능하다
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/&gt;
  );
}
export default App;
</code></pre>
<p>React의 경우, 서버로부터 응답을 받을 때 HTML 파일은 비어 있기에 사용자는 초기에는 빈 화면만을 볼 수 있습니다. 
이후 자바스크립트 파일이 다운로드되고 해석되면, React 애플리케이션의 진입 지점인 App.js가 렌더링되어 사용자가 보게 됩니다.
 이 작업은 <code>createRoot</code> 함수를 사용하여 DOM 요소에 렌더링을 설정하는 것으로 이루어집니다.
<img src="https://velog.velcdn.com/images/sj_yun/post/fdf0c167-037d-4d2e-887f-a0f6ed53a7b7/image.png" alt=""></p>
<p>이러한 특성으로 인해 SSR 방식과는 달리, CSR은 개발자 도구를 통한 미리보기가 불가능하며, 초기 로딩 속도가 느리고 SEO에 불리한 측면이 있습니다. 그러나 한 번 초기 로딩이 완료되면, 사용자 경험을 향상시키는 <strong>새로고침 없는 조작과 빠른 웹사이트 렌더링</strong>을 제공하여 SSR보다 빠르고 동적인 사용자 인터랙션을 가능케 합니다.</p>
<h2 id="결론">결론</h2>
<p>SSR은 초기 페이지 로딩 시 서버로부터 완전한 HTML을 받아 화면을 빠르게 표시할 수 있고 검색 엔진 최적화에 유리합니다. 그러나 페이지 이동 시 서버에 재요청이 필요하고, 화면 깜빡임 현상이 발생할 수 있으며 서버 과부하 문제도 발생할 수 있습니다.</p>
<p>CSR은 초기에는 비어있는 HTML을 받아 사용자는 아무 정보도 볼 수 없지만, JavaScript 파일을 통해 동적으로 화면을 구성하고 렌더링하기 때문에 사용자 경험 측면에서는 더 부드럽고 빠른 웹사이트를 제공할 수 있습니다. 그러나 검색 엔진 최적화에는 불리하고 초기 로딩 속도가 SSR에 비해 느릴 수 있습니다.</p>
<p>따라서 프로젝트의 목적과 요구사항에 따라 SSR과 CSR 중에서 선택하여 개발해는 것이 중요하며 검색 엔진 최적화나 초기 로딩 속도가 중요하다면 SSR을 선택하고, 사용자 경험과 애플리케이션의 동적인 특성이 중요하다면 CSR을 선택하는 것이 적절하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[성능개선] 성능 향상을 위한 코드 분할하기(React.lazy, Suspense)]]></title>
            <link>https://velog.io/@sj_yun/%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%BD%94%EB%93%9C-%EB%B6%84%ED%95%A0%ED%95%98%EA%B8%B0React.lazy-Suspense</link>
            <guid>https://velog.io/@sj_yun/%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%BD%94%EB%93%9C-%EB%B6%84%ED%95%A0%ED%95%98%EA%B8%B0React.lazy-Suspense</guid>
            <pubDate>Fri, 01 Mar 2024 07:02:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sj_yun/post/92458efb-3a51-4bd6-bfa0-7e58e4b6c5bd/image.png" alt="코드 스필리팅 전"></p>
<p>React + TypeScript 방식으로 개발한 프로젝트의 성능개선요소를 고려하던 중 React는 CSR(Client Side Rendering) 방식을 채택하고 있으며 초기 로딩 속도가 느리다는 단점을 개선해보기로 했습니다.</p>
<p>CSR(Client Side Rendering)에 대해 궁금하다면<a href="https://velog.io/@sj_yun/CSR-vs-SSR-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EC%B0%A8%EC%9D%B4"> 해당 포스팅</a>을 확인해주세요.</p>
<p> Lighthouse 도구를 통해 검사한 결과 js bundle 사이즈의 개선 가능성을 확인했고 js bundle 사이즈를 줄이고 초기 로딩속도 지연이라는 문제를 해결하기위해 코드분할 방식을 찾게 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/06e81274-99fe-40c5-9d66-66522cf7e547/image.png" alt=""></p>
<h3 id="코드분할code-splitting-이란">코드분할(Code Splitting) 이란?</h3>
<p>코드분할은 애플리케이션의 코드를 여러 번에 나누어 로드하는 것을 의미합니다. 이는 초기 로딩 시간을 단축하고 사용자가 페이지에 더 빠르게 접근할 수 있도록 도와주며 React에서는 <code>react.lazy</code>와 <code>Suspense</code>를 사용하여 코드분할을 쉽게 구현할 수 있습니다.</p>
<h3 id="reactlazy란">react.lazy란?</h3>
<p>React에서 컴포넌트 파일을 코드 최상단으로 불러와 정의하고 동적으로 불러오면 에러가 발생합니다. 하지만 <code>react.lazy</code> 함수를 사용하면 동적으로 컴포넌트를 로드할 수 있습니다. </p>
<pre><code class="language-js">import React from &#39;react&#39;;
import MyComponent from &#39;./MyComponent&#39;; // 일반적인 import</code></pre>
<pre><code class="language-js">import React, { lazy, Suspense } from &#39;react&#39;;
const MyComponent = lazy(() =&gt; import(&#39;./MyComponent&#39;)); //lazy 함수를 사용한 import</code></pre>
<p>동적으로 로드한 컴포넌트들은 손쉽게 코드분할이 가능하며, 이렇게 로드한 컴포넌트는 <code>Suspense</code> 컴포넌트로 감싸서 로딩 중일 때 보여줄 UI를 지정할 수 있습니다.</p>
<pre><code class="language-js">import React, { lazy, Suspense } from &#39;react&#39;;

const MyComponent = lazy(() =&gt; import(&#39;./MyComponent&#39;));

function App() {
  return (
    &lt;div&gt;
      &lt;Suspense fallback={&lt;div&gt;로딩중입니다...&lt;/div&gt;}&gt; // 로딩 중 보여줄 UI 
        &lt;MyComponent /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="react-router와-함께-사용">React Router와 함께 사용</h3>
<p> 위 프로젝트 규모가 소규모 프로젝트임을 감안해 웹 페이지를 불러오고 진입하는 단계인 App.js에서 이 두 기능을 적용시켜 사용하였습니다.</p>
<pre><code class="language-js">import { createBrowserRouter, Navigate } from &#39;react-router-dom&#39;;
import React, { lazy, Suspense } from &#39;react&#39;;
import Loader from &#39;./components/Loader&#39;;
import NotFound from &#39;./pages/NotFound&#39;;
import Layout from &#39;./Layout&#39;;
const Browse = lazy(() =&gt; import(&#39;./pages/Browse&#39;));
const MyList = lazy(() =&gt; import(&#39;./pages/MyList&#39;));
const Search = lazy(() =&gt; import(&#39;./pages/Search&#39;));

const router = createBrowserRouter([
  {
    element: (
      &lt;Suspense fallback={&lt;Loader /&gt;}&gt;
        &lt;Layout /&gt;
      &lt;/Suspense&gt;
    ),
    children: [
      {
        path: &#39;/&#39;,
        element: &lt;Navigate to=&#39;browse/movie&#39; /&gt;,
      },
      {
        path: &#39;browse/:section&#39;,
        element: (
          &lt;Suspense fallback={&lt;Loader /&gt;}&gt;
            &lt;Browse /&gt;
          &lt;/Suspense&gt;
        ),
      },
      {
        path: &#39;search/:section&#39;,
        element: (
          &lt;Suspense fallback={&lt;Loader /&gt;}&gt;
            &lt;Search /&gt;
          &lt;/Suspense&gt;
        ),
      },
      {
        path: &#39;mylist&#39;,
        element: (
          &lt;Suspense fallback={&lt;Loader /&gt;}&gt;
            &lt;MyList /&gt;
          &lt;/Suspense&gt;
        ),
        children: [
          {
            path: &#39;:section&#39;,
            element: (
              &lt;Suspense fallback={&lt;Loader /&gt;}&gt;
                &lt;MyList /&gt;
              &lt;/Suspense&gt;
            ),
          },
        ],
      },
      {
        path: &#39;*&#39;,
        element: (
          &lt;Suspense fallback={&lt;Loader /&gt;}&gt;
            &lt;NotFound /&gt;
          &lt;/Suspense&gt;
        ),
      },
    ],
  },
]);

export default router;

</code></pre>
<p>React Router를 React.lazy와 Suspense와 함께 사용하여 사용자가 Posting 페이지에 접근하면 해당 컴포넌트가 비동기적으로 로드되게 하였고, 사용자가 PostDetail 페이지나 EditPost 페이지에 접근할 때도 마찬가지로 컴포넌트가 필요한 시점에 비동기적으로 로드됩니다. </p>
<p>코드 스플리팅 적용 전 
<img src="https://velog.velcdn.com/images/sj_yun/post/1c54efd5-27e7-4325-950b-1807b67b1416/image.png" alt=""></p>
<p>코드 스플리팅 적용 후
<img src="https://velog.velcdn.com/images/sj_yun/post/1fcbf9fb-943a-4a41-95c4-2b448968d244/image.png" alt="">
<img src="https://velog.velcdn.com/images/sj_yun/post/bbaa82f5-2d9d-40cc-ad65-c1240ac2a40f/image.png" alt=""></p>
<p>이로 인해 위와 같이 초기 페이지 로딩 속도를 2.83초에서 2.23초로(0.6초) 약 21.22% 감소하였으며, 
전송 데이터량 또한  211B/6.7MB 에서 221B/105KB으로 줄였으며,  96.72%의 데이터를 감소하였습니다.</p>
<p>코드 분할은 초기 렌더링 시간이 줄어드는 분명한 장점이 있으나 페이지를 이동하는 과정마다 로딩 화면이 보이기 때문에 서비스의 규모와 특성에 맞게 적용 여부를 결정해야 합니다. </p>
<p>References: <a href="https://wikidocs.net/197644">https://wikidocs.net/197644</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Query] useQuery 사용법]]></title>
            <link>https://velog.io/@sj_yun/React-Query-useQuery-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@sj_yun/React-Query-useQuery-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Tue, 07 Nov 2023 07:21:16 GMT</pubDate>
            <description><![CDATA[<p>React-Query와 useQuery를 어떻게 사용하는지에 대한 정리입니다.</p>
<h4 id="설치">설치</h4>
<p><code>npm i @tanstack/react-query</code></p>
<p>React Query를 사용하기 위해선 queryClient 생성 후 provider로 감싸 주어야 됩니다.</p>
<h4 id="indexjs">index.js</h4>
<p><code>import { QueryClient, QueryClientProvider } from &quot;@tanstack/react-query&quot;;</code></p>
<pre><code class="language-js">import React from &#39;react&#39;;
import { createRoot } from &#39;react-dom/client&#39;;
import App from &#39;./App&#39;;
import { ReactQueryDevtools } from &#39;@tanstack/react-query-devtools&#39;;
import { QueryClient, QueryClientProvider } from &#39;@tanstack/react-query&#39;;

const queryClient = new QueryClient(); //QueryClient 생성
const container = document.getElementById(&#39;root&#39;);
const root = createRoot(container);
root.render(
  &lt;QueryClientProvider client={queryClient}&gt;  //provider 감싸주기
      &lt;App /&gt;
    &lt;ReactQueryDevtools initialIsOpen={true} /&gt;
  &lt;/QueryClientProvider&gt;
);
</code></pre>
<p>이제 Query를 사용하기 위한 준비는 모두 마쳤습니다. 하지만 React Query의 편리성을 알기 위해 먼저 React Query를 사용하지 않고 진행해보겠습니다.</p>
<p>진행해볼 것은 todoList 할일목록을 받아와 화면에 보여주는 구현을 해보겠습니다</p>
<p><a href="https://jsonplaceholder.typicode.com/todos">가짜 todoList 데이터를 가져오는 API를 예시로</a> 자세히 알아보겠습니다.</p>
<h3 id="react-query-적용-전-코드">React Query 적용 전 코드</h3>
<pre><code class="language-js">import React from &#39;react&#39;;
import axios from &#39;axios&#39;;
import { useState, useEffect } from &#39;react&#39;;
import styled from &#39;styled-components&#39;;
import { useQuery } from &#39;@tanstack/react=-query&#39;;
export default function App() {
  const [data, setData] = useState([]);
  const [isLoading, setisLoading] = useState(false);

  const toDoListUrl = &#39;https://jsonplaceholder.typicode.com/todos&#39;;

  const fetchTodos = () =&gt; {
    axios.get(toDoListUrl).then((res) =&gt; {
      setData(res);
      setisLoading(false);
    });
  };

  useEffect(() =&gt; {
    setisLoading(true);
    fetchTodos();
  }, []);

  if (isLoading) return &lt;Title&gt;is Loading...&lt;/Title&gt;;

  return (
    &lt;&gt;
      &lt;ul&gt;
        {data.data?.slice(0, 30).map((todo) =&gt; (
          &lt;ListItem key={todo.id}&gt;{todo.title}&lt;/ListItem&gt;
        ))}
      &lt;/ul&gt;
    &lt;/&gt;
  );
}

const Title = styled.h1`
  font-size: 34px;
  font-weight: bolder;
  text-align: center;
  margin: 50px auto;
`;

const ListItem = styled.li`
  margin-bottom: 20px;
`;
</code></pre>
<p>useState로 2가지 리액트 스테이트(State)를 만들었습니다.</p>
<p>data는 axios로 가져오는 할일목록 데이터이고,</p>
<p>isLoading은 외부 API 서버와의 통신으로 발생할 수 있는 잠깐의 시간 동안 화면에 Loading... 이라는 문구를 표시하기 위한 겁니다.</p>
<p>그리고 실제, axios를 이용한 외부 데이터 fetching은 useEffect 훅을 이용해서 리액트 컴포넌트가 처음 시작할 때 작동하도록 했습니다.</p>
<p>나머지, UI 코드는 보시면 쉽게 이해할 수 있습니다.</p>
<p>axios는 객체를 리턴하는데 그 객체에 data라는 항목이 바로 우리가 원하는 데이터입니다.</p>
<p>그래서 data.data라고 이중으로 참조한 겁니다.</p>
<h4 id="적용-전-실행결과">적용 전 실행결과</h4>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/e918e6b1-2549-40a7-8d29-55d80ec7b7af/image.gif" alt="쿼리사용전">
위 코드만으로도 데이터를 받아오고 받아오는동안 isLoading을 확인하는 것은 정상적으로 작동합니다. 하지만 React Query를 사용하면 <code>data</code>와 <code>isLoading</code>의 상태를 업데이트하지 않아도** 데이터를 받아오고 데이터의 로딩완료 여부를 자체적으로 해주는** 편리하고 강력한 기능을 사용할 수 있습니다.</p>
<h3 id="react-query-적용-후-코드">React Query 적용 후 코드</h3>
<pre><code class="language-js">import React from &#39;react&#39;;
import axios from &#39;axios&#39;;
import styled from &#39;styled-components&#39;;
import { useQuery } from &#39;@tanstack/react-query&#39;;
export default function App() {
  const toDoListUrl = &#39;https://jsonplaceholder.typicode.com/todos&#39;;
  const fetchTodos = async () =&gt; {
    return await axios.get(toDoListUrl);
  };

  const { isLoading, data } = useQuery({
    queryKey: [&#39;toDos&#39;],
    queryFn: fetchTodos,
  });

  if (isLoading) return &lt;Title&gt;is Loading...&lt;/Title&gt;;

  return (
    &lt;&gt;
      &lt;Title&gt;ToDo&lt;/Title&gt;
      &lt;ul&gt;
        {data.data?.slice(0, 30).map((todo) =&gt; (
          &lt;ListItem key={todo.id}&gt;{todo.title}&lt;/ListItem&gt;
        ))}
      &lt;/ul&gt;
    &lt;/&gt;
  );
}

const Title = styled.h1`
  font-size: 34px;
  font-weight: bolder;
  text-align: center;
  margin: 50px auto;
`;

const ListItem = styled.li`
  margin-bottom: 20px;
`;
</code></pre>
<h4 id="적용-후-실행결과">적용 후 실행결과</h4>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/f6171bc0-d1de-4465-9676-52a5f524ce57/image.gif" alt="query 적용 후"></p>
<p>query 적용 후 코드를 보면 위에 설명처럼 useState없이도 데이터 fething과 is Loading 완료여부가 잘 작동하는 것을 볼 수 있습니다.
위 동작을 가능하게 해주는 것이 바로 React-query의 핵심인 <code>useQuery</code> 입니다.</p>
<p>useQuery hook을 사용하기 위해선 사용하고자 하는 파일 상단에 import 해야 됩니다.</p>
<pre><code class="language-js">import { useQuery } from &#39;@tanstack/react-query&#39;; // useQuery import


const { isLoading, data } = useQuery({
    queryKey: [&#39;toDos&#39;], //key값 이름(자유롭게 설정 가능)
    queryFn: fetchTodos,  //데이터 fething 함수
  });</code></pre>
<p>useQuery훅을 사용하기 위해선 반드시 <code>queryKey</code> 값을 설정해 두어야 되는데 querykey는 반드시 <strong>배열</strong>이어야 합니다.</p>
<p><code>v4</code> 버전 이후로 queryKey를 작성할 때는 key값만 작성하더라도 배열(<code>[]</code>)안에 담아줘야 합니다.</p>
<p>배열을 사용함으로써 아래와 같은 장점을 얻을 수 있습니다.</p>
<pre><code>1. 동적으로 생성된 키를 사용하여 여러 쿼리를 추적하기에 더욱 적합하다.
2. 복잡한 키를 나타내기에 편리하다.
3. 동적으로 쿼리를 추가하거나 쿼리 그룹을 만드는 작업이 더욱 간편하다.</code></pre><p>추가로 <code>queryFn</code> 에서 실행되는 함수의 실행 결과 데이터가 <code>queryKey</code> 안에 담깁니다.</p>
<p>위에서는 data와 isLoading으로만 예시를 보여드렸지만 useQuery는 보다 <a href="https://tanstack.com/query/latest/docs/react/reference/useQuery">다양한 함수들을 지원합니다</a>.</p>
<hr>
출처: https://tanstack.com/query/latest/docs/react/quick-start


]]></description>
        </item>
        <item>
            <title><![CDATA[[React]  setState의 동작 원리, prevState는 왜 사용할까?]]></title>
            <link>https://velog.io/@sj_yun/React-setState%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-prevState%EB%8A%94-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@sj_yun/React-setState%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-prevState%EB%8A%94-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 26 Oct 2023 08:19:23 GMT</pubDate>
            <description><![CDATA[<h3 id="🤔-prevstate">🤔 PrevState?</h3>
<p>React를 사용하면서 상태를 업데이트 할 때 쓰게되는 <code>prevState</code> 분명 react 초기에는 사용원리에 대해 알았지만 이제는 사용 의미를 알고 쓰기보단 무의식적으로 쓰게 되어 다시금 prevState를 왜 사용하는지에 대해 글로 정리해보고자 한다.</p>
<h3 id="setstate-의-동작원리">setState() 의 동작원리</h3>
<p> prev state 를 왜 사용하는지를 이해하려면 우선  <code>setState()</code> 가 정확히 어떤동작을 하는지에 대해 짚고 넘어가야 한다.</p>
<p> 1) <code>state</code>가 바뀌면 화면 전체가 자동으로 다시 그려진다. (리렌더링)
 다시 공부하면서 알게 된 사실 :<code>let은 값이 바뀌어도 다시 렌더링 하지 않는다</code></p>
<p> 2)setState() 는 <strong>비동기</strong>로 작동한다</p>
<pre><code>setStatae(updater [, callback])

setState(updater [, callback]) 의 콜백 함수가 실행된 후 리렌더링 된다.

updater: (state, props) = &gt; stateChange</code></pre><p>즉 한번에 여러개의 state를 사용해도 <strong>모든 함수의 실행이 종료 된 후에</strong> 리렌더링이 진행된다. </p>
<p>💡 Why?
<code>setState</code>가 비동기가 아닌 동기로 작동하게 되면 변경될 때마다 바로바로 렌더링이 일어나 비효율적이기 때문이다.</p>
<p>위 성질을 이해한 뒤 아래 코드를 보겠습니다.</p>
<pre><code class="language-js">const [state, setState] = useState(0);
  const sumAll1 = () =&gt; {
    setState(state + 1);//0
    setState(state + 2);
    setState(state + 3);
    setState(state + 4);
  };

  return (
    &lt;&gt;
      &lt;div&gt;결과는 : {state}&lt;/div&gt;
      &lt;button type=&#39;button&#39; onClick={sumAll1}&gt;
        실행1 !
      &lt;/button&gt;

    &lt;/&gt;
  );</code></pre>
<ul>
<li><p>위 코드는 sumAll1 함수가 실행되었을때 값이 10이 되기를 기대하며 작성한 코드이다. 그렇지만 아래 실행 결과를 보면 10이 아닌 4가 출력된다.</p>
</li>
<li><p>위에서 설명했듯 변경된 값이 바로 반영되는 것이 아니고 <strong>임시 저장공간</strong>에 넣어두고, 함수가 끝나고 나서 한번만 일어나기 때문에 마지막에 호출된 setState(state + 4)만 반영되어 리렌더링이 진행된다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/b2c16970-68cc-481e-83b4-e63f3726ee37/image.png" alt=""></p>
<h3 id="prevstate">prevState</h3>
<ul>
<li><p>이럴 때 <code>prevState</code>를 사용하면 <strong>임시 저장공간에 있는 값을 받아올 수 있다</strong></p>
</li>
<li><p>임시 저장공간에 값이 없으면 default 값을 가져온다.</p>
</li>
<li><p>(prevState) 의 네이밍은 자유롭다.</p>
</li>
<li><p>리렌더는 동일하게 함수가 종료되고 나서 한번만 일어난다.</p>
</li>
</ul>
<pre><code class="language-js"> const sumAll2 = () =&gt; {
    setState((prev) =&gt; prev + 1);
    setState((prev) =&gt; prev + 2);
    setState((prev) =&gt; prev + 3);
    setState((prev) =&gt; prev + 4);

   return (
    &lt;&gt;
      &lt;div&gt;결과는 : {state}&lt;/div&gt;
      &lt;button type=&#39;button&#39; onClick={sumAll2}&gt;
        실행2 !
      &lt;/button&gt;

    &lt;/&gt;
  };</code></pre>
<p>이제 prev(이전) 값을 받아 모든 setState구문이 동작하게 되니 10이 정상적으로 출력되는 것을 볼 수 있다.
<img src="https://velog.velcdn.com/images/sj_yun/post/66ed4554-76fa-499a-9b80-9e3777913b02/image.png" alt=""></p>
<ul>
<li><p>prevState 활용
boolean 값을 변경시킬 때 편리하게 활용할 수 있다.</p>
<pre><code class="language-js">const [isOpen, setIsOpen] = useState(false);

const onToggleModal = () =&gt; {
  setIsOpen((prev) =&gt; !prev);
};</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[axios 요청 인터셉터로 API 관리하기]]></title>
            <link>https://velog.io/@sj_yun/axios-%EC%9A%94%EC%B2%AD-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0%EB%A1%9C-API-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sj_yun/axios-%EC%9A%94%EC%B2%AD-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0%EB%A1%9C-API-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 24 Oct 2023 05:42:03 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행하다 보면 아래와 같은 이유로 fecth 보다 axios 라이브러리를 주로 사용하고 있습니다. </p>
<ul>
<li>node에서도 사용할 수 있다 </li>
<li>요청을 취소할 수 있다</li>
<li>요청과 응답을 가로챌 수 있다</li>
<li>Promise 기반이고, response.data로 데이터에 접근할 수 있다</li>
</ul>
<h3 id="axios-interceptor란">Axios interceptor란?</h3>
<p><a href="https://axios-http.com/kr/docs/interceptors">Axios docs - interceptor</a>를 보면 요청 인터셉터는 axios에서 제공하는 기능 중 하나로, Promise 응답에 대해 then, catch로 처리되기 전에 요청과 응답을 가로채서 추가적인 처리나 수정을 할 수 있는 기능을 제공한다고 나와있습니다.</p>
<h3 id="axios-interceptor를-어디에-사용하면-좋을까">Axios interceptor를 어디에 사용하면 좋을까?</h3>
<p>여러 프로젝트에서 Axios 라이브러를 사용하면서 서버에 토큰 인증을 필요로 하는 API 요청을 할때마다 HTTP Authorization 요청 헤더에 토큰을 넣어줘야하고 401(Unauthorized) 에러가 서버로부터 들어오면 토큰을 갱신해준 후 재요청을 보내는 과정을 일일이 처리하는 번거로움이 있었는데 위 interceptor 기능을 이용해 토큰이 필요한 요청에만 헤더에 토큰을 넣어주고 그렇지 않은 경우에는 헤더를 삭제하여 중복 코드를 제거하고 유지 보수성을 향상시키기 위해 Axios 인터셉트를 사용하기로 했습니다. </p>
<h3 id="사용-코드">사용 코드</h3>
<p>1) Axios 인스턴스 생성</p>
<pre><code class="language-js">import axios from &#39;axios&#39;;
const instance = axios.create({
  baseURL: &#39;요청을 보낼 api 주소&#39;,
});

instance.defaults.headers.post[&#39;Content-Type&#39;] = &#39;application/json&#39;;</code></pre>
<ul>
<li><p>instance.defaults.headers.post[&#39;Content-Type&#39;] = &#39;application/json&#39;;: 생성한 axios 인스턴스의 defaults.headers를 사용하여 기본 헤더를 설정한다.</p>
</li>
<li><p>baseURL과 헤더 설정이 기본적으로 포함된 상태로 요청을 보낼 수 있다.</p>
</li>
</ul>
<p>2) 요청 인터셉터 등록</p>
<pre><code class="language-js">instance.interceptors.request.use(
  function (config) {
    const token = localStorage.getItem(&#39;token&#39;);
    if (token) {
      //토큰이 있을 시 헤더에 토큰 추가
      config.headers[&#39;Authorization&#39;] = &#39;Bearer &#39; + token;
    } else {
      //토큰이 없다면 헤더가 필요 없으므로 제거
      delete config.headers[&#39;Authorization&#39;];
    }
    return config;
  },
  function (error) {
    return Promise.reject(error);
  }
);</code></pre>
<p><code>instance.interceptors.request.use</code> 는 요청 인터셉터를 등록하는 메서드입니다. use 메서드는 두 개의 함수를 인자로 받을 수 있습니다.</p>
<ul>
<li>성공 함수
첫 번째 함수는 요청 설정 객체 config를 인자로 받습니다. 이 함수 내에서 다음과 같은 작업을 수행합니다:</li>
</ul>
<p>브라우저의 localStorage에서 &#39;token&#39; 키로 저장된 값을 가져옵니다.
토큰이 존재할 경우 (if (token)), Authorization 헤더를 설정하며, 해당 헤더의 값으로 &#39;Bearer &#39; + token을 지정합니다. 이렇게 하면 요청이 보내질 때 마다 해당 토큰 값이 Authorization 헤더에 포함되어 전송됩니다.</p>
<p>만약 토큰이 없을 경우 (else), 기존에 설정된 Authorization 헤더를 삭제합니다.</p>
<p>처리된 config 객체를 반환합니다. 이 반환된 config가 실제로 axios에 의해 사용되어 요청이 발생합니다.</p>
<ul>
<li>에러 함수:
두 번째 함수는 에러 정보를 담고 있는 error 객체를 인자로 받습니다. 이 함수에서는 단순히 에러를 거절하는 Promise를 반환하도록 설정되어 있습니다 (return Promise.reject(error);). 이렇게 하면 요청 처리 중에 문제가 발생할 경우 해당 에러를 반환하게 됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[제한된 API 에서 추가 정보 요청하기]]></title>
            <link>https://velog.io/@sj_yun/%EC%A0%9C%ED%95%9C%EB%90%9C-API-%EC%97%90%EC%84%9C-%EC%B6%94%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%9A%94%EC%B2%AD%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sj_yun/%EC%A0%9C%ED%95%9C%EB%90%9C-API-%EC%97%90%EC%84%9C-%EC%B6%94%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%9A%94%EC%B2%AD%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 12 Sep 2023 13:18:08 GMT</pubDate>
            <description><![CDATA[<h3 id="프로젝트에서의-문제-상황">프로젝트에서의 문제 상황</h3>
<p>프로젝트 팀원들과 원데이 클래스를 주제로 한 프로젝트를 진행하던 중, 사용자가 회원 가입 시에 수강생 또는 강사로 나누어 회원 가입을 진행하게 만들고자 하였습니다.</p>
<ul>
<li>수강생: 클래스 신청, 창작품 게시물 공유</li>
<li>강사: 클래스 개설, 클래스 정보 등록</li>
</ul>
<p>문제는 프로젝트 팀이 프론트엔드 개발자 4명으로만 구성되어 있었고, 제공된 API만을 사용해야 하는 제한적인 환경이었습니다. 그래서 기존 데이터 구조를 변경하거나 추가하는 것이 불가능했습니다.</p>
<h3 id="해결-방안-기존-데이터에-새로운-데이터-추가">해결 방안: 기존 데이터에 새로운 데이터 추가</h3>
<p>기존 데이터에 새로운 데이터를 추가하여 이를 하나로 묶어 서버에 전달하는 방식을 채택하여 해결하였습니다.</p>
<h4 id="signup-페이지">SignUp 페이지</h4>
<p>먼저 회원 가입 페이지에서는 사용자가 &#39;일반 회원 (수강생)&#39; 또는 &#39;강사 회원&#39; 중에서 선택하도록 하였습니다.</p>
<pre><code class="language-js">//회원가입 일부코드
const [type, setType] = useState(&#39;Student&#39;);

const handleStudentBtnClick = () =&gt; {
  setType(&#39;Student&#39;);
};

const handleTeacherBtnClick = () =&gt; {
  setType(&#39;Teacher&#39;);
};</code></pre>
<p>이렇게 선택된 유형은 type 변수에 저장됩니다.</p>
<pre><code class="language-js">const setSignup = useSetRecoilState(SignUpAtom);
if (email &amp;&amp; password &amp;&amp; emailValid &amp;&amp; passwordValid) {
  setSignup({ email, password, type });
  navigate(&#39;/account/set_profile&#39;);
} else {
  setSignup(false);
}</code></pre>
<p>회원 가입 시에 입력한 정보와 선택한 회원 유형(type)은 가입한 이메일과 비밀번호와 함께 Recoil의 useSetRecoilState hook을 통해 SignUpAtom유저 정보로 저장됩니다. Reocil과 관련된 자세한 포스팅은 (링크)를 참조해주세요</p>
<h4 id="signupatom-컴포넌트">SignUpAtom 컴포넌트</h4>
<pre><code class="language-js">import { atom } from &quot;recoil&quot;;
import { recoilPersist } from &quot;recoil-persist&quot;;

const { persistAtom } = recoilPersist();

//회원가입 토큰 정보
export const SignUpAtom = atom({
  key: &quot;SignUpAtom&quot;,
  default: {},
  effects_UNSTABLE: [persistAtom],
});
</code></pre>
<h4 id="setprofile-페이지">setProfile 페이지</h4>
<pre><code class="language-js">import { SignUpAtom } from &#39;../../Store/AtomSignupState&#39;; // SignUpAtom import

const SetProfile = () =&gt; {
  const signupInfo = useRecoilValue(SignUpAtom);
  const [userInfo, setUserInfo] = useState({ ...signupInfo });  
}</code></pre>
<p>SetProfile 컴포넌트 내에서, useRecoilValue(SignUpAtom)을 이용해 SignUpAtom의 현재 값을 읽어옵니다. 그 값을 signupInfo에 저장합니다.
useState({ ...signupInfo })를 통해 signupInfo의 값을 복사하여 userInfo라는 유저정보다 담긴 로컬 상태를 생성합니다.</p>
<pre><code class="language-js">//프로필 설정 일부코드

const handleSetProfileSubmit = async (event) =&gt; {
  event.preventDefault();

  if (username &amp;&amp; accountname &amp;&amp; usernameValid &amp;&amp; accountValid) {
    //accountname과 type을 같이 전달 ex) StudentJunny
    const updatedAccountname = `${userInfo.type}${accountname}`;
    setUserInfo((prevValue) =&gt; {
      return {
        ...prevValue,
        username: username,
        accountname: updatedAccountname, // 같이 합친 값을 accountname으로 요청
        intro: intro,
        image: image,
      };
    });
    navigate(&#39;/account/login&#39;);
  }
};

 useEffect(() =&gt; {
  if (username &amp;&amp; accountname &amp;&amp; usernameValid &amp;&amp; accountValid) {
    PostSignUp(userInfo);
  }
}, [username, accountname, usernameValid, accountValid, userInfo]);</code></pre>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/0b6e1d9e-d1d4-46d7-8b9e-c97983af9bf9/image.png" alt=""></p>
<p>프로필 설정 페이지로 넘어와 프로필 설정란을 다 작성하고 제출하기 위해 handleSetProfileSubmit가 실행될 때, API요청 명세에는 유저의 회원 유형에 대한 별도의 처리가 없기 때문에 회원 유형을 식별할 수 있도록 accountname (계정 ID) 값 앞에 Student 또는 Teacher 중 하나의 type 값을 추가한 변수인 <code>updatedAccountname</code>에  함께 전달하여 학생과 강사 계정을 구분할 수 있도록 하였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ Image Lazy Loading 를 이용한 이미지 최적화]]></title>
            <link>https://velog.io/@sj_yun/%EC%A0%90%EC%A7%84%EC%A0%81-%EB%A1%9C%EB%94%A9-%EA%B8%B0%EB%B2%95-lazy-loading%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@sj_yun/%EC%A0%90%EC%A7%84%EC%A0%81-%EB%A1%9C%EB%94%A9-%EA%B8%B0%EB%B2%95-lazy-loading%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Fri, 01 Sep 2023 12:09:16 GMT</pubDate>
            <description><![CDATA[<h2 id="image-lazy-loading이란">Image Lazy Loading이란?</h2>
<p>Image lazy loading은 아직 화면에 보여지지 않은 이미지들은 로딩 시점을 뒤로 미루어 초기 로딩시간을 단축할 수 있는 웹 성능 최적화 기법.</p>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/66524b4a-92ea-42ba-b3d5-6606df21b761/image.png" alt=""></p>
<h2 id="언제-사용하면-좋을까">언제 사용하면 좋을까?</h2>
<p>위 이미지는 팔로우한 사용자들이 작성한 게시글들을 화면에 보여주는 홈 피드이다. 팔로우한 사람들이 점점 많아질수록 홈 피드에는 로드해야 될 게시글들 또한 많아져 로딩시간이 길어질 것이므로 최적화를 위해 Image Lazy Loading 을 사용하기로 하였다.</p>
<h2 id="image-lazy-loading-적용시">Image Lazy Loading 적용시</h2>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/d4b1879e-5a0b-4e2f-95bf-0f15d84e45b9/image.gif" alt="">
이미지가 로딩될 때 원본 이미지 대신 저화질의 이미지를 보여줌으로써 UX를 향상 시켰다.</p>
<h2 id="구현-방법">구현 방법</h2>
<p>Image lazy loading 기법은 홈피드에 로드되는 게시글 이미지 뿐만 아니라 클래스 이미지나, 유저 게시글 이미지에도 중복해서 사용할 수 있기 때문에 재사용성을 고려해 
ProgressiveImg 컴포넌트를 만들고, 각 컴포넌트 내의 이미지에 적용시켜 주도록 설계하였다. </p>
<p>웹페이지의 특정 요소가 화면에 보이는지 아닌지를 감지하는 API인 *<em>react-intersection-observer *</em>라이브러리를 이용하여 lazy-loading를 구현하였다.</p>
<h3 id="전체-코드">전체 코드</h3>
<pre><code class="language-js">//ProgressiveImg.jsx
import React, { useEffect, useState } from &#39;react&#39;;
import { Img } from &#39;./ProgressiveImgStyle&#39;;

import { useInView } from &#39;react-intersection-observer&#39;;

import NoImg from &#39;../../../assets/img/No_Image_Available.jpg&#39;; //원본 이미지 로드 실패시 보여줄 이미지 
export default function ProgressiveImg({
  placeholderSrc,
  src,
  styles,
  ...props
}) {
  const [imgSrc, setImgSrc] = useState(placeholderSrc || src);
  const [isLazy, setIsLazy] = useState(true);
  const { ref, inView } = useInView();
  const customClass = isLazy ? &#39;loading&#39; : &#39;loaded&#39;;
  useEffect(() =&gt; {
    if (inView &amp;&amp; imgSrc === placeholderSrc) {
      const img = new Image();
      img.src = src;
      img.onload = () =&gt; {
        setImgSrc(src);
        setIsLazy(false);
      };
      img.onerror = () =&gt; {
        setImgSrc(NoImg);
        setIsLazy(false);
      };
    }
  }, [src, inView]);

  return (
    &lt;Img
      {...{ src: imgSrc, ...props }}
      className={customClass}
      style={styles}
      loading=&#39;lazy&#39;
      alt={props.alt || &#39;&#39;}
      ref={ref}
    /&gt;
  );
}

</code></pre>
<h3 id="주요-코드">주요 코드</h3>
<pre><code class="language-js">import { useInView } from &#39;react-intersection-observer&#39;;

const { ref, inView } = useInView();</code></pre>
<p>react-intersection-observeruse 라이브러리에서 제공하는 InView hook을 사용하여 이미지 요소가 화면에 보이는지 여부를 감지한다. 화면에 보이는 경우, inView는 true가 된다. </p>
<ul>
<li><p>ProgressvieImg Props</p>
<ul>
<li><code>placeholderSrc</code> : 원본 이미지가 로딩되기전 보여줄 저화질의 이미지 url   </li>
<li><code>src</code> : 실제 로드할 원본 이미지의 URL<ul>
<li><code>...props</code> : 기타 이미지들에 전달할 props</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>상태 관리</p>
<ul>
<li><p><code>imgSrc</code> : 현재 보여줄 이미지의 URL이며 초기값은 placeholderSrc 또는 src로 설정</p>
</li>
<li><p><code>isLazy</code> : 이미지가 lazy-loading 중인지의 상태를 나타냄. 초기값은 true로 설정, 이미지가 로드되면 false로 변경되고 이미지 블러처리를 해제 </p>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>이미지 로딩 로직</p>
<pre><code class="language-js">const img = new Image();
img.src = src;

    img.onload = () =&gt; {
      setImgSrc(src);
      setIsLazy(false);
    };

    img.onerror = () =&gt; {
      console.log(&#39;이미지 로드 실패:&#39;, src);
      setImgSrc(NoImg); 
      setIsLazy(false);
    };</code></pre>
<p>Image 객체를 생성하여 원본 이미지를 로드합니다. <code>onload</code> 이벤트는 이미지 로드가 성공적으로 완료되었을 때  setImgSrc에 원본 이미지 src를, <code>onerror</code> 이벤트는 이미지 로드에 실패했을 보여줄 이미지 src를 설정한다.</p>
</li>
</ul>
<pre><code class="language-js">//ProgressiveImgStyle.jsx
import styled from &#39;styled-components&#39;;

export const Img = styled.img`
  transition: all 0.5s;

  &amp;.loading {
    filter: blur(10px);
    clip-path: inset(0);
  }
  &amp;.loaded {
    filter: blur(0px);
  }
`;
export const Placeholder = styled.div`
  width: 100%;
  height: 100%;
  background-color: ${(props) =&gt; props.color || &#39;#eee&#39;};
`;
</code></pre>
<p>저화질 이미지 블러 설정/해제는 ProgressiveImgStyle에서 작성했다.</p>
<h2 id="컴포넌트-적용">컴포넌트 적용</h2>
<p>이제 위에서 만든 ProgressiveImg 컴포넌트를 작성 게시물 컴포넌트인 PostContent에 적용해보자 </p>
<pre><code class="language-js">//PostContent에 필요한 Import 
import ProgressiveImg from &#39;../ProgressiveImg/ProgressiveImg&#39;;
import PlaceholderImg from &#39;../../../assets/img/placeholderImg.svg&#39;; // 로딩 전에 보여줄 저화질 이미지 </code></pre>
<ul>
<li>ProgressiveImg 적용 전</li>
</ul>
<pre><code class="language-js">   &lt;img
         key={index}
         src={HandleNormalizeImage(postImage)}
         width={postImages.length &gt; 1 ? &#39;168px&#39; : &#39;304px&#39;}
         alt=&#39;&#39;
    /&gt; </code></pre>
<ul>
<li><p>ProgressiveImg 적용 후 </p>
<pre><code class="language-js">&lt;ProgressiveImg
        key={index}
        src={HandleNormalizeImage(postImage)} // // 로딩 되면 보여줄 원본 이미지 
        width={postImages.length &gt; 1 ? &#39;168px&#39; : &#39;304px&#39;}
        alt=&#39;게시글 이미지&#39;
        placeholderSrc={PlaceholderImg} // 로딩 전에 보여줄 저화질 이미지 
  /&gt;
</code></pre>
<h2 id="image-lazy-loading-적용-전-후-비교">Image Lazy Loading 적용 전 후 비교</h2>
<p>개발자 전용 도구 네트워크탭에서 웹 페이지를 로드하는 동안 실제로 네트워크를 통해 전송된 데이터의 양 비교를 해본 결과</p>
</li>
</ul>
<blockquote>
<p> 이 값들이 크면 페이지 로드 시간이 늘어날 수 있으므로, 최적화 작업에서 이 값을 최소화하는 것이 중요합니다.
이미지  전송량: 9.4MB =&gt; 14.7kb ( 99.85% 축소)
 최적화 전 로딩속도 : 5.37초 =&gt; 3초(2.37초  단축)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/084b4608-183e-4e5d-ab82-67785315dff6/image.png" alt="">
 <img src="https://velog.velcdn.com/images/sj_yun/post/d8695282-1b92-43cf-9829-5693370c50a0/image.png" alt="">
페이지 로딩시간이 단축된 것을 확인할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ [TS] TypeScript  타입]]></title>
            <link>https://velog.io/@sj_yun/TypeScript-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@sj_yun/TypeScript-%ED%83%80%EC%9E%85</guid>
            <pubDate>Thu, 27 Jul 2023 05:01:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>기본타입</p>
</blockquote>
<h3 id="number">number</h3>
<ul>
<li>보통 일반적인 프로그래밍 언어에서 정수는 short, int, long, 실수는 float, double을 사용한다.</li>
<li>하지만, TypeScript에서는 number 타입이 정수, 실수 뿐 아니라 2, 8, 16진수까지 표현할 수 있어 하나로 충분하다.</li>
<li>모든 수치 연산에 사용되는 값은 number 타입으로 명시를 해야한다.</li>
</ul>
<pre><code class="language-js">    let a: number
    a = 3;
    a = &quot;3&quot;; // error</code></pre>
<h3 id="string">string</h3>
<ul>
<li>string 타입은 문자열 데이터를 나타낸다.</li>
<li>작은 따옴표(’), 큰 따옴표(”), 백쿼트(`) 를 사용<pre><code class="language-js">  let name : string = &#39;seojun&#39;
  name = seojun //error</code></pre>
`</li>
</ul>
<h3 id="boolean">boolean</h3>
<ul>
<li>두가지 상태를 담고 싶은 변수에 사용한다.</li>
<li>3가지 이상은 <code>enum</code> 이나 <code>string</code> 으로 사용한다.</li>
<li><code>boolean</code> 타입은 참(true) 또는 거짓(false) 값이다.<pre><code class="language-js">  let age: number = &#39;25&#39;
  let isAdult:boolean = true;
</code></pre>
</li>
</ul>
<pre><code>
### Array

array 자료안에 들어갈 타입은 타입명`[]`으로 지정하면 된다
```js
    let a: number[] = [1,2,3]
    let a2: Array&lt;number&gt; = [1,2,3]
    let fruits: string [] = [&quot;apple&quot;, &quot;bannna&quot;, &quot;grape&quot;]

    fruits.push(3) // error </code></pre><pre><code class="language-js">    function showItems(arr: number[]){
      arr.forEach((item) =&gt; console.log(item);
       });           
    }

    showItems([1,2,3]); 
    showItems([&#39;1&#39;,&#39;2&#39;,&#39;3&#39;]);</code></pre>
<h3 id="object">Object</h3>
<ul>
<li><p>object 자료안에 들어갈 타입은 선언한 object와 똑같은 모습으로 지정하면 된다.</p>
</li>
<li><p>변수명 오른쪽에 오는 것들은 전부 타입지정 문법입니다. </p>
</li>
</ul>
<pre><code class="language-js">    let info : { age : number } = { age : 20 }</code></pre>
<h3 id="tuple튜플">tuple(튜플)</h3>
<ul>
<li>튜플은 서로 다른 타입의 원소를 순서에 맞게 가질 수 있는 특수한 형태의 배열이다.</li>
<li>배열은 number[], string[] 처럼 같은 타입의 원소만 가질 수 있었지만 튜플은 </li>
<li><em>어떤 타입의 원소를 허용할 것인지*</em> 정의만 해주면 허용된 타입의 데이터를 저장할 수 있다.</li>
</ul>
<pre><code class="language-js">const person: [string, number, boolean] = [&#39;Spartan&#39;, 25, false];
const person2: [string, number, boolean] = [25, &#39;Spartan&#39;, false]; // error</code></pre>
<ul>
<li>그러나 타입의 <code>순서</code>가 정의와 일치해야 하며, 들어오는 데이터의 개수도 맞춰야한다.</li>
</ul>
<h3 id="enum열거형-데이터-타입">enum(열거형 데이터 타입)</h3>
<ul>
<li>enum은 명확하게 관련된 상수 값들을 <code>그룹화</code> 하고자 할 때 사용한다.</li>
<li>다양한 상수를 이름으로 접근하고 사용한다.
enum 안에 있는 각 요소는 값이 설정되어 있지 않으면 기본적으로 숫자 <code>0</code> 으로 시작한다.
enum 안에 있는 요소에는 <code>number</code> 혹은 <code>string</code> 타입의 값만을 할당할 수 있다.
<img src="https://velog.velcdn.com/images/sj_yun/post/ba5390dd-6c54-4cb7-b294-e716d8f7635c/image.png" alt=""></li>
<li>enum 안에 있는 각 요소는 값이 설정되어 있지 않으면 기본적으로 숫자 0으로 시작한다.
<img src="https://velog.velcdn.com/images/sj_yun/post/460a1031-1363-45ff-bf3e-5e402669c84c/image.png" alt="">
<img src="https://velog.velcdn.com/images/sj_yun/post/e25a45f3-c481-4a6f-ba9c-32756fd2f33e/image.png" alt=""></li>
<li>요소의 값을 설정하면 다음 변수는 설정한 요소의 값을 반영한다</li>
</ul>
<h2 id="타입을-지정하지-않는다면">타입을 지정하지 않는다면?</h2>
<p>위에서 타입 지정하는 것을 살펴보았습니다. 하지만 모든 변수에 타입을 지정하려고 한다면 여간 귀찮은 일이 아닐겁니다. 그렇기에** 타입스크립트는 변수 생성시 타입스크립트가 타입을 자동으로 부여해줍니다**. </p>
<p><img src="https://velog.velcdn.com/images/sj_yun/post/88750240-780a-47ea-af04-42d497a6f78c/image.png" alt=""></p>
<p>보시다싶이 문자열인 &#39;서준&#39;을 담고 있는 변수 myName에 따로 string 타입을 지정하지 않았음에도 변수 myName을 확인하면 자동으로 타입이 string으로 지정된 것을 볼 수 있습니다.</p>
]]></description>
        </item>
    </channel>
</rss>