<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>min_jae.log</title>
        <link>https://velog.io/</link>
        <description>고양이 간식 사줄려고 개발하는 사람</description>
        <lastBuildDate>Sat, 03 May 2025 08:13:29 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>min_jae.log</title>
            <url>https://velog.velcdn.com/images/min_jae/profile/055ec871-44bc-4c46-a1a5-3927fa25ced7/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. min_jae.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/min_jae" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Next.js에서 PWA 사용하기]]></title>
            <link>https://velog.io/@min_jae/Nextjs%EC%97%90-PWA-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@min_jae/Nextjs%EC%97%90-PWA-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 03 May 2025 08:13:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min_jae/post/bc7fc5e9-529c-4b9e-9d48-2744d41b1eae/image.png" alt="PWA"></p>
<blockquote>
<p>이미지 출처: <a href="https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%A0%88%EC%8B%9C%EB%B8%8C_%EC%9B%B9_%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98">wikipedia</a></p>
</blockquote>
<p>Progressive Web Application(PWA)은 웹사이트를 모바일 앱처럼 사용할 수 있게 해주는 기술입니다. 이번 포스트에서는 Next.js 프로젝트에 PWA 기능을 추가하는 방법에 대해 알아보겠습니다.</p>
<hr>
<h2 id="pwa란">PWA란?</h2>
<p>PWA(Progressive Web Application)는 웹과 네이티브 앱의 장점을 결합한 웹 애플리케이션입니다. 모바일에서 앱 스토어를 통해 설치하는 네이티브 앱처럼 사용할 수 있으면서도, 웹의 접근성과 편리함을 유지합니다.</p>
<h3 id="pwa의-주요-특징">PWA의 주요 특징</h3>
<ul>
<li>오프라인 작동 가능</li>
<li>홈 화면에 설치 가능</li>
<li>푸시 알림 지원</li>
<li>네이티브 앱과 유사한 사용자 경험 제공</li>
<li>항상 최신 상태 유지</li>
</ul>
<hr>
<h2 id="nextjs-프로젝트에서-pwa-적용하기">Next.js 프로젝트에서 PWA 적용하기</h2>
<p>Next.js 프로젝트에 <code>next-pwa</code> 라이브러리를 사용하여 PWA 기능을 추가하는 방법입니다.</p>
<h3 id="1-next-pwa-패키치-설치">1. next-pwa 패키치 설치</h3>
<p>사용하는 패키지 매니저에 따라 <code>next-pwa</code>를 설치합니다.</p>
<pre><code class="language-bash">// npm 
npm install next-pwa

// yarn
yarn add next-pwa

// pnpm
pnpm add next-pwa</code></pre>
<h3 id="2-nextjs-설정-업데이트">2. Next.js 설정 업데이트</h3>
<p><code>next.config.ts</code> 파일을 수정하여 PWA 설정을 추가합니다.</p>
<pre><code class="language-ts">import withPWA from &#39;next-pwa&#39;;

const pwaConfig = withPWA({
  dest: &#39;public&#39;,
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === &#39;development&#39;,
});

export default pwaConfig(nextConfig);</code></pre>
<h3 id="3-manifestjson-파일-생성">3. manifest.json 파일 생성</h3>
<p>PWA의 메타데이터를 정의하는 <code>manifest.json</code> 파일을 <code>publice</code> 디렉토리에 생성합니다.</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;PWA manifest&quot;,
  &quot;short_name&quot;: &quot;PWA manifest&quot;,
  &quot;description&quot;: &quot;PWA manifest&quot;,
  &quot;start_url&quot;: &quot;/&quot;,
  &quot;display&quot;: &quot;standalone&quot;,
  &quot;background_color&quot;: &quot;#ffffff&quot;,
  &quot;theme_color&quot;: &quot;#000000&quot;,
  &quot;icons&quot;: [
    {
      &quot;src&quot;: &quot;/images/icon.png&quot;,
      &quot;sizes&quot;: &quot;192x192&quot;,
      &quot;type&quot;: &quot;image/png&quot;,
    },
    {
      &quot;src&quot;: &quot;/images/icon.png&quot;,
      &quot;sizes&quot;: &quot;512x512&quot;,
      &quot;type&quot;: &quot;image/png&quot;,
    }
  ]
}</code></pre>
<p><code>manifest.json</code>에서 정의한 아이콘들을 <code>public/icons/</code> 디렉토리에 추가합니다. 최소한 192x192와 512x512 크기의 아이콘을 준비하는 것이 권장됩니다.</p>
<h3 id="4-rootlayouttsx-설정">4. RootLayout.tsx 설정</h3>
<blockquote>
<p>저는 App Router를 사용하므로 App Router 기준입니다.</p>
</blockquote>
<blockquote>
<p>Pages Router를 사용한다면 <code>pages/_document.tsx</code> 파일에 추가합니다.</p>
</blockquote>
<p><code>app/layout.tsx</code> 파일에 PWA 관련 메타태그를 추가합니다.</p>
<pre><code class="language-tsx">export const metadata: Metadata = {
  title: &#39;Create Next App&#39;,
  description: &#39;Generated by create next app&#39;,
  manifest: &#39;/manifest.json&#39;,
};
</code></pre>
<h3 id="5-next-pwa-타입-정의-추가">5. next-pwa 타입 정의 추가</h3>
<p>TypeScript를 사용하신다면 <code>next-pwa</code>의 타입 정의를 추가해야 합니다.</p>
<pre><code class="language-ts">// next-pwa.d.ts
/* eslint-disable @typescript-eslint/no-unused-vars */
declare module &#39;next-pwa&#39; {
  import { NextConfig } from &#39;next&#39;;

  type PWAConfig = {
    dest?: string;
    disable?: boolean;
    register?: boolean;
    skipWaiting?: boolean;
  };

  const withPWA =
    (config: PWAConfig): ((nextConfig: NextConfig) =&gt; NextConfig) =&gt;
    (nextConfig: NextConfig) =&gt;
      NextConfig;

  export default withPWA;
}
</code></pre>
<hr>
<h2 id="pwa-테스트-방법">PWA 테스트 방법</h2>
<pre><code class="language-bash">pnpm build &amp;&amp; pnpm start</code></pre>
<hr>
<h2 id="pwa-적용-결과">PWA 적용 결과</h2>
<h3 id="pc-버전">PC 버전</h3>
<img src='https://velog.velcdn.com/images/min_jae/post/abfc1687-ef38-4394-b414-230935a226d8/image.png' width=400 />


<img src='https://velog.velcdn.com/images/min_jae/post/b75f1eba-9296-45ae-9a11-669948b06ffe/image.png' width=400 />


<h3 id="mobile-버전">Mobile 버전</h3>
<img src='https://velog.velcdn.com/images/min_jae/post/e54edaca-7633-400f-b9ec-d2615a2dffaa/image.jpeg' width=200 />


<img src='https://velog.velcdn.com/images/min_jae/post/fabefbf9-de4f-4183-8fce-4435ee9490c5/image.png' width=200 />

<hr>
<h2 id="결론">결론</h2>
<p>Next.js에서 <code>next-pwa</code> 라이브러리를 사용하면 간단하게 PWA 기능을 구현할 수 있습니다. 오프라인 지원과 설치 가능한 웹앱을 제공함으로써 사용자 경험을 크게 향상시킬 수 있습니다.</p>
<hr>
<p>✅ 참고</p>
<ul>
<li><a href="https://nextjs.org/docs/app/guides/progressive-web-apps">Next.js 공식 문서 - Progressive Web Apps</a></li>
<li><a href="https://velog.io/@hyunjoong/Next.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-PWA-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0">Next.js 프로젝트 PWA 구축하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js App Router에서 next-intl로 다국어 지원하기]]></title>
            <link>https://velog.io/@min_jae/Next.js-App-Router%EC%97%90%EC%84%9C-next-intl%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@min_jae/Next.js-App-Router%EC%97%90%EC%84%9C-next-intl%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 23 Apr 2025 13:51:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min_jae/post/c3a1dd79-85aa-4570-8c4d-00482560599f/image.png" alt=""></p>
<p>Next.js의 App Router를 사용하여 다국어 웹사이트를 개발할 때 <a href="https://next-intl.dev/">next-intl</a>은 매우 유용한 라이브러리입니다. 이 글에서는 next-intl을 설치하고 구성하는 방법부터 실제 사용 예시까지 자세히 살펴보겠습니다.</p>
<h2 id="next-intl-설치-및-폴더-구조">next-intl 설치 및 폴더 구조</h2>
<h3 id="설치">설치</h3>
<pre><code class="language-bash">// npm
npm install next-intl

// yarn 
yarn add next-intl

// pnpm
pnpm add next-intl</code></pre>
<p>사용하시는 패키지 매니저에 맞게 <code>next-intl</code>를 설치합니다.</p>
<h3 id="폴더-구조">폴더 구조</h3>
<p>다음은 <code>next-intl</code>을 사용하기 위한 권장 폴더 구조입니다.</p>
<pre><code>├── messages
│   ├── ko.json
│   ├── en.json
│   └── ...
├── next.config.ts
└── src
    ├── i18n
    │   ├── routing.ts    # 라우팅 설정
    │   ├── navigation.ts # 네비게이션 유틸리티
    │   └── request.ts    # 서버 요청 관련 유틸리티
    ├── middleware.ts     # 언어 감지 및 리다이렉션
    └── app
        └── [locale]      # 동적 경로 세그먼트로 언어 코드를 받음
            ├── layout.tsx
            ├── page.tsx
            └── YOUR_PAGE_PATH
                └── page.tsx</code></pre><hr>
<h2 id="설정">설정</h2>
<h3 id="기본-json-파일">기본 JSON 파일</h3>
<p>언어별 번역 파일을 JSON 파일에 설정합니다.</p>
<pre><code class="language-json">// ko
{
  &quot;fetchError&quot;: &quot;데이터를 불러오는데 실패했습니다.&quot;,
  &quot;noSearchResults&quot;: &quot;검색 결과가 없습니다&quot;,
}</code></pre>
<pre><code class="language-json">// en
{
  &quot;fetchError&quot;: &quot;Failed to fetch data.&quot;,
  &quot;noSearchResults&quot;: &quot;No search results found&quot;,
}</code></pre>
<h3 id="nextjs-설정">Next.js 설정</h3>
<pre><code class="language-ts">// next.config.ts 
import type { NextConfig } from &#39;next&#39;;
import createNextIntlPlugin from &#39;next-intl/plugin&#39;;

const nextConfig: NextConfig = {};

const withNextIntl = createNextIntlPlugin();

export default withNextIntl(nextConfig);</code></pre>
<h3 id="i18n-디렉토리-설정">i18n 디렉토리 설정</h3>
<h4 id="srci18nroutingts">src/i18n/routing.ts</h4>
<p>이 파일은 지원하는 로케일과 기본 로케일을 설정합니다,</p>
<pre><code class="language-ts">import {defineRouting} from &#39;next-intl/routing&#39;;

export const routing = defineRouting({
  // A list of all locales that are supported
  locales: [&#39;en&#39;, &#39;de&#39;],

  // Used when no locale matches
  defaultLocale: &#39;en&#39;
});</code></pre>
<h4 id="srci18nnavigationts">src/i18n/navigation.ts</h4>
<p>이 파일은 클라이언트 컴포넌트에서 사용할 네비게이션 유틸리티를 제공합니다.</p>
<pre><code class="language-ts">import {createNavigation} from &#39;next-intl/navigation&#39;;
import {routing} from &#39;./routing&#39;;

export const {Link, redirect, usePathname, useRouter, getPathname} =
  createNavigation(routing);</code></pre>
<h4 id="srcrequestts">src/request.ts</h4>
<p>서버 컴포넌트에서 번역을 가져오는 방법을 제공합니다</p>
<pre><code class="language-ts">import {getRequestConfig} from &#39;next-intl/server&#39;;
import {hasLocale} from &#39;next-intl&#39;;
import {routing} from &#39;./routing&#39;;

export default getRequestConfig(async ({requestLocale}) =&gt; {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default
  };
});</code></pre>
<h4 id="srcmiddlewarets">src/middleware.ts</h4>
<p>미들웨어는 사용자의 요청을 인터셉트하여 로케일을 확인하고 필요시 리다이렉션합니다.</p>
<pre><code class="language-ts">import createMiddleware from &#39;next-intl/middleware&#39;;
import {routing} from &#39;./i18n/routing&#39;;

export default createMiddleware(routing);

export const config = {
  // Match all pathnames except for
  // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
  // - … the ones containing a dot (e.g. `favicon.ico`)
  matcher: &#39;/((?!api|trpc|_next|_vercel|.*\\..*).*)&#39;
};</code></pre>
<hr>
<h2 id="사용-예시">사용 예시</h2>
<h3 id="srcapplocalelayouttsx">src/app/[locale]/layout.tsx</h3>
<p>루트 레이아웃에서 <code>next-intl</code>의 <code>NextIntlClientProvider</code>를 설정합니다.</p>
<pre><code class="language-tsx">import { NextIntlClientProvider } from &#39;next-intl&#39;;
import { getMessages } from &#39;next-intl/server&#39;;

export async function generateStaticParams() {
  return [&#39;en&#39;, &#39;ko&#39;, &#39;zh&#39;].map(locale =&gt; ({ locale }));
}

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise&lt;{ locale: string }&gt;;
}) {
  const resolvedParams = await params;
  const locale = resolvedParams.locale;
  const messages = await getMessages({ locale });

  return (
    &lt;NextIntlClientProvider locale={locale} messages={messages}&gt;
      {children}
    &lt;/NextIntlClientProvider&gt;
  );
}</code></pre>
<p><code>generateStaticParams</code> 함수는 빌드 시 지원할 언어 목록을 정적으로 생성합니다. 여기서는 영어(en), 한국어(ko), 중국어(zh)를 지원하도록 설정했습니다.</p>
<h3 id="srcapplocalepagetsx">src/app/[locale]/page.tsx</h3>
<pre><code class="language-tsx">&#39;use client&#39;;

import { useState, useEffect } from &#39;react&#39;;
import { useTranslations } from &#39;next-intl&#39;;
import { useParams } from &#39;next/navigation&#39;;



const HistoricSite = () =&gt; {
  const t = useTranslations();
  const { locale } = useParams();

  return (
    &lt;div className=&quot;container mx-auto px-4 py-8&quot;&gt;
        &lt;div className=&quot;text-center py-8 text-gray-500&quot;&gt;{t(&#39;noSearchResults&#39;)}&lt;/div&gt;
    &lt;/div&gt;
  );
};

export default HistoricSite;
</code></pre>
<blockquote>
<p>위 JSON 파일에 작성된 <code>noSearchResults</code> 제외 다른 코드들은 지웠습니다.</p>
</blockquote>
<p>이 예시에서는 <code>useTranslations</code> 훅을 사용하여 현재 활성화된 언어의 번역을 가져옵니다.</p>
<p>위와 같이 구현하면 기존 <code>domain/HistoricSite</code> URL이 이제 <code>domain/ko/HistoricSite</code> 또는 <code>domain/en/HistoricSite</code>와 같이 로케일 경로가 포함된 형태로 변경됩니다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/min_jae/post/0266fe99-fd11-49e4-8066-8fc0be52c673/image.png" alt="ko"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/99b0be18-58ed-4bf5-be2c-9f94ab2eda7e/image.png" alt="en"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/26d70f2b-f475-45dc-9413-9e5cce19c0e5/image.png" alt="zh"></p>
<blockquote>
<p>저는 사진 우측상단 드롭다운을 통해 [locales]를 변경했습니다. </p>
</blockquote>
<hr>
<h2 id="결론">결론</h2>
<p><code>next-intl</code>은 Next.js App Router에서 다국어 지원을 구현하는 데 강력한 도구입니다. 기본적인 번역부터 날짜/시간 포맷팅, 복수형 처리까지 다양한 국제화 기능을 제공합니다.</p>
<p><code>useParams</code>를 사용하여 현재 로케일 정보를 가져오고, <code>useTranslations</code>를 통해 해당 언어의 번역 텍스트를 렌더링함으로써 사용자에게 친숙한 언어로 콘텐츠를 제공할 수 있습니다.</p>
<hr>
<p>✅ 참고 </p>
<ul>
<li><p><a href="https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing">next-intl_docs</a></p>
</li>
<li><p><a href="https://codesign.tistory.com/441">next-intl을 활용한 다국어 웹사이트 구현하기</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nextjs] Error: localStorage is not defined]]></title>
            <link>https://velog.io/@min_jae/Nextjs-localStorage-Error%EC%99%80-Hydration-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@min_jae/Nextjs-localStorage-Error%EC%99%80-Hydration-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 12 Apr 2025 15:04:16 GMT</pubDate>
            <description><![CDATA[<p>서버 사이드 렌더링(SSR)을 지원하는 Next.js의 특성 때문에 발생하는 문제로, 간단하게 이해하고 해결하는 방법을 공유하겠습니다.</p>
<h2 id="1-error-localstorage-is-not-defined">1. Error: localStorage is not defined</h2>
<p><img src="https://velog.velcdn.com/images/min_jae/post/0c29420a-253d-4d3f-b882-27a580379459/image.png" alt="Error: localStorage is not defined"></p>
<h3 id="발생-원인">발생 원인</h3>
<p>이 오류는 Next.js의 서버 사이드 렌더링 때문에 발생합니다. 코드가 서버에서 실행될 때 <code>localStorage</code>는 브라우저 환경에서만 사용 가능한 API이기 때문에 서버에서는 접근할 수 없습니다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p><code>typeof window !== &#39;undefined&#39;</code>를 사용하여 페이지가 마운트될 때까지 기다렸다가 <code>localStorage</code>에 접근하도록 합니다.</p>
<pre><code class="language-tsx">if (typeof window !== &#39;undefined&#39;) {
  localStorage.getItem(&#39;~~~&#39;);
}</code></pre>
<hr>
<p>✅ 참고</p>
<ul>
<li><a href="https://velog.io/@hyo123/Next.js-localStorage-%EC%97%90%EB%9F%AC%ED%95%B8%EB%93%A4%EB%A7%81">[NextJS] localStorage 에러핸들링 -localStorage is not defined-_hyo</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] useState vs useRef]]></title>
            <link>https://velog.io/@min_jae/React-useState-vs-useRef</link>
            <guid>https://velog.io/@min_jae/React-useState-vs-useRef</guid>
            <pubDate>Mon, 24 Mar 2025 16:09:54 GMT</pubDate>
            <description><![CDATA[<p>최근 기술 면접을 보며 </p>
<blockquote>
<p><code>useState</code>와 <code>useRef</code>의 차이를 아시나요?</p>
</blockquote>
<p>라는 질문을 받았습니다. 당시 뭐라 답했는지 기억은 나지 않지만 긴장감 속에서 제대로 답하지 못했던 기억이 있습니다.</p>
<p><code>useState</code>와 <code>useRef</code>에 대해서 정리해보겠습니다.</p>
<hr>
<h2 id="usestate">useState</h2>
<p>공식 문서에 따르면 </p>
<blockquote>
<p><code>useState</code>는 컴포넌트에 state 변수를 추가할 수 있는 React Hook 입니다.</p>
</blockquote>
<p>React에서 컴포넌트는 자신의 상태 또는 props가 바뀌면 리렌더링 됩니다.
상태를 관리하기 위해 React에서 <code>useState</code>를 활용합니다.</p>
<pre><code class="language-jsx">const [state,setState] = useState(initialState);</code></pre>
<p><code>useState</code>는 상태 유지 값가 그 값을 갱신하는 함수를 반환합니다.
<code>setState</code> 함수는 새 state를 받아 컴포넌트 리렌더링 큐에 등록합니다.</p>
<p>컴포넌트는 다음 렌더링 시에 <code>useState</code>를 통해 반환받은 첫번째 값은 항상 갱신된 최신 state가 됩니다.</p>
<h3 id="usestate-예시">useState 예시</h3>
<pre><code class="language-jsx">import { useState } from &quot;react&quot;;

function Counter() {
  const [count, setCount] = useState(0);

  return (
    &lt;div&gt;
      &lt;p&gt;현재 카운트: {count}&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;증가&lt;/button&gt;
      &lt;button onClick={() =&gt; setCount(count - 1)}&gt;감소&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="usestate-주요-특징">useState 주요 특징</h3>
<ul>
<li><strong>비동기적 업데이트</strong>: setState는 비동기적으로 동작하며, 즉시 상태를 업데이트 하지 않습니다.</li>
<li><strong>함수형 업데이트</strong>: 이전 상태를 기반으로 업데이트할 때는 함수형 업데이트를 사용할 수 있습니다.</li>
</ul>
<hr>
<h2 id="useref">useRef</h2>
<p>공식 문서에 따르면</p>
<blockquote>
<p><code>useRef</code>렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook 입니다.</p>
</blockquote>
<p><code>useRef</code>는 <code>useState</code>와 달리 <strong>리렌더링을 유발하지 않습니다.</strong> 값이 변경되어도 컴포넌트가 다시 렌더링 되지 않습니다.
<code>ref</code> 속성을 사용하여 특정 DOM 요소에 접근할 때 활용되며 렌더링과 무관하게 값을 유지해야 할 경우 유용합니다.</p>
<h3 id="useref-예시">useRef 예시</h3>
<pre><code class="language-jsx">import { useRef } from &quot;react&quot;;

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

  const focusInput = () =&gt; {
    inputRef.current.focus(); // input 요소에 직접 접근하여 포커스를 줌
  };

  return (
    &lt;div&gt;
      &lt;input ref={inputRef} type=&quot;text&quot; /&gt;
      &lt;button onClick={focusInput}&gt;Focus Input&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<hr>
<h2 id="usestate와-useref의-차이">useState와 useRef의 차이</h2>
<table>
<thead>
<tr>
<th align="center">특징</th>
<th align="center">useState</th>
<th align="center">useRef</th>
</tr>
</thead>
<tbody><tr>
<td align="center">값 변경 시 리렌더링</td>
<td align="center">O</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">값 유지 여부</td>
<td align="center">O</td>
<td align="center">O</td>
</tr>
<tr>
<td align="center">DOM 요소 접근</td>
<td align="center">X</td>
<td align="center">O</td>
</tr>
<tr>
<td align="center">상태 변경 함수 제공</td>
<td align="center">O(<code>setState</code>)</td>
<td align="center">X(직접 수정)</td>
</tr>
<tr>
<td align="center">값 접근 방법</td>
<td align="center">변수로 직접 접근</td>
<td align="center"><code>.current</code> 속성으로 접근</td>
</tr>
<tr>
<td align="center">초기화 시점</td>
<td align="center">컴포넌트 렌더링 마다</td>
<td align="center">컴포넌트 마운트 시 한 번만</td>
</tr>
<tr>
<td align="center">업데이트 시점</td>
<td align="center"><code>setState</code> 호출 후 다음 렌더링</td>
<td align="center">즉시</td>
</tr>
</tbody></table>
<h4 id="언제-사용할까">언제 사용할까?</h4>
<ul>
<li>컴포넌트가 변경된 값에 따라 리렌더링이 필요: <code>useState</code></li>
<li>렌더링과 관계없이 값을 유지: <code>useRef</code></li>
<li>DOM 요소에 직접 접근: <code>useRef</code></li>
</ul>
<hr>
<p>✅ 참고</p>
<ul>
<li><a href="https://ko.react.dev/reference/react/useState">React_useState_docs</a></li>
<li><a href="https://ko.react.dev/reference/react/useRef">React_useRef_docs</a></li>
<li><a href="https://velog.io/@hyunjine/useState-vs-useRef">useState vs useRef_현진</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 이벤트 루프]]></title>
            <link>https://velog.io/@min_jae/JavaScript-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84</link>
            <guid>https://velog.io/@min_jae/JavaScript-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84</guid>
            <pubDate>Sun, 16 Mar 2025 12:49:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;이벤트 루프에 대해 설명해주세요.&quot;</p>
</blockquote>
<p>프론트엔드 자바스크립트 기술 면접 단골 손님 이벤트 루프에 대해서 알아보겠습니다.</p>
<blockquote>
<p> ❗️ 내가 이해하기 위해 적은 글 ❗️</p>
</blockquote>
<h2 id="싱글단일-스레드">싱글(단일) 스레드</h2>
<p>자바스크립트는 <strong>싱글 스레드</strong> 기반의 언어이고, 자바스크립트 엔진은 하나의 콜 스택만 사용합니다. 즉, <strong>동시에 하나의 작업만 처리할 수 있다</strong> 는 뜻입니다.</p>
<p>하지만 자바스크립트를 사용하다 보면 마치 동시에 여러 작업이 처리되는 것처럼 보입니다. 예를 들어, 이벤트가 발생하는 동안에도 다른 작업이 진행되거나, 여러 개의 HTTP 요청이 동시에 처리되는 것처럼 보이는 경우가 있습니다.</p>
<p>이러한 동작이 가능한 이유는 바로 <strong>이벤트 루프</strong> 덕분입니다.
자바스크립트는 싱글 스레드 언어이지만, 자바스크립트가 실행되는 환경(예: 브라우저, Node.js)은 멀티 스레드 기능을 제공합니다.</p>
<p>이 환경과 자바스크립트 엔진이 연동하여 비동기 작업을 처리할 수 있도록 해주는 핵심 장치가 바로 <strong>이벤트 루프</strong> 입니다.</p>
<hr>
<h2 id="이벤트-루프의-동작-원리">이벤트 루프의 동작 원리</h2>
<p><img src="https://velog.velcdn.com/images/min_jae/post/bfb4f47c-42b6-401d-ae31-82170d09a3c8/image.png" alt=""></p>
<blockquote>
<p>이미지 출처: <a href="https://felixgerschau.com/javascript-event-loop-call-stack/">JavaScript Event Loop And Call Stack Explained</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/min_jae/post/4d145a3f-2f85-4d6b-903f-447c7d488b81/image.png" alt=""></p>
<blockquote>
<p>이미지 출처: <a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Execution_model">MDN_이벤트 루프</a></p>
</blockquote>
<p>이벤트 루프는 다음과 같은 방식으로 동작합니다.</p>
<ol>
<li>Heap (힙): 객체와 같은 동적 메모리가 할당되는 영역입니다.</li>
<li>Call Stack(콜 스택): 코드가 실행될 때 함수들이 쌓이는 스택 메모리입니다. 싱글 스레드이므로 한 번에 하나의 작업만 실행할 수 있습니다.</li>
<li>Web APIs: 브라우저가 제공하는 API로, <code>setTimeout</code>, <code>Promise</code>, <code>DOM 이벤트</code> 등이 포합됩니다. 이들은 비동기 작업을 처리하는 데 사용됩니다.</li>
<li>Callback Queue(콜백 큐): 비동기 작업이 완료되면 해당 콜백 함수가 여기에 대기하며, 클 스택이 비어야 실행됩니다.</li>
<li>Microtask Queue(마이크로태스크 큐): <code>Promise.then()</code>, <code>MutationObserver</code> 같은 고우선순위 비동기 작업이 들어가는 큐입니다. 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐의 작업을 실행한 후, 콜백 큐에서 대기 중인 작업을 실행합니다.</li>
</ol>
<hr>
<h2 id="이벤트-루프-흐름을-이해하기-위한-예제">이벤트 루프 흐름을 이해하기 위한 예제</h2>
<pre><code class="language-js">console.log(&quot;Start&quot;);

setTimeout(() =&gt; console.log(&quot;setTimeout&quot;), 0);

Promise.resolve().then(() =&gt; console.log(&quot;Promise&quot;));

console.log(&quot;End&quot;);</code></pre>
<h4 id="출력-결과">출력 결과</h4>
<pre><code>Start
End
Promise
setTimeout</code></pre><h4 id="실행-과정-분석">실행 과정 분석</h4>
<ol>
<li><p><code>console.log(&quot;Start&quot;)</code> 실행 → Call Stack에서 실행 후 제거</p>
</li>
<li><p><code>setTimeout</code> 실행 → Web API에 넘김 (0ms 후 콜백 큐로 이동)</p>
</li>
<li><p><code>Promise.resolve().then()</code> 실행 → 마이크로태스크 큐에 저장</p>
</li>
<li><p><code>console.log(&quot;End&quot;)</code> 실행 → Call Stack에서 실행 후 제거</p>
</li>
<li><p>Call Stack이 비어 있음 → Microtask Queue에서 <code>Promise</code> 실행 후 제거</p>
</li>
<li><p>Call Stack이 다시 비어 있음 → Callback Queue에서 <code>setTimeout</code> 실행 후 제거</p>
</li>
</ol>
<blockquote>
<p>&quot;Microtask Queue &gt; Animation Frame &gt; Task Queue&quot; 순으로  Microtask Queue가 가장 먼저 실행되고 Task Queue가 가장 늦게 실행됩니다.</p>
</blockquote>
<hr>
<p>✅ 참고</p>
<ul>
<li><p><a href="https://talkwithcode.tistory.com/89">Javascript Event Loop 이벤트 루프 정리</a></p>
</li>
<li><p><a href="https://velog.io/@robinyoondev/JavaScript-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84-%EC%BD%9C-%EC%8A%A4%ED%83%9D-%ED%83%9C%EC%8A%A4%ED%81%AC-%ED%81%90-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B8%B0%EC%88%A0%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84">JavaScript-이벤트 루프, 콜 스택, 태스크 큐</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트(desub) 회고]]></title>
            <link>https://velog.io/@min_jae/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8desub-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@min_jae/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8desub-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 01 Mar 2025 15:15:48 GMT</pubDate>
            <description><![CDATA[<p>부트캠프 교육 마지막 과정 파이널 프로젝트가 2월 28일부로 끝이 났다. 동시에 부트캠프도 수료를 했다.
사실 팀원들과 프로젝트를 더 발전시켜 나가고 싶었지만, 현실은 그렇게 호락호락하지 않았다. 여러 이슈가 겹치면서 결국 여기서 마무리를 짓기로 결정했다. (이야기의 뒷부분은 조금 있다가 들려주겠다.)
프로젝트를 연속 두번을 이어서 하다보니 지난 프로젝트의 회고를 하지 못해서 이번에는 절대 놓치지 않고, 프로젝트 회고를 남기려 한다.</p>
<hr>
<h2 id="🎨첫-디자이너와-협업-피그마-그리고-회의의-힘">🎨첫 디자이너와 협업, 피그마 그리고 회의의 힘</h2>
<p><code>desub</code> 프로젝트는 처음으로 디자이너와 함께 진행한 프로젝트였다. 더군다나 디자이너 분들이 직접 운영해보고 싶은 주제로 기획된 만큼, 직접 기획하던 기존과는 전혀 다른 방식이 진행했다. 팀 구성은 프론트엔드 4명, 백엔드 2명, 디자이너 2명으로 총 8명이었고, 자연스럽게 커뮤니케이션 툴로 피그마(Figma)를 사용하게 되었다.</p>
<p>처음에는 피그마의 댓글 기능을 활용해 의견을 주고받았지만, <strong>텍스트만으로는 서로의 의도를 100% 이해하기가 어려웠다.</strong> 단순히 디자인 피드백을 주고받는 걸 넘어, &quot;이 의도가 맞을까?&quot;, &quot;이 흐름이 적절할까?&quot; 같은 맥락까지 공유하려면 더 직접적인 소통이 필요했다.</p>
<p>그래서 매주 수요일, 정기 미팅을 열었다. 그리고 결론부터 말하자면…</p>
<blockquote>
<p>이 미팅 없었으면 프로젝트 완성도는 바닥을 쳤을 거다.</p>
</blockquote>
<p>매주 진행 상황을 공유하고, 피드백을 반영하고, 현실적인 일정 조율까지 할 수 있는 시간이었기 때문이다. 여기서 가장 크게 배운 점은, &quot;일정 내에 가능한 것과 불가능한 것을 명확히 구분하는 것&quot; 이었다.</p>
<p>특히, 팀 내에 퍼블리셔로 일하던 분이 있어서 많이 보고 배울 수 있었다.</p>
<p>&quot;이건 기한 내 구현하기 어려울 것 같은데, 이런 방향으로 조정해보는 건 어떨까요?&quot;
&quot;이 기능은 후순위로 미뤄도 괜찮을까요?&quot;</p>
<p>이런 식으로 기능의 우선순위를 정리하고, 일정 안에서 가능한 범위를 조정하는 과정이 필수적이라는 걸 뼈저리게 느꼈다. 프로젝트는 완벽함이 아니라, 기한 내에 최선의 결과물을 내는 것이 중요하다는 걸 다시 한번 깨닫게 된 경험이었다.</p>
<hr>
<h2 id="📱반응형--애니메이션-둘-다-잡을-수-있을까">📱반응형 + 애니메이션, 둘 다 잡을 수 있을까?</h2>
<p>프로젝트에서 TailwindCSS를 사용한 스타일링과 Framer Motion을 활용해 스크롤 애니메이션을 구현했다.</p>
<p>무엇이 문제였을까? 다들 예상한 대로 그 문제였다.
📌 <strong>웹뷰에서는 완벽한데, 모바일뷰에서는 깨진다?</strong>
📌 <strong>모바일뷰를 완벽하게 수정하면, 웹뷰에서 깨진다?</strong></p>
<p>애니메이션을 유지하며 반응형을 적용하려고 하니 레이아웃이 깨지거나 부자연스러운 움직임이 발생하는 문제가 발생했다. 사실 반응형을 진행하기 전부터 예상했다.</p>
<p>프로젝트 프론트엔드 멘토님께서는 <code>monorepo</code>을 활용하여 모바일뷰를 따로 작업하는 것을 추천하였다.
프로젝트의 폴더 루트가 달라지기 때문에 서로의 영향이 없어 수월하게 진행할 수 있다고 이유였다..
멘토님의 말씀대로 디자이너 분들께 제안을 했지만, &quot;나중에 저희 쪽에서 유지보수 하기에는 반응형이 편해서 반응형으로 부탁합니다.&quot;</p>
<p>&quot;<strong>넵.</strong>&quot;</p>
<p>그렇다. 반응형으로 진행했다. 
피그마에 모바일뷰 프레임이 추가된 상황에 다른 팀원들은 API 연결 작업과 Admin 시스템을 구축하고 있었고 어쩌다보니 대부분의 페이지 반응형 작업을 내가 작업하는 상황이 만들어졌다.
어쩔 수 있나? 잠을 줄이고 시간을 갈아넣었다..</p>
<hr>
<h2 id="🎢프로젝트를-이어가지-못한-이유">🎢프로젝트를 이어가지 못한 이유</h2>
<p>6개월 동안의 부트캠프를 마쳤다.
하지만 부트캠프만으로 바로 취업 시장에서 살아남기엔 아직 부족하다는 현실을 마주해야 했다.
특히 요즘 <strong>신입 개발자의 취업 시장은 역대급으로 힘든 상황</strong>이라,
우리 팀원들도 프로젝트를 계속 발전시키면서 리팩토링하고, 취업 준비도 병행하는 방법을 고민했다.
그럼에도 결국 프로젝트를 종료하기로 결정했다.</p>
<h3 id="🚪각자의-선택-각자의-길">🚪각자의 선택, 각자의 길</h3>
<p>팀원들은 각자 다른 이유로 프로젝트를 계속할 수 없는 상황이었다.</p>
<ul>
<li><p>이미 취업해도 이상하지 않을 실력자
퍼블리셔로 일한 경험이 있는 팀원이 있었다. 실력적으로 당장 프론트엔드 직무로도 취업이 가능할 수준이었고, 실제 부트캠프 막바지에 여러 회사 면접을 다녀왔다. 
&quot;취업에 집중 이슈.&quot;</p>
</li>
<li><p>개발자가 아닌 PM의 길을 선택한 팀원
프로젝트를 하며 개발보다는 기획과 팀 운영에 더 흥미를 느낀 팀원도 있었다.</p>
</li>
<li><p>현생이 너무 바빠진 팀원들
위에 작성된 팀원들을 제외하고도 모두 취업 준비를 집중해야 했고, 각자의 계획이 있었다.
사이드 프로젝트, 자격증 준비 등 각자 사정으로 더 이상 프로젝트에 시간을 할애하기 어려운 상황이 되었다.</p>
</li>
</ul>
<h3 id="📦-디자이너들에게-프로젝트를-넘기며">📦 디자이너들에게 프로젝트를 넘기며</h3>
<p>프로젝트를 이어가고 싶었지만, 각자의 목표가 달라지면서 지속적인 유지보수가 어려웠다.
이런 상황에서 디자이너 분들이 프로젝트를 계속 운영하고 싶어 했고,
우리는 개발된 코드를 디자이너분들(기획자들)에게 넘기며 프로젝트를 마무리하기로 했다.</p>
<p>나는 코드를 넘기면서도 이런 생각이 들었고 실제로 백엔드 팀원 <a href="https://github.com/mountain-kangkang">@moutain-kangkang님</a>은 이렇게 말했다.</p>
<blockquote>
<p>&quot;시간이 조금 더 있었더라면,,
&quot;조금만 더 하면,,&quot;</p>
</blockquote>
<h3 id="🏁-프로젝트-종료-그리고-앞으로">🏁 프로젝트 종료, 그리고 앞으로</h3>
<p>이 프로젝트를 끝으로 나 또한 취업 준비와 자격증 준비에 집중할 예정이다.
2025년 2월 정보처리기사 필기에 응시했고 4월에 실기를 앞두고 있다.
비록 프로젝트와 부트캠프는 여기서 끝이 났지만, 개발자로서의 길은 이제 시작이라고 생각한다.</p>
<blockquote>
<p>취준생 주민재는 죽었다. 프론트 개발자 다시 태어난 주민재</p>
</blockquote>
<p>ㅋㅋㅋ 부트캠프 수료 소식을 친구들에게 전했더니 온 답장이다. 친구의 말처럼 프론트 개발자로 다시 태어나는 그 날까지..</p>
<hr>
<p><img src="https://velog.velcdn.com/images/min_jae/post/ec59a6d6-6066-41a0-ba47-0fa4e27db6ae/image.png" alt="1"></p>
<p>현업 기획자로 일하고 있는 형도 </p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/9b555311-216c-437b-941f-253d9fc2782d/image.png" alt="2"></p>
<p>현재 개발자를 준비하는 있는 동기들도</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/8bc1be73-a7bf-4eec-8460-fff0d89b85de/image.png" alt="3"></p>
<p>현업 개발자로 일하고 있는 동기들도</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/117f1f12-a32e-4b08-886a-2ab9368f7416/image.png" alt="4"></p>
<p>같이 부트캠프 수강하다가 취업해서 졸업한 학생들도</p>
<p>나의 노력을 알아줘서 고맙다..</p>
<hr>
<p>마지막으로, 함께 프로젝트를 진행했던 팀원들, 그리고 6개월 동안 함께 고생한 부트캠프 수료생들에게 이 말을 전하고 싶다.</p>
<p><strong>Keep Moving, Keep Growing, Keep Learning</strong>
<strong>See you at work</strong></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/9f0ce35b-741b-416a-9b37-74aac6867995/image.png" alt="seeyouatwork"></p>
<p>덴젤 워싱턴의 이 말처럼,
우리가 업계에서 다시 만나게 될 그날까지 모두 응원합니다. 🚀🔥</p>
<hr>
<p><img src="https://velog.velcdn.com/images/min_jae/post/dafa47ba-ef1c-442a-bc2d-d784ef5465f1/image.png" alt="11"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/81b52923-8e3a-40f9-be2b-6d25e5831e82/image.png" alt="12"></p>
<p>운영에 힘써주신 조교, 코치, 매니저님들 모두 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js SEO 최적화]]></title>
            <link>https://velog.io/@min_jae/%EA%B2%80%EC%83%89-%EC%97%94%EC%A7%84-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@min_jae/%EA%B2%80%EC%83%89-%EC%97%94%EC%A7%84-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Mon, 24 Feb 2025 12:58:19 GMT</pubDate>
            <description><![CDATA[<p>검색 엔진 최적화(SEO)는 웹사이트의 가시성을 높이고 검색 결과에서 더 나은 순위를 얻기 위한 중요한 과정입니다. Next.js는 SEO에 친화적인 기능들을 제공하여 개발자들이 쉽게 최적화를 할 수 있도록 돕습니다. 이 글에서는 Next.js를 사용하여 SEO를 최적화하는 주요 방법들을 살펴보겠습니다.</p>
<hr>
<h3 id="1-메타데이터-설정">1. 메타데이터 설정</h3>
<p>Next.js 14에서는 <code>metadata</code> 객체를 통해 각 페이지의 메타데이터를 쉽게 설정할 수 있습니다.</p>
<pre><code>import type { Metadata } from &#39;next&#39;;

export const metadata: Metadata = {
  title: &#39;페이지 제목&#39;,
  description: &#39;페이지 설명&#39;,
  keywords: [&#39;키워드1&#39;, &#39;키워드2&#39;], // 검색 키워드
  openGraph: {
    title: &#39;OG 제목&#39;,
    description: &#39;OG 설명&#39;,
    images: [{
      url: &#39;/image.jpg&#39;,
      width: 1200,
      height: 630,
      alt: &#39;이미지 설명&#39;
    }]
  }
};</code></pre><blockquote>
<p>메타데이터는 다음과 같은 중요한 SEO 요소들을 포함할 수 있습니다.</p>
</blockquote>
<ul>
<li>기본 메타데이터: title, description, keywords</li>
<li>Open Graph 메타데이터: 소셜 미디어 공유용</li>
<li>Twitter 카드</li>
</ul>
<hr>
<h3 id="2-시맨틱-html-마크업">2. 시맨틱 HTML 마크업</h3>
<p>시맨틱 HTML 태그를 사용하면 웹페이지의 구조와 의미를 검색 엔진이 더 잘 이해할 수 있습니다. Next.js에서는 일반적인 HTML5 시맨틱 태그를 모두 사용할 수 있습니다. 예를 들어</p>
<pre><code class="language-tsx">&lt;header&gt;
  &lt;nav&gt;
    {/* 네비게이션 내용 */}
  &lt;/nav&gt;
&lt;/header&gt;
&lt;main&gt;
  &lt;article&gt;
    &lt;h1&gt;메인 제목&lt;/h1&gt;
    &lt;p&gt;본문 내용...&lt;/p&gt;
  &lt;/article&gt;
&lt;/main&gt;
&lt;footer&gt;
  {/* 푸터 내용 */}
&lt;/footer&gt;</code></pre>
<hr>
<h3 id="3-이미지-최적화">3. 이미지 최적화</h3>
<p>Next.js의 <code>Image</code> 컴포넌트를 사용하면 자동으로 이미지를 최적화할 수 있습니다. 이는 페이지 로딩 속도를 개선하고 사용자 경험을 향상시켜 간접적으로 SEO에 도움을 줍니다.</p>
<pre><code>import Image from &#39;next/image&#39;

&lt;Image
  src=&quot;/images/profile.jpg&quot;
  alt=&quot;프로필 이미지&quot;
  width={500}
  height={500}
/&gt;</code></pre><blockquote>
<p>이미지 컴포넌트뿐만 아니라 폰트 최적화를 위해 next/font 사용, 적절한 캐싱 전략 구현, 코드 분할과 지연 로딩 활용을 종합적으로 적용하면 검색 엔진에서 더 높은 순위를 얻을 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="4-sitemap-설정하기">4. Sitemap 설정하기</h3>
<p>사이트맵은 웹사이트의 구조를 검색 엔진에 알려주는 중요한 파일입니다. Next.js에서는 정적 또는 동적으로 사이트맵을 생성할 수 있습니다.</p>
<p>정적 사이트맵을 생성하려면 <code>app</code> 디렉토리의 루트에 <code>sitemap.xml</code> 파일을 생성하면 됩니다. 동적 사이트맵을 생성하려면 <code>app</code> 디렉토리에 <code>sitemap.ts</code> 또는 <code>sitemap.js</code> 파일을 만들고 다음과 같이 작성할 수 있습니다.</p>
<pre><code class="language-tsx">import { MetadataRoute } from &#39;next&#39;

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: &#39;https://www.example.com&#39;,
      lastModified: new Date(),
    },
    {
      url: &#39;https://www.example.com/about&#39;,
      lastModified: new Date(),
    },
    // 추가 URL들...
  ]
}</code></pre>
<hr>
<h3 id="5-robotstxt-설정하기">5. robots.txt 설정하기</h3>
<p><code>robots.txt</code> 파일은 검색 엔진 크롤러에게 어떤 페이지를 크롤링해야 하는지 알려줍니다. Next.js에서는 정적 파일이나 동적 생성을 통해 <code>robots.txt</code>를 설정할 수 있습니다.</p>
<p>정적 <code>robots.txt</code> 파일을 사용하려면 <code>app</code> 디렉토리의 루트에 파일을 생성하면 됩니다. 동적으로 생성하려면 <code>app</code> 디렉토리에 <code>robots.ts</code> 또는 <code>robots.js</code> 파일을 만들고 다음과 같이 작성할 수 있습니다.</p>
<pre><code>import { MetadataRoute } from &#39;next&#39;

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: &#39;*&#39;,
      allow: &#39;/&#39;,
      disallow: &#39;/private/&#39;,
    },
    sitemap: &#39;https://www.example.com/sitemap.xml&#39;,
  }
}</code></pre><hr>
<h3 id="적용">적용</h3>
<p><img src="https://velog.velcdn.com/images/min_jae/post/3e398ba2-aecb-4ab2-95bb-7dfda7f7c914/image.png" alt="lb"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/d4b296f0-ce29-4e99-8a78-076b5ed38b9d/image.png" alt="la"></p>
<p>위 검색 엔진 최적화 기법을 적절히 활용하여 Lighthouse SEO 점수를 73에서 91로 향상시켰습니다. </p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/d85aa5b8-7bcc-49eb-997a-a557e5467217/image.png" alt="tb"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/8b39cd94-1f0d-4b59-a47d-9261616ad77d/image.png" alt="ta"></p>
<p>또한, metadata의 openGraph를 사용하여 소셜 미디어 공유 시 미리보기 이미지도 설정하였습니다.</p>
<p>이러한 SEO 최적화 기법들을 종합적으로 적용함으로써, 웹사이트의 검색 엔진 순위를 효과적으로 개선하고 사용자 경험을 향상시킬 수 있습니다. Next.js App Router의 새로운 기능들을 활용하면 더욱 효율적이고 강력한 SEO 전략을 구현할 수 있습니다.</p>
<hr>
<p>✅ 참고</p>
<ul>
<li><a href="https://velog.io/@zinukk/d-v8gyfq4x">Next.js SEO 최적화 적용하기</a></li>
<li><a href="https://magomercy.com/javascript/Nextjs-SEO-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C-%EA%B2%80%EC%83%89-%EC%88%9C%EC%9C%84-%EB%86%92%EC%9D%B4%EA%B8%B0-a93ca19d">Next.js SEO 최적화 가이드: 검색 순위 높이기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js는 Image 최적화를 어떻게 할까?]]></title>
            <link>https://velog.io/@min_jae/Nextjs-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@min_jae/Nextjs-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Tue, 18 Feb 2025 14:19:14 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min_jae/post/e6bda488-243c-4b56-bbe3-9d9782b992bf/image.png" alt="HTTP_아카이브_이미지_리포트"></p>
<p>웹 페이지에서 이미지는 큰 비중을 차지하며, <a href="https://httparchive.org/">HTTP 아카이브</a>에 따르면 웹 페이지의 대략 58%가 이미지로 구성되어 있고 이미지의 용량도 다른 콘텐츠에 비해 월등히 높습니다.
따라서 이미지 최적화는 웹 성능을 향상시키는 핵심 요소입니다.</p>
<p>최적화되지 않은 이미지는 페이지 로딩을 느리게 하여 사용자 이탈률을 높이며, SEO에도 부정적인 영향을 미칠 
수 있습니다.</p>
<blockquote>
<p>Next.js는 이러한 문제를 해결하기 위해 강력한 이미지 최적화 기능을 제공하는 <code>next/image</code> 컴포넌트를 제공합니다. <code>next/image</code>의 이미지 최적화 원리에 대해서 알아보겠습니다.</p>
</blockquote>
<h3 id="nextimage-최적화-기능">next/image 최적화 기능</h3>
<p><code>next/image</code> 컴포넌트는 아래 기능들을 제공합니다.</p>
<ul>
<li><strong>최신 포맷으로 변환</strong>: WebP, AVIF 등 차세대 이미지 형식으로 자동 변환</li>
<li><strong>이미지 크기 최적화</strong>: 디바이스 크기에 맞는 이미지 제공</li>
<li><strong>Lazy Loading</strong>: 레이지 로딩 (Lazy Loading) 구현</li>
<li><strong>Placeholder Blur</strong>: 블러 이미지 플레이스홀더 제공</li>
<li><strong>CDN 최적화</strong>: <a href="https://vercel.com/">Vercel</a> 배포 시 자동으로 CDN을 활용한 최적화 수행</li>
</ul>
<p><code>next/image</code>는 클라이언트가 지원하는 최적의 포맷으로 타입을 결정하는데, 만약 formats 배열의 값을 모두 지원하지 않는다면 원본 포맷을 유지합니다.</p>
<pre><code class="language-js">// next.config.mjs
/** @type {import(&#39;next&#39;).NextConfig} */
const nextConfig = {
  images: {
    formats: [&#39;image/avif&#39;, &#39;image/webp&#39;], 
  },
}

export default nextConfig</code></pre>
<p><code>next.config</code>에서 options를 설정할 수 있으며 기본값은 <code>image/webp</code> 입니다.</p>
<h3 id="최적화-되지-않는-이미지">최적화 되지 않는 이미지</h3>
<p>동적으로 최적화를 해야 하므로 이미지 최적화가 필요 없는 SVG와 같은 vector 이미지, 그리고 GIF와 같은 상대적으로 복잡하고 최적화에 오래 걸리는 애니메이션 이미지의 경우에서는 코드 레벨에서 최적화를 진행하지 않고 바로 응답으로 내려주게 되어 있습니다.</p>
<p>GIF는이미지 최적화 과정을 건너뛰기 위해 <code>unoptimized={true}</code>를 권장합니다. </p>
<p><code>next/image</code>에서 SVG를 사용하려면 <code>unoptimized={true}</code>를 반드시 적용해야 합니다.</p>
<blockquote>
<p><em><code>next/image</code>에서 unoptimized는 <code>true</code>인 경우, 품질, 크기 또는 형식을 변경하지 않고 원본 이미지를 그대로 제공합니다.</em></p>
</blockquote>
<pre><code class="language-js">// next.config.mjs
/** @type {import(&#39;next&#39;).NextConfig} */
const nextConfig = {
  images: {
    formats: [&#39;image/avif&#39;, &#39;image/webp&#39;],
    dangerouslyAllowSVG: true, 
    contentSecurityPolicy: &quot;default-src &#39;self&#39;; script-src &#39;none&#39;; sandbox;&quot;, 
  },
}

export default nextConfig</code></pre>
<p><code>next.config</code>에서 프로젝트 전체에 적용할 수 있습니다. 대신 <code>dangerouslyAllowSVG</code> 설정이 필요하고,
<strong>XSS 공격 등 보안성 위험</strong> 때문에 <code>contentSecurityPolicy</code> 설정을 통해 보안 정책을 설정 할 수 있습니다.</p>
<h3 id="최적화는-언제-이루어지는가">최적화는 언제 이루어지는가?</h3>
<p><code>next/image</code>의 최적화는 빌드 시점이 아닌, 브라우저에서 이미지를 요청할 때 이루어집니다.</p>
<ol>
<li>HTML에서 이미지 태그 렌더링</li>
<li>브라우저가 서버에 이미지 요청</li>
<li>서버가 이미지 최적화 수행 </li>
<li>최적화된 이미지 응답</li>
</ol>
<p>과정으로 최적화가 이루어집니다.</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/2a97f8fb-4d16-4f25-a26b-2fcde8740516/image.png" alt="img"></p>
<p><code>next/image</code> 컴포넌트를 사용하면 이미지 URL을 <code>image?url=</code> 형태로 반환합니다. 이 URL로 요청이 들어오면 Next.js의 이미지 최적화 미들웨어가 동작합니다.</p>
<h3 id="sharp-라이브러리">sharp 라이브러리</h3>
<p>Next.js는 기본으로 Squoosh를 최적화 모듈로 사용하고 있습니다. Squoosh는 빠른 설치 속도와 개발 환경에서 적합합니다. </p>
<blockquote>
<p>운영 환경에서는 sharp를 사용하는 것을 <strong>매우 강력하게 권장</strong>하고 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/min_jae/post/a520f68d-402e-44f2-b43c-b872ab6e7c9c/image.png" alt="sharp"></p>
<p>Next.js는 sharp의 설치 여부를 확인하고 이후에 동작하는 로직에서 sharp 변수를 기준으로 동작하는 방식으로 코드가 작성되어 있습니다.</p>
<blockquote>
<p>Next.js 15 버전은 sharp를 기본적으로 사용하고 있습니다.</p>
</blockquote>
<hr>
<h3 id="이미지-최적화-과정">이미지 최적화 과정</h3>
<ol>
<li>avif, webp 사용하기(최신 포맷으로 변환)</li>
</ol>
<p>WebP는 Google이 개발한 이미지 포맷으로, JPEG보다 작은 파일 크기로 비슷한 품질을 제공합니다. AVIF는 더 최신의 포맷으로, WebP보다 더 나은 압축률을 제공할 수 있습니다.</p>
<ol start="2">
<li>이미지 크기 최적화</li>
</ol>
<p>Next.js는 <code>sizes</code> 속성을 사용하여 다양한 화면 크기에 맞는 이미지를 생성합니다. 이를 통해 모바일 기기에는 작은 이미지를, 데스크톱에는 큰 이미지를 제공하여 불필요한 데이터 전송을 줄입니다.</p>
<ol start="3">
<li>Lazy Loading</li>
</ol>
<p>Lazy Loading을 통해 뷰포트 밖에 있는 이미지의 로딩을 지연시켜, 초기 페이지 로드 시간을 크게 줄일 수 있습니다. 이는 특히 이미지가 많은 페이지에서 효과적입니다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/min_jae/post/28c78932-06c5-4348-97c5-cc89b0a91b75/image.png" alt="before"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/49a15380-fe36-47fa-84c8-906951ff5502/image.png" alt="after"></p>
<p>위 과정을 적절히 사용하여 이미지의 Size와 Time을 최적화가 가능합니다.</p>
<hr>
<p>✅ 참고</p>
<ul>
<li><p><a href="https://oliveyoung.tech/2023-06-09/nextjs-image-optimization/">NEXT.JS의 이미지 최적화는 어떻게 동작하는가?-올리브영테크블로그</a></p>
</li>
<li><p><a href="https://deun.dev/post/nextjs-image-optimization">NextJS는 어떻게 이미지를 최적화할까?</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Tailwind에서 스크롤바 숨기기]]></title>
            <link>https://velog.io/@min_jae/Tailwind%EC%97%90%EC%84%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EB%B0%94-%EC%88%A8%EA%B8%B0%EA%B8%B0</link>
            <guid>https://velog.io/@min_jae/Tailwind%EC%97%90%EC%84%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EB%B0%94-%EC%88%A8%EA%B8%B0%EA%B8%B0</guid>
            <pubDate>Mon, 03 Feb 2025 10:28:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min_jae/post/6e2fc078-2b1b-4ee2-b41f-819c2c4cf2d8/image.png" alt="tailwind"></p>
<p>Tailwind CSS를 사용해 스타일링을 하던 중, 스크롤바를 숨기고 싶었습니다. 하지만 Tailwind 공식 문서에서는 기본적으로 스크롤바를 숨기는 유틸리티 클래스를 제공하지 않는다는 걸 알게 되었고, 이에 대한 해결 방법을 찾던 중, <code>tailwind-scrollbar-hide</code> 플러그인을 활용하면 간단하게 해결할 수 있다는 것을 발견했습니다.</p>
<h3 id="📌-tailwind에서-기본적으로-스크롤바를-숨길-수-없을까">📌 Tailwind에서 기본적으로 스크롤바를 숨길 수 없을까?</h3>
<p>Tailwind는 다양한 스타일링 유틸리티를 제공하지만, 기본적으로 스크롤바를 숨기는 클래스를 포함하고 있지 않습니다. 따라서 직접 스타일을 추가하거나, 플러그인을 활용해야 합니다.</p>
<p>여기서는 <code>tailwind-scrollbar-hide</code> 플러그인을 사용해 스크롤바를 숨기는 방법을 소개합니다.</p>
<hr>
<h2 id="🔧-tailwind-scrollbar-hide-플러그인-설치-및-설정">🔧 tailwind-scrollbar-hide 플러그인 설치 및 설정</h2>
<h3 id="1️⃣-플러그인-설치">1️⃣ 플러그인 설치</h3>
<p>아래 명령어 중 사용하는 패키지 매니저에 맞게 설치합니다.</p>
<pre><code class="language-bash">// npm
npm install tailwind-scrollbar-hide

// yarn
yarn add tailwind-scrollbar-hide

// pnpm 
pnpm add tailwind-scrollbar-hide
</code></pre>
<h3 id="2️⃣-tailwind-설정-파일tailwindconfigts-수정">2️⃣ Tailwind 설정 파일(tailwind.config.ts) 수정</h3>
<p>설치한 플러그인을 Tailwind 설정 파일에 추가합니다.</p>
<pre><code class="language-ts">import type { Config } from &#39;tailwindcss&#39;;
import scrollbarHide from &#39;tailwind-scrollbar-hide&#39;;

export default {
  content: [
    &#39;./src/pages/**/*.{js,ts,jsx,tsx,mdx}&#39;,
    &#39;./src/components/**/*.{js,ts,jsx,tsx,mdx}&#39;,
    &#39;./src/app/**/*.{js,ts,jsx,tsx,mdx}&#39;,
  ],
  theme: {
    extend: {
      colors: {
        background: &#39;var(--background)&#39;,
        foreground: &#39;var(--foreground)&#39;,
      },
    },
  },
  plugins: [scrollbarHide],
} satisfies Config;</code></pre>
<h3 id="3️⃣-scrollbar-hide-클래스-적용">3️⃣ scrollbar-hide 클래스 적용</h3>
<p>이제 <code>scrollbar-hide</code> 클래스를 적용하면 스크롤바를 감출 수 있습니다.</p>
<pre><code class="language-tsx">&lt;div className=&quot;scrollbar-hide overflow-auto h-64&quot;&gt;
  {/* 내용 */}
&lt;/div&gt;</code></pre>
<p>위처럼 scrollbar-hide를 적용하면 해당 요소의 스크롤바가 완전히 숨겨진다.</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/4462d69d-2c3d-4296-aaa4-b0822e97d1b0/image.png" alt="before"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/7fc6e7af-d154-46f6-a34a-4ace291e14b6/image.png" alt="after"></p>
<hr>
<h2 id="추가-scrollbar-hide-플러그인-없이-구현하는-방법">(추가) scrollbar-hide 플러그인 없이 구현하는 방법</h2>
<p>만약 추가적인 플러그인을 설치하지 않고 직접 Tailwind에서 구현하고 싶다면 <code>@layer utilities</code>를 활용해 아래와 같이 직접 스타일을 추가할 수도 있습니다.</p>
<pre><code class="language-ts">@layer utilities {
  .scrollbar-hide {
    &amp;::-webkit-scrollbar {
      display: none;
    }
    scrollbar-width: none;
  }
}</code></pre>
<p>이렇게 하면 Tailwind의 <code>@apply</code> 기능을 활용하여 <code>scrollbar-hide</code> 클래스를 사용할 수 있습니다.</p>
<pre><code class="language-tsx">&lt;div className=&quot;scrollbar-hide overflow-auto h-64&quot;&gt;
  {/* 내용 */}
&lt;/div&gt;</code></pre>
<hr>
<h2 id="마무리">마무리</h2>
<p>Tailwind CSS에서는 기본적으로 스크롤바를 숨기는 유틸리티 클래스를 제공하지 않기 때문에, <code>tailwind-scrollbar-hide</code> 플러그인을 활용하거나 직접 <code>@layer utilities</code>를 이용해 스타일을 추가하는 방법을 사용할 수 있습니다.</p>
<hr>
<p>✅ 참고 </p>
<ul>
<li><p><a href="https://www.npmjs.com/package/tailwind-scrollbar-hide">npm tailwind-scrollbar-hide</a></p>
</li>
<li><p><a href="https://v3.tailwindcss.com/">tailwind v3 docs</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[{ font-size: 62.5% }]]></title>
            <link>https://velog.io/@min_jae/font-size-62.5</link>
            <guid>https://velog.io/@min_jae/font-size-62.5</guid>
            <pubDate>Thu, 23 Jan 2025 16:19:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min_jae/post/a51d8f41-8a22-4b0d-8295-18b845bd72cd/image.png" alt="절규편"></p>
<p>디자이너분들이 만들어주신 피그마를 만들고 있는 장면입니다. 여기 사진을 보면 계산기가 있는 것을 볼 수 있습니다. 계산기가 왜 있을까요?</p>
<p>px값을 rem으로 바꾸기 위해서죠. 하나씩 계산해가며 노가다를 하다가 생각이 들었습니다. 이 방법을 좀 더 쉽게 하는 방법은 없을까?</p>
<p>그래서 🐶고수 <a href="https://github.com/soheekimdev">soheekim</a>님께 바로 물어봤습니다.</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/90c174af-a735-4836-b611-c9e9fdc62a7a/image.png" alt="희망편1"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/73c20040-5ef4-49e1-b81d-d08d9546ae3c/image.png" alt="희망편2"></p>
<blockquote>
<p>font-size 를 62.5로 설정하라고 ? 그게 뭐야</p>
</blockquote>
<hr>
<h2 id="font-size-625">font-size: 62.5%</h2>
<h3 id="1-브라우저-기본-폰트-크기">1. 브라우저 기본 폰트 크기</h3>
<p>대부분의 브라우저에서 기본 폰트 크기는 16px로 설정되어 있습니다. 이 기본 크기는 html 요소의 폰트 크기로 사용됩니다. CSS에서 다른 요소에 <code>rem</code> 단위를 사용할 경우, 이 기본 폰트 크기를 기준으로 계산됩니다.</p>
<pre><code class="language-css">html {
  font-size: 16px;
}

h1 {
  font-size: 2rem; /* 16px * 2 = 32px */
}</code></pre>
<hr>
<h3 id="2-625로-폰트-크기-변경">2. 62.5%로 폰트 크기 변경</h3>
<p><code>font-size: 62.5%</code>는 기본 폰트 크기인 16px의 62.5% 값을 설정합니다.</p>
<p>계산을 해보면 <code>16px × 0.625 = 10px</code></p>
<p>즉, <code>html</code> 태그의 폰트 크기를 10px로 설정한 것과 동일한 효과를 냅니다.</p>
<p>기본 폰트 크기를 10px로 설정하면, <code>rem</code> 단위는 다음과 같이 직관적으로 변환됩니다.</p>
<ul>
<li><code>1rem = 10px</code></li>
<li><code>1.6rem = 16px</code></li>
<li><code>3.2rem = 32px</code></li>
</ul>
<hr>
<h3 id="3-625-설정">3. 62.5% 설정</h3>
<pre><code class="language-css">html {
  /* 62.5% of 16px browser font size is 10px (16px * 0.625 = 10px) */
  font-size: 62.5%;
}

body {
  /* 1.6 * 10px = 16px */
  font-size: 1.6rem; /* 16px와 동일 */
}

.tag {
  font-size: 2.4rem; /* 24px와 동일 */
}</code></pre>
<p>이 설정을 통해 개발자는 디자인 가이드라인에서 제시된 px 단위 값을 rem으로 변환하는 과정을 더 쉽게 할 수 있습니다.</p>
<blockquote>
<p>Thanks to <em>soheekim</em> 🙇🏻</p>
</blockquote>
<hr>
<p>25.01.30</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/80a868ee-a65f-4ef7-944a-a4fdf34242b8/image.png" alt="extension"></p>
<p>VSCode Extension에도 역시나 있었습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebSocket을 이용한 실시간 채팅 구현하기]]></title>
            <link>https://velog.io/@min_jae/WebSocket%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@min_jae/WebSocket%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 18 Jan 2025 06:54:07 GMT</pubDate>
            <description><![CDATA[<p>실시간 채팅은 구매자와 판매자 간의 원활한 소통을 위한 필수 기능 중 하나입니다. Next.js와 WebSocket을 활용하여 양방향 통신 기반의 실시간 채팅 애플리케이션을 구현했습니다.</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/1d569b53-4149-46d9-983f-e75af61095dd/image.gif" alt=""></p>
<h2 id="1-프로젝트-구조">1. 프로젝트 구조</h2>
<p>두 개의 주요 컴포넌트로 구성됩니다</p>
<p>1.<code>ChatList</code>: 사용자의 채팅 목록을 표시하고 관리합니다.
2.<code>ChatRoom</code>: 실제 채팅이 이루어지는 공간으로, WebSocket 연결을 관리합니다.</p>
<h2 id="2-chatlist-컴포넌트">2. ChatList 컴포넌트</h2>
<p>ChatList 컴포넌트는 사용자의 채팅 목록을 표시하고, 필터링 기능을 제공합니다.</p>
<pre><code class="language-tsx">import React, { useState, useEffect } from &#39;react&#39;;
import useAuthStore from &#39;@/stores/authStore&#39;;
import { Chat } from &#39;@/types/chat&#39;;
import { MessageSquare } from &#39;lucide-react&#39;;
import Link from &#39;next/link&#39;;

export interface ChatListProps {
  initialChats: Chat[];
  selectedChatId: number | null;
  onChatCreated?: (newChat: Chat) =&gt; void;
}

type FilterType = &#39;all&#39; | &#39;buy&#39; | &#39;sell&#39;;

const ChatList: React.FC&lt;ChatListProps&gt; = ({ initialChats, selectedChatId }) =&gt; {
  const { user } = useAuthStore();
  const myName = user?.username;
  const [filteredChats, setFilteredChats] = useState(initialChats);
  const [activeFilter, setActiveFilter] = useState&lt;FilterType&gt;(&#39;all&#39;);

  // ... (필터링 로직 및 UI 렌더링 코드)

  return (
    &lt;div className=&quot;h-full border-r border-[#d9d9d9] bg-white flex flex-col&quot;&gt;
      {/* ... (필터 버튼 및 채팅 목록 UI) */}
    &lt;/div&gt;
  );
};

export default ChatList;</code></pre>
<blockquote>
<p><a href="https://github.com/chaeuda-TEAM/oz-main-fe-06-team2/blob/main/src/components/chat/ChatList.tsx">ChatList.tsx</a></p>
</blockquote>
<h2 id="3-chatroom-컴포넌트">3. ChatRoom 컴포넌트</h2>
<p>ChatRoom 컴포넌트는 WebSocket을 사용하여 실시간 채팅 기능을 구현합니다.</p>
<pre><code class="language-tsx">import useAccessToken from &#39;@/hooks/useAccessToken&#39;;
import React, { useEffect, useRef, useState, useCallback, useLayoutEffect } from &#39;react&#39;;
import { Send } from &#39;lucide-react&#39;;
import useAuthStore from &#39;@/stores/authStore&#39;;
import { ChatRoomProps, Message } from &#39;@/types/chat&#39;;

const ChatRoom: React.FC&lt;ChatRoomProps&gt; = ({ chatId }) =&gt; {
  const socketRef = useRef&lt;WebSocket | null&gt;(null);
  const [isConnected, setIsConnected] = useState&lt;boolean&gt;(false);
  const [messages, setMessages] = useState&lt;Message[]&gt;([]);
  const [inputMessage, setInputMessage] = useState&lt;string&gt;(&#39;&#39;);

  // ... (WebSocket 연결 및 메시지 처리 로직)

  return (
    &lt;div className=&quot;flex flex-col h-full bg-white&quot;&gt;
      {/* ... (메시지 표시 및 입력 UI) */}
    &lt;/div&gt;
  );
};

export default ChatRoom;</code></pre>
<blockquote>
<p><a href="https://github.com/chaeuda-TEAM/oz-main-fe-06-team2/blob/main/src/components/chat/ChatRoom.tsx">ChatRoom.tsx</a></p>
</blockquote>
<h2 id="4-websocket-연결-관리">4. WebSocket 연결 관리</h2>
<p>ChatRoom 컴포넌트에서 WebSocket 연결을 관리하는 방법을 자세히 살펴보겠습니다.</p>
<pre><code class="language-tsx">const connect = useCallback(() =&gt; {
  if (!accessToken) return;

  socketRef.current = new WebSocket(
    `${process.env.NEXT_PUBLIC_WS_URL}/${chatId}/?token=${encodeURIComponent(accessToken)}`,
  );

  socketRef.current.onopen = () =&gt; {
    setIsConnected(true);
    // ... (연결 성공 처리)
  };

  socketRef.current.onmessage = event =&gt; {
    // ... (메시지 수신 및 처리)
  };

  socketRef.current.onerror = error =&gt; {
    console.error(&#39;WebSocket error:&#39;, error);
    setIsConnected(false);
  };

  socketRef.current.onclose = () =&gt; {
    setIsConnected(false);
    // ... (재연결 로직)
  };
}, [accessToken, chatId]);

useEffect(() =&gt; {
  connect();
  return () =&gt; {
    disconnect();
  };
}, [connect, disconnect]);</code></pre>
<h2 id="5-메시지-송수신">5. 메시지 송수신</h2>
<p>메시지 송수신은 다음과 같이 구현됩니다.</p>
<pre><code class="language-tsx">const sendMessage = () =&gt; {
  if (
    socketRef.current &amp;&amp;
    socketRef.current.readyState === WebSocket.OPEN &amp;&amp;
    inputMessage.trim()
  ) {
    const messageData = {
      message: inputMessage,
      sender: myName,
    };
    socketRef.current.send(JSON.stringify(messageData));
    setInputMessage(&#39;&#39;);
  }
};

// 메시지 수신 처리 (socketRef.current.onmessage 내부)
const newMessage: Message = {
  id: data.id?.toString() || Date.now().toString(),
  content: data.message || &#39;&#39;,
  sender: (typeof data.sender === &#39;object&#39; ? data.sender.username : data.sender) || &#39;Unknown&#39;,
  createdAt: new Date(data.created_at || Date.now()),
  chatRoom: data.chat_room_id?.toString() || chatId,
};
setMessages(prev =&gt; [...prev, newMessage]);</code></pre>
<hr>
<p>✅ 참고 </p>
<ul>
<li><a href="https://mingule.tistory.com/60">Frontend를 위한, Socket과 WebSocket_mingule</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nextjs] Next.js Middleware로 JWT Access Token 갱신하기]]></title>
            <link>https://velog.io/@min_jae/duqtyjmv</link>
            <guid>https://velog.io/@min_jae/duqtyjmv</guid>
            <pubDate>Fri, 10 Jan 2025 09:42:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min_jae/post/b0c8031c-59cc-4102-83f1-4984acbffb5c/image.svg" alt="jwt"></p>
<p><a href="https://jwt.io/">이미지출처</a></p>
<p>JWT(JSON Web Token)는 사용자 인증을 관리하기 위한 강력한 도구입니다. 하지만, JWT의 짧은 유효 기간 때문에 사용자가 인증 상태를 유지하려면 Access Token을 주기적으로 갱신해야 합니다. 이번 글에서는 Next.js의 Middleware를 활용하여 Access Token을 자동으로 갱신하는 방법을 살펴보겠습니다.</p>
<hr>
<h3 id="1-문제-정의-access-token의-유효-기간">1. 문제 정의: Access Token의 유효 기간</h3>
<p>Access Token은 클라이언트가 인증된 상태를 증명하기 위해 서버에 전송하는 JSON Web Token입니다. 보안상의 이유로 Access Token은 유효 기간(exp)을 짧게 설정하는 경우가 많습니다.</p>
<p>이로 인해 사용자는 토큰이 만료될 때마다 새로 로그인해야 하는 불편함을 겪을 수 있습니다. 이를 해결하기 위해 Refresh Token을 활용하여 Access Token을 갱신하는 방식이 일반적입니다.</p>
<p>현재 진행하는 프로젝트에서는 Access Token은 30분, Refresh Token은 7일의 유효 기간이고 토큰 갱신 코드가 완성되면 Access Token의 유효 기간을 더 줄여서 보안 강화할 계획입니다.</p>
<hr>
<h3 id="2-nextjs-middleware의-역할">2. Next.js Middleware의 역할</h3>
<p>Next.js Middleware는 서버와 클라이언트 사이의 요청을 가로채어 필요한 전처리를 수행할 수 있는 강력한 기능을 제공합니다. 이를 활용해 보호된 경로에 접근하기 전에 JWT의 유효성을 검증하고, 필요할 경우 Access Token을 갱신할 수 있습니다.</p>
<hr>
<h3 id="3-구현-middleware-코드">3. 구현: Middleware 코드</h3>
<p><img src="https://velog.velcdn.com/images/min_jae/post/3644d1d3-9004-4661-84f7-de03eda76eaf/image.svg" alt=""></p>
<pre><code class="language-ts">// middleware.ts
import { NextResponse } from &#39;next/server&#39;;
import type { NextRequest } from &#39;next/server&#39;;
import { sendRefreshTokenRequest } from &#39;@/api/auth&#39;;
import { jwtDecode } from &#39;jwt-decode&#39;;

const PROTECTED_ROUTES = [&#39;/create&#39;, &#39;/chat&#39;, &#39;/mypage&#39;];
const AUTH_ROUTES = [&#39;/auth/signIn&#39;, &#39;/auth/signUp&#39;];

export const middleware = async (req: NextRequest) =&gt; {
  const { pathname } = new URL(req.url);

  if (AUTH_ROUTES.some(route =&gt; pathname.startsWith(route))) {
    const refreshToken = req.cookies.get(&#39;refreshToken&#39;)?.value;

    if (refreshToken) {
      return NextResponse.redirect(new URL(&#39;/mypage&#39;, req.url));
    }
    return NextResponse.next();
  }

  if (!PROTECTED_ROUTES.some(route =&gt; pathname.startsWith(route))) {
    return NextResponse.next();
  }

  const accessToken = req.cookies.get(&#39;accessToken&#39;)?.value;
  const refreshToken = req.cookies.get(&#39;refreshToken&#39;)?.value;

  if (!accessToken &amp;&amp; !refreshToken) {
    return NextResponse.redirect(new URL(&#39;/auth/signIn&#39;, req.url));
  }

  try {
    if (accessToken) {
      const decodedToken: { exp: number } = jwtDecode(accessToken);
      const currentTime = Math.floor(Date.now() / 1000);

      if (decodedToken.exp &gt; currentTime) {
        return NextResponse.next();
      }
    }

    if (refreshToken) {
      const refreshResponse = await sendRefreshTokenRequest(refreshToken);

      if (refreshResponse.success &amp;&amp; refreshResponse.tokens) {
        const response = NextResponse.next();

        response.cookies.set(&#39;accessToken&#39;, refreshResponse.tokens.access, {
          httpOnly: true,
          secure: process.env.NODE_ENV === &#39;production&#39;,
          path: &#39;/&#39;,
        });
        response.cookies.set(&#39;refreshToken&#39;, refreshResponse.tokens.refresh, {
          httpOnly: true,
          secure: process.env.NODE_ENV === &#39;production&#39;,
          path: &#39;/&#39;,
        });

        return response;
      } else {
        console.error(&#39;Refresh token 갱신 실패:&#39;, refreshResponse.message);
        const response = NextResponse.redirect(new URL(&#39;/auth/signIn&#39;, req.url));
        response.cookies.delete(&#39;refreshToken&#39;);
        return response;
      }
    }

    return NextResponse.redirect(new URL(&#39;/auth/signIn&#39;, req.url));
  } catch (error) {
    console.error(&#39;Token verification error:&#39;, error);
    const response = NextResponse.redirect(new URL(&#39;/auth/signIn&#39;, req.url));
    response.cookies.delete(&#39;refreshToken&#39;);
    return response;
  }
};

export const config = {
  matcher: [&#39;/create/:path*&#39;, &#39;/chat/:path*&#39;, &#39;/mypage/:path*&#39;, &#39;/auth/signIn&#39;, &#39;/auth/signUp&#39;],
};</code></pre>
<hr>
<h3 id="4-access-token-갱신-요청">4. Access Token 갱신 요청</h3>
<p><code>sendRefreshTokenRequest</code> 함수는 서버로 Refresh Token을 전송하여 Access Token을 갱신합니다.</p>
<pre><code class="language-ts">export const sendRefreshTokenRequest = async (refresh: string): Promise&lt;RefreshResponse&gt; =&gt; {
  if (!refresh) {
    return {
      success: false,
      message: &#39;Refresh token이 없습니다.&#39;,
    };
  }

  try {
    const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/users/refresh`, {
      method: &#39;POST&#39;,
      headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;,
        Authorization: `Bearer ${refresh}`,
      },
      body: JSON.stringify({ refresh }),
      credentials: &#39;include&#39;,
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() =&gt; ({}));
      console.error(&#39;Refresh API Error:&#39;, {
        status: response.status,
        statusText: response.statusText,
        error: errorData,
      });

      return {
        success: false,
        message: `토큰 갱신 실패 (${response.status}): ${errorData.message || response.statusText}`,
      };
    }

    const data: RefreshResponse = await response.json();
    return data;
  } catch (error) {
    console.error(&#39;토큰 갱신 요청 오류:&#39;, error);
    return {
      success: false,
      message: error instanceof Error ? error.message : &#39;토큰 갱신 요청에 실패했습니다.&#39;,
    };
  }
};</code></pre>
<hr>
<h3 id="마무리">마무리</h3>
<p>Next.js의 Middleware는 클라이언트와 서버 간의 인증 로직을 관리하는 데 매우 유용합니다. 위 코드를 참고하여 여러분의 프로젝트에 Access Token 갱신 기능을 통합해 보세요. 이 과정에서 보안 및 사용자 경험을 모두 만족시키는 인증 시스템을 구현할 수 있을 것입니다.</p>
<hr>
<p>✅ 참고</p>
<ul>
<li><a href="https://velog.io/@clydehan/Next.js%EB%A1%9C-Access-Token-%EB%A7%8C%EB%A3%8C-%ED%99%95%EC%9D%B8-%EB%B0%8F-%EC%9E%AC%EB%B0%9C%EA%B8%89-%EB%B0%9B%EA%B8%B0">Next.js로 Access Token 만료 확인 및 재발급 받기_ClydeHan</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nextjs] Next.js Middleware로 인증 및 리디렉션  처리하기]]></title>
            <link>https://velog.io/@min_jae/Nextjs-Next.js-Middleware%EB%A1%9C-%EC%9D%B8%EC%A6%9D-%EB%B0%8F-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%EC%85%98-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@min_jae/Nextjs-Next.js-Middleware%EB%A1%9C-%EC%9D%B8%EC%A6%9D-%EB%B0%8F-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%EC%85%98-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 06 Jan 2025 11:59:52 GMT</pubDate>
            <description><![CDATA[<p>Next.js Middleware는 요청(Request)을 서버에서 처리하기 전에 실행되는 함수로, 인증 및 권한 관리를 포함한 다양한 작업을 수행할 수 있는 강력한 도구입니다. 이 글에서는 Next.js Middleware를 활용해 인증 상태에 따라 페이지 접근을 제어하는 방법을 다뤄보겠습니다.</p>
<h2 id="middleware란">Middleware란?</h2>
<p>Middleware는 HTTP 요청이 특정 페이지로 전달되기 전에 실행됩니다. 그런 다음, 들어오는 요청에 따라 응답을 재작성, 리디렉션, 요청 또는 응답 헤더 수정, 또는 직접 응답할 수 있습니다.</p>
<h3 id="주요-특징">주요 특징</h3>
<ul>
<li>Edge 실행: Middleware는 Edge Runtime에서 실행되며, 빠르고 서버 리소스를 적게 사용합니다.</li>
<li>유연성: 요청을 수정하거나 리다이렉션, 응답을 차단할 수 있습니다.</li>
<li>코드 실행: 클라이언트 요청이 서버에 도달하기 전에 코드를 실행할 수 있습니다.</li>
</ul>
<hr>
<h2 id="인증-상태에-따른-페이지-접근-제어">인증 상태에 따른 페이지 접근 제어</h2>
<ol>
<li>로그인하지 않은 사용자가 게시물 작성, 채팅, 마이페이지와 같은 페이지에 접근하면 로그인 페이지로 리디렉션</li>
<li>로그인한 사용자가 로그인 또는 회원가입 페이지에 접근하면 마이페이지로 리디렉션</li>
</ol>
<h3 id="middleware-작성하기">Middleware 작성하기</h3>
<pre><code class="language-ts">// middleware.ts
import { NextResponse } from &#39;next/server&#39;;
import type { NextRequest } from &#39;next/server&#39;;

export const middleware = (req: NextRequest) =&gt; {
  const refreshToken = req.cookies.get(&#39;refreshToken&#39;);

  if (
    !refreshToken &amp;&amp;
    (req.url.includes(&#39;/create&#39;) || req.url.includes(&#39;/chat&#39;) || req.url.includes(&#39;/mypage&#39;))
  ) {
    return NextResponse.redirect(new URL(&#39;/auth/signIn&#39;, req.url));
  }

  if (refreshToken &amp;&amp; (req.url.includes(&#39;/auth/signIn&#39;) || req.url.includes(&#39;/auth/signUp&#39;))) {
    return NextResponse.redirect(new URL(&#39;/mypage&#39;, req.url));
  }

  return NextResponse.next();
};

export const config = {
  matcher: [&#39;/create/:path*&#39;, &#39;/chat/:path*&#39;, &#39;/mypage/:path*&#39;, &#39;/auth/signIn&#39;, &#39;/auth/signUp&#39;],
};</code></pre>
<h4 id="matcher">Matcher</h4>
<p><code>matcher</code>를 사용하여 특정 경로에서만 Middleware를 실행하도록 필터링 할 수 있습니다.</p>
<p>단일 경로나 여러 경로를 배열 구문으로 매칭할 수 있습니다.</p>
<ul>
<li><code>matcher: &#39;/create/:path*&#39;</code></li>
<li><code>mathcer: [&#39;/create/:path*&#39;, &#39;/chat/:path*&#39;]</code></li>
</ul>
<blockquote>
<p>❗️ middleware는 페이지와 동일한 수준(루트 또는 src 디렉터리에)에 위치해야 합니다.</p>
</blockquote>
<h3 id="클라이언트-코드-간소화">클라이언트 코드 간소화</h3>
<p>이제 클라이언트에서 조건부 라우팅을 제거할 수 있습니다.</p>
<pre><code class="language-tsx">// 기존 코드 예시
const { isAuthenticated } = useAuthStore();

const handleCreateClick = () =&gt; {
    if (isAuthenticated) {
      router.push(&#39;/create&#39;);
    } else {
      router.push(&#39;/auth/signIn&#39;);
    }
  };</code></pre>
<pre><code class="language-tsx">// 수정 코드 예시
  const handleCreateClick = () =&gt; {
    router.push(&#39;/create&#39;);
  };</code></pre>
<p>이제 router.push는 페이지 이동만 처리하며, 접근 제어는 Middleware에서 수행됩니다.</p>
<hr>
<h2 id="결론">결론</h2>
<p>Next.js Middleware는 인증 및 권한 관리를 효율적으로 처리할 수 있는 강력한 도구입니다. 클라이언트와 서버 간 역할을 명확히 분리하여 코드를 간소화하고 보안을 강화할 수 있습니다.</p>
<hr>
<p>✅ 참고</p>
<ul>
<li><a href="https://nextjs.org/docs/app/building-your-application/routing/middleware">NEXT.JS_docs</a></li>
<li><a href="https://velog.io/@hwisaac/NextJS-Middleware">NextJS: Middleware_hwisaac</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[심화록] 심화록 프로젝트 회고]]></title>
            <link>https://velog.io/@min_jae/%EC%8B%AC%ED%99%94%EB%A1%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-%EB%82%A0-%EC%9E%8A%EC%A7%80%EB%A7%88</link>
            <guid>https://velog.io/@min_jae/%EC%8B%AC%ED%99%94%EB%A1%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-%EB%82%A0-%EC%9E%8A%EC%A7%80%EB%A7%88</guid>
            <pubDate>Wed, 01 Jan 2025 18:59:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min_jae/post/0d8abad1-cfe4-41bc-bd6d-795a9a1fe72a/image.png" alt=""></p>
<p>2024년 10월부터 시작된 팀 <a href="https://github.com/Ju-MINJAE/communiT">프로젝트</a>가 12월 17일부로 마무리되었습니다. 벌써 약 2주가 흘렀지만, 팀원들과 함께 즐겁게 프로젝트를 진행했던 기억은 여전히 생생합니다. 이 글은 그 시간을 되돌아보며 남기는 작은 기록입니다. <del>(연말에 바빠서 이제야 쓰는 건 절대 아닙니다...)</del></p>
<hr>
<h2 id="프로젝트-준비">프로젝트 준비</h2>
<p>우리 팀은 총 5명으로 구성되어 두 팀으로 나뉘어 진행하기로 했습니다. 팀 구성은 랜덤이었고, 제가 속한 팀은 세 명이 함께 작업하게 되었습니다.</p>
<p>팀원들의 배경은 다양했는데, 리액트를 처음 사용하는 분도 있었고, 팀 프로젝트 경험이 없는 분도 있었습니다. 걱정이 될 수도 있었지만, 그런 감정보다는 기대감이 더 컸습니다. 현업에서 일하신 분, 전 퍼블리셔 초고수, 컴퓨터공학 전공자, 그리고 영어 강사 출신의 팀원까지. 모두 대단한 이력을 가진 분들이었고, 지금 돌아보면 독학으로 공부해온 제가 가장 초라해 보였을 정도였습니다.</p>
<p>우여곡절 끝에 기술 스택을 정하고, 가장 어려웠던(?) 팀명도 정했습니다. 우리 팀은 모두 MBTI가 T여서 <code>커뮤니T</code>라는 이름으로 결정했는데, 이 이름만큼이나 논리적인 팀워크를 보여주고 싶다는 다짐을 했습니다.</p>
<h2 id="프로젝트-시작">프로젝트 시작</h2>
<p>프로젝트가 시작되면서 저는 팀장이 되었습니다. 이유는 모르겠지만, 두 팀원이 저를 지목했죠.</p>
<blockquote>
<p>특히, 팀원 중 한 분이 피그마에 능숙하셨는데, 혼자서 프로젝트 기획을 피그마로 정리해 주셨습니다. 덕분에 초기 작업을 매우 수월하게 진행할 수 있었습니다. 피그마를 활용한 디자인 프로세스를 직접 경험하면서, 사용자 경험(UX)에 대한 이해를 넓히고 피그마 사용법까지 자연스럽게 배울 수 있었던 좋은 기회였습니다.</p>
</blockquote>
<p>저는 팀원들에게 하고 싶은 테스크를 먼저 맡겼고, 제 업무도 차근차근 진행하기 시작했습니다. 초반에는 모든 것이 낯설고 어려웠지만, 함께 공부하고 자료를 공유하면서 문제를 하나씩 해결해 나갔습니다. 이 과정에서 팀원들과의 <strong>커뮤니케이션의 중요성</strong>을 깊이 느꼈습니다. 시간에 상관없이 각자의 의견을 자유롭게 말하고, 다른 팀원의 의견을 묻는 방식으로 소통하면서 문제를 효율적으로 해결할 수 있었습니다.</p>
<p>또한, GPT와 Claude 같은 AI 도구들의 도움을 많이 받았는데, 덕분에 기술적 난관도 비교적 수월하게 넘어갈 수 있었습니다. 이번 프로젝트를 통해 AI의 위대함을 다시 한 번 체감하는 동시에, 효과적인 팀 커뮤니케이션이 프로젝트 성공의 핵심이라는 것을 깨달았습니다.</p>
<hr>
<h2 id="연말이라는-고비">연말이라는 고비</h2>
<p>프로젝트의 마감일은 12월 17일로 정해져 있었습니다. 11월까지는 순조롭게 진행되던 작업이 12월에 접어들면서 예기치 못한 문제들에 부딪히기 시작했습니다. AWS Jam 참여와 연말 바쁜 일정이 겹치면서 모든 팀원들의 프로젝트 참여가 어려워졌습니다. 결국 기획했던 기능 중 일부, 예를 들어 채팅과 알람 기능을 제외하기로 했습니다.</p>
<p>12월 초부터는 진행 속도가 현저히 느려졌습니다. 사실 느려진 것이 아니라 거의 멈췄다고 해도 과언이 아니었습니다. 어느 정도 만들어진 결과물로 마무리할 수도 있었지만, 저는 프로젝트를 끝까지 완성하고 싶었습니다. 다른 두 팀원은 동시에 진행 중인 다른 프로젝트가 있었기 때문에 제가 주도적으로 마무리하겠다고 나섰습니다.</p>
<p>하지만 놀랍게도, 두 팀원 모두 <code>커뮤니T</code> 프로젝트에 다시 적극적으로 참여하기 시작했습니다. 덕분에 우리는 막판 스퍼트를 내며 프로젝트를 마무리할 수 있었습니다. 서로가 서로를 격려하며 끝까지 함께할 수 있었다는 점에서 이번 프로젝트는 정말 의미 있는 경험이었습니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 프로젝트는 단순히 결과물을 만드는 것을 넘어, 협업과 성장의 소중함을 느끼게 해준 시간이었습니다. <code>Redux</code>, <code>Axios</code> 등 다양한 라이브러리를 사용해보는 경험도 얻었고 각자의 상황이 다르고 환경이 어려웠음에도 불구하고 끝까지 함께 노력했던 팀원들에게 감사함을 전하고 싶습니다.<code>커뮤니T</code>라는 이름처럼, 앞으로도 논리적이고 끈끈한 협업의 가치를 이어가고 싶습니다.</p>
<blockquote>
<p>많은 것을 배우게 도와준 <a href="https://github.com/Kwonyeojiny">여진</a>, <a href="https://github.com/soheekimdev">소희</a>, <a href="https://github.com/seyoonagain">세윤</a>, <a href="https://github.com/wdohoon">도훈</a> 모두 감사합니다 🙇🏻</p>
</blockquote>
<blockquote>
<p>백엔드 작업과 우리를 키워주신 <a href="https://github.com/orlein">창준_멘토님</a>.. 압도적 감사합니다..</p>
</blockquote>
<blockquote>
<p><a href="https://github.com/Ju-MINJAE/communiT">라이브 서버는 끝났지만 깃허브 코드는 영원하다</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/min_jae/post/54213bed-33c9-4d70-b43a-d2477cce7f33/image.GIF" alt="커뮤니티"></p>
<hr>
<p>마지막 사진은 같이 했던 자리와 도서관에서 UX/UI 책 읽는 도훈님..</p>
<div style="text-align:center;">
  <img src="https://velog.velcdn.com/images/min_jae/post/3d5f922b-a155-4325-8915-992810e69b32/image.jpeg" width="200"/>
  <img src="https://velog.velcdn.com/images/min_jae/post/1c079d5f-272a-40d8-8eb4-dfb860dde6ef/image.jpeg" width="200"/>
</div>]]></description>
        </item>
        <item>
            <title><![CDATA[[Zustand] Next.js와 Zustand로 전역 상태 관리하기]]></title>
            <link>https://velog.io/@min_jae/Zustand-Zustand%EB%A1%9C-auth-store-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@min_jae/Zustand-Zustand%EB%A1%9C-auth-store-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 30 Dec 2024 16:19:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min_jae/post/f0ef651d-8a1a-461f-99eb-6b7328d83057/image.jpg" alt="zustand">
<a href="https://github.com/pmndrs/zustand">이미지 출저: zustand github</a></p>
<p>Zustand는 React 애플리케이션에서 상태 관리를 간단하고 직관적으로 할 수 있도록 도와주는 상태 관리 라이브러리입니다. Next.js와 함께 사용할 때, Zustand는 전역 상태를 효과적으로 관리할 수 있는 훌륭한 선택이 됩니다. 이 블로그 글에서는 Next.js에서 Zustand를 사용하여 전역 상태를 관리하는 방법을 안내하겠습니다.</p>
<h2 id="1-zustand-설치">1. Zustand 설치</h2>
<p>먼저 Zustand를 프로젝트에 설치합니다. 프로젝트에서 사용하는 패키지 매니저에 따라 설치 방법이 다릅니다. 아래 명령어 중 하나를 실행하세요.</p>
<pre><code class="language-bash">// npm
npm install zustand
// yarn
yarn add zustand
// pnpm
pnpm add zustand</code></pre>
<h2 id="2-zustand-store-생성">2. Zustand Store 생성</h2>
<p>Zustand를 사용하려면 상태를 관리할 store를 만들어야 합니다. 저는 <code>authStore.ts</code>라는 파일을 만들었습니다.</p>
<pre><code class="language-tsx">// src/stores/authStore.ts
import { create } from &#39;zustand&#39;;
import { persist } from &#39;zustand/middleware&#39;;
import { Tokens, User } from &#39;@/types/types&#39;;

type AuthState = {
  user: User | null;
  isAuthenticated: boolean;
  token: Tokens | null;
  login: (userData: User, token: Tokens) =&gt; void;
  logout: () =&gt; void;
};

const useAuthStore = create&lt;AuthState&gt;()(
  persist(
    set =&gt; ({
      user: null,
      isAuthenticated: false,
      token: null,
      login: (userData, token) =&gt;
        set({
          user: userData,
          isAuthenticated: true,
          token,
        }),
      logout: () =&gt;
        set({
          user: null,
          isAuthenticated: false,
          token: null,
        }),
    }),
    {
      name: &#39;auth-storage&#39;,
    },
  ),
);

export default useAuthStore;</code></pre>
<p>위 코드에서는 <code>authStore.ts</code>라는 파일을 생성하고, 사용자 인증 정보를 관리하는 상태를 저장합니다. persist 미들웨어를 사용하여 로컬 스토리지에 상태를 유지할 수 있습니다. user, isAuthenticated, token 상태와 login, logout 함수를 정의하여 사용자 로그인/로그아웃 기능을 구현합니다.</p>
<h2 id="3-zustand-store-사용하기">3. Zustand Store 사용하기</h2>
<p><code>useAuthStore</code> 훅을 통해 이 상태를 전역적으로 접근할 수 있습니다. 예를 들어, 네비게이션 바에서 사용자의 로그인 상태를 확인하고, 로그아웃 버튼을 구현하는 방식입니다.</p>
<pre><code class="language-tsx">import useAuthStore from &#39;@/stores/authStore&#39;;

const Nav = () =&gt; {
  const { isAuthenticated, logout } = useAuthStore();

  return (
    &lt;nav&gt;
      {isAuthenticated ? (
        &lt;&gt;
          &lt;span&gt;Welcome, User!&lt;/span&gt;
          &lt;button onClick={logout}&gt;Logout&lt;/button&gt;
        &lt;/&gt;
      ) : (
        &lt;span&gt;Please log in&lt;/span&gt;
      )}
    &lt;/nav&gt;
  );
};</code></pre>
<p>이 예제에서는 isAuthenticated를 통해 사용자가 로그인한 상태인지 확인하고, logout 함수를 호출하여 로그아웃 기능을 제공합니다.</p>
<hr>
<p>Zustand는 간단한 상태 관리에 적합하지만, 상태를 보다 세부적으로 나누어 관리할 수 있습니다. 예를 들어, <code>authStore</code> 외에도 다른 기능을 담당하는 상태를 별도로 만들 수 있습니다. <code>persist</code> 미들웨어를 활용하여 상태를 브라우저에 저장하고, 페이지 새로 고침 시에도 상태를 유지할 수 있습니다.</p>
<p>Zustand는 Next.js 프로젝트에서 상태 관리를 간단하고 효율적으로 처리할 수 있는 훌륭한 도구입니다. 이 글에서는 Zustand를 사용하여 로그인 상태를 관리하는 방법을 살펴봤습니다. Zustand를 통해 전역 상태를 쉽게 관리하고, 페이지 간에 상태를 공유하는 방식으로 애플리케이션의 일관성을 유지할 수 있습니다.</p>
<hr>
<p>✅ 참고</p>
<ul>
<li><a href="https://github.com/pmndrs/zustand">zustand</a></li>
<li><a href="https://usage.tistory.com/210">Zustand로 전역 상태 관리하기_유세지</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 프로젝트를 Vercel에 배포하고 Route53으로 도메인 연결하기]]></title>
            <link>https://velog.io/@min_jae/Next.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-Vercel%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B3%A0-Route53%EC%9C%BC%EB%A1%9C-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@min_jae/Next.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-Vercel%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B3%A0-Route53%EC%9C%BC%EB%A1%9C-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 26 Dec 2024 09:28:07 GMT</pubDate>
            <description><![CDATA[<p>Next.js 프로젝트를 Vercel에 배포한 후, AWS Route53을 이용해 구매한 도메인을 연결하는 방법을 안내합니다.
저는 <a href="https://www.gabia.com/">가비아</a>에서 <code>.shop</code> 도메인을 구매했습니다.</p>
<hr>
<h2 id="1-vercel에서-도메인-추가하기">1. Vercel에서 도메인 추가하기</h2>
<p><img src="https://velog.velcdn.com/images/min_jae/post/9448716a-abdf-45d4-9847-f20b62b34acb/image.png" alt="vercel_settings_domain"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/0d1327b2-6a88-4ed7-9ee0-867ee60a579a/image.png" alt="vercel_settings_domain_a_cname"></p>
<ol>
<li>Vercel에서 프로젝트를 배포한 후, 프로젝트 <strong>Settings - Domain</strong> 메뉴로 이동합니다.</li>
<li>연결할 도메인을 입력하고 추가합니다.</li>
<li>추가 후 오류 메세지가 나타납니다. 이때 Vercel에서 제공하는 <strong>A 레코드</strong>와 <strong>CNAME</strong> 값을 확인하고 복사합니다.</li>
</ol>
<h3 id="a-레코드와-cname-레코드의-차이와-사용-시점">A 레코드와 CNAME 레코드의 차이와 사용 시점</h3>
<h4 id="1-a-레코드-address-record">1. A 레코드 (Address Record)</h4>
<p>A 레코드는 도메인 이름을 고정된 IPv4 주소와 연결할 때 사용합니다.</p>
<ul>
<li><p>사용 사례</p>
<ul>
<li>루트 도메인(예: example.com)을 서버의 IP 주소와 연결할 때</li>
<li>특정 호스트 이름이 특정 IP를 가리키도록 할 때</li>
</ul>
</li>
<li><p>장점</p>
<ul>
<li>직접 IP 주소를 지정해 빠른 연결이 가능</li>
<li>모든 트래픽이 지정된 IP 주소로 이동</li>
</ul>
</li>
<li><p>제한사항</p>
<ul>
<li>서버 IP가 변경되면 A 레코드도 반드시 업데이트해야 함</li>
</ul>
</li>
</ul>
<h4 id="2-cname-레코드-canonical-name-record">2. CNAME 레코드 (Canonical Name Record)</h4>
<p>CNAME 레코드는 도메인을 다른 도메인 이름(별칭)과 연결할 때 사용합니다</p>
<ul>
<li><p>사용 사례</p>
<ul>
<li>서브도메인(예: <a href="http://www.example.com)%EC%9D%84">www.example.com)을</a> 루트 도메인(예: example.com)이나 다른 도메인으로 리디렉션할 때</li>
<li>콘텐츠 배포 네트워크(CDN), 클라우드 서비스(Vercel, Netlify) 등의 동적 서비스 주소와 연결할 때</li>
</ul>
</li>
<li><p>장점</p>
<ul>
<li>별칭 주소가 변경되어도 대상 도메인만 업데이트하면 됨</li>
<li>관리가 간편하고 유연성 제공</li>
</ul>
</li>
<li><p>제한사항</p>
<ul>
<li>루트 도메인에는 사용 불가 (RFC 규약)</li>
<li>도메인 연결 속도가 간접적으로 느려질 수 있음</li>
</ul>
</li>
</ul>
<hr>
<h2 id="route53에서-레코드-설정하기">Route53에서 레코드 설정하기</h2>
<p><img src="https://velog.velcdn.com/images/min_jae/post/181456aa-fdee-4540-a7f8-7e48139c618e/image.png" alt="a레코드"></p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/af3b9fb4-b2f5-49fb-8199-24fe4e20f4c6/image.png" alt="cname"></p>
<ol>
<li><p>AWS Route53 콘솔로 이동해, 도메인을 선택하거나 추가합니다.</p>
</li>
<li><p>호스팅 영역 세부 정보에서 <strong>레코드 생성</strong>을 클릭합니다.</p>
<ul>
<li>A 레코드<ul>
<li>레코드 이름: 공백</li>
<li>값: Vercel에서 제공한 A 레코드 값</li>
</ul>
</li>
<li>CNAME<ul>
<li>레코드 이름: <code>www</code></li>
<li>값: Vercel에서 제공한 CNAME 값</li>
</ul>
</li>
</ul>
</li>
<li><p>저장 후 변경 사항이 적용될 때까지 시간이 소요됩니다.</p>
</li>
</ol>
<hr>
<h2 id="3-연결-확인하기">3. 연결 확인하기</h2>
<p>설정이 완료되면 브라우저에서 도메인을 입력해 연결 여부를 확인합니다.
<code>example.com</code>과 <code>www.example.com</code> 모두 확인합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nextjs] Next.js 프로젝트에 네이버 지도 API 적용하기]]></title>
            <link>https://velog.io/@min_jae/Nextjs-Nextjs%EC%97%90%EC%84%9C-Naver-Map%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@min_jae/Nextjs-Nextjs%EC%97%90%EC%84%9C-Naver-Map%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 23 Dec 2024 15:59:34 GMT</pubDate>
            <description><![CDATA[<h2 id="api-키-발급받기">API 키 발급받기</h2>
<p><a href="https://www.ncloud.com/">네이버 클라우드 플랫폼</a>에 가입하고 <a href="https://www.ncloud.com/product/applicationService/maps">네이버 지도 API</a> 이용 신청을 합니다.</p>
<p>애플리케이션을 등록하면 API key가 발급됩니다. 이 key는 나중에 다시 확인할 수 있으니 복사를 못했다고 걱정하지 않으셔도 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/14e9614e-5cd6-4b02-ab26-9362896d7892/image.png" alt="api_key"></p>
<p>API 무료 사용 횟수가 제한이 있어서 저는 우선 제한 한도를 무료 사용 횟수에 맞춰놨습니다.</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/a27960e8-9ee0-45c6-a42f-336ce97f9313/image.png" alt="console"></p>
<hr>
<h2 id="프로젝트-설정">프로젝트 설정</h2>
<p>네이버 지도 API의 타입을 명시적으로 선언하기 위해 @types/navermaps 설치합니다.</p>
<pre><code class="language-bash">pnpm add -D @types/navermaps</code></pre>
<hr>
<h2 id="지도-표시">지도 표시</h2>
<p>저는 메인 화면, 즉 홈에서 map을 띄우기 위해 <code>src/app/page.tsx</code>에서 작업했습니다.</p>
<pre><code class="language-tsx">declare global {
  interface Window {
    naver: any;
  }
}</code></pre>
<p>네이버 지도 API가 <code>window.naver</code> 객체에 정의되므로 타입스크립트에서 이를 알 수 있도록 명시적으로 선언합니다. 타입스크립트가 <code>window.naver</code>의 속성을 추론하지 못하는 문제를 방지하도록 <code>any</code> 타입으로 설정합니다.</p>
<pre><code class="language-tsx">interface NaverMapProps {
  width?: string;
  height?: string;
  initialCenter?: { lat: number; lng: number };
  initialZoom?: number;
}</code></pre>
<p>지도 컨테이너의 크기, 지도의 초기 중심 좌표와 초기 줌 레벨을 설정하는 props 타입을 정의합니다.</p>
<pre><code class="language-tsx">const Home = ({
  width = &#39;100%&#39;,
  height = &#39;700px&#39;,
  initialCenter = { lat: 37.5656, lng: 126.9769 },
  initialZoom = 13,
}: NaverMapProps) =&gt; {</code></pre>
<p>기본값을 설정하여 사용자 입력이 없을 경우에도 동작하도록 합니다.</p>
<pre><code class="language-tsx">  const mapRef = useRef&lt;HTMLDivElement&gt;(null);</code></pre>
<p>지도 객체가 렌더링될 DOM 요소를 참조합니다.</p>
<pre><code class="language-tsx">  useEffect(() =&gt; {
    const initializeMap = () =&gt; {
      if (!window.naver) return;

      const mapOptions = {
        center: new window.naver.maps.LatLng(initialCenter.lat, initialCenter.lng),
        zoom: initialZoom,
        zoomControl: true,
        zoomControlOptions: {
          position: window.naver.maps.Position.TOP_RIGHT,
        },
      };
    };

    const script = document.createElement(&#39;script&#39;);
    script.src = `https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=${process.env.NEXT_PUBLIC_NAVER_MAP_CLIENT_ID}`;
    script.async = true;
    script.onload = initializeMap;
    document.head.appendChild(script);

    return () =&gt; {
      document.head.removeChild(script);
    };
  }, [initialCenter, initialZoom]);</code></pre>
<ul>
<li><p>스크립트 추가: 네이버 지도 API를 로드하기 위해 동적으로 <script> 태그를 추가합니다.</p>
<ul>
<li><code>src</code>: 네이버 지도 API URL과 클라이언트 ID를 포함합니다.</li>
<li><code>async</code>: 비동기로 스크립트를 로드하여 렌더링 차단을 방지합니다.</li>
<li><code>onload</code>: 스크립트 로드가 완료된 후 initializeMap을 호출합니다.</li>
</ul>
</li>
<li><p>initializeMap</p>
<ul>
<li><code>window.naver</code>가 준비되었는지 확인 후 지도를 초기화합니다.</li>
<li><code>mapOptions</code>는 지도 초기 설정(중심, 줌 레벨, 줌 컨트롤 위치 등)을 지정합니다.</li>
<li><code>new window.naver.maps.Map</code>은 네이버 지도 객체를 생성합니다.</li>
</ul>
</li>
<li><p>정리(cleanup)</p>
<ul>
<li>컴포넌트가 언마운트될 때 동적으로 추가된 스크립트를 제거하여 메모리 누수를 방지합니다.</li>
</ul>
</li>
</ul>
<pre><code class="language-tsx">return &lt;div ref={mapRef} style={{ width, height }} /&gt;;</code></pre>
<p><img src="https://velog.velcdn.com/images/min_jae/post/d598ee1c-7ff3-4e2c-aa72-fd1d7cfd658f/image.png" alt="map"></p>
<hr>
<h2 id="특정-위치에-marker-표시하기">특정 위치에 Marker 표시하기</h2>
<pre><code class="language-tsx">const map = new window.naver.maps.Map(mapRef.current, mapOptions);

new window.naver.maps.Marker({
  position: new window.naver.maps.LatLng(initialCenter.lat, initialCenter.lng),
  map: map,
  title: &#39;Marker Test&#39;,
  });</code></pre>
<p>위 코드를 <code>useEffect</code> 안에 작성하면 Marker가 나타납니다. 저는 지도 초기 중심 좌표에 찍었습니다.</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/2769b72b-79dd-4fce-b5a0-662c96df9516/image.png" alt="marker"></p>
<hr>
<p>  ✅ 참고</p>
<ul>
<li><a href="https://velog.io/@osohyun0224/Next.js-14%EB%A1%9C-%EB%84%A4%EC%9D%B4%EB%B2%84-%EC%A7%80%EB%8F%84-API%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%A7%80%EB%8F%84-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0">Next.js 14로 네이버 지도 API를 이용해 지도 기능 개발하기_Sohyun0</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Github] Webhook으로 변경내용을 디스코드로 받아보자]]></title>
            <link>https://velog.io/@min_jae/Github-Webhook%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD%EB%82%B4%EC%9A%A9%EC%9D%84-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B0%9B%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@min_jae/Github-Webhook%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD%EB%82%B4%EC%9A%A9%EC%9D%84-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B0%9B%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 22 Dec 2024 08:56:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min_jae/post/143d7dc3-ca35-4119-b4c7-cdfd743c3443/image.jpg" alt=""></p>
<p>깃허브와 디스코드 웹훅을 사용하면, 깃허브에서 발생하는 이벤트에 대해 디스코드 채널에서 알림을 받을 수 있습니다. 팀 프로젝트를 진행하게 되며 팀원들끼리 작업 내용과 커밋 상황을 디스코드로 공유할 수 있으므로 유용하게 사용이 가능합니다. </p>
<p>깃허브와 디스코드 웹훅을 연동하는 방법에 대해 포스팅하겠습니다.</p>
<hr>
<h2 id="디스코드-웹훅-생성">디스코드 웹훅 생성</h2>
<h3 id="1-디스코드-채널-설정">1. 디스코드 채널 설정</h3>
<p><img src="https://velog.velcdn.com/images/min_jae/post/c05384e2-b1cb-4748-9da5-d18cacd25bf5/image.png" alt="discord_1"></p>
<ul>
<li>웹훅 알람을 받을 디스코드 채널에서 설정을 엽니다.</li>
<li>연동 → 웹훅 만들기를 선택합니다.</li>
</ul>
<h3 id="2-웹훅-설정">2. 웹훅 설정</h3>
<p><img src="https://velog.velcdn.com/images/min_jae/post/d037f40c-7406-439b-906b-d53c0e7752a6/image.png" alt="discord_2"></p>
<ul>
<li>웹훅의 이름과 알림을 보낼 채널을 설정합니다.</li>
<li>생성 후 웹훅 URL을 복사합니다.</li>
</ul>
<hr>
<h2 id="깃허브-웹훅-설정">깃허브 웹훅 설정</h2>
<h3 id="1-깃허브-레포지토리-설정">1. 깃허브 레포지토리 설정</h3>
<p><img src="https://velog.velcdn.com/images/min_jae/post/d194d0c2-59e7-4c30-8e69-c23471355e03/image.png" alt="github_1"></p>
<ul>
<li>알림을 설정할 레포지토리로 이동하여 Setting → Webhooks → Add webhoos 을 클릭합니다.</li>
</ul>
<h3 id="2-웹훅-정보-입력">2. 웹훅 정보 입력</h3>
<p><img src="https://velog.velcdn.com/images/min_jae/post/75f1ad92-a450-416d-a298-49010e57f7a7/image.png" alt="github_2"></p>
<ul>
<li>Payload URL: 복사한 디스코드 웹훅 URL을 붙여넣고 마지막에 /github를 추가합니다.</li>
<li>Content type: application/json 선택합니다.</li>
<li>Events to trigger: 모든 알림을 원할 경우 Send me everything 선택합니다.</li>
</ul>
<blockquote>
<p>이렇게 하면 디스코드와 깃허브 웹훅 연동이 됩니다. 참 쉽죠?</p>
</blockquote>
<h3 id="디스코드-알림">디스코드 알림</h3>
<p><img src="https://velog.velcdn.com/images/min_jae/post/fc32a6cb-7052-4b47-b1df-1ae5819a43ae/image.png" alt="result"></p>
<p>연동을 하고 커밋 메시지를 &quot;hello webhooks&quot;로 하여 테스트를 해보니 디스코드에 알람이 오는 것을 확인할 수 있습니다.</p>
<p>이제 팀 프로젝트에서 실시간으로 작업 상황을 공유할 수 있습니다!</p>
<hr>
<p>✅ 참고</p>
<ul>
<li><a href="https://velog.io/@thyoondev/Github%EA%B3%BC-Discord-Webhook-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0">Github과 Discord Webhook 연동하기_taeheeyoon</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nextjs] Next.js에서 fetch 활용 가이드: 클라이언트와 서버의 데이터 처리]]></title>
            <link>https://velog.io/@min_jae/Nextjs-Next.js%EC%97%90%EC%84%9C-fetch-%ED%99%9C%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%EC%99%80-%EC%84%9C%EB%B2%84%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@min_jae/Nextjs-Next.js%EC%97%90%EC%84%9C-fetch-%ED%99%9C%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%EC%99%80-%EC%84%9C%EB%B2%84%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Sat, 21 Dec 2024 05:01:21 GMT</pubDate>
            <description><![CDATA[<p>Next.js에서 <code>fetch</code> 함수는 데이터를 클라이언트나 서버에서 가져오기 위해 사용됩니다. Next.js는 React 기반의 프레임워크로, 서버사이드 렌더링(SSR)과 정적 사이트 생성(SSG)을 지원합니다. 이 특성에 따라 <code>fetch</code>는 클라이언트와 서버 양쪽에서 사용할 수 있지만, 각 환경에서 동작 방식이 다릅니다.</p>
<hr>
<h2 id="1-클라이언트-fetch">1. 클라이언트 fetch</h2>
<p>클라이언트에서는 브라우저의 기본 <code>fetch</code>  API를 활용하여 데이터를 가져옵니다. 이는 React 컴포넌트의 <code>useEffect</code>나 이벤트 핸들러 내부에서 자주 사용됩ㄴ다.</p>
<pre><code class="language-jsx">import { useEffect, useState } from &#39;react&#39;; 

export default function ClientFetch() {
  const [data, setData] = useState(null);

  useEffect(() =&gt; {
    fetch(&#39;/...&#39;) // api url
      .then((res) =&gt; res.json())
      .then((data) =&gt; setData(data));
}, []);

  return &lt;div&gt;{data ? JSON.stringify(data) : &#39;Loading...&#39;}&lt;/div&gt;</code></pre>
<ul>
<li>클라이언트에서 실행되며, 브라우저의 네트워크 요청을 사용합니다.</li>
<li>API 호출 시 CORS 정책을 준수해야 합니다.</li>
<li>사용자와 상호작용 후 데이터를 동적으로 가져올 때 유용합니다.</li>
</ul>
<hr>
<h2 id="2-서버-fetch">2. 서버 fetch</h2>
<p>Next.js의 서버 환경에서는 Node.js의 네이티브 환경에서 <code>fetch</code>가 동작합니다. 서버 컴포넌트나 API 라우트에서 사용할 수 있으며, 특히 SSR 및 SSG에서 많이 활용됩니다.</p>
<pre><code class="language-jsx">export async function getServerSideProps() {
  const res await fetch(&#39;...&#39;); 
  const data = await res.json();

  return { props: { data } };
}

export default function ServerFetch ({ data }) {
  return &lt;div&gt;{JSON.stringify(data)}&lt;/div&gt;;
}</code></pre>
<ul>
<li>서버에서 실행되며 클라이언트에 노출되지 않습니다.</li>
<li>SSG(정적 생성)에서는 빌드 시 데이터를 가져오고, SSR에서는 요청마다 데이터를 가져옵니다.</li>
<li>백엔드 API 호출에 유리하며 CORS 이슈를 피할 수 있습니다.</li>
</ul>
<hr>
<h2 id="3-nextjs와-fetch의-주요-활용-방식">3. Next.js와 fetch의 주요 활용 방식</h2>
<ul>
<li>getServerSideProps: 요청마다 데이터를 서버에서 가져와 렌더링합니다.</li>
<li>getStaticProps: 정적 빌드 시 데이터를 미리 가져옵니다.</li>
<li>API Routes: Neext.js의 <code>/api</code> 경로를 통해 클라이언트와 서버 간 데이터를 주고받습니다.</li>
</ul>
<pre><code class="language-jsx">export async function getStaticProps() {
  const res = await fetch(&#39;/...&#39;);
  const data = await res.json();

  return { props: { data } };
}

export default function StaticPage({data}) {
  return &lt;div&gt;{JSON.stringify(data)}&lt;/div&gt;;
}</code></pre>
<hr>
<h2 id="4-주의-사항">4. 주의 사항</h2>
<ul>
<li>환경 분리: <code>fetch</code>를 사용하는 위치가 클라이언트인지 서버인지에 따라 API 호출 방식과 효율성이 달라집니다.</li>
<li>절대 URL 사용: 서버사이드 코드에서는 절대 경로를 사용해야 정확한 API 호출이 가능합니다.</li>
<li>에러 처리: 항상 에러 핸들링 로직을 추가해 안정성을 확보해야 합니다.</li>
</ul>
<pre><code class="language-js">try {
  const res = await fetch(&#39;/...&#39;);
  if(!res.ok) throw new Error(&#39;Error Message&#39;);
  const data = await res.json();
} catch(error) {
  console.error(&#39;Fetch Error: &#39;, error);
}</code></pre>
<hr>
<h3 id="요약">요약</h3>
<ul>
<li>클라이언트와 서버 모두에서 <code>fetch</code>를 사용할 수 있으며 각 환경에 따라 데이터 가여오기 방식이 다릅니다. </li>
<li>Next.js <code>getServerSideProps</code>와 <code>getStaticProps</code>를 통해 SSR/SSG에서 <code>fetch</code>를 쉽게 통합할 수 있습니다.</li>
<li><code>API Routes</code>를 사용하면 데이터를 보다 안전하고 효율적으로 관리할 수 있습니다.</li>
</ul>
<p>```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Feature Sliced Design]]></title>
            <link>https://velog.io/@min_jae/Feature-Sliced-Design</link>
            <guid>https://velog.io/@min_jae/Feature-Sliced-Design</guid>
            <pubDate>Tue, 17 Dec 2024 08:39:53 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 개발자는 개발 과정에서 애플리케이션의 구조적 문제에 종종 직면합니다. 프로젝트가 커질수록 코드베이스는 점점 복잡해지고, 기능 확장은 물론 유지보수조차 어려워지는 상황이 발생합니다.
이 문제를 해결하기 위해서는 확장성, 모듈 간의 느슨한 결합, 그리고 높은 응집력을 제공할 수 있는 아키텍처가 필요합니다.</p>
<p>최근 많은 개발자들이 이러한 문제를 해결하기 위해 선택하는 아키텍처가 바로 <strong>Feature Sliced Design(FSD)</strong>입니다. 이번 글에서는 FSD란 무엇이고, 왜 주목받고 있는지 함께 알아보겠습니다.</p>
<hr>
<h2 id="fsd의-주요-요소">FSD의 주요 요소</h2>
<p>프론트엔드 아키텍처인 Feature Sliced Design(FSD)는 프로젝트의 확장성과 유지보수를 용이하게 만들기 위해 레이어(Layers), 슬라이스(Slices), 그리고 세그먼트(Segments)라는 개념을 사용합니다. 각 요소가 어떤 역할을 하는지 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/01bba568-0934-4ca7-8176-4132818c04b1/image.jpg" alt="fsd"></p>
<p>출처: <a href="https://dev.to/m_midas/feature-sliced-design-the-best-frontend-architecture-4noj">dev.to</a></p>
<h3 id="layer">Layer</h3>
<p>레이어는 최상위 레벨 디렉토리이자 분해의 첫 번째 단계로, 코드의 목적과 역할을 기반으로 프로젝트를 분리합니다.</p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/43bdfb01-5163-4e5b-9e4e-6072e96c29c5/image.png" alt="layer"></p>
<p>출처: <a href="https://dev.to/m_midas/feature-sliced-design-the-best-frontend-architecture-4noj">dev.to</a></p>
<ul>
<li>레이어의 수는 최대 7개로 제한되어 있어 복잡도를 줄이고, 각 레이어의 역할이 명확해집니다.</li>
<li>핵심 개념은 책임 분리이며, 이를 통해 유지보수성과 확장성이 높은 아키텍처를 구현할 수 있습니다.</li>
</ul>
<blockquote>
<p><code>app</code>: 애플리케이션 설정 및 루트 레이아웃을 관리
<code>processes</code>: 여러 페이지에서 사용되는 비즈니스 프로세스나 유저 흐름을 정의
<code>pages</code>: 페이지 단위의 컴포넌트
<code>widgets</code>: 페이지나 여러 feature를 조합한 UI 블럭
<code>features</code>: 프로젝트의 기능 단위로 구성
<code>entities</code>: 도메인 모델과 상태 관리를 담당
<code>shared</code>: 재사용 가능한 컴포넌트와 유틸리티</p>
</blockquote>
<h3 id="layer-구조의-장점">Layer 구조의 장점</h3>
<ul>
<li>책임 분리: 코드가 각자의 역할에 맞게 분리되어 가독성과 유지보수성이 향상됩니다.</li>
<li>확장성: 새로운 기능이나 페이지를 추가할 때 다른 레이어에 영향을 주지 않으므로 확장이 용이합니다.</li>
<li>유지보수: 특정 기능이나 프로세스의 위치가 명확해, 수정 및 관리가 쉬워집니다.</li>
</ul>
<h3 id="slice">Slice</h3>
<p>슬라이스는 각 레이어 안에서 기능(feature) 또는 도메인(domain) 단위로 코드를 그룹화하는 서브 디렉토리입니다. 슬라이스를 사용하면 관련된 코드들이 </p>
<p><img src="https://velog.velcdn.com/images/min_jae/post/b8e4d985-6c2b-4e30-9d74-948d096a9cf6/image.png" alt="slice"></p>
<p>출처: <a href="https://dev.to/m_midas/feature-sliced-design-the-best-frontend-architecture-4noj">dev.to</a></p>
<ul>
<li>슬라이스를 사용하면 코드의 관련된 부분들이 한곳에 모여 있어 재사용성과 유지보수성이 향상됩니다.</li>
<li>기능 단위의 코드 구조를 유지하므로 새로운 기능을 추가하거나 기존 기능을 수정할 때도 프로젝트 전체에 영향을 미치지 않습니다.</li>
<li>슬라이스의 이름은 정해진 것이 없습니다.</li>
</ul>
<h3 id="slice-구조의-장점">Slice 구조의 장점</h3>
<ul>
<li>명확한 경로: 각 슬라이스는 특정 기능이나 도메인에 대한 모든 코드를 포함합니다.</li>
<li>독립성: 각 슬라이스는 다른 슬라이스와 독립적으로 동작하므로 수정이 용이합니다</li>
<li>확장성: 기능이 추가될 때 새로운 슬라이스를 생성하면 됩니다. 기존 구조에 영향을 주지 않습니다.</li>
</ul>
<h3 id="segment">Segment</h3>
<p>세그먼트는 슬라이스 내부의 코드를 목적에 따라 나우어 관리하는 단위입니다. 세그먼트를 사용하면 슬라이스 내의 코드 구조가 명확해지고, 유지보수성과 재사용성이 더욱 향상됩니다.</p>
<blockquote>
<p><code>api</code>: 필수적인 서버 요청
<code>ui</code>: 슬라이스의 UI 컴포넌트
<code>model</code>: 비즈니스 로직
<code>lib</code>: 슬라이스 안에서 사용되는 헬퍼 기능
<code>config</code>: 슬라이스에서 요구되는 필수적인 설정
<code>consts</code>: 상수</p>
</blockquote>
<h3 id="public-api">Public API</h3>
<p>각 슬라이스와 세그먼트에는 공개 API(Publice API)가 있습니다. 이를 통해 슬라이스 또는 세그먼트에서 외부로 노출할 기능만 선택적으로 추출하고, 불필요한 내부 구현은 격리할 수 있습니다.</p>
<p>대부분 <code>index.js</code> 또는 <code>index.ts</code> 파일을 사용합니다.</p>
<pre><code class="language-plaintext">features/
└── cart/
    ├── api/
    ├── ui/
    ├── model/
    ├── lib/
    ├── config/
    ├── consts/
    └── index.js</code></pre>
<pre><code class="language-js">// features/cart/index.js
export { fetchCartItems } from &#39;./api&#39;;
export { CartItem } from &#39;./ui/CartItem&#39;;
export { $cart } from &#39;./model&#39;;
export { calculateTotal } from &#39;./lib/calculateTotal&#39;;
export { CART_API_URL } from &#39;./config&#39;;
export { CART_EMPTY_MESSAGE } from &#39;./consts&#39;;</code></pre>
<p>이제 외부에서 <code>cart</code> 슬라이스를 사용할 때 다음과 같이 명확하게 가져올 수 있습니다.</p>
<pre><code class="language-js">import { fetchCartItems, CartItem, calculateTotal } from &#39;features/cart&#39;;</code></pre>
<hr>
<h2 id="fsd를-도입할-때의-고려사항">FSD를 도입할 때의 고려사항</h2>
<blockquote>
<p>❓ 언제 FSD를 도입해야 할까요?
❗️ 프로젝트 초기부터 도입하면 좋지만, 기능이 복잡해지고 구조가 무너지는 시점에 도입해도 좋습니다.</p>
</blockquote>
<blockquote>
<p>❓ 팀 협업에서의 장점이 있나요?
❗️ 팀원들이 특정 슬라이스에만 집중해 작업할 수 있어 병렬 개발이 수월해집니다.</p>
</blockquote>
<blockquote>
<p>❓ 기존 프로젝트에 적용할 수 있나요?
❗️ 특정 레이서부터 FSD로 전환하면 기존 프로젝트를 점진적으로 마이그레이션할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="fsd의-장단점">FSD의 장단점</h2>
<table>
<thead>
<tr>
<th align="center">장점</th>
<th align="center">단점</th>
</tr>
</thead>
<tbody><tr>
<td align="center">유지보수성과 확장성이 뛰어남</td>
<td align="center">초기 설정이 다소 복잡할 수 있음</td>
</tr>
<tr>
<td align="center">관심사 분리가 명확함</td>
<td align="center">소규모 프로젝트에는 과함</td>
</tr>
<tr>
<td align="center">재사용성과 독립성이 높음</td>
<td align="center">팀원이 아키텍처를 이해해야함</td>
</tr>
</tbody></table>
<hr>
<h2 id="fsd와-다른-아키텍처-비교">FSD와 다른 아키텍처 비교</h2>
<table>
<thead>
<tr>
<th align="center">Atomic Design</th>
<th align="center">MVC</th>
<th align="center">FSD</th>
</tr>
</thead>
<tbody><tr>
<td align="center">UI 컴포넌트의 계층 구조에 초점을 맞춤</td>
<td align="center">고전적인 구조</td>
<td align="center">기능과 도메인에 집중</td>
</tr>
<tr>
<td align="center">기능 단위 구조 부족</td>
<td align="center">대규모 프로젝트에선 확장성이 부족</td>
<td align="center">대규모 프로젝트에 적합</td>
</tr>
</tbody></table>
<hr>
<p>✅ 참고</p>
<ul>
<li><a href="https://dev.to/m_midas/feature-sliced-design-the-best-frontend-architecture-4noj">dev.to</a></li>
<li><a href="https://emewjin.github.io/feature-sliced-design/">(번역) 기능 분할 설계 - 최고의 프런트엔드 아키텍처_emewjin</a></li>
<li><a href="https://velog.io/@jay/fsd">FSD 아키텍처 알아보기_dante Yoon</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>