<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>real-bird.log</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 개발자가 되고 싶다</description>
        <lastBuildDate>Fri, 08 Dec 2023 19:20:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>real-bird.log</title>
            <url>https://images.velog.io/images/real-bird/profile/4fe140e6-c1a1-4755-ae91-07f3a7bb6a1f/KakaoTalk_20220210_092712055.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. real-bird.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/real-bird" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Next.js] 공식 문서만 보고 Next.js 익히기(5)]]></title>
            <link>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B05</link>
            <guid>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B05</guid>
            <pubDate>Fri, 08 Dec 2023 19:20:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공식 문서 보기를 돌같이 하는 버릇을 고치자!</p>
</blockquote>
<h2 id="11-handling-errors">11. Handling Errors</h2>
<p>예기치 못한 에러 발생은 사용자에게 안 좋은 경험을 심어준다. 거기에 에러 스택까지 보여준다면? 사용자에게도, 개발자에게도 최악의 페이지가 될 것이다. <code>Next.js</code>에서는 파일로 에러를 핸들링하는 기능을 제공한다. 에러 처리가 필요한 폴더 하위에 <a href="https://nextjs.org/docs/app/api-reference/file-conventions/error"><code>error.tsx</code></a>를 추가한다.</p>
<pre><code class="language-tsx">&#39;use client&#39;;

import { useEffect } from &#39;react&#39;;

export default function Error({
  error,
  reset,
}: {
  error: Error &amp; { digest?: string };
  reset: () =&gt; void;
}) {
  useEffect(() =&gt; {
    // Optionally log the error to an error reporting service
    console.error(error);
  }, [error]);

  return (
    &lt;main className=&quot;flex h-full flex-col items-center justify-center&quot;&gt;
      &lt;h2 className=&quot;text-center&quot;&gt;Something went wrong!&lt;/h2&gt;
      &lt;button
        className=&quot;mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400&quot;
        onClick={
          // Attempt to recover by trying to re-render the invoices route
          () =&gt; reset()
        }
      &gt;
        Try again
      &lt;/button&gt;
    &lt;/main&gt;
  );
}</code></pre>
<p><code>error</code>는 <code>JS</code>의 에러 객체이고, <code>reset</code>은 에러 경계로 되돌리는 함수이다. <code>reset</code>을 실행하면 에러가 발생한 라우트의 리렌더링을 시도하도록 유도할 수 있다.</p>
<p><code>error.tsx</code>는 전체 에러를 핸들링하는 역할이라면, <code>not-found.tsx</code>는 404 error를 핸들링한다. <code>not-found</code>가 필요한 라우트 하위에 파일을 생성한다.</p>
<p><img src="https://nextjs.org/_next/image?url=%2Flearn%2Fdark%2Fnot-found-file.png&w=1920&q=75&dpl=dpl_AMSyfMpgLTgL1H5bxt6C3RVhhjQ4" alt=""></p>
<pre><code class="language-tsx">// app/dashboard/invoices/[id]/edit/not-found.tsx
import Link from &#39;next/link&#39;;
import { FaceFrownIcon } from &#39;@heroicons/react/24/outline&#39;;

export default function NotFound() {
  return (
    &lt;main className=&quot;flex h-full flex-col items-center justify-center gap-2&quot;&gt;
      &lt;FaceFrownIcon className=&quot;w-10 text-gray-400&quot; /&gt;
      &lt;h2 className=&quot;text-xl font-semibold&quot;&gt;404 Not Found&lt;/h2&gt;
      &lt;p&gt;Could not find the requested invoice.&lt;/p&gt;
      &lt;Link
        href=&quot;/dashboard/invoices&quot;
        className=&quot;mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400&quot;
      &gt;
        Go Back
      &lt;/Link&gt;
    &lt;/main&gt;
  );
}</code></pre>
<p>페이지에서는 <code>notfound()</code> 함수를 호출한다.</p>
<pre><code class="language-diff">// app/dashboard/invoices/[id]/edit/page.tsx

import { fetchInvoiceById, fetchCustomers } from &#39;@/app/lib/data&#39;;
import { updateInvoice } from &#39;@/app/lib/actions&#39;;
+ import { notFound } from &#39;next/navigation&#39;;

export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id),
    fetchCustomers(),
  ]);

+  if (!invoice) {
+    notFound();
+  }

  // ...
}</code></pre>
<h2 id="12-improving-accessibility">12. Improving Accessibility</h2>
<p>접근성은 <strong>장애인을 포함한 모든 사람이 사용할 수 있는 애플리케이션을 설계하고 구현하는 것</strong>을 말한다. 참고 <a href="https://web.dev/learn/accessibility/">web.dev - learn accessibility</a></p>
<h3 id="12-1-using-the-eslint-accessibility-plugin-in-nextjs">12-1. Using the ESLint accessibility plugin in Next.js</h3>
<p><code>Next.js</code>는 기본적으로 <a href="https://www.npmjs.com/package/eslint-plugin-jsx-a11y"><code>eslint-plugin-jsx-a11y</code></a>를 포함하고 있다. 이 플러그인은 <code>aria-*</code>, <code>alt</code>, <code>role</code> 등 접근성에 대한 이슈를 체크해준다. <code>&quot;lint&quot;: &quot;next lint&quot;</code> 스크립트를 추가하여 검사할 수 있다. 예를 들어, 이미지의 <code>alt</code>가 없다면 검사했을 때의 경고 문구이다.</p>
<pre><code>./app/ui/invoices/table.tsx
29:23  Warning: Image elements must have an alt prop, either with meaningful text, or an empty string for decorative images.  jsx-a11y/alt-text</code></pre><h3 id="12-2-improving-form-accessibility">12-2. Improving form accessibility</h3>
<p>사용하고 있는 <code>form</code>에서는 접근성 향상을 위해 3가지를 이미 실천하고 있다.</p>
<ol>
<li>Semantic HTML : <code>div</code> 대신 <code>input</code>, <code>option</code> 등의 시멘틱 태그를 사용하여 사용자에게 양식을 탐색하고 이해하기 쉽게 만든다.</li>
<li>Labelling : <code>label</code>과 <code>htmlFor</code>를 사용하면 사용자가 label를 클릭하여 해당 입력 필드에 집중할 수 있다.</li>
<li>Focus Outline : 탭을 눌러 필드에 초점이 맞춰졌을 때 윤곽선이 표시되도록하여 키보드 및 화면 리더 사용자가 양식의 현재 위치를 이해하는 데 도움이 된다.</li>
</ol>
<p>일반적인 접근성 설정을 했지만, form validation을 충족하지는 못한다. 현재 비어 있는 양식을 제출하면 에러가 발생하기 때문이다.</p>
<h3 id="12-3-form-validation">12-3. Form validation</h3>
<p>클라이언트 측에서 간단한 대응은 <code>input</code>에 <code>required</code> 속성을 추가하는 것이다.</p>
<pre><code class="language-diff">&lt;input
  id=&quot;amount&quot;
  name=&quot;amount&quot;
  type=&quot;number&quot;
  placeholder=&quot;Enter USD amount&quot;
  className=&quot;peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500&quot;
+ required
/&gt;</code></pre>
<p>일반적으로는 괜찮지만, dev tool에서 해당 요소의 <code>required</code> 속성을 지우면 검증이 먹히지 않는다. <code>Next.js</code>에서는 서버 측 대안을 제시한다. 서버 측 검증에서는</p>
<ul>
<li>데이터를 데이터베이스로 보내기 전에 데이터가 예상되는 형식인지 확인한다.</li>
<li>악의적인 사용자가 클라이언트 측 유효성 검사를 우회하는 위험을 줄일 수 있다.</li>
<li>유효한 데이터로 간주되는 데이터에 대한 신뢰할 수 있는 단일 소스를 확보한다.</li>
</ul>
<p><code>useFormState</code>를 사용하여 서버 측 검증을 진행한다. 훅을 사용하므로 <code>use client</code>를 명시해 클라이언트 컴포넌트로 전환한다.</p>
<pre><code class="language-tsx">&#39;use client&#39;;

// ...
import { useFormState } from &#39;react-dom&#39;;</code></pre>
<p><a href="https://react.dev/reference/react-dom/hooks/useFormState"><code>useFormState</code></a>는 <a href="https://react.dev/reference/react/useReducer">useReducer</a>와 유사한 실험적 기능이다. <code>action</code>과 <code>initialState</code>를 인자로 받고, <code>state</code>와 <code>dispatch</code>를 반환한다. <code>&lt;form action={}&gt;</code>에 <code>dispatch</code>를 주입한다.</p>
<pre><code class="language-tsx">// ...
import { useFormState } from &#39;react-dom&#39;;

export default function Form({ customers }: { customers: CustomerField[] }) {
  const initialState = { message: null, errors: {} };
  const [state, dispatch] = useFormState(createInvoice, initialState);

  return &lt;form action={dispatch}&gt;...&lt;/form&gt;;
}</code></pre>
<p><code>zod</code>를 이용해 검증할 데이터 스키마를 추가한다.</p>
<pre><code class="language-ts">const FormSchema = z.object({
  id: z.string(),
  customerId: z.string({
    invalid_type_error: &#39;Please select a customer.&#39;,
  }),
  amount: z.coerce
    .number()
    .gt(0, { message: &#39;Please enter an amount greater than $0.&#39; }),
  status: z.enum([&#39;pending&#39;, &#39;paid&#39;], {
    invalid_type_error: &#39;Please select an invoice status.&#39;,
  }),
  date: z.string(),
});</code></pre>
<p><code>useFormState</code>를 위해 <code>createInvoice</code>에 <code>prevState</code>를 추가한다. 사용하지 않더라도 <code>useFormState</code>가 필수로 요구하기 때문이다.</p>
<pre><code class="language-ts">// This is temporary until @types/react-dom is updated
export type State = {
  errors?: {
    customerId?: string[];
    amount?: string[];
    status?: string[];
  };
  message?: string | null;
};

export async function createInvoice(prevState: State, formData: FormData) {
  // ...
}</code></pre>
<p><code>zod</code>의 <code>parse</code> 대신 <code>safeParse</code>로 교체한다. <code>safeParse</code>는 성공 또는 실패 시의 객체를 반환하여 <code>try/catch</code> 구문에 넣지 않아도 유효성 검사를 처리할 수 있다.</p>
<pre><code class="language-ts">export async function createInvoice(prevState: State, formData: FormData) {
  // Validate form fields using Zod
  const validatedFields = CreateInvoice.safeParse({
    customerId: formData.get(&#39;customerId&#39;),
    amount: formData.get(&#39;amount&#39;),
    status: formData.get(&#39;status&#39;),
  });

  // If form validation fails, return errors early. Otherwise, continue.
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: &#39;Missing Fields. Failed to Create Invoice.&#39;,
    };
  }

  // ...
}</code></pre>
<p>검증을 통과하지 못하면 <code>useFormState</code>의 <code>state</code>는 다음과 같은 에러 객체를 반환한다.</p>
<pre><code class="language-js">{
  errors: { customerId: Array(1), amount: Array(1), status: Array(1) },
  message: &quot;Missing Fields. Failed to Create Invoice.&quot;
}</code></pre>
<p>에러가 발생하면 화면에 표시하는 코드를 작성한다.</p>
<pre><code class="language-diff">&lt;form action={dispatch}&gt;
  {/* ... */}
    &lt;select
      id=&quot;customer&quot;
      name=&quot;customerId&quot;
      className=&quot;peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500&quot;
      defaultValue=&quot;&quot;
+     aria-describedby=&quot;customer-error&quot;
    &gt;
    {/* ... */}
    &lt;/select&gt;
+   &lt;div id=&quot;customer-error&quot; aria-live=&quot;polite&quot; aria-atomic=&quot;true&quot;&gt;
+     {state.errors?.customerId &amp;&amp;
+       state.errors.customerId.map((error: string) =&gt; (
+         &lt;p className=&quot;mt-2 text-sm text-red-500&quot; key={error}&gt;
+           {error}
+         &lt;/p&gt;
+       ))}
+   &lt;/div&gt;
    {/* ... */}
&lt;/form&gt;</code></pre>
<p><code>aria-describedby=&quot;customer-error&quot;</code>는 <code>id=&quot;customer-error&quot;</code>와 관계를 형성해 오류가 발생하면 화면 판독기가 해당 설명을 읽게 된다. <code>aria-live=&quot;polite&quot;</code>는 오류가 발생했음을 알릴 때 사용자를 방해하지 않고 유휴 상태일 때만 알린다.</p>
<h2 id="13-adding-authentication">13. Adding Authentication</h2>
<p><strong>Authentication(인증)</strong>은 웹앱의 핵심 중 하나로, 실제 유저와 시스템이 바라는 유저가 같은지 확인하는 단계이다. 인증의 방법에는 여러 가지가 있다. 아이디와 비밀번호 입력 후 인증 코드를 발급한다거나 Google이나 Naver 등의 서드 파티를 이용한다. 혹은 외부 앱을 이용한 2단계 인증(2FA)으로 보안을 강화할 수도 있다. 이러한 인증 방법은 로그인 정보에 접근하더라도 고유 토큰 없이는 액세스할 수 없도록 한다.</p>
<h3 id="13-1-authentication-vs-authorization">13-1. Authentication vs. Authorization</h3>
<p>웹 개발에서 Authentication과 Authorization은 비슷하지만 서로 다른 역할을 하는 개념이다.</p>
<p><strong>Authentication(인증)</strong>은 사용자가 자신이 말한 사람이 <strong>맞는지</strong> 확인하는 것이다.</p>
<p><strong>Authorization(인가)</strong>은 사용자의 신원이 확인되면 애플리케이션의 어떤 부분에 대한 <strong>권한</strong>이 있는지 결정하는 것이다.</p>
<h3 id="13-2-nextauthjs">13-2. NextAuth.js</h3>
<p>여기서 사용하는 인증 라이브러리는 <a href="https://authjs.dev/reference/nextjs"><code>NextAuth.js</code></a>이다. 세션 관리, 로그인 및 로그아웃, 기타 인증과 관련된 복잡한 과정에 대한 솔루션을 제공한다. <code>Next.js @14</code>와 호환되는 버전을 <code>npm install next-auth@beta</code>로 설치한다.</p>
<p>다음으로 <code>openssl rand -base64 32</code>로 비밀키를 생성하고, <code>.env</code>에 추가한다.</p>
<pre><code>AUTH_SECRET=your-secret-key</code></pre><p>루트 폴더에 <code>auth.config.ts</code> 파일을 생성해 <code>NextAuth</code>에 대한 옵션을 추가한다.</p>
<pre><code class="language-ts">import type { NextAuthConfig } from &#39;next-auth&#39;;

export const authConfig = {
  pages: {
    signIn: &#39;/login&#39;,
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith(&#39;/dashboard&#39;);
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
        return Response.redirect(new URL(&#39;/dashboard&#39;, nextUrl));
      }
      return true;
    },
  },
  providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;</code></pre>
<p><code>pages</code> 옵션을 통해 로그인, 로그아웃, 오류 페이지 경로 등을 지정할 수 있다. <code>signIn: &#39;/login&#39;</code>을 추가하면 <code>NextAuth</code>의 기본 페이지가 아닌 커스텀 로그인 페이지로 리디렉션한다(고 한다. 뭔 소린지 모르겠다.).</p>
<p><code>callbacks</code>은 <code>Next.js</code>의 <code>middleware</code>를 통해 로그인 요청이 완료되기 전 권한이 있는지 확인하는 데 사용한다. <code>auth</code>는 유저의 session, <code>request</code> 속성 등이 들어있다.</p>
<p><code>providers</code>는 다양한 로그인 옵션을 나열하는 배열이다. <code>NextAuth</code>의 구성을 충족하기 위해 일단 빈 배열로 설정되어 있다.</p>
<p><code>authConfig</code> 객체를 가져올 <code>middleware.ts</code> 파일도 루트 폴더에 생성한다.</p>
<pre><code class="language-ts">import NextAuth from &#39;next-auth&#39;;
import { authConfig } from &#39;./auth.config&#39;;

export default NextAuth(authConfig).auth;

export const config = {
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  matcher: [&#39;/((?!api|_next/static|_next/image|.*\\.png$).*)&#39;],
};</code></pre>
<p><code>NextAuth</code> 객체를 초기화하고, <code>matcher</code>에 따라 <code>middleware</code>를 실행한다. 배열에 담긴 문자열은 <code>api</code>나 <code>_next/static</code>, <code>_next/image</code>, <code>.png</code>로 끝나는 경로를 제외하고 <code>middleware</code>를 실행한다는 의미이다. 이렇게 하면 미들웨어가 보호된 경로에서 인증이 확인될 때까지 렌더링을 하지 않아 성능 향상의 이점이 생긴다.</p>
<p>패스워드는 보통 생성할 때 해싱하여 저장한다. 이 프로젝트에서는 <code>bcrypt</code> 패키지를 사용하여 해싱 처리했는데, 문제는 Node API에 의존하는 패키지이기 때문에 미들웨어에서는 실행할 수 없다는 점이다. 이를 위해 <code>authConfig</code> 객체를 퍼트리는 <code>auth.ts</code> 파일을 새로 만든다.</p>
<pre><code class="language-ts">import NextAuth from &#39;next-auth&#39;;
import { authConfig } from &#39;./auth.config&#39;;

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
});</code></pre>
<p><code>NextAuth</code>에 <code>providers</code> 옵션을 추가해야 한다. <code>providers</code>는 Google 또는 GitHub와 같은 다양한 로그인 옵션을 나열하는 배열이다. 여기서는 <a href="https://nextjs.org/learn/dashboard-app/adding-authentication">Credentials provider</a>만 사용한다.</p>
<pre><code class="language-ts">import NextAuth from &#39;next-auth&#39;;
import { authConfig } from &#39;./auth.config&#39;;
import Credentials from &#39;next-auth/providers/credentials&#39;;

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [Credentials({})],
});</code></pre>
<p><code>zod</code>를 사용하여 이메일과 비밀번호 유효성을 검사한 후 유저 정보를 확인하자.</p>
<pre><code class="language-ts">import NextAuth from &#39;next-auth&#39;;
import { authConfig } from &#39;./auth.config&#39;;
import Credentials from &#39;next-auth/providers/credentials&#39;;
import { z } from &#39;zod&#39;;
import { sql } from &#39;@vercel/postgres&#39;;
import type { User } from &#39;@/app/lib/definitions&#39;;
import bcrypt from &#39;bcrypt&#39;;

async function getUser(email: string): Promise&lt;User | undefined&gt; {
  try {
    const user = await sql&lt;User&gt;`SELECT * FROM users WHERE email=${email}`;
    return user.rows[0];
  } catch (error) {
    console.error(&#39;Failed to fetch user:&#39;, error);
    throw new Error(&#39;Failed to fetch user.&#39;);
  }
}

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);
        if (parsedCredentials.success) {
          const { email, password } = parsedCredentials.data;
          const user = await getUser(email);
          if (!user) return null;

          const passwordsMatch = await bcrypt.compare(password, user.password);

          if (passwordsMatch) return user;
        }
        return null;
      },
    }),
  ],
});</code></pre>
<p>로그인 <code>action</code>을 작성한 다음 form을 수정한다.</p>
<pre><code class="language-ts">// lib/actions

import { signIn } from &#39;@/auth&#39;;
import { AuthError } from &#39;next-auth&#39;;

// ...

export async function authenticate(
  prevState: string | undefined,
  formData: FormData,
) {
  try {
    await signIn(&#39;credentials&#39;, formData);
  } catch (error) {
    if (error instanceof AuthError) {
      switch (error.type) {
        case &#39;CredentialsSignin&#39;:
          return &#39;Invalid credentials.&#39;;
        default:
          return &#39;Something went wrong.&#39;;
      }
    }
    throw error;
  }
}</code></pre>
<pre><code class="language-tsx">// login-form

&#39;use client&#39;;

import { lusitana } from &#39;@/app/ui/fonts&#39;;
import {
  AtSymbolIcon,
  KeyIcon,
  ExclamationCircleIcon,
} from &#39;@heroicons/react/24/outline&#39;;
import { ArrowRightIcon } from &#39;@heroicons/react/20/solid&#39;;
import { Button } from &#39;@/app/ui/button&#39;;
import { useFormState, useFormStatus } from &#39;react-dom&#39;;
import { authenticate } from &#39;@/app/lib/actions&#39;;

export default function LoginForm() {
  const [errorMessage, dispatch] = useFormState(authenticate, undefined);

  return (
    &lt;form action={dispatch} className=&quot;space-y-3&quot;&gt;
      &lt;div className=&quot;flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8&quot;&gt;
        &lt;h1 className={`${lusitana.className} mb-3 text-2xl`}&gt;
          Please log in to continue.
        &lt;/h1&gt;
        &lt;div className=&quot;w-full&quot;&gt;{/* ... */}&lt;/div&gt;
        &lt;LoginButton /&gt;
        &lt;div
          className=&quot;flex h-8 items-end space-x-1&quot;
          aria-live=&quot;polite&quot;
          aria-atomic=&quot;true&quot;
        &gt;
          {errorMessage &amp;&amp; (
            &lt;&gt;
              &lt;ExclamationCircleIcon className=&quot;h-5 w-5 text-red-500&quot; /&gt;
              &lt;p className=&quot;text-sm text-red-500&quot;&gt;{errorMessage}&lt;/p&gt;
            &lt;/&gt;
          )}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  );
}

function LoginButton() {
  const { pending } = useFormStatus();

  return (
    &lt;Button className=&quot;mt-4 w-full&quot; aria-disabled={pending}&gt;
      Log in &lt;ArrowRightIcon className=&quot;ml-auto h-5 w-5 text-gray-50&quot; /&gt;
    &lt;/Button&gt;
  );
}</code></pre>
<p>로그아웃 기능도 추가한다.</p>
<pre><code class="language-tsx">import Link from &#39;next/link&#39;;
import NavLinks from &#39;@/app/ui/dashboard/nav-links&#39;;
import AcmeLogo from &#39;@/app/ui/acme-logo&#39;;
import { PowerIcon } from &#39;@heroicons/react/24/outline&#39;;
import { signOut } from &#39;@/auth&#39;;

export default function SideNav() {
  return (
    &lt;div className=&quot;flex h-full flex-col px-3 py-4 md:px-2&quot;&gt;
      // ...
      &lt;div className=&quot;flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2&quot;&gt;
        &lt;NavLinks /&gt;
        &lt;div className=&quot;hidden h-auto w-full grow rounded-md bg-gray-50 md:block&quot;&gt;&lt;/div&gt;
        &lt;form
          action={async () =&gt; {
            &#39;use server&#39;;
            await signOut();
          }}
        &gt;
          &lt;button className=&quot;flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3&quot;&gt;
            &lt;PowerIcon className=&quot;w-6&quot; /&gt;
            &lt;div className=&quot;hidden md:block&quot;&gt;Sign Out&lt;/div&gt;
          &lt;/button&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p><code>use server</code>를 최상단이 아닌 <code>action</code> 속성 내에 선언하는 게 신기했다. 이게 그 유명한 밈으로 탄생한 부분인 듯하다.</p>
<p><img src="https://media.licdn.com/dms/image/D4D12AQEZJHiY-2xb5w/article-cover_image-shrink_720_1280/0/1700390410238?e=1707350400&v=beta&t=8rnKQ49XVBsBa6FaYfV0Tw-E3su-v8YtfhZA35q_Ea0" alt=""></p>
<p>아무튼 다 작성하고 나니 위 <code>authConfig</code>에서 궁금했던 <code>signIn: &#39;/login&#39;</code>의 정체를 알았다. 이것을 설정하지 않으면 비로그인 유저가 인증이 필요한 페이지로 접근했을 때 리디렉션하지 않고 <code>not-found</code>를 실행한다. 설정했다면 자동으로 로그인 페이지로 연결한다.</p>
<h2 id="14-adding-metadata">14. Adding Metadata</h2>
<p><strong>SEO</strong>와 콘텐츠 공유에 관련해서 웹앱의 <code>metadata</code>는 매우 중요한 요소이다. 최종장에서는 <code>metadata</code>에 대해서 학습한다.</p>
<h3 id="14-1-what-is-metadata">14-1. What is metadata?</h3>
<p><code>metadata</code>는 웹 페이지에 대한 추가 세부 정보를 제공한다. 사용자에게는 표시되지 않지만, <code>&lt;head&gt;</code> 태그 내에 포함되어 백그라운드에서 동작한다. 이는 검색 엔진 등이 웹 페이지를 더 잘 이해하도록 돕는다. SEO를 향상시키는 데 중요한 역할을 하며, 적절한 metadata는 검색 엔진에서 웹 페이지의 순위를 높인다.</p>
<p><code>Next.js</code>가 제공하는 <strong>Metadata API</strong>에는 두 가지 방식이 있다. 하나는 <code>Config-based</code>로, <strong>정적인 metadata 객체</strong>를 export하거나 <strong>동적으로 metadata를 생성하는 함수</strong>를 <code>layout.tsx</code>나 <code>page.tsx</code>에서 export하는 방식이다. 다른 하나는 <code>File-based</code>로, <code>favicon.ico</code>, <code>opengraph-image.jpg</code>, <code>robots.txt</code>, <code>sitemap.xml</code> 등 특수한 목적의 이름을 가진 파일들을 사용한다. 두 방식을 사용하면 <code>Next.js</code>가 자동으로 적절한 <code>&lt;head&gt;</code> 요소를 생성한다.</p>
<p>예시로, <code>/public</code> 폴더에 <code>favicon.ico</code>와 <code>opengraph-image.jpg</code> 파일이 들어있다. 이것을 <code>/app</code> 폴더로 옮기면 자동으로 <code>head</code>에 추가된다.</p>
<p><code>layout.tsx</code>나 <code>page.tsx</code> 파일 내에 <code>metadata</code> 객체를 포함할 수도 있다.</p>
<pre><code class="language-tsx">// app/layout.tsx

import { Metadata } from &#39;next&#39;;

export const metadata: Metadata = {
  title: &#39;Acme Dashboard&#39;,
  description: &#39;The official Next.js Course Dashboard, built with App Router.&#39;,
  metadataBase: new URL(&#39;https://next-learn-dashboard.vercel.sh&#39;),
};

export default function RootLayout() {
  // ...
}</code></pre>
<p>특정 페이지에 추가하여 해당 페이지의 정보만 담을 수 있다.</p>
<pre><code class="language-tsx">// dashboard/invoices
import { Metadata } from &#39;next&#39;;

export const metadata: Metadata = {
  title: &#39;Invoices | Acme Dashboard&#39;,
};

export default async function Page() {}</code></pre>
<p>하지만 이렇게 입력하면 문제가 하나 있다. 회사명과 같은 주요 정보가 바뀌었을 때 모든 페이지의 metadata를 찾아 수정해야 한다. 그렇게 하는 대신 <code>title.template</code>을 사용하여 기본값을 정하고 페이지마다 다른 <code>title</code>을 부여할 수 있다.</p>
<pre><code class="language-tsx">// app/layout.tsx

import { Metadata } from &#39;next&#39;;

export const metadata: Metadata = {
  title: {
    template: &#39;%s | Acme Dashboard&#39;,
    default: &#39;Acme Dashboard&#39;,
  },
  description: &#39;The official Next.js Learn Dashboard built with App Router.&#39;,
  metadataBase: new URL(&#39;https://next-learn-dashboard.vercel.sh&#39;),
};

// dashboard/invoices

export const metadata: Metadata = {
  title: &#39;Invoices&#39;,
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] 공식 문서만 보고 Next.js 익히기(4)]]></title>
            <link>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B04</link>
            <guid>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B04</guid>
            <pubDate>Tue, 05 Dec 2023 14:53:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공식 문서 보기를 돌같이 하는 버릇을 고치자!</p>
</blockquote>
<h2 id="9-adding-search-and-pagination">9. Adding Search and Pagination</h2>
<p>이 장에서는 URL search params을 통한 검색과 pagination을 학습한다.</p>
<h3 id="9-1-why-use-url-search-params">9-1. Why use URL search params?</h3>
<p>URL search params를 이용하여 검색을 하면 몇 가지 이점이 있다.</p>
<ul>
<li>북마크 및 공유 가능 : 검색 정보가 URL에 담겨 있기 때문에 사용자는 북마크에 추가해 나중에 참조하거나 공유하기 쉽다.</li>
<li>서버 측 렌더링과 초기 로드 : URL 매개변수를 서버에서 직접 조작하여 초기 상태 렌더링을 더 처리하기 쉽다.</li>
<li>분석 및 추적: 검색 쿼리와 필터를 URL에 직접 넣으면 추가적인 클라이언트 측 로직 없이도 사용자 행동을 더 쉽게 추적할 수 있다.</li>
</ul>
<p>URL search params를 이용하는데 <code>Next.js</code>의 다음 훅들을 사용한다.</p>
<ul>
<li><a href="https://nextjs.org/docs/app/api-reference/functions/use-search-params"><code>useSearchParams</code></a> : 현재 URL의 매개변수에 액세스한다. 예를 들어, <code>/dashboard/invoices?page=1&amp;query=pending</code>에 대한 검색 매개 변수는<code>{page: &#39;1&#39;, query: &#39;pending&#39;}</code>이다.</li>
<li><a href="https://nextjs.org/docs/app/api-reference/functions/use-pathname"><code>usePathname</code></a> : 현재 URL의 경로 이름을 읽는다. 예를 들어, <code>/dashboard/invoices</code>의 경우, 사용 경로명은 <code>/dashboard/invoices</code>를 반환한다.</li>
<li><a href="https://nextjs.org/docs/app/api-reference/functions/use-router#userouter"><code>useRouter</code></a> : 클라이언트 구성 요소 내에서 경로 간 탐색을 활성화한다.</li>
</ul>
<h3 id="9-2-adding-the-search-functionality">9-2. Adding the search functionality</h3>
<p>검색의 첫 번째 순서는 유저의 입력 정보를 얻는 것으로, 클라이언트 컴포넌트에서 이루어진다. 때문에 검색 파일 최상단에 <code>use client</code>를 작성하여 클라이언트 컴포넌트임을 명시해야 이벤트 리스너나 훅을 사용할 수 있다.</p>
<pre><code class="language-tsx">&#39;use client&#39;; // client component

import { useSearchParams, usePathname, useRouter } from &#39;next/navigation&#39;;

export default function Search({ placeholder }: { placeholder: string }) {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const { replace } = useRouter();

  function handleSearch(term: string) {
    console.log(`Searching... ${term}`);

    const params = new URLSearchParams(searchParams);
    if (term) {
      params.set(&#39;query&#39;, term);
    } else {
      params.delete(&#39;query&#39;);
    }
    replace(`${pathname}?${params.toString()}`);
  }

  return (
    &lt;div className=&quot;relative flex flex-1 flex-shrink-0&quot;&gt;
      &lt;label htmlFor=&quot;search&quot; className=&quot;sr-only&quot;&gt;
        Search
      &lt;/label&gt;
      &lt;input
        className=&quot;peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500&quot;
        placeholder={placeholder}
        onChange={(e) =&gt; {
          handleSearch(e.target.value);
        }}
        defaultValue={searchParams.get(&#39;query&#39;)?.toString()}
      /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li><code>URLSearchParams</code>는 Web API 메서드로, 쿼리 파라미터를 <code>?page=1&amp;query=a</code>와 같은 문자열로 만들어준다. <code>set</code>으로 검색어를 추가하고 <code>delete</code>로 비운다.</li>
<li><code>usePathname</code>으로 가져온 경로에 <code>useRouter</code>의 <code>replace</code>를 이용하여 쿼리를 추가한다.</li>
<li>URL로 직접 이동했을 때 쿼리와 <code>input</code>을 동기화하려면 <code>defaultValue</code>를 설정한다.</li>
</ul>
<h3 id="9-3-best-practice-debouncing">9-3. Best practice: Debouncing</h3>
<p>검색 기능을 최적화하자.</p>
<pre><code>Searching... S
Searching... St
Searching... Ste
Searching... Stev
Searching... Steve
Searching... Steven</code></pre><p>지금은 입력할 때마다 요청을 보내 서버 부하를 유발한다. 입력 이벤트가 끝났을 때만 쿼리를 보내도록 <code>Debounce</code>로 이벤트를 제어한다. 여기서는 <a href="https://www.npmjs.com/package/use-debounce"><code>use-debounce</code></a> 라이브러리를 사용한다.</p>
<pre><code class="language-tsx">// ...
import { useDebouncedCallback } from &#39;use-debounce&#39;;

// Inside the Search Component...
const handleSearch = useDebouncedCallback((term) =&gt; {
  console.log(`Searching... ${term}`);

  const params = new URLSearchParams(searchParams);
  if (term) {
    params.set(&#39;query&#39;, term);
  } else {
    params.delete(&#39;query&#39;);
  }
  replace(`${pathname}?${params.toString()}`);
}, 300);</code></pre>
<p><code>300ms</code>이내에 아무런 입력값이 없을 때 쿼리 요청을 보낸다. 띄엄띄엄 입력했을 때의 결과다.</p>
<pre><code>Searching... ste
Searching... steven</code></pre><h3 id="9-4-adding-pagination">9-4. Adding pagination</h3>
<p>pagination도 비슷한 과정으로 진행한다.</p>
<pre><code class="language-tsx">&#39;use client&#39;;

// ...
import { usePathname, useSearchParams } from &#39;next/navigation&#39;;

export default function Pagination({ totalPages }: { totalPages: number }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const currentPage = Number(searchParams.get(&#39;page&#39;)) || 1;

  const createPageURL = (pageNumber: number | string) =&gt; {
    const params = new URLSearchParams(searchParams);
    params.set(&#39;page&#39;, pageNumber.toString());
    return `${pathname}?${params.toString()}`;
  };
  // ...
}</code></pre>
<p>검색했을 때는 페이지가 <code>1</code>이 되도록 <code>Search</code>를 수정한다.</p>
<pre><code class="language-tsx">export default function Search({ placeholder }: { placeholder: string }) {
  // ...
  const handleSearch = useDebouncedCallback((term) =&gt; {
    // ...
    params.set(&#39;page&#39;, &#39;1&#39;);
    // ...
  }, 300);</code></pre>
<h2 id="10-mutating-data">10. Mutating Data</h2>
<p>이전 장에서 CRUD 중 Read를 배웠으니 여기서는 Create, Update, Delete 기능을 추가한다.</p>
<h3 id="10-1-what-are-server-actions">10-1. What are Server Actions?</h3>
<p><code>React Server Actions</code>는 서버에서 실행되는 비동기 함수를 클라이언트나 서버에서 호출하여 사용하고, API 엔드포인트 없이 데이터 변경이 가능하다. <code>Next.js</code>가 서버 액션을 권장하는 이유는 <strong>보안</strong> 때문이다. 다양한 공격으로부터 데이터를 안전하게 보호하고 접근을 보장하는 효과적인 보안 솔루션을 제공한다고 한다. POST 요청, 암호화, 엄격한 입력 확인, 오류 메세지 해싱, 호스트 제한과 같은 기술을 통해 보안 목표를 달성하면서 앱의 안정성을 크게 향상시킨다.</p>
<p><code>JS</code>의 내장 API인 <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData"><code>FormData</code></a>를 통해 <code>action</code> 속성으로 입력값을 수신할 수 있다.</p>
<pre><code class="language-tsx">// Server Component
export default function Page() {
  // Action
  async function create(formData: FormData) {
    &#39;use server&#39;;

    // Logic to mutate data...
  }

  // Invoke the action using the &quot;action&quot; attribute
  return &lt;form action={create}&gt;...&lt;/form&gt;;
}</code></pre>
<p><code>use server</code>는 서버 컴포넌트를 가리키는데, 서버 컴포넌트에서 서버 액션을 호출하면 클라이언트의 <code>JS</code>가 비활성화되어 있더라도 양식이 작동하는 이점이 있다.</p>
<p><code>Next.js</code>에서의 서버 액션은 <a href="https://nextjs.org/docs/app/building-your-application/caching"><code>Next.js Caching</code></a>과 긴밀하게 통합되어 있다. 서버 액션을 통해 양식이 제출되면 해당 액션을 사용하여 데이터를 변경할 수 있을 뿐만 아니라 <code>revalidatePath</code> 및 <code>revalidateTag</code>와 같은 API를 사용하여 관련 캐시의 유효성을 다시 검사할 수도 있다.</p>
<h3 id="10-2-create-a-server-action">10-2. Create a Server Action</h3>
<p>서버 액션에서 사용하는 함수를 모아둔 파일을 만들고 최상단에 <code>use server</code> 지시문을 작성한다. 해당 지시문이 추가된 파일에서 내보낸 함수는 서버 함수로 표시되어 클라이언트나 서버에서 다양하게 사용할 수 있다.</p>
<pre><code class="language-ts">// app/lib/actions.ts
&#39;use server&#39;;

export async function createInvoice(formData: FormData) {}</code></pre>
<p>생성한 서버 액션 함수를 form에 전달한다.</p>
<pre><code class="language-tsx">&#39;use client&#39;;

import { customerField } from &#39;@/app/lib/definitions&#39;;
import Link from &#39;next/link&#39;;
import {
  CheckIcon,
  ClockIcon,
  CurrencyDollarIcon,
  UserCircleIcon,
} from &#39;@heroicons/react/24/outline&#39;;
import { Button } from &#39;@/app/ui/button&#39;;
import { createInvoice } from &#39;@/app/lib/actions&#39;;

export default function Form({
  customers,
}: {
  customers: customerField[];
}) {
  return (
    &lt;form action={createInvoice}&gt;
      // ...
  )
}</code></pre>
<p>HTML의 <code>&lt;form&gt;</code>과 다른 점은 <code>action</code>에 URL이 아닌 함수가 들어갔다는 점이다. React에서는 특별한 속성으로 간주되어 액션을 호출할 수 있도록 그 위에 빌드됨을 의미한다. 서버 액션은 뒤에서 POST API 엔드포인트를 자동으로 생성한다.</p>
<h3 id="10-3-validate-and-revalidate-and-redirect">10-3. Validate and Revalidate and Redirect</h3>
<p>form을 제출하여 서버 액션이 실행되었을 때 다음과 같은 타입을 기댓값으로 원한다.</p>
<pre><code class="language-ts">export type Invoice = {
  id: string; // Will be created on the database
  customer_id: string;
  amount: number; // Stored in cents
  status: &#39;pending&#39; | &#39;paid&#39;;
  date: string;
};</code></pre>
<p>하지만 <code>console.log(typeof rawFormData.amount)</code>를 해보면 <code>number</code>가 아닌 <code>string</code>으로 찍히는 것을 볼 수 있다. <code>input type=&quot;number&quot;</code>를 했다손 쳐도 <code>FomData</code>에서는 <code>string</code>을 반환한다. 이러한 검증을 수동으로 할 수도 있지만, 여기서는 <a href="https://zod.dev/"><code>Zod</code></a> 라이브러리를 사용하여 검증한다.</p>
<pre><code class="language-ts">&#39;use server&#39;;

import { z } from &#39;zod&#39;;

const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum([&#39;pending&#39;, &#39;paid&#39;]),
  date: z.string(),
});

const CreateInvoice = FormSchema.omit({ id: true, date: true });

export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get(&#39;customerId&#39;),
    amount: formData.get(&#39;amount&#39;),
    status: formData.get(&#39;status&#39;),
  });
  // ...
}</code></pre>
<p>데이터를 새로 생성하면 기존의 <code>invoices</code> 페이지가 stale한지 아닌지 검증해야 한다. 또한, 작성이 완료되었으므로 생성 페이지에서 <code>invoices</code> 페이지로 리다이렉트한다.</p>
<pre><code class="language-ts">&#39;use server&#39;;

// ...

export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get(&#39;customerId&#39;),
    amount: formData.get(&#39;amount&#39;),
    status: formData.get(&#39;status&#39;),
  });
  // ...
  revalidatePath(&#39;/dashboard/invoices&#39;);
  redirect(&#39;/dashboard/invoices&#39;);
}</code></pre>
<h3 id="10-4-updating-an-invoice">10-4. Updating an invoice</h3>
<p><code>invoice</code>를 수정하기 위해서 개별 페이지를 만들어야 한다. <code>id</code>에 따라 보여지는 <code>invoice</code> 페이지가 다르므로 <a href="https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes">Dynamic Routes</a>로 개별 페이지를 구현한다. <code>invoices/[id]/edit/page.tsx</code> 경로로 파일을 만든다.</p>
<p><img src="https://nextjs.org/_next/image?url=%2Flearn%2Fdark%2Fedit-invoice-route.png&w=1920&q=75&dpl=dpl_8s8Dnm8T2UqYs4Sz3a1AK4vKuj5w" alt=""></p>
<p>만약 <code>id</code>가 <code>1</code>인 <code>invoice</code>를 수정한다면 경로는 <code>dashboard/invoices/1/edit</code>이 될 것이다.</p>
<p><code>id</code>를 받아 업데이트하는 서버 액션을 만든다.</p>
<pre><code class="language-tsx">// ...
import { updateInvoice } from &#39;@/app/lib/actions&#39;;

export default function EditInvoiceForm({
  invoice,
  customers,
}: {
  invoice: InvoiceForm;
  customers: CustomerField[];
}) {
  const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);

  return (
    &lt;form action={updateInvoiceWithId}&gt;
      &lt;input type=&quot;hidden&quot; name=&quot;id&quot; value={invoice.id} /&gt;
    &lt;/form&gt;
  );
}</code></pre>
<p><code>bind</code>를 사용한 이유는 <code>action</code>에 id 인수를 담은 <code>updateInvoice(invoice.id)</code>를 사용할 수 없기 때문이다. 하지만 이렇게는 사용할 수 있더라. 이후 로직은 <code>Create</code>와 유사하다.</p>
<h3 id="10-5-deleting-an-invoice">10-5. Deleting an invoice</h3>
<p>Delete는 <code>id</code>를 받아 삭제 요청을 보내면 된다.</p>
<pre><code class="language-tsx">import { deleteInvoice } from &#39;@/app/lib/actions&#39;;

// ...

export function DeleteInvoice({ id }: { id: string }) {
  const deleteInvoiceWithId = deleteInvoice.bind(null, id);

  return (
    &lt;form action={deleteInvoiceWithId}&gt;
      &lt;button className=&quot;rounded-md border p-2 hover:bg-gray-100&quot;&gt;
        &lt;span className=&quot;sr-only&quot;&gt;Delete&lt;/span&gt;
        &lt;TrashIcon className=&quot;w-4&quot; /&gt;
      &lt;/button&gt;
    &lt;/form&gt;
  );
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] 공식 문서만 보고 Next.js 익히기(3)]]></title>
            <link>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B03</link>
            <guid>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B03</guid>
            <pubDate>Sun, 03 Dec 2023 11:08:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공식 문서 보기를 돌같이 하는 버릇을 고치자!</p>
</blockquote>
<h2 id="6-fetching-data">6. Fetching Data</h2>
<p><code>Next.js</code>는 라우트를 이용해 API 엔드포인트를 제공하여 서드 파티 프로그램이나 서버 없이도 데이터를 패칭할 수 있다. 스킵했지만, 앞선 장에서 <code>Vercel</code>이 제공하는 서비스를 이용해 <code>postgres</code> DB를 생성했는데, <code>prisma</code>같은 <code>ORM</code>을 통해 관계형 DB를 호출할 수 있다.</p>
<blockquote>
<p><code>ORM</code>이란?
Object Relational Mapping의 약자로, 구현한 객체와 관계형 DB의 불일치를 자동으로 매핑한 SQL문을 생성해 호환가능하게 해주는 기술이다.</p>
</blockquote>
<p>데이터 패치를 하기에 앞서, <code>Next.js</code>는 기본적으로 <code>React Server Component</code>를 사용하는데, 몇 가지 이점을 알려준다.</p>
<ul>
<li>프로미스를 지원하여 <code>useState</code>나 <code>useEffect</code>, 데이터 패치 라이브러리, 추가 API 계층 없이 <code>async/await</code>을 사용하여 데이터를 가져올 수 있다.</li>
<li>서버에서 실행되기 때문에 비용이 많이드는 로직은 서버에서 실행하고, 결과만 클라이언트로 전송할 수 있다.</li>
</ul>
<h3 id="6-1-fetching-data-for-the-dashboard-overview-page">6-1. Fetching data for the dashboard overview page</h3>
<p>제공해준 dashboard 페이지의 코드를 보자.</p>
<pre><code class="language-tsx">import { Card } from &#39;@/app/ui/dashboard/cards&#39;;
import RevenueChart from &#39;@/app/ui/dashboard/revenue-chart&#39;;
import LatestInvoices from &#39;@/app/ui/dashboard/latest-invoices&#39;;
import { lusitana } from &#39;@/app/ui/fonts&#39;;

export default async function Page() {
  return (
    &lt;main&gt;
      &lt;h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}&gt;
        Dashboard
      &lt;/h1&gt;
      &lt;div className=&quot;grid gap-6 sm:grid-cols-2 lg:grid-cols-4&quot;&gt;
        {/* &lt;Card title=&quot;Collected&quot; value={totalPaidInvoices} type=&quot;collected&quot; /&gt; */}
        {/* &lt;Card title=&quot;Pending&quot; value={totalPendingInvoices} type=&quot;pending&quot; /&gt; */}
        {/* &lt;Card title=&quot;Total Invoices&quot; value={numberOfInvoices} type=&quot;invoices&quot; /&gt; */}
        {/* &lt;Card
          title=&quot;Total Customers&quot;
          value={numberOfCustomers}
          type=&quot;customers&quot;
        /&gt; */}
      &lt;/div&gt;
      &lt;div className=&quot;mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8&quot;&gt;
        {/* &lt;RevenueChart revenue={revenue}  /&gt; */}
        {/* &lt;LatestInvoices latestInvoices={latestInvoices} /&gt; */}
      &lt;/div&gt;
    &lt;/main&gt;
  );
}</code></pre>
<p>페이지 컴포넌트는 <code>async function</code>으로 되어 있다. 이는 <code>await</code>을 곧장 사용할 수 있음을 의미한다. 주석 처리된 컴포넌트들은 모두 데이터를 받는다. 데이터를 패치해 보자.</p>
<pre><code class="language-tsx">// ...
import { fetchRevenue } from &#39;@/app/lib/data&#39;;

export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}</code></pre>
<p>컴포넌트에서 <code>await</code>을 사용하는 게 매우 신기하다. ts 에러가 발생하지만 런타임에는 아무 지장없이 잘 실행된다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/ce8df1a9-3ddf-4139-83d5-aeb992b9f807/image.png" alt="챕터6 dashboard page" title="챕터6 dashboard page"></p>
<p>여기서 두 가지 주의해야 할 사항이 있다고 한다.</p>
<ol>
<li>데이터 요청이 의도치 않게 서로를 차단해 <strong>요청 폭포수</strong>를 만들고 있다.</li>
<li>다른 하나는 <code>Next.js</code>가 성능 개선을 위해 기본적으로 prerender하여 정적 렌더링을 하기 때문에 데이터 변화가 있어도 동적으로 반영되지 않는다는 점이다.</li>
</ol>
<p>이 장에서는 1번을 살피고, 다음 장에서 2번을 살핀다.</p>
<h3 id="6-2-what-are-request-waterfalls">6-2. What are request waterfalls?</h3>
<p><code>waterfall</code>은 <strong>이전 요청의 완료 여부에 따라 달라지는 일련의 네트워크 요청을 의미</strong>한다. 여기서는 앞선 데이터 패칭이 완료 되어야 다음 데이터 패칭이 이루어지는 것이다.</p>
<p><img src="https://nextjs.org/_next/image?url=%2Flearn%2Fdark%2Fsequential-parallel-data-fetching.png&w=1920&q=75&dpl=dpl_GGugRB3M3WE9C8xcmftCsUL7LkbG" alt=""></p>
<p>이전 패칭의 결과가 후행 패치에 영향을 미칠 경우에는 나쁘지 않은 패턴이다. 하지만 앞선 주의사항 대로 성능에 영향을 미칠 수 있다.</p>
<h3 id="6-3-parallel-data-fetching">6-3. Parallel data fetching</h3>
<p>여러 데이터 요청이 동시에 발생하는 경우 <code>JS</code>가 제공하는 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">Promise.all</a>이나 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled">Promise.allSettled</a>를 사용하여 병행 처리할해 성능을 향상시킬 수 있다. 또한, 자바스크립트가 제공하는 함수를 사용하기 때문에 다른 프레임워크에서도 재사용 가능하다.</p>
<h2 id="7-static-and-dynamic-rendering">7. Static and Dynamic Rendering</h2>
<p>이전 챕터에서 문제삼은 주의사항 2번을 해결하는 챕터이다. 정적 렌더링은 빌드나 재검증(revalidate) 중 데이터를 가져오고 렌더링하는 과정이다. 이 결과물을 CDN에 배포해 캐싱한다.</p>
<p><img src="https://nextjs.org/_next/image?url=%2Flearn%2Fdark%2Fstatic-site-generation.png&w=1920&q=75&dpl=dpl_GGugRB3M3WE9C8xcmftCsUL7LkbG" alt=""></p>
<p>이러한 방식은 다음과 같은 이점이 있다.</p>
<ul>
<li>더 빠른 웹사이트 : 미리 렌더링된 콘텐츠를 캐싱하여 배포하므로 전 세계 사용자가 웹사이트에 더 빠르고 안정적으로 액세스할 수 있다.</li>
<li>서버 부하 감소 : 콘텐츠가 캐시되므로 서버에서 각 사용자 요청에 대해 콘텐츠를 동적으로 생성할 필요가 없다.</li>
<li>SEO : 미리 렌더링된 콘텐츠는 페이지가 로드될 때 이미 콘텐츠를 사용할 수 있으므로 검색 엔진 크롤러가 색인을 생성하기가 더 쉽다. 이는 검색 엔진 순위 향상으로 이어질 수 있다.</li>
</ul>
<p>따라서 정적 렌더링은 데이터 변화가 없거나 적은 블로그나 제품 페이지 등에 적합하다. 그러나 <code>dashboard</code>와 같이 데이터에 변화가 잦은 페이지에는 적합하지 않을 수 있다.</p>
<p>이와 반대되는 개념이 <strong>동적 렌더링(Dynamic Rendering)</strong>이다.</p>
<h3 id="7-1-what-is-dynamic-rendering">7-1. What is Dynamic Rendering?</h3>
<p>동적 렌더링은 사용자가 페이지에 방문했을 때 렌더링하여 콘텐츠를 생성한다. 이점은 다음과 같다.</p>
<ul>
<li>실시간 데이터 : 애플리케이션에서 실시간 또는 자주 업데이트되는 데이터를 표시할 수 있다. 데이터가 자주 변경되는 애플리케이션에 이상적이다.</li>
<li>사용자별 콘텐츠 : 대시보드나 사용자 프로필과 같은 개인화된 콘텐츠를 제공하고 사용자 상호 작용에 따라 데이터를 업데이트하는 것이 더 쉽다.</li>
<li>요청 시간 정보 : 동적 렌더링을 사용하면 쿠키나 URL 검색 매개변수와 같이 요청 시점에만 알 수 있는 정보에 액세스할 수 있다.</li>
</ul>
<h3 id="7-2-using-dynamic-rendering">7-2. Using Dynamic Rendering</h3>
<p>데이터 패치 함수 초입에 <code>unstable_noStore</code>를 불러와 적용한다.</p>
<pre><code class="language-ts">// ...
import { unstable_noStore as noStore } from &#39;next/cache&#39;;

export async function fetchRevenue() {
  noStore();
  // ...fetch logic
}

export async function fetchLatestInvoices() {
  noStore();
  // ...fetch logic
}

export async function fetchCardData() {
  noStore();
  // ...fetch logic
}

export async function fetchFilteredInvoices(
  query: string,
  currentPage: number,
) {
  noStore();
  // ...fetch logic
}

export async function fetchInvoicesPages(query: string) {
  noStore();
  // ...fetch logic
}

export async function fetchFilteredCustomers(query: string) {
  noStore();
  // ...fetch logic
}

export async function fetchInvoiceById(query: string) {
  noStore();
  // ...fetch logic
}</code></pre>
<p><code>unstable_noStore</code>는 실험적인 API이므로 추후 변경될 수도 있다고 한다. 안정적인 API는 <a href="https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config">Route Segment Config</a>의 <code>export const dynamic = &quot;force-dynamic&quot;</code>를 사용한다.</p>
<pre><code class="language-tsx">export const dynamic = &#39;force-dynamic&#39;;

export default function MyComponent() {}</code></pre>
<p>동적 렌더링이 가져오는 문제는 <strong>느리게 도착하는 데이터에 의해 앱의 성능이 결정된다</strong>는 점이다. 이를 해결하는 과정을 다음 챕터에서 안내한다.</p>
<h2 id="8-streaming">8. Streaming</h2>
<p>느린 데이터 가져오기 환경을 개선하는 방법을 알려주는 장이다.</p>
<h3 id="8-1-what-is-streaming">8-1. What is streaming?</h3>
<p>스트리밍(streaming)은 <strong>데이터를 &#39;작은 조각(chunk)&#39;로 분할하여 서버에서 준비되는 대로 클라이언트 측에 보내는 전송 방식</strong>을 말한다. 느린 데이터 요청으로 인한 앱 전체가 차단되는 것을 방지하고, 전체 데이터 패칭이 완료되지 않아도 일부 페이지를 조작할 수 있도록 한다.</p>
<p><img src="https://nextjs.org/_next/image?url=%2Flearn%2Fdark%2Fserver-rendering-with-streaming-chart.png&w=1920&q=75&dpl=dpl_GGugRB3M3WE9C8xcmftCsUL7LkbG" alt=""></p>
<p>리액트 컴포넌트는 하나의 청크로 간주될 수 있기 때문에 스트리밍을 적용하기에 좋다. 페이지에서는 <code>loading.tsx</code>을, 컴포넌트에서는 <code>&lt;Suspense&gt;</code>를 사용하여 스트리밍을 적용할 수 있다.</p>
<h3 id="8-2-using-loadingtsx">8-2. Using loading.tsx</h3>
<p>페이지 전체 로딩을 적용하는 방법은 매우 간단하다. 라우트 경로에 <code>loading.tsx</code>를 추가한다.</p>
<pre><code class="language-tsx">// app/dashboard/loading.tsx
export default function Loading() {
  return &lt;div&gt;Loading...&lt;/div&gt;;
}</code></pre>
<p>의도적으로 데이터 패치 중 하나를 느리게 만들면 로딩 화면이 보인다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/a8e5fdc1-b0d2-4189-a8b2-9f5712cd696d/image.gif" alt="챕터 8 스트리밍 로딩" title="챕터 8 스트리밍 로딩"></p>
<p>제공한 스켈레톤으로 로딩을 교체했는데, 작은 버그가 하나 있다. <code>dashboard</code> 바로 아래에 <code>loading</code>을 생성한 탓에 하위 라우트인 <code>dashboard/invoices</code>와 <code>dashboard/customers</code>에서도 로딩이 적용된다. <code>dashboard</code>에만 적용하려면 하위에 <code>(overview)</code> 폴더를 추가하고, <code>page.tsx</code>와 <code>loading.tsx</code>를 옮긴다.</p>
<pre><code>🗂app/
  └─🗂dashboard/
    ├─layout.tsx
    ├─🗂(overview)
    │ ├─loading.tsx
    │ └─page.tsx
    ├─🗂invoices/
    └─🗂customers/</code></pre><p>이렇게 경로 나누는 방식이 <a href="https://nextjs.org/docs/app/building-your-application/routing/route-groups">Route Groups</a>이며, 괄호로 작성한 폴더를 경로에 포함시키지 않으면서 나눌 수 있다. 예를 들어, 여기서 사용한 <code>loading.tsx</code>는 <code>(overview)</code> 하위에 있는 <code>page.tsx</code>에만 적용된다.</p>
<h3 id="8-3-streaming-a-component">8-3. Streaming a component</h3>
<p>위의 방식이 전체 페이지 스트리밍에 해당한다면, <code>&lt;Suspense&gt;</code>는 데이터가 필요한 특정 컴포넌트만 지연 로딩하는 방식이다. 지연 로딩할 부분을 <code>&lt;Suspense&gt;</code>로 감싸고 지연되는 동안 보여줄 <code>fallback</code>을 추가한다.</p>
<p><code>dashboard</code>에서 하나의 요청을 의도적으로 지연시켜 전체 페이지에 로딩이 발생했다. 해당 요청을 제거하고, 해당 컴포넌트를 <code>&lt;Suspense&gt;</code>로 감싼다.</p>
<pre><code class="language-diff">import RevenueChart from &#39;@/app/ui/dashboard/revenue-chart&#39;;
+ import { fetchLatestInvoices, fetchCardData } from &#39;@/app/lib/data&#39;;  // remove fetchRevenue
import { Suspense } from &#39;react&#39;;
import { RevenueChartSkeleton } from &#39;@/app/ui/skeletons&#39;;

export default async function Page() {
-  const revenue = await fetchRevenue // delete this line
  // ...

  return (
    &lt;main&gt;
      &lt;h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}&gt;
        Dashboard
      &lt;/h1&gt;
      &lt;div className=&quot;grid gap-6 sm:grid-cols-2 lg:grid-cols-4&quot;&gt;
        {/* ...Cards */}
      &lt;/div&gt;
      &lt;div className=&quot;mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8&quot;&gt;
+        &lt;Suspense fallback={&lt;RevenueChartSkeleton /&gt;}&gt;
+          &lt;RevenueChart /&gt;
+        &lt;/Suspense&gt;
        {/* ...*/}
      &lt;/div&gt;
    &lt;/main&gt;
  );
}</code></pre>
<p>특정 컴포넌트의 요청이 끝날 때까지 전체 로딩하던 화면에서 특정 컴포넌트만 지연 로딩되는 화면으로 바뀌었다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/f83f1566-0ccf-4c42-b146-5b9dce2d7fe6/image.gif" alt="챕터 8 스트리밍 서스펜스" title="챕터 8 스트리밍 서스펜스"></p>
<h3 id="8-3-deciding-where-to-place-your-suspense-boundaries">8-3. Deciding where to place your Suspense boundaries</h3>
<p><code>Suspense</code>의 경계는 원하는 사용자 경험, 콘텐츠 우선순위, 컴포넌트가 의존하는 데이터 패칭에 따라 달라진다. 정답은 없지만 일반적으로 데이터가 필요한 컴포넌트를 <code>Suspense</code>로 감싸는 게 낫고, 필요한 경우 전체 페이지를 스트리밍한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] 공식 문서만 보고 Next.js 익히기(2)]]></title>
            <link>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B01-dim2olj6</link>
            <guid>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B01-dim2olj6</guid>
            <pubDate>Thu, 30 Nov 2023 01:12:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공식 문서 보기를 돌같이 하는 버릇을 고치자!</p>
</blockquote>
<h2 id="3-optimizing-fonts-and-images">3. Optimizing Fonts and Images</h2>
<p>이 장에서는 <code>next/font</code>와 <code>next/image</code> 적용과 최적화 방법을 배운다.</p>
<h3 id="3-1-why-optimize-fonts">3-1. Why optimize fonts?</h3>
<p>기본 폰트가 아닌 사용자 정의 폰트는 외부에서 로드하는 시간이 필요하다. 그 과정에서 <a href="https://web.dev/articles/cls?hl=ko">Cumulative Layout Shift(CLS)</a> 점수를 높여 나쁜 사용자 경험을 제공할 수도 있다.</p>
<blockquote>
<p><strong>Cumulative Layout Shift</strong>란, 구글에서 레이아웃 변경을 측정하는 지표이다. 요소의 이동이나 변경이 많을수록 높은 점수가 측정된다.</p>
</blockquote>
<p><code>Next</code>에서는 <code>next/font</code>를 이용해 빌드 시점에 폰트를 다운로드하여 자동 최적화한다.</p>
<h3 id="3-2-adding-a-primary-font">3-2. Adding a primary font</h3>
<p><code>next/font/google</code>에서 <code>Inter</code> 폰트를 가져온다.</p>
<pre><code class="language-ts">// app/ui/fonts.ts
import { Inter } from &#39;next/font/google&#39;;

export const inter = Inter({ subsets: [&#39;latin&#39;] });</code></pre>
<p>폰트를 <code>layout</code>의 <code>body</code>에 추가하여 기본 폰트로 설정한다.</p>
<pre><code class="language-tsx">import { inter } from &#39;@/app/ui/fonts&#39;;

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body className={`${inter.className} antialiased`}&gt;{children}&lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
<p><code>antialiased</code>는 <code>Tailwind</code> 코드로, 폰트를 부드럽게 처리하는 클래스이다. 멋지라고 넣었다고 한다.</p>
<h3 id="3-3-why-optimize-images">3-3. Why optimize images?</h3>
<p>이미지 파일을 수동으로 지정할 경우 다양한 부분을 고려하기 어려워 진다.</p>
<ul>
<li>다양한 화면 크기에서 이미지가 반응하는지 확인해야 한다.</li>
<li>다양한 디바이스에 맞는 이미지 크기를 지정해야 한다.</li>
<li>이미지 로드 시 레이아웃 이동을 방지해야 한다.</li>
<li>사용자 뷰포트 외부에 있는 이미지를 지연 로드해야 한다.</li>
</ul>
<p>이런 포인트를 <code>next/image</code> 모듈의 <code>&lt;Image&gt;</code> 컴포넌트를 이용해 자동으로 최적화한다.</p>
<ul>
<li>이미지가 로드될 때 레이아웃이 자동으로 바뀌는 것을 방지</li>
<li>뷰포트가 작은 기기에 큰 이미지가 전송되지 않도록 이미지 크기 조정</li>
<li>기본적으로 이미지 지연 로딩(이미지가 뷰포트에 들어올 때 로드됨)</li>
<li>브라우저에서 지원하는 경우 WebP 및 AVIF와 같은 최신 형식의 이미지 제공</li>
</ul>
<pre><code class="language-tsx">import Image from &#39;next/image&#39;;

export default function Page() {
  return (
    // ...
    &lt;div className=&quot;flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12&quot;&gt;
      &lt;Image
        src=&quot;/hero-desktop.png&quot;
        width={1000}
        height={760}
        className=&quot;hidden md:block&quot;
        alt=&quot;Screenshots of the dashboard project showing desktop version&quot;
      /&gt;
    &lt;/div&gt;
    //...
  );
}</code></pre>
<p>이미지가 로드되는 동안 레이아웃이 변경되지 않도록 <code>width</code>와 <code>height</code>를 지정하고, 원본과 같은 비율로 설정하는 것이 좋다.</p>
<p><code>width</code>와 <code>height</code>를 비율에 맞춰 자동으로 설정하려면 이미지를 import해 이미지 컴포넌트에 제공한다.</p>
<pre><code class="language-tsx">import Image from &#39;next/image&#39;;
import heroMobile from &#39;../public/hero-mobile.png&#39;;

export default function Page() {
  return (
    // ...
    &lt;div className=&quot;flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12&quot;&gt;
      &lt;Image
        src={heroMobile}
        className=&quot;block md:hidden&quot;
        alt=&quot;Screenshots of the dashboard project showing mobile version&quot;
      /&gt;
    &lt;/div&gt;
    //...
  );
}</code></pre>
<p>이미지나 폰트 최적화에 관한 자세한 정보는 <a href="https://nextjs.org/docs/app/building-your-application/optimizing/images">Image Optimization</a>과 <a href="https://nextjs.org/docs/app/building-your-application/optimizing/fonts">Font Optimization</a>를 참고한다.</p>
<h2 id="4-creating-layouts-and-pages">4. Creating Layouts and Pages</h2>
<p>새로운 페이지를 만드는 장이다. 파일 시스템 라우트를 사용하여 파일과 폴더의 역할과 <code>layout</code>을 이해하는 것이 주 목표이다.</p>
<h3 id="4-1-nested-routing">4-1. Nested routing</h3>
<p>파일 시스템 라우팅에서 폴더는 <code>URL</code>의 path에 따라 구분하는 역할을 한다. 최상위인 <code>app</code>은 <code>root(/)</code>를 의미하고, 하위 폴더들은 <code>pathname</code>을 의미한다. 즉, <code>/</code>로 구분되는 영역이다. 예를 들어, <code>localhost / dashboard / invoices</code> 경로라면, <code>🗂app/🗂dashboard/🗂invoices</code>이다.</p>
<p>각각의 폴더는 <code>layout.tsx</code>과 <code>page.tsx</code> 파일을 갖는다. <code>page.tsx</code>는 라우트에 해당하는 view 컴포넌트를 내보내는 역할을 한다. <code>🗂app/🗂dashboard/page.tsx</code>를 만들고 <code>localhost:3000/dashboard</code>에 접근하면 해당 컴포넌트가 렌더링된 화면을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/1d11c97c-270d-43b3-83d0-d590ee9b13cf/image.png" alt="챕터4 dashboard page" title="챕터4 dashboard page"></p>
<p><code>🗂app/🗂dashboard/sidebar.tsx</code>라는 파일을 만들었을 때, <code>/dashboard/sidebar</code>로 접근할 수 있는 게 아닌가 하는 의문이 들었다. 하지만 <code>Next.js</code>에서는 <code>page</code>와 ui 컴포넌트, 테스트 파일 등이 공존 가능한 <a href="https://nextjs.org/docs/app/building-your-application/routing#colocation">colocation</a>을 허용한다. 오직 <code>page.tsx</code> 파일이 있어야 라우트로 접근할 수 있다.</p>
<h3 id="4-2-layout">4-2. layout</h3>
<p><code>layout.tsx</code> 파일은 여러 페이지에서 같은 레이아웃을 공유하는 역할이다. <code>layout</code>의 이점 중 하나는 다른 페이지 컴포넌트를 업데이트할 때 <code>layout</code>은 리렌더링을 하지 않는다는 것이다. <a href="https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#3-partial-rendering">partial rendering</a>이라고 부른다고 한다.</p>
<p>나의 실행 환경에서는 페이지 이동마다 <code>layout</code>도 리렌더링되던데...게다가 <code>SPA</code>가 아닌 <code>MPA</code>처럼 동작한다. 뭔가 조치가 더 필요한 건가? 약간의 의문 추가.</p>
<p><code>🗂app/layout.tsx</code>는 <strong>Root layout</strong>으로, 모든 페이지가 공유하는 필수 레이아웃이다. 여기에서 <code>&lt;html&gt;</code>과 <code>&lt;body&gt;</code> 태그를 수정하거나 <code>metadata</code>를 추가할 수도 있다.</p>
<h2 id="5-navigating-between-pages">5. Navigating Between Pages</h2>
<p>페이지 간의 이동을 다루는 장이다. 위에서 품었던 의문이 곧바로 해결되었다.</p>
<h3 id="51-link-component">5.1 Link Component</h3>
<p><code>dashboard</code>의 하위 페이지 간 이동은 <code>&lt;a&gt;</code> 태그를 이용하고 있었다. 이동마다 전체 페이지가 새로고침되는 원인이었다. <code>next/link</code>의 <code>&lt;Link&gt;</code> 컴포넌트로 대체하면 새로고침 없이 이동한다.</p>
<pre><code class="language-tsx">import Link from &#39;next/link&#39;;

export default function NavLinks() {
  return (
    &lt;&gt;
      {/* ... */}
      &lt;Link key={link.name} href={link.href}&gt;
        &lt;LinkIcon className=&quot;w-6&quot; /&gt;
        &lt;p className=&quot;hidden md:block&quot;&gt;{link.name}&lt;/p&gt;
      &lt;/Link&gt;
      {/* ... */}
    &lt;/&gt;
  );
}</code></pre>
<h3 id="52-automatic-code-splitting-and-prefetching">5.2 Automatic code-splitting and prefetching</h3>
<p><code>Next.js</code>는 자동으로 코드를 분할한다. 분할된 코드는 고립되었다는 의미이며, 이 페이지에서 에러가 발생해도 다른 페이지는 정상 동작한다. 또한, <code>&lt;Link&gt;</code> 컴포넌트가 동작할 때마다 백그라운드에서 라우트의 코드를 <code>prefetch</code>한다. 백그라운드에서 미리 로드된 목적지 페이지는 사용자가 클릭했을 때 거의 즉시 전환된다.</p>
<h3 id="53-pattern-showing-active-links">5.3 Pattern: Showing active links</h3>
<p>일반적인 UI는 현재 페이지와 같은 링크를 활성화 상태로 보여준다. 그러기 위해서 현재 <code>pathname</code>을 알아야 한다. <code>next/navigation</code>의 <code>usePathname</code>을 사용해 <code>pathname</code>에 접근한다.</p>
<pre><code class="language-tsx">&#39;use client&#39;;

import { usePathname } from &#39;next/navigation&#39;;</code></pre>
<p>훅은 <code>Client Component</code>에서 사용하므로 최상단에 <code>use client</code>를 명시해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] 공식 문서만 보고 Next.js 익히기(1)]]></title>
            <link>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B01</link>
            <guid>https://velog.io/@real-bird/Next.js-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Next.js-%EC%9D%B5%ED%9E%88%EA%B8%B01</guid>
            <pubDate>Sun, 26 Nov 2023 16:43:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공식 문서 보기를 돌같이 하는 버릇을 고치자!</p>
</blockquote>
<p>13버전을 익히기도 전에 14버전이 출시된 <code>Next.js</code>... 마지막으로 공부했던 버전이 12였기에 새로 공부해야 함을 느꼈다. 다행히 공식 문서가 매우매우 잘 되어 있었다. <a href="https://nextjs.org/learn/dashboard-app">Learn Next</a> 페이지를 이용하여 14버전을 익혀보려고 한다.</p>
<h2 id="0-introduction">0. Introduction</h2>
<p>대시보드 앱을 만드는 과정으로, 16장으로 이루어졌다. 모두 익히면 <code>Next.js</code>의 필수 기술을 갖추게 된다고 한다. 다음은 배울 내용이다.</p>
<ul>
<li>Styling: Next.js에서 애플리케이션의 스타일을 지정하는 다양한 방법.</li>
<li>Optimizations: 이미지, 링크 및 글꼴을 최적화하는 방법.</li>
<li>Routing: 파일 시스템 라우팅을 사용하여 중첩 레이아웃과 페이지를 만드는 방법.</li>
<li>Data Fetching: Vercel에서 데이터베이스를 설정하는 방법과 가져오기 및 스트리밍에 대한 모범 사례.</li>
<li>Search and Pagination: URL 검색 매개변수를 사용하여 검색 및 페이지 매김을 구현하는 방법.</li>
<li>Mutating Data: React 서버 액션을 사용하여 데이터를 변경하고 Next.js 캐시를 재검증하는 방법.</li>
<li>Error Handling: 일반 오류와 404 찾을 수 없음 오류를 처리하는 방법.</li>
<li>Form Validation and Accessibility: 서버 측 양식 유효성 검사를 수행하는 방법과 접근성을 개선하기 위한 팁.</li>
<li>Authentication: NextAuth.js 및 미들웨어를 사용하여 애플리케이션에 인증을 추가하는 방법.</li>
<li>Metadata: 메타데이터를 추가하고 소셜 공유를 위해 애플리케이션을 준비하는 방법.</li>
</ul>
<p><strong>React와 자바스크립트에 대한 기본적인 이해가 있다고 가정</strong>하며 <code>Node</code>는 <strong>18.17.0</strong> 이상을 기준으로 한다.</p>
<h2 id="1-getting-started">1. Getting Started</h2>
<p><code>Next</code>의 설치는 <code>npx create-next-app@latest</code>로 하는데, 여기서는 제공하는 예제를 포함하여 설치한다.</p>
<pre><code>npx create-next-app@latest nextjs-dashboard --use-npm --example &quot;https://github.com/vercel/next-learn/tree/main/dashboard/starter-example&quot;</code></pre><p>예제의 <code>app</code> 디렉토리의 구성은 다음과 같다.</p>
<pre><code class="language-shell">├─public
│
├─app
│   │  layout.tsx # 전체 레이아웃 (필수)
│   │  page.tsx # index 페이지 (필수)
│   │
│   ├─lib
│   │   data.ts # 데이터 패치
│   │   definitions.ts # 타입 정의
│   │   placeholder-data.js # 미리 생성한 mockup 데이터
│   │   utils.ts # 화폐 단위, 날짜 형식, 컬럼 설정, 페지네이션 생성 등
│   │
│   └─ui # UI를 구성하는 컴포넌트
│       │  acme-logo.tsx
│       │  button.tsx
│       │  global.css
│       │  login-form.tsx
│       │  search.tsx
│       │  skeletons.tsx
│       │
│       ├─customers
│       │      table.tsx
│       │
│       ├─dashboard
│       │      cards.tsx
│       │      latest-invoices.tsx
│       │      nav-links.tsx
│       │      revenue-chart.tsx
│       │      sidenav.tsx
│       │
│       └─invoices
│             breadcrumbs.tsx
│             buttons.tsx
│             create-form.tsx
│             edit-form.tsx
│             pagination.tsx
│             status.tsx
│             table.tsx
└─scripts
      seed.js # 데이터베이스를 채울 시딩</code></pre>
<p>컴포넌트 이름과 내부의 이름이 깔끔해서 어렵게 고민할 필요가 없었다. 이게 네이밍 센스란 건가? 벌써 하나 배웠다.</p>
<p><code>npm run dev</code>로 실행한 첫 화면이다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/3f88b495-2ace-4297-a629-cd13765b552b/image.png" alt="챕터1 기본 화면" title="챕터1 기본 화면"></p>
<h2 id="2-css-styling">2. CSS Styling</h2>
<p>2장에서는 아무것도 꾸며지지 않은 홈페이지를 어떻게 스타일링하는가를 알려준다.</p>
<h3 id="2-1-global-styling">2-1. Global Styling</h3>
<p>앱 전체에 공용으로 적용하는 스타일은 최상위 루트에서 호출하여 사용한다. <code>Next</code>에서는 <code>app</code> 디렉토리의 바로 아래에 있는 <code>layout.tsx</code>이 <strong>Root Layout</strong>이다.</p>
<p><code>RootLayout</code>에서 <code>global.css</code>를 호출한다.</p>
<pre><code class="language-diff">// layout.tsx
+ import &#39;@/app/ui/global.css&#39;;

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body&gt;{children}&lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
<p><code>Next</code>는 <code>Tailwind CSS</code>를 지원하기 때문에 <code>global.css</code>에는 <code>Tailwind CSS</code>의 기본 지시문을 포함하고 있다.</p>
<pre><code class="language-css">/* global.css*/
@tailwind base;
@tailwind components;
@tailwind utilities;</code></pre>
<p><code>global.css</code>를 적용한 화면은 처음과 달리 보기 편안하다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/ed09eb1b-4d25-4333-8307-b74a3ffd056a/image.png" alt="챕터2 global.css"></p>
<h3 id="2-2-tailwind-css">2-2. Tailwind CSS</h3>
<p>앞서 언급한 <a href="https://tailwindcss.com/">Tailwind CSS</a>는 <strong>className</strong>으로 빠르고 쉽게 스타일을 적용하는 <em>CSS Framework</em>이다. <code>Next</code>에서 기본적으로 지원하며, <code>create-next-app</code>으로 세팅 시 사용 여부를 묻는다. <code>yes</code>를 선택하면 필요한 자원을 알아서 설치해준다.</p>
<pre><code class="language-tsx">&lt;Link
  href=&quot;/login&quot;
  className=&quot;flex items-center gap-5 self-start rounded-lg bg-blue-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-blue-400 md:text-base&quot;
&gt;
  &lt;span&gt;Log in&lt;/span&gt; &lt;ArrowRightIcon className=&quot;w-5 md:w-6&quot; /&gt;
&lt;/Link&gt;</code></pre>
<p>위 이미지에서 보이는 <code>Log in</code> 버튼의 코드이다. 개인적으로 제일 선호하는 스타일링 시스템이기도 하다. <code>config</code> 파일에서 <strong>커스텀 클래스</strong>를 만들 수도 있고, 수치가 적용되는 클래스명에 직접 설정할 수도 있다.</p>
<pre><code class="language-js">// tailwind.config.js

import type { Config } from &#39;tailwindcss&#39;;

const config: Config = {
  theme: {
    extend: {
      // custom color를 세팅한다
      colors: {
        blue: {
          400: &#39;#2589FE&#39;,
          500: &#39;#0070F3&#39;,
          600: &#39;#2F6FEB&#39;,
        },
      },
    },
  },
};
export default config;

// temp.jsx
&lt;p className=&quot;p-[10px]&quot;&gt;패딩 수치 직접 설정&lt;/p&gt;</code></pre>
<h3 id="2-3-css-modules">2-3. CSS Modules</h3>
<p><code>CSS Modules</code>는 CSS의 범위를 컴포넌트 범위로 축소해 더 쉽게 스타일을 관리하고 충돌을 방지하는 이점을 제공한다. <code>*.module.css</code>로 생성한 후 <code>import styles from &quot;./temp.module.css&quot;</code>로 호출했다. <code>className={styles.wrapper}</code>으로 적용했다.</p>
<pre><code class="language-tsx">import styles from &#39;./temp.module.css&#39;;

function Temp() {
  return (
    &lt;div className={styles.wrapper}&gt;
      &lt;h1 className={styles.title}&gt;Hello CSS Modules!&lt;/h1&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>선호하는 방식은 아니다. CSS 작성도 귀찮고, 파일도 늘어나고, 보기도 불편하기 때문이다. 아직 스타일 변화가 잦아 유지보수가 빡세게 필요한 무언가를 만들어 본 경험이 없어서 그럴 수도. 아무튼 지금은 패스.</p>
<h3 id="2-4-clsx">2-4. clsx</h3>
<p><a href="https://www.npmjs.com/package/clsx">clsx</a>는 조건부로 클래스명을 입력할 때 사용할 수 있는 라이브러리이다.</p>
<pre><code class="language-tsx">import clsx from &#39;clsx&#39;;

export default function InvoiceStatus({ status }: { status: string }) {
  return (
    &lt;span
      className={clsx(
        &#39;inline-flex items-center rounded-full px-2 py-1 text-sm&#39;,
        {
          &#39;bg-gray-100 text-gray-500&#39;: status === &#39;pending&#39;,
          &#39;bg-green-500 text-white&#39;: status === &#39;paid&#39;,
        },
      )}
    &gt;
    // ...
)}</code></pre>
<p>value가 <code>truthy</code>일 경우 key로 받은 클래스명을 적용한다.</p>
<h3 id="2-5-other-styling-solutions">2-5. Other styling solutions</h3>
<p>이 외에 <code>CSS-in-JS</code> 방식의 <code>styled-components</code>, <code>styled-jsx</code>, <code>emotion</code> 등이나 <code>SASS</code>가 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] JavaScript는 이상하다]]></title>
            <link>https://velog.io/@real-bird/JS-JavaScript%EB%8A%94-%EC%9D%B4%EC%83%81%ED%95%98%EB%8B%A4</link>
            <guid>https://velog.io/@real-bird/JS-JavaScript%EB%8A%94-%EC%9D%B4%EC%83%81%ED%95%98%EB%8B%A4</guid>
            <pubDate>Wed, 22 Nov 2023 18:13:10 GMT</pubDate>
            <description><![CDATA[<p><code>JS</code>는 자유도(?)가 워낙 높아 이상하다는 것을 알고 있었지만, 막상 <a href="https://jsisweird.com/">JS is Weird</a>를 풀어 보니 절반도 못 맞췄다. 그 충격을 정리한다.</p>
<h1 id="스포일러-주의">스포일러 주의</h1>
<p>문제와 답을 함께 적을 예정이라 스포일러가 될 수 있다. 스크롤을 내리기 전에 풀어보는 편이 좋다.</p>
<h2 id="1-true--false-o">1. true + false (O)</h2>
<blockquote>
<p>answer : 1</p>
</blockquote>
<p>다행히 처음 문제는 쉬웠다. <code>true</code>는 1로, <code>false</code>는 0으로 치환된다. 그러므로 답은 <code>1</code>이다.</p>
<h2 id="2-----length-o">2. [ , , , ].length (O)</h2>
<blockquote>
<p>answer : 3</p>
</blockquote>
<p>배열의 콤마(<code>,</code>)는 빈 요소를 구분한다. 눈으로 보기에는 4칸이지만 길이가 3인 이유는 마지막 콤마가 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Trailing_commas">후행 쉼표(Trailing commas)</a>이기 때문이다. 새 요소나 매개변수, 속성 등을 추가하기 쉽도록 고안된 <code>JS</code> 고유 문법이라고 한다.</p>
<p>이걸 가지고 연산하는 사람은 없겠지만, 연산하는 순간 이상해진다.</p>
<pre><code class="language-js">[ , , , ] + [ , , ] // &quot;,,,&quot;
([,,,] + [,,,]).length // 4
[,,,,].length // 4</code></pre>
<p>배열과 배열을 더하면 배열 요소와 콤마가 합쳐진 문자열이 되는데, 후행 쉼표는 생략된다. 음, 보기 싫은 광경이다.</p>
<h2 id="3-1-2-3--4-5-6-o">3. [1, 2, 3] + [4, 5, 6] (O)</h2>
<blockquote>
<p>answer : &quot;1,2,34,5,6&quot;</p>
</blockquote>
<p>위에서 설명한 바와 같다. <code>3</code>과 <code>4</code>이 합쳐지는 게 보기 불편하다면 다음과 같은 방법이 있다고 알려준다.</p>
<pre><code class="language-js">[1, 2, 3] + [, 4, 5, 6];
[1, 2, 3, &quot;&quot;] + [4, 5, 6];</code></pre>
<p>가장 좋은 방법은 <code>+</code> 연산자를 사용하지 않고 배열을 합치는 것이라고. (진짜 배열을 더하는 사람이 있긴 할까? 의아, 궁금)</p>
<pre><code class="language-js">[...[1, 2, 3], ...[4, 5, 6]];</code></pre>
<h2 id="4-02--01--03-o">4. 0.2 + 0.1 === 0.3 (O)</h2>
<blockquote>
<p>answer : false</p>
</blockquote>
<p>워낙 유명한 문제라 맞췄다. <code>JS</code>의 부동소수점과 관련 있는데 <a href="https://stackoverflow.com/questions/588004/is-floating-point-math-broken">stackoverflow에 자세한 답변</a>이 있다.</p>
<h2 id="5-102-x">5. 10,2 (X)</h2>
<blockquote>
<p>answer : 2</p>
</blockquote>
<p>이 글을 쓰게 된 계기인데, <code>,</code>가 어떻게 연산자인 거냐고! <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Trailing_commas">Comma operator</a>라니... 정말 충격과 공포 그 자체였다. 이제라도 알아서 다행이다.</p>
<h2 id="6--o">6. !!&quot;&quot; (O)</h2>
<blockquote>
<p>answer : false</p>
</blockquote>
<p>빈 문자열은 <code>falsy</code>이고, boolean을 역전시키는 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_NOT">Logical NOT</a>을 사용하면 <code>true</code>가 된다. 한 번 더 <code>NOT</code>을 하면 다시 역전되어 <code>false</code>가 된다.</p>
<h2 id="7--o">7. +!![] (O)</h2>
<blockquote>
<p>answer : 1</p>
</blockquote>
<p>빈 배열은 놀랍게도 <code>truthy</code>이다. 따라서 <code>!!</code>의 값은 <code>true</code>이고, <code>+</code>하면 숫자가 되므로 <code>1</code>이 나온다.</p>
<h2 id="8-parseint00000005">8. parseInt(0.0000005)</h2>
<blockquote>
<p>answer : 5</p>
</blockquote>
<p>어, 음... 정수로 만들어주는 함수 아니었나? 설명을 보면 소수점 아래 7번째부터 달라지는 듯하다. <code>String(0.0000005)</code> 의 값은 <code>5e-7</code>인데, <code>parseInt</code>는 <code>e-7</code>을 스킵한다. <a href="https://dmitripavlutin.com/parseint-mystery-javascript/">Solving a Mystery Behavior of parseInt() in JavaScript</a></p>
<p>세상에 마상에...</p>
<h2 id="9-true--true-x">9. true == &quot;true&quot; (X)</h2>
<blockquote>
<p>answer : false</p>
</blockquote>
<p><code>1 == &quot;1&quot;</code>은 <code>true</code>라며? 그래서 틀렸다. <code>1</code>, <code>&quot;1&quot;</code>, <code>true</code>는 number로 변환되지만, <code>&quot;true&quot;</code>는 <code>NaN</code>이 되기 때문이다. <code>1 == NaN</code>은 <code>false</code>다.</p>
<h2 id="10-010---03-x">10. 010 - 03 (X)</h2>
<blockquote>
<p>answer : 5</p>
</blockquote>
<p><code>0</code>은 자동 생략되어서 <code>10 - 3</code>인줄 알았는데 아니었다. 무려 <code>010</code>은 <strong>8진수</strong>였다! 앞에 <code>0</code>이 붙고 뒤가 7이하의 수라면 <code>JS</code>은 8진수로 세팅한다. 8이상은 10진수로. 이게 똑똑한 건지, 멍청한 건지 모르겠다. 으아아아아ㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏ</p>
<h2 id="11-------x">11. &quot;&quot; - - &quot;&quot; (X)</h2>
<blockquote>
<p>answer : 0</p>
</blockquote>
<p>이게 왜 연산이 되는 건데?! <code>&quot;&quot; - - &quot;&quot;</code>를 보기 쉽게 표현하면 다음과 같다.</p>
<pre><code class="language-js">+&quot;&quot; - -&quot;&quot;;
+0 - -0;</code></pre>
<p>근데 이건 에러다.</p>
<pre><code class="language-js">--&quot;&quot;; // -&gt; SyntaxError</code></pre>
<h2 id="12-null--0-x">12. null + 0 (X)</h2>
<blockquote>
<p>answer : 0</p>
</blockquote>
<p><code>null</code>의 타입이 object여서 <code>NaN</code>이 나올 줄 알았다. 지금 보니 <code>+</code> 연산을 숫자와 할 때는 number로 치환이 가능하냐에 따라 달라지는 듯하다, 라고 하기엔 <code>&quot;&quot; + 1</code>은 <code>&quot;1&quot;</code>인데. <code>true = 1</code>, <code>false = 0</code>, <code>null = 0</code>이라고 외우자.</p>
<h2 id="13-00-x">13. 0/0 (X)</h2>
<blockquote>
<p>answer : NaN</p>
</blockquote>
<p>하도 이상하게 당해서 이것도 이상하게 나오겠지 싶어 <code>Infinity</code>를 선택했지만 아니었다. 정신 나갈 것 같다. ㅋㅋㅋㅋㅋ</p>
<h2 id="14-10--10--1000-x">14. 1/0 &gt; 10 ** 1000 (X)</h2>
<blockquote>
<p>answer : false</p>
</blockquote>
<p><code>10 ** 1000</code>은 유한한 정수여서 <code>Infinity</code>가 더 크다고 생각했다. 하지만 10의 1000승은 마찬가지로 <code>Infinity</code>였다.</p>
<h2 id="15-true-x">15. true++ (X)</h2>
<blockquote>
<p>answer : SyntaxError</p>
</blockquote>
<p>이것까지 증가되지는 않았다. 그래도 양심은 있네.</p>
<h2 id="16----1-x">16. &quot;&quot; - 1 (X)</h2>
<blockquote>
<p>answer : -1</p>
</blockquote>
<p><code>&quot;&quot; + 1&quot;</code>은 문자열이면서 <code>-</code> 연산자는 숫자다.</p>
<h2 id="17-null---0--0-x">17. (null - 0) + &quot;0&quot; (X)</h2>
<blockquote>
<p>answer : &quot;00&quot;</p>
</blockquote>
<p><code>NaN</code>일 줄 알았는데, <code>(null -0)</code>은 <code>0</code>이고 문자열과 더해 <code>&quot;00&quot;</code>이 되었다. 이제 좀 보인다.</p>
<h2 id="18-true--true---0-o">18. true + (&quot;true&quot; - 0) (O)</h2>
<blockquote>
<p>answer : NaN</p>
</blockquote>
<p><code>(&quot;true&quot; - 0)</code>이 <code>NaN</code>이어서 정답을 맞췄다.</p>
<h2 id="19-5--5-o">19. !5 + !5 (O)</h2>
<blockquote>
<p>answer : 0</p>
</blockquote>
<p><code>false + false</code>라고 생각하면 되니까 답은 <code>0</code>. 만약 <code>!0 + !0</code>이었다면 답은 <code>2</code>다.</p>
<h2 id="20----o">20. [] + [] (O)</h2>
<blockquote>
<p>answer : &quot;&quot;</p>
</blockquote>
<p>제일 앞에서 했던 것의 반복이었다.</p>
<h2 id="21-1--2--3-o">21. 1 + 2 + &quot;3&quot; (O)</h2>
<blockquote>
<p>answer : &quot;33&quot;</p>
</blockquote>
<p><code>number + string</code> or <code>string  + number</code>는 <code>string</code>을 반환한다. 앞의 숫자 연산 후 문자열을 더해 <code>&quot;33&quot;</code>이 되었다.</p>
<h2 id="22-typeof-nan-o">22. typeof NaN (O)</h2>
<blockquote>
<p>answer : number</p>
</blockquote>
<p>처음 <code>JS</code>를 배울 때 <code>stirng</code>, <code>object</code> 혹은 <code>NaN</code>이라는 타입이라고 생각했지만, 이것은 <strong>숫자가 아님을 나타내는 <code>number</code> 타입(Not a Number)</strong>이었다. &#39;잉?&#39;했던 느낌을 잊을 수가 없다.</p>
<h2 id="23-undefined--false-o">23. undefined + false (O)</h2>
<blockquote>
<p>answer : NaN</p>
</blockquote>
<p><code>undefined</code>는 <code>undefined</code> 타입이다. 연산이 안 되므로 <code>NaN</code>이 나온다.</p>
<h2 id="24----0-x">24. &quot;&quot; &amp;&amp; -0 (X)</h2>
<blockquote>
<p>answer : &quot;&quot;</p>
</blockquote>
<p>빈 문자열은 <code>falsy</code>니까 <code>false</code> 아닌가? 네, 아닙니다. <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND">Logical AND</a> 연산자는 <code>if</code>문이 아닌 경우 반환값이 Boolean이 아니다. 첫 번째 표현식이 <code>true</code>이면 두 번째 표현식을 반환하고, 아니라면 첫 번째 표현식을 반환한다.</p>
<p>React에서 <code>0 &amp;&amp; &lt;Component /&gt;</code>로 조건부 렌더링했을 때 <code>0</code>이 나오는 경우가 이것 때문인가 보다. <code>0</code>이 <code>falsy</code>이므로 첫 번재 표현식이 반환되는 것.</p>
<h2 id="25-nan--------give-up">25. +!!NaN * &quot;&quot; - - [,] (Give up)</h2>
<blockquote>
<p>answer : 0</p>
</blockquote>
<p>정신 나가서 포기했다. 지금 보니 <code>0</code>인 게 보인다. <code>+!!NaN</code>은 <code>0</code>이다. <code>&quot;&quot; - - [,]</code>는 <code>&quot;&quot; - - &quot;&quot;</code>이고, 이건 위에서 <code>0</code>임을 알았다. 그러므로 <code>0 * 0 = 0</code>이 된다.</p>
<h2 id="결론">결론</h2>
<p>정신이 한 번 나갔지만, 정리하면서 정신줄 붙잡고 &#39;나는 이런 코드 생각도 말아야지&#39;라는 다짐을 하게 되었다. 호기심 해결되는 문제도 있어 도움이 되었다. <code>JS</code>는 많이 이상한 언어지만, 내가 배우고 사용하는 만큼 더 열심히 공부해야겠다. 언젠가 개발자가 되는 그날을 위하여!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React, Vite] 공식 문서만 보고 Vite-React SSR 예제 살펴보기]]></title>
            <link>https://velog.io/@real-bird/React-Vite-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Vite-React-SSR-%EC%98%88%EC%A0%9C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@real-bird/React-Vite-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Vite-React-SSR-%EC%98%88%EC%A0%9C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 15 Nov 2023 12:57:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공식 문서를 돌같이 보는 버릇을 고치자!</p>
</blockquote>
<p><code>CRA</code>나 <code>Vite</code>로 생성한 프로젝트는 <code>CSR(Client Side Rendering)</code>로 실행된다. <code>CSR</code>은 웹을 구성하는 모든 자원을 브라우저에서 다운로드해 실행하기 때문에 서버 부하를 최소화하고, 페이지 전환이나 웹 내의 상호작용이 빠르다는 장점이 있다. 또한, 템플릿이 제공되기 때문에 상대적으로 개발 효율성도 높은 편이다.</p>
<p>하지만 모든 자원을 한 번에 받는 만큼 초기 로딩 속도가 느릴 수밖에 없다. <code>JS</code>가 다운로드되어 실행되기 전까지 사용자는 빈 페이지를 봐야 한다. 여기서 SEO 문제가 유발된다. 렌더링되기 전까지 html은 <code>&lt;div&gt;</code> 단 하나만 가지고 있다. 때문에 검색 엔진 크롤러는 페이지에 대한 아무런 정보도 가지고 가지 못한다. (요즘 구글 크롤러는 JS까지 실행해서 긁어간다는데, 일반적인 내용은 아닌 듯하니 넘어가자.)</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/real-bird/post/33d4ded3-5d0e-4052-b89e-f030bc91671c/image.png" alt="">
preview도 비었고, response의 div도 비었다.</p>
</blockquote>
<p>검색 엔진에 노출되어 유입을 유도하는 입장에서 SEO는 중요한 문제이다. 검색 엔진 크롤러가 페이지의 정보를 잘 긁어가게 하기 위해서는 사이트에 방문했을 때 내용이 담긴 html을 보여줘야 한다. JS가 실행되기 전에 페이지 내용을 담은 자원을 호출하는 방식이 <code>SSR(Server Side Rendering)</code>이다. 서버에서 페이지의 골조를 만들어 보내주기 때문에 초기 로딩 속도가 빠르고 SEO 대응도 원활하다. 대신 <code>CSR</code>과 반대로 서버가 할 일이 많아 부하가 늘어나고, 페이지 전환 시 새로운 html을 생성하므로 전환 속도가 느릴 수 있다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/real-bird/post/e3fc3d00-e6b6-449c-8825-6681ed47e7a5/image.png" alt="">
preview도, reponse의 div로 뭔가 들어있다.</p>
</blockquote>
<p>어쨌든 <code>SSR</code>은 중요하기 때문에 내가 애용하는 <code>Vite</code>에서도 SSR을 지원한다. 스스로 구현하기에는 아직 잘 모르겠으니 <code>Vite</code>에서 제공하는 예제 코드를 살펴보자. React를 기준이다.</p>
<blockquote>
<p>예제 코드는 다음 명령어로 설치할 수 있다.
<code>npm create vite-extra@latest</code>
한국어 공식 문서 예제 코드 깃허브 : <a href="https://github.com/bluwy/create-vite-extra">https://github.com/bluwy/create-vite-extra</a>
영어 공식 문서 <code>React</code> 예제 코드 : <a href="https://github.com/vitejs/vite-plugin-react/tree/main/playground/ssr-react">https://github.com/vitejs/vite-plugin-react/tree/main/playground/ssr-react</a></p>
</blockquote>
<h2 id="packagejson">Package.json</h2>
<pre><code class="language-json">{
  &quot;dependencies&quot;: {
    &quot;compression&quot;: &quot;^1.7.4&quot;,
    &quot;express&quot;: &quot;^4.18.2&quot;,
    &quot;react&quot;: &quot;^18.2.0&quot;,
    &quot;react-dom&quot;: &quot;^18.2.0&quot;,
    &quot;sirv&quot;: &quot;^2.0.3&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@types/react&quot;: &quot;^18.2.28&quot;,
    &quot;@types/react-dom&quot;: &quot;^18.2.13&quot;,
    &quot;@vitejs/plugin-react&quot;: &quot;^4.1.0&quot;,
    &quot;cross-env&quot;: &quot;^7.0.3&quot;,
    &quot;vite&quot;: &quot;^4.4.11&quot;
  }
}</code></pre>
<p>서버에서 렌더링을 해야 하기 때문에 서버 구성 패키지가 눈에 띈다. <code>express</code>는 노드 서버 생성을 위해서, <code>compression</code>은 production 단계에서 리소스를 압축하기 위해서, <code>sirv</code>는 정적 파일을 효율적으로 전달하기 위해 사용한다. <code>cross-env</code>는 실행 환경에 따라 동적으로 env를 변경하기 위해 사용한다.</p>
<p>스크립트도 신기했다.</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;node server&quot;,
    &quot;build&quot;: &quot;npm run build:client &amp;&amp; npm run build:server&quot;,
    &quot;build:client&quot;: &quot;vite build --ssrManifest --outDir dist/client&quot;,
    &quot;build:server&quot;: &quot;vite build --ssr src/entry-server.jsx --outDir dist/server&quot;,
    &quot;preview&quot;: &quot;cross-env NODE_ENV=production node server&quot;
  }
}</code></pre>
<p><code>express</code>를 실행해야 하므로 기존의 CSR 명령어와 다르게 <code>vite</code>가 아닌 <code>node server</code>로 대체되었다. build 역시 서버와 클라이언트로 나뉘었다.</p>
<p><code>build:client</code>의 <code>--ssrManifest</code>는 <a href="https://ko.vitejs.dev/guide/ssr.html#generating-preload-directives">모듈 ID와 관련된 청크 파일이나 에셋 파일에 대한 매핑이 포함</a>된 파일이다.  <code>--outDir</code>은 빌드 결과물이 생성될 디렉토리이다.</p>
<p><code>build:server</code>는 <code>--ssr</code>을 붙여 SSR 빌드임을 명시하고 진입점을 지정한다. 마찬가지로 결과물은 <code>--outDir</code>로 지정한 디렉토리에 생성된다.</p>
<h2 id="indexhtml">index.html</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;

&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot; /&gt;
  &lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/vite.svg&quot; /&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
  &lt;title&gt;Vite + React&lt;/title&gt;
  &lt;!--app-head--&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;div id=&quot;root&quot;&gt;&lt;!--app-html--&gt;&lt;/div&gt;
  &lt;script type=&quot;module&quot; src=&quot;/src/entry-client.jsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;

&lt;/html&gt;</code></pre>
<p>일반 템플릿의 <code>html</code>과 별다를 바 없지만, 주석이 의아했다. 저건 왜 붙어 있는 거지? 의문점은 <code>server.js</code>를 보고 풀렸다. 하지만 맛있는 건 나중에 먹기로 하고, 진입점 파일부터 보도록 하자.</p>
<h2 id="entry-clientjsx">entry-client.jsx</h2>
<pre><code class="language-jsx">import &quot;./index.css&quot;;
import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom/client&quot;;
import App from &quot;./App&quot;;

ReactDOM.hydrateRoot(
  document.getElementById(&quot;root&quot;),
  &lt;React.StrictMode&gt;
    &lt;App /&gt;
  &lt;/React.StrictMode&gt;
);</code></pre>
<p><code>React</code>에서는 서버에서 사전에 만들어진 html 내부에 ReactDom을 그리기 위한 <code>hydrateRoot</code> 함수가 있다. <a href="https://ko.react.dev/reference/react-dom/client/hydrateRoot">React - hydrateRoot</a></p>
<p>서버에서 html을 만들어 보내면 React는 그 문서 위에 ReactDom을 붙여 정적인 파일을 축축(hydrate)하게 만든다. 아무런 이벤트가 없는 문서에 상호작용 가능한 이벤트를 달아주는 것이다.</p>
<h2 id="entry-serverjsx">entry-server.jsx</h2>
<pre><code class="language-jsx">import React from &quot;react&quot;;
import ReactDOMServer from &quot;react-dom/server&quot;;
import App from &quot;./App&quot;;

export function render() {
  const html = ReactDOMServer.renderToString(
    &lt;React.StrictMode&gt;
      &lt;App /&gt;
    &lt;/React.StrictMode&gt;
  );
  return { html };
}</code></pre>
<p>함수 명에서 알 수 있듯이, <code>renderToString</code>은 React 컴포넌트를 문자열로 변환한다. <a href="https://ko.react.dev/reference/react-dom/server/renderToString">React - renderToString</a></p>
<p><code>hydrateRoot</code>를 호출하면 서버에서 생성된 HTML을 상호작용하게 만든다.</p>
<h2 id="serverjs">server.js</h2>
<p>가장 중요하게 보이는 <code>server.js</code>를 살펴보자. 여기서 조건부로 개발 환경과 프로덕션 환경을 나누어 서버를 실행한다.</p>
<h3 id="constants--assets">Constants &amp; Assets</h3>
<pre><code class="language-js">import fs from &quot;node:fs/promises&quot;;

// Constants
const isProduction = process.env.NODE_ENV === &quot;production&quot;;
const port = process.env.PORT || 5173;
const base = process.env.BASE || &quot;/&quot;;

// Cached production assets
const templateHtml = isProduction
  ? await fs.readFile(&quot;./dist/client/index.html&quot;, &quot;utf-8&quot;)
  : &quot;&quot;;
const ssrManifest = isProduction
  ? await fs.readFile(&quot;./dist/client/ssr-manifest.json&quot;, &quot;utf-8&quot;)
  : undefined;</code></pre>
<p>문자열 그대로다. 프로덕션인지 아닌지에 따라 개발 환경과 프로덕션 환경을 구분한다. 프로덕션일 때는 서버 요청마다 <code>html</code>과 <code>ssrManifest</code>를 읽지 않도록 <code>fs</code> 모듈을 이용해 미리 읽어온다. <code>async</code> 없이 사용되는 <code>await</code>은 <code>Top-level Await</code>이라는 새로 추가된 문법이다. <a href="https://github.com/tc39/proposal-top-level-await#use-cases">TC39 - Top-level Await</a></p>
<h3 id="server">Server</h3>
<pre><code class="language-js">import express from &quot;express&quot;;

// Create http server
const app = express();

// Add Vite or respective production middlewares
let vite;
if (!isProduction) {
  const { createServer } = await import(&quot;vite&quot;);
  vite = await createServer({
    server: { middlewareMode: true },
    appType: &quot;custom&quot;,
    base,
  });
  app.use(vite.middlewares);
} else {
  const compression = (await import(&quot;compression&quot;)).default;
  const sirv = (await import(&quot;sirv&quot;)).default;
  app.use(compression());
  app.use(base, sirv(&quot;./dist/client&quot;, { extensions: [] }));
}</code></pre>
<p><code>express</code>로 서버를 생성한다. <code>vite</code>의 역할이 달라졌다. 여기에서 <code>vite</code>는 미들웨어 역할을 하며 프로덕션과 개발 환경을 분리하여 사용한다. <code>appType</code>을 &#39;custom&#39;으로 적는 이유는 vite 자체의 html 제공 로직을 비활성화하기 위함이다. <a href="https://ko.vitejs.dev/guide/ssr.html#setting-up-the-dev-server">Vite - 서버 측 렌더링:개발 서버 구성하기</a></p>
<p>프로덕션 환경에서는 동적으로 <code>compression</code>과 <code>sirv</code>를 호출해 미들웨어를 등록한다.</p>
<h3 id="serve-html">Serve HTML</h3>
<pre><code class="language-js">// Serve HTML
app.use(&quot;*&quot;, async (req, res) =&gt; {
  try {
    const url = req.originalUrl.replace(base, &quot;&quot;);

    let template;
    let render;
    if (!isProduction) {
      // Always read fresh template in development
      template = await fs.readFile(&quot;./index.html&quot;, &quot;utf-8&quot;);
      template = await vite.transformIndexHtml(url, template);
      render = (await vite.ssrLoadModule(&quot;/src/entry-server.jsx&quot;)).render;
    } else {
      template = templateHtml;
      render = (await import(&quot;./dist/server/entry-server.js&quot;)).render;
    }

    const rendered = await render(url, ssrManifest);

    const html = template
      .replace(`&lt;!--app-head--&gt;`, rendered.head ?? &quot;&quot;)
      .replace(`&lt;!--app-html--&gt;`, rendered.html ?? &quot;&quot;);

    res.status(200).set({ &quot;Content-Type&quot;: &quot;text/html&quot; }).end(html);
  } catch (e) {
    vite?.ssrFixStacktrace(e);
    console.log(e.stack);
    res.status(500).end(e.stack);
  }
});</code></pre>
<p>조금씩 구분해서 보도록 하자.</p>
<pre><code class="language-js">const url = req.originalUrl.replace(base, &quot;&quot;);</code></pre>
<p>공식 문서에서는 <code>req.originalUrl</code>만 썼는데, 템플릿에서는 왜 <code>replace</code>를 했는지 모르겠다. 일종의 안전장치인가? 뭔가 <code>base</code>를 지워야 하는 상황이 있기 때문에 <code>replace</code>를 했을 것이다. 근데 지금은 모르겠다.</p>
<pre><code class="language-js">let template;
let render;
if (!isProduction) {
  // Always read fresh template in development
  template = await fs.readFile(&quot;./index.html&quot;, &quot;utf-8&quot;);
  template = await vite.transformIndexHtml(url, template);
  render = (await vite.ssrLoadModule(&quot;/src/entry-server.jsx&quot;)).render;
}</code></pre>
<p><code>fs.readFile</code>로 <code>index.html</code>을 가져온다. <code>vite.transformIndexHtml</code>에서는 HMR(Hot Module Replacement)이 가능하도록 hook을 붙인다. <code>vite.ssrLoadModule</code>는 SSR 진입점을 서버에 알려주고, <code>entry-server.jsx</code>에서 export한 <code>render</code> 함수를 가져온다.</p>
<pre><code class="language-js">else {
  template = templateHtml;
  render = (await import(&quot;./dist/server/entry-server.js&quot;)).render;
}</code></pre>
<p>프로덕션에서는 사전에 로드한 <code>templateHtml</code>을 그대로 사용하고, 빌드된 서버 측 <code>render</code>를 가져온다.</p>
<pre><code class="language-js">const rendered = await render(url, ssrManifest);</code></pre>
<p><code>render</code>에 <code>url</code>과 <code>ssrManifest</code>가 왜 들어가는지도 모르겠다. 내부에서 뭔가 처리를 하는 건가? React 공식 문서의 <code>renderToString</code>에도 별다른 설명이 없는 걸 보니 모종의 이유가 있는 듯한데 그걸 모르겠다. 으으, 답답. 일단 여기도 패스.</p>
<pre><code class="language-js">const html = template
                .replace(`&lt;!--app-head--&gt;`, rendered.head ?? &quot;&quot;)
                .replace(`&lt;!--app-html--&gt;`, rendered.html ?? &quot;&quot;);</code></pre>
<p>개인적으로 여기가 가장 중요해 보였다. <code>index.html</code>에서 보았던 주석은 이곳에서 사용하기 위함이었다. 공식 문서 설명을 보면 주석은 React 코드가 주입될 <strong>placeholder</strong>였다! 예전에 <em>원티드 프리온보딩 7월</em> 강의에서 비슷한 무언가를 본 기억이 났다. <code>Next.js</code>의 SSR에서 <code>B:0</code>, <code>S:0</code> 등의 표시로 주입될 자리를 명시하고 요소를 갈아 끼운다는 점 말이다.</p>
<p>이렇게 자리를 정하고 갈아끼우기 때문에 서버와 클라이언트 간의 DOM 위치를 정확하게 맞춘다.</p>
<pre><code class="language-js">res.status(200).set({ &quot;Content-Type&quot;: &quot;text/html&quot; }).end(html);</code></pre>
<p>완성된 문서의 타입을 지정하고 보내면 SSR 작업이 완료된다.</p>
<h2 id="결과">결과</h2>
<p>빌드 후 결과물 폴더 구조는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/2b7dcfbe-ebc4-46fe-9757-375e4f9f769a/image.png" alt=""></p>
<p><code>&quot;preview&quot;: &quot;cross-env NODE_ENV=production node server&quot;</code> 스크립트를 이용해 실행해 보면 CSR과 달리 문서가 채워진 채로 온다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/01a31b58-40bb-407e-a193-dcc0c4fa7d3b/image.png" alt=""></p>
<p>이렇게만 보면 그다지 어려워 보이지는 않는다. 하지만 응용해서 무언가를 만들면 미지의 에러에 빠지겠지? 새삼 SSR을 편하게 생성하도록 프레임워크를 내주신 여타 분들에게 감사를 전하고 싶어진다.</p>
<hr>
<p><strong>참고</strong>
<a href="https://ko.vitejs.dev/guide/ssr.html">Vite - 서버 측 렌더링(SSR)</a>
<a href="https://github.com/tc39/proposal-top-level-await#use-cases">ECMAScript proposal: Top-level await</a>
<a href="https://github.com/bluwy/create-vite-extra">create-vite-extra</a>
<a href="https://ko.react.dev/reference/react-dom/client/hydrateRoot">React - hydrateRoot</a>
<a href="https://ko.react.dev/reference/react-dom/server/renderToString">React - renderToString</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Currying과 Composition]]></title>
            <link>https://velog.io/@real-bird/JS-Currying-Composition</link>
            <guid>https://velog.io/@real-bird/JS-Currying-Composition</guid>
            <pubDate>Sun, 12 Nov 2023 14:21:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
<ul>
<li><strong>함수형 프로그래밍</strong>은 함수를 <strong>1급 객체</strong>로 취급한다.</li>
<li><strong>1급 객체</strong>는 표현식에 할당할 수 있다.</li>
<li><code>JS</code>는 함수를 <strong>1급 객체</strong>로 취급하고, 변수에 할당하여 평가할 수 있다.</li>
<li>그러므로 <code>JS</code>는 함수형 프로그래밍과 잘 어울린다.</li>
</ul>
</blockquote>
<p><strong>커링(<code>Currying</code>)</strong>과 <strong>컴포지션(<code>Composition</code>)</strong>은 함수형 프로그래밍의 핵심 개념에 속한다. 이 둘을 잘 활용하면 가독성과 재사용성이 높은 코드를 작성할 수 있다고 한다.</p>
<h2 id="커링currying">커링(Currying)</h2>
<p><strong>커링</strong>은 <a href="https://ko.wikipedia.org/wiki/%ED%95%B4%EC%8A%A4%EC%BC%88_%EC%BB%A4%EB%A6%AC">Haskell Curry</a>에서 유래된 함수 개념으로, <strong>여러 인자를 받는 함수를 단일 인자 함수로 변경하는 방식</strong>을 말한다. <a href="https://ko.wikipedia.org/wiki/%EC%BB%A4%EB%A7%81">커링 - 위키백과</a></p>
<p>세 개의 number를 받아 곱한 결과를 반환하는 함수를 예시로 들어 보자.</p>
<pre><code class="language-js">function multiple(a, b, c) {
  return a * b * c;
}</code></pre>
<p>여기서 인자는 서로에게 영향을 끼친다. 만약 특정 상황에서 <code>a</code>는 2로 고정하고 <code>b</code>와 <code>c</code>만 바꾼다고할 때 코드 작성은 귀찮아질 것이다.</p>
<pre><code class="language-js">const multiA = multiple(2, 2, 2);
const multiB = multiple(2, 3, 3);
const multiC = multiple(2, 3, 4);</code></pre>
<p><code>multiple</code>을 커링으로 작성하면 코드가 심플해진다.</p>
<pre><code class="language-js">function multiple(a) {
  return function (b) {
    return function (c) {
      return a * b * c;
    }
  }
}

const fixTwo = multiple(2);
const twoTwo = fixTwo(2)(2);
const threeThree = fixTwo(3)(3);
const threeFour = fixTwo(3)(4);</code></pre>
<p>재사용성도 높아진다.</p>
<pre><code class="language-js">// a는 2로 고정
const fixTwo = multiple(2);
// b를 3으로 고정
const addFixThree = fixTwo(3);
let result = addFixThree(4); // 24
result = addFixThree(5); // 30

// b를 4로 고정
const addFixFour = fixTwo(4);
result = addFixFour(4); // 32
result = addFixFour(5); // 40</code></pre>
<p><code>ES6</code>의 화살표 함수로 표현하면 다음과 같다.</p>
<pre><code class="language-js">const multiple = (a) =&gt; (b) =&gt; (c) =&gt; a * b * c;</code></pre>
<h2 id="컴포지션composition">컴포지션(Composition)</h2>
<p><strong>컴포지션</strong>은 두 개 이상의 함수를 결합해 하나의 함수를 생성하는 과정이다. 유튜브를 보다가 배운 내용으로 정리한다.</p>
<pre><code class="language-js">const user = {
  id: 1,
  firstName: &quot;Real&quot;,
  lastName: &quot;Bird&quot;
}

genFullName(user);
genAddress(user);
removeNames(user);

result = {
  id: 1,
  fullName: &quot;Real Bird&quot;,
  address: &quot;Republic of Korea&quot;
}</code></pre>
<p><code>user</code> 객체가 있고 각각의 함수를 거치면 <code>result</code> 객체가 되어야 한다. 함수를 하나씩 실행하거나 하나의 함수에서 처리할 수도 있지만, 그런 방식은 <em>명령형</em>에 가깝다. 커링을 이용한 컴포지션 함수를 만들면 <em>선언형</em>으로 처리할 수 있다.</p>
<pre><code class="language-js">const compose = (...fns) =&gt; (obj) =&gt; fns.reduce((c, fn) =&gt; fn(c), obj);</code></pre>
<p><code>ES6+</code>의 스프레드 문법으로 여러 개의 인자를 받는다. 그 후 커링으로 객체를 받고, <code>reduce</code> 함수를 이용해 이전 함수 실행 결과를 다음 함수의 인자로 주입한다.</p>
<pre><code class="language-js">const genFullName = (user) =&gt; {
  return {
    ...user,
    fullName:`${user.firstName} ${user.lastName}`
  }
}

const genAddress = (user) =&gt; {
  return {
    ...user,
    address: &quot;Republic of Korea&quot;
  }
}

const removeNames = (user) =&gt; {
  delete user.firstName;
  delete user.lastName;

  return user;
}

const result = compose(genFullName, genAddress, removeNames);

console.log(result(user));

/*
user {
  id: 1,
  fullName: &quot;Real Bird&quot;,
  address: &quot;Republic of Korea&quot;
}
*/</code></pre>
<h3 id="es5-문법으로-작성하기">ES5 문법으로 작성하기</h3>
<p>위 코드는 <code>ES6</code> 문법을 주로 사용하여 작성했다. 참고한 영상에서 해당 코드를 <code>ES5</code> 문법으로 작성해 보라고 하여 한 번 작성해 봤다.</p>
<pre><code class="language-js">var user = {
  id: 1,
  firstName: &quot;Real&quot;,
  lastName: &quot;Bird&quot;
}

function compose() {
  var fns = arguments;
  return function (obj) {
    var result;
    for (var i = 0; i &lt; fns.length; i = i + 1){
      result = fns[i](obj);
    }
    return result;
  }
}

function genFullName(user) {
  user.fullName = user.firstName + &quot; &quot; + user.lastName;
  return user;
}

function genAddress(user) {
  user.address = &quot;Republic of Korea&quot;;
  return user;
}

function removeNames(user) {
  delete user.firstName;
  delete user.lastName;

  return user;
}

var result = compose(genFullName, genAddress, removeNames);

console.log(result(user));</code></pre>
<p><code>ES6</code> 문법을 싹 걷어냈다.</p>
<p><code>const</code> 대신 <code>var</code>, 화살표 함수(<code>()=&gt;{}</code>) 대신 <code>function</code>을 사용했다.</p>
<p>스프레드(<code>...fns</code>) 문법도 사용할 수 없기 때문에 <code>compose</code>에서 받는 인자들은 <code>arguments</code>를 이용했다. 객체는 몽키 패치로 새 속성을 추가했다.</p>
<p><code>reduce</code>는 es5 빌트인 메서드라 사용해도 괜찮지만, <code>reduce</code>를 사용하지 않는 조건이 있다고 쳤다. 나는 반복문을 돌려 <code>fns</code>의 함수들을 실행했다.</p>
<p>영상에서는 <strong>재귀</strong>를 이용해 <code>compose</code>를 구현하더라.</p>
<pre><code class="language-js">function compose() {
  var fns = arguments;
  return function rfn(obj, i) {
    i = i || 0;
    if (i &lt; fns.length) {
      return rfn(fns[i](obj), i + 1);
    }
    return obj;
  }
}</code></pre>
<h2 id="결론">결론</h2>
<p>커링과 컴포지션에 대해 듣기만 했을 뿐, 무엇인지 전혀 알지 못했다. 우연한 계기로 유튜브 영상을 보고 아주 유용하고 중요한 개념 하나를 배웠다. 익숙해지도록 노력해야지.</p>
<hr>
<p><strong>참고</strong>
<a href="https://youtu.be/jlLTcYdjo9I?si=zffXQR2Z0PCIKygC">@시코 - React 이론 4강 - 함수형 프로그래밍의 백미, Currying과 Composition</a>
<a href="https://youtu.be/7afBIdMcCDU?si=FJkSveChTfHiTCmS">@시코 - React 이론 5강 - Currying, composition 풀이 및 첨언</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] Media Stream Cleanup 하기(feat. react-webcam)]]></title>
            <link>https://velog.io/@real-bird/React-Media-Stream-Cleanup-%ED%95%98%EA%B8%B0feat.-react-webcam</link>
            <guid>https://velog.io/@real-bird/React-Media-Stream-Cleanup-%ED%95%98%EA%B8%B0feat.-react-webcam</guid>
            <pubDate>Wed, 08 Nov 2023 09:17:02 GMT</pubDate>
            <description><![CDATA[<p><code>Media Capture and Streams API</code>를 이용해 웹캠이나 모바일 카메라를 조작하는 기능을 가지고 여차저차 개발을 했다. 카메라를 켜는 것까지는 성공했지만, 이후가 문제를 발견했다.</p>
<p>캠을 동작하면 브라우저에는 <strong>카메라 켜짐</strong> 표시가 나타난다. 그리고 페이지를 벗어나면 <strong>표시가 꺼지기를 기대</strong>한다. 하지만 나의 문제는 그 표시가 꺼지지 않는다는 것이었다. 실질적으로 화면이 촬영되지 않는다고 해도 카메라 켜짐 표시가 계속 떠있는 건 찝찝했다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/36ad35d4-96d8-4ab2-a172-2d624c689976/image.png" alt=""></p>
<p>분명 video 요소가 사라졌음에도 캠 동작 표시는 온전히 남아있다. 이걸 해결하고자 다분히 노력했다.</p>
<h2 id="노력1-cleanup-function에서-처리하기">노력1. Cleanup Function에서 처리하기</h2>
<p><code>cleanup function</code>은 컴포넌트가 언마운트될 때 참조하거나 실행될 수 있는 코드를 정리하는 기능이다. <code>useEffect</code>에서 <code>return</code> 뒤에 함수를 작성하면 되고, 클래스 컴포넌트의<code>componentWillUnmount</code>와 같다.</p>
<p>그렇게 생각해서 <code>useRef</code>에 담은 <code>video element</code>를 비우는 작업을 cleanup에서 처리했다. 물론 <strong>실패</strong>했다.</p>
<h2 id="노력2-media-stream-분리">노력2. Media Stream 분리</h2>
<p>cleanup에서 캠 동작을 멈추는 것은 올바랐다. 문제는 내부 로직이 어떻게 동작하느냐였다. 하지만 어떻게 해야 할지 감이 안 잡혀 무지성으로 ChatGPT한테 질문했더니 지역 변수에 담아보라는 조언을 얻었다. 해서 <code>useState</code>를 이용해 <code>stream</code>을 담았고, 멈추는 동작을 할 때 <code>state</code>를 비웠다. 이렇게 분리하니 원하는대로 cleanup에서 캠이 멈추는 것처럼 보였다.</p>
<p>그러나 다시 재진입하면 이전 문제가 반복되었다. 도저히 모르겠어서 잠시 포기하는 시간을 가졌다.</p>
<h2 id="노력3-react-webcam-참고하기">노력3. react-webcam 참고하기</h2>
<p>해결 방법을 찾아 이러저리 검색을 하다가 <code>react-webcam</code> 라이브러리를 써볼까 하는 고민에 빠졌다. 테스트용으로 설치해보니까 내가 겪은 문제는 아무것도 아니라는 듯 너무 잘 동작했기 때문이다. 그러나 단순히 카메라 화면만 보이면 되기에 스스로 구현해보고 싶은 욕심이 커서 라이브러리 사용은 포기했다. 대신 코드만 참고했다.</p>
<p>나에게 필요한 부분은 stream을 멈추는 부분이었다.</p>
<pre><code class="language-tsx">componentWillUnmount() {
  this.unmounted = true;
  this.stopAndCleanup();
}</code></pre>
<p><code>react-webcam</code>은 클래스 컴포넌트여서 <code>componentWillUnmount</code>로 cleanup했다. <code>unmounted</code>는 혹여나 언마운트되었을 때 media stream을 참조하게 되는 경우 해제하기 위함으로 보였다.</p>
<pre><code class="language-tsx">private stopAndCleanup() {
  const { state } = this;

  if (state.hasUserMedia) {
    Webcam.stopMediaStream(this.stream);

  // (...)
  }
}</code></pre>
<p>media stream이 동작 중일 때 현재의 media stream을 받는 <code>stopMediaStream</code>을 실행한다. 여기서도 video의 srcObject와 media stream을 분리한 것을 확인할 수 있었다. 내 접근법 자체가 잘못된 건 아니었다.</p>
<pre><code class="language-tsx">private static stopMediaStream(stream: MediaStream | null) {
    if (stream) {
      if (stream.getVideoTracks &amp;&amp; stream.getAudioTracks) {
        stream.getVideoTracks().map(track =&gt; {
          stream.removeTrack(track);
          track.stop();
        });
        stream.getAudioTracks().map(track =&gt; {
          stream.removeTrack(track);
          track.stop()
        });
      } else {
        ((stream as unknown) as MediaStreamTrack).stop();
      }
    }
  }</code></pre>
<p>기본적은 stream 제거 코드다. media stream의 참조를 완전히 끊기 위해서 <code>stop</code> 뿐만 아니라 <code>removeTrack</code>까지 작성한 것으로 보인다.</p>
<p>보면서 의아했던 건 <code>requestUserMediaId</code>의 존재였다. mediaId가 왜 필요한 걸까? 바로 <code>React.StrictMode</code> 때문이었다.</p>
<p>리액트는 순수 함수로 이뤄지기 때문에 재실행해도 같은 UI를 렌더링해야 한다. 개발 단계에서는 <code>React.StrictMode</code>를 통해 이를 검증한다. 그러나 media stream은 외부 api이기 때문에 2번 실행했을 경우 같은 결과를 보장하지 않는다. video에 주입한 1번 media stream이 있고, 추후 실행된 2번 media stream이 있는 것이다. 이를 구분하기 위해서 렌더링에 영향이 없는 변수에 media stream 실행 때마다 <code>requestUserMediaId</code>를 증가시켜 구분하는 것이었다.</p>
<pre><code class="language-tsx">private requestUserMedia() {
  // ...
  this.requestUserMediaId++
  const myRequestUserMediaId = this.requestUserMediaId

  navigator.mediaDevices
    .getUserMedia(constraints)
    .then(stream =&gt; {
    if (this.unmounted || myRequestUserMediaId !== this.requestUserMediaId) {
      Webcam.stopMediaStream(stream);
    } else {
      this.handleUserMedia(null, stream);
    }
  })
    .catch(e =&gt; {
    this.handleUserMedia(e);
  });
  // ...
}
</code></pre>
<p><code>navigator.mediaDevices.getUserMedia</code>는 <code>requestUserMedia</code>가 실행되는 시점에 생성된<code>myRequestUserMediaId</code>를 가지지만, <code>requestUserMediaId</code>는 외부에서 생성되었으므로 최신값을 계속 유지한다. 이후 비동기로 실행되며 이전에 실행되었던 media stream은 <code>stopMediaStream</code>의 단계를 거치게 된다. 이로 인해 <code>react-webcam</code>은 <code>React.StrictMode</code>에서도 원활하게 media stream이 제거된다.</p>
<h2 id="노력4-내-코드에-적용">노력4. 내 코드에 적용</h2>
<p>media stream을 <code>useState</code>에 저장했던 과정은 렌더링에 영향을 주기도 하고, media stream에 대한 올바른 참조가 아니었다. 때문에 렌더링에 영향이 없으면서 참조가 될 수 있도록 <code>useRef</code>에 media stream을 저장했다.</p>
<pre><code class="language-tsx">export default function Webcam(){
  const webcamRef = useRef&lt;HTMLVideoElement&gt;(null);
  const mediaStreamRef = useRef&lt;MediaStream|null&gt;(null);

  const playStream = () =&gt; {
    navigator.mediaDevices.getUserMedia({video:true}).then(stream =&gt; {
      mediaStreamRef.current = stream;
      if (webcamRef.current) {
          webcamRef.current.srcObject = stream;
      }
    });
  }

  return (
    &lt;video ref={webcamRef} autoPlay playsInline width={300} height={300}&gt;&lt;/video&gt;
    )
}</code></pre>
<p>제거하는 작업은 다음과 같이 구현했다.</p>
<pre><code class="language-tsx">export default function Webcam(){
  // ...
  const mediaStreamRef = useRef&lt;MediaStream|null&gt;(null);

  const playStream = () =&gt; {
    // ...
  }

  const stopStream = (stream:MediaStream|null) =&gt; {
    if (stream) {
      stream.getTracks().forEach(track =&gt; {
        stream.removeTrack(track);
        track.stop();
      });
    }
  }

  return //...
}</code></pre>
<p>언마운트될 때 <code>stopStream</code>이 실행되도록 cleanup을 작성했다.</p>
<pre><code class="language-tsx">export default function Webcam(){
  const webcamRef = useRef&lt;HTMLVideoElement&gt;(null);
  const mediaStreamRef = useRef&lt;MediaStream|null&gt;(null);

  const playStream = () =&gt; {
     // ...
  }

  const stopStream = (stream:MediaStream|null) =&gt; {
    // ...
  }

    useEffect(() =&gt; {
      playStream();
    return ()=&gt;{
      if (mediaStreamRef.current) {
      stopStream(mediaStreamRef.current);
      }
    }
  },[])

  return // ...
}</code></pre>
<p>프로젝트에서는 <code>StrictMode</code> 대비를 몰라 적용하지 않았지만, 재현 코드에서는 한 번 적용해봤다.</p>
<pre><code class="language-tsx">export default function Webcam(){
  const webcamRef = useRef&lt;HTMLVideoElement&gt;(null);
  const mediaStreamRef = useRef&lt;MediaStream|null&gt;(null);
  const requestMediaId = useRef&lt;number&gt;(0);

  const playStream = () =&gt; {
       requestMediaId.current++;
    const myRequestMediaId = requestMediaId.current;

    navigator.mediaDevices.getUserMedia({video:true}).then(stream =&gt; {
      if (myRequestMediaId !== requestMediaId.current) {
        stopStream(stream);
      } else {
          mediaStreamRef.current = stream;
        if (webcamRef.current) {
          webcamRef.current.srcObject = stream;
        }
      }
    });
  }

  const stopStream = (stream:MediaStream|null) =&gt; {
    // ...
  }

  useEffect(() =&gt; {
    // ...
  },[])

  return // ...
}</code></pre>
<h2 id="느낀점">느낀점</h2>
<p>포기하지 않고 계속 머릿속에 굴리다 보니 어찌어찌 해결을 위한 단초를 떠올렸다. 검색을 아무리 해도 내가 원하는 방법은 나오지 않아 정말 힘들었지만, 멋진 라이브러리 덕분이었다.</p>
<p>라이브러리의 코드도 뜯어보는 계기도 되었다. 처음으로 진지하게 하나하나 살펴봤다. 이전에도 다른 라이브러리 코드를 보긴 했지만, 너무 커서 내 머리가 못 따라가더라. 하지만 <code>react-webcam</code>은 컴포넌트 하나만 있는 작은 라이브러리여서 부담 없이 볼 수 있었다. 좋은 교보재였다.</p>
<p>또한, 외부 API를 사용할 때는 <code>React.StrictMode</code>를 염두에 둬야 한다는 것을 배웠다. 지우면 해결되지만, 해당 API를 사용할 때마다 지우고 다시 작성하고 할 수는 없으니까.</p>
<p><strong>몇 개월 간 고민했던 부분</strong>인데 드디어 해결해 다행이다.</p>
<h2 id="demo">Demo</h2>
<p>!codesandbox[webcam-test-4jvvys?fontsize=14&amp;hidenavigation=1&amp;theme=dark]</p>
<hr>
<p><strong>참고</strong>
<a href="https://github.com/mozmorris/react-webcam/tree/master">React Webcam GitHub</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Zustand] 공식 문서만 보고 Zustand 적용해 보기]]></title>
            <link>https://velog.io/@real-bird/Zustand-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Zustand-%EC%A0%81%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@real-bird/Zustand-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Zustand-%EC%A0%81%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 30 Oct 2023 13:11:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공식 문서를 돌같이 보는 버릇을 고치자!</p>
</blockquote>
<h2 id="0-개요">0. 개요</h2>
<p>그동안 내가 한 프로젝트는 상태관리 라이브러리를 사용하지 않았다. React가 제공하는 <code>Context API</code>와 <code>useReducer</code>를 조합하면 충분히 전역 상태관리가 되었기 때문이다. 규모가 매우매우 작기에 가능한 부분이었다. 하지만 언제까지고 모른 채로 지낼 수는 없었다. 게다가 지난 번에 <code>Redux Toolkit</code>을 사용해 보면서 라이브러리의 편리함을 절감했다. 해서 이참에 다양한 상태관리 라이브러리를 체험해 보고자 했다.</p>
<p><code>RTK</code>에 이은 라이브러리는 <code>Zustand</code>이다. 대표적인 상태관리 라이브러리들 중 <strong>weekly downloads</strong> 3위이고, 개인적으로 채용 공고에서도 많이 봐왔기 때문이다. 문서도 깔끔하게 정리되어 있고, 러닝 커브도 낮은 게 마음에 들었다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/real-bird/post/5d7f29af-eed9-47ea-9fda-f682e6a6992e/image.png" alt="상태관리 라이브러리 순위" title="상태관리 라이브러리 순위">
상태관리 라이브러리 순위
<a href="https://npmtrends.com/@reduxjs/toolkit-vs-jotai-vs-mobx-vs-recoil-vs-redux-vs-zustand">https://npmtrends.com/@reduxjs/toolkit-vs-jotai-vs-mobx-vs-recoil-vs-redux-vs-zustand</a></p>
</blockquote>
<h2 id="1-zustand">1. Zustand</h2>
<p><code>Zustand</code>는 <code>상태</code>이라는 뜻의 독일어이다. 공식 문서에 따르면, <a href="https://docs.pmnd.rs/zustand/getting-started/introduction"><strong>작고 빠르며 확장 가능한 베어본 상태 관리 솔루션</strong></a>이다. <code>Redux</code>와 크게 차이나진 않지만, <code>provider</code>가 필요 없다는 점과 <code>action</code>이 없어도 상태 변경이 가능하다는 점이 다르다. 나머지는 마찬가지로 <code>store</code>를 생성하고, <code>selector</code>를 이용해 상태를 호출한다. <a href="https://docs.pmnd.rs/zustand/getting-started/comparison">Comparison</a></p>
<p>대표적으로 권장하는 패턴은 하나의 <code>store</code>를 두고, <code>set/setState</code>를 사용하여 상태를 변경하는 것이다. 혹은 <code>Redux</code>에 익숙하다면 그와 비슷한 패턴으로 만들어 사용할 수도 있다. <a href="https://docs.pmnd.rs/zustand/guides/flux-inspired-practice">Flux inspired practice</a></p>
<h2 id="2-create-store">2. Create Store</h2>
<p><code>TypeScript</code> 환경에서 사용하므로 곧장 <a href="https://docs.pmnd.rs/zustand/guides/typescript">TypeScript Guide</a>로 진입했다. <code>state</code>의 타입을 정의하고 <code>create</code>의 제네릭에 주입한다. 주의할 점은 <code>create&lt;T&gt;()</code>가 아니라 <code>create&lt;T&gt;()()</code>라는 점이다. 이렇게 커링(currying)으로 작성하는 이유는 <a href="https://github.com/microsoft/TypeScript/issues/10571">microsoft/TypeScript#10571</a>와 관련이 있다. 요약해 보면, &lt;T, S&gt; 제네릭을 설정한 경우 T만 주입하면 에러가 발생한다. 불필요하더라도 선언한 제네릭은 적어야 한다. <code>create&lt;T&gt;()()</code>는 이러한 문제를 해결하는 방법이다.</p>
<pre><code class="language-ts">const userStore = create&lt;UserState &amp; UserDispatch&gt;()(
  (set) =&gt; ({
    username: &quot;&quot;,
    dispatch: (action) =&gt; set((state) =&gt; dispatchReducer(state, action)),
  })
);</code></pre>
<p>이전에 <code>useReducer</code>를 사용했기에 initial state와 reducer를 사용한 패턴으로 store를 작성했다. 여기서 <code>set</code>은 <code>zustand</code>가 상태를 변경하는 함수이다.</p>
<pre><code class="language-ts">export enum UserActionTypes {
  SET_USER = &quot;SET_USER&quot;,
}

const dispatchReducer = (state: UserState, action: UserAction): UserState =&gt; {
  switch (action.type) {
    case UserActionTypes.SET_USER: {
      return { ...state, username: action.payload.username };
    }
    default: {
      throw new Error(&quot;Can not find action&#39;s type&quot;);
    }
  }
};</code></pre>
<p><code>redecer</code>를 작성하면서도 의구심이 들었다. state 단 하나 변경하는 것도 이렇게 장황한데 조금만 더 복잡해지면 어떨까? 이전에 사용했던 방법(<code>context api + useReducer</code>)과 무슨 차이가 있나? 보일러플레이트가 많지 않다는 <code>zustand</code> 취지에 맞는 건가? 그런 생각을 하면서 다른 서비스의 코드도 작성했으나, 예상대로 엄청 장황해지는 것을 느꼈다. 코드가 길어짐에 따라 느낌은 확신이 되어서 방식을 바꾸기로 했다.</p>
<h2 id="3-slice-pattern">3. Slice Pattern</h2>
<p>익숙한 방식에서 벗어나 공식 문서가 추천하는 방식을 따랐다. <code>RTK</code>에서 봤던 것과 비슷했는데 하나의 <code>store</code>를 두고 상태마다 <code>Slice</code>를 만들어 병합하는 식이다. 물론 이러한 사실을 안 것은 프로젝트 빌드 이후의 일...(작성 후 수정해야지) <a href="https://docs.pmnd.rs/zustand/guides/slices-pattern">Slices Pattern</a></p>
<pre><code class="language-ts">const createUserSlice: SlicePattern&lt;UserSlice&gt; = (set) =&gt; ({
  username: &quot;&quot;,
  setUsername: (payload) =&gt;
    set((state) =&gt; {
      state.username = payload;
    }),
});</code></pre>
<p>일단 <code>user</code>에 대한 slice를 만들었다. 타입의 경우, 중복되는 코드가 있어 커스텀으로 재정의했다. 공식 문서에서 타입에 대해 참고했다. <a href="https://docs.pmnd.rs/zustand/guides/typescript#slices-pattern">TypeScript Guide - Slices pattern
</a></p>
<pre><code class="language-ts">import { StateCreator } from &quot;zustand&quot;;

declare module &quot;zustand&quot; {
  type SlicePattern&lt;T, S = T&gt; = StateCreator&lt;
    S &amp; T,
    [[&quot;zustand/immer&quot;, never], [&quot;zustand/devtools&quot;, never]],
    [],
    T
  &gt;;
}</code></pre>
<p>모든 slice에서 <code>immer</code>와 <code>devtools</code> 미들웨어를 사용한다. <code>StateCreator</code>는 다른 slice 타입과 합쳐진 유니온 타입과 해당 slice 타입을 받는데, 다른 slice 타입이 없는 경우 해당 slice 타입만 사용하도록 설정했다. 다른 slice 타입을 주입하면 유니온 타입이 된다.</p>
<h2 id="4-with-middleware">4. with Middleware</h2>
<h3 id="4-1-devtools">4-1. devtools</h3>
<pre><code class="language-ts">import { devtools } from &quot;zustand/middleware&quot;;
import { immer } from &quot;zustand/middleware/immer&quot;;

const UserBoundStore = create&lt;UserSlice&gt;()(
  devtools(immer((...a) =&gt; ({ ...createUserSlice(...a) })))
);</code></pre>
<p>slice를 병합하는 store를 생성했다. 여기서 실수를 발견했다. <code>boundStore</code>는 여러 slice를 병합하는 곳이다. 나는 <code>User</code>와 <code>Main Service</code>의 store를 나눴다. 문제는 없지만, 디버깅이 힘들더라. <code>devtools</code>는 크롬 확장 프로그램인 <code>Redux Devtools</code>를 이용해 상태를 추적할 수 있게 해주는데, store가 다르면 상태가 호출되었을 때 해당 store만 추적된다.</p>
<p>예를 들어, 나처럼 <code>userStore</code>가 있고, <code>mainStore</code>가 있다면, user state를 호출했을 때는 <code>username</code>만 추적되고, main state 호출로 변경되면 <code>main</code>만 추적되는 식이다. 개발하고 있을 때는 이 생각이 왜 안 났나 몰라. 원활한 디버깅을 위해서라면 <code>유일한 진실의 원천</code> 원칙을 잘 지켜야겠다.</p>
<h3 id="4-2-immer">4-2. immer</h3>
<p><code>immer</code>를 사용하려면 별도로 설치해 종속성을 주입해야 한다. <a href="https://docs.pmnd.rs/zustand/integrations/immer-middleware">Immer middleware</a></p>
<pre><code class="language-shell">npm install immer</code></pre>
<p><code>immer</code> 미들웨어를 사용하면 state 변경이 더 쉬워진다. <code>immer</code>가 상태의 불변성을 보장하는 만큼 새로운 상태를 갈아끼울 필요 없이 재할당하는 느낌으로 변경 가능하다.</p>
<pre><code class="language-ts">setUsername: (payload) =&gt; set((state) =&gt; { state.username = payload; })</code></pre>
<p>주의할 점은 immer 원칙을 지켜야 한다는 것이다. 그렇지 않으면 다음과 같은 에러가 발생한다.</p>
<pre><code>[Immer] An immer producer returned a new value *and* modified its draft.
Either return a new value *or* modify the draft.</code></pre><p>이것은 <strong>임시 객체를 수정</strong>하는 행동과 <strong>새 객체를 반환</strong>하는 행동을 동시에 했음을 의미한다. 위 두 행동을 동시에 하는 것은 유효하지 않으며, 새 객체를 반환 <strong>하거나</strong> 임시 객체 수정, 둘 중 하나만 허용된다. 여기서는 <strong>임시 객체를 수정</strong>했다.</p>
<h2 id="5-selector">5. Selector</h2>
<p>state를 사용하려면 store에서 해당 state를 꺼내면 된다. <a href="https://docs.pmnd.rs/zustand/guides/updating-state">Updating state</a></p>
<pre><code class="language-ts">const firstName = usePersonStore((state) =&gt; state.firstName)

// of

const firstName = usePersonStore().firstName</code></pre>
<p>기본적인 사용 방법이지만, 나는 좀 더 구분하기 위해 상태를 호출하는 훅과 변경 함수를 호출하는 훅을 따로 만들었다.</p>
<pre><code class="language-ts">export const useUserState = () =&gt; UserBoundStore((state) =&gt; state.username);

export const useUserDispatch = () =&gt;
  UserBoundStore((state) =&gt; state.setUsername);</code></pre>
<p><code>useUserState</code>는 상태만 반환하고, <code>useUserDispatch</code>는 변경 함수만 반환한다. 자동으로 셀렉터를 만들어주는 방법도 있고, 라이브러리도 있다고 한다. <a href="https://docs.pmnd.rs/zustand/guides/auto-generating-selectors">Auto Generating Selectors</a></p>
<h2 id="6-느낀점">6. 느낀점</h2>
<p>main도 이렇게 수정했지만, 정리하면서 store 구성에 문제가 있었음을 인지했으니 작성을 마치는 즉시 수정해야겠다. 프로그램 동작에 문제는 없지만 찝찝하니까.</p>
<p><code>Context API + useReducer</code> 대신 <code>Zustand</code>를 써 보면서 좋았던 점은</p>
<ol>
<li>장황하게 적어 내려가던 <code>reducer</code>를 개별 함수로 줄임</li>
<li>불변성을 고려하며 새 객체를 일일이 만들지 않아도 됨</li>
<li>전역 상태 생성마다 필요한 곳을 감싸던 <code>Context Provider</code>를 지움</li>
<li>공식 문서가 깔끔하면서도 자세해서 빠르게 익힐 수 있음</li>
</ol>
<p>반면, 불편했던 점은 비동기 호출이 <code>RTK</code>처럼 직관적이지 않았다는 점이다. 공식 문서에서는 보기 어려워 검색을 좀 해봤는데, 대부분 다른 라이브러리와 함께 사용하더라. 비동기 패칭과 관련해서 좀 더 시도해 봐야겠다.</p>
<h2 id="7-bound-store--set-action-name">7. Bound Store &amp; Set Action Name</h2>
<p>위에서 언급했던 실수를 수정했다. 나눠진 store를 하나로 합쳤고, <code>devtools</code> 미들웨어를 원활히 사용하기 위해 slice의 액션명을 설정했다.</p>
<pre><code class="language-ts">const BoundStore = create&lt;BoundSlice&gt;()(
  devtools(
    immer((...a) =&gt; ({
      ...createUserSlice(...a),
      ...createBookInfoSlice(...a),
      ...createBookcaseSlice(...a),
    })),
    { name: &quot;bip-bound-store&quot; }
  )
);

type BoundSlice = UserSlice &amp; BookcaseSlice &amp; BookInfoSlice;

export default BoundStore;</code></pre>
<p>각각의 slice에서는 <code>devtools</code>에서 액션을 쉽게 확인할 수 있도록 <code>set</code>의 이름을 지정했다. <a href="https://github.com/pmndrs/zustand#redux-devtools">zustand/readme - Redux devtools</a></p>
<pre><code class="language-ts">enum UserActions {
  SET_USERNAME = &quot;user/SetUsername&quot;,
}

const createUserSlice: SlicePattern&lt;UserSlice&gt; = (set) =&gt; ({
  username: &quot;&quot;,
  setUsername: (payload) =&gt;
    set(
      (state) =&gt; {
        state.username = payload;
      },
      false,
      UserActions.SET_USERNAME // action name
    ),
});</code></pre>
<p>이전에는 <code>Redux Devtools</code>에서 액션이 <code>anonymous</code>로 표기되었으나 이제는 각각 변경에 맞는 액션명이 보인다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/4e6efbfa-ef1f-40ec-bb48-38e47c81fe9b/image.png" alt="devtools middlware" title="devtools middlware"></p>
<p>만약 <code>production</code> 단계에서는 <code>devtools</code>를 숨기고 싶다면 <code>devtools</code> option 중 <code>enable</code>을 설정한다.</p>
<pre><code class="language-ts">const BoundStore = create&lt;BoundSlice&gt;()(
  devtools(
    immer((...a) =&gt; ({
      ...createUserSlice(...a),
      ...createBookInfoSlice(...a),
      ...createBookcaseSlice(...a),
    })),
    { name: &quot;bip-bound-store&quot;, enabled: !!import.meta.env.DEV }
  )
);</code></pre>
<p><code>vite</code> 환경에서 개발했기 때문에 <code>import.meta.env.DEV</code>로 개발 환경을 구분했다. 일반 <code>node</code> 환경이라면 <code>process.env.NODE === &quot;production&quot;</code> 등으로 구분할 수 있을 듯하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 컴포넌트에서 제네릭 사용하기]]></title>
            <link>https://velog.io/@real-bird/React-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90-Mapped-type-props-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@real-bird/React-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90-Mapped-type-props-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 24 Oct 2023 16:34:46 GMT</pubDate>
            <description><![CDATA[<h2 id="타입-매개변수-제네릭-전달">타입 매개변수 제네릭 전달</h2>
<p>타입을 구체적으로 명시하지 않고 <code>TS</code>의 추론에 맞긴다면, <code>타입 매개변수</code>를 이용해 손쉽게 추론 가능하다.</p>
<pre><code class="language-tsx">const MyComponent = &lt;T extends unknown&gt;({ a, b, c }: MyComponentProps&lt;T&gt;) =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;{a}&lt;/h1&gt;
      &lt;h2&gt;{b}&lt;/h2&gt;
      &lt;ul&gt;
       {Array.isArray(c) ? (
          c.map((item, idx) =&gt; (
            &lt;li key={JSON.stringify(item) + `${idx}`}&gt;
              {JSON.stringify(item)}
            &lt;/li&gt;
          ))
        ) : (
          &lt;strong&gt;{JSON.stringify(c)}&lt;/strong&gt;
        )}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
};

interface MyComponentProps&lt;T&gt; {
  a: string;
  b: number;
  c: T;
}

export default MyComponent;</code></pre>
<p><code>a</code>와 <code>b</code>의 타입은 고정이지만, <code>c</code>는 어떤 타입이 들어와도 받을 수 있다.</p>
<h2 id="mapped-type">Mapped Type</h2>
<pre><code class="language-ts">type Mappped&lt;T&gt; = {
  [P in keyof T]: T[P];
};

interface MyComponentProps&lt;T&gt; {
  a: string;
  b: number;
  c: Mappped&lt;T&gt;;
}</code></pre>
<p><code>{ [P in keyof T]: T[P] }</code>는 Mapped Type이라고 불리는 형태이며, 이렇게 작성 시 위의 <code>T</code>를 그냥 사용한 것과 똑같은 결과를 나타낸다. <code>c</code>가 primitive 타입이라면 그 타입의 모든 속성을 props로 가지게 되고, object 타입이라면 내부 속성만 값으로 가지게 된다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/c1c377e5-4836-4d7c-ab41-50419d729e92/image.png" alt=""></p>
<p>각각의 타입에 따라 위 이미지와 같은 속성을 얻게 된다.</p>
<h2 id="객체만-전달">객체만 전달</h2>
<p>primitive나 배열이 아닌 객체 형태의 속성만 전달할 필요가 있을 수도 있다. <code>TS</code>의 유틸리티 타입 중 <code>Record</code>를 사용해 객체 타입만 받도록 지정하면 된다.</p>
<pre><code class="language-tsx">const MyComponent = &lt;T extends Record&lt;string, unknown&gt;&gt;(
        { a, b, c }: MyComponentProps&lt;T&gt;
  ) =&gt; {
  return (
    &lt;div&gt;
      {/* ... */}
    &lt;/div&gt;
  );
};

interface MyComponentProps&lt;T&gt; {
  a: string;
  b: number;
  c: T;
}</code></pre>
<p><code>Record&lt;keyType, valueType&gt;</code>는 속성 key의 타입을 첫 번째 제네릭으로 받고, value의 타입을 두 번째 제네릭으로 받는다. 이렇게 <code>T</code>를 확장시키면 객체 이외의 c값은 오류를 발생한다. </p>
<pre><code class="language-tsx">function App() {
  const temp = {
    a: &quot;A&quot;,
    child: [
      {
        b: &quot;B&quot;,
        c: &quot;C&quot;
      }
    ]
  };
  return (
    &lt;div className=&quot;App&quot;&gt;
  {/* Type &#39;string&#39; is not assignable to type &#39;Record&lt;string, unknown&gt;&#39;. */}
      &lt;MyComponent a={&quot;a&quot;} b={1} c={&quot;A&quot;} /&gt;
  {/* Type &#39;string[]&#39; is not assignable to type &#39;Record&lt;string, unknown&gt;&#39;. */} 
      &lt;MyComponent a={&quot;b&quot;} b={2} c={[&quot;A&quot;, &quot;B&quot;, &quot;C&quot;]} /&gt;
  {/* Type &#39;(string | number | true)[]&#39; is not assignable to type &#39;Record&lt;string, unknown&gt;&#39;. */}
      &lt;MyComponent a={&quot;b&quot;} b={2} c={[&quot;A&quot;, 1, true]} /&gt;
  {/* No Error */}      
      &lt;MyComponent a={&quot;b&quot;} b={2} c={temp} /&gt;
    &lt;/div&gt;
  );
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SASS] SCSS 살펴보기]]></title>
            <link>https://velog.io/@real-bird/SCSS-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@real-bird/SCSS-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 18 Oct 2023 15:49:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://nomadcoders.co/css-layout-masterclass">노마드코더 - CSS Layout 마스터클래스</a> 강의를 정리한 내용입니다.</p>
</blockquote>
<p><code>SCSS</code>는 CSS에 다양한 편의 기능을 추가한 확장 버전으로, <code>nesting</code>, <code>mixins</code>, <code>modules</code> 등의 문법을 제공한다. 그러나 이 문법을 브라우저가 해석하지 못하기 때문에 전처리 과정이 필요하다. <code>SCSS</code>를 <code>CSS</code>로 <code>SASS</code>를 활요해 전처리한다.</p>
<p><code>Vite</code> 환경에서는 <code>npm add -D sass</code>로 전처리기를 추가한다.</p>
<h2 id="변수-사용">변수 사용</h2>
<p><code>CSS</code>에서는 변수를 선언할 때 <code>:root</code> 블록 안에 <code>--</code>를 <code>prefix</code>하여 선언한다. 변수의 사용은 <code>var()</code> 함수를 이용한다.</p>
<pre><code class="language-css">root: {
  --bgColor: red;
}

body {
  background-color: var(--bgColor);
}</code></pre>
<p><code>SCSS</code>에서는 <code>$</code>를 이용해 변수를 선언한다. <code>CSS</code>와 다르게 변수를 그대로 사용한다.</p>
<pre><code class="language-scss">$bgColor: red;

body {
  background-color: $bgColor;
}</code></pre>
<h2 id="nesting">Nesting</h2>
<p>어떤 태그의 자식 요소에만 스타일을 적용한다면, <code>CSS</code>에서는 태그 트리를 자세히 적어줘야 한다.</p>
<pre><code class="language-css">ul {
  list-style-type: none;
  padding: 0;
  display: flex;
  gap: 10px;
}

ul li {
  background-color: tomato;
  color: white;
  padding: 5px 10px;
  border-radius: 7px;
}

ul li a {
  text-decoration: none;
  color: white;
  text-transform: uppercase;
}</code></pre>
<p>여기에 <code>hover</code> 상태에 따라 <code>li</code>의 <code>opacity</code>와 <code>a</code>의 <code>color</code>가 변한다고 하자.</p>
<pre><code class="language-css">ul li:hover {
  opacity: 0.8;
}

ul li:hover a {
  color: gray;
}</code></pre>
<p>이런 식으로 일일이 설정해야 한다.
<code>SCSS</code>에서는 <code>Nesting</code> 기능을 제공해 태그 안에 <code>&amp;</code>를 이용하여 태그 안에 적을 수 있다.</p>
<pre><code class="language-scss">ul {
  list-style-type: none;
  padding: 0;
  display: flex;
  gap: 10px;

  li {
    background-color: tomato;
    color: white;
    padding: 5px 10px;
    border-radius: 7px;
    a {
      text-decoration: none;
      color: white;
      text-transform: uppercase;
    }

    &amp;:hover {
      opacity: 0.8;
      a {
        color: gray;
      }
    }
  }
}</code></pre>
<h2 id="extend">Extend</h2>
<p><code>@extend</code>는 공통 로직을 확장하는 키워드이다.</p>
<p>스타일은 같고 배경만 다른 3개의 컴포넌트를 만든다면, 먼저 <code>%</code>를 이용해 공통 스타일을 작성한다.
컴포넌트마다 <code>@extend %공통스타일</code>을 확장하고, 개별적인 스타일을 덧입히면 된다.</p>
<pre><code class="language-scss">%alert {
  margin: 10px;
  padding: 10px 20px;
  border-radius: 10px;
  border: 1px dashed black;
}

.success {
  @extend %alert;
  background-color: green;
}
.error {
  @extend %alert;
  background-color: tomato;
}
.warning {
  @extend %alert;
  background-color: yellow;
}</code></pre>
<h2 id="mixins">Mixins</h2>
<p><code>@extend</code>는 유용한 기능이지만, 반복되는 코드가 보인다. 이를 좀더 관리하기 편하도록 함수처럼 만들 수 있다. <code>@mixin</code>과 <code>@include</code>를 사용한다.</p>
<pre><code class="language-scss">@mixin alert($bgColor, $borderColor) {
  background-color: $bgColor;
  margin: 10px;
  padding: 10px 20px;
  border-radius: 10px;
  border: 1px dashed $borderColor;
}

.success {
  @include alert(green, blue);
}
.error {
  @include alert(tomato, white);
}
.warning {
  @include alert(yellow, black);
}</code></pre>
<p><code>@mixin</code>으로 <code>%alert</code>을 <code>alert()</code>으로 변경해 반복되는 속성(<code>background-color</code>)을 인자로 받았다. 이를 사용하는 컴포넌트에서는 <code>@include</code>로 호출해 인자를 주입하면 된다.</p>
<p>만약 <code>@include</code>로 호출한 함수에 속성을 추가하고 싶다면 중괄호(<code>{}</code>)를 열고 안에 속성을 적는다. 그 후 <code>@mixin</code>에 <code>@content</code>를 추가하면 된다.</p>
<pre><code class="language-scss">@mixin alert($bgColor, $borderColor) {
  /* ... */
  @content;
}

.success {
  @include alert(green, blue) {
    font-size: 12px;
  }
}
.error {
  @include alert(tomato, white) {
    text-decoration: underline;
  }
}
.warning {
  @include alert(yellow, black) {
    text-transform: uppercase;
  }
}</code></pre>
<h2 id="responsive-mixins">Responsive Mixins</h2>
<p><code>@mixin</code>을 이용해 반응형 속성을 쉽게 구현할 수 있다.</p>
<pre><code class="language-scss">$breakpoint-sm: 480px;
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
$breakpoint-xl: 1200px;

@mixin smallDevice {
  @media screen and (min-width: $breakpoint-sm) {
    @content;
  }
}

@mixin mediumDevice {
  @media screen and (min-width: $breakpoint-md) {
    @content;
  }
}

@mixin largeDevice {
  @media screen and (min-width: $breakpoint-lg) {
    @content;
  }
}

@mixin xlDevice {
  @media screen and (min-width: $breakpoint-xl) {
    @content;
  }
}</code></pre>
<p><code>mixin</code> 안에 <code>media querie</code>를 설정한다.</p>
<pre><code class="language-scss">body {
  @include smallDevice {
    background-color: blue;
  }

  @include mediumDevice {
    background-color: tomato;
  }

  @include largeDevice {
    background-color: purple;
  }

  @include xlDevice {
    background-color: brown;
  }
}</code></pre>
<p>필요한 곳에서 <code>@include</code>를 호출하면 반응형으로 스타일링할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BiP] 로그인 로직 변경 기록]]></title>
            <link>https://velog.io/@real-bird/BiP-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%A1%9C%EC%A7%81-%EB%B3%80%EA%B2%BD-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@real-bird/BiP-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%A1%9C%EC%A7%81-%EB%B3%80%EA%B2%BD-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Sun, 15 Oct 2023 19:53:03 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.wanted.co.kr/events/pre_challenge_fe_14">프리온보딩 FE 챌린지 10월 - 로그인 기능 구현, 하나부터 열까지!</a> 강의를 들으면서 내가 만든 개인 프로젝트의 로그인 로직이 계속 신경 쓰였다. 만들고 싶었던 기능에만 치중해 기본이면서도 가장 중요한 로그인을 대충 만들었기 때문이다. 얼마나 대충 만들었는지 보면 깜짝 놀라리라.</p>
<p>아무튼 강의에서 들은 내용을 참고해서 봐주기 힘든 로그인 과정을 그나마 볼 만한 모양새로 고친 과정을 기록해 보기로 한다.</p>
<h2 id="유저-식별과-로그인-유지">유저 식별과 로그인 유지</h2>
<p>로그인의 목적은 유저에게 서비스 이용 권한이 있는지 <strong>식별</strong>하고, 유저의 행위를 <strong>제어</strong>하고 <strong>관리</strong>하기 위함이다. 등록되지 않은 유저가 서비스의 CRUD 요청을 멋대로 해서는 안 되고, 권한이 없는 페이지에 접근해서도 안 된다.</p>
<p>식별된 유저라면 서비스를 계속 이용할 수 있도록 인증 상태를 <strong>유지</strong>해야 한다. 로그인하고 A서비스 이용하고, 로그인하고 B서비스 이용하는 식이라면... 누구도 이용하지 않는 서비스가 될 것이다.</p>
<p>나 역시 이러한 점을 고려해 로그인을 구현했다. 표면적으로는 말이다.</p>
<h2 id="변경-전-로그인-로직">변경 전 로그인 로직</h2>
<p>초기 로직을 한 눈에 보기 위해 시퀀스 다이어그램을 그려봤다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/9a37f20f-d841-46b3-aa98-fbb6d02c8af6/image.png" alt="수정 전 로그인 로직" title="수정 전 로그인 로직"></p>
<p>엉망진창이지만 생애최초 다이어그램이니 넘어가자.</p>
<p>먼저 로그인 유지는 다음 과정을 따랐다.</p>
<ol>
<li>서비스에 접속하면 로컬 스토리지에 저장한 토큰을 확인한다.<ul>
<li>토큰이 없으면 로그인 페이지로 이동한다.</li>
</ul>
</li>
<li>토큰이 있다면 쿼리 파라미터에 담아 서버의 <code>/auth/check</code>로 식별을 요청한다.</li>
<li>서버에서 토큰에 해당하는 유저를 확인한 후 유저 정보를 담은 응답을 보낸다.<ul>
<li>유저 정보가 없다면 로그인 페이지로 이동한다.</li>
</ul>
</li>
<li>이후 서비스는 로컬 스토리지에 저장된 토큰을 이용한다.</li>
</ol>
<p>로그인 과정은 다음과 같다.</p>
<ol>
<li>구글 로그인 버튼을 누르면 서버의 <code>/auth/google/callback</code>으로 이동하는 창을 연다.</li>
<li>서버에서 passport의 google oauth20 전략을 이용해 소셜 로그인을 시도한다.<ul>
<li>유저의 email과 profile을 받아온다.</li>
<li>기존 유저가 있다면 유저 정보를 서버 세션에 담고, 없다면 생성 후 담는다.</li>
</ul>
</li>
<li>처리가 끝나면 로그인 페이지로 리다이렉트한다.</li>
<li>로그인 페이지로 돌아오면 서버에 <code>/auth/login</code> 요청을 보내 세션에 담긴 유저 정보를 응답에 담아 클라이언트로 보낸다.</li>
<li>받은 응답이 올바르면 유저의 <code>id</code>(!!)를 로컬 스토리지에 토큰으로 저장하고 <code>home</code>으로 푸시한다.<ul>
<li>아니라면 로그인 페이지에 남겨 둔다.</li>
</ul>
</li>
</ol>
<p>누군가 옆에서 봤다면 곧장 &#39;으악!&#39;했을 로직이다. 문제 되는 부분을 스스로 짚어 보자.</p>
<h3 id="보안-문제">보안 문제</h3>
<p>먼저 유저의 정보라고 할 수 있는 부분을 암호화도 하지 않은 채 로컬 스토리지에 저장한 것이 가장 큰 문제였다. 프로필에 담긴 속성 중 <code>sub</code>를 사용했다. 고유값이긴 하지만 숫자로만 되어 있어서 별로 중요하지 않다고 판단했기 때문이었다. 프로젝트 자체도 털릴 게 없었고.</p>
<p>하지만 이 자체가 위험한 생각이었다. 보안 측면에서 보면 그 내용이 뭐가 되었든 유저의 정보에 해당한다면 숨기는 게 맞았다. 애초에 uuid 등으로 id를 해시값으로 만들었다면 문제가 덜했겠지만, 아무튼 프론트엔드 개발만 생각해 서버쪽은 대충 구현했다.</p>
<h3 id="세션-사용">세션 사용</h3>
<p>로그인 유지를 위해서 초기에 세션 방식으로 구현했다. 잘못된 선택은 아니었지만, 그렇다고 특별한 이유가 있지도 않았다. 나아가 <code>REST</code>한 방식으로 API를 구현했다고 생각했는데, 따지고 보면 세션은 <code>REST</code>하지 않은 느낌이 들었다.</p>
<p><code>REST API</code>의 원칙 중 하나는 클라이언트와 서버 간의 <code>stateless</code>이다. 클라이언트가 서버의 상태를 가지면 안 되고, 서버도 클라이언트의 상태를 가지고 있으면 안 된다. 그러나 세션 방식은 만료 기한이 있다손 치더라도 서버가 클라이언트의 정보를 가지고 있는 셈이다. 내 생각과 구현을 일치시키기 위해서 세션 방식을 쳐냈다.</p>
<h3 id="불필요한-로그인-요청">불필요한 로그인 요청</h3>
<p>변경할 방식을 이렇게 저렇게 생각하다 보니 <code>login</code> 요청도 말이 안 됐다. 페이지 랜딩 시 유저 정보를 검사하는 함수를 실행한다. 로그인 시도 후 페이지로 돌아오면 로그인 함수도 실행되고, 검사 함수도 실행된다. 어쩌다 이런 멍청한 코드를 짠 건지 의문이 드는 대목이었다. 지금도 멍청하지만, 예전에는 더했다. 이것도 고치기로 했다.</p>
<p>이 문제들에 중점을 두고 로직을 변경하기 시작했다.</p>
<h2 id="변경-후-로그인-로직">변경 후 로그인 로직</h2>
<p><img src="https://velog.velcdn.com/images/real-bird/post/f05cf555-197a-4689-86a7-c30772dc60af/image.png" alt="수정 후 로그인 로직" title="수정 후 로그인 로직"></p>
<p>변경한 로직의 시퀀스 다이어그램이다. 수정한 부분을 중점으로 보자.</p>
<h3 id="jwt를-활용한-토큰-방식-사용">JWT를 활용한 토큰 방식 사용</h3>
<p>세션 방식을 버리고 <code>refresh token</code>과 <code>access token</code>을 사용하는 토큰 방식을 채용했다. 구글 로그인을 요청하면 성공 콜백함수에서 <code>refresh token</code>을 생성하여 응답 쿠키에 담아 보낸다. 이후 로그인 페이지로 <code>redirection</code>되고, 서버에 유저 정보를 체크하는 요청을 보내는 <code>check</code> 함수가 실행된다.</p>
<p>서버에서는 헤더의 <code>Authorization</code>에 담아 보낸 <code>Bearer access token</code>을 확인한다. <code>access token</code>이 유효하면 유저 정보와 기존 토큰을 응답한다. 만약 <code>access token</code>이 없거나 유효하지 않다면 쿠키에 담긴 <code>refresh token</code>을 검사한다. 유효하다면 새로운 <code>access token</code>을 발급하지만, 이것마저 유효하지 않다면 에러을 응답한다.</p>
<p>발급 받은 <code>access token</code>은 이전과 마찬가지로 로컬 스토리지에 저장했다. 차이점은 유저 식별 정보가 암호화되었다는 것과 토큰에 만료 기한이 있어 영구 보관이 되지 않는다는 점이다. 이러한 <code>check</code> 과정이 모든 페이지에서 실행되도록 했다. 덕분에 위에서 언급한 <strong>불필요한 로그인 요청</strong>을 제거할 수 있었다.</p>
<h2 id="느낀점">느낀점</h2>
<p>코드는 크게 의미 있는 것 같지 않고, 너무 못생겨서 첨부하지 않았다. 코드 보다 로직을 이해하는 게 더 중요하기도 하고.</p>
<p>간단하게만 생각했던 로그인이 전혀 간단하지 않음을 깨달았다. 그나마 내 프로젝트 규모가 작아서 사용자 식별만 생각하면 되는 것이지, 다른 큰 서비스처럼 admin이 나뉘고, 다른 도메인과 연결하고 하다 보면 진짜 어마어마하게 복잡할 것이다.</p>
<p>또 서버 코드를 직접 변경하면서 어떤 식으로 요청을 주고 받는지 더 깊게 이해할 수 있었다. 이전에는 단순하게 클라이언트에서 요청을 보내면 서버에서 그 요청에 해당하는 응답만 해주면 된다고 생각했다. 그래서 <strong>변경 전 로직</strong>이 그 모양이었고. 이제는 내가 어떻게 요청을 보내야 서버에서 어떤 응답을 하는지 사전에 고민해 보고, 적절한 에러핸들링까지 포함한 코드를 작성할 수 있을 것 같다.</p>
<p>로그인 뿐만 아니라 본 서비스에 대한 요청과 처리도 이번에 배운점을 이용해서 더 나은 코드로 리팩토링해 봐야겠다. 로직과 코드가 일치하는 개발자를 목표로 하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CSS] Grid]]></title>
            <link>https://velog.io/@real-bird/CSS-Grid</link>
            <guid>https://velog.io/@real-bird/CSS-Grid</guid>
            <pubDate>Sun, 08 Oct 2023 18:59:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://nomadcoders.co/css-layout-masterclass">노마드코더 - CSS Layout 마스터클래스</a> 강의를 정리한 내용입니다.</p>
</blockquote>
<p><code>FlexBox</code>가 1차원 형태로 레이아웃을 정의했다면, <code>Grid</code>는 2차원 형태로 레이아웃을 정의하는 기능이다. 행렬을 가지며 그와 관련된 속성으로 레이아웃을 수정할 수 있다.</p>
<p>부모 요소에서 설정해야 하며 <code>display: grid</code>로 선언한다.</p>
<h2 id="grid">Grid</h2>
<h3 id="columns-and-rows">Columns and Rows</h3>
<p>열은 <code>grid-template-columns</code>로 설정하고, 행은 <code>grid-template-rows</code>로 설정한다. 단순히 개수만 적는 게 아니라 각 행열의 크기도 입력해야 한다.</p>
<p>예를 들어, 200px인 열 2개와 100px인 행 2개로 이뤄진 grid 컨테이너를 만든다면 다음 코드처럼 적는다.</p>
<pre><code class="language-css">.father {
  display: grid;
  grid-template-columns: 200px 200px;
  grid-template-rows: 100px 100px;
  gap: 10px;
}</code></pre>
<p>여기서 <code>gap</code>은 FlexBox와 마찬가지로 <code>column-gap</code>과 <code>row-gap</code>으로 나눌 수도 있고, 한 줄로 단축할 수도 있다.</p>
<h3 id="grid-lines">Grid Lines</h3>
<p>gird 컨테이너를 생성하면 크롬의 개발자 도구에서 컨테이너 형태를 시각적으로 확인할 수 있다. 컨테이너 요소 옆에 <code>grid</code> 버튼을 활성화하면 된다.</p>
<p><img src="./assets/grid-container.png" alt="grid container"></p>
<p>다양한 숫자가 보이는데, 각각 grid의 자식요소가 차지할 공간을 의미한다. 예를 들어, <code>child 1</code>은 세로줄 1번부터 세로줄 2번까지 차지하고 있고, 가로로는 1번과 2번을 차지하고 있다. 즉, 자식요소가 시작되는 라인이 <code>grid-###-start</code>이고, 끝나는 라인이 <code>grid-###-end</code>인 셈이다(<code>###</code>에는 <code>colum</code>이나 <code>row</code>의 자리이다.).</p>
<p><code>grid-column-start</code>는 자식 요소가 어느 세로 라인에서 시작할지 정한다. <code>grid-colum-end</code>는 자식 요소가 어느 세로 라인까지 가서 끝날지 정한다. <code>row</code>는 가로 라인에 영향을 끼친다.</p>
<p>만약 <code>child 1</code>을 두 개의 열 모두 차지하게 한다면 <code>grid-column-start: 1; grid-column-end: 3;</code>으로 입력한다.</p>
<pre><code class="language-css">.child:first-child {
  grid-column-start: 1;
  grid-column-end: 3;
}</code></pre>
<p><img src="./assets/grid-column-start-end.png" alt="grid-column-start/end"></p>
<p>단축 속성으로는 <code>grid-column: 1 / 3</code>을 입력하면 된다. <code>row</code>도 마찬가지다.</p>
<p>음수의 역할은 기준선을 거꾸로 뒤집었다고 생각하면 된다. <code>-1</code>은 grid 컨테이너의 맨 끝을 의미한다. <code>grid-column: 1 / -1</code>은 위에 작성한 것과 같다. 단, 열이 늘어났을 때 양수만 적은 코드는 레이아웃이 망가질 수 있지만, 음수로 끝을 정한 코드는 늘어난 열 끝까지 차지한다.</p>
<h4 id="line-names">Line Names</h4>
<p>숫자로 라인을 설정하다 보면 헷갈릴 위험이 크고 보기도 좋지 않다. 이런 점을 대비해 grid는 라인별로 명명하는 기능을 제공한다.</p>
<pre><code class="language-css">.father {
  display: grid;
  grid-template-columns: [apple] 100px [pure] 200px [cherry] 50px [orange];
  grid-template-rows: [sword] 200px [spear] 100px [bow];
  gap: 10px;
}</code></pre>
<p>행열의 크기를 정한 단위 사이마다 대괄호(<code>[]</code>)를 이용해 라인 이름을 짓는다. 크롬 개발자 도구의 <code>element</code> 하위 탭인 <code>Layout</code>에서 <code>Show line names</code>로 변경하면 적용한 라인 이름을 볼 수 있다.</p>
<p><img src="./assets/change-nums-view-to-names.png" alt="change-nums-view-to-names"></p>
<p>라인 표기가 숫자에서 명명한 이름으로 변경되었다.</p>
<p><img src="./assets/grid-line-names.png" alt="grid-line-names"></p>
<p><code>grid-column</code> 등의 설정은 숫자 대신 명명한 이름으로 적어 설정한다.</p>
<pre><code class="language-css">.child:first-child {
  grid-column: apple / cherry;
  grid-row: sword / bow;
}</code></pre>
<h3 id="grid-template">Grid Template</h3>
<h4 id="frfraction">fr(fraction)</h4>
<p>grid의 영역을 지정하는 단위 중 <code>fr</code>이 있다. 비율로 공간의 영역을 정하는 단위이다.</p>
<pre><code class="language-css">body {
  height: 100vh;
  display: grid;
  grid-template-columns: 1fr 2fr 1fr 1fr;
  grid-template-rows: 1fr 1fr 1fr 1fr;
}</code></pre>
<p>grid 컨테이너의 공간 중 열은 <code>1:2:1:1</code> 비율로 나누고, 행은 <code>1:1:1:1</code> 비율로 나눈 것을 의미한다. 화면을 키우거나 줄여도 2번째 열의 요소는 다른 요소보다 2배 크게 유지된다.</p>
<h4 id="grid-area">Grid Area</h4>
<p>grid 컨테이너 내에 영역 이름을 짓고, 자식 요소에 어느 영역에 있어야 하는지 이름을 지정하여 레이아웃을 구성한다.</p>
<pre><code class="language-css">body {
  height: 100vh;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;
  grid-template-rows: 1fr 2fr 1fr;
  grid-template-areas:
    &quot;header header header header&quot;
    &quot;content content content menu&quot;
    &quot;footer footer footer footer&quot;;
}</code></pre>
<p><code>header</code>는 4개의 열을 차지하며 첫 번째 행에 위치한다. <code>content</code>는 3개의 열과 두 번째 행에 위치하고, 같은 행 남은 열에는 <code>menu</code>가 위치한다.
<code>footer</code>는 4개의 열과 마지막 행에 위치한다.</p>
<pre><code class="language-css">header {
  background-color: aqua;
  grid-area: header;
}

section {
  background-color: coral;
  grid-area: content;
}

aside {
  background-color: forestgreen;
  grid-area: menu;
}

footer {
  background-color: deeppink;
  grid-area: footer;
}</code></pre>
<p>해당 영역에 위치할 요소에 <code>grid-area</code>를 이용해서 영역 이름을 부여한다.</p>
<p><img src="./assets/grid-template-area.png" alt="grid-template-area"></p>
<p>정한 이름에 맞춰 해당 태그들이 위치한 것을 볼 수 있다.</p>
<h4 id="grid-template-1">grid-template</h4>
<p>행열과 영역의 속성을 단축하여 <code>grid-template</code>을 쓸 수 있다.</p>
<pre><code class="language-css">body {
  grid-template:
    &quot;header header header header&quot; 1fr
    &quot;content content content menu&quot; 2fr
    &quot;footer footer footer footer&quot; 1fr / 1fr 1fr 1fr 1fr;
}</code></pre>
<p>영역명 바로 옆에 있는 것은 <code>row</code>의 크기, 슬래시(<code>/</code>) 옆에 있는 것은 <code>column</code>의 크기를 가리킨다.</p>
<h3 id="span-keyword">Span Keyword</h3>
<p>명시적으로 요소의 끝지점을 입력하고 싶지 않을 때 <code>span</code> 키워드를 사용한다. <code>span</code>은 시작 라인부터 요소를 지정한 크기만큼 늘린다. <code>grid-column: span 2</code>라면 세로 1라인부터 2줄 늘어나 세로 3라인까지 요소가 늘어난다. 시작 라인을 바꾸면 변경된 시작점에서 <code>span 크기</code>만큼의 영역으로 늘어나게 된다.</p>
<h3 id="auto-columns-and-rows">Auto Columns and Rows</h3>
<p>grid가 다룰 수 있는 <strong>행/열</strong> 보다 더 많은 content가 추가될 때, 이후에 생성되는 요소의 grid 크기를 자동으로 맞출 수 있다.</p>
<p><code>grid-auto-rows</code>는 새 요소의 <code>row</code> 크기를 자동으로 맞춘다.
<code>grid-auto-columns</code>는 새 요소의 <code>column</code> 크기를 자동으로 맞춘다.
<code>grid-auto-flow</code>은 grid가 <code>row/column</code> 중 어느 방향으로 적용될지 설정한다. 기본값은 <code>row</code>이다.</p>
<pre><code class="language-css">.father {
  display: grid;
  min-height: 50vh;
  gap: 10px;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(2, 1fr);
  grid-auto-rows: 1fr;
  grid-auto-columns: 1fr;
  grid-auto-flow: column;
}</code></pre>
<h3 id="align-and-justify-items">Align and Justify Items</h3>
<p>grid 셀 내에서 자식 요소의 위치를 지정할 때 <code>align-items</code>와 <code>justify-items</code>을 사용한다. 두 속성을 하나로 축약한 단축 속성으로 <code>place-items</code>가 있다.
특정 자식 요소의 위치를 지정할 때는 <code>align-self</code>와 <code>justify-self</code>를 사용한다. <code>place-items</code>로 축약하여 사용할 수 있다.</p>
<ul>
<li><p><code>align-items</code> : 셀 내 요소의 수직 위치를 옮긴다. <code>start</code>, <code>center</code>, <code>end</code> 그리고 <code>stretch</code>가 있다. <code>stretch</code>의 경우 자식 요소가 높이나 너비를 가지지 않아야 동작한다.</p>
</li>
<li><p><code>justify-items</code> : 셀 내 요소의 수평 위치를 옮긴다. 속성값은 위와 동일하다.</p>
</li>
<li><p><code>place-items</code>: 위 두 속성의 단축 속성으로, 첫 번째 값으로 <code>align-items</code> 값이 오고, 두 번째 값으로 <code>justify-items</code> 값이 온다. 같을 경우 하나로 합칠 수 있다.</p>
</li>
<li><p><code>align-self</code>, <code>justify-self</code>, <code>place-self</code> : 위와 똑같이 동작하지만, 특정 자식 요소에 적용한다는 차이가 있다.</p>
</li>
</ul>
<h3 id="align-and-justify-content">Align and Justify Content</h3>
<p>grid의 모든 셀을 하나로 취급하면 content가 된다. content의 위치를 지정하는 속성이 <code>align-content</code>와 <code>justify-content</code>이며, 단축 속성으로 <code>place-content</code>가 있다. 주의 사항은 grid 컨테이너에 content를 옮길 공간이 있어야 한다는 것이다. 만약 <code>1fr</code>로 설정해 빈 공간이 없다면 해당 속성은 동작하지 않는다. 값은 flex에서의 <code>justify-content</code> 속성과 같다.</p>
<ul>
<li><code>align-content</code> : content의 수직 위치를 옮긴다.</li>
<li><code>justify-content</code> : content의 수평 위치를 옮긴다.</li>
<li><code>place-content</code> : 위 두 속성의 단축 속성으로, 첫 번째 값은 <code>align-content</code>, 두 번째 값은 <code>justify-content</code>이다.</li>
</ul>
<h3 id="auto-sizing-and-minmax">Auto Sizing and Minmax</h3>
<p>grid 셀 내의 내용물 사이즈에 따라 row/column의 크기를 조절할 수 있는 값이 있다.</p>
<ul>
<li><code>max-content</code> : 자식 요소의 content가 필요한 만큼 크기를 할당한다.</li>
<li><code>min-content</code> : 자식 요소의 content가 가질 수 있는 최소 크기를 할당한다.</li>
<li><code>minmax(minimum, maximum)</code> : minimum 값보다 작아지지 않고 maximum 값보다 커지지 않는 크기를 할당한다.</li>
</ul>
<h3 id="auto-fill-and-auto-fit">Auto Fill and Auto Fit</h3>
<p><code>auto-fill</code>과 <code>auto-fit</code>은 정해진 row/column 크기에 따라 자동으로 grid의 셀 개수를 설정해주는 속성이다.</p>
<h4 id="auto-fill">auto-fill</h4>
<pre><code class="language-css">.father {
  display: grid;
  min-height: 50vh;
  gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}</code></pre>
<p><code>auto-fill</code>의 경우, 화면이 줄어들면서 셀의 크기가 200px보다 작아지면 모든 셀의 열 크기가 <code>1fr</code>에 맞춰지도록 열의 개수를 줄인다.
단, 화면의 크기가 늘어났을 때 요소가 담기지 않은 셀이라도 <code>1fr</code>을 충족한다면 열의 개수를 채운다.</p>
<h4 id="auto-fit">auto-fit</h4>
<pre><code class="language-css">.father {
  display: grid;
  min-height: 50vh;
  gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}</code></pre>
<p><code>auto-fit</code>도 마찬가지로 동작하지만, 늘어났을 때의 반응이 다르다. 빈 공간이 <code>1fr</code>을 충족하더라도 무시하고 요소들의 크기를 빈 공간과 합쳐 <code>1fr</code>을 유지한다.</p>
<h2 id="grid-garden">GRID GARDEN</h2>
<p>grid 속성을 게임으로 익힐 수 있는 사이트
<a href="https://cssgridgarden.com/#ko">https://cssgridgarden.com/#ko</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RTK] 공식 문서만 보고 Redux Toolkit 적용해 보기(2)]]></title>
            <link>https://velog.io/@real-bird/RTK-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Redux-Toolkit-%EC%A0%81%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B02</link>
            <guid>https://velog.io/@real-bird/RTK-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Redux-Toolkit-%EC%A0%81%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B02</guid>
            <pubDate>Sat, 07 Oct 2023 16:12:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공식 문서를 돌같이 보는 버릇을 고치자!</p>
</blockquote>
<p><a href="https://velog.io/@real-bird/RTK-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Redux-Toolkit-%EC%A0%81%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B01">[RTK] 공식 문서만 보고 Redux Toolkit 적용해 보기(1)</a>에 이어 작성하는 글이다.</p>
<h3 id="5-todoreducer">5) todoReducer</h3>
<p><code>todo</code>의 로직도 <code>auth</code>와 큰 차이는 없다. 다만, CRUD 요청이 모두 비동기로 발생해 <code>createAsyncThunk</code>를 4번 사용했다. <code>extraReducers</code>에서도 콜백 함수의 <code>builder</code>를 4번 호출했다. <code>.addCase</code>로 연결하여 호출할 수도 있지만, 개인적으로 가독성이 떨어져 api 호출별로 <code>builder.addCase</code>를 나눠 작성했다.</p>
<pre><code class="language-ts">extraReducers: (builder) =&gt; {
  // get todo list
  builder
    .addCase(fetchTodoList.pending, (state) =&gt; {
    state.isLoading = true;
  })
    .addCase(fetchTodoList.fulfilled, (state, action) =&gt; {
    state.isLoading = false;
    state.todoList = action.payload;
  })
    .addCase(fetchTodoList.rejected, (state) =&gt; {
    state.isLoading = false;
  });
  // create todo
  builder
    .addCase(fetchCreateTodo.pending, (state) =&gt; {
    state.isLoading = true;
  })
    .addCase(fetchCreateTodo.fulfilled, (state, action) =&gt; {
    state.isLoading = false;
    state.todoList.todos = [
      ...state.todoList.todos,
      action.payload.newToDoData,
    ];
  })
    .addCase(fetchCreateTodo.rejected, (state) =&gt; {
    state.isLoading = false;
  });
  // update todo
  builder
    .addCase(fetchUpdateTodo.pending, (state) =&gt; {
    state.isLoading = true;
  })
    .addCase(fetchUpdateTodo.fulfilled, (state, action) =&gt; {
    state.isLoading = false;
    state.todoList.todos = state.todoList.todos
      .map((todo) =&gt; todo.id === action.payload.updateTodo.id
           ? action.payload.updateTodo
           : todo
          );
  })
    .addCase(fetchUpdateTodo.rejected, (state) =&gt; {
    state.isLoading = false;
  });
  // delete todo
  builder
    .addCase(fetchDeleteTodo.pending, (state) =&gt; {
    state.isLoading = true;
  })
    .addCase(fetchDeleteTodo.fulfilled, (state, action) =&gt; {
    state.isLoading = false;
    state.todoList.todos = state.todoList.todos.filter(
      (todo) =&gt; todo.id !== action.payload.id
    );
  })
    .addCase(fetchDeleteTodo.rejected, (state) =&gt; {
    state.isLoading = false;
  });
},</code></pre>
<p>주석으로 구분하긴 했지만, 그래도 보기가 좋지 않았다. 호출하는 함수만 다른 같은 로직이 수두룩 빽빽했기 때문이다. 이걸 어떻게 해결해야 하나 진짜 오래 고민했다. 그러다 참지 못하고 <strong>검색 찬스</strong>를 사용하고 말았다...!</p>
<p><code>RTK extrareducers builder combine</code>을 검색하니 가장 위에 나와 비슷한 고민을 한 사람의 <a href="https://stackoverflow.com/questions/66925231/combine-extrareducers-with-exact-same-code-with-redux-toolkit">스택오버플로 질문</a>이 있었다. 이 사람도 에러 로직이 같은데 어떻게 <strong>DRY</strong>하게 만드냐고 질문했다. 그 중 <code>addMatcher</code>를 사용한 답변을 참고했다.</p>
<p>공식 문서를 살펴 보니 <code>addMatcher</code>는 <a href="https://redux-toolkit.js.org/api/createReducer#builderaddmatcher">들어오는 작업을 action.type 속성 대신 자체 필터 함수와 일치시킬 수 있</a>는 메서드이다.(역시 공식 문서는 답을 알고 있다, 두둥!)</p>
<p>첫 번째 인자로 <code>matcher</code> 함수를 받고, 두 번째 인자로 <code>reducer</code>를 받는다.</p>
<p>내게 필요한 과정은 <code>pending</code>일 때 <code>isLoading = true</code>로, <code>fulfilled</code> or <code>rejected</code>일 때는 <code>isLoading = false</code>로 변화시키는 것이다.</p>
<pre><code class="language-ts">enum AsyncThunkTypes {
  pending = &quot;pending&quot;,
  fulfilled = &quot;fulfilled&quot;,
  rejected = &quot;rejected&quot;,
}

const todoReducer = createSlice({
  // (...)
  extraReducers: (builder) =&gt; {
    // ...builder.addCase(...)

    builder
      .addMatcher(
      (action: PayloadAction) =&gt; action.type.endsWith(AsyncThunkTypes.pending),
      (state) =&gt; {
        state.isLoading = true;
      }
    )
      .addMatcher(
      (action: PayloadAction) =&gt;
      action.type.endsWith(AsyncThunkTypes.fulfilled || AsyncThunkTypes.rejected),
      (state) =&gt; {
        state.isLoading = false;
      }
    );
  },
}</code></pre>
<p><code>pending</code>일 때의 <code>addMatcher</code>와 <code>fulfilled</code> 또는 <code>rejected</code>일 때의 <code>addMatcher</code>로 나눠 적용했다. 어쨌든 반복되는 부분을 줄여 만족했다.</p>
<p>6) 커스텀 훅 내용 대체</p>
<p><code>auth</code> 때와 마찬가지로 <code>todo</code>도 <code>useTodoList</code> 커스텀 훅의 내용을 수정했다. </p>
<pre><code class="language-ts">function useTodoList() {
  const { createTodo, getTodos, updateTodo, deleteTodo } = useToDoContext();
  const [todoList, setTodoList] = useState&lt;ResponseToDoType[]&gt;([]);
  const { state, loading, onFetching } = useFetch(getTodos);

  useEffect(() =&gt; {
    if (state?.ok) {
      setTodoList(state.todos);
      }
  }, [loading, state]);
  return { ... }
}</code></pre>
<p>상태 관리와 별개로 <code>useFetch</code> 커스텀 훅으로 받아온 데이터를 다시 <code>useState</code>에 저장하는 쓸데없는 코드와 <code>context</code>를 제거했다.</p>
<pre><code class="language-ts">function useTodoList() {
  const {
    todoList: { todos: todoList },
    isLoading: loading,
  } = useAppSelector(selectTodoState);
  const dispatch = useAppDispatch();

  useEffect(() =&gt; {
    dispatch(fetchTodoList());
  }, [dispatch]);
  return { ... }
}</code></pre>
<h2 id="배운점">배운점</h2>
<ol>
<li><code>Redux</code>를 적용하고자 공식 문서를 보면서 열심히 달렸다. 충격적인 사실은 내가 본 것은 <strong><code>Redux</code></strong> 공식 문서였고, 내가 사용한 <code>Redux Toolkit</code>의 공식 문서는 따로 있었다는 사실이다! 어쩐지 API를 아무리 봐도 스펙 설명이 없더라니... 조금 더 꼼꼼히 살펴야겠다. - <a href="https://redux-toolkit.js.org/">Redux Toolkit Docs</a></li>
<li><code>RTK</code>를 처음 사용해봤지만, 개발하는데 필요한 정보는 공식 문서에 다 있었다. 평소 라이브러리를 사용할 때 강의나 서적으로 접해 써 보고, 에러가 발생하면 구글링부터 했다. 그러나 이번에 공식 문서만 보면서 강의 및 구글링의 유혹을 떨쳐냈고, 목표한 바를 구현했다. 중간에 참지 못하고 검색했지만, 그 문제의 해결책 역시 이미 공식 문서에 있었다. 앞으로는 공식 문서로 먼저 공부해야겠다.</li>
<li>개인적으로 상태 관리 라이브러리를 사용하니 로직을 보기 편했다. 중요한 수정은 <code>reducer</code>만 건들면 되니까. 적절한 라이브러리의 사용은 삶의 질을 향상시키는 것 같다.</li>
<li>아쉬운 점은 여전히 보는 게 익숙하지 않아 전체를 살피지는 못했다는 점이다. 분명 더 나은 코드를 작성할 수 있고, 더 좋은 기능이 있을 텐데. 앞으로도 차차 살피면서 익혀야 할 문제다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RTK] 공식 문서만 보고 Redux Toolkit 적용해 보기(1)]]></title>
            <link>https://velog.io/@real-bird/RTK-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Redux-Toolkit-%EC%A0%81%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B01</link>
            <guid>https://velog.io/@real-bird/RTK-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C%EB%A7%8C-%EB%B3%B4%EA%B3%A0-Redux-Toolkit-%EC%A0%81%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B01</guid>
            <pubDate>Fri, 06 Oct 2023 21:22:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공식 문서를 돌같이 보는 버릇을 고치자!</p>
</blockquote>
<p>FE를 꿈꾸면서 상태 관리 라이브러리를 등한시했다. 프로젝트라고 해봤자 개인 프로젝트이고 규모가 토이 수준이다 보니 &#39;굳이?&#39;라는 생각을 많이 했다. <code>React</code>로 개발하니 <code>Context API</code>와 <code>useReducer</code>면 충분했으니까. 하지만 채용 공고에서 상태 관리를 자주 언급되다 보니 익히지 않을 수는 없었다.</p>
<p>그 와중에 나는 공식 문서를 보는데 매우 서툴렀다. 이런 부분도 고치는 의도에서 공식 문서만 보고 토이 프로젝트에 적용해 보는 연습을 계획했다.</p>
<p>제일 처음 고른 공식 문서는 <code>Redux</code>이다. 상태 관리도 익히면서 공식 문서도 읽을 수 있기 때문에 선택했다.</p>
<p>소재로 사용한 토이 프로젝트는 <strong>ToDo App</strong>으로, 예~전에 <em>원티드 프리온보딩 인턴십 프론트엔드 11차</em>에서 선발 과제로 했던 프로젝트를 사용했다.</p>
<h2 id="redux">Redux</h2>
<blockquote>
<p><em>Redux는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너입니다. - <a href="https://ko.redux.js.org/introduction/getting-started">Redux 시작하기</a></em></p>
</blockquote>
<p>공식 문서의 한 줄처럼 <code>JavaScript</code> 환경이라면 어디에서든 사용할 수 있는 상태 관리 라이브러리이다.</p>
<p>상태를 모아두는 하나의 <code>store</code>를 컨테이너처럼 두고, 필요한 곳에서 불러와 사용한다(<strong>진실은 하나의 근원</strong>). 이때, 상태는 불변성을 유지하는 객체이며, 변화는 <code>action</code>이라는 트리거를 통해 동작한다(<strong>상태는 읽기 전용</strong>). 트리거되는 동작의 조건은 순수 함수인 <code>reducer</code>에서 지정한다(<strong>변화는 순수 함수로 작성</strong>). - <a href="https://ko.redux.js.org/understanding/thinking-in-redux/three-principles">3가지 원칙</a></p>
<h2 id="redux는-언제-사용할까">Redux는 언제 사용할까?</h2>
<p>공식 문서에서 <code>Redux</code>를 추천하는 경우는 다음과 같다.</p>
<blockquote>
<ul>
<li>앱의 여러 위치에서 필요한 대량의 애플리케이션 상태가 있는 경우</li>
<li>앱 상태가 자주 업데이트되는 경우</li>
<li>해당 상태를 업데이트하는 로직이 복잡한 경우</li>
<li>앱에 중대형 코드베이스가 있고 많은 사람이 작업하는 경우</li>
<li>시간이 지남에 따라 해당 상태가 어떻게 업데이트되는지 확인해야 하는 경우 - <a href="https://ko.redux.js.org/faq/general/#when-should-i-use-redux">FAQ - General</a></li>
</ul>
</blockquote>
<p>이 설명을 보면 주로 규모가 큰 프로젝트에서 사용하는 라이브러리라는 생각이 든다. 나는 이런 경우가 없었기에 강의나 서적에서 배웠어도 고려를 안 했다. <strong>댄 아브라모브</strong>도 &quot;React에서 문제가 생길 때까지 Redux를 사용하지 말아라&quot;라고 했다니까.</p>
<p>따라서 내 토이 프로젝트에 Redux를 적용하는 것은 이론상 부적절한 부분이다. 그러나 어쩌겠는가, 배우려면 어디엔가는 적용해야 익히지. 나는 그런 마음으로 사용했다.</p>
<h2 id="rtk-적용하기">RTK 적용하기</h2>
<p>강의나 서적에서 <em>Redux + Redux-Thunk</em> 조합이나 <em>Redux + Redux-Saga</em> 조합을 주로 배웠다. 그래서 나도 이렇게 써봐야 하나 고민했는데, 마침 공식 문서에 이런 문장이 있었다.</p>
<blockquote>
<p>Redux Toolkit은 Redux 로직을 작성하기 위해 저희가 공식적으로 추천하는 방법입니다. RTK는 Redux 앱을 만들기에 필수적으로 여기는 패키지와 함수들을 포함합니다. 대부분의 Redux 작업을 단순화하고, 흔한 실수를 방지하며, Redux 앱을 만들기 쉽게 해주는 모범 사례를 통해 만들어졌습니다. - <a href="https://ko.redux.js.org/introduction/getting-started">Redux 시작하기</a></p>
</blockquote>
<p>무려 <strong>공식적</strong>으로 추천하는 방법이며 <strong>모범 사례</strong>를 통해 만들어졌다고 한다. 못 참는 단어의 조합이었다.</p>
<h3 id="0-설치">0) 설치</h3>
<p>가이드에 따라 <code>RTK</code>와 <code>React-Redux</code>를 설치했다. - <a href="https://ko.redux.js.org/tutorials/quick-start">Quick Start</a></p>
<pre><code>npm install @reduxjs/toolkit react-redux</code></pre><p>천천히 살펴 보니 기본 사용법은 배운 것과 큰 차이가 없었다.</p>
<h3 id="1-store-만들기">1) store 만들기</h3>
<pre><code class="language-ts">// store
import { configureStore } from &quot;@reduxjs/toolkit&quot;;

const store = configureStore({
  reducer: {},
});

export default store;</code></pre>
<p>상태 컨테이너로 사용할 store를 만들고 export하고, 최상위 트리에서 <code>Provider</code>에 정의한 store를 추가한다.</p>
<pre><code class="language-tsx">// index.tsx
import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom/client&quot;;
import &quot;./index.css&quot;;
import { RouterProvider } from &quot;react-router-dom&quot;;
import router from &quot;./Router&quot;;
import { HelmetProvider } from &quot;react-helmet-async&quot;;

// Redux를 사용하기 위한 store와 Provider 추가
import { Provider } from &quot;react-redux&quot;;
import store from &quot;./store&quot;;

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

root.render(
  &lt;React.StrictMode&gt;
    &lt;HelmetProvider&gt;
      &lt;Provider store={store}&gt;
        &lt;RouterProvider router={router} /&gt;
      &lt;/Provider&gt;
    &lt;/HelmetProvider&gt;
  &lt;/React.StrictMode&gt;
);</code></pre>
<h3 id="2-탐색하기">2) 탐색하기</h3>
<p>처음 작성하려니 어디서부터 시작해야 할지 막막했다. 일반 <code>Redux</code>와 다르게 <code>createSlice</code>라는 함수를 사용하여 <code>reducer</code>를 생성했기 때문이다. 그래서 조금 천천히 뜯어봤다. - <a href="https://ko.redux.js.org/tutorials/quick-start">Quick Start</a></p>
<pre><code class="language-js">import { createSlice } from &#39;@reduxjs/toolkit&#39;

export const counterSlice = createSlice({
  // store에 저장될 상태의 이름표
  name: &#39;counter&#39;,
  // 초기 상태 값
  initialState: {
    value: 0
  },
  // 상태를 변경하는 reducer들
  reducers: {
    increment: state =&gt; {
      state.value += 1
    },
    decrement: state =&gt; {
      state.value -= 1
    },
    incrementByAmount: (state, action) =&gt; {
      state.value += action.payload
    }
  }
})

// disaptch로 사용할 트리거들
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer</code></pre>
<p>여기서 눈에 띈 점은 <code>state.value += 1</code>처럼 <strong>상태를 직접 변경</strong>하는 듯 보이는 코드였다. &#39;상태는 읽기 전용 아니었나?&#39;라고 생각했으나</p>
<blockquote>
<p>createSlice는 Immer 라이브러리를 사용하는 리듀서를 작성할 수 있게 해줍니다. 이를 통해 state.value = 123과 같은 &quot;변형 (mutating)&quot; JS 문법을 spreads 없이도 불변성을 유지하며 업데이트할 수 있습니다. - <a href="https://ko.redux.js.org/introduction/why-rtk-is-redux-today#redux-toolkit%EC%9D%80-%EB%AC%B4%EC%96%BC-%ED%95%98%EB%82%98%EC%9A%94">Redux Toolkit은 무얼 하나요?</a></p>
</blockquote>
<p>위 문장을 통해 이유를 알 수 있었다. <code>immer</code> 라이브러리는 객체를 불변성으로 만들어주는 역할을 하는데, <code>draft</code>를 통해 <code>draft += 1</code>과 같이 직접 수정하는 것처럼 작성하여 새 상태를 반환해주는 기능을 제공한다. 뭔가 보기에는 불변성을 위배하는 것 같아 찝찝하지만, 근거도 명확하고 신경 쓸게 덜어져 확실히 편하기는 했다.</p>
<p>여기에 더해서, <code>TypeScript</code>를 필수로 사용하므로 <code>TS</code> 예제도 함께 살펴 봤다. <a href="https://ko.redux.js.org/tutorials/typescript-quick-start#project-setup">RTK TypeScript Quick Start - Define Root State and Dispatch Types</a></p>
<pre><code class="language-ts">import { configureStore } from &#39;@reduxjs/toolkit&#39;
// ...

const store = configureStore({
  reducer: {
    posts: postsReducer,
    comments: commentsReducer,
    users: usersReducer
  }
})

// 스토어 자체에서 &#39;RootState&#39; 및 &#39;AppDispatch&#39; 유형을 추론합니다.
export type RootState = ReturnType&lt;typeof store.getState&gt;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch</code></pre>
<p>다른 장소에서 <code>state</code>가 어떤 요소들과 트리거를 지녔는지 알기 위해 <code>RootState</code>와 <code>Dispatch</code> 타입을 정의한다. <code>store.getState()</code>는 <code>Redux</code>에서 <code>store</code>에 저장된 상태를 반환해주는 함수로, <code>ReturnType&lt;typeof store.getState&gt;</code>은 그 함수의 반환 값 타입을 <code>RootState</code>의 타입으로 지정함을 의미한다. 가령, <code>state:RootState</code>라면 그 안에는 <code>posts</code>, <code>comments</code>, <code>users</code>가 담겨 있다.</p>
<p><code>TS</code> 환경에서는 <code>useSelector</code>나 <code>useDispatch</code> 훅을 새로 정의해야 한다.</p>
<pre><code class="language-ts">import { TypedUseSelectorHook, useDispatch, useSelector } from &#39;react-redux&#39;
import type { RootState, AppDispatch } from &#39;./store&#39;

export const useAppSelector: TypedUseSelectorHook&lt;RootState&gt; = useSelector
export const useAppDispatch: () =&gt; AppDispatch = useDispatch</code></pre>
<p>이렇게 정의하는 데에는 특별한 이유가 있다고 한다. - <a href="https://ko.redux.js.org/tutorials/typescript-quick-start#define-typed-hooks">RTK TypeScript Quick Start - Define Typed Hooks</a></p>
<blockquote>
<ul>
<li><code>useSelector</code>의 경우 매번 입력할 필요가 없습니다(<code>state: RootState</code>).</li>
<li><code>useDispatch</code>의 경우 기본 <code>Dispatch</code> 유형은 thunks에 대해 알지 못합니다. thunks를 올바르게 dispatch하려면 store에서 thunk middleware 유형이 포함된 특정 사용자 정의 AppDispatch 유형을 사용하고 이를 useDispatch와 함께 사용해야 합니다. 미리 입력된 useDispatch 훅을 추가하면 필요한 곳에 AppDispatch를 가져오는 것을 잊어버리지 않게 됩니다.</li>
</ul>
</blockquote>
<p><code>reducer</code>부분에서도 타입이 필요해진다. <code>initialState</code>는 물론, 매개변수인 <code>action</code>의 타입도 지정해줘야 한다. - <a href="https://ko.redux.js.org/tutorials/typescript-quick-start#define-slice-state-and-action-types">RTK TypeScript Quick Start - Define Slice State and Action Types</a></p>
<pre><code class="language-ts">import { createSlice, PayloadAction } from &#39;@reduxjs/toolkit&#39;
import type { RootState } from &#39;../../app/store&#39;

interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0
}

export const counterSlice = createSlice({
  name: &#39;counter&#39;,
  initialState,
     // ...
    incrementByAmount: (state, action: PayloadAction&lt;number&gt;) =&gt; {
      state.value += action.payload
    }
  }
})

// export actions...

export const selectCount = (state: RootState) =&gt; state.counter.value

export default counterSlice.reducer</code></pre>
<p>이러면 얼추 준비가 끝난 것 같으니 프로젝트에 적용해 보기로 했다.</p>
<h3 id="3-authreducer">3) authReducer</h3>
<p>내 프로젝트에서 상태를 사용하는 곳은 두 곳이었다. 로그인을 담당하는 <code>auth</code>와 ToDo 서비스를 담당하는 <code>todo</code>. 그 중 <code>auth</code>가 간단해 보였기에 먼저 적용해봤다.</p>
<pre><code class="language-ts">import {
  createAsyncThunk,
  createSlice,
  type PayloadAction,
} from &quot;@reduxjs/toolkit&quot;;
import { RootState } from &quot;../store&quot;;

// 초깃값 설정
const initialState: AuthSliceState = {
  response: { ok: false, message: &quot;&quot; },
  isLoading: false,
  validation: {
    emailError: &quot;&quot;,
    passwordError: &quot;&quot;,
  },
};

export const authReducer = createSlice({
  name: &quot;auth&quot;,
  initialState,
  reducers: {
    // 필요에 따라 상태를 초기화하기 위한 로직을 짰다.
    // all일 경우에는 전체 초깃값으로 변경하고,
    // 아니라면 payload로 받은 속석만 초기화한다.
    initialize: (
      state,
      action: PayloadAction&lt;keyof AuthSliceState | &quot;all&quot;&gt;
    ) =&gt; {
      if (action.payload === &quot;all&quot;) {
        state = initialState;
        return state;
      }
      const newState = {
        ...state,
        [action.payload]: initialState[action.payload],
      };
      return newState;
    },
  },
});

export const { initialize, validate } = authReducer.actions;

export default authReducer.reducer;</code></pre>
<p>문제는 비동기 통신이었다. 자연스럽게 <code>reducer</code>내부에서 <code>async/await</code>을 휘갈기니 에러가 발생했다. <code>actions</code>의 반환 값은 <code>Promise</code> 타입이 아니기 때문에 당연한 결과였다. 그래서 다시 고민에 빠졌다.</p>
<p>어디를 참고해야 하나 방황하다가 <a href="https://ko.redux.js.org/tutorials/essentials/part-5-async-logic#fetching-data-with-createasyncthunk">Redux 핵심, Part 5: Async Logic and Data Fetching - Fetching Data with <code>createAsyncThunk</code></a>를 발견하고 머리가 맑아졌다.</p>
<p><code>createAsyncThunk</code>는 <strong>action type</strong>과 <strong>Promise를 반환하는 콜백함수</strong>를 인자로 받는 함수다. 여기서 반환하는 값이 state로 담긴다.</p>
<pre><code class="language-ts">export const fetchSignin = createAsyncThunk(
  &quot;auth/fetchSignin&quot;,
  async (body: RequestBodyType, { rejectWithValue }) =&gt; {
    try {
      const response = await authService.signin(body);
      return response;
    } catch (e) {
      return rejectWithValue(e);
    }
  }
);</code></pre>
<p>로그인하는 로직만 가져왔다. <code>rejectWithValue</code>는 api 요청이 거부되었을 때 결과를 반환한다. 에러 핸들링을 위한 처리는 아니었고, 무슨 이유로 요청이 실패해서 확인차 넣었다. 물론 넣은 김에 에러 핸들링이 되어서 남겨 두었다.</p>
<p>이렇게 fetch한 데이터는 일반 <code>reducer</code>로는 컨트롤할 수 없고, <code>extraReducer</code>를 사용해야 한다. - <a href="https://ko.redux.js.org/tutorials/essentials/part-5-async-logic#reducers-and-loading-actions">Redux 핵심, Part 5: Async Logic and Data Fetching - Reducers and Loading Actions</a></p>
<pre><code class="language-ts">export const authReducer = createSlice({
  name: &quot;auth&quot;,
  initialState,
  reducers: { ... },

  extraReducers: (builder) =&gt; {
    builder
      .addCase(fetchSignin.pending, (state) =&gt; {
        state.isLoading = true;
      })
      .addCase(fetchSignin.fulfilled, (state, action) =&gt; {
        state.isLoading = false;
        state.response = action.payload;
      })
      .addCase(fetchSignin.rejected, (state) =&gt; {
        state.isLoading = false;
      });
  },
});</code></pre>
<p><code>extraReducers</code>의 <code>builder</code>는 외부에서 작성된 함수의 액션을 실행하여 응답에 대한 상황과 상태를 반환한다. <code>addCase</code>를 통해 여러 상황을 구성할 수 있으며, <code>createAsyncThunk</code>로 만든 함수에 있는 <code>pending</code>, <code>fulfilled</code>, <code>rejected</code>를 이용하여 fetch 로딩 상태나 거부 상태를 손쉽게 설정할 수 있다. 이 부분은 참 매력적이었다. 따로 고민 안 해도 패치 시작 부분과 종료, 거부 부분을 설정할 수 있으니 말이다.</p>
<pre><code class="language-ts">// store/index.ts
const store = configureStore({
  reducer: {
    auth: authReducer,
  },
});

export default store;

// reducer/auth.ts
export const selectAuthState = (state: RootState) =&gt; state.auth;</code></pre>
<p>이렇게 설정한 리듀서를 store에 등록한다. 그리고 select를 등록해두면 사전에 정의한 <code>useAppSelector</code>를 사용할 때 <code>selectAuthState</code>를 매개 변수로 주입해 바로 사용할 수 있다.</p>
<h3 id="4-커스텀-훅-내용-대체">4) 커스텀 훅 내용 대체</h3>
<p>적용한 <code>Redux</code>를 실제로 사용해 기존에 작성한 <code>useSignin</code>이라는 커스텀 훅의 내용을 수정했다. 기존 코드가 아주 더러웠으므로 변한 부분만 짚어 보려고 한다.</p>
<pre><code class="language-ts">// 이전 코드
function useSignin() {
  const { signin } = useSigninContext();
  const { state, onFetching, loading, error } = useFetch&lt;{
    ok: boolean;
    message?: string;
  }&gt;(() =&gt; signin({ email, password }), true );

  const onCompleteSignin = (e: FormEvent) =&gt; {
    e.preventDefault();
    onFetching();
  };
  return { ... }
}

// 현재 코드
function useSignin() {
  const { response, isLoading } = useAppSelector(selectAuthState);
  const dispatch = useAppDispatch();

  const onCompleteSignin = (e: FormEvent) =&gt; {
    e.preventDefault();
    dispatch(fetchSignin({ email, password }));
  };
  return { ... }
}</code></pre>
<p>이전에는 class 객체를 만들어 context로 보낸 다음 메서드를 가져와 사용했고, <code>useFetch</code>라는 커스텀 훅을 만들어 패치한 데이터를 <code>useState</code>에 담아 그것을 반환하여 가져다 썼다. 로직이 반복되어 나름 효율을 추구한다며 만들었지만, 보기에 좋지는 않았다. 또한, 렌더링 시 실행될 로직과 핸들링할 로직을 분리하느라 트리거로 <code>true</code>를 사용했더랬다. <code>true</code>가 있으면 <code>useEffect</code>를 중단하고 <code>onFetching</code>으로 핸들링하는 식이었다.</p>
<p>수정한 코드는 기능 면에서 똑같지만 훨씬 간결하고 직관적이다. 트리거도 <code>dispatch</code>를 통해 내가 핸들링할 수 있다.</p>
<hr>
<p>지면이 길어져 <code>ToDo</code> 파트는 다음에 이어서 작성해야겠다. 이 글과 큰 차이는 없지만 정리에 의의가 있는 거니까.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CSS] Flex Box]]></title>
            <link>https://velog.io/@real-bird/CSS-Flex-Box</link>
            <guid>https://velog.io/@real-bird/CSS-Flex-Box</guid>
            <pubDate>Mon, 25 Sep 2023 11:15:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://nomadcoders.co/css-layout-masterclass/lobby">노마드코더 - CSS Layout 마스터클래스</a> 강의를 정리한 내용입니다.</p>
</blockquote>
<h2 id="before-flexbox">before FlexBox</h2>
<p><strong>FlexBox</strong> 등장 이전에는 요소가 가지는 기본 특성을 이용해 화면을 구성했다.</p>
<p><code>block</code> 속성을 가진 요소는 <code>width</code>와 <code>height</code>를 가지며 화면의 한 줄을 전부 차지하고 다른 요소가 옆으로 오는 것을 막는다.</p>
<p>반면에, <code>inline</code>은 <code>width</code>와 <code>height</code> 속성을 가지지 않으면서 해당 줄(line) 안(in)에 속한다.</p>
<p><code>block</code> 요소를 가지면서 <code>inline</code>으로 정렬하고 싶다면 <code>display: inline-block</code>을 선언한다.</p>
<pre><code class="language-css">.box {
  width: 200px;
  height: 200px;
  background-color: tomato;
  display: inline-block;
}

.box:first-child {
  margin-left: 300px; /* 350px, 310px, 315px ...*/
}

.box:last-child {
  margin-right: 300px; /* 350px, 310px, 315px ...*/
}</code></pre>
<p>화면 정렬에 대한 요구사항이 들어왔다면 위처럼 하나씩 계산해 정렬한다.</p>
<p><code>float</code>을 통해 요소의 정렬을 조금 더 쉽게 할 수 있다. 그러나 세세한 설정은 수동으로 조작해야 한다.</p>
<pre><code class="language-css">.box:first-child {
  float: left;
}

.box:last-child {
  float: right;
}

.box:nth-child(2) {
  margin: 0 50%; /* 25%, 13%, 10% ...*/
}</code></pre>
<p>하나하나 조정해야 하는 방식은 <strong>명령형</strong>이며, 이러한 방법은 번거롭고 어렵다. 다양한 화면 크기가 등장하면서 이 어려움은 더욱 심화되었다.</p>
<p>요구사항이 다양해지면서 <strong>그렇게 되어야 한다</strong>라고 브라우저에게 알려주면 알아서 계산해주는 방식인 <strong>선언형</strong>이 등장했다.</p>
<p>예를 들어, <strong>text-align</strong> 속성은 브라우저에게 <strong>항상 문자열은 이렇게 정렬되어야 한다</strong>를 선언하고, 이후 작성된 문자열은 모두 해당 속성에 맞게 <strong>알아서</strong> 정렬된다.</p>
<p><strong>선언형</strong>으로 화면 구성을 그려주는 것이 <strong>FlexBox</strong>이다.</p>
<h2 id="flexbox">FlexBox</h2>
<p><strong>FlexBox</strong>는 <code>inline-block</code>이나 <code>float</code>과 다르게 자식 요소에 관심이 없다. 부모 요소에 작성하면 그대로 자식 요소를 정렬한다. 단, 바로 아래 있는 <strong>직속</strong> 자식이어야 만한다.</p>
<pre><code class="language-css">.father {
  display: flex;
  justify-content: space-between;
}

.box {
  width: 200px;
  height: 200px;
  background-color: tomato;
}</code></pre>
<p>예시로, <code>justify-content: space-between</code>를 선언하면 <code>margin</code>을 계산할 필요없이 첫 번째와 마지막 <code>box</code>는 양 끝에, 두 번째 <code>box</code>는 가운데로 정렬한다.</p>
<h3 id="flexbox-properties">FlexBox Properties</h3>
<p><code>FlexBox</code>의 속성은 전부 <code>부모</code> 요소에서 작성한다.</p>
<h4 id="gap">gap</h4>
<p>자식 요소마다 <code>margin</code>을 부여하고 싶다면 <code>gap</code> 속성을 사용한다.</p>
<pre><code class="language-css">.father {
  display: flex;
  gap: 10px;
}</code></pre>
<p><code>gap</code>은 <code>row-gap</code>과 <code>column-gap</code>을 합친 속성이다. 한 줄로 행열 <code>margin</code>을 설정하고 싶다면 <code>gap: row-gap column-gap</code> 순서로 작성한다.</p>
<h4 id="flex-direction">flex-direction</h4>
<p>자식 요소 정렬 방향을 정하는 속성은 <code>flex-direction</code>이며, 기본값은 <code>row</code>이다. 세로로 정렬할 시에는 <code>column</code>으로 작성한다. 또한, 정렬되는 자식 요소의 순서를 뒤집을 때는 <code>-reverse</code> 접미사를 더한다.</p>
<pre><code class="language-css">.father {
  display: flex;
  gap: 10px;
  flex-direction: column; /* default: row*/
  /* row-reverse, column-reverse */
}</code></pre>
<h4 id="justify-content주축-and-align-items교차축">justify-content(주축) and align-items(교차축)</h4>
<p><code>flex-direction</code>이 정하는 방향은 매우 중요하다. <code>FlexBox</code>를 정의하는 두 개의 축이 있기 때문이다. <strong><code>Main Axis(주축)</code></strong>와 <strong><code>Cross Axis(교차축)</code></strong>이다.</p>
<p><strong><code>Main Axis(주축)</code></strong>는 방향이 <code>row</code>일 때는 가로선(왼쪽➡오른쪽)이 되고, <code>column</code>일 때는 세로선(위쪽➡아래쪽)이 된다. <strong><code>Cross Axis(교차축)</code></strong>은 그와 반대, 즉 <code>row</code>일 때는 세로선이, <code>column</code>일 때는 가로선이 된다. <code>-reverse</code>는 반대 방향으로 동작한다.</p>
<p><strong><code>Main Axis(주축)</code></strong>의 요소 이동은 <code>justify-content</code> 속성을 사용한다.</p>
<p><strong><code>Cross Axis(교차축)</code></strong>의 요소 이동은 <code>align-items</code> 속성을 사용한다.</p>
<h5 id="flex-direction이-row인-경우"><code>flex-direction</code>이 <code>row</code>인 경우</h5>
<p><img src="https://velog.velcdn.com/images/real-bird/post/503b7982-3d2a-4089-a3f1-fcb0c3dc3d05/image.png" alt="flex-box-row-axis" title="flex-box-row-axis"></p>
<h5 id="flex-direction이-column인-경우"><code>flex-direction</code>이 <code>column</code>인 경우</h5>
<p><img src="https://velog.velcdn.com/images/real-bird/post/a4d3788d-5be3-4fa9-929a-275ccf9c5194/image.png" alt="flex-box-column-axis" title="flex-box-column-axis"></p>
<h4 id="flex-wrap">flex-wrap</h4>
<p>flex 컨테이너가 한 줄인지, 여러 줄인지 정한다.</p>
<p>기본값은 <code>nowrap</code>으로, 자식 요소를 전부 한 줄에 압축해 넣는다. 자식 요소의 기본 크기는 무시된다.</p>
<p><code>wrap</code>인 경우, 컨테이너를 넘치는 자식 요소의 경우 다음 줄로 내린다.</p>
<p><code>-reverse</code> suffix를 가지고 있다.</p>
<h4 id="flex-flow">flex-flow</h4>
<p><code>flex-direction</code>과 <code>flex-wrap</code>을 합쳐 놓은 단축 속성이다. <code>flex-flow: row wrap</code> 형태로 사용한다.</p>
<h4 id="align-content">align-content</h4>
<p>컨테이너가 여러 줄(<code>flex-wrap: wrap</code>)일 때 요소들을 줄에 맞춰 정렬한다.</p>
<pre><code class="language-css">.father {
  display: flex;
  height: 100vh;
  width: 100%;
  gap: 10px;
  justify-content: center;
  align-items: flex-start;
  flex-flow: row wrap;
}</code></pre>
<p>가령, 이렇게 작성하면 첫 번째 줄과 두 번째 줄 사이에 빈 공간이 생긴다. 이 간격은 <code>align-content</code>를 사용해 줄일 수 있다.</p>
<pre><code class="language-css">.father {
  /* ... */
  align-content: center;
}</code></pre>
<p>교차축의 가운데를 기준으로 모든 라인이 정렬된다.</p>
<h4 id="order">order</h4>
<p>자식 요소에 부여하는 속성으로, flex 컨테이너 내부에서 자식 요소의 순서를 결정한다.</p>
<p>순서는 상대적으로 정해지며, 기본값은 <code>0</code>이다.</p>
<pre><code class="language-css">.box:nth-child(3) {
  order: 1;
  background-color: blueviolet;
}

.box:nth-child(6) {
  order: 2;
  background-color: aqua;
}</code></pre>
<p>이렇게 작성할 경우 <code>box 3</code>은 끝에서 <strong>2번째</strong>로, <code>box 6</code>은 <strong>맨끝</strong>으로 이동한다. 나머지 <code>box</code>의 <code>order</code>가 <code>0</code>이기 때문이다.</p>
<h4 id="align-self">align-self</h4>
<p>자식 요소에 부여하는 속성으로, 요소에 개별적으로 <code>align-items</code> 속성을 적용한다.</p>
<pre><code class="language-css">.father {
  display: flex;
  height: 100vh;
  width: 100%;
  gap: 10px;
  justify-content: center;
  align-items: center;
  flex-flow: row wrap;
}

.box {
  width: 200px;
  height: 200px;
  background-color: tomato;
}

.box:first-child {
  background-color: blueviolet;
  align-self: flex-start;
}

.box:last-child {
  background-color: aqua;
  align-self: flex-end;
}</code></pre>
<h4 id="flex-grow">flex-grow</h4>
<p>자식 요소에서 설정하며, 값을 직접 설정하지 않고 flex 컨테이너의 크기만큼 늘리는 속성이다. 기본값은 <strong>0</strong>이고 단위는 비율로 사용한다. <code>flex-grow: 1</code>이라면 flex 컨테이너의 공간 최대 크기만큼 늘어난다.</p>
<pre><code class="language-css">.box:first-child {
  background-color: blueviolet;
  flex-grow: 1;
}

.box:last-child {
  background-color: aqua;
  flex-grow: 1;
}</code></pre>
<p>두 개의 박스가 1:1 비율로 컨테이너의 공간만큼 늘어난다.</p>
<h4 id="flex-shrink">flex-shrink</h4>
<p>자식 요소에서 설정하며, 값을 직접 설정하지 않고 flex 컨테이너의 비율에 맞춰 줄이는 속성이다. 기본값은 <strong>1</strong>이다.</p>
<pre><code class="language-css">.box:first-child {
  background-color: blueviolet;
  flex-shrink: 3;
}

.box:nth-child(2) {
  background-color: teal;
  flex-shrink: 0;
}

.box:last-child {
  background-color: aqua;
  flex-shrink: 1;
}</code></pre>
<p><code>box 2</code>는 화면 크기가 줄어도 크기가 그대로인 반면, <code>box 1</code>과 <code>box 3</code>은 크기가 줄어든다.</p>
<h4 id="flex-basis">flex-basis</h4>
<p>자식 요소에서 설정하며, <code>flex-grow</code>와 <code>flex-shrink</code>의 초기값을 설정하는 속성이다. <code>flex-basis: 500px</code>로 설정하면 크기가 <code>500</code>을 넘어가는 시점에서 <code>grow</code>되거나 <code>shrink</code>된다.</p>
<pre><code class="language-css">.box:first-child {
  background-color: blueviolet;
  flex-grow: 1;
  flex-shrink: 0;
  flex-basis: 500px;
}</code></pre>
<p>위 코드에서 <code>box 1</code>은 화면이 아무리 작아져도 <code>500px</code> 아래로는 줄어들지 않는다.</p>
<h4 id="flex">flex</h4>
<p>위의 세 속성, <code>flex-grow</code>, <code>flex-shrink</code>, <code>flex-basis</code>를 하나로 합친 단축 속성이다. <code>flex: grow shrink basis</code> 순서로 적는다.</p>
<pre><code class="language-css">.box:first-child {
  background-color: blueviolet;
  flex: 1 0 500px;
}</code></pre>
<h4 id="flexbox-froggy-game">Flexbox Froggy Game</h4>
<p>FlexBox를 게임으로 연습할 수 있는 사이트이다.</p>
<p><a href="https://flexboxfroggy.com/#ko">https://flexboxfroggy.com/#ko</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 빌드 용량 최적화하기(feat. Vite)]]></title>
            <link>https://velog.io/@real-bird/React-%EB%B9%8C%EB%93%9C-%EC%9A%A9%EB%9F%89-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0feat.-Vite</link>
            <guid>https://velog.io/@real-bird/React-%EB%B9%8C%EB%93%9C-%EC%9A%A9%EB%9F%89-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0feat.-Vite</guid>
            <pubDate>Sun, 20 Aug 2023 07:03:47 GMT</pubDate>
            <description><![CDATA[<p>얼마 전에 본 면접에서 <code>SPA(Single Page Application)</code>의 단점을 어떻게 해결하느냐에 대한 질문을 받았다. &quot;코드 스플리팅을 통해 파일을 여러 개로 분산하여 로드한다&quot;라고 답했으나, 양심의 가책을 느꼈다. 내 프로젝트에서는 코드를 나눴어도 <code>index.js</code>의 용량이 어마어마했기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/72abba23-380f-4fd6-ba7d-5683ca478007/image.png" alt="bip 빌드 후 용량"></p>
<p><code>lazy</code>를 사용하여 컴포넌트 코드는 스플리팅했지만, 중심이 되는 파일의 용량은 매우 컸다. 뭐 이리 뚱뚱한가 봤더니 사용한 라이브러리가 모두 <code>index</code>에 담겨 있었다. 고민했지만 답은 경고 문구에 있었다.</p>
<blockquote>
<p>Use build.rollupOptions.output.manualChunks to improve chunking: <a href="https://rollupjs.org/configuration-options/#output-manualchunks">https://rollupjs.org/configuration-options/#output-manualchunks</a></p>
</blockquote>
<p><code>vite</code>에서 빌드 도구로 사용하는 <code>rollup</code>의 수동 스플릿 방법을 안내한다. 공통으로 사용하는 모듈을 분리하는 역할이다. 사용법을 익히기 위해 간단한 프로젝트를 만들어봤다.</p>
<h2 id="setting">setting</h2>
<p><code>npm create vite@latest</code>를 실행하고 <code>React-js</code> 환경으로 설정했다. 페이지를 스플리팅하기 위해 <code>react-router-dom</code>도 함께 설치했다.</p>
<pre><code>/src
  ┣─ App.jsx
  ├─ About.jsx
  └─ main.jsx</code></pre><p>대충 이렇게 파일을 만들고 라우트를 작성했다.</p>
<pre><code class="language-jsx">import React, { Suspense, lazy } from &quot;react&quot;;
import ReactDOM from &quot;react-dom/client&quot;;
import &quot;./index.css&quot;;
import { BrowserRouter, Routes, Route, Link } from &quot;react-router-dom&quot;;
import App from &quot;./App&quot;;
import About from &quot;./About&quot;;

ReactDOM.createRoot(document.getElementById(&quot;root&quot;)).render(
  &lt;React.StrictMode&gt;
    &lt;BrowserRouter basename=&quot;/&quot;&gt;
      &lt;nav&gt;
        {/* 대충 네비게이션 */}
      &lt;/nav&gt;
      &lt;Routes&gt;
        {/* 대충 라우트 */}
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  &lt;/React.StrictMode&gt;
);</code></pre>
<p>기본적인 방식으로 컴포넌트를 라우트에 등록했다. 이 상태로 여과 없이 빌드해 보자.</p>
<h2 id="초기-빌드">초기 빌드</h2>
<p><img src="https://velog.velcdn.com/images/real-bird/post/119e8274-fff3-40ee-aa9a-bd6d02e741e5/image.png" alt="초기 빌드"></p>
<p>빌드 로그를 보면 파일 세 개만 생겼다. 내가 사용한 모듈과 컴포넌트는 모두 <code>index.js</code>에 들어있을 터이다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/61af29bc-ba02-4592-9ee8-b2898d46308d/image.png" alt="초기 빌드 후 로드"></p>
<p>빌드한 파일을 실행해 보면 빌드된 단 하나의 <code>index.js</code>만 로드한다. 큰 프로젝트가 이렇게 로드된다고 생각해 보자. <code>SPA</code>의 단점인 <strong>초기 로딩 속도 느림</strong>이 아주 도드라질 것이다.</p>
<h2 id="lazy-사용-빌드">lazy 사용 빌드</h2>
<p>이번에는 <code>React.lazy</code>를 사용해서 스플리팅 후 빌드해보자.</p>
<pre><code class="language-jsx">const App = lazy(() =&gt; import(&quot;./App&quot;));
const About = lazy(() =&gt; import(&quot;./About&quot;));

ReactDOM.createRoot(document.getElementById(&quot;root&quot;)).render(
  &lt;React.StrictMode&gt;
    &lt;BrowserRouter basename=&quot;/&quot;&gt;
      &lt;nav&gt;
        {/* 대충 네비게이션 */}
      &lt;/nav&gt;
      &lt;Routes&gt;
        {/* 대충 라우트 */}
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  &lt;/React.StrictMode&gt;
);</code></pre>
<p><img src="https://velog.velcdn.com/images/real-bird/post/f36b36a1-4076-4a99-b445-669dc41e2435/image.png" alt="lazy 빌드"></p>
<p>의도한 대로 <code>App</code>과 <code>About</code> 컴포넌트는 코드가 분리되어 새로운 파일로 생성되었다. 여기까지가 내가 생각한 코드 스플리팅이었는데, 아무리 봐도 저 <code>index.js</code>의 크기가 납득되지 않았다. 겨우 <code>0.11kb</code> 분리하고 스플리팅이라니. 그렇기 때문에 위에서 언급한 <code>rollupOptions</code>를 추가한 것이다.</p>
<h2 id="manualchunks-빌드">manualChunks 빌드</h2>
<p><code>rollupOptions</code>는 <code>vite.config.js</code>에서 <code>build</code> 속성에 추가하여 설정한다.</p>
<pre><code class="language-js">import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: (id) =&gt; {
          if (id.includes(&quot;node_modules&quot;)) {
            return `vendor`;
          }
        },
      },
    },
  },
});</code></pre>
<p><code>manualChunks</code>에 추입하는 <code>id</code>에 뭐가 들었나 궁금해 콘솔을 찍어봤더니 사용한 모듈의 경로가 쭉 나왔다. 이 상태로 빌드하면 <code>node_modules</code>에서 사용한 코드가 <code>vendor.js</code> 에 모두 모인다. <code>index.js</code>는 가벼워지겠지만, <code>vendor</code>가 무거워진다. 라이브러리가 무거운 거야 어쩔 수 없지마는, 조절할 수 있다면 최대한 줄여보는 게 좋겠다 싶었다.</p>
<p>일단 <code>id</code>에 찍힌 모듈 모양은 이렇다.</p>
<pre><code>D:/work-space/test-build/node_modules/react-dom/index.js?commonjs-module
D:/work-space/test-build/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-proxy
D:/work-space/test-build/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports
...
D:/work-space/test-build/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports</code></pre><p>필요한 부분은 <code>node_modules</code> 뒷부분의 모듈명이다. 이것만 잘라낸다.</p>
<pre><code class="language-js">const module = id.split(&quot;node_modules/&quot;).pop().split(&quot;/&quot;)[0];

/*
...
react
react
react
react-dom
scheduler
scheduler
*/</code></pre>
<p><code>vendor/${module}</code>을 반환값으로 설정하면 <code>vendor</code> 디렉토리에 모듈별로 파일이 생긴다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/35ebbadd-54f3-4521-b780-ac79f478d76e/image.png" alt="rollupOptions 수정 빌드"></p>
<p>빌드 파일로 앱을 실행해 보면 차이점이 확연히 드러난다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/301fa1a5-cbc4-45d1-941f-62f24a4cc57e/image.png" alt="rollupOptions 수정 빌드 후 로드"></p>
<h2 id="결과">결과</h2>
<p>이런 방법으로 내 프로젝트의 빌드 용량을 최적화하여 <strong>690KB</strong>에 육박하던 <code>index.js</code> 크기를 약 <strong>30KB</strong>로 줄였다.</p>
<p>찾은 방법으로 코드를 나누긴 했지만 맞는 방법인지는 모르겠다. 더 나은 방법은 더 찾아봐야겠지만, 소기의 목적은 달성했다. 공부하다 보면 더 나은 방법이 떠오를지도 모르겠다.</p>
<hr>
<p><strong>참고</strong>
<a href="https://rollupjs.org/configuration-options/#output-manualchunks">rollup - Configuration Options#output-manualChunks</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Context에 전달된 인스턴스 bind한 이유]]></title>
            <link>https://velog.io/@real-bird/React-Context%EC%97%90-%EC%A0%84%EB%8B%AC%EB%90%9C-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-bind%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@real-bird/React-Context%EC%97%90-%EC%A0%84%EB%8B%AC%EB%90%9C-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-bind%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 15 Aug 2023 09:14:30 GMT</pubDate>
            <description><![CDATA[<p>약 한 달 전, <strong>원티드</strong>에서 진행한 <strong>프리온보딩 프론트엔드 인턴십 7월</strong>의 과제를 진행하면서 팀원의 코드 리뷰를 받은 일이 있었다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/9dac6816-683d-4705-8a67-ec33edc209dd/image.png" alt="context bind" title="context bind"></p>
<p>당시 멘토의 코드를 참고해 작성한 터라 왜 그렇게 했는지 이유를 몰랐다. 그냥 &#39;아, 아무 생각 없이 작성했구나&#39; 하고 넘겼다. 그럼에도 한 달 내내 이 궁금증이 가시지 않아 드디어 찾아보게 되었다.</p>
<h2 id="1-typeerror-cannot-read-properties-of-undefined">1. TypeError: Cannot read properties of undefined</h2>
<p>일단 어떤 문제가 발생하는지 궁금해서 issues를 불러오는 메서드의 <code>bind</code>를 제거했다.</p>
<pre><code>TypeError: Cannot read properties of undefined (reading &#39;httpClient&#39;)</code></pre><p>context에서 props로 받은 <code>issuesInstance</code>는 <code>httpClient</code>를 인자로 받아 <code>this.httpClient</code>에 할당한 후 <code>fetch</code> 메서드를 실행한다. 그러므로 여기서 <code>httpClient</code> 속성을 읽지 못하는 것은 <code>this</code>에 해당 속성이 없음을 말한다.</p>
<h2 id="2-재현">2. 재현</h2>
<p>프로젝트를 일일이 뜯어가며 재현하기는 어려워서 간단한 재현 코드를 작성했다.</p>
<pre><code class="language-js">class TempClass {
  constructor() {
    this.one = 1;
    this.two = 2;
  }

  sum() {
    return this.one + this.two;
  }
}

const tempClass = new TempClass();

const abc = (tempClass) =&gt; {
  const x = tempClass.sum.bind(tempClass);
  return x;
};

console.log(abc(tempClass)()); // 3</code></pre>
<p>정상적으로 <code>TempClass</code>를 <code>bind</code>한 <code>abc</code> 함수는 기댓값인 <code>3</code>을 반환한다. 여기서 <code>bind</code>를 지우면 위의 에러가 재현된다.</p>
<pre><code class="language-js">const tempClass = new TempClass();

const abc = (tempClass) =&gt; {
  const x = tempClass.sum;
  return x;
};

console.log(abc(tempClass)());
// TypeError: Cannot read properties of undefined (reading &#39;one&#39;)</code></pre>
<p><code>sum</code> 메서드의 <code>this</code>를 찍어 보면 <code>undefined</code>가 나온다. 인스턴스를 제대로 전달한 것 같은데 왜 에러가 발생하는 것일까? 문제의 원인은 <code>JavaScript</code>의 <code>this</code> 호출 방식에 있었다.</p>
<h2 id="3-this">3. this</h2>
<p>여타 객체 지향 언어에서 <code>this</code>는 클래스로 생성한 객체를 가리키지만, <code>JS</code>의 경우는 조금 다르다고 한다. <strong>실행 컨텍스트</strong>가 생성될 때 <code>this</code>가 생성되기 때문에 함수 호출 방식에 따라 가리키는 방향이 달라진다.</p>
<h3 id="3-1-전역에서의-this">3-1. 전역에서의 this</h3>
<p>전역에서 <code>this</code>는 당연히 전역 객체를 가리킨다. 브라우저라면 <code>window</code>, 노드라면 <code>global</code>이다. 전역 변수로 선언한 <code>x</code>가 있다면 이는 <code>window.x</code>와 <code>this.x</code>가 동일한 참조를 가진다. 자바스크립트 엔진이 전역 변수 <code>x</code>를 전역 객체에 할당하고, <code>this</code>는 그 전역 객체를 가리키기 때문이다.</p>
<pre><code class="language-js">const x = 1;
window.x // 1
this.x // 1</code></pre>
<h3 id="3-2-메서드에서의-this">3-2. 메서드에서의 this</h3>
<p>메서드는 <strong>객체의 속성으로 할당하고 객체의 메서드로서 호출</strong>하는 함수를 말한다.</p>
<pre><code class="language-js">const obj = {
  x: 1,
  method: function () {
    return console.log(this);
  },
};

obj.method();

// { x: 1, method: [Function: method] }</code></pre>
<p>메서드의 <code>this</code>는 점 앞의 객체를 가리킨다.</p>
<h3 id="3-3-함수에서의-this">3-3. 함수에서의 this</h3>
<p>일반 함수로 호출하는 경우에는 <code>this</code>가 지정되지 않고 전역 객체를 가리킨다.</p>
<pre><code class="language-js">function checkThis(text) {
  return console.log(text, &quot; &quot;, this);
}

checkThis(&quot;common func&quot;); // common func   Window {window: Window, …}</code></pre>
<p>이처럼 일반 함수에서 <code>this</code> 호출은 전역 객체가 바인딩되므로 주의해야 한다. <code>method</code>로서 선언하였다 하더라도 호출 자체를 일반 함수로 한다면 <code>this</code>는 객체를 가리키지 않는다.</p>
<h3 id="3-4-콜백-함수에서의-this">3-4. 콜백 함수에서의 this</h3>
<p>콜백 함수에서도 마찬가지로 일반 함수로 호출된 경우 <code>this</code>는 전역 객체를 가리킨다. 반면, 어떤 객체의 메서드 내에서 실행된다면 <code>this</code>는 해당 객체를 가리킨다.</p>
<pre><code class="language-js">setTimeout(func(&quot;callback func&quot;), 300);
// callback func   Window {window: Window, …}

[1, 2, 3, 4, 5].forEach(func);
/*
1 &#39; &#39; Window {window: Window, …}
2 &#39; &#39; Window {window: Window, …}
3 &#39; &#39; Window {window: Window, …}
4 &#39; &#39; Window {window: Window, …}
5 &#39; &#39; Window {window: Window, …}
*/

document.body.innerHTML += `&lt;button id=&quot;a&quot;&gt;클릭&lt;/button&gt;`;
document.body.querySelector(&quot;#a&quot;).addEventListener(&quot;click&quot;, function (e) {
  console.log(this);
});
// &lt;button id=&quot;a&quot;&gt;클릭&lt;/button&gt;</code></pre>
<h3 id="3-5-생성자-함수에서의-this">3-5. 생성자 함수에서의 this</h3>
<p>생성자 함수는 <code>new</code> 키워드를 통해 호출한 함수로, 새로운 인스턴스를 생성하며 <code>this</code>는 자기자신을 가리킨다.</p>
<pre><code class="language-js">new checkThis(&quot;me&quot;); // me   checkThis {}</code></pre>
<h2 id="4-메서드에서-일반-함수로">4. 메서드에서 일반 함수로</h2>
<p>대강 <code>this</code>를 살펴봤으니 재현 코드의 문제점으로 돌아가 보자.</p>
<pre><code class="language-js">const tempClass = new TempClass();

const abc = (tempClass) =&gt; {
  const x = tempClass.sum;
  return x;
};

console.log(abc(tempClass)());
// TypeError: Cannot read properties of undefined (reading &#39;one&#39;)</code></pre>
<p>오류가 발생한 이유는 <strong>메서드에서 일반 함수로 변경</strong>되었기 때문이다. <code>abc</code> 내부의 <code>x</code>는 <code>tempClass.sum</code> 참조만 하고 실행하지 않았기 때문에 <code>TempClass</code>에 대한 인스턴스와 관련된 실행 컨텍스트가 생성되지 않는다. 오히려 일반 함수 실행 컨텍스트가 생성되어 <code>TempClass</code>와의 연관성이 사라진다. 그렇기에 <code>x</code>로부터 실행되는 <code>sum</code> 메소드의 <code>this</code>는 아무것도 가리키지 않는 <code>undefined</code>가 된다. 따라서 명시적인 바인딩을 통해 <code>this</code>가 가리켜야 할 인스턴스를 알려주면 문제없이 실행된다.</p>
<pre><code class="language-js">class TempClass {
  constructor() {
    this.one = 1;
    this.two = 2;
  }

  sum() {
    console.log(this); // TempClass {one: 1, two: 2}
    return this.one + this.two;
  }
}

const tempClass = new TempClass();

const abc = (tempClass) =&gt; {
  const x = tempClass.sum.bind(tempClass);
  return x;
};

console.log(abc(tempClass)()); // 3</code></pre>
<h2 id="5-결론">5. 결론</h2>
<p><code>React</code>의 <code>Context API</code>에서 인스턴스의 여러 메서드를 할당하고 명시적으로 바인딩한 이유를 다시 살펴봤다. 과제 코드에서는 필요한 곳에서 메서드를 실행하기 위해 참조만 변수에 할당했다. 그렇기 때문에 인스턴스와 연관성이 사라졌고 <code>this</code>는 갈 곳을 잃어 에러를 뱉어냈다. 아마 <code>Context</code> 내에서 메서드를 모두 실행한 후 결과만 <code>value</code>로 넘기는 식이었다면 명시적 바인드가 필요없었을 것이다.</p>
<p>참 <code>this</code>는 어렵다.</p>
<hr>
<p><strong>참고</strong>
<a href="http://aladin.kr/p/VWuS0" title="코어 자바스크립트">코어 자바스크립트 - this</a>
<a href="https://chat.openai.com/">ChatGPT</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL 21] - [React] 라이브러리 없이 SPA 구현 리팩토링]]></title>
            <link>https://velog.io/@real-bird/TIL-21-React-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%97%86%EC%9D%B4-SPA-%EA%B5%AC%ED%98%84-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@real-bird/TIL-21-React-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%97%86%EC%9D%B4-SPA-%EA%B5%AC%ED%98%84-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</guid>
            <pubDate>Sat, 08 Jul 2023 16:59:20 GMT</pubDate>
            <description><![CDATA[<p><strong>프리온보딩 챌린지 FE 7월</strong> 과제로 라이브러리 없이 SPA 라우터를 구현했다. 잘 동작하길래 만족했더니 함정이 숨어 있었다. 과제에서 제시한 대로만 구현하면 불필요한 마운트가 생긴다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/50e7babd-c15b-4ebb-a18d-ec74d2f6ad96/image.png" alt="non-essential component"></p>
<p>구현하면서 현재 <code>Root</code>에 있다면 렌더링하지 않은 <code>About</code> 라우트는 나타나지 않기를 기대했다. 숨겨진 진실(?)을 확인하니 많이 당황스러웠다.</p>
<p>피드백 내용과 제공해준 블로그 내용을 바탕으로 코드를 수정했다.</p>
<h2 id="1-route-내에서-렌더링-로직-실행">1. <code>Route</code> 내에서 렌더링 로직 실행</h2>
<p>현재 url의 pathname과 요청한 path가 같으면 컴포넌트가 렌더링된다. 이 로직은 <code>Route</code>에서 실행되었다.</p>
<pre><code class="language-tsx">const Route = ({ path, component }: RouteProps) =&gt; {
  const pathname = useRouterState();
  const setPathname = useRouterDispatch();
  const [isCurrentPath, setIsCurrentPath] = useState(false);

  useEffect(() =&gt; {
    setIsCurrentPath(path === pathname ? true : false);

    const handlePopstate = () =&gt; {
      setPathname(window.location.pathname);
      return;
    };

    window.addEventListener(&quot;popstate&quot;, handlePopstate);

    return () =&gt; window.removeEventListener(&quot;popstate&quot;, handlePopstate);
  }, [pathname]);

  return isCurrentPath ? component : null;
};</code></pre>
<p>얼핏 보면 현재 경로와 같을 때 <code>component</code>를 반환하고, 아닐 경우 <code>null</code>을 반환하니 잘 동작할 것 같다. 그러나 이는 <code>Route</code>의 하위 컴포넌트를 결정하는 것이고, 어쨌든 <code>null</code>도 객체이니 <code>Route</code>는 존재하는 컴포넌트로 마운트된다.</p>
<p>게다가 <code>useEffect</code>까지 사용하여 렌더링되라고 주문을 외웠으니 남지 않기를 바라는 게 욕심이었다.</p>
<p>내가 바라는 결과는 현재 경로의 <code>Route</code>를 제외한 나머지는 마운트하지 않는 것이다. 일단 <strong>로직부터 제거</strong>했다.</p>
<pre><code class="language-tsx">interface RouteProps {
  path: string;
  component: ReactElement;
}

const Route = (props: RouteProps) =&gt; {
  return &lt;&gt;{props.component}&lt;/&gt;;
};</code></pre>
<p>이제 <code>Route</code>는 렌더링 페이지 컴포넌트를 담는 깡통이 되었다.</p>
<h2 id="2-제거한-로직-위치-변경">2. 제거한 로직 위치 변경</h2>
<p>경로 처리 로직은 필요하니 어딘가에 넣어야 하는데 처음에는 <code>routeContext</code>인 <code>Routes</code>에서 처리하려고 했다. 실제로 코드를 작성해 실행해 봐도 아무런 탈 없이 잘 굴러갔다. 그러나 <code>pathname</code>의 상태관리를 위해 작성한 context에서 모든 로직을 처리해도 되나? 하는 의문점이 생겼다. 최근 <strong>관심사 분리</strong>에 대해 들었기 때문이었다. 선행한 분의 블로그에도 그런 이유로 처리 로직을 분리했다고 한다.</p>
<p>여기서 <code>Router</code>와 <code>Route</code>로 구현하라고 했었구나, 하는 작은 깨달음을 얻었다. 나는 그게 그거려니 생각해 대충 <code>Routes</code>와 <code>Route</code>로 나눴었다. 그러니 다시 중간 매개체를 고려해야 하는 문제가 발생했다.</p>
<p><code>routeContext</code>는 <code>Router</code>로 수정하고, 로직을 담당하는 <code>Routes</code>를 새로 생성했다.</p>
<p>처리 방안을 모색했던 컴포넌트와 path를 검증하는 로직을 <code>Routes</code>에서 처리했다.</p>
<pre><code class="language-tsx">const Routes = ({ children }: RoutesProps) =&gt; {
  const pathname = useRouterState();
  const setPathname = useRouterDispatch();

  /* 추가 */
  const isCurrentPathComponent = (
    component: ReactElement&lt;{ path: string }&gt;
  ) =&gt; {
    return pathname === component.props.path;
  };
  /* **** */
  useEffect(() =&gt; {
    const handlePopstate = () =&gt; {
      setPathname(window.location.pathname);
      return;
    };

    window.addEventListener(&quot;popstate&quot;, handlePopstate);

    return () =&gt; window.removeEventListener(&quot;popstate&quot;, handlePopstate);
  }, [pathname]);

  return &lt;&gt;{children.find(isCurrentPathComponent) ?? &lt;NotFound /&gt;}&lt;/&gt;;
};</code></pre>
<p><code>children</code>에서 <code>url pathname</code>과 컴포넌트의 <code>path</code>가 일치하는지 검증하는 함수 <code>isCurrentPathComponent</code>를 추가했다. 반환하는 컴포넌트는 <code>children</code>을 순회하면서 <code>true</code>인 값인 하위 <code>Route</code>만 렌더링한다. 추가로 잘못된 페이지 접근도 처리했다.</p>
<h2 id="3-route가-하나인-경우-처리">3. Route가 하나인 경우 처리</h2>
<p>그러나 한 가지 미스가 또 있었다. 항상 <code>Route</code>가 여러 개 들어올 거라는 확신이었다. <code>children</code>의 타입을 <code>ReactElemental[]</code>로 선언했는데, 모종의 이유로 <code>Route</code>를 하나만 입력하면 에러가 발생한다. <code>children</code>이 배열인지 확인하고, 아닐 경우 배열로 바꿔주는 코드를 추가했다.</p>
<pre><code class="language-tsx">const childrenArray = Array.isArray(children) ? children : [children];</code></pre>
<p>이제 불필요한 <code>Route</code>를 마운트하지 않으면서 원하는 대로 동작한다.</p>
<p><img src="https://velog.velcdn.com/images/real-bird/post/3a600728-c4ee-4ec4-92b6-ce49129e201b/image.png" alt="complete"></p>
<h2 id="4-isvalidelement-">4. isValidElement ?</h2>
<p><code>React</code>에서 제공해주는 함수 중 <code>isValidElement</code>는 값이 React 엘리먼트인지 확인하여 boolean을 반환한다.</p>
<blockquote>
<p><code>isValidElement</code>
isValidElement checks whether a value is a React element.
isValidElement는 값이 React 엘리먼트인지 확인합니다.</p>
</blockquote>
<p>선행하신 분의 글에 나와 있어서 이걸 왜 사용했나 고민했다. 없어도 잘 굴러가던데……. React 엘리먼트가 아닌 것이 들어오는 경우도 있나?</p>
<p>나의 경우 타입 선언 자체를 <code>ReactElement</code>로 선언하여 아무런 문제가 없는데, 만약 <code>ReactNode</code>로 선언한 경우 검증이 필요할 수 있다.</p>
<pre><code class="language-tsx">const isCurrentPathComponent = (component: ReactNode) =&gt; {
  if (!isValidElement(component)) {
    return false;
  }
  return pathname === component.props.path;
};</code></pre>
<p><code>ReactNode</code>는 다양한 속성으로 이루어져 있다.</p>
<pre><code class="language-ts">type ReactNode =
  | ReactElement
  | string
  | number
  | Iterable&lt;ReactNode&gt;
  | ReactPortal
  | boolean
  | null
  | undefined;</code></pre>
<p>만약 <code>isValidElement</code>를 통과하지 못하면 저 중 어떠한 속성인지 <code>TypeScript</code>는 알지 못한다. 통과하면 저 속성 중 <code>ReactElement</code>이므로 <code>props</code>에 접근할 수 있다.</p>
<p>지금처럼 간단하고 확실한 경우에는 직접 타입과 제네릭을 선언해도 되지만, 더 복잡한 경우에는 아주 유용한 함수일 것 같다.</p>
<hr>
<p><strong>참고</strong>
<a href="https://ghoon99.tistory.com/91">React Router를 직접 만들어보는 과정</a></p>
]]></description>
        </item>
    </channel>
</rss>