<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sxungchxn.dev</title>
        <link>https://velog.io/</link>
        <description>🏠 버튼을 누르면 더 많은 글들을 보실 수 있습니다</description>
        <lastBuildDate>Wed, 17 Jul 2024 01:52:23 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sxungchxn.dev</title>
            <url>https://velog.velcdn.com/images/seungchan__y/profile/d4b5dd60-56df-4a5e-ab40-81075343dd11/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sxungchxn.dev. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/seungchan__y" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Notion으로 나만의 블로그 CMS 만들기]]></title>
            <link>https://velog.io/@seungchan__y/Notion%EC%9C%BC%EB%A1%9C-%EB%82%98%EB%A7%8C%EC%9D%98-%EB%B8%94%EB%A1%9C%EA%B7%B8-CMS-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/Notion%EC%9C%BC%EB%A1%9C-%EB%82%98%EB%A7%8C%EC%9D%98-%EB%B8%94%EB%A1%9C%EA%B7%B8-CMS-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 17 Jul 2024 01:52:23 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 <a href="https://www.sxungchxn.dev/blog/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10">여기서</a> 더 멋지게 볼 수 있습니다.</p>
</blockquote>
<blockquote>
<p>블로그 레포지토리는 <a href="https://github.com/sxungchxn/sxungchxn-dev">여기서</a> 확인하실 수 있습니다.</p>
</blockquote>
<h1 id="intro">Intro</h1>
<p><code>notion</code> 을 통한 기존의 에디터 경험은 살릴 수 있으면서도 블로그 CMS로서 유용한 기능들을 제공할 수 있도록 추가한 다음의 기능들에 대해 공유 해보고자 한다. </p>
<ul>
<li>블로그 데이터를 만들기 위한 데이터베이스 구축</li>
<li>블로그 글을 개성넘치는 웹페이지로 보여주기 위한 렌더링 기능</li>
<li>노션 이미지의 한계를 극복하기 위한 이미지 업로드</li>
<li>정적 페이지를 빌드 없이 쉽게 업데이트 하도록 파이프라인 만들기</li>
</ul>
<p>이를 구축해놓으면 <code>notion</code> 을 에디터를 넘어서 편리한 블로그 CMS가 될 수 있도록 만들 수 있다.</p>
<h1 id="💾-notion을-데이터베이스로-활용하기">💾 notion을 데이터베이스로 활용하기</h1>
<h2 id="데이터베이스-구축하기">데이터베이스 구축하기</h2>
<p><code>notion</code> 에 작성한 블로그 글을 데이터로서 활용하기 위해 데이터베이스를 구축하는 작업이다. 여기서는 <code>notion</code> 에서 데이터베이스라는 <code>block</code>을 활용한다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054620/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_1.png" alt="Untitled"></p>
<p>이 데이터베이스 블록을 통해 블로그 데이터를 아래와 같이 표의 형태로 만들어낼 수 있다. 구축하려는 사람의 목적마다 다르겠으나 나의 경우에는 다음과 같이 테이블의 컬럼들을 생성하였다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054623/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_2.png" alt="Untitled"></p>
<ul>
<li><code>id</code> : 게시글 별 고유 ID</li>
<li><code>name</code> : 게시글 제목</li>
<li><code>description</code> : 블로그 게시글 요약 문장</li>
<li><code>tags</code> : 게시글 토픽과 관련한 주제 태그들</li>
<li><code>releasable</code> : 초안이 아니라 블로그에서 보여줄 수 있는 글인지를 표시하는 데이터</li>
<li><code>featured</code> : 인기 게시글로 구분되는 글인지를 표시하는 데이터</li>
<li><code>createdAt</code> : 블로그 게시글 생성일자</li>
<li><code>updatedAt</code> : 게시글 업데이트 일자</li>
<li><code>thumbnailUrl</code> : 블로그 게시글 썸네일 이미지</li>
<li><code>prevArticleId</code> : 이전 게시글로 연결될 게시글 고유 ID</li>
<li><code>nextArticleId</code> : 다음 게시글로 연결될 게시글 고유 ID</li>
</ul>
<h2 id="notion에서-데이터-추출하기-위한-사전-준비">notion에서 데이터 추출하기 위한 사전 준비</h2>
<p><a href="https://developers.notion.com/reference/capabilities">여기서</a> 관련된 가이드 내용이 자세히 설명되어 있지만 <code>API</code>를 통해 데이터를 추출하려면 사전 준비가 필요하다.</p>
<p>로그인 한 뒤 <a href="https://www.notion.so/profile/integrations">https://www.notion.so/profile/integrations</a> 로 이동해서 API 통합을 추가해주어야 한다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054625/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_3.png" alt="Untitled"></p>
<p>또한 콘텐츠 기능과 관련한 권한들을 모두 사용할 수 있게 모두 허용해주면 준비가 완료된다.  통합 시크릿 키는 API를 사용해야할때 쓰일 것이다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054631/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_4.png" alt="Untitled"></p>
<h2 id="notion에서-데이터-추출하기">notion에서 데이터 추출하기</h2>
<p><code>notion</code> API를 직접 활용할수도 있으나 노션에서 공식적으로 제공하는 <code>sdk</code> (<a href="https://github.com/makenotion/notion-sdk-js"><code>@notionhq/client</code></a>) 를 활용하면 보다 쉽게 데이터를 추출할 수 있다.</p>
<pre><code class="language-tsx">import { Client } from &#39;@notionhq/client&#39;

export const notion = new Client({
  auth: process.env.NOTION_TOKEN, // 아까 통합 생성시 발급되는 시크릿 키
})</code></pre>
<p>우선 앞서 통합을 생성하면서 만들어진 API 시크릿 키를 활용해 다음과 같이 클라이언트 인스턴스를 생성해줘야 한다. </p>
<p>API가 추출할 수 있는 데이터는 크게 두가지로 페이지 자체에 대한 데이터를 추출하는 <code>query</code> 와 데이터베이스의 메타데이터를 추출할 수 있는 <code>retrieve</code> 가 있다.  쉽게 설명할 수 있게 예를 들어 설명하자면,</p>
<p>내가 작성한 모든 게시글들 중 블로그들에 표시될 수 있는 게시글 데이터들을 뽑아오기 위해서 다음과 같이 <code>query</code> 를 사용해볼 수 있다. 아래에서는 <code>releasable</code> 컬럼이 <code>true</code> 값인 게시글들을 모아 <code>createdAt</code> 컬럼을 오름차순으로 정렬하여 뽑아낸다.</p>
<pre><code class="language-tsx">// 블로그의 모든 게시글들 뽑아내기
const queryResponse = await notion.databases.query({
    database_id: process.env.NOTION_DATABASE_ID!, // notion에서 참조하고 있는 데이터베이스 ID
    filter: {
      and: [
        {
          property: &#39;releasable&#39;,
          checkbox: {
            equals: true,
          },
        },
      ],
    },
    sorts: [
      {
        property: &#39;createdAt&#39;,
        direction: &#39;descending&#39;,
      },
    ],
  })</code></pre>
<p>반면, 내가 아래와 같이 데이터베이스에서 설정한 <code>tag</code>들의 목록을 뽑아내래면 <code>retrieve</code> 를 활용할 수 있다. <code>retrieve</code> 는 주로 데이터베이스의 입력된 <code>row</code> 들 보단 <code>column</code> 자체와 관련된 정보들을 뽑아낼 때 사용한다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054644/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_5.png" alt="Untitled"></p>
<pre><code class="language-tsx">const metaDataResponse = await notion.databases.retrieve({
    database_id: process.env.NOTION_DATABASE_ID!,
}).properties.tags.select_options</code></pre>
<h1 id="📝-notion-마크다운-데이터의-렌더링">📝 notion 마크다운 데이터의 렌더링</h1>
<p>노션 API로부터 추출된 데이터를 HTML로 렌더링 하는 방법에는 크게 두가지가 존재했다.</p>
<h2 id="react-notion-x">react-notion-x</h2>
<pre><code class="language-tsx">import { NotionAPI } from &#39;notion-client&#39;
import { NotionRenderer } from &#39;react-notion-x&#39;

const notion = new NotionAPI()

const recordMap = await notion.getPage(&#39;067dd719a912471ea9a3ac10710e7fdf&#39;)

&lt;NotionRenderer recordMap={recordMap} fullPage={true} darkMode={false} /&gt;</code></pre>
<p><a href="https://github.com/NotionX/react-notion-x"><strong><code>react-notion-x</code></strong></a> 라는 라이브러리를 활용하면 간단한 코드 몇줄로 마크다운 데이터를 <code>HTML</code> 페이지로 렌더링 해준다. 노션 데이터를 불러오는 것은 <code>notion-client</code> 라는 라이브러리가 대신 수행하며 이 값을 <code>NotionRenderer</code> 컴포넌트에 넘겨주면, 아래와 같은 화면이 완성된다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054647/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_6.png" alt="Untitled"></p>
<p>노션과 완전히 동일한 화면이 출력되는 것을 볼 수 있다. 이것이 장점이자 단점이 되었다. 나는 보라색 테마의 블로그를 디자인했고 이에 맞게 렌더링 하고 싶었는데 <code>react-notion-x</code>는 이를 커스텀하기가 다소 어려운 구조였다. 이 때문에 이 방식을 내가 사용하기에는 어려웠다.</p>
<h2 id="notion-→-markdown-→-html">notion → markdown → html</h2>
<p>두번째 방법은 <code>notion</code> 데이터를 마크다운으로 변환하고 이를 또 한번 <code>HTML</code>로 변환하는 방식이다. 이는 앞선 <code>react-notion-x</code> 와 다르게 높은 자유도를 제공한다.</p>
<h3 id="1️⃣-notion-→-markdown">1️⃣ notion → markdown</h3>
<p>우선은 마크다운으로 변환할 수 있도록 하는 <a href="https://github.com/souvikinator/notion-to-md"><code>notion-to-md</code></a> 라이브러리를 사용한다.</p>
<pre><code class="language-tsx">import { Client } from &#39;@notionhq/client&#39;
import { NotionToMarkdown } from &#39;notion-to-md&#39;

export const notion = new Client({
  auth: process.env.NOTION_SECRET_ID!,
})

export const n2m = new NotionToMarkdown({
  notionClient: notion,
})

export const fetchArticleContent = async (pageId: string) =&gt; {
    const mdBlocks = await n2m.pageToMarkdown(pageId)
    return n2m.toMarkdownString(mdBlocks)
}</code></pre>
<p>노션의 <code>sdk</code>인 <code>@notionhq/client</code> 를 통해 받아온 데이터를 <code>NotionToMarkdown</code> 을 통해 마크다운 문자열 형태로 파싱하는 방식이다.</p>
<h3 id="2️⃣-markdown-→-html">2️⃣ markdown → html</h3>
<p>다음은 파싱된 마크다운을 <code>HTML</code> 로 변환하는 과정이다. 여기서는 마크다운을 리액트 컴포넌트로 렌더링 시켜주는 <a href="https://github.com/remarkjs/react-markdown"><code>react-markdown</code></a> 라이브러리를 활용한다. 앞서 언급했듯, 자유도가 높은 방식이기 때문에 내가 일일이 어떻게 렌더링 해야할지 지정해줘야해서 여러가지 플러그인들과 커스텀 컴포넌트들을 사용해야 한다. </p>
<pre><code class="language-tsx">import ReactMarkdown from &#39;react-markdown&#39;

// 플러그인들
import remarkGfm from &#39;remark-gfm&#39;
import rehypeRaw from &#39;rehype-raw&#39;
import remarkToc from &#39;remark-toc&#39;
import rehypeHighlight from &#39;rehype-highlight&#39;
import remarkRehype from &#39;remark-rehype&#39;
import rehypeSlug from &#39;rehype-slug&#39;
import rehypeAutolinkHeadings from &#39;rehype-autolink-headings&#39;
import &#39;highlight.js/styles/base16/dracula.min.css&#39;

const { parent: content } = await fetchArticleContent()

&lt;ReactMarkdown
  remarkPlugins={[remarkGfm, remarkToc, remarkRehype]}
  rehypePlugins={[
    rehypeRaw,
    rehypeHighlight,
    rehypeSlug,
    rehypeAutolinkHeadings,
  ]}
  ...
&gt;
  {content}
&lt;/ReactMarkdown&gt;</code></pre>
<p>플러그인들 부분부터 살펴보자. 우선, 플러그인은 크게 2가지 종류로 나뉘며 각각의 역할이 다르다.</p>
<ul>
<li><code>remark</code> : 마크다운 파싱 역할을 하는 플러그인들<ul>
<li><a href="https://github.com/remarkjs/remark-gfm">remark-gfm</a> - 마크다운 내 <code>table</code>, <code>todo-list</code>과 같은 요소를 알맞게 파싱해주는 플러그인</li>
<li><a href="https://github.com/remarkjs/remark-toc">remark-toc</a> - table of content(목차 부분) 생성을 도와주는 플러그인</li>
<li><a href="https://github.com/remarkjs/remark-rehype">remark-rehype</a> - <code>rehype</code>가 호환되게 주어진 마크다운을 <code>html</code>로 파싱하는 플러그인</li>
</ul>
</li>
<li><code>rehype</code> : <code>html</code> 로 파싱하는 역할을 하는 플러그인들<ul>
<li><a href="https://github.com/rehypejs/rehype-raw">rehype-raw</a> - 마크다운을 알맞은 형태의 <code>html</code> 로 파싱하는 역할을 하는 플러그인 (ex - <code>&lt;br/&gt;</code> <code>&lt;em/&gt;</code> 등을 썻을때 <code>html</code>과 같은 효과를 내게 공백을 조정)</li>
<li><a href="https://github.com/rehypejs/rehype-highlight">rehype-highlight</a> - <code>highlight.js</code> 를 활용해 마크다운 내 코드 <code>block</code>을 알맞은 형태로 변환 해주는 플러그인</li>
<li><a href="https://github.com/rehypejs/rehype-slug">rehype-slug</a> - 마크다운 헤더 요소에 id를 자동으로 부여하는 플러그인으로 목차내의 특정 요소를 클릭할때 해당 블럭으로 이동시키는 역할을 한다</li>
<li><a href="https://github.com/rehypejs/rehype-autolink-headings">rehype-autolink-heading</a> - <code>rehype-slug</code>와 연계해서 <code>heading</code> 요소에 링크 연결 버튼 같은 것을 생성하도록 해준다.</li>
</ul>
</li>
</ul>
<p>이외에도 수많은 플러그인들이 있으니 용도에 맞게 사용하면 될것이다.</p>
<pre><code class="language-tsx">export const Table = ({
  className,
  ...props
}: DetailedHTMLProps&lt;TableHTMLAttributes&lt;HTMLTableElement&gt;, HTMLTableElement&gt;) =&gt; {
  return &lt;table className={clsx(className, styles.table)} {...props} /&gt;
}

&lt;ReactMarkdown
    ...
  components={{
    h1: props =&gt; &lt;Heading as=&quot;h1&quot; {...props} /&gt;,
      ...
    hr: Divider,
    img: Image,
    ul: props =&gt; &lt;List as=&quot;ul&quot; {...props} /&gt;,
    ol: props =&gt; &lt;List as=&quot;ol&quot; {...props} /&gt;,
    li: ({ children }) =&gt; (
      &lt;Text as=&quot;li&quot; variant=&quot;body2&quot; color=&quot;textPrimary&quot;&gt;
        {children}
      &lt;/Text&gt;
    ),
    a: Anchor,
    p: Paragraph,
    blockquote: BlockQuote,
    code: CodeBlock,
    table: Table,
    th: Th,
    td: Td,
  }}
&gt;
  {content}
&lt;/ReactMarkdown&gt;</code></pre>
<p>다음은 <code>HTML</code>로 파싱되어 렌더링될때 어떤 스타일링을 입혀 표시할지 커스텀하는 부분이다. 헤더, 이미지, <code>listbox</code>, <code>anchor</code>, <code>code</code> 등 렌더링 시 사용되는 많은 <code>html</code> 태그들을 원하는 스타일링이 적용된 컴포넌트로 치환해주어야 한다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054651/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_7.png" alt="Untitled"></p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054653/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_8.png" alt="Untitled"></p>
<p>컴포넌트를 넣어주기 전과 후를 비교하면 얼마나 큰 역할을 하는지 체감할 수 있을 것이다.</p>
<h1 id="🌠-notion의-이미지를-영구적으로-바꾸기">🌠 notion의 이미지를 영구적으로 바꾸기</h1>
<p><code>notion</code> 자체 이미지는 글을 작성하면서 업로드 하기만 바로 연동이 되어 제공이 되기에 매우 편리하다. 하지만, <code>notion</code> 의 업로드하는 이미지는 링크로 제공될때 만료기간이 존재한다.</p>
<pre><code class="language-json">{
    &quot;url&quot;: &quot;https://s3.us-west-2.amazonaws.com/secure.notion-static.com/9bc6c6e0-32b8-4d55-8c12-3ae931f43a01/brocolli.jpeg?...&quot;,
    &quot;expiry_time&quot;: &quot;2020-03-17T19:10:04.968Z&quot;
}</code></pre>
<p>실제 노션에 업로드된 이미지를 API를 통해 <code>block</code> 형태로 조회하면 다음과 같이 <code>url</code> 과 만료기간이 함께 제공된다. 그리고 공식문서에 따르면 해당 파일의 공개 <code>URL</code>은 1시간마다 만료된다. 이 때문에 노션 API로 불러온 이미지 데이터는 1시간마다 만료되어 제대로 된 이미지를 렌더링하지 못한다.</p>
<p>이러한 문제를 해결할 수 있도록 블로그 게시글을 추가하는 과정에서 이미지 블락들의 찾아내어 notion에 호스팅된 이미지 URL을 외부 저장소를 통해 이미지를 업로드 하는 작업이 필요하다.</p>
<p>위와 같은 이미지 변환 작업의 과정들을 정리해보자면 다음과 같다.</p>
<ul>
<li>notion API로부터 이미지 block 데이터들 불러오기</li>
<li>외부 저장소에 이미지를 업로드</li>
<li>업로드 한 이미지 URL로 notion에 업로드 된 이미지를 교체</li>
</ul>
<h2 id="1️⃣-게시글에서-이미지-블록-모두-불러오기">1️⃣ 게시글에서 이미지 블록 모두 불러오기</h2>
<p><code>notion</code> API 를 이용해서 페이지 내에 있는 이미지 블록들을 모두 조회하는 과정이다. 이에 앞서 모든 블록들 부터 불러와야 한다.</p>
<pre><code class="language-tsx">export const fetchAllBlocksInPage = cache(
  async (blockOrPageId: string): Promise&lt;GetBlockResponse[]&gt; =&gt; {
    let hasMore = true
    let nextCursor: string | null = null
    const blocks: GetBlockResponse[] = []

    // 요청당 불러올 수 있는 블락 응답의 크기가 한정되어 있어 모든 블록들을 불러올때 까지
    // notion 페이지의 block들을 불러오는 과정들을 반복해야 한다
    while (hasMore) {
      const result: ListBlockChildrenResponse = await notion.blocks.children.list({
        block_id: blockOrPageId,
        start_cursor: nextCursor ?? undefined,
      })

      blocks.push(...result.results)
      hasMore = result.has_more
      nextCursor = result.next_cursor

      if (hasMore) {
        console.log(&#39;load more blocks in page...&#39;)
      }
    }

    // 블록들 중 자식요소로 있는 블록들을 찾아 이 역시 블러온다
    // 예를 들면 toggle block이 있을 것이다.
    const childBlocks = await Promise.all(
      blocks
        .filter(block =&gt; &#39;has_children&#39; in block &amp;&amp; block.has_children)
        .map(async block =&gt; {
          const childBlocks = await fetchAllBlocksInPage(block.id)
          return childBlocks
        }),
    )

      // 참고로 block들의 순서쌍은 보장되지 않는다
      // 어차피 렌더링 할때 사용되는 것은 아니므로 순서쌍이 보장될 필요는 없다
    return [...blocks, ...childBlocks.flat()]
  },
)</code></pre>
<p>이렇게 모든 블록들을 불러왔으면 이미지 블록들만 간추려야 한다. 이때 <code>block</code>의 속성 중 <code>type</code>이 <code>image</code> 에 해당하는 것들만 찾아내면 된다.</p>
<pre><code class="language-tsx">export const fetchAllImageBlocksInPage = cache(async (pageId: string) =&gt; {
  const allBlocks = await fetchAllBlocksInPage(pageId)
  return allBlocks.filter(
    block =&gt; &#39;type&#39; in block &amp;&amp; block.type === &#39;image&#39;,
  ) as ImageBlockObjectResponse[]
})</code></pre>
<h2 id="2️⃣-외부-저장소에-이미지-올리기feat-cloudinary">2️⃣ 외부 저장소에 이미지 올리기(feat. cloudinary)</h2>
<p>이제 notion으로부터 불러온 이미지를 업로드할 차례 이다. 우선 내가 예시로 들어 사용해볼 서비스는 <code>cloudinary</code> 이다. 이를 선택한 이유는 무료로 이미지를 업로드할 수 있으며 손쉽게 사용 가능한 자바스크립트 sdk를 오픈소스로 제공하고 있기 때문이었다.</p>
<p>이를 업로드 하기에 앞서, <code>cloudinary</code> 에 가입하는 절차가 필요하다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054655/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_9.png" alt="Untitled"></p>
<p>가입하고 나서 위의 이미지 처럼 <code>assets</code> 탭을 선택하고 상단에서 <code>folders</code> 를 선택한 뒤에 원하는 이름으로 폴더를 하나 생성해준다. </p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054658/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_10.png" alt="Untitled"></p>
<p>또한, 설정 페이지에서 API key를와 API secret 값을 알아와야 한다. 그리고 아래와 같이 <code>cloudinary</code> 를 이용한 이미지 업로드 코드를 작성하면 된다</p>
<pre><code class="language-tsx">// 1. notion에 호스팅된 이미지를 base64 형태로 다운로드
// 2. cloudinary에 업로드

import https from &#39;https&#39;
import { type UploadApiOptions, v2 as cloudinary } from &#39;cloudinary&#39;

class CloudinaryApi {
  // 생성자를 통한 cloundinary sdk config 초기화
  constructor() {
      // cloundinary url 값
    const cloudinaryUrl = process.env.CLOUDINARY_URL!
    const urlRegex = /^cloudinary:\/\/([a-z0-9-_]+):([a-z0-9-_]+)@([a-z0-9-_]+)$/i
    if (!urlRegex.test(cloudinaryUrl)) {
      throw new Error(`Invalid Cloudinary URL provided. It should match ${urlRegex.toString()}`)
    }
    const [, apiKey, apiSecret, cloudName] = cloudinaryUrl.match(urlRegex) ?? []

    cloudinary.config({
      secure: true,
      api_key: apiKey,
      api_secret: apiSecret,
      cloud_name: cloudName,
    })
  }

  // 호스팅된 이미지를 다운로드 하여 base64 문자열로 변환
  private downloadImageToBase64(url: string): Promise&lt;string&gt; {
    return new Promise((resolve, reject) =&gt; {
      const req = https.request(url, response =&gt; {
        const chunks: unknown[] = []

        response.on(&#39;data&#39;, function (chunk) {
          chunks.push(chunk)
        })

        response.on(&#39;end&#39;, function () {
          const result = Buffer.concat(chunks as ReadonlyArray&lt;Uint8Array&gt;)
          resolve(result.toString(&#39;base64&#39;))
        })
      })
      req.on(&#39;error&#39;, reject)
      req.end()
    })
  }

  // cloudinary uploader를 통해 이미지 업로드
  private uploadImage(image: string, options: UploadApiOptions = {}): Promise&lt;{ url: string }&gt; {
    return cloudinary.uploader
      .upload(image, options)
      .then(result =&gt; ({
        url: result.secure_url,
      }))
      .catch(error =&gt; {
        console.error(error)
        return { url: &#39;&#39; }
      })
  }

  // notion image를 영구 이미지로 변환
  async convertToPermanentImage(notionImageUrl: string, title: string) {
    const imgBase64 = await this.downloadImageToBase64(notionImageUrl)
    const { url: cloudinaryUrl } = await this.uploadImage(`data:image/jpeg;base64,${imgBase64}`, {
      // 앞서 cloudinary에서 생성했던 폴더명
      folder: process.env.CLOUDINARY_UPLOAD_FOLDER!,
      // 업로드되는 이미지의 제목 제목과
      public_id: title.split(&#39; &#39;).join(&#39;_&#39;).trim(),
      overwrite: true,
    })

    return cloudinaryUrl
  }
}

export const cloudinaryApi = new CloudinaryApi()
</code></pre>
<p>이러한 이미지 업로드 코드를 게시글 <code>page</code> 를 조회하는 코드와 <code>notion block</code>을 업데이트 하는 코드를 결합해 사용하면 다음의 코드가 완성된다.</p>
<pre><code class="language-tsx">// 주어진 pageId를 기반으로 페이지 내의 모든 이미지 블록들의 이미지 url을 변경하는 함수
export const updateImageBlocks = async (pageId: string) =&gt; {
  const allImageBlocks = await fetchAllImageBlocksInPage(pageId)

    // 모든 이미지 블록들에 대해서 다음의 과정들을 수행
  for (const [index, imageBlock] of allImageBlocks.entries()) {
    const { image, id: blockId } = imageBlock
    // notion에 직접 업로드된 이미지 파일들만 cloudinary에 업로드하여 변환
    if (&#39;type&#39; in image &amp;&amp; image.type === &#39;file&#39;) {
      const convertedImageUrl = await cloudinaryApi.convertToPermanentImage(
        (image as FileImageBlock).file.url,
        `${pageId}_imageblock_${index + 1}`,
      )
      // cloudinary에 업로드 된 이미지 url로 이미지 블록들을 업데이트 한다
      await notion.blocks.update({
        block_id: blockId,
        image: {
          external: {
            url: convertedImageUrl,
          },
        },
      })
    }
  }
}</code></pre>
<p>참고로, 노션에 직접적으로 호스팅된 이미지는 <code>image.type</code> 값이 <code>file</code> 로 나타나며, 외부 이미지는 <code>external</code> 값을 가진다. 또한 한번 <code>cloudinary</code> 에 업로드 된 이미지는 위의 조건문에 따라 cloudinary로 변환되어 업로드 되는 과정이 발생하지 않게 된다. 이로써 <code>notion</code> 에 업로드된 이미지도 영구적으로 호스팅 될 수 있게 되었다.</p>
<h1 id="🔄-블로그-업데이트-자동화">🔄 블로그 업데이트 자동화</h1>
<p>정적인 형태로 제공하는 블로그 서비스에서 수정된 내용을 반영하기 위한 조치가 필요하다. 가장 간단하게는 다시 빌드해서 배포하는 방법이 있지만 이는 매우매우 비효율적이다. 내가 사용한 <code>Next.js</code> 프레임워크에서는 여러가지 방식으로 정적 컨텐츠를 업데이트 하는 방법들을 제공해주고 있다.</p>
<h2 id="주기적인-업데이트">주기적인 업데이트</h2>
<pre><code class="language-tsx">// app/blog/page.tsx

// 3600초 = 60분마다 업데이트 진행
export const revalidate = 6000

export default function Blog() {
  return (
    ...
  )
}</code></pre>
<p>일정 주기마다 정적으로 생성해둔 페이지를 업데이트 하는 방법이다. 위와 같이 페이지 컴포넌트 내에서 <code>revalidate</code> 라는 값을 <code>export</code> 해주면 일정 주기마다 업데이트가 적용된다. </p>
<p>이렇게 일정시간 마다 업데이트 시키면, 컨텐츠가 업데이트 될 뿐만 아니라 앞서 언급한 <code>notion</code> 이미지 만료 문제의 경우, 매번 새로운 노션 이미지로 교체되기 때문에 해결해볼 수 있지 않을까 싶을 수 있다. 하지만 내가 외부 저장소로 이미지를 업로드를 해보기 전에 주기적인 업데이트 방법을 적용해봐도 제대로 이미지 URL이 갱신되지 않아서 간혹 이미지를 불러올때 <code>502</code> 에러가 주기적으로 발생했다.</p>
<p>이 뿐만 아니라 업데이트 된 내용이 실제로는 존재하지 않으면서도 전체적인 페이지 업데이트가 적용되는다는 점이 매우 비효율적이다.</p>
<h2 id="필요할때-업데이트-하기-on-demand">필요할때 업데이트 하기 (on-demand)</h2>
<pre><code class="language-tsx">export default async function Page() {
    const pageId = &#39;...&#39;

  // 페이지에서 불러오는 fetch 함수 호출 부분에 &#39;collection&#39; 태그를 추가해놓는다
  const res = await fetch(&#39;https://...&#39;, { next: { tags: [pageId] } })
  const data = await res.json()
  // ...
}

// app/api/revalidate/route.ts
// tag 갱신 endpoint -&gt; 여기로 POST 요청을 보내면 페이지 데이터가 갱신된다
import { revalidateTag } from &#39;next/cache&#39;

export async function POST(request: NextRequest) {
    const { pageId } = await request.json()

    revalidateTag([pageId])

    return Response.json({ revalidated: true })
}</code></pre>
<p>또 다른 방법으로 <code>on-demand revalidation</code>  전략을 제공해주고 있다. 간단하게 서버 컴포넌트에서 불러온 데이터에 <code>tag</code> 를 바인딩하고 필요한 경우 <code>nextjs</code> 의 엔드포인트를 생성해서 해당 태그의 데이터를 갱신해주는 작업을 해줄 수 있다. </p>
<p>하지만 나같은 경우, 블로그 데이터를 불러올때 <code>fetch</code> 가 아닌 <code>notion</code>의 <code>sdk</code> 를 사용한 데이터를 사용하고 있었다. 이런 경우, <code>fetch</code>가 아닌 <code>unstable_cache</code>를 활용하여 데이터를 불러오는 부분을 감싸면 <code>tag</code> 바인딩이 가능하다.</p>
<pre><code class="language-tsx">import { unstable_cache } from &#39;next/cache&#39;

export const fetchArticlePageContent = (pageId: string) =&gt; {
  const cacheKey = ARTICLE_CONTENT(pageId) // `${pageId}_content`

 // unstable_cache 
  return unstable_cache(
    async (pageId: string) =&gt; {
      await updateImageBlocks(pageId)

      const mdBlocks = await n2m.pageToMarkdown(pageId)
      return n2m.toMarkdownString(mdBlocks)
    },
    [cacheKey],
    {
      tags: [cacheKey],
    },
  )(pageId)
}

export const ArticleDetailContentSection = async ({ pageId }: ArticleDetailContentSectionProps) =&gt; {
  const { parent } = await fetchArticlePageContent(pageId)

  return &lt;ArticleContentRenderer content={parent as string} /&gt;
}
</code></pre>
<p>이렇게 바인딩 해놓은 상태로 정적인 배포를 적용하고 업데이트가 필요한 시점에 <code>revalidateTag</code> 를 수행하는 API를 호출하면 페이지 업데이트를 손쉽게 진행할 수 있다.</p>
<blockquote>
<p>💡 참고로 <code>revalidateTag</code> 를 실행하면 바로 업데이트가 수행되는 것이 아니라, <code>revalidateTag</code>를 실행한뒤 페이지 방문이 발생하면 그때서야 업데이트가 수행된다. 자세한 내용은 <a href="https://nextjs.org/docs/app/api-reference/functions/revalidateTag">공식문서</a>를 참고해주기를 바란다.</p>
</blockquote>
<h2 id="zapier로-revalidation-자동화하기">zapier로 revalidation 자동화하기</h2>
<p>위의 방식으로 페이지 내용을 업데이트 하는 방법은 마련했지만, 이를 내가 직접 수행해줘야 한다는 번거러움이 있다. 누군가 노션 데이터베이스에서 업데이트 내역을 알아서 확인해서 페이지를 업데이트하는 API를 호출해줄 수 는 없는 것일까? 그것을 해주는 도구가 바로 <code>zapier</code> 이다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054659/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_11.png" alt="Untitled"></p>
<p><code>zapier</code> 는 여러가지 웹 앱들(<code>Slack</code>, <code>Trello</code>, <code>Google Docs</code> 등)을 통합해 자동화 할 수 있는 여러 기능들을 제공한다. 여기서 <code>notion</code> 통합을 이용하면 우리가 번거롭게 여기던 업데이트 과정을 자동화하도록 만들수 있다. 여기서 내가 만들어낼 작업을 다음과 같다</p>
<ul>
<li><code>notion</code> 데이터베이스의 업데이트 내역을 포착한다</li>
<li>업데이트가 발생한 <code>pageId</code>를 알아낸다.</li>
<li>해당 <code>pageId</code>로 업데이트 API 요청을 보낸다</li>
</ul>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054661/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_12.png" alt="Untitled"></p>
<p>우선 <code>zapier</code>에서 <code>zap</code> 이라는걸 생성한뒤 노션의 업데이트를 감지하는 <code>action</code> 을 선택해주어야 한다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1721054662/sxungchxn-dev/47eb7d5f-0c8b-4e45-8c6e-4c8a103f9c10_imageblock_13.png" alt="Untitled"></p>
<p>이후에는 파악한 업데이트 내역을 바탕으로 API 호출을 할 수 있도록 하는 코드 액션을 선택해주면 된다. 여기서 아래와 같은 코드를 작성해 주면 된다. 여기서 주의해서 봐야할 것은 <code>inputData</code> 라는 변수는 앞서 액션에서 제공해주는 <code>output</code> 값이다.</p>
<pre><code class="language-tsx">// 이전 단계에서 불러온 database item
const item = inputData;

// database item의 releasable 속성이 false인 경우 함수 실행 중단
if (item.releasable === &#39;False&#39;) {
  console.log(&#39;impossible to revalidate&#39;)
  output = { revalidated: false, pageId: null }
  return;
}

// database item의 releasable 속성이 true인 경우
// 해당 item의 id를 추출하여 api 요청을 보낸다
const pageId = item.pageId;

// GET 요청을 보내기 위한 옵션 설정
const options = {
  method: &quot;POST&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
  body: JSON.stringify({
    pageId,
    revalidateKey: &quot;xxxxxxxxxxxxx&quot;,
  })
};

// api 요청을 보내기 위한 url 설정
const url = `https://...../api/revalidate?pageId=${pageId}`;

let responseData = null

try {
  // fetch 함수를 사용하여 GET 요청을 보낸다
  const res = await fetch(url, options);
  // 요청에 대한 응답을 출력한다
  responseData = await res.json()  
  if(responseData.revalidated){
    console.log(&#39;success to revalidate&#39;)
  }
  else {
    console.log(&#39;fail to revalidate&#39;)
  }

}
catch(err) {
  console.log(err)
  console.log(&#39;fail to request&#39;)
}

// 응답 결과를 객체로 출력한다
output = { pageId, response: responseData, revalidated: responseData?.revalidated ?? false };</code></pre>
<p>이렇게 자바스크립트 코드를 작성하여 <code>action</code> 을 완성한뒤 <code>zap</code> 을 완성해주면 자동화하는 과정이 완성된다.</p>
<h1 id="결론">결론</h1>
<p>간단할 수 있는 블로그 프로젝트이지만, 나의 취향에 맞는 그리고 정말 편리한 블로그를 만들려다보니 여러가지 수고로운 작업들을 수행해야 했다. 하지만, 이러한 작업들 덕분에 나의 편리한 글쓰기 경험을 제공하기 위한 블로그를 완성할 수 있었다. 관련하여 궁금한 점이 있다면 언제든 댓글을 남겨주길 바란다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[블로그 마이그레이션 및 제작 후기]]></title>
            <link>https://velog.io/@seungchan__y/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EB%B0%8F-%EC%A0%9C%EC%9E%91-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EB%B0%8F-%EC%A0%9C%EC%9E%91-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 11 Jul 2024 13:54:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 <a href="https://www.sxungchxn.dev/blog/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34">여기서</a> 더 멋지게 볼 수 있습니다.</p>
</blockquote>
<blockquote>
<p>블로그 레포지토리는 <a href="https://github.com/sxungchxn/sxungchxn-dev">여기서</a> 확인하실 수 있습니다.</p>
</blockquote>
<h1 id="🥲-기존-블로그-사용의-문제점">🥲 기존 블로그 사용의 문제점</h1>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1720705659/sxungchxn-dev/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34_imageblock_1.png" alt="Untitled"></p>
<p>기존에 사용하던 블로그 플랫폼인 <code>velog</code> 는 개발 블로그를 시작하기에는 더할나위 없이 좋은 플랫폼이 맞다. 특히나, 유동인구가 워낙 많아서 흐름에만 잘 편승(?)하면 좋아요를 많이 받아볼 수도 있었다. 이렇게 좋은 점도 있었으나 계속 블로그 글을 작성하기에는 아쉬운 점들이 많았다.</p>
<h2 id="글-작성-과정에서의-번거러움">글 작성 과정에서의 번거러움</h2>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1720705661/sxungchxn-dev/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34_imageblock_2.png" alt="Untitled"></p>
<p><code>velog</code> 의 에디터 자체는 글을 작성하기에는 나쁘지 않은 편이다. 하지만, 블로그 글의 길이가 길어서 쓰는 시간이 엄청 길어지면 브라우저 내에서만 글을 쓰기에는 어려움이 있었다. 왜냐하면 쓰는 시간이 하루를 넘어버리면 브라우저 탭을 실수로 닫아버리기 일 수 였다. 글을 쓰다가 실수로 <code>임시 저장</code> 을 누르지 않고 브라우저 탭을 닫아버리면… 그야 말로 대참사가 일어나는 것 이였다. 또한 글이 길어지면 내용을 스크롤이 길어지면서 검토하는 과정도 매우 번거러워 졌다.</p>
<p>그래서 나는 임시 방편으로 <code>notion</code>을 이용해 블로그를 작성하는 방식으로 바꿔보았다. <code>notion</code>은 태생이 에디터 이기에 글을 쓰는 경험도 매우 좋아서 나름 괜찮은 선택지였다.</p>
<h2 id="노션과-velog는-다소-달랐다">노션과 velog는 다소 달랐다</h2>
<p><code>notion</code>으로 글을 쓰는것 까지는 괜찮다. 그런데 이제 <code>velog</code>로 옮기는 과정이 하나 추가된다. <code>velog</code> 로 옮기는 과정은 복사 붙이기만 하면 되니 매우 쉬운 것 아닐까?</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1720705663/sxungchxn-dev/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34_imageblock_3.png" alt="스크린샷 2024-07-09 오후 10.56.55.png"></p>
<p>이 첫번째 사진의 <code>notion</code> 글을 <code>velog</code> 로 옮겨보면 다음과 같아진다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1720705667/sxungchxn-dev/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34_imageblock_4.png" alt="Untitled"></p>
<p>여기서 발생하는 문제점을 정리해보면 다음과 같다</p>
<ul>
<li><code>notion</code>에는 잘 업로드 되었던 이미지가 <code>velog</code> 로 옮기면 망가져 버린다.</li>
<li><code>notion</code> 에서 보았던 글 모습과 <code>velog</code> 에서의 모습이 상당히 다르다 (간격이 좀 다르고, 블락도 의도된대로 보이지 않았다.)</li>
</ul>
<p>이러다 보니 <code>notion</code> 에서 글이 잘 완성 되었다가도 <code>velog</code> 에서 올리기 위한 또 다른 작업이 필요했다. 이미지들 중 깨지는 것 마다 대응을 해주기 위해 <code>notion</code>에 있던 이미지를 다운받아서 다시 <code>velog</code> 에 올려야 했다. </p>
<p>또한 <code>notion</code> 에서 보이는 것과 다르게 <code>velog</code> 버전의 글에서 이상하거나 이상한 요소들은 내가 일일이 수정해줘야 했다. 특히나 길이가 긴 글과 같은 경우, 스크롤을 내리며 잘못된 요소를 찾아 수정을 하는 작업이 정말 번거로웠다.</p>
<p>이렇게 글을 쓰기 까지 매번 번거로운 작업들이 발생하게 되었고 이로 인해 글을 쓰는데 필요한 시간이 계속해서 늘어나게 되면서 글 하나를 작성하는게 망설여지기 시작했다. 이렇게 좋지 않은 나의 글쓰기 경험을 개선해보고자 기존의 블로그를 벗어나 새로운 블로그 방식을 만들어보자 라고 결심하게 되었다.</p>
<h1 id="🎯-블로그-마이그레이션의-목표">🎯 블로그 마이그레이션의 목표</h1>
<p>이번 블로그 프로젝트를 진행하면서 이루고자 했던 목표들이다. 일차적인 최우선 과제는 나의 글쓰기 경험 향상이다.</p>
<h2 id="나에게-편리한-글쓰기-경험-제공하기">나에게 편리한 글쓰기 경험 제공하기</h2>
<p>노션의 좋은 글쓰기 경험은 살리면서도 스무스한 글 업로드가 가능하게 하고 싶었다. 즉, <code>notion</code>을 에디터로 활용하고 이로 부터 블로그에 업로드된 게시글을 추출해주는 작업이 필요했다. 다행히도 <code>notion</code>에서는 작성할 글을 마크다운 형태로 추출할 수 있도록 API가 마련되어 있었다.</p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1720705670/sxungchxn-dev/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34_imageblock_5.png" alt="Untitled"></p>
<p>또한, 이러한 API를 ORM 형태로 사용할 수 있게 클라이언트 라이브러리 <a href="https://www.npmjs.com/package/@notionhq/client">(<code>@notionhq/client</code></a>) 도 마련되어 있어서 <code>notion</code> 의 데이터베이스만 잘 구축 해놓는다면, 데이터베이스와 백엔드 API가 마련된채로 웹서비스를 만들어낼 수도 있다. 이를 이용하는 과정에서는 정말 많은 시행착오가 있었는데 나중에 또 다른 글로 이에 대해서 이야기 해보고 싶다.</p>
<h2 id="나만의-디자인으로-프로젝트-만들기">나만의 디자인으로 프로젝트 만들기</h2>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1720705673/sxungchxn-dev/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34_imageblock_6.png" alt="Untitled"></p>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1720705674/sxungchxn-dev/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34_imageblock_7.png" alt="Untitled"></p>
<p>디자인에 무척 관심있던 나로서는 한번 쯤은 나만의 개성을 표현할 수 있는 사이트를 만들어 보고 싶었다. 이 때문에 개발에 착수하기에 앞서 디자인 과정부터 심혈을 기울였다 (<del>덕분에 디자이너가 아닌 개발자 하기를 잘했다는 확신을 얻었다</del>). 디자인 토큰 부터, 폰트, 반응형까지 신경쓰다보니 정말 힘들긴했다. 그래도 이러한 디자인 작업을 먼저 해둔 덕분에 나름 만족스러운 퀄리티가 나왔다는 생각이 들었다. </p>
<h2 id="새로운-학습의-기회로-만들기">새로운 학습의 기회로 만들기</h2>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1720705676/sxungchxn-dev/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34_imageblock_8.png" alt="Untitled"></p>
<p>단순히 블로그 사이트 만들기로 이번 프로젝트 기회를 끝내버리고 싶진 않았다. 이번 블로그 프로젝트를 하면서 여러가지 학습적 요소들을 추가하며 의미있는 프로젝트를 만들어보고 싶었다.</p>
<ul>
<li>모노레포 아키텍쳐 구성하고 이해하기</li>
<li>패키지 제작 및 배포에 대해 이해하기</li>
<li>디자인 시스템 제작하기</li>
<li>아이콘 에셋 패키지 만들어 보기</li>
</ul>
<p>이러한 학습적 요소도 같이 하며 블로그 프로젝트를 하다보니 진행 시간이 굉장히 길어져버리긴 했다. 하지만 이 덕분에 정말 프론트엔드 엔지니어로서 많은 요소들을 배워볼 수 있었으며, 단기간에 많은 성장을 이뤄낼 수 있었다고 생각한다. 모노레포와 디자인시스템 그리고 아이콘 에셋에 대해서도 정말 할 얘기가 많아서 다른 포스트에서 다루어 보고 싶다.</p>
<h1 id="🦋-아직-갈길이-멀다">🦋 아직 갈길이 멀다</h1>
<p><img src="https://res.cloudinary.com/dbgkriwoo/image/upload/v1720705678/sxungchxn-dev/d3d6d08d-b2a8-4c5d-b1d4-701e1fc80a34_imageblock_9.png" alt="Untitled"></p>
<p>결과적으로는 나에게는 멋진 블로그가 만들어져서 너무 좋았다. 하지만 아직 갈길이 멀다. 이제는 내가 글을 써서 블로그의 영양분을 채워줄 차례이다. 진정한 기술 블로그로 거듭날 수 있도록 많은 기록들을 써내려가야 할 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[번역] React Query 적으로 사고하기]]></title>
            <link>https://velog.io/@seungchan__y/React-Query-%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%82%AC%EA%B3%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/React-Query-%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%82%AC%EA%B3%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 21 Jun 2023 05:52:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 아티클은 Tkdodo의 <a href="https://tkdodo.eu/blog/thinking-in-react-query">Thinking in React Query</a> 블로그 글을 번역한 내용입니다.</p>
</blockquote>
<blockquote>
<p>이 글은 <a href="https://www.sxungchxn.dev/blog/b2875688-bd23-4940-9a84-45c7c8f5132f">여기서</a> 더 멋지게 보실 수 있습니다 😆</p>
</blockquote>
<blockquote>
<p>🗣️ 오늘의 아티클은 다른 형태로 진행 됩니다. 얼마전 비엔나에서의 밋업과 예전에 <a href="https://reactsummit.com/">React Summit</a>에서 진행했던 발표의 슬라이드와 대본으로 이루어져 있습니다. <a href="https://tkdodo.eu/blog/thinking-in-react-query">슬라이드</a>를 좌우로 스와이프 하거나 양끝에 있는 화살표 버튼을 클릭하여 슬라이드를 이동할 수 있습니다. 그럼 즐겁게 읽어주세요!</p>
</blockquote>
<h1 id="intro">Intro</h1>
<hr>
<p><img src="https://d33wubrfki0l68.cloudfront.net/0811c4aabd6e598e0075daab043d2eb505594bae/703f8/blog/images/thinking-in-react-query/2.png" alt=""></p>
<p>안녕하세요 여러분 👋, 오늘 이 자리에 있게 되어 영광입니다. 오늘 이야기 하고자 하는 바는…여러분의 신발끈을 바로 잡는 것에 대해서 입니다.</p>
<p>대부분의 사람들이 신발끈을 묶는 데에 있어서 정확한 방법과 잘못된 방법의 차이를 구분해내지 못합니다. 두 방법은 언뜻 보기에 비슷해 보여도 전자의 매듭은 꽉 묶여있지만 후자의 매듭은 걸을때 풀리기 십상입니다. 이는 여러분의 삶을 변화시킬 수 있는 작은 변화일 것 입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/4.png" alt=""></p>
<p>리액트 쿼리를 사용할 때도 작은 변화가 큰 차이를 만들어낼 수 있는 몇가지 상황들이 존재하곤 합니다.</p>
<p>제가 2020년경에 오픈소스 커뮤니티 활동을 한창 시작했을때 이러한 상황들을 발견하곤 했습니다. 여러 플랫폼들을 통해서 질의응답을 하는 활동은 제가 오픈소스 활동을 시작하게 된 아주 훌륭한 방법이였습니다. 여러분들이 이러한 문제들을 해결해주면 사람들은 고마워하고 감사해하며, 저 역시 이전에는 마주치지 못했던 문제들을 질문으로 받아보면서 많은 것들을 배워 볼 수 있었습니다.</p>
<p>이러한 활동 덕에 리액트 쿼리에 대해 매우 잘 알게 되었고, 그와 동시에 여러 질문들에서 공통된 패턴들을 파악할 수 있었습니다. 질문해주신 분들 중 많은 분들이 리액트 쿼리가 무엇인지 또는 무엇을 하는지에 대해 오해하고 있었고 이러한 오해는 약간의 다른 생각들을 갖도록 했습니다.</p>
<p>다시 돌아와서, 오늘 제가 이야기하고자 하는 바는 리액트 쿼리를 이해하는데 있어서 더 나은 사고방식을 가질 수 있는 3가지 요소들 입니다. 신발 끈을 바로 매는 것 처럼 이번 시간을 통해 여러분들은 리액트 쿼리를 쉽지만 정확하게 이해할 수 있게 될 것입니다.</p>
<p>자 이제 본격적으로 “<strong>React Query적으로 사고”</strong>할 수 있는 요소들에 대해 알아봅시다</p>
<br/>

<h1 id="1-리액트-쿼리는-데이터-패칭-라이브러리가-아니다">1. 리액트 쿼리는 데이터 패칭 라이브러리가 아니다.</h1>
<p>리액트 쿼리가 리액트의 데이터 패칭에 있어서 숨겨진 퍼즐 조각이라고 종종 묘사되는 만큼 여러분들이 들으면 놀랄만한 사실이 있는데 그것은 바로 <strong>리액트 쿼리는 데이터 패칭 라이브러리가 아니라는 점</strong> 입니다. 리액트 쿼리는 여러분들에게 데이터 패칭 기능을 제공해 주지 않습니다. 왜 그런지는 아래 예시를 살펴보면 알 수 있습니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/10.png" alt=""></p>
<p>위 코드는 일반적인 <code>useQuery</code> 를 사용하는 예제 코드입니다. 첫번째 인자로는 리액트 쿼리가 데이터를 저장하는 데 사용하는 고유한 <code>queryKey</code> 를 넘겨주고 두번째 인자로는 데이터를 불러올때마다 실행되는 함수인 <code>queryFn</code> 을 넘겨줍니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/13.png" alt=""></p>
<p>우리는 이러한 훅을 컴포넌트 내부에서 이용함으로써 데이터를 렌더링하고 쿼리가 가질 수 있는 상태들을 사용할 수 있게 됩니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/14.png" alt=""></p>
<p>잠시 <code>queryFn</code> 부분을 다시 보면  <code>axios</code> 라는 라이브러리로 작성되어 있다라는 점을 확인할 수 있습니다. 리액트 쿼리는 데이터 패치를 어디서 어떻게 하는지 관여하지 않습니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/15.png" alt=""></p>
<p>리액트 쿼리가 관여하는 부분은 <code>queryFn</code> 의 실행 결과로 <code>fulfilled</code> 혹은 <code>rejected</code> 상태인 <code>Promise</code> 객체가 반환된 이후 뿐 입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/16.png" alt=""></p>
<p>사실, 누군가 리액트 쿼리 메인테이너인 저에게 리액트 쿼리에 관한 이슈를 작성 했을때 API가 공개되지 않은 상태라 그 문제점을 재현할 수 없다고 하는 상황이라고 말한다면, 저는 위에 처럼 <code>queryFn</code> 부분을 데이터 패칭 라이브러리 없이 <code>Promise</code> 객체를 반환하는 함수를 이용해서 간단하게 그 문제 상황을 재현할 수 있다고 답할 것입니다. 당연하게도 <code>axios</code>, <code>fetch</code>, <code>graphql-request</code>와 같은 데이터 패칭 라이브러리는 <code>Promise</code> 객체를 반환하기에 리액트 쿼리와 잘 동작합니다.</p>
<p>여러분들이 리액트 쿼리가 데이터 패치에는 관여하지 않는 것을 알게 되었다면, 바라건대 리액트 쿼리의 질문들 중 데이터 패치와 관련된 내용들은 없어져야 할 것이 분명합니다. 예를 들어, 아래와 같은 데이터 패치와 관련된 질문들은 모두 동일한 정답을 가질 것 입니다.</p>
<blockquote>
<p>🤔 : 리액트 쿼리에서 baseURL을 어떻게 정의하나요?</p>
<p>🤔 : 리액트 쿼리에서 response header들은 어떻게 접근하나요?</p>
<p>🤔 : 리액트 쿼리에서 graphQL 요청은 어떻게 생성할 수 있나요?</p>
<p>🗣️ : 리액트 쿼리는 그러한 것들에 관여하지 않습니다. 그저 <code>queryFn</code>의 자리에는 <code>Promise</code>나 반환해주세요.</p>
</blockquote>
<p>그렇다면 이제 새로운 의문점을 갖게 될 수 있습니다. </p>
<blockquote>
<p>🤔 : 데이터 패칭 라이브러리가 아니라면 리액트 쿼리는 도대체 무엇인가요?</p>
</blockquote>
<p>이 질문에 대한 저의 답변은 항상 다음과 같았습니다.</p>
<blockquote>
<p>🗣️ : 비동기 상태 관리자(<strong>Async State Manager</strong>) 입니다.</p>
</blockquote>
<p>자 여기서 <strong>비동기 상태</strong> 라는 것이 무엇을 의미하는지 이해하는게 중요합니다. </p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/21.png" alt=""></p>
<p>리액트 쿼리의 창시자인 Tanner Linsley는 2020년 5월 경에 <a href="https://www.youtube.com/watch?v=seU46c6Jz7E">It’s Time to break up with your “Global State”</a> 라는 내용으로 발표를 진행했었습니다. 위의 발표 내용이 이번 아티클과 매우 밀접하게 관련되어 있으니 꼭 한 번 시청하는 것을 권장드립니다. </p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/22.png" alt=""></p>
<p>위 발표 내용의 핵심은 우리가 오랜 기간 동안 “상태” 라는 것을 <strong>어디에 보관 해둘지</strong>에 대해서만 고민하는 것에 몰두했다는 것 입니다. 예를 들어, 우리는 하나의 상태가 한 컴포넌트 내부에서만 필요하다면 위의 그림처럼 컴포넌트 내부에서 지역 상태를 정의하여 사용할 것입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/23.png" alt=""></p>
<p>상위 계층에서도 사용해야 한다면, 해당 상태를 부모 컴포넌트로 끌어올리고 필요한 곳에 props로 내려주는 형태로 사용할 것 입니다. </p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/24.png" alt=""></p>
<p>부모보다도 더 높은 계층이나 더 넓은 영역에서 상태를 사용해야 한다면 어떻게 할 수 있을까요? <code>redux</code>나 <code>zustand</code>와 같은 “전역 상태 관리자”에게 상태값을 위임함으로써 리액트 컴포넌트 외부에서 상태를 관리하되 컴포넌트 모든 컴포넌트들에서 해당 상태를 사용할 수 있도록 할 수 있을 것입니다. </p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/25.png" alt=""></p>
<p>그리고 우리는 한동안 이러한 방식을 모든 종류의 상태에 대해서 적용했습니다. 버튼을 클릭하면 테마가 전환되는데 필요한 상태부터 네트워크 상에서 불러오는 이슈의 목록이나 프로필 데이터와 같은 종류의 상태에 이르기까지 말이죠. 우린 그들을 <strong>모두 동일한 전역 상태</strong>로 취급했습니다.</p>
<p>이러한 사고방식은 우리가 상태를 다르게 분류하면서 부터 변화하게 됩니다. 상태가 어디에서 사용되는지가 아니라 <strong>어떠한 종류의 상태인지</strong>를 따지면서 부터 말이죠.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/26.png" alt=""></p>
<p>다크모드 토글 버튼을 클릭할때 사용되는 상태처럼 우리가 완전히 제어권을 가지고 동기적으로 동작하는 상태는 이슈들의 리스트 데이터와 같이 외부에서 통제되고 비동기적으로 동작하는 상태와는 완전히 다른 특성들을 가집니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/27.png" alt=""></p>
<p>비동기 상태 혹은 서버 상태를 사용할때 클라이언트는 데이터를 불러올 당시의 <strong>스냅샷</strong>만을 갖고 있게 됩니다. 클라이언트가 그 상태에 대한 제어권을 갖고 있지 않다보니 이러한 상태는 언제 든지 만료될 수 있죠. 백엔드, 정확히는 데이터베이스가 이 상태를 소유하게 됩니다. 클라이언트는 그저 그 스냅샷을 화면상에서 띄울 수 있게 빌려오는 것 뿐입니다.</p>
<p>사용자가 브라우저 탭을 열어놓고 30분간 다른 탭을 띄워놓다가 다시 그 탭으로 되돌아가는 것을 해보면 스냅샷을 클라이언트가 빌려온다는 개념이 크게 체감될 것 입니다. 이런 상황에서 사용자가 되돌아 왔을때 페이지 내 데이터가 자동으로 갱신되어 있다면 더 멋진 경험을 제공해주지 않을까요? 이런 자동 갱신 과정이 필요한 이유는 사용자가 페이지를 떠나있는 동안 다른 이용자가 데이터를 언제든지 바꿀 수 있기 때문입니다. </p>
<p>그리고 이 서버 상태는 로딩과 에러 상태에 관한 정보와 같이 메타 정보들을 관리하는 작업이 필요하기에 동기적으로는 사용할 수 없는 상태값 입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/28.png" alt=""></p>
<p>그래서 데이터를 자동으로 최신화 하는 작업을 비롯해 비동기적인 라이프사이클(에러, 로딩)을 관리하는 작업들은 기존의 다목적 상태 관리자들에게 바랄 수 없는 것들 이였습니다. 그러나 이제 여러분에게는 이런 요소들을 잘 수행해주는 도구들이 갖추어졌으니 비동기 상태들을 더 잘 사용할 수 있게 되었습니다. 그저 더 알맞은 일들을 할 수 있게 적합한 도구를 사용하기만 하면 됩니다.</p>
<br/>

<h1 id="2-상태를-효율적으로-관리하기feat-staletime">2. 상태를 효율적으로 관리하기(feat. staleTime)</h1>
<p>두번째는 상태 관리자가 무엇인지 그리고 왜 리액트 쿼리가 상태 관리자 중 하나인지를 이해하는 것입니다.</p>
<p>일반적으로 상태 관리자들은 앱 내에서 상태를 효율적으로 이용하게 해줍니다. 여기서 중요한 <strong>효율적</strong> 이란 용어를 다음와 같이 정의하고 싶습니다. </p>
<blockquote>
<p>효율적인 업데이트 = 업데이트를 하되 너무 많이 하지 않도록 하기</p>
</blockquote>
<p>너무 많은 업데이트가 문제가 되지 않지 않았다면, 우리 모두 React Context를 통해 상태 관리 하는 방식을 택했을 겁니다. 그러나 이는 문제사항으로 여겨지고 있고 대다수의 상태 관리 라이브러리들은 여러가지 방법을 통해 불필요하게 많은 업데이트가 일어나는 문제들을 해결하려고 하고 있죠. </p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/31.png" alt=""></p>
<p>두 개의 유명한 상태 관리 라이브러리인 <code>Redux</code>와 <code>zustand</code> 는 <code>selector</code> 라는 api를 제공 하고 있습니다. 이들은 컴포넌트가 관심있어 하는 상태의 일부만 구독할 수 있게 합니다. <code>store</code> 내 구독하고 있지 않는 다른 부분이 업데이트 된다 하더라도 컴포넌트들은 신경쓰지 않도록 말이죠. 그리고 이 두 라이브러리은 모두 전역으로 이용가능하게 만들어 놓았기에 <code>hook</code> 을 호출하여 앱 내부 어디에서나 원하는 상태값에 접근할 수 있습니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/32.png" alt=""></p>
<p>리액트 쿼리도 이 원칙에서 크게 벗어나지 않습니다. 다만, 클라이언트가 구독하고 있는 대상이 기존의 라이브러리 와는 다르게 <code>queryKey</code> 라는 고유 키를 통해 구분 된다는 약간의 차이만 있을 뿐입니다.</p>
<p>클라이언트가 <code>useIssue</code> 이라는 커스텀 훅을 호출하고 있는 상황에서, <code>QueryCache</code> 내의 <code>issues</code> 라는 <code>slice</code>에 변화가 생기면 해당 데이터는 업데이트가 이루어지게 됩니다.  </p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/33.png" alt=""></p>
<p>앞의 기능들만으로 충분하지 않다면 리액트 쿼리에도 있는 <code>selector</code> 를 이용해볼 수 있습니다. 이를 통해 저장된 결과들 중 일부만을 도출하여 컴포넌트가 관심있는 것만 가져가는 정제화된 상태 구독도 가능합니다. 예를 들어, 위의 예시의 코드를 사용할때 클라이언트 내 컴포넌트가 이슈를 <code>“opened”</code> 상태에서 <code>“closed”</code> 상태로 바꾼다 하더라도 <code>issue</code>의 길이는 변하지 않기에 <code>useIssueCount</code> 훅을 이용해 구독중인 컴포넌트에는 어떠한 리렌더링도 발생하지 않습니다.</p>
<p>그리고 다른 상태 관리자들과 마찬가지로 클라이언트는 사용하고자 하는 모든 영역에서 <code>useQuery</code> 를 호출함으로써 필요한 상태 데이터에 접근할 수 있게 됩니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/34.png" alt=""></p>
<p>위 사진에서 보이는 코드들은 지양해야될 패턴들입니다. <code>useEffect</code> 를 이용하거나 <code>onSuccess</code> 라는 콜백을 이용해 별도의 <code>state</code> 에 데이터를 저장해 두는 작업들 말입니다.</p>
<p>이러한 형태의 상태 동기화 작업은 단일 진실 공급원(<a href="https://www.lesstif.com/software-engineering/ssot-single-source-of-truth-128122887.html">Single Source Of Truth</a>) 원칙을 위반하며 불필요한 작업이라고 여겨집니다. 리액트 쿼리가 이미 상태 관리자이기에 별도의 상태로 관리할 필요가 없기 때문이죠.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/35.png" alt=""></p>
<p>여기까지 보셨다면 여러분들은 이런 생각이 드실지 모르겠습니다. 예를 들어, 위의 그림처럼 <code>useIssues</code> 훅을 서로 다른 3개의 컴포넌트에서 사용한다고 합시다. 그런데 여기서 Dialog와 같이 일부 컴포넌트가 조건부로 렌더링된다고 한다면, 동일한 API endpoint에 대한 데이터가 여러번 중복해서 불러와지는 것을 보게될 것입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/36.png" alt=""></p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/37.png" alt=""></p>
<p>첫번째 사진의 상황을 보고는 “2초 전에 이미 불러왔는데 왜 또 다시 불러오는 걸까??” 라고 생각할 수 있습니다. 이제 여러분은 백엔드로의 중복요청을 막아야 한다는 이유로 공식문서에 들어가 <code>refetch</code>와 관련된 모든 옵션을 비활성화하게 될 수도 있습니다. 그러고는 “상태값을 리덕스에 뒀어야했나..”라고 후회하게 될지도 모르죠.</p>
<p>이러한 일이 벌어지는 것에는 다 이유가 있으니 잠시만 저의 이야기를 들어주시길 바랍니다. 왜 리액트 쿼리는 이렇게 많은 요청을 보내는 것일까요?</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/38.png" alt=""></p>
<p>이는 비동기 상태에서 필요한 요구사항들을 수행해야 되기 때문입니다. 비동기 상태는 언제든지 만료될 수 있기에 특정 시점에서 업데이트하는 작업이 필요합니다. 리액트 쿼리에서는 이러한 업데이트 작업을 다음의 특정 상황들에서 진행 합니다. </p>
<ul>
<li>window에 포커스가 있을때</li>
<li>컴포넌트가 마운트 되었을때</li>
<li>네트워크 연결이 다시 이루어졌을때</li>
<li>QueryKey가 바뀔때</li>
</ul>
<p>이러한 이벤트가 일어날때마다 리액트 쿼리는 자동으로 쿼리를 다시 패치할 것 입니다. 그러나 이게 전부가 아닙니다. 더 중요한 것은 리액트 쿼리가 이러한 업데이트 작업을 모든 <code>Querie</code>들에 대해서 하지는 않는 다는 것 입니다. 오로지 <code>stale</code> 하다고 여겨지는 Query에 대해서만 위 작업을 진행합니다. 여기서 <code>stale</code> 에 대한 내용은 이번 아티클에서 두번째로 중요한 내용이 될 것입니다. 바로 <code>staleTime</code> 여러분의 가장 친구라는 것 입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/40.png" alt=""></p>
<p>리액트 쿼리는 또한 데이터 동기화 도구이기는 하나, 무자비하게 모든 쿼리의 데이터를 백그라운드 단에서 다시 불러오지는 않습니다. 다시 패치할지 말지를 결정하는 기준은 <code>staleTime</code> 에 의해 정해지며 여기서 <code>staleTime</code>은 “데이터가 <code>stale</code> 해지는 데 까지 걸리는 시간”을 의미합니다. </p>
<p><code>stale</code> 의 반댓말은 <code>fresh</code> 인데, 이는 <code>fresh</code> 하다고 여겨지는 데이터는 다시 불러오지 않고 미리 캐시된 데이터를 클라이언트에게 전달됨을 의미합니다. <code>fresh</code> 한 상태가 아니라면 클라이언트는 캐시된 데이터를 얻음과 동시에 다시 데이터를 불러 오는 과정이 진행됩니다.</p>
<p>이 말인 즉슨, 오로지 <code>stale</code> 한 쿼리들만이 자동으로 업데이트 되는 것을 의미합니다. 다만, <code>staleTime</code>은 기본적으로 <code>0</code>으로 설정됩니다. 당연히 여기서 <code>0</code>이라는 숫자는 <code>0</code> 밀리초를 의미하며, 리액트 쿼리는 별다른 설정이 없다면 모든 쿼리를 즉시 <code>stale</code> 한 상태라고 여깁니다. 이러한 방식은 엄청나게 반복된 데이터 패칭을 야기할 수 있지만, 리액트 쿼리에서는 네트워크 상에서의 요청을 최소화 하지 않고 데이터를 최신 상태로 유지하는 쪽을 선택했습니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/42.png" alt=""></p>
<p>그래서 이제 <code>staleTime</code> 값은 여러분에게 주어진 리소스와 요구사항에 따라 알맞게 정의하는게 필요합니다. 또한 <code>staleTime</code> 값에는 정해진 정답이 있는게 아닙니다. 여러분이 생각하기에 서버가 재시작 될때에만 변화하는 게 필요하다고 생각된다면 <code>staleTime</code>을 <code>Infinity</code> 로 설정하는 것이 가장 적합한 선택이 될 것입니다. 반면에, 협력툴과 같이 많은 사용자들이 동시에 데이터를 업데이트한다면, <code>staleTime</code>을 <code>0</code>으로 두는 것이 더 나은 선택이 될 것입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/43.png" alt=""></p>
<p>따라서 리액트 쿼리을 활용하는데 있어서 매우 중요한 부분은 <code>staleTime</code> 을 알맞게 조절하는 것입니다. 다시 한번 말하지만 이는 정답이 정해져있지 않으며 제가 가장 선호하는 바는 위의 사진과 같이 전역적으로 기본값을 설정해두고 필요할때마다 덮어쓰는 방식입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/44.png" alt=""></p>
<p>자, 앞서서 한번 봤던 비동기 상태에 대한 요구사항들을 다시 살펴보도록 합시다. 우리는 이제 리액트 쿼리에서는 데이터가 <code>stale</code>한 상태가 되거나 위에서 언급한 이벤트들 중 하나가 발생하면 캐시를 최신화 시킨다는 사실을 알고 있습니다. 그 이벤트 중 제가 가장 강조하고 싶은 것 중 하나가 바로 <code>QueryKey</code>가 변화하는 이벤트 입니다.</p>
<p>그럼 <code>QueryKey</code>가 변화하는 이벤트는 언제 가장 많이 발생할까요? 이 질문에 대한 대답은 다음 주제로 이어집니다.</p>
<br/>

<h1 id="3-파라미터들을-의존성으로-받아들여라">3. 파라미터들을 의존성으로 받아들여라</h1>
<p>세번째 주제는 <code>Query</code>의 파라미터들을 의존성으로서 봐야 한다는 것입니다. 저는 이 주제를 공식문서에도 강조하고 별도의 <a href="https://tkdodo.eu/blog/practical-react-query#treat-the-query-key-like-a-dependency-array">블로그 포스트</a>로 올렸음에도 다시 한번 강조하고 싶습니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/46.png" alt=""></p>
<p>여러분이 위와 같이 <code>filters</code> 라는 파라미터를 정의해서 <code>queryFn</code> 함수에서 데이터 패치 요청시에 사용하고 있다면 여러분은 <code>queryKey</code> 에도 <code>filters</code> 라는 변수를 추가해 주어야 합니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/47.png" alt=""></p>
<p>이러한 규칙은 리액트 쿼리가 잘 작동해주는 것을 보장해줍니다. Query 내 엔트리들이 그들의 입력값(예시에서는 <code>filters</code>)에 따라 별도로 캐싱되도록 합니다. 그래서 다른 필터값을 가지게 되면 캐시 상에서 다른 키 값으로 저장되도록 하여 <a href="https://en.wikipedia.org/wiki/Race_condition">Race Condition</a> 문제를 피하도록 해줍니다. </p>
<p>또한 이러한 방식은 <code>filters</code> 값이 변화했을때 자동으로 데이터를 다시 불러오도록 해주는데 이는 이전 cache 엔트리에서 다른 엔트리로 이동하는 작업으로 여겨지기 때문입니다. 게다가 이는 디버깅 하기 까다로운 <code>stale closure</code> 문제를 예방하곤 합니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/48.png" alt=""></p>
<p>또한 이러한 규칙을 잘 준수할 수 있도록 도와주는 <code>eslint plugin</code>이 있습니다. 여러분이 만약 <code>queryFn</code> 내부에서 파라미터를 사용하고 있다면 <code>queryKey</code> 에서 의존성으로서 명시하도록 도와주며 이는 자동으로 <code>fixable</code>하기에 해당 플러그인을 사용되는 것이 매우 권장됩니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/49.png" alt=""></p>
<p>여러분들이 <code>queryKey</code> 를 <code>useEffect</code> 에서 사용되는 의존성 배열과 비슷하다고 생각할 수는 있으나 의존성 배열처럼 참조적 안정성(<code>referential stability</code>)에 주의해야할 필요는 없습니다. 위의 예시처럼 참조적 안정성을 지키려고 <code>queryKey</code>나 <code>queryFn</code> 에 메모이제이션 훅을 사용하는 행위까지는 할 필요 없다는 의미입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/50.png" alt=""></p>
<p>이제 마지막으로 새로운 문제에 대해서 소개해보자 합니다. 우리는 <code>useQuery</code> 라는 훅을 원하는 컴포넌트 계층에서 자유롭게 쓰고 있는데 만약 특정 컴포넌트에서만 존재하는 값이 <code>Query</code>의 의존성이 되어버린다면 어떻게 해야할까요? 예를 들어 <code>useIssues</code> 라는 훅이 <code>filters</code> 에 의존적인 상황에서 <code>filters</code> 에 대한 접근 권한이 특정 컴포넌트에게만 있는 제한적인 상황이라면 어떻게 해야할까요?</p>
<p>정답은 다시 말하지만, <strong>리액트 쿼리가 이에 대해 관여하지 않는다</strong> 입니다. 이러한 문제는 순전히 클라이언트 상태 관리 문제일 것입니다. 왜냐하면 위 예시에서의 <code>filter</code> 값은 클라이언트 상태이기 때문이죠. 그리고 그것을 어떻게 관리할지는 여러분에게 달려있고요. 지역상태나 전역 상태나 여러분에게 가장 적합한 것을 사용하면 됩니다. <code>filters</code> 라는 값을 <code>url</code>에 저장해두는 것 역시 좋은 방법이 될 것입니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/52.png" alt=""></p>
<p>예를 들어, <code>zustand</code> 와 같은 상태 관리자에게 <code>filters</code> 값을 저장했을때 어떠한 양상을 보이는지 확인해봅시다. 이전 예시와 다른 점은 그저 <code>filters</code> 가 커스텀 훅의 파라미터로 들어오는 대신에 스토어로부터 직접 얻어온다는 점 입니다. 이는 커스텀 훅을 작성할때 훅들을 합성을 하는 것의 강력함을 보여줍니다.</p>
<p>그리고 우리는 여기서 <code>useQuery</code> 를 통해 관리되는 서버 상태와 <code>useStore</code> 에 의해서 관리되는 클라이언트 상태의 극명한 차이를 느낄 수 있습니다. 우리가 스토어에 있는 <code>filters</code> 의 값이 변함에 따라 Query는 자동으로 실행되어 캐시로부터 이용 가능한 가장 최신의 데이터를 가져오게 되는 것 입니다. </p>
<p>이러한 패턴은 리액트 쿼리를 순수한 비동기 상태 관리자로서 사용하는 것을 가능하게 해줍니다.</p>
<p><img src="https://tkdodo.eu/blog/images/thinking-in-react-query/53.png" alt=""></p>
<p>이번에 다룬 내용들을 요약해보자면 다음과 같습니다.</p>
<blockquote>
<ol>
<li><p>리액트 쿼리는 데이터 패칭 라이브러리가 아니라 비동기 상태 관리 라이브러리 입니다. </p>
</li>
<li><p><code>staleTime</code> 은 여러분의 가장 친한 친구입니다 - 그러나 필요에따라 알맞은 수정이 필요합니다.</p>
</li>
<li><p>파라미터를 의존성으로 생각해야하며, 이전에 소개한 lint rule을 꼭 사용해야합니다.</p>
</li>
</ol>
</blockquote>
<p>앞서 언급한 이 세가지 원칙들을 따르도록 우리의 사고방식을 바꾸게 된다면 신발끈을 바로 매는 것이 우리의 삶의 질을 바꿔 놓은 것처럼 리액트 쿼리를 다루는데에 있어서 훨씬 더 나은 경험을 할 수 있게 될 것입니다. </p>
<p>오늘의 이야기는 여기까지 입니다. <a href="https://twitter.com/tkdodo">트위터</a> 팔로우와 <a href="https://tkdodo.eu/blog/">블로그</a> 구독을 부탁드립니다. 리액트 쿼리 v5 출시까지도 얼마 남지 않았으니 트위터와 블로그를 통해 이에 대한 최신 정보를 받아보실 수 있을 겁니다. 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 18 Concurrent 로 UX 개선하기]]></title>
            <link>https://velog.io/@seungchan__y/React-18-Concurrent-%EB%A7%9B%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/React-18-Concurrent-%EB%A7%9B%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 05 May 2023 14:25:01 GMT</pubDate>
            <description><![CDATA[<h1 id="🤔-concurrent-feature-에-대해-알아보기">🤔 Concurrent Feature 에 대해 알아보기</h1>
<p><code>Concurrent Feature</code>는 리액트 18버전에서 release 된 기능이다. 여기서 Concurrent는 동시성을 의미하며 보통 동시성이라고 하면 운영체제에서 흔히 사용되는 용어로 컴퓨터가 동시에 여러가지 일들을 처리하는 것처럼 보이도록 하기 위해 할 일들을 작게 쪼개고 이를 번갈아가며 실행하는 방식을 의미한다. </p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/28b08914-a16b-4768-b6dc-a5a3652fb8ff/image.png" alt=""></p>
<p>동시성이 없었다면 위과 같이 커피를 만드는 것을 기다렸다가 잼바른 빵을 만드는 일을 해야 한다. 이렇게 되면 커피는 식어버릴 것이다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/6138e075-bc36-4dd2-b3bb-53b9121ffdfe/image.png" alt=""></p>
<p> 이를 위와 같이 두 개의 큰 일들을 세부적인 태스크들로 잘게 쪼갠 뒤 이들을 번갈아 진행한다면 두 가지 결과를 거의 동시에 끝 마치며 맛있는 아침식사를 할 수 있다.</p>
<p>이러한 동시성의 개념은 리액트 상에서 복잡한 UI 상호작용을 처리하는데 있어서 필요해졌다. 본격적으로 리액트의 동시성 프로그래밍에 대해 알아보기전에 리액트에서 사용되는 데이터 패칭 전략들을 알아보자</p>
<br/>

<h2 id="fetch-on-render">Fetch-on-render</h2>
<pre><code class="language-jsx">function App(){
    return (
        &lt;&gt;
            &lt;ArticlePage/&gt;
            ...
        &lt;/&gt;
    );
}

function ArticlePage() {
    const { articles, isLoading } = useArticlesQuery();

    // articles가 로딩되지 않는다면 다른 UI를 보여준다.
    if(isLoading){
        return (
                &lt;Spinner /&gt;
        );
    }

    // articles 로딩이 완료되면 해당되는 UI를 보여준다.
    // isLoading이 false가 되기 전까지 TrendArticles는 실행조차 되지 못한다.
    return (
        &lt;&gt;
            {articles.map((article, idx) =&gt; &lt;ArticleCard key={idx} article={article}/&gt;}
            &lt;TrendArticles /&gt;
        &lt;/&gt;
    );
}</code></pre>
<h3 id="특징">특징</h3>
<ul>
<li>데이터 패칭이 이뤄지고 나서야 관련된 컴포넌트를 렌더링 하는 방식이다.</li>
</ul>
<h3 id="문제점">문제점</h3>
<ul>
<li><p>** <code>Waterfall</code> 현상 발생** : <code>TrendArticles</code> 컴포넌트는 <code>articles</code> 데이터가 로딩이 완료되어야 렌더링 되기 시작한다. 만약 <code>TrendArticles</code> 내부에 비동기 데이터를 호출하는 작업이 있다면 이는 <code>articles</code>  데이터가 모두 불러와져야 진행이 가능하다. 이러한 <code>fetch-on-render</code> 방식이 계층 별로 반복되면 데이터 패칭도 순차적으로 일어나면서 렌더링 성능에 나쁜 영향을 끼치게 된다.</p>
</li>
<li><p><strong>나쁜 가독성</strong> : 하나의 컴포넌트 코드 내에서 로딩 상태에 대한 로직이 포함되어야 한다는 것이다. 이 때문에 <code>ArtciePage</code> 에서 핵심로직에 집중하기 어려워 진다.</p>
</li>
</ul>
<br/>

<h2 id="fetch-then-render">Fetch-then-render</h2>
<pre><code class="language-jsx">function ArticlePage({ ... }) {
  const [allArticles, setAllArticles] = useState([]);
  const [trendArticles, setTrendArticles] = useState([]);

  useEffect(() =&gt; {
    // 비동기 작업을 동시에 병렬적으로 실행
    Promise.all([fetchAllArticles(), fetchTrendArticles()]).then(([allArticles, trendArticles]) =&gt; {
      setAllArticles(allArticles);
      setTrendArticles(trendArticles);
    });
  }, []);

  return (
    &lt;&gt;
      &lt;AllArticleList articles={allArticles} /&gt;
      &lt;TrendArticleList articles={trendArticles} /&gt;
    &lt;/&gt;
  );
}</code></pre>
<h3 id="특징-1">특징</h3>
<ul>
<li><code>Fetch-on-render</code> 방식의  <code>Waterfall</code> 현상을 해결할 수 있도록 비동기 데이터 호출을 <code>Promise.all</code> 로 묶어 병렬적으로 처리하는 방법이다.<ul>
<li>위 예시에서는 기존의 <code>fetch-on-render</code> 방식을 변형해 비동기 데이터들을 호출하는 부분을 최상단에서 몰아 처리할 수 있도록 하고 있다.</li>
</ul>
</li>
</ul>
<h3 id="문제점-1">문제점</h3>
<ul>
<li><p><strong>불필요한 관심사들의 결합 :</strong>  병렬적으로 처리하는 과정에서 불가피하게 서로 다른 비동기 데이터가 <code>Promise.all</code> 로 묶이게 된다. 이로 인해 서로 다른 데이터가 하나의 코드에서 호출되게 되며 강한 결합도를 가진다.</p>
<ul>
<li>가령 <code>TrendArticles</code> 를 불러오는 부분이 실패하면 <code>AllArticles</code> 부분도 렌더링 되지 못한다.</li>
</ul>
</li>
<li><p><strong>다른 비동기 데이터가 완료 되어야 렌더링이 가능 :</strong> 가령 <code>TrendArticles</code> 은 받아올 데이터가 극소수여서 <code>20ms</code>만에 완료되는 한편,  <code>AllArticles</code> 는 받아올 데이터가 너무 많아 <code>100ms</code> 가 소요된다고 하자. 이렇게되면 <code>TrendArticles</code> 의 데이터는 빠르게 로드되었음에도 <code>AllArticles</code> 때문에 렌더링이 역시 지연 되어 버린다.</p>
</li>
</ul>
<br/>

<h2 id="render-as-you-fetchwith-suspense">Render-as-you-fetch(with Suspense)</h2>
<pre><code class="language-jsx">function App(){
    return (
    &lt;&gt;
        &lt;Suspense fallback={&lt;Spinner/&gt;}&gt;
            &lt;AllArticlePage/&gt;
        &lt;/Suspense&gt;
        &lt;Suspense fallback={&lt;Spinner/&gt;}&gt;
            &lt;TrendArticlesPage/&gt;
        &lt;/Suspense&gt;
        ...
    &lt;/&gt;;
}

function AllArticlesPage() {
    const { articles } = useArticlesQuery();

    return (
        &lt;&gt;
            {articles.map((article, idx) =&gt; &lt;ArticleCard key={idx} article={article}/&gt;}
            ...
        &lt;/&gt;
    );
}

function TrendArticlesPage() {
    const { articles } = useTrendArticlesQuery();

    return (
        &lt;&gt;
            {articles.map((article, idx) =&gt; &lt;ArticleCard key={idx} article={article}/&gt;}
            ...
        &lt;/&gt;
    );
}
</code></pre>
<h3 id="특징-2">특징</h3>
<ul>
<li>렌더링 작업과 비동기 데이터 호출 과정이 동시에 이루어진다.<ul>
<li>비동기 데이터를 호출하는 과정, fallback UI를 보여주는 과정, 완성된 UI를 보여주는 과정 등 기존의 렌더링 과정들이 여러 작은 태스크들로 쪼개진 뒤 번갈아가며 진행된다.</li>
</ul>
</li>
<li>비동기 데이터 호출을 통해 로딩이 발생하면 <code>Suspense</code> 가 이를 포착하여 UI는 <code>fallback</code> 으로 보여주고 로딩이 완료되면 완성된 UI를 보여준다<ul>
<li>이를 통해 컴포넌트 내부에선 로딩 상태에 대한 분기 처리가 필요없어져 코드의 가독성도 높아진다.</li>
<li>비동기 데이터에 대한 분기처리로 인해 waterfall 현상 역시 사라진다.</li>
</ul>
</li>
</ul>
<br/>

<h1 id="🧪-suspense-동작방식-알아보기">🧪 Suspense 동작방식 알아보기</h1>
<p>리액트 공식문서 예시에서는 Promise를 사용하는데 있어서 아래와 같은 <code>wrapPromise</code> 함수를 예시로 제공해주고 있다. </p>
<pre><code class="language-jsx">function wrapPromise(promise) {
  let status = &quot;pending&quot;;
  let result;
  let suspender = promise.then(
    (r) =&gt; {
      status = &quot;success&quot;;
      result = r;
    },
    (e) =&gt; {
      status = &quot;error&quot;;
      result = e;
    }
  );

  return {
    read() {
      if (status === &quot;pending&quot;) {
        throw suspender;
      } else if (status === &quot;error&quot;) {
        throw result;
      } else if (status === &quot;success&quot;) {
        return result;
      }
    },
  };
}

export function fetchProfileData() {
  let userPromise = fetchUser();
  return {
    user: wrapPromise(userPromise),
  };
}

function fetchUser() {
  return new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      resolve({
        name: &quot;Ringo Starr&quot;,
      });
    }, 2000);
  });
}

function ProfileDetails({ resource }) {
  const user = resource.user.read();
  return &lt;h1&gt;{user.name}&lt;/h1&gt;;
}

function ProfilePage({ resource }) {
  return (
    &lt;&gt;
      &lt;Suspense fallback={&lt;h2&gt;Loading details...&lt;/h2&gt;}&gt;
        &lt;ProfileDetails resource={resource} /&gt;
      &lt;/Suspense&gt;
    &lt;/&gt;
  );
}

function App() {
  const [tab, setTab] = useState(&quot;home&quot;);
  const [resource, setResource] = useState(fetchProfileData());

  return &lt;ProfilePage resource={resource} /&gt;
}</code></pre>
<p>위 코드의 실행과정을 살펴보면 아래와 같다.</p>
<ul>
<li><code>fetchProfileData</code> 실행 → <code>wrapPromise</code> 는 실행 시 pending 중인 Promise를 반환하는 <code>read</code> 함수를 반환한다.</li>
<li><code>App</code> 컴포넌트 렌더링 → <code>ProfilePage</code> → <code>ProfileDetails</code> 이 실행되면서 <code>read</code>함수가 실행된다</li>
<li>이로 인해 <code>read</code> 함수는 pending 중인 <code>Promise</code>를 반환하고 이를 <code>ProfileDetails</code> 상단의 <code>Suspense</code> 에 의해 캐치되어 fallback UI를 띄운다.</li>
<li><code>Promise</code>의 비동기 처리가 완료되어 status가 <code>success</code> 로 전환되면 <code>read</code> 함수가 재실행되며 결과값을 반환한다.</li>
<li>이후에는 <code>ProfileDetails</code> 컴포넌트가 다시 실행되며 완성된 UI를 보여준다.</li>
</ul>
<p>여기서 <code>read</code> 함수가 다시 실행되는 것이나, <code>ProfileDetails</code> 컴포넌트가 진짜 다시 실행되는지 의문이 든다면 아래의 샌드박스를 확인해보면 좋을 것 같다.</p>
<p>!codesandbox[strange-kilby-2cs057]</p>
<p>추가적으로 확인할 수 있는 한가지 흥미로운 점은 <code>Suspense</code> 가 연달아 존재하더라도 블락킹 되는 것이 아니라 병렬적으로 실행된다는 점이다.</p>
<pre><code class="language-jsx">function ProfilePage({ resource, showProfile }) {
  return (
    &lt;&gt;
      &lt;Suspense fallback={&lt;h2&gt;Loading details...&lt;/h2&gt;}&gt;
        &lt;ProfileDetails resource={resource} /&gt;
      &lt;/Suspense&gt;
      &lt;button onClick={showProfile}&gt;Refresh&lt;/button&gt;
      &lt;Suspense fallback={&lt;h2&gt;Loading posts...&lt;/h2&gt;}&gt;
        &lt;ProfileTimeline resource={resource} /&gt;
      &lt;/Suspense&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>위 코드에서 보이는 <code>ProfileDetails</code> 와 <code>ProfileTimeline</code> 은 서로 다른 비동기 처리를 함에도  조건문을 통해 분기 처리 했을때 아래 내용들이 블로킹 되는 것과 다르게 서로 동시에 실행된다. 이로 인해 <code>waterfall</code> 현상이 발생하지도 않게되며 여기서 리액트가 도입한 Concurrent 모드의 진가가 확인된다.</p>
<br/>

<h1 id="💣-suspense의-문제점">💣 Suspense의 문제점</h1>
<p><code>Suspense</code> 로 로딩상태를 분리함으로 인해서 코드는 훨씬 간결하게 처리할 수는 있었으나 만약 응답속도가 매우 빠르게 이루어지는 비동기 요청에 대해서는 <code>Spinner</code> 로 인해서 오히려 깜빡임이 발생할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/31f128cc-6fe0-41ff-91d8-bb758bae8892/image.gif" alt=""></p>
<p>위 사진에서 보이는 것 처럼 비동기 호출이 이뤄질때 어느정도 로딩이 발생한다면 Spinner 를 보여주는 UI는 자연스러운 과정으로 이해해볼 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/b9e79d4f-1618-4ed6-8ac0-8d76ad26a3ea/image.gif" alt=""></p>
<p>하지만 두번째 사진처럼 비동기 처리가 매우 빠르게 처리된다면 Spinner를 띄우는 과정 때문에 오히려 깜빡임을 발생시킨다. 이는 사용자 경험상 좋지 않은 UI가 되어버린다.</p>
<p>이를 해결해줄 수 있는 방법으로 리액트 18에서는 <code>useTransition</code> 이라는 API를 제공한다.</p>
<pre><code class="language-jsx">const [isPending, startTransition] = useTransition();

const onClick = id =&gt; {
  startTransition(() =&gt; {
    setId(id);
  });
};

return(
    &lt;Suspense fallback={&lt;Spinner /&gt;}&gt;
      &lt;TvShowDetails id={id} /&gt;
    &lt;/Suspense&gt;
);

const Details = ({ id }) =&gt; {
  const tvShowResource = getTvDataResource(id).read();
  return (
    &lt;div className=&quot;flex&quot;&gt;
      ...
    &lt;/div&gt;
  );
};

export const TvShowDetails = ({ id }) =&gt; {
  return (
    &lt;div className=&quot;tvshow-details&quot;&gt;
      &lt;Suspense fallback={&lt;Spinner /&gt;}&gt;
        &lt;Details id={id} /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;
  );
};
</code></pre>
<p><code>useTransition</code> 으로 부터 나온 <code>startTransition</code>  이라는 함수에 상태 업데이트 로직을 부여하면 해당 상태 업데이트로 인해 새롭게 발생하는 <strong>비동기 처리가 끝날때까지 화면 렌더링 변화를 지연시킨다. 정확히는 원래의 UI를 보여주다가 업데이트된 UI를 보여주는 형태다.</strong> 이를 적용해보면 아래처럼 깜빡임 없이 개선해볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/d521d468-0f1f-49b1-8a8c-b387879da91f/image.gif" alt=""></p>
<br/>

<h1 id="🪡-transition의-동작과-그-차이">🪡 Transition의 동작과 그 차이</h1>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/d6a0bf1b-5d5f-4ab5-afae-b268b938104f/image.png" alt=""></p>
<blockquote>
<p>💡 timeout으로 인해 Receded 상태로 넘어가는 것은 현재 삭제되었다. 즉, 얼마나 오래걸리든 비동기 상태가 완료될때까지는 원래의 UI를 계속 유지하게 된다.</p>
</blockquote>
<p>그렇다면 <code>Transition</code> 은 어떠한 이유로 기존의 방식과 다른 동작 결과를 낳는 것일까? <code>setState</code>로 인해 <code>state</code> 업데이트가 일어나면 다음의 3가지 단계로 나뉘어 화면 렌더링에 반영된다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/f6a70697-0722-4c11-ad7b-d89f05972281/image.png" alt=""></p>
<h3 id="transition-단계">Transition 단계</h3>
<ul>
<li><p><code>Receded</code> : 영어단어 자체로는 물러난다는 의미를 가진다. <code>Transition</code>을 사용하지 않는 경우 발생하는 단계이며, <code>ProfileDetails</code> 내부에서 비동기 작업이 일어나면 <code>Suspense</code> 에 의해서 둘러 쌓인 상위 계층의 <code>Fallback</code> 으로 물러남을 의미한다.</p>
</li>
<li><p><code>Pending</code> : <code>Transition</code> 을 사용하는 경우 발생하는 단계이며, <code>Suspense</code> 내부에서 비동기 작업이 일어나면 UI 업데이틀르 <strong>지연</strong>시키고 원래 보여주던 UI를 계속 유지 한다.</p>
</li>
</ul>
<h3 id="loading-단계">Loading 단계</h3>
<ul>
<li><code>Loading</code> 은 현재 컴포넌트(<code>ProfilePage</code>)의 자식 요소에서 발생하는 비동기를 처리하는 과정을 처리중인 단계이다. 여기서는 <code>ProfileTimeline</code> 을 띄울때가 해당되며 Loading posts…가 UI 상에서 나오게 된다.</li>
</ul>
<h3 id="done-단계">Done 단계</h3>
<ul>
<li>비동기 처리가 완료됨에 따라 완성된 UI를 보여준다</li>
</ul>
<p>상태변화에 따른 UI 변화 단계들을  <code>transition</code>을 쓰는 경우와 쓰지 않는 경우로 나누어 정리해보면 아래와 같이 표현해볼 수 있다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Transition 단계</th>
<th>Loading 단계</th>
<th>Done 단계</th>
</tr>
</thead>
<tbody><tr>
<td>Transition 사용</td>
<td>Pending</td>
<td>Skeleton</td>
<td>Complete</td>
</tr>
<tr>
<td>Transition 미사용</td>
<td>Receded</td>
<td>Skeleton</td>
<td>Complete</td>
</tr>
</tbody></table>
<p>즉,<code>Transition</code> 을 도입하면 <code>Receded</code> 상태가 아닌<code>Pending</code> 상태로 전환되면서 비동기가 처리 되는 동안에는 이전의 UI를 유지하도록 하였고, 이는 기존의 업데이트 시작 -&gt; 로딩 -&gt; 업데이트 완료가 너무 빠르게 이뤄지면서 발생한 깜빡임을 해소할 수 있게 만들어준 것이다. </p>
<br/>

<h1 id="🧶-recoil과-transition">🧶 Recoil과 Transition</h1>
<p>아쉽게도 <code>transition</code> 은 아직 리액트 18에서도 개발중인 기능 중 하나이기에 써드파티 라이브러리에서 공식적으로 지원되고 있지는 않다. 다만, <code>Recoil</code> 의 경우 아직 불안전한 형태이지만 <code>transition</code> 이 적용되는 상태 API를 제공하고 있다.</p>
<pre><code class="language-jsx">// TrendSelect.tsx

import { trendAtom } from &#39;@/recoils/trendAtom&#39;;
import { useRecoilState_TRANSITION_SUPPORT_UNSTABLE } from &#39;recoil&#39;;

const TrendSelect = () =&gt; {
  const [selectedTrend, setSelectedTrend] = useRecoilState_TRANSITION_SUPPORT_UNSTABLE(trendAtom);

  const handleClickTrend = (word: Trend) =&gt; () =&gt; {
    startTransition(() =&gt; {
      setSelectedTrend(word);
    });
  };

    return (...)    
}

// TrendArticlesContent.tsx

import { useRecoilValue_TRANSITION_SUPPORT_UNSTABLE } from &#39;recoil&#39;;

import { trendAtom } from &#39;@/recoils/trendAtom&#39;;

const TrendArticlesContent = ({ isMobile }: Props) =&gt; {
  const trendType = useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(trendAtom);
  const { data: trends } = useTrendingArticlesQuery({ type: trendType });

  return (
    &lt;div&gt;
      {trends?.slice(0, isMobile ? 1 : 3).map(({ id, title, author, timestamp }, idx) =&gt; (
        &lt;TrendArticleItem
          key={idx}
          href={`${process.env.NEXT_PUBLIC_API_URL as string}/articles/${id}`}
          title={title}
          author={author}
          timestamp={timestamp}
        /&gt;
      ))}
    &lt;/div&gt;
  );
};

// App.tsx

&lt;Suspense
    fallback={
          &lt;LoadingWrapper&gt;
            &lt;Loading type=&quot;spinner&quot; /&gt;
          &lt;/LoadingWrapper&gt;
    }
    &gt;
        &lt;TrendArticlesContent isMobile={isMobile} /&gt;
&lt;/Suspense&gt;
</code></pre>
<p>위에서 볼 수 있듯이 불안정 버전이긴 하나 <code>transtition</code> 과 호환되는 recoil 전역 상태를 정의하여 사용 가능하다.</p>
<h1 id="⛳️-출처">⛳️ 출처</h1>
<p><a href="https://17.reactjs.org/docs/concurrent-mode-patterns.html#the-three-steps">Concurrent UI Patterns (Experimental) – React</a></p>
<p><a href="https://velog.io/@cadenzah/react-concurrent-mode">What is React Concurrent Mode?</a></p>
<p><a href="https://yrnana.dev/post/2022-04-12-react-18/">React 18 둘러보기 | nana.log</a></p>
<p><a href="https://react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react">React v18.0 – React</a></p>
<p><a href="https://blog.mathpresso.com/conceptual-model-of-react-suspense-a7454273f82e">Conceptual Model of React Suspense</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Suspense 도입과 Waterfall 현상 해결하기]]></title>
            <link>https://velog.io/@seungchan__y/Suspense-%EB%8F%84%EC%9E%85%EA%B3%BC-Waterfall-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/Suspense-%EB%8F%84%EC%9E%85%EA%B3%BC-Waterfall-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 13 Feb 2023 13:41:55 GMT</pubDate>
            <description><![CDATA[<h1 id="⏳-로딩-상태에-대한-분기처리">⏳ 로딩 상태에 대한 분기처리</h1>
<p>사내 프로젝트에서 환경 설정 페이지의 추가적인 기능들을 구현하는 부분을 맡게되었다. 서버 상태를 관리하고 이용하는데 있어서 <code>React Query</code> 를 활용하고 있었는데 대부분의 구조가 아래와 같이 명령형으로 서버 로딩상태에 대해 분기 처리되고 있었다.</p>
<pre><code class="language-jsx">const SettingPage = () =&gt; {
  const { data, isLoading } = useSettingQuery(); 

  if(isLoading){
    return null;
  }

  return (
       &lt;SettingPageForm&gt;
      {...}
    &lt;/SettingPageForm&gt;
  )
}</code></pre>
<p>로딩 중일때는 어떠한 컴포넌트도 보여주고 있지 않다가 로딩이 완료되면 띄우고자 하는 컴포넌트를 띄우는 방식이다. 이러한 방식은 컴포넌트가 자신의 핵심 로직 외에도 로딩상태에 대한 로직을 포함하고 있어야만 했다. 이는 코드의 가독성을 떨어뜨리고 컴포넌트의 독립성을 해치는 좋지 못한 패턴으로 여겨졌다. 특히나 요구사항을 추가해야하는 나로써는 위의 로직을 그대로 사용한다면 아래와 같은 코드를 작성해야했다.</p>
<pre><code class="language-jsx">const SettingPage = () =&gt; {
  const { data: setting1, isLoading: isLoading1 } = useSettingQuery(); 
  const { data: setting2, isLoading: isLoading2 } = useSettingQuery2();
  const { data: setting3, isLoading: isLoading3 } = useSettingQuery3();

  if(isLoading1 || isLoading2 || isLoading3){
    return null;
  }

  return (
       &lt;SettingPageForm&gt;
      {...}
    &lt;/SettingPageForm&gt;
  )
}
</code></pre>
<p>사용해야되는 쿼리가 늘어남에 따라 로딩 상태에 대한 개수도 늘어나고 이는 if 조건문을 비대하게 만들어버린다. 이를 개선하고자 비동기 상태에 대한 처리 로직을 컴포넌트 외부로 따로 뽑아 처리할 수 있는 <code>Suspense</code> 를 도입하기로 했다.</p>
<br/>

<h1 id="👏-suspense를-통해-선언형-컴포넌트로-바꾸기">👏 Suspense를 통해 선언형 컴포넌트로 바꾸기</h1>
<p><code>Suspense</code> 란 컴포넌트 내에서 사용하는 비동기적 데이터를 불러오는 동안 인자로 지정한 <code>fallback</code> 을 대체해서 보여주었다가 비동기처리가 완료되면 컴포넌트의 UI를 보여주는 역할을 하는 녀석이다.</p>
<pre><code class="language-jsx">function wrapPromise(promise) {
  let status = &#39;pending&#39;
  let response

  const suspender = promise.then(
    (res) =&gt; {
      status = &#39;success&#39;
      response = res
    },
    (err) =&gt; {
      status = &#39;error&#39;
      response = err
    },
  )

  const read = () =&gt; {
    switch (status) {
      case &#39;pending&#39;:
        throw suspender
      case &#39;error&#39;:
        throw response
      default:
        return response
    }
  }

  return { read }
}

export default wrapPromise


/*****************************************************/

function fetchData(url) {
  const promise = fetch(url)
    .then((res) =&gt; res.json())
    .then((res) =&gt; res.data)

  return wrapPromise(promise)
}


/*****************************************************/

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

const resource = fetchData(&#39;fetchURL...&#39;);

const UserPage = () =&gt; {
  const userData = resource.read()

  return (
    &lt;div&gt;
      &lt;Suspense fallback={&lt;p&gt;Loading user details...&lt;/p&gt;}&gt;
        &lt;UserProfile data={userData} /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;
  )
}

export default UserWelcome
</code></pre>
<p>본래 <code>Suspense</code> 를 사용하려면 위와 같이 <code>promise</code> 객체를 인자로 받아 상태에 따라 반환값을 달리해주는 함수를 구현한뒤 이를 컴포넌트 내부에서 사용해야한다. 위에서 보다시피 구현체가 복잡하며 컴포넌트 위에 리소스 변수를 선언해야되는 등 사용하기가 까다롭다.</p>
<p>다행히도 <code>React Query</code> 를 비롯한 다양한 서버 상태 관리 라이브러리에서 <code>Suspense</code> 기능을 손쉽게 사용할 수 있도록 알맞은 API를 제공해주고 있다.</p>
<pre><code class="language-jsx">
const fetcher = (url) =&gt; axios.get(url);


/*****************************************************/


const UserPage = () =&gt; {
     const { data } = useQuery([&#39;user&#39;], fetcher, { suspense: true });

  return (
    &lt;div&gt;
        &lt;UserProfile data={userData} /&gt;
    &lt;/div&gt;
  )
}

/*****************************************************/

&lt;Suspense fallback={&lt;p&gt;loading...&lt;/p&gt;}&gt;
  &lt;UserPage/&gt;
&lt;/Suspense&gt;

</code></pre>
<p><code>Suspense</code>를 사용하기 위해서 리액트 쿼리에 해줘야할 일은 평소처럼 사용하던 <code>query</code>에 <code>suspense</code> 옵션을 추가해주면 된다. </p>
<p>사내 프로젝트에서도 <code>Suspense</code> 를 통해서 로딩상태는 제거하고 컴포넌트를 간결하게 선언할 수 있었다.</p>
<pre><code class="language-jsx">const SettingPage = () =&gt; {
  const { data: setting1 } = useSettingQuery(); 
  const { data: setting2 } = useSettingQuery2();
  const { data: setting3 } = useSettingQuery3();

  return (
       &lt;SettingPageForm&gt;
      {...}
    &lt;/SettingPageForm&gt;
  )
}

/*****************************************************/


&lt;Suspense fallback={&lt;p&gt;Loading&lt;/p&gt;}&gt;
  &lt;SettingPage ... /&gt;
&lt;/Suspense&gt;
</code></pre>
<p>특히나 로딩상태로 인해 조건문이 줄줄이 붙어있던 부분이 사라지면서 컴포넌트 파일안에서는 컴포넌트 핵심로직에만 집중할 수 있게 되었다.</p>
<h1 id="🛝-waterfall-발생">🛝 Waterfall 발생</h1>
<p>여기서 <code>Waterfall</code> 이란 순차적으로 물흐르듯 네트워크 상의 흐름이 발생하는 것을 뜻한다. </p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/0777cade-20bd-4d42-b5d7-31e02d490b5e/image.png" alt=""></p>
<p>위의 사진을 보면 좀더 와닿을텐데 3개의 쿼리를 위해서 3번의 API 호출을 하는 과정에서 각각의 호출이 순차적으로 이루어진다는 것이다. 이는 컴포넌트의 코드를 읽는 과정에서 query 부분을 만날때마다 <code>Suspense</code> 에 의해서 API 호출로 인한 로딩이 발생하는 동안 <code>fallback</code> 컴포넌트로 대체되기 때문에 일어나는 현상이다.</p>
<p>이는 하나의 컴포넌트에 여러번의 비동기처리가 발생해야되기 때문에 발생한 것이다. 이를 해결해볼 수 있는 방법은 <code>useQuery</code> 대신 <code>useQueries</code> 훅을 사용해보는 것이다.</p>
<pre><code class="language-jsx">const SettingPage = () =&gt; {
  /*const { data: setting1 } = useSettingQuery(); 
  const { data: setting2 } = useSettingQuery2();
  const { data: setting3 } = useSettingQuery3();*/

  const results = useQueries([
    {
      queryKey: [&#39;setting1&#39;],
      queryFn: fetchData1,
      suspense: true,
    },
    {
      queryKey: [&#39;setting2&#39;],
      queryFn: fetchData2,
      suspense: true,
    },
    {
      queryKey: [&#39;setting3&#39;],
      queryFn: fetchData3,
      suspense: true,
    },
  ]);

  return (
       &lt;SettingPageForm&gt;
      {...}
    &lt;/SettingPageForm&gt;
  )
}

/*****************************************************/


&lt;Suspense fallback={&lt;p&gt;Loading&lt;/p&gt;}&gt;
  &lt;SettingPage ... /&gt;
&lt;/Suspense&gt;
</code></pre>
<p><code>useQueries</code> 훅은 다수의 쿼리를 처리할때 <code>useQuery</code>를 대신해서 사용할 수 있으며 <code>suspense</code> 옵션을 지정해주면 여러개의 쿼리도 병렬로 처리해준다. 하지만 이는 4.5 버전부터 사용이 가능했다. 특히나 현재 사내프로젝트에서는 리액트 쿼리 v3를 사용하고 있는 터였다. 버전업하기에는 여건이 되지 않는 터라 다른 방법을 고안해야했다.</p>
<pre><code class="language-jsx">
&lt;Suspense fallback={&lt;p&gt;Loading&lt;/p&gt;}&gt;
  &lt;SettingPage ... /&gt;
&lt;/Suspense&gt;

/*****************************************************/

const SettingPage = () =&gt; {

  ...

  return (
       &lt;SettingPageForm&gt;
      &lt;SettingSection1/&gt;
      &lt;SettingSection2/&gt;
      &lt;SettingSection3/&gt;
    &lt;/SettingPageForm&gt;
  )
}

const SettingSection1 = () =&gt; {
    const { data: setting1 } = useSettingQuery1(); 

      return (
          ...
    );
}

const SettingSection2 = () =&gt; {
    const { data: setting2 } = useSettingQuery2(); 

      return (
          ...
    );
}

const SettingSection3 = () =&gt; {
    const { data: setting3 } = useSettingQuery3(); 

      return (
          ...
    );
}

</code></pre>
<p>다행히도 작업중인 페이지 컴포넌트 내부에는 각각의 쿼리가 하나의 섹션에만 사용되고 있어서 위와 같이 섹션별로 컴포넌트를 분리하고 각 섹션에서 하나의 쿼리만을 호출하여 사용하도록 분리했다. 이를 통해 아래와 같이 병렬적으로 세개의 API가 동시에 처리되는 것을 확인할 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/f56229bd-6aa9-458d-bf86-518813280d28/image.png" alt=""></p>
<h1 id="⛳️-출처-및-참고자료">⛳️ 출처 및 참고자료</h1>
<ul>
<li><p><a href="https://velog.io/@bbaa3218/React-Suspense%EB%9E%80">[React] Suspense란?</a></p>
</li>
<li><p><a href="https://tech.kakaopay.com/post/react-query-2/">React Query와 함께 Concurrent UI Pattern을 도입하는 방법</a></p>
</li>
<li><p><a href="https://happysisyphe.tistory.com/54">혹시 무분별하게 Suspense 를 사용하고 계신가요? (react-query)</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[클린한 모달 사용하기 - 모달과 컴포넌트의 분리]]></title>
            <link>https://velog.io/@seungchan__y/%ED%81%B4%EB%A6%B0%ED%95%9C-%EB%AA%A8%EB%8B%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EB%AA%A8%EB%8B%AC%EA%B3%BC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EB%B6%84%EB%A6%AC</link>
            <guid>https://velog.io/@seungchan__y/%ED%81%B4%EB%A6%B0%ED%95%9C-%EB%AA%A8%EB%8B%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EB%AA%A8%EB%8B%AC%EA%B3%BC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EB%B6%84%EB%A6%AC</guid>
            <pubDate>Tue, 24 Jan 2023 12:28:00 GMT</pubDate>
            <description><![CDATA[<h1 id="📕-tldr">📕 tl;dr</h1>
<ul>
<li>컴포넌트와 결합된 모달을 분리시킨다.</li>
<li>모달을 전역에서 관리하고 렌더링 시키도록 한다.</li>
<li>모달의 열림/닫힘 상태 대신 렌더링할 모달들을 상태값으로 지정한다.</li>
<li>코드 스플리팅을 적용해 모달 컴포넌트로 인한 성능 저하를 예방한다.</li>
</ul>
<br/>

<h1 id="🧹-시작은-클린코드-영상으로부터">🧹 시작은 클린코드 영상으로부터..</h1>
<p>유튜브에서 프론트엔드 클린코드 관련 영상을 보면서 되게 인상 깊은 코드 사례를 보게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/e8305f6f-f8a1-4b42-b8ee-f6c5404264c1/image.png" alt=""></p>
<p>출처: <em><a href="https://youtu.be/edWbHp_k_9Y">SLASH21 - 실무에서 바로 쓰는 Frontend Clean Code</a></em></p>
<p>위 사례에서는 컴포넌트 내부적으로 사용할 팝업(모달)을 위해서 <code>렌더링 여부를 담는 상태값</code>, <code>팝업 내 이벤트 핸들러</code>, <code>팝업 컴포넌트</code> 관련 코드들을 작성해야했고 이들은 뿔뿔히 흩어지게 되는 경우를 보여주고 있다. 이로 인해 팝업 관련 로직은 읽기가 정말 어려워 졌다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/f17ad954-7bd4-4f20-b004-4ae922867934/image.png" alt="">
출처: <em><a href="https://youtu.be/edWbHp_k_9Y">SLASH21 - 실무에서 바로 쓰는 Frontend Clean Code</a></em></p>
<p>영상에서는 이러한 문제점을 해결하기 위해 팝업을 여는 것과 관련된 훅인 <code>usePopup</code> 을 정의한 뒤 팝업 컴포넌트를 최상단에서 렌더링 하도록 하여 관련 로직을 하나로 응집시키고 코드의 가독성을 높였다. 이 영상을 보면서 기존에 했던 프로젝트에서도 이와 유사한 방식을 적용해 볼 수 있다고 느꼈다. </p>
<br>

<h1 id="👻-기존-프로젝트-내-모달의-문제점">👻 기존 프로젝트 내 모달의 문제점</h1>
<h2 id="1-모달-로직으로-인한-코드의-가독성-저하">1. 모달 로직으로 인한 코드의 가독성 저하</h2>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/e3fa5cd2-5b0b-45dd-a7dd-aa702902e599/image.png" alt=""></p>
<p>위의 코드는 버튼 클릭시 지원과 관련된 모달을 띄우고 모달 내 버튼 클릭시 지원하기 로직을 처리하는 코드이다. 위에서 볼 수 있듯이 모달을 띄우기 위해서 <code>상태값 정의</code>, <code>모달 이벤트 핸들러 정의</code>, <code>모달 컴포넌트</code>와 관련된 코드들을 작성해야 했으며 이는 컴포넌트 내에서 뿔뿔이 흩어져 있다. 이로 인해 <code>지원하기 버튼</code> 이라는 컴포넌트 내에서 핵심 로직이 <code>지원하기</code> 이지만 모달을 띄우는 로직들로 인해 관련 내용을 파악하기 어려워 졌다.</p>
<br/>


<h2 id="2-불필요하게-많은-모달-컴포넌트-렌더링">2. 불필요하게 많은 모달 컴포넌트 렌더링</h2>
<p><img src="https://user-images.githubusercontent.com/38908080/206221931-5a1306fa-57af-4d45-9b1f-27f28001d441.gif" alt=""></p>
<p>기존 프로젝트 내에서 위와 같이 작성한 댓글이 있으면 해당 댓글을 삭제할 수 있는지 확인받도록 모달을 띄우는 로직을 정의할 필요가 있었다. 이를 위해 아래와 같은 <code>Comment</code> 컴포넌트를 정의하고 사용했다.</p>
<p> <img src="https://velog.velcdn.com/images/seungchan__y/post/37c19398-4776-46ae-8399-72f62f3de4d1/image.png" alt=""></p>
<p><code>Comment</code> 컴포넌트에서 모달 로직은 응집성이 떨어진 채로 선언되어있으며 더 큰 문제점은 <code>ConfirmModal</code>이라는 컴포넌트가 댓글의 개수만큼 조건부 렌더링 될 것이다. 화면 내에서 보이는 요소는 똑같은 모달 하나 뿐이며 쓰일지도 모를 모달을 위해 <strong>불필요하게 많은 모달 컴포넌트들이 렌더링 영역 어딘가를 차지하고 있다는</strong> 말이다.</p>
<p>이러한 문제점들이 생기는 이유는 <strong>본질적으로 모달이라는 요소가 다른 컴포넌트들과 불필요하게 결합되어 있기 때문이다.</strong> 이 모달들은 화면 내에서 어떠한 요소들 보다항상 최상단에서 렌더링 되고 있는 점에서 모달 컴포넌트 자체는 다른 컴포넌트들과 직접적으로 결합될 이유가 전혀 없다. </p>
<br/>

<h1 id="🪝-1차-리팩토링---모달을-전역으로-관리하기">🪝 1차 리팩토링 - 모달을 전역으로 관리하기</h1>
<p>모달과 컴포넌트가 결합되는 문제를 해결하기 위해 모달을 전역에서 관리하도록 바꿔줄 필요가 있다. 방법은 간단하다.</p>
<ul>
<li><p>전역에서 모달의 열림/닫힘 상태값를 소유하고 이 값에 따라 모달 컴포넌트를 렌더링 한다.</p>
</li>
<li><p>모달을 띄우는 로직이 필요한 컴포넌트에서 전역단에 선언한 모달 열림/닫힘 상태값을 변경할 수 있도록 한다.</p>
</li>
</ul>
<p>이를 위해 우선 아래와 같이 모달의 상태를 전역으로 사용하고 관리할 수 있는 커스텀 훅을 선언하자.</p>
<pre><code class="language-tsx">import { atom, useRecoilState } from &#39;recoil&#39;;

const modalOpenAtom = atom({
  key: &#39;modalOpenAtom&#39;,
  default: false,
});

const useModals = () =&gt; {
  const [modalOpen, setModalOpen] = useRecoilState(modalOpenAtom);

  const openModal = useCallback(() =&gt; {
      setModalOpen(true);
  }, [setModalOpen]);

  const closeModal = useCallback(() =&gt; {
      setModalOpen(false);
  }, [setModalOpen]);

  return {
    modalOpen,
    openModal,
    closeModal,
  };
};

export default useModals;</code></pre>
<blockquote>
<p>위에서는 기존의 프로젝트에 전역 클라이언트 상태관리를 모두 <code>Recoil</code>로 처리했기에 이를 사용했으나 <code>Context</code>로 충분히 대체할 수 있다.</p>
</blockquote>
<blockquote>
<p><code>useModals</code> 훅은 여러 군데에서 사용되어지기 때문에 이 훅을 사용하는 곳에서 리렌더링이 발생하는 것을 방지하기 위해 훅 내 함수들은 <code>useCallback</code>을 이용해 메모이제이션 해주었다.</p>
</blockquote>
<p>이러한 훅을 사용해 아래처럼 페이지 최상단에서 모달을 조건부 렌더링 시킬 수 있다.</p>
<pre><code class="language-tsx">
// 최상단 컴포넌트
const App = () =&gt; {
      ...
      return (
      &lt;&gt;
           &lt;Modals /&gt;
        &lt;Component {...pageProps} /&gt;
      &lt;/&gt;
    );
}


const Modals = () =&gt; {
     const { modalOpen, closeModal } = useModals();

      // 모달이 열려서 확인버튼을 눌렀을때 처리하는 로직
      const handleClickConfirmButton = () =&gt; {
        비즈니스_로직();
          closeModal();
    }

      return (
      &lt;&gt;
              &lt;ConfirmModal 
              open={openModal} 
              onConfirmButtonClick={handleClickConfirmButton} 
              message=&quot;...&quot; 
              onClose={closeModal} 
            /&gt;
         }    
      &lt;/&gt;
    );

}</code></pre>
<p>그리고 이를 사용하는 측에선 다음과 같이 훅을 통해 모달을 열도록 제어할 수 있다. 아래는 <code>ApplyButton</code> 에 적용한 예시이다. </p>
<pre><code class="language-tsx">const ApplyButton = () =&gt; {

  const { openModal } = useModals();

  return (
      &lt;Button onClick={openModal}&gt;
        참가하기
      &lt;/Button&gt;
  );
};</code></pre>
<p>이렇게 1차적인 리팩토링을 적용하여 컴포넌트 내의 모달 로직을 분리시켜보았다. 이렇게 <code>Modals</code> 라는 컴포넌트에 모달 렌더링 로직을 위임함으로써 모달이 필요한 컴포넌트에서 불필요한 코드를 포함하지 않도록 할 수 있었다.</p>
<h1 id="💣-잘못된-리팩토링">💣 잘못된 리팩토링</h1>
<p>이렇게 컴포넌트와 모달간의 불필요한 결합을 없애는데에는 성공했으나 다른 문제점들이 존재한다.</p>
<h2 id="1-컴포넌트와-비즈니스-로직의-분리">1. 컴포넌트와 비즈니스 로직의 분리</h2>
<pre><code class="language-tsx">
// 지원하기 버튼 컴포넌트

const ApplyButton = () =&gt; {
  const { openModal } = useModals();
  return (
      &lt;Button onClick={openModal}&gt;
        참가하기
      &lt;/Button&gt;
  );
};

// 모달을 관리하는 상위 컴포넌트

const Modals = () =&gt; {
     const { modalOpen, closeModal } = useModals();

      // 모달이 열려서 확인버튼을 눌렀을때 처리하는 로직
      const handleClickConfirmButton = () =&gt; {
        비즈니스_로직();
          closeModal();
    }

      return (
          &lt;&gt;
              &lt;ConfirmModal 
              open={openModal} 
              onConfirmButtonClick={handleClickConfirmButton} 
              message=&quot;참가 신청하시겠습니까?&quot; 
              onClose={closeModal} 
            /&gt;
          &lt;/&gt;
    );

}</code></pre>
<p><code>ApplyButton</code> 은 버튼 클릭시 모달을 띄우고 모달 내 확인 버튼을 눌렀을때 비즈니스 로직을 실행하도록 설계되어 있었다. 허나 리팩토링 이후 상황에서는<code>ApplyButton</code> 내에 존재해야될 비즈니스 로직이 <code>Modals</code> 라는 다른 컴포넌트에 존재해야 된다.</p>
<br/>

<h2 id="2-취약한-확장성">2. 취약한 확장성</h2>
<p>현재는 참가신청 버튼에 대한 모달만을 띄우도록 변경해놓았지만 다른 컴포넌트들에서 또다른 모달을 사용해야한다면 어떻게 해야할까? </p>
<pre><code class="language-tsx">
const ApplyButton = () =&gt; {
  const { openModal } = useApplyModal();
  return (
      &lt;Button onClick={openModal}&gt;
        참가하기
      &lt;/Button&gt;
  );
};

const DeleteButton = () =&gt; {
  const { openModal } = useDeleteModal();
  return (
      &lt;Button onClick={openModal}&gt;
        삭제하기
      &lt;/Button&gt;
  );
};

const Modals = () =&gt; {
      const { modalOpen: applyModalOpen, closeModal: closeApplyModal } = useApplyModal();
     const { modalOpen: deleteModalOpen, closeModal: closeDeleteModal } = useDeleteModal();
      ...

      // 모달이 열려서 확인버튼을 눌렀을때 처리하는 로직
      const handleClickApplyModalConfirm = () =&gt; {
        비즈니스_로직1();
          closeApplyModal();
    }

    const handleClickDeleteModalConfirm = () =&gt; {
         비즈니스_로직2(); 
        closeDeleteModal();
    }
    ...

      return (
          &lt;&gt;
              &lt;ConfirmModal 
              open={applyModalOpen} 
              onConfirmButtonClick={handleClickApplyModalConfirm} 
              message=&quot;참가 신청하시겠습니까?&quot; 
              onClose={closeApplyModal} 
            /&gt;
             &lt;ConfirmModal 
              open={deleteModalOpen} 
              onConfirmButtonClick={handleClickDeleteModalConfirm} 
              message=&quot;삭제 하시겠습니까?&quot; 
              onClose={closeDeleteModal} 
            /&gt;
            ...
          &lt;/&gt;
    );

}
</code></pre>
<ul>
<li><p>모달이 추가될때마다 해당 모달의 열림/닫힘 여부와 관련된 상태값을 생성해주어야 한다. -&gt; <strong>이로 인해 새로운 <code>atom</code>과 새로운 커스텀 훅이 매번 생성된다.</strong></p>
</li>
<li><p>모달에 필요한 <strong>비즈니스 로직을 매번 추가</strong>해주어야한다. 그것도 저 만치 멀리 있는 곳에..</p>
</li>
<li><p>모달이 추가될때마다 관련 컴포넌트를 추가 렌더링 시켜줘야 한다.</p>
</li>
</ul>
<p>모달과 관련된 코드는 요구사항이 늘어남에 따라 비대해지고 가독성이 떨어지게 된다. 이로 인해 추가적인 모달이 생겨야하는 추가적인 요구사항을 반영하기 점점 어려워진다.</p>
<br/>

<h1 id="🤔-어떻게-해결해-볼-수-있을까">🤔 어떻게 해결해 볼 수 있을까?</h1>
<p>위에서 서술한 문제점에 따른 요구사항들을 요약하면 아래와 같다.</p>
<ul>
<li><p>모달을 사용하는 컴포넌트와 비즈니스 로직간의 격리 -&gt; <strong>비즈니스 로직은 사용하는 컴포넌트 내에 있어야 한다.</strong></p>
</li>
<li><p>모달들의 확장성이 떨어진다 -&gt; <strong>확장성을 고려해야한다.</strong></p>
</li>
</ul>
<p>이를 충족할 수 있는 방법은 논리적으로는 간단하다. 바로 <strong>모달을 오픈할 때 모달에 대한 정보를 사용하는 컴포넌트에서 동적으로 제공</strong>하는 것이다. <code>ApplyButton</code> 컴포넌트를 예시로 들면</p>
<pre><code class="language-tsx">const ApplyButton = () =&gt; {

  const { openModal } = useModals();

  const handleClickButton = () =&gt; {
    openModal(
      {
        1. ConfirmModal을 열어라,
          2. Modal 내 메시지는 &quot;참가 신청하겠습니까?&quot;로 해라,
        3. 클릭시에는 비즈니스_로직을 실행시켜라,
        4. 닫힘 버튼 클릭시에는 모달을 닫아라
      }
    );
  }



  return (
      &lt;Button onClick={handleClickButton}&gt;
        참가하기
      &lt;/Button&gt;
  );
};
</code></pre>
<p>위와 같이 <code>openModal</code> 이라는 함수에 인자로 관련된 비즈니스 로직을 넘겨 확장성을 만족시킬 수 있을 것이다. 이를 좀 더 구체화해보면 아래와 같이 모달 컴포넌트 자체를 넘기는 식으로 처리할 수 있을 것이다. (아직은 실제로 동작할 수 없는 단계이다.)</p>
<pre><code class="language-tsx">const ApplyButton = () =&gt; {

  const { openModal } = useModals();

  const applyForRecruitment = () =&gt; {
       지원_비즈니스_로직(); 
  }

  const handleClickButton = () =&gt; {
    openModal(
        &lt;ConfirmModal
          message=&quot;참가 신청하겠습니까?&quot;
          onConfirmButtonClick={applyForRecruitment}
          onCancelButtonClick={모달 닫기}
          ...
        /&gt;
    );
  }



  return (
      &lt;Button onClick={handleClickButton}&gt;
        참가하기
      &lt;/Button&gt;
  );
};</code></pre>
<p>즉, <strong>모달과 관련된 로직을 사용하는 측에서 원하는대로 정함</strong>으로써 확장성을 고려해 볼 수 있다는 것이다. </p>
<p>그렇다면 사용처에서 모달의 정보들을 다 정해줬으니 <strong>전역에서 모달을 띄우는 곳에선 사용처가 정해준 모달을 렌더링</strong>만 시켜주면 되지 않을까?</p>
<pre><code class="language-tsx">const useModals = () =&gt; {
  // 모달들의 목록을 보관하는 상태값
  const [modals, setModals] = useRecoilState(modalOpenAtom);

  // 인자로 모달을 받으면 이를 모달 목록에 추가하기
  const openModal = useCallback((modal) =&gt; {
    setModals(modals =&gt; [...modals, modal]);
  }, [setModalOpen]);

  ...

  return {
    modals,
    openModal,
    closeModal,
  };
};


const Modals = () =&gt; {
     const { modals } = useModals();

      return (
      &lt;&gt;
        // 목록에 추가된 모달들을 렌더링시키기
        {modals.map((modal) =&gt; &lt;modal /&gt;}  
      &lt;/&gt;
    );

}

</code></pre>
<p>즉, 기존 처럼 모달의 열림/닫힘 상태를 보관하는 대신 <strong>필요한 모달 컴포넌트와 그에 대한 정보들을 전역 상태값에 담아두고 이를 렌더링 하는 방식으로 변경하는 것이다</strong>. 더 이상 모달을 관리하는 쪽에서는 모달별 비즈니스 로직을 신경 쓸 필요가 없어졌으며 모달이 여러개여도 코드의 가독성이 떨어지지 않도록 구성할 수 있을 것이다.</p>
<p>다시, 해결방법을 정리해보면 아래와 같다.</p>
<ul>
<li><strong>사용하는 컴포넌트에서 모달에 대한 정보를 지정해준다.</strong></li>
<li><strong>렌더링 할 모달과 그에 대한 정보들을 상태로 보관한다.</strong></li>
<li><strong>모달을 관리하는 측에선 전달받은 모달을 렌더링 시킨다.</strong></li>
</ul>
<br/>

<h1 id="🧼-2차-리팩토링---모달-컴포넌트들을-상태값으로-보관하기">🧼 2차 리팩토링 - 모달 컴포넌트들을 상태값으로 보관하기</h1>
<h2 id="1-모달-컴포넌트를-상태값으로-보관하기">1. 모달 컴포넌트를 상태값으로 보관하기</h2>
<p>우선 모달 컴포넌트들을 상태값으로 보관하도록 <code>useModals</code> 훅을 아래와 같이 바꿔보자.</p>
<pre><code class="language-tsx">import { atom, useRecoilState } from &#39;recoil&#39;;

const modalsAtom = atom({
  key: &#39;modalsAtom&#39;,
  default: [],
});

const useModals = () =&gt; {
  const [modals, setModals] = useRecoilState(modalsAtom);

  const openModal = useCallback((Component, props) =&gt; {
      setModals((modals) =&gt; [...modals, { Component, props: { ...props, open: true } }]);
    },
    [setModals]
  );

  const closeModal = useCallback((Component) =&gt; {
      setModals((modals) =&gt; modals.filter((modal) =&gt; modal.Component !== Component));
    },
    [setModals]
  );

  return {
    modals,
    openModal,
    closeModal,
  };
};
</code></pre>
<ul>
<li>이제 <code>modalsAtom</code>은 상태값에 모달 컴포넌트들을 배열형태로 보관하게 된다.</li>
<li><code>openModal</code> 에서는 컴포넌트(함수)와 그의 <code>props</code>를 인자로 받아 상태값에 추가시킨다.</li>
<li><code>closeModal</code> 에서는 컴포넌트(함수)를 인자로 받으면 이를 필터링 시켜 기존의 상태값에서 제거하는 역할을 한다.</li>
</ul>
<br/>

<h2 id="2-상태값으로-보관한-모달들-렌더링하기">2. 상태값으로 보관한 모달들 렌더링하기</h2>
<p>이제 전역에서 모달을 렌더링하는 <code>Modals</code> 컴포넌트를 아래와 같이 수정하자.</p>
<pre><code class="language-tsx">import ConfirmModal from &#39;@components/common/ConfirmModal&#39;;
import ParticipantsModal from &#39;@components/article/ParticipantModal&#39;;

// 사용할 모달 컴포넌트들을 담은 Object
export const modals = {
  confirm: ConfirmModal, 
  participants: ParticipantsModal,
};

const Modals = () =&gt; {
  const { modals } = useModals();

  return (
    &lt;&gt;
      {modals.map(({ Component, props }, idx) =&gt; {
        return &lt;Component key={idx} {...props} /&gt;;
      })}
    &lt;/&gt;
  );
};</code></pre>
<ul>
<li><p><code>modals</code> 객체 : 모달 컴포넌트들을 담고 있다. 이를 이용해 향후 사용처 컴포넌트에서 모달 컴포넌트들의 코드를 import 하지 않고도 모달의 종류를 선택할 수 있게 도와준다.</p>
</li>
<li><p><code>Modals</code> 컴포넌트 : <code>useModals</code> 훅을 통해 <code>modals</code>상태값을 받아와 이를 컴포넌트로 렌더링 시킨다.</p>
</li>
</ul>
<h2 id="3-모달-종류와-로직-넘겨주기">3. 모달 종류와 로직 넘겨주기</h2>
<p>사용처에서 모달 컴포넌트의 종류와 해당 컴포넌트의 props를 넘겨주는 부분이다. 아래는 <code>ApplyButton</code> 컴포넌트의 예시이다.</p>
<pre><code class="language-tsx">import { modals } from &#39;@components/common/Modals&#39;;

const ApplyButton = (...) =&gt; {

  const { openModal, closeModal } = useModals();

  return (
    &lt;Button
      onClick={() =&gt;
        openModal(modals.confirm, {
          message: &#39;참가 신청하시겠습니까?&#39;,
          onConfirmButtonClick: () =&gt; {
                지원_비즈니스_로직();
                closeModal(modals.confirm);
          },
          onCancelButtonClick: () =&gt; closeModal(modals.confirm),
        })
      }
    &gt;
      참가하기
    &lt;/Button&gt;
  );
};
</code></pre>
<ul>
<li>이전에 정의한 <code>modals</code>를 import 하여 필요한 모달이 무엇인지 지정한다.</li>
<li><code>openModal</code> 함수의 인자로 모달 컴포넌트와 그의 props 들을 넘기는 방식이다.</li>
</ul>
<p>이러한 방식을 통해 모달 로직들의 응집성을 높여 가독성을 높이고 추가적인 모달이 생기더라도 높은 확장성을 가져갈 수 있게 하였다.</p>
<h1 id="✅-부록1---타입스크립트">✅ 부록1 - 타입스크립트</h1>
<pre><code class="language-tsx">    &lt;Button
      onClick={() =&gt;
        openModal(modals.confirm, {
          message: &#39;참가 신청하시겠습니까?&#39;,
          onConfirmButtonClick: () =&gt; {
                지원_비즈니스_로직();
                closeModal(modals.confirm);
          },
          onCancelButtonClick: () =&gt; closeModal(modals.confirm),
           hello: &#39;world&#39;,
          asdadasd: &#39;asdasdad&#39;,
        })
      }
    &gt;
      참가하기
    &lt;/Button&gt;
</code></pre>
<p>위 예시에서 <code>ConfirmModal</code>에 들어갈 props가 아닌 다른 props를 넣어주면 어떻게 될까? props 부분은 단순 Object로 인식 되기에 다른 이상한 값을 넣어줘도 그대로 통과된다. 이를 방지하기 위해선 아래와 같이 타입 지정을 추가적으로 해줘야한다.</p>
<pre><code class="language-tsx">// modalsAtom
import { ComponentProps, FunctionComponent } from &#39;react&#39;;

const modalsAtom = atom&lt;Array&lt;{ Component: FunctionComponent&lt;any&gt;; props: ComponentProps&lt;FunctionComponent&lt;any&gt;&gt; }&gt;&gt;({
  key: `modalsAtom/${uuid()}`,
  default: [],
}); 


// useModals 훅

import { ComponentProps, FunctionComponent, useCallback } from &#39;react&#39;;

const useModals = () =&gt; {
  ...

  const openModal = useCallback(
    &lt;T extends FunctionComponent&lt;any&gt;&gt;(Component: T, props: Omit&lt;ComponentProps&lt;T&gt;, &#39;open&#39;&gt;) =&gt; {
      setModals((modals) =&gt; [...modals, { Component, props: { ...props, open: true } }]);
    },
    [setModals]
  );

  const closeModal = useCallback(
    &lt;T extends FunctionComponent&lt;any&gt;&gt;(Component: T) =&gt; {
      setModals((modals) =&gt; modals.filter((modal) =&gt; modal.Component !== Component));
    },
    [setModals]
  );

  ...
};

// Modals
import { ComponentProps, FunctionComponent } from &#39;react&#39;;

import ConfirmModal from &#39;@components/common/ConfirmModal&#39;;
import ParticipantsModal from &#39;@components/article/ParticipantModal&#39;;

export const modals = {
  confirm: ConfirmModal as FunctionComponent&lt;ComponentProps&lt;typeof ConfirmModal&gt;&gt;,
  participants: ParticipantsModal as FunctionComponent&lt;ComponentProps&lt;typeof ParticipantsModal&gt;&gt;,
};
</code></pre>
<ul>
<li><p><code>modalsAtom</code> 에서는 컴포넌트와 props담은 객체들의 배열의 타입을 정의해주었다.</p>
</li>
<li><p><code>openModal</code>과 <code>closeModal</code> 에서는 제너릭을 통해 인자로 받은 컴포넌트로 부터 타입을 유추시킨다. 그래서 <code>openModal</code>의 경우에는 아래와 같이 받는 인자를 해당 컴포넌트의 props에 해당하는 것으로만 제한시킬 수 있다. 
<img src="https://velog.velcdn.com/images/seungchan__y/post/c81f9639-b36b-407b-9f19-1da95344c910/image.gif" alt=""></p>
</li>
</ul>
<ul>
<li><code>modals</code> 객체는 모달 컴포넌트들을 담고있다. 여기서는 타입 호환을 위해 <code>FunctionComponent</code> 를 이용해 타입 단언을 시켰다. 타입 단언을 하지 않을 경우 <code>(props: Props) =&gt; JSX.Element</code>로 타입이 추론되어 타입을 맞추기 불가능했기 때문에 사용했다.</li>
</ul>
<p>다만, 여러 컴포넌트에서 호환성을 맞추기 위해 <code>any</code>를 불가피하게 사용하게 되었다 🥲. 최대한 <code>unknown</code>을 쓰고 싶었지만 도저히 호환되는 타입을 정의할 수 없었다. <del>타입스크립트는 너무 어렵다.</del> </p>
<h1 id="🖖-부록2---코드-스플리팅">🖖 부록2 - 코드 스플리팅</h1>
<p>모달들을 담고있는 <code>modals</code> 쪽 코드를 다시 살펴보자.</p>
<pre><code class="language-tsx">import { ComponentProps, FunctionComponent } from &#39;react&#39;;

import ConfirmModal from &#39;@components/common/ConfirmModal&#39;;
import ParticipantsModal from &#39;@components/article/ParticipantModal&#39;;

export const modals = {
  confirm: ConfirmModal as FunctionComponent&lt;ComponentProps&lt;typeof ConfirmModal&gt;&gt;,
  participants: ParticipantsModal as FunctionComponent&lt;ComponentProps&lt;typeof ParticipantsModal&gt;&gt;,
};
</code></pre>
<p> 모달들을 import 해서 이들을 객체에 담은 뒤 이 객체를 export 시키고 이를 이용해 컴포넌트에서 사용하도록 하고 있다. 그런데 만약 모달의 종류들이 점점 더 많아지면 어떻게 될까?</p>
<pre><code class="language-tsx">import { ComponentProps, FunctionComponent } from &#39;react&#39;;

import ConfirmModal from &#39;@components/common/ConfirmModal&#39;;
import ParticipantsModal from &#39;@components/article/ParticipantModal&#39;;
import Modal1 from &#39;...&#39;;
import Modal2 from &#39;...&#39;;
import Modal3 from &#39;...&#39;;
...

export const modals = {
  confirm: ConfirmModal as FunctionComponent&lt;ComponentProps&lt;typeof ConfirmModal&gt;&gt;,
  participants: ParticipantsModal as FunctionComponent&lt;ComponentProps&lt;typeof ParticipantsModal&gt;&gt;,
  modal1: Modal1,
  modal2: Modal2,
  modal3: Modal3,
  ...
};
</code></pre>
<p>모달이 늘어나더라도 모달의 개수만 많아질 뿐 코드상으로는 문제가 없으나 문제는 <code>modals</code> 객체를 컴포넌트 단에서 사용하기 위해서 저 수많은 모달들을 모두 import 시켜야 한다는 것이다. 이렇게 되면 <strong>모달이 늘어남에따라</strong> 번들링되는 자바스크립트가 커지면서 <strong>컴포넌트 로딩 성능에도 영향을 미칠 수 있게 되는 것</strong>이다. </p>
<pre><code class="language-tsx">import loadable from &#39;@loadable/component&#39;;

const ConfirmModal = loadable(() =&gt; import(&#39;@components/common/ConfirmModal&#39;), { ssr: false });
const ParticipantsModal = loadable(() =&gt; import(&#39;@components/article/ParticipantsModal&#39;), {
  ssr: false,
});

export const modals = {
  confirm: ConfirmModal as FunctionComponent&lt;ComponentProps&lt;typeof ConfirmModal&gt;&gt;,
  participants: ParticipantsModal as FunctionComponent&lt;ComponentProps&lt;typeof ParticipantsModal&gt;&gt;,
};</code></pre>
<p>이를 방지하고자 모달 컴포넌트들에 대해서 코드스플리팅을 적용해볼 수 있다. 코드 스플리팅을 적용하면 해당 <strong>모달 컴포넌트가 렌더링 될때만 관련 코드를 로드하도록</strong> 하여 <strong>모달로 인한 리소스 로딩 성능 저하를 예방</strong>할 수 있다.  여기선 <code>loadable</code> 이라는 라이브러리를 사용했으며 기존 프로젝트는 <code>Next.js</code> 기반으로 이루어져 있기 때문에 <code>ssr</code> 옵션을 <code>false</code> 값으로 주도록 했다.</p>
<h1 id="😳-리팩토링-적용">😳 리팩토링 적용</h1>
<p>실제 리팩토링이 적용된 코드들은 <a href="https://github.com/boostcampwm-2022/web13-moyeomoyeo/pull/348">여기서</a> 살펴볼 수 있다.</p>
<h1 id="⛳️-출처-및-참고자료">⛳️ 출처 및 참고자료</h1>
<ul>
<li><p><a href="https://youtu.be/edWbHp_k_9Y">SLASH21 - 실무에서 바로 쓰는 Frontend Clean Code</a></p>
</li>
<li><p><a href="https://nakta.dev/how-to-manage-modals-1">효율적인 modal 관리 with React</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSR 환경의 다크모드 깜빡임 현상 해결하기]]></title>
            <link>https://velog.io/@seungchan__y/SSR-%ED%99%98%EA%B2%BD%EC%9D%98-%EB%8B%A4%ED%81%AC%EB%AA%A8%EB%93%9C-%EA%B9%9C%EB%B9%A1%EC%9E%84-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/SSR-%ED%99%98%EA%B2%BD%EC%9D%98-%EB%8B%A4%ED%81%AC%EB%AA%A8%EB%93%9C-%EA%B9%9C%EB%B9%A1%EC%9E%84-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 06 Dec 2022 13:53:30 GMT</pubDate>
            <description><![CDATA[<h1 id="😱-사건의-발단">😱 사건의 발단</h1>
<p><code>NextJS</code>와 <code>Emotion</code>을 기반으로 진행한 프로젝트에서 다크모드를 도입하여 사이트를 구성해보았다. 그런데 웬걸 새로고침이나 페이지 진입 할 때 카메라 플래시마냥 깜빡임 현상이 발생하고 말았다 😇</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/dbf27c65-588f-4dec-8ba8-c4cbd1b03984/image.gif" alt=""></p>
<p>이번 포스트에서는 이 깜빡임이 무슨 원인에서 비롯되었는지 분석해보고 어떤 식으로 해결할 수 있는지 공유해볼 것이다.</p>
<p><br></br></p>
<h1 id="👀-다크모드-짚어보기">👀 다크모드 짚어보기</h1>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/2745158b-a58c-4e26-8f86-e1df91682b68/image.png" alt=""></p>
<p>이번 프로젝트에서 구현해본 다크모드는 다음의 기능들을 만족해야한다.</p>
<ol>
<li><p>이미 진입했던 사용자가 로컬스토리지를 통해 저장해두었던 테마값이 있을 경우 이에 따라 렌더링을 진행한다.</p>
</li>
<li><p>그렇지 않고 디바이스에서 지정한 테마(<code>prefers-color-scheme</code>)값이 존재하는 경우 이에 맞는 화면을 렌더링한다.</p>
</li>
<li><p>그마저도 아니라면 기본 색상인 다크 테마를 띄운다.</p>
</li>
</ol>
<p>이제 이 요구사항을 구현해보자!</p>
<h1 id="🌚-다크모드-구현하기">🌚 다크모드 구현하기</h1>
<p>다음과 테마별로 사용할 스타일 속성을 지정하였다.</p>
<pre><code class="language-tsx">import { Theme } from &#39;@emotion/react&#39;;

const lightTheme: Theme = {
  mode: &#39;light&#39;,
  text: &#39;black&#39;,
  background: &#39;#fafafa&#39;,
  borderColor: &#39;#eaeaea&#39;,
  bodyColor: &#39;white&#39;,
};

const darkTheme: Theme = {
  mode: &#39;dark&#39;,
  text: &#39;white&#39;,
  background: &#39;#111&#39;,
  borderColor: &#39;#222&#39;,
  bodyColor: &#39;black&#39;,
};

export { lightTheme, darkTheme };
</code></pre>
<p>그리고 라이트/다크 모드 여부를 확인할 수 있는 ColorModeContext 를 아래와 같이 구성한다.</p>
<pre><code class="language-tsx">import { ReactNode, createContext, useEffect, useRef, useContext, useState } from &#39;react&#39;;
import useMediaQuery from &#39;../hooks/useMediaQuery&#39;;

interface ColorModeContextValue {
  colorMode: string | null;
  setColorMode: (value: string) =&gt; void;
}

const ColorModeContext = createContext&lt;ColorModeContextValue | null&gt;(null);

const ColorModeProvider = ({ children }: { children: ReactNode }) =&gt; {
  const [colorMode, setRawColorMode] = useState(&#39;dark&#39;); // 기본 테마는 다크로 간주
  const systemPrefers = useMediaQuery(&#39;(prefers-color-scheme: dark)&#39;);
  const firstRender = useRef(true);

  const setColorMode = (value: string) =&gt; {
    setRawColorMode(value);
    window.localStorage.setItem(&#39;color-mode&#39;, value);
  };

  useEffect(() =&gt; {
    // 첫번째 렌더링 시에만 실행
    if (firstRender.current) {
      // osTheme는 운영체제 지정 테마
      const osTheme = systemPrefers ? &#39;dark&#39; : &#39;light&#39;;
      // 유저가 선택한 테마(로컬스토리지에 저장된 값을 추출)
      const userTheme = window.localStorage.getItem(&#39;color-mode&#39;);
      // userTheme를 우선으로 하고 없다면 osTheme로 지정
      const theme = userTheme || osTheme;
      setRawColorMode(theme);
      firstRender.current = false;
    } else {
      // 마운트 이후에는 바뀌는 사용자 선호도에 테마 변화를 대응
      setRawColorMode(systemPrefers ? &#39;dark&#39; : &#39;light&#39;);
    }
  }, [systemPrefers]);

  return (
    &lt;ColorModeContext.Provider value={{ colorMode, setColorMode }}&gt;
      {children}
    &lt;/ColorModeContext.Provider&gt;
  );
};

const useColorModeContext = () =&gt; {
  const context = useContext(ColorModeContext);
  if (context === null) {
    throw new Error(&#39;useColorModeContext must be used within a ThemeProvider&#39;);
  }
  return context;
};

export { ColorModeProvider, useColorModeContext };
</code></pre>
<p>그 다음에는 ColorModeContext 로부터 변화하는 colorMode에 따라 알맞은 테마를 주도록 다음과 같이 ThemeProvider를 구성한다</p>
<pre><code class="language-tsx">import { ReactNode } from &#39;react&#39;;
import { lightTheme, darkTheme } from &#39;../styles/theme&#39;;
import { ThemeProvider as EmotionThemeProvider } from &#39;@emotion/react&#39;;
import { useColorModeContext } from &#39;./ColorModeContext&#39;;

const ThemeProvider = ({ children }: { children: ReactNode }) =&gt; {
  const { colorMode } = useColorModeContext();
  return (
    &lt;EmotionThemeProvider theme={colorMode === &#39;light&#39; ? lightTheme : darkTheme}&gt;
      {children}
    &lt;/EmotionThemeProvider&gt;
  );
};

export default ThemeProvider;</code></pre>
<p>마지막으로 _app.tsx를 다음과 같이 구성한다.</p>
<pre><code class="language-tsx">import type { AppProps } from &#39;next/app&#39;;
import ThemeProvider from &#39;../components/ThemeProvider&#39;;
import { ColorModeProvider } from &#39;../components/ColorModeContext&#39;;
import GlobalStyles from &#39;../styles/GlobalStyles&#39;;

export default function App({ Component, pageProps }: AppProps) {
  return (
    &lt;ColorModeProvider&gt;
      &lt;ThemeProvider&gt;
        &lt;GlobalStyles /&gt;
        &lt;Component {...pageProps} /&gt;
      &lt;/ThemeProvider&gt;
    &lt;/ColorModeProvider&gt;
  );
}
</code></pre>
<p>참고로 이렇게 했을때 <code>development</code> 모드에서는 잘 안될 수가 있다.이는 <code>reactStrictMode</code>가 켜져있기 때문이다. 아래와 같이 <code>next.config.js</code>를 수정해주면 된다.</p>
<pre><code class="language-js">/** @type {import(&#39;next&#39;).NextConfig} */
const nextConfig = {
  // reactStrictMode: true, // 이 부분을 지워버리자!
  swcMinify: true,
};

module.exports = nextConfig;
</code></pre>
<pre><code>💡 reactStrictMode는 development 모드에서 버그를 찾는데 도움을 주는 요소이기 때문에 다크모드 구현이 끝나면 되돌리는 것을 추천드립니다.</code></pre><p>관련된 자세한 코드는 <a href="https://github.com/Yangseungchan/Darkmode-practice/tree/first-dark-mode">여기서</a> 확인할 수 있다. </p>
<p>일차적으로 구현했을때의 모습은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/c46cb05e-070b-4c63-be47-112be7661c73/image.gif" alt=""></p>
<p>버튼을 누르면서 테마를 전환할 수 있고 시스템 선호도를 변경하면 이에 따라 반응하는 것을 확인할 수 있다. 또한 새로고침을 하더라도 시스템 선호도가 아니라 사용자가 지정한 값으로 초기화되는 것을 확인할 수 있다.</p>
<h1 id="🔦-문제와-원인-분석">🔦 문제와 원인 분석</h1>
<p>잘 되는 것 같지만 새로고침을 하게되면 깜빡임 현상이 발생한다. 순간 검은색으로 변하는 현상이 발생하는 것이 보일 것이다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/d4618b37-eda1-4906-89b4-752d8f029f05/image.gif" alt=""></p>
<p>이러한 문제가 발생하는 것은 초기에 렌더링한 색상과 차후에 구현한 다크모드 기능끼리 충돌이 발생하기 때문이다.</p>
<pre><code class="language-tsx">const ColorModeProvider = ({ children }: { children: ReactNode }) =&gt; {
  const [colorMode, setRawColorMode] = useState(&#39;dark&#39;); // 초기 테마값
  const systemPrefers = useMediaQuery(&#39;(prefers-color-scheme: dark)&#39;);
  const firstRender = useRef(true);

  ...

  useEffect(() =&gt; {
    // 마운트 이후 조정되는 테마값
    if (firstRender.current) {
      const osTheme = systemPrefers ? &#39;dark&#39; : &#39;light&#39;;
      const userTheme = window.localStorage.getItem(&#39;color-mode&#39;);
      const theme = userTheme || osTheme;
      setRawColorMode(theme);
      firstRender.current = false;
    } else{
      ...
    }
  }, [systemPrefers]);

  return (
    &lt;ColorModeContext.Provider value={{ colorMode, setColorMode }}&gt;
      {children}
    &lt;/ColorModeContext.Provider&gt;
  );
};</code></pre>
<p>서버사이드 렌더링이 이뤄질 당시에는 브라우저 API를 참조할 수 없기 때문에 임의로 테마값을 렌더링 할 수 밖에없다. 그러면 기본값인 <strong>다크모드</strong>의 페이지가 나오게 될것이다. 그러다가 마운트 되고 나서는 로컬스토리지와 시스템선호도값을 파악하게 되고 이때 <strong>라이트모드</strong>의 페이지로 바꿔줘야한다. 이 때문에 깜빡임이 발생하게 되는 것이다.</p>
<p><br></br></p>
<h1 id="🤔-해결책을-고민해보자">🤔 해결책을 고민해보자</h1>
<h2 id="1-서버가-브라우저-없이도-미리-사용자의-테마를-알고-있기">1. 서버가 브라우저 없이도 미리 사용자의 테마를 알고 있기?</h2>
<p>이런 기능을 구현하기 위해서는 사용자별로 테마를 데이터베이스에 들고 있어야 하고 테마 전환시 API 통신을 해야하는 등 배보다 배꼽이 큰 상황이 발생하게 될 것이다. 또한 서비스에 처음으로 진입하는 사용자의 테마는 알길이 없으므로 이는 해결책으로 가져가기 어려운 방법이다. 따라서 클라이언트 단에서 해결하는 방법이 고안되어야 한다.</p>
<h2 id="2-클라이언트-단에서-돔요소를-렌더링하기-전에-테마를-먼저-파악한다면">2. 클라이언트 단에서 돔요소를 렌더링하기 전에 테마를 먼저 파악한다면?</h2>
<p>돔요소들을 띄우기 전에 테마가 무엇인지 파악하여 다크모드인지 라이트 모드인지를 알맞게 설정 한다면 정확한 페이지를 렌더링 할 수 있을 것 같다!</p>
<p><br></br></p>
<h1 id="🛼-짚고가야할-사전-지식들">🛼 짚고가야할 사전 지식들</h1>
<p>해결책을 설명하기에 앞서 짚고가야할 지식들을 알아보자</p>
<h2 id="렌더링의-프로세스">렌더링의 프로세스</h2>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/cec81842-1ed4-41dd-af8f-832b8e93c965/image.png" alt=""></p>
<p>기본적으로 웹 페이지를 렌더링하는 과정에는 크게 6가지의 과정들이 있다. 여기서 렌더링이란 화면요소를 그리는 과정을 의미한다.</p>
<h3 id="1-html-파싱">1. HTML 파싱</h3>
<p>HTML 파일을 읽어들이면서 마크업 단위로 쪼개는 과정이다. <code>div</code>, <code>img</code>, <code>h1</code> 등 여러가지 태그들을 읽어들이며 의미있는 단위로 만들어낸다. 파싱순서는 위에서 아래로 그리고 우에서 좌측으로 진행된다. 이때 유념해야 될 것은 <code>script</code> 태그 같은 경우 따로 속성을 지정하지 않으면(<code>defer</code> 등) <code>script</code> 해석이 끝날때까지 HTML 파싱이 블로킹 된다는 점이다. </p>
<h3 id="2-css-파싱">2. CSS 파싱</h3>
<p>CSS 파일을 읽어들이는 과정이다. HTML과 비슷하게 읽어들이는 과정이다.</p>
<h3 id="3-dom-cssom-트리-생성">3. DOM, CSSOM 트리 생성</h3>
<p>앞서 파싱된 HTML과 CSS를 바탕으로 DOM 트리, CSSOM 트리를 만들어낸다. 어떤 요소들과 부모 자식 관계를 가져내는지를 트리 구조로 나타내는 과정이다.</p>
<h3 id="4-렌더-트리-생성">4. 렌더 트리 생성</h3>
<p>DOM, CSSOM 트리가 만들어지고나서 각각 특정 DOM이 어느 CSSOM과 결합되는지를 나타내는 과정이다. 이때까지도 실제 화면이 그려지는게 아니라 객체의 형태로만 나타난다.</p>
<h3 id="5-레이아웃">5. 레이아웃</h3>
<p>형성된 렌터 트리 내 요소들이 어느 위치에 그려지는지를 연산하는 과정이다. 예를 들어 <code>div</code> 는 x좌표가 20이고 y좌표가 50 이어야 겠다라는 것을 계산하는 과정이다. 이 레이아웃 과정은 다른 요소들에게도 영향을 주는 과정이라 비용이 굉장히 비싸다.</p>
<h3 id="6-페인트">6. 페인트</h3>
<p>레이아웃으로 구성된 요소들을 실제 화면에 그려내는 과정이다. 앞써 레이아웃이 비싼 과정이었다면 페인트는 분석된 값들을 화면에 그려내기만 하면 되기 때문에 비용이 비싼 과정은 아니다.</p>
<p>여기서 집중해서봐야될 과정은 1, 2, 3번이다. DOM 트리와 CSSOM 트리가 생성되면서 어떤 요소들이 어떤 스타일로 띄워질지 결정되는 과정들이기 때문이다. </p>
<p>그렇다면 우리의 문제와 결합시켜서 생각해보았을때 이 <strong>DOM트리가 생성되기 직전에 어떤 테마로 화면을 그려낼지 결정을 하면 되지 않을까?</strong> </p>
<h2 id="html-blocking">HTML Blocking</h2>
<p>앞서 렌더링의 1번 과정을 설명할때 <code>script</code> 태그가 해석되는 동안에는 HTML 파싱이 중단된다고 하였다. 이러한 현상을 <code>HTML Blocking</code> 이라고 한다.</p>
<pre><code class="language-tsx">&lt;body&gt;
  &lt;script&gt;
    alert(&#39;다크모드 개선하자!&#39;);
  &lt;/script&gt;
  &lt;div&gt;
    ...
  &lt;/div&gt;
&lt;/body&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/27186715-2cff-4c57-9f9d-a9fa5d5ff31a/image.gif" alt=""></p>
<p>위 코드의 실행결과인 위의 사진을 보면 확인해볼 수 있듯이 <code>script</code> 태그 내부의 <code>alert</code> 함수 호출로인하여 <code>h1</code> 태그가 렌더링 되지 않고 막혀버리는 모습이다. 이 원리를 활용해본다면 <strong>DOM트리가 생성되기 직전에 어떤 테마로 화면을 그려낼지 결정</strong> 하는 과정을 잘 처리해볼 수 있을 것 같다.</p>
<pre><code class="language-html">&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;Dark Mode&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;script&gt;
      /*
        - 로컬 스토리지를 확인해본다.
        - prefers-color-scheme라는 미디어 쿼리 속성을 확인해본다.
        - 어떤 테마로 띄울지를 결정한다.
      /*
    &lt;/script&gt;
    &lt;div&gt;
         ...
    &lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>위와 같이 HTML을 구성해본다면 어떤 테마로 띄워볼지 결정해보는 과정을 돔요소를 띄우기 전에 할 수 있을 것이다.</p>
<h2 id="css-변수">CSS 변수</h2>
<p>기존에 Emotion으로 라이트/다크모드에 맞는 테마를 렌더링하는 로직은 아래와 같다.</p>
<pre><code class="language-tsx">const lightTheme: Theme = {
  mode: &#39;light&#39;,
  text: &#39;black&#39;,
  background: &#39;#fafafa&#39;,
  borderColor: &#39;#eaeaea&#39;,
  bodyColor: &#39;white&#39;,
};

const darkTheme: Theme = {
  mode: &#39;dark&#39;,
  text: &#39;white&#39;,
  background: &#39;#111&#39;,
  borderColor: &#39;#222&#39;,
  bodyColor: &#39;black&#39;,
};

const ThemeProvider = ({ children }: { children: ReactNode }) =&gt; {
  const { colorMode } = useColorModeContext();
  return (
    &lt;EmotionThemeProvider theme={colorMode === &#39;light&#39; ? lightTheme : darkTheme}&gt;
      {children}
    &lt;/EmotionThemeProvider&gt;
  );
};
</code></pre>
<p>그리고 이런 테마값들을 이용해 컴포넌트들을 스타일링하면 다음과 같이 할 수 있다.</p>
<pre><code class="language-tsx">const Button = styled.button`
  background: ${({ theme }) =&gt; theme.background};
  color: ${({ theme }) =&gt; theme.text};
  padding: 1rem;
  border-radius: 4px;
`;
</code></pre>
<p>이 로직이 문제가 되는 점은 바로 <code>useColorModeContext</code> 에 의존적이라는 것이다. 즉, 마운트가 되면서 바뀔 수 있는 값 <code>colorMode</code> 에 의해 스타일 속성들이 좌지우지 되버리고 이로 인해 깜빡임 현상이 발생하게 된다.</p>
<p>그러므로 돔요소들이 파싱되기 전에 스타일링을 할 수 있도록 해야한다. 한편, 그 CSS 값은 테마가 바뀜에 따라 동일하게 변화할 수 있어야한다. 이때 사용해 볼 수 있는 것이 <strong>css 변수</strong>이다! CSS 변수는 선언해놓으면 <code>body</code> 태그보다 상단에 위치하게 되며, 그 값을 바꾸면 바로 렌더링에 반영된다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/c2413b0d-8609-4417-9a89-1b112e0a30e7/image.gif" alt=""></p>
<p>CSS가 아니라 어색할 수는 있지만 Emotion과 같은 CSS-in-JS 방식에서도 CSS 변수를 손쉽게 사용할 수 있다.</p>
<pre><code class="language-tsx">const Button = styled.button`
  background: var(--background-color);
  color: var(--text-color);
  padding: 1rem;
  border-radius: 4px;
`;</code></pre>
<p>따라서 우리가 적용해볼 수 있는 해결책은</p>
<p><strong>1. CSS 변수를 이용해 테마별로 다른 속성들을 스타일링한다.</strong></p>
<p><strong>2. 돔요소가 렌더링 되기 전에 테마를 파악하고 그 테마에 맞는 CSS 값을 설정할 수 있도록 스크립트를 적절히 삽입한다.</strong></p>
<p>로 종합해 볼 수 있다.</p>
<p><br><br/></p>
<h1 id="🤛-해결해보기">🤛 해결해보기!</h1>
<h2 id="스크립트-삽입">스크립트 삽입</h2>
<p>위에서 살펴봤던 사전 지식들을 바탕으로 깜빡임 현상을 해결할 차례가 되었다. 우선 스크립트를 삽입해보는 방법에 대해서 알아보자.</p>
<p>Next.js에서 모든 돔요소보다 상단에 스크립트가 존재하기 위해서는 <code>_document.tsx</code> 라는 파일을 만들어주어야 한다. 공식문서에서는 다음과 같이 만들어야한다는 가이드라인을 제공해주고 있다.</p>
<pre><code class="language-tsx">import { Html, Head, Main, NextScript } from &#39;next/document&#39;

export default function Document() {
  return (
    &lt;Html&gt;
      &lt;Head /&gt;
      &lt;body&gt;
        &lt;Main /&gt;
        &lt;NextScript /&gt;
      &lt;/body&gt;
    &lt;/Html&gt;
  )
}</code></pre>
<p>여기서 <code>_documents.tsx</code> 는 next.js 내 모든 페이지에서 전역적으로 사용되는 HTML 역할이라고 생각하면 된다. 보통은 lang 속성을 지정하는 용도로 사용을 하곤 하는데 우리는 스타일 속성을 지정할 수 있는 스크립트를 넣어줄 것이다.</p>
<pre><code class="language-tsx">const ScriptTag = () =&gt; {
  const codeToRunOnClient = `(function() {
  alert(&quot;다크모드 개선하자!&quot;);
})()`;

  return &lt;script dangerouslySetInnerHTML={{ __html: codeToRunOnClient }} /&gt;;
};

export default function Document() {
  return (
    &lt;Html&gt;
      &lt;Head /&gt;
      &lt;body&gt;
        &lt;ScriptTag /&gt;
        &lt;Main /&gt;
        &lt;NextScript /&gt;
      &lt;/body&gt;
    &lt;/Html&gt;
  );
}</code></pre>
<p>형태는 위와 같으며 클라이언트 단에서 실행될 코드를 문자열화 한뒤 이를 script 태그의 <code>dangerouslySetInnerHTML</code> 속성에 위치시켜준다. 이렇게해두면 다음과 같이 돔 요소가 렌더링 되기 전에 <code>alert</code>가 먼저 실행되는 것을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/ea56029d-09de-4de2-83fe-6fecf32d6fca/image.gif" alt=""></p>
<p>참고로 리액트 내부적으로 스크립트를 삽입하여 사용할 때는 다음과 같이 dangerouslySetInnerHTML 속성을 이용해주는 것이 좋은데 이는 리<a href="https://ko.reactjs.org/docs/dom-elements.html#:~:text=dangerouslySetInnerHTML,%EC%88%98%20%EC%9E%88%EA%B8%B0%20%EB%95%8C%EB%AC%B8%EC%97%90%20%EC%9C%84%ED%97%98%ED%95%A9%EB%8B%88%EB%8B%A4">액트에서 XSS 공격을 방지하기 위한 정책을 우회</a>하기 위함이다.</p>
<h2 id="필요한-스크립트-작성하기">필요한 스크립트 작성하기</h2>
<p>스크립트 내부에서 실행되어야할 내용은 다음과 같다.</p>
<ul>
<li><p>로컬스토리지와 미디어 쿼리의 <code>prefers-color-scheme</code> 속성을 확인해 렌더링할 컬러모드를 파악한다.</p>
</li>
<li><p>현재 어떤 컬러모드인지를 style 프로퍼티의 <code>--initial-color-code</code> 라는 이름으로 값을 넣는다.</p>
</li>
<li><p>컬러모드 별 필요한 스타일링 속성에 따라 CSS 변수값을 설정한다</p>
</li>
</ul>
<p>이러한 요구사항을 만족할 수 있게 스크립트와 관련 함수를 작성해보자!</p>
<pre><code class="language-tsx">const COLOR_MODE_KEY = &#39;color-mode&#39;;
const INITIAL_COLOR_MODE_CSS_PROP = &#39;--initial-color-mode&#39;;

function setColorsByTheme() {
  const modeProperties = &#39;[modeProperties]&#39;;
  const colorModeKey = &#39;[colorModeKey]&#39;;
  const colorModeCssProp = &#39;[colorModeCssProp]&#39;;

  // 사용자 선호도 파악
  const mql = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;);
  const prefersDarkFromMq = mql.matches;

  // 로컬 스토리지에 저장된 테마값
  const persistedPreference = localStorage.getItem(colorModeKey);

  let colorMode = &#39;dark&#39;; // 컬러모드 기본값은 다크

  const hasUsedToggle = typeof persistedPreference === &#39;string&#39;; // 로컬스토리지에 저장된 테마값이 있는지 여부를 저장

  if (hasUsedToggle) {
    colorMode = persistedPreference; // 저장했으면 로컬스토리지값 대로 컬러모드 지정
  } else {
    colorMode = prefersDarkFromMq ? &#39;dark&#39; : &#39;light&#39;; // 아니라면 선호도에 따라 컬러모드 지정
  }

  const root = document.documentElement;

  // 스타일 태그 속성에 현재 컬러모드 값을 기록
  root.style.setProperty(colorModeCssProp, colorMode);

  // theme 속성값을 기반으로 css 변수를 만들어내기
  // 예를 들어 
  //  &quot;card-background&quot;: {
  //          light: themeColors.primary.light,
  //          dark: themeColors.secondary.dark,
  //   },
  // 라는 테마값은 var(--card-background)로 변화함
  Object.entries(modeProperties).forEach(([name, colorByTheme]) =&gt; {
    const cssVarName = `--${name}`;
    // @ts-ignore
    root.style.setProperty(cssVarName, colorByTheme[colorMode]);
  });
}
</code></pre>
<p>이제 이 함수를 문자열화 하여 스크립트에 삽입하자!</p>
<pre><code class="language-tsx">
const ScriptTag = () =&gt; {
  const stringifyFn = String(setColorsByTheme)
    // eslint-disable-next-line quotes
    .replace(&#39;&quot;[MODEPROPERTIES]&quot;&#39;, JSON.stringify(themeProperties)) // JSON은 문자열로 변환시 쌍따옴표가 생겨서 추가적으로 쌍따옴표를 붙여서 처리해야 함.
    .replace(&#39;[COLORMODEKEY]&#39;, COLOR_MODE_KEY) 
    .replace(&#39;[COLORMODECSSPROP]&#39;, INITIAL_COLOR_MODE_CSS_PROP);

  const fnToRunOnClient = `(${stringifyFn})()`;

  return &lt;script dangerouslySetInnerHTML={{ __html: fnToRunOnClient }} /&gt;;
};

export default function Document() {
  return (
    &lt;Html&gt;
      &lt;Head /&gt;
      &lt;body&gt;
        &lt;ScriptTag /&gt;
        &lt;Main /&gt;
        &lt;NextScript /&gt;
      &lt;/body&gt;
    &lt;/Html&gt;
  );
}
</code></pre>
<p>이 과정들은 다소 생소할 수 있다. 여기서 한가지 의문이 들 수 있다.</p>
<h3 id="？-왜-함수와-변수를-문자열로-변환하여-저장하는-것인가">？ 왜 함수와 변수를 문자열로 변환하여 저장하는 것인가?</h3>
<p>이 스크립트 태그는 번들링된 자바스크립트를 불러오기 전에 실행이 된다. 이 때문에 번들링 된 값을 그대로 표시되면 원하는대로 실행이 될 수 없다. 가령,</p>
<pre><code class="language-tsx">import { themeProperties } from &#39;../styles/theme&#39;;

export function setColorsByTheme() {
  const modeProperties = themeProperties;

  ...

  // generating css variables based on modeProperties
  Object.entries(modeProperties).forEach(([name, colorByTheme]) =&gt; {
    const cssVarName = `--${name}`;
    // @ts-ignore
    root.style.setProperty(cssVarName, colorByTheme[colorMode]);
  });
}</code></pre>
<p>이렇게 작성한다면 <code>themeProperties</code> 라는 변수가 번들링된 자바스크립트를 가져와야 되고 이렇게 할 경우 스크립트가 제대로 실행될 수 없는 것이다. 이러한 이유로 함수 뿐만 아니라 내부 변수들도 문자열화한것은 replace 시키는 것이다. 이렇게 문자열로 변환하여 작성하게 되면 HTML에 다음과 같이 삽입된다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/0ed625f6-a163-456e-b119-2fe2fd78c3c3/image.png" alt=""></p>
<p>이렇게 삽입이 되면 어떤 번들링 로드 없이도 바로 실행가능한 스크립트가 완성된다!</p>
<h2 id="테마값-변경하기">테마값 변경하기</h2>
<p>테마에서 해야될 것이 있다. <code>setColorsByTheme</code> 함수 마지막 부분에서는 modeProperties라는 오브젝트를 변환해 알맞은 css 변수로 변환해주고 있다.</p>
<p>예를 들어 </p>
<pre><code class="language-json">&quot;card-background&quot;: {
    light: themeColors.primary.light,
    dark: themeColors.secondary.dark,
}</code></pre>
<p>라는 테마객체를 <code>var(--card-background)</code>로 변화시키는 방식이다. 이를 위해서 theme 객체에 다음과 같이 값을 추가해준다.</p>
<pre><code class="language-tsx">// theme.tsx

const themeProperties = {
  &#39;mode-color&#39;: {
    light: lightTheme.mode,
    dark: darkTheme.mode,
  },
  &#39;text-color&#39;: {
    light: lightTheme.text,
    dark: darkTheme.text,
  },
  &#39;background-color&#39;: {
    light: lightTheme.background,
    dark: darkTheme.background,
  },
  &#39;border-color&#39;: {
    light: lightTheme.borderColor,
    dark: darkTheme.borderColor,
  },
  &#39;body-color&#39;: {
    light: lightTheme.bodyColor,
    dark: darkTheme.bodyColor,
  },
};
</code></pre>
<p>앞으로 다크모드와 라이트모드에 따라 변경되는 스타일 값이 있다면 위와 같은 형식으로 테마 속성을 작성하면 된다.</p>
<h2 id="css-변수로-스타일-변경하기">CSS 변수로 스타일 변경하기</h2>
<p>추가적으로 기존의 스타일링 코드를 emotion theme 대신 css 변수로 변환하기만 하면 된다. 가령</p>
<pre><code class="language-tsx">const Button = styled.button`
  background: ${({ theme }) =&gt; theme.background};
  color: ${({ theme }) =&gt; theme.text};
  padding: 1rem;
  border-radius: 4px;
`;</code></pre>
<p>다음과 같이 작성된 코드는 아래로 바꾸면 된다.</p>
<pre><code class="language-tsx">const Button = styled.button`
  background: var(--background-color);
  color: var(--text-color);
  padding: 1rem;
  border-radius: 4px;
`;</code></pre>
<h2 id="colormode-변경함수-기능-추가">colorMode 변경함수 기능 추가</h2>
<p>마지막으로 버튼을 누르거나 시스템 선호도 변경시 테마 변경 대응하는 부분에서 css 변수도 알맞게 변경해주도록 하는 부분을 추가해주면 된다.</p>
<pre><code class="language-tsx">const ColorModeProvider = ({ children }: { children: ReactNode }) =&gt; {
  const [colorMode, setRawColorMode] = useState&lt;string | undefined&gt;(undefined);
  const systemPrefers = useMediaQuery(&#39;(prefers-color-scheme: dark)&#39;);
  const firstRender = useRef(true);

  // 컬러모드 설정 함수
  const setColorMode = useCallback((value: &#39;light&#39; | &#39;dark&#39;) =&gt; {
    const root = window.document.documentElement;
    // 바뀐 값에 대응해 css 변수들도 교체
    Object.entries(themeProperties).forEach(([name, colorByTheme]) =&gt; {
      const cssVarName = `--${name}`;
      root.style.setProperty(cssVarName, colorByTheme[value]);
    });

    setRawColorMode(value);
    window.localStorage.setItem(&#39;color-mode&#39;, value);
  }, []);

  useEffect(() =&gt; {
    if (firstRender.current) {
      const osTheme = systemPrefers ? &#39;dark&#39; : &#39;light&#39;;
      const userTheme = window.localStorage.getItem(&#39;color-mode&#39;);
      const theme = userTheme || osTheme;
      setRawColorMode(theme);
      firstRender.current = false;
    } else {
      setColorMode(systemPrefers ? &#39;dark&#39; : &#39;light&#39;);
    }
  }, [systemPrefers]);

  return (
    &lt;ColorModeContext.Provider value={{ colorMode, setColorMode }}&gt;
      {children}
    &lt;/ColorModeContext.Provider&gt;
  );
};</code></pre>
<p><br></br></p>
<h1 id="✨-결과-확인하기">✨ 결과 확인하기!</h1>
<p>기존의 기능은 살리면서도 새로고침 시 더이상 깜빡임이 발생하지 않는 것을 확인할 수 있다!<img src="https://velog.velcdn.com/images/seungchan__y/post/c5930cb6-aa75-4978-bd80-f929fdf8cf4f/image.gif" alt=""></p>
<p>완성된 코드는 이 <a href="https://github.com/Yangseungchan/Darkmode-practice">레포지토리</a>에서 확인해볼 수 있다.</p>
<p><br></br></p>
<h1 id="⛳️-출처">⛳️ 출처</h1>
<ul>
<li><p>[웹브라우저 렌더링 프로세스]
(<a href="https://cresumerjang.github.io/2019/06/24/critical-rendering-path/">https://cresumerjang.github.io/2019/06/24/critical-rendering-path/</a>) </p>
</li>
<li><p><a href="https://www.joshwcomeau.com/react/dark-mode">The quest for perfect dark mode</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Prototype에 대해 이해하기]]></title>
            <link>https://velog.io/@seungchan__y/Prototype%EC%97%90-%EB%8C%80%ED%95%B4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/Prototype%EC%97%90-%EB%8C%80%ED%95%B4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 02 Oct 2022 12:39:12 GMT</pubDate>
            <description><![CDATA[<h2 id="정의">정의</h2>
<p>자바스크립트에서는 다른 상속 기반의 언어와 달리, <code>prototype</code>(원형) 기반의 언어로, 모든 객체는 특정 객체를 원형(<code>prototype</code>)으로 삼고  이를 복제(참조)하는 방식을 통해 상속과 비슷한 효과를 가지게 한다.</p>
<h2 id="개념-추상화">개념 추상화</h2>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/3268222c-922c-48e8-9706-d558954d3801/image.png" alt=""></p>
<p>프로토타입을 추상화 하면 위와 같은 그림이 나오는데 이 그림은 다음의 의미를 가진다.</p>
<ul>
<li>생성자(<code>Constructor</code>)은 원형(<code>prototype</code>)을 속성으로 가진다.</li>
<li>생성자와 <code>new</code> 키워드를 결합하면 인스턴스(<code>instance</code>) 가 생성된다.</li>
<li>이때 인스턴스는 자동으로 <code>__proto__</code> 속성을 가진다.</li>
<li>이 <code>__proto__</code> 속성은 생성자의 <code>prototype</code> 을 참조한다.</li>
</ul>
<h2 id="인스턴스와-proto-의-관계와-이해">인스턴스와 <strong>proto</strong> 의 관계와 이해</h2>
<pre><code class="language-jsx">let Person = function (name) {
    this._name = name;
}

Person.prototype.getName = function () {
    console.log(&#39;call getName method&#39;);
    return this._name;
}</code></pre>
<p>위와 같이 생성자 함수 <code>Person</code> 을 정의하고 생성자 함수의 <code>prototype</code> 에<code>getName</code> 이라는 메소드를 정의하였다.</p>
<pre><code class="language-jsx">const j113 = new Person(&quot;seungchan&quot;);

j113.__proto__.getName(); 
// call getName method
// undefined</code></pre>
<p><code>j113</code> 이라는 인스턴스를 생성해주었고 이 인스턴스의 <code>prototype</code> 에 정의된 <code>getName</code> 이라는 메소드를 호출하였다. 이때 결과로 <code>undefined</code> 가 나오고 에러가 발생하지는 않는다.</p>
<pre><code class="language-jsx">Person.prototype === j113.__proto__ // true</code></pre>
<p>참고로, 인스턴스에서 호출하는 <code>__proto__</code> 는 <code>Person</code> 의 <code>prototype</code> 과 동일하기에 <code>getName</code> 메소드를 호출 할 수 있다.</p>
<p>이를 통해 알 수 있는 것은 <code>prototype</code> 에 정의된 <code>getName</code> 메소드에는 잘 도달하지만 <code>getName</code> 에서 반환하는 <code>this._name</code> 이 문제가 될 것이다. 이는 <code>getName</code> 메소드에서 <code>this</code> 가 무엇인지를 확인하면 원인파악이 되는데 <code>getName</code> 메소드를 호출하는 주체가 <code>j113.__proto__</code> 이고 이는 곧 <code>Person</code>의 <code>prototype</code> 인데 여기에는 <code>_name</code> 이라는 것이 존재하지 않는다. 그래서 위의 예시에서 <code>undefined</code> 가 나오게 된 것이다. </p>
<pre><code class="language-jsx">// Person 생성자함수 정의는 생략
Person.prototype._name = &quot;seungchan&quot;

const j113 = new Person(&quot;seungchan&quot;);

j113.__proto__.getName();
// seungchan</code></pre>
<p>실제로 위에 처럼 <code>Person</code> 의 <code>prototype</code> 자체에 <code>_name</code> 프로퍼티를 정의해 놓으면 <code>getName</code> 이 원하는대로 출력되는 것을 확인할 수 있다.</p>
<pre><code class="language-jsx">const j113 = new Person(&quot;seungchan&quot;);

j113.getName();// seungchan</code></pre>
<p>또한, 인스턴스에서 <code>__proto__</code> 라는 키워드를 생략한뒤 메소드를 호출하면 <code>this</code> 가 인스턴스가 됨으로써 원하는 결과가 나오게 된다. 여기서 인스턴스에서 어떻게 <code>prototype</code> 으로 접근이 가능한지 궁금할 수 있는데 이는 자바스크립트 문법을 설계하면서 정해진 규칙이라고 한다 (<del>이런거 보면 참 근본 없다.</del>)</p>
<p>이러한 특징들을 정리하자면 다음과 같다.</p>
<ul>
<li>생성자 함수의 <strong>prototype</strong>에 정의된 메소드나 프로퍼티는 생성된 인스턴스에서 자신의 것처럼 접근이 가능하다.</li>
<li>인스턴스에서 <code>__proto__</code> 키워드는 생략이 가능하다.</li>
</ul>
<br>

<h2 id="prototype을-기반으로-array에-대해-이해하기">prototype을 기반으로 Array에 대해 이해하기</h2>
<p>자바스크립트 내 배열 자료구조를 통해서 생성자함수 <code>Array</code> 와 이것의 인스턴스를 이해해보고자 한다.</p>
<pre><code class="language-jsx">const arr = [1,2];

console.dir(arr);

console.dir(Array);</code></pre>
<p>위의 출력 결과는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/7d7b2401-4662-4e37-a38b-0bdff7c0d62f/image.png" alt=""></p>
<p>생성자함수 <code>Array</code>의 인스턴스인 <code>arr</code>의 <code>__proto__</code> 는 생성자함수 <code>Array</code>의 <code>prototype</code> 과 동일한 것을 참조하는 걸 알 수 있다. 이를 추상화 해보면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/77afb317-41d9-4eef-b5f6-363e97b93892/image.png" alt=""></p>
<br>

<h2 id="메소드-오버라이드">메소드 오버라이드</h2>
<p>인스턴스는 <code>__proto__</code> 라는 키워드를 생략하더라도 <code>prototype</code> 에 정의된 메소드 및 프로퍼티에 자유롭게 접근이 가능하다. 한편 프로토타입에 정의된 메소드와 동일한 이름의 메소드를 인스턴스에 정의하게 될 경우에는 인스턴스의 함수로 덮어씌워진다.</p>
<pre><code class="language-jsx">let Person = function(name){
    this.name = name;
}

Person.prototype.getName = function() {
    return this.name;
}

const seungchan = new Person(&#39;승찬&#39;);

seungchan.getName = function () {
    return &#39;i am &#39; + this.name;
}

console.log(seungchan.getName()); // i am 승찬</code></pre>
<p>한편, 인스턴스의 메소드가 아닌 <code>prototype</code> 에서 정의한 메소드에 직접 접근하기 위해서는 아래와 같이 <code>__proto__</code> 키워드를 사용해야 한다. </p>
<pre><code class="language-jsx">console.log(seungchan.__proto__.getName()); // undefined</code></pre>
<p>하지만 위와 같이 <code>this</code> 가 가리키는 대상이 인스턴스의 <code>__proto__</code> 가 참조하는 <code>prototype</code> 이 되기 때문에 적절한 <code>name</code> 을 가져오지 못한다. 아래와 같이 이는 <code>call</code> 를 사용하여 <code>this</code> 를 인스턴스로 지정해주면 해결 될 수 있다.</p>
<pre><code class="language-jsx">console.log(seungchan.__proto__.getName()).call(seungchan); // &quot;seungchan&quot;</code></pre>
<br>

<h2 id="프로토타입-체이닝">프로토타입 체이닝</h2>
<p>배열 인스턴스의 프로토타입인  <code>Array</code> 생성자함수의 프로토타입을 자세히 살펴보면 아래와 같다.</p>
<pre><code class="language-jsx">console.dir([1,2]); </code></pre>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/0663edb0-fa6f-40af-b880-ef414ff19d58/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/c2c750b7-27e7-4b01-9e2e-96bed9e59b81/image.png" alt=""></p>
<p><code>[1,2]</code> 의 <code>__proto__</code>는 <code>Array</code> 를 가리키고 다시 <code>Array</code> 의 <code>__proto__</code> 는 <code>Object</code> 를 가리키는 것을 확인할 수 있다. 이를 추상화하면 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/b109f439-05d1-4eee-9a89-f5fd91c8e71b/image.png" alt=""></p>
<p>이러한 관계 덕분에 배열의 인스턴스인 <code>[1,2]</code> 는 Array의 메소드 뿐만 아니라 Object에서 정의된 메소드가 까지 사용이 가능해진다.</p>
<pre><code class="language-jsx">var arr = [1,2];

arr(.__proto__).push(3); // Array에 정의된 push 메소드

arr(.__proto__)(.__proto__).hasOwnProperty(2); // Object에 정의된 메소드</code></pre>
<p>한편 모든 타입은 <code>prototype</code> 을 가지고 <code>prototype</code> 은 <code>Object</code> 형태를 가지게 되기 때문에 <code>prototype</code> 체인의 최상단에는 <code>Object</code> 가 존재하게 된다. </p>
<p>이렇다보니 모든 데이터타입에서 <code>Object</code> 프로토타입에 정의된 메소드들을 접근할 수 있게 된다. 가령 <code>hasOwnProperty</code> 가 대표적인 예이다. 한편 모든 <code>Object</code> 의 메소드 중 일부는 모든 데이터타입에서 사용될 수 있게 설계 되어 있지는 않다. 이 때문에 그러한 메소드들은 정적인 메소드로 정의되게 되었다.</p>
<pre><code class="language-jsx">Object.freeze(...);

Object.entries(...);

Obejcts.keys(...)</code></pre>
<p>이 때문에 위의 메서드들은 <code>Object</code> 키워드와 함께 사용되어야 하는 것이다.</p>
<br>

<h2 id="⛳️-출처-및-참고자료">⛳️ 출처 및 참고자료</h2>
<p><a href="https://www.coupang.com/vp/products/295759416?itemId=932616303&amp;vendorItemId=5350528142&amp;src=1042503&amp;spec=10304984&amp;addtag=400&amp;ctag=295759416&amp;lptag=10304984I932616303V5350528142&amp;itime=20221002213829&amp;pageType=PRODUCT&amp;pageValue=295759416&amp;wPcid=18155083520566216361524&amp;wRef=&amp;wTime=20221002213829&amp;redirect=landing&amp;gclid=CjwKCAjw7eSZBhB8EiwA60kCW6EdgiNs4Sf6o8WBqH4F6x-fgdfT5FGhcsfWrGkocTuU96WnPrvf5xoCTeoQAvD_BwE&amp;campaignid=18394378295&amp;adgroupid=&amp;isAddedCart=">코어 자바스크립트 - 정재남 지음</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트와 이벤트루프]]></title>
            <link>https://velog.io/@seungchan__y/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%99%80-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A3%A8%ED%94%84</link>
            <guid>https://velog.io/@seungchan__y/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%99%80-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A3%A8%ED%94%84</guid>
            <pubDate>Sun, 14 Aug 2022 16:21:23 GMT</pubDate>
            <description><![CDATA[<h2 id="👀-tldr">👀 tl;dr</h2>
<hr>
<ul>
<li>자바스크립트 엔진 자체는 싱글 스레드로 동작하고 자바스크립트를 구동하는 환경이 멀티스레드 환경에서 동작한다.</li>
<li>Task Queue는 두가지로 분류되어 Call stack이 비어있을 때 우선순위 따라 다르게 처리된다.</li>
</ul>
<br>

<h3 id="🪡-자바스크립트와-비동기-처리">🪡 자바스크립트와 비동기 처리</h3>
<hr>
<p>자바스크립트는 단일 쓰레드 기반의 언어로 기본적으로 스레드가 하나이기에 동시에 하나의 작업만을 처리할 수 있다는 것을 시사한다. 하지만 실제로 자바스크립트가 사용되는 환경을 생각해보면 하나의 작업만이 아닌 더 많은 작업들이 동시에 처리되는 것을 확인할 수 있다. 예를 들어, 웹브라우저는 애니메이션 효과를 보여주면서 마우스 입력을 받아 처리한다든지, <code>Node.js</code> 기반의 웹서버에서는 동시에 여러 개의 <code>HTTP</code> 요청을 처리하기도 한다. 이처럼 단일 스레드인 자바스크립트에서도 동시성을 지원할 수 있는 비결에는 <code>이벤트 루프</code> 가 존재하여 비동기적인 일 처리를 가능케 하기 때문이다.</p>
<br>

<h3 id="👍-자바스크립트-자체는-단일-스레드이다">👍 자바스크립트 자체는 단일 스레드이다.</h3>
<hr>
<p>위에서 자바스크립트가 단일 스레드 기반임에도 동시성을 지원한다고 서술하였지만, 자바스크립트 엔진에는 이벤트 루프를 취급하지 않는다. 자바스크립트의 엔진인 V8의 경우 단일 호출 스택(<code>Call stack</code>)을 사용하며 요청이 들어올 때 마다 해당 요청을 순차적으로 호출 스택에 담아 처리할 뿐이다.</p>
<p>비동기 요청은 자바스크립트 엔진을 구동하는 환경, 즉 <code>브라우저</code>나 <code>NodeJS</code>가 담당한다. 브라우저 환경은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/fb296026-5ff1-4093-918f-bbe80f2b594b/image.png" alt="자바스크립트와 브라우저 환경"></p>
<p>비동기 호출을 위해 사용하는 <code>setTimeout</code> 이나 <code>XMLHttpRequest</code> 와 같은 함수들은 자바스크립트 엔진이 아닌 <code>Web API</code> 영역에 따로 정의되어 있다. 또한 이벤트 루프와 태스크 큐와 같은 장치들도 자바스크립트 엔진 밖에 구현되어 있는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/f8b9a4e8-06d0-429a-b3e0-f22b4b57fd16/image.png" alt=""></p>
<p><code>NodeJS</code> 환경에서도 브라우저와 거의 비슷한 구조를 볼 수 있는데, 차이점이 있다면 비동기 IO를 지원하기 위하여 <code>libuv</code> 라이브러리를 사용하며 이 <code>libuv</code> 가 이벤트 루프를 제공한다. 자바스크립트 엔진은 비동기 작업을 위해서 <code>NodeJS</code>의 API를 호출하며, 이때 넘겨진 콜백은 <code>libuv</code> 의 이벤트 루프를 통해 스케쥴되고 실행된다.</p>
<p>위에서 확인해 보았듯이 자바스크립트가 <code>단일 스레드</code> 기반의 언어라는 말은 <code>자바스크립트 엔진이 단일 호출 스택을 사용한다</code>는 관점에서만 사실이다. 실제 자바스크립트가 구동되는 환경(브라우저, NodeJS 등)에서는 주로 여러 개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 자바스크립트 엔진과 상호 연동하기 위해 사용하는 장치가 바로 <code>이벤트 루프</code> 인 것이다.</p>
<br>

<h3 id="♻️-이벤트-루프와-task-queue">♻️ 이벤트 루프와 Task Queue</h3>
<hr>
<p>앞서 자바스크립트에서는 비동기 이벤트들을 처리하기 위해서 <code>Task Queue</code> 와 이벤트 루프를 활용한다고 하였다. </p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/14dc5161-92f6-449a-85f5-329f7206b99f/image.png" alt=""></p>
<p>위의 그림에서 볼 수 있듯이 자바스크립트에서 <code>Web API</code>에 정의 되어있는 비동기 이벤트들을 호출하게 되면 Callback(Task) Queue와 이벤트 루프가 연계하여 비동기 이벤트를 처리하게 된다. 이를 자세하게 알아보기 위해 아래의 코드를 실행해본다고 하자.</p>
<pre><code class="language-jsx">const foo = () =&gt; console.log(&quot;First&quot;);
const bar = () =&gt; setTimeout(() =&gt; console.log(&quot;Second&quot;), 500);
const baz = () =&gt; console.log(&quot;Third&quot;);

bar();
foo();
baz();</code></pre>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/2ee81248-5393-46cb-bd99-d0219e25bb5a/image.gif" alt=""></p>
<p>실행 과정은 다음의 순서로 진행된다.</p>
<ol>
<li><code>bar</code> 함수가 호출된다. 이때 <code>Call stack</code>에 들어온  <code>setTimeout</code> 이라는 비동기 API를 처리해야 되기에 <code>WebAPI</code> 에서 500 밀리 초 동안 대기하게 된다</li>
<li>대기 하는 동안 <code>foo</code> 함수가 호출되고 이로 인해 출력창에 <code>“First”</code> 가 나온다.</li>
<li><code>setTimeout</code>에서 명시한 대기시간이 만료되어 <code>Task Queue</code> 로 전달된다. <code>Task Queue</code> 에 쌓이는 내용들은 이벤트 루프에 의해서 처리되는데, 이는 <code>Call stack</code> 이 비어있는 경우에만 이벤트루프에 의해 처리된다.</li>
<li><code>baz</code> 함수가 호출되고 출력창에 <code>“Third”</code> 가 나온다.</li>
<li><code>Call stack</code> 이 비어있는 상태인것을 파악한 이벤트 루프가 <code>Task Queue</code> 에 저장되어 있던 <code>setTimeout</code> 의 콜백함수를 콜스택에 넘기고 실행이 이루어지면서 출력창에는 <code>“Second”</code> 가 나오게 된다.</li>
</ol>
<p>즉 정리해보자면, 동기적인 Task들은 <code>Call stack</code> 에 의하여 순차적으로 처리되는 반면 <code>setTimeout</code> 과 같은 비동기 이벤트들은 Call stack에 들어오더라도 Web API와 Task Queue를 거친 뒤 <code>Call stack</code> 이 비어있을때에만 이벤트 루프에 의해 <code>Call stack</code> 으로 옮겨져 다시 처리됨을 알 수 있다.</p>
<br>

<h3 id="⚙️-micro-task--macro-task">⚙️ Micro Task , Macro Task</h3>
<hr>
<p><code>Task Queue</code> 에 의해서 비동기적으로 처리되는 Task들은 크게 두 가지 종류로 나뉘게 된다. 우선순위 다소 높은 비동기 API들을 <code>Macro Task</code>로 분류하며 대표적인 예로 위에서 다뤘던 <code>setTimeout</code> , <code>setInterval</code> , <code>setImmediate</code> 등이 있다. 한편, 이보다 더 높은 우선순위를 가지는 비동기 API들은 <code>Micro Task</code> 로 분류하며, <code>process.nextTick</code>, <code>Promise</code>객체의 <code>callback</code>, <code>queueMicrotask</code> 등이 있다. 그럼 이들이 어떤 방식으로 처리되게 될까?</p>
<br>

<p><img src="https://velog.velcdn.com/images/seungchan__y/post/104e6f34-1daf-4fb4-8b92-6d4ef1ede5ae/image.gif" alt=""></p>
<p>앞에서 말했듯이 우선순위가 더 높은 Task들이 <code>Micro Task</code> 들로 분류되기에 위 그림 처럼 여기에 저장되어있던 비동기 Task들이 우선적으로 처리된 이후에, <code>Macro Task</code> 에 저장된 Task들이 처리되는 모습이다. 물론 <code>Micro Task</code> 들 역시도 <code>Call stack</code> 에서 더 이상 처리할게 없어야 비로소 실행된다.</p>
<p>그러면 실제 예시에서 <code>setTimeout</code> 가 <code>Promise</code> 객체의 코드가 동시에 주어졌을때 어떻게 처리되는지 확인해보자. 예시로 다음의 코드를 살펴보자.</p>
<pre><code class="language-jsx">console.log(&#39;Start!&#39;);

setTimeout(() =&gt; {
    console.log(&#39;Timeout!&#39;);
}, 0);

Promise.resolve(&#39;Promise!&#39;).then(res =&gt; console.log(res));

console.log(&#39;End!&#39;);</code></pre>
<p>코드 내용을 살펴보면, 첫번째 비동기 부분에선 <code>setTimeout</code> 을 통해서 0초동안 대기 하였다가 <code>Timeout!</code> 을 출력하는 콜백함수를 실행한다. 두번째 비동기 부분에선 Promise 객체에 의해 비동기 적으로  <code>Promise!</code> 라는 텍스트를 출력하는 부분이다. 이 코드를 실행해보면 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/daffade2-1f3a-4561-88db-729f1c9f4fc1/image.gif" alt=""></p>
<p><code>Call Stack</code>에 첫번째 코드 부분이 쌓인 뒤 실행 되어 콘솔창에 <code>Start!</code> 가 출력된다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/0dd73b90-841b-4ce7-b3f3-067b8c2e9fd7/image.gif" alt=""></p>
<p><code>setTimeout</code> 부분이 콜스택에 적재된 이후에 <code>WebAPI</code> 에 의해 타이머가 작동하게 된다. 0초라서 사실상 바로 타이머는 종료된다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/seungchan__y/post/880395f4-d76f-47c9-a932-b9d60969204c/image.gif" alt=""></p>
<p>타이머가 종료됨에 따라 <code>setTimeout</code> 의 콜백함수 부분은 <code>MacroTask</code> 로 분류된다. 세번째 부분인 <code>Promise</code> 부분에선 콜스택에 적재 된다. 이때 <code>Promise.resolve</code> 는 인자로 받은 값을 <code>.then</code> 키워드 부분에 전달하는 역할을 함과 동시에 비동기 처리를 해제하는 역할을 한다. 이에 따라 <code>.then</code> 이후 부분은 <code>MircroTask</code> 에 분류시킨다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/seungchan__y/post/5f642434-99ee-407a-9a69-eded921fb1bb/image.gif" alt=""></p>
<p>네번째 부분이 실행됨에 따라 <code>End!</code> 라는 글자가 콘솔창에 출력된다. 이때 Task Queue에 적재된 Task들은 아직 <code>Call Stack</code> 내부가 아직 비워지지 않음에 따라 아직 <code>Queue</code>에서 빠져나가지 않는다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/seungchan__y/post/c612820d-0058-4f2a-96df-5432e164b14e/image.gif" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/f9ea2759-1b94-453b-8dc0-068fcf137a1b/image.gif" alt=""></p>
<p><code>Call stack</code> 이 비워진 것을 파악한 이벤트 루프는 순차적으로 <code>MicroTask Queue</code> 에 있는 Task들을 비워낸 후 <code>MacroTask Queue</code> 에 있는 Task들을 비워낸다. 비워낼때에는 당연히 Call Stack에 적재 시킨뒤에 순차적으로 실행시켜 처리한다.</p>
<br>

<h3 id="🚧-asyncawait-와-이벤트-루프">🚧 Async/Await 와 이벤트 루프</h3>
<hr>
<p><code>Async/Await</code> 키워드는 비동기 처리를 위해 <code>ES7</code> 부터 새롭게 도입된 것으로 기존의 <code>Promise</code> 와 잘 호환되게 설계 되었다. 이전에는 비동기 이벤트 처리를 위해 <code>Promise</code> 객체를 직접적으로 명시하는 수고가 필요했는데 이는 잘 작동하겠지만, 여전히 직관성이 떨어지는 코드에 해당된다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/d28a1895-9d62-4d4b-bfca-8e0d15792270/image.png" alt=""></p>
<br>

<p><code>Async/await</code> 키워드를 이용해 이제 <code>Promise</code> 객체를 반환하는 부분을 직관적으로 표현할 수 있게 된다.</p>
<p>이를 자세하게 파헤쳐 보고자 아래의 코드를 예시로 알아보고자 한다.</p>
<pre><code class="language-jsx">const one = () =&gt; Promise.resolve(&#39;One!&#39;);

async function myFunc(){
    console.log(&#39;In function!&#39;);
    const res = await One();
    console.log(res);
}

console.log(&#39;Before Function!&#39;);
myFunc();
console.log(&#39;After Function!&#39;);</code></pre>
<p>위의 코드를 실행해보면 다음과 같은 결과를 얻게 된다. 어떻게 되는건지 한줄한줄 파헤쳐 보자.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/eb5b24f6-0761-4272-b9de-8cace361c195/image.gif" alt=""></p>
<br>



<p><img src="https://velog.velcdn.com/images/seungchan__y/post/1aeb0a42-5aac-496a-a019-7ced61a72a5b/image.gif" alt=""></p>
<p>당연하게도 첫번째에서는 Call stack에 <code>console.log</code> 부분이 적재된 이후에 실행되어  <code>‘Before function!’</code> 이라는 문장이 출력되게 된다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/seungchan__y/post/43566b06-4538-4816-89cb-773a17bc4c72/image.gif" alt=""></p>
<p>두번째 부분에서 <code>myFunc</code> 함수가 호출되고 내부에 <code>console.log</code> 부분이 호출됨에 따라 콘솔 창에는 <code>In Function!</code> 이 추가로 출력된다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/seungchan__y/post/0c521a7d-a0d0-4a5b-8f17-234f488a739c/image.gif" alt=""></p>
<p>세번째에서는 <code>myFunc</code> 내부함수 <code>one</code> 이 호출되고 이는 <code>Promise</code> 객체를 반환한다. 이때 중요한 것은 <code>await</code> 키워드가 있다는 점인데, 이로 인해 <code>myFunc</code> 함수의 내부 실행은 잠시 중단되고 <code>Call stack</code> 에서 빠져나와 나머지 부분은 <code>Microtask Queue</code> 에 의해 처리된다. 이는 자바스크립트 엔진이 <code>await</code> 키워드를 인식하면 <code>async</code> 함수의 실행은 지연되는 것으로 처리하기 때문이다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/seungchan__y/post/c804f1f3-bd4b-4f93-bf1f-5305eebddbe5/image.gif" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/6bf593b8-fbda-4355-b1fe-1de1b1bee485/image.gif" alt=""></p>
<p><code>“After function!”</code> 이 출력하는 부분을 실행 한 뒤에 <code>Microstask Queue</code>  에 저장된 <code>myFunc</code> 실행 부분이 <code>Call Stack</code> 에 적재되어 실행된다. 그렇기에 마지막으로 <code>one</code> 이 반환하는 <code>“One!”</code> 이 출력된다.</p>
<p>참고로, 아래의 코드 처럼 <code>await</code> 키워드를 myFunc 실행부분 앞에도 추가하게 되면 위와 다른 결과가 나올것이다.</p>
<pre><code class="language-jsx">const one = () =&gt; Promise.resolve(&#39;One!&#39;);

async function myFunc(){
    console.log(&#39;In function!&#39;);
    const res = await one();
    console.log(res);
}

console.log(&#39;Before Function!&#39;);
await myFunc();
console.log(&#39;After Function!&#39;);</code></pre>
<p>이는 <code>myFunc</code>을 호출한 최상단부 자체를 <code>Microtask Queue</code> 로 넘기고  res를 출력하는 부분부터 <code>After  Function</code> 을 출력하는 부분까지 순차적으로 처리되게 되기 때문이다.</p>
<h3 id="📌-출처와-참고자료">📌 출처와 참고자료</h3>
<p><a href="https://meetup.toast.com/posts/89">자바스크립트와 이벤트 루프 : NHN Cloud Meetup</a></p>
<p><a href="https://velog.io/@titu/JavaScript-Task-Queue%EB%A7%90%EA%B3%A0-%EB%8B%A4%EB%A5%B8-%ED%81%90%EA%B0%80-%EB%8D%94-%EC%9E%88%EB%8B%A4%EA%B3%A0-MicroTask-Queue-Animation-Frames-Render-Queue">[JavaScript] Task Queue말고 다른 큐가 더 있다고? (MicroTask Queue, Animation Frames)</a></p>
<p><a href="https://medium.com/sjk5766/javascript-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%95%B5%EC%8B%AC-event-loop-%EC%A0%95%EB%A6%AC-422eb29231a8">JavaScript 비동기 핵심 Event Loop 정리</a></p>
<p><a href="https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif">✨♻️ JavaScript Visualized: Event Loop</a></p>
<p><a href="https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke#syntax">⭐️🎀 JavaScript Visualized: Promises &amp; Async/Await</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트는 Compiler / Interpreter 언어다?]]></title>
            <link>https://velog.io/@seungchan__y/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-Compiler-Interpreter-%EC%96%B8%EC%96%B4%EB%8B%A4</link>
            <guid>https://velog.io/@seungchan__y/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-Compiler-Interpreter-%EC%96%B8%EC%96%B4%EB%8B%A4</guid>
            <pubDate>Fri, 29 Jul 2022 08:12:26 GMT</pubDate>
            <description><![CDATA[<h2 id="tldr">tl;dr</h2>
<hr>
<p>자바스크립트는 인터프리터언어이다. 그런데 컴파일러를 얹은.</p>
<h1 id="🔍-컴파일러--인터프리터의-정의">🔍 컴파일러 / 인터프리터의 정의</h1>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/a5b6f1d1-3227-46a6-9054-6cb93bc9db3c/image.png" alt=""></p>
<h2 id="compiler-컴파일러">Compiler (컴파일러)</h2>
<hr>
<p>컴파일러는 특정 프로그래밍 언어로 쓰여 있는 문서를 다른 프로그래밍 언어로 옮기는 언어 번역 프로그램을 의미한다. 가령 자바스크립트라는 고급 프로그래밍 언어를 기계어라는 다른 프로그래밍 언어로 번역하는 역할을 컴파일러가 수행한다.</p>
<h2 id="interpreter-인터프리터">Interpreter (인터프리터)</h2>
<hr>
<p>기계어를 다른 언어로 번역할 필요 없이 프로그래밍 언어의 소스 코드를 바로 실행하는 컴퓨터 프로그램 또는 환경을 말한다. 코드 한줄한줄씩 바로 실행해나가는 방식으로 진행된다. 이처럼 바로 실행이 가능하기에 변경사항을 빠르게 테스트해보기 용이하다는 장점이 있다.</p>
<h2 id="자바스크립트는-기본적으로-인터프리터-언어이다">자바스크립트는 기본적으로 인터프리터 언어이다.</h2>
<hr>
<p>기본적으로 자바스크립트는 인터프리터 언어에 해당한다. 다른 대표적인 컴파일러 언어인 C언어 혹은 C++의 경우에는 컴파일 과정(+어셈블러 +링커) 을 통해 실행파일을 생성해주어야 비로소 프로그램 실행이 가능해진다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/bdc5777a-8e4b-4e70-bb6b-74b2d6f9e0f8/image.png" alt=""></p>
<p>반면 파이썬이나 자바스크립트 같은 언어의 경우에는 위의 사진처럼 코드 한줄씩만 입력하더라도  바로바로 실행하며 결과를 확인하는 것이 가능하기에 인터프리터 언어에 해당하게 된다. 이는 자바스크립트가 만들어질 당시 웹문서 구조를 동적으로 나타내기 위해 제작된 언어이기에 가벼운 인터프리터 구조가 적합했기 때문이다.</p>
<p>그렇다고 자바스크립트가  따로 별도의 과정없이 바로 컴퓨터가 실행가능한 것은 아니다. 자바스크립트가 인터프리터에 전해지기 일련의 과정을 거쳐야 한다.</p>
<h2 id="자바스크립트-구동원리">자바스크립트 구동원리</h2>
<hr>
<p>자바스크립트 뿐만 아니라 모든 고급언어들은 컴퓨터에서 구동되기 위해서 기본적으로 기계가 이해가능한 기계어로 변환되어질 필요가 있다. </p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/55d8565a-59c2-48e3-860b-88aa9dcfea00/image.png" alt=""></p>
<p>위에서 보다시피 자바스크립트는 기계에게 전달되기전에 바이트 코드로 변환되고 이를 받아 가상머신에 의해 기계어로 변환된다. 이러한 일련의  변환 과정은 아래와 같이 진행된다.</p>
<p><strong>1) 바이트 코드로의 변환</strong></p>
<p>자바스크립트 엔진에 의해 바이트코드로 변환된다.</p>
<p><strong>2) 기계어로 변환</strong></p>
<p>CPU마다 기계어를 다르게 해석하기에 가상 머신은 최적화된 기계어를 제작해낸다. 이 가상머신 덕분에 개발자는 따로 CPU별로 최적화된 기계어를 만들어낼 필요는 없다.</p>
<p><strong>3) CPU 코드 실행</strong></p>
<p>기계어를 실행하여 데이터 저장 및 연산 작업을 진행한다.</p>
<h2 id="자바스크립트-엔진--tokenizer--parser--ast--인터프리터">자바스크립트 엔진 : Tokenizer &gt; Parser &gt; AST &gt; 인터프리터</h2>
<hr>
<p>이제 JS가 자바스크립트 엔진에 의해 어떻게 바이트 코드로 변환되는지 알아본다. 이는 엔진 내 인터프리터가 진행한다. 인터프리터에게 전달되기 전에도 일련의 과정이 필요한데 이는 이번 6일차에서 배웠던 Tokenizer, Parser를 거쳐 AST가 되는 과정이다.</p>
<p>이를 단순화해서 보면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/7e873760-fc6f-4522-8cbc-619f1082afe4/image.avif" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/55523060-3b18-442b-b349-3606ff088542/image.png" alt=""></p>
<ul>
<li><p><code>Tokenizing</code> : 주어진 소스코드를 의미있는 단위로 나누는 과정이다. 이렇게 나누어진 것을 Token이라고도 한다. (보통 의미를 부여하는 lexer의 역할도 여기에 포함된다.)</p>
</li>
<li><p><code>Parser</code> : <code>Tokenizer</code> 로부터 생성된 토큰들의 배열을 바탕으로 이를 자바스크립트 문법에 알맞은 방식으로 <code>AST(Abstract Syntax Tree)</code> 로 변화 시킨다.</p>
</li>
<li><p>이렇게 생성된 <code>AST</code> 는 인터프리터를 거쳐 기계가 알아볼 수 있는 바이트 코드롤 변환되게 되는 것이다.</p>
</li>
</ul>
<h2 id="이때까지만-해도-인터프리터-언어였다-이게-등장하기-전까진">이때까지만 해도 인터프리터 언어였다… 이게 등장하기 전까진</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/64b2f396-bb10-43bb-ba9e-502228cb4ec6/image.png" alt=""></p>
<p>이러한 자바스크립트는 인터프리터 언어로서 기능을 해왔지만, 점차 웹에서도  다양한 요구사항들이 추가되면서 더 많은 기능들을 갖추어야 했고 이는 자바스크립트가 점차 성능상 무거워지는 계기가 되었다. 한편, 2009년 당시 구글은 웹에서 이용가능한 지도인 구글맵스를 개발하려고 있었는데 지도 어플리케이션은 사용자 상호작용이 많이 필요한 만큼 성능상 개선이 필요했고 이를 개선하고자 내놓은 것이 바로 <code>Chrome V8</code> 엔진이다. 이를 통해 자바스크립트 언어에서도 컴파일을 진행하게 된 계기가 되었다.</p>
<h2 id="🤔-컴파일-언어의-성능--인터프리터-언어의-성능">🤔 컴파일 언어의 성능 &gt; 인터프리터 언어의 성능?</h2>
<hr>
<p>이에 대해 알아보기전에 왜 컴파일 언어의 성능이 더 효율적인지 이해해볼 필요가 있다. 컴파일 언어와 인터프리터 언의 가장 큰 차이점은 바로 실행전 미리 기계어로 바꾸어 놓는다는 점이다. 인터프리터처럼 고급언어를 기계어로 번역하는 것이 아니라 미리 변경해놓기에 빠른 것인데 이는 아래 예시를 보면 훨씬 이해하기 수월해진다.</p>
<pre><code class="language-jsx">function sum () {
    let result = 0
    for (let i = 1 ; i &lt;= 10 ; i++){
        result += i;
    }
    return result;
}
sum() // for loop.. -&gt; 55
sum() // for loop.. -&gt; 55
sum() // for loop.. -&gt; 55

// compile 결과
sum() = 55
sum() = 55
sum() = 55</code></pre>
<p>이러한 코드가 있다고 할때 컴파일 과정을 거친 <code>sum</code> 함수의 결과값은 미리 기계어로 번역되기에 실제 실행할 필요없이 미리 정해져있다. 반면 인터프리터는 한줄한줄 실행해 나가는 방식이기에 <code>sum</code> 를 만나게 되면 일일이 실행하여 <code>for</code> 문을 거쳐야 비로소 결과가 도출된다. </p>
<p>이를 통해 컴파일 언어가 인터프리터 언어보다 좋은 성능을 보이는지 확인해보았다.</p>
<h2 id="자바스크립트의-컴파일">자바스크립트의 컴파일</h2>
<hr>
<p>다시 돌아와 V8 엔진에 의해서 어떻게 자바스크립트도 컴파일과정을 거치는지 알아볼 차례이다. V8 엔진은 기존의 Parser를 거쳐 AST로 변환된 내용을 인터프리터에게 전달하는 과정에 덧붙여 자바스크립트 변환과정을 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/59b446be-bac9-4031-b57a-86b4a2c17819/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/7f97490e-90a5-47ca-87a0-b1d3c754a850/image.png" alt=""></p>
<p>위 그림에서 자바스크립트가 Parser, AST, Interpreter를 거쳐 ByteCode로 변모하는 것은 V8 엔진이 등장하기 전까지의 JS의 모습이다. 이에 추가적으로 <code>Profiler</code> 라는게 등장한다. 이 <code>Profiler</code> 는 인터프리터를 관찰하며 실행되는 코드를 계속해서 모니터링 한다. 모니터링하는 과정에 코드내에 반복 실행되는 것이 있다면 이를 컴파일러에게 넘겨 실시간으로 컴파일 하도록 한다. 이를 통해 최적화된 바이트 코드를 생성해낸다. 이전의 코드 예시에서 <code>sum</code> 함수처럼 반복해서 진행되는 여지가 있는 코드가 컴파일의 대상이 되겠다.</p>
<p>이처럼 필요할때 마다 컴파일 하는 컴파일러를 <code>JIT(Just-In-Time)</code> 컴파일러라고 부른다. 또한 필요할 경우  <code>Decompile</code> 과정을 진행하는데 이는 컴파일러가 판단할때 컴파일하는게 좋다고 판단했던 것이 잘못되었음을 알고 되돌리는 과정이다. 이는 컴파일 하는 비용을 줄이기 위함이라고 한다.</p>
<h1 id="자바스크립트는-compiler--interpreter-언어다">자바스크립트는 Compiler / Interpreter 언어다?</h1>
<p>이제 이 질문에 대답할 수 있는 차례가 왔다. 정답은 살펴본것 처럼 둘다에 해당한다. 기본적으로는 <code>Interpreter</code> 언어로서의 성질을 가지지만, 성능상의 최적화를 위해 <code>Compiler</code> 언어의 특성도 같이 가진다. </p>
<h1 id="🌈-출처-및-참고자료">🌈 출처 및 참고자료</h1>
<p><a href="https://curryyou.tistory.com/237">자바스크립트 코드 실행 동작 원리: 엔진, 가상머신, 인터프리터, AST 기초</a></p>
<p><a href="https://devlog-of-yein.tistory.com/m/6">컴파일이란 무엇이며, 자바스크립트는 인터프리터 언어인가?</a></p>
<p><a href="https://pks2974.medium.com/v8-%EC%97%90%EC%84%9C-javascript-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%8B%A4%ED%96%89%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC%ED%95%B4%EB%B3%B4%EA%B8%B0-25837f61f551"></a></p>
<p><a href="https://www.oowgnoj.dev/review/advanced-js-1">JavaScript, 인터프리터 언어일까?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Context API 를 컴포넌트로 사용하기]]></title>
            <link>https://velog.io/@seungchan__y/Context-API-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/Context-API-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 17 Jul 2022 08:53:21 GMT</pubDate>
            <description><![CDATA[<h1 id="context-컴포넌트로-사용하기">Context 컴포넌트로 사용하기</h1>
<blockquote>
<p>해당 포스트는 Context API에 대한 이해를 요구합니다. Context API에 대해 처음이시라면 <a href="https://react.vlpt.us/basic/22-context-dispatch.html">velopert님의 포스트</a>를 참고해주세요.</p>
</blockquote>
<p><code>React</code> 에서는 전역 상태 관리를 위하여 자체적으로 <code>ContextAPI</code> 를 제공해준다.</p>
<p><code>ContextAPI</code> 를 적절히 이용하면, <code>Props Drilling</code>을 이용해 Props를 전달해야했던 불편함은 해소되고 불필요한 Props 공유를 막을 수 있어 매우 좋은 도구중 하나이다. 간단하게 카운터 앱에서 카운트 숫자와 카운트를 증가하는 Dispatch 함수를 공유하는 <code>context</code>를  정의해보자.</p>
<h2 id="🎈countercontext-정의-해보기">🎈counterContext 정의 해보기</h2>
<ul>
<li><code>App.tsx</code></li>
</ul>
<pre><code class="language-tsx">import { useState, createContext, Dispatch, useContext } from &#39;react&#39;;
import &#39;./App.css&#39;;
import Counter from &#39;./components/Counter&#39;;
import DisplayCount from &#39;./components/DisplayCount&#39;;

interface counterContextValue {
  count: number;
  setCount: Dispatch&lt;number&gt;;
}

export const counterContext = createContext&lt;counterContextValue | undefined&gt;(undefined);

export const useCounterContext = () =&gt; {
  const context = useContext(counterContext);
  if(!context){
    throw new Error(&#39;useCounterContext must be used within a CounterContextProvider&#39;);
  }
  return context;
}

function App() {
  const [count, setCount] = useState&lt;number&gt;(0);
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;counterContext.Provider value={{count, setCount}}&gt;
        &lt;DisplayCount/&gt;
        &lt;Counter/&gt;
      &lt;/counterContext.Provider&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<ul>
<li><code>components/Counter.tsx</code></li>
</ul>
<pre><code class="language-tsx">import { useCounterContext } from &quot;../App&quot;;

export default function Counter() {
    const { count, setCount } = useCounterContext();

    return (
        &lt;div&gt;
            &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
                Click me
            &lt;/button&gt;
        &lt;/div&gt;
    );
}</code></pre>
<ul>
<li><code>components/DisplayCount.tsx</code></li>
</ul>
<pre><code class="language-tsx">import { useCounterContext } from &quot;../App&quot;;

export default function DisplayCount ()  {
    const {count} = useCounterContext();
    return &lt;p&gt;You clicked {count} times&lt;/p&gt;;
}</code></pre>
<p>간단하게 <code>App.tsx</code> 카운트 숫자와 카운트를 증가시키는 Dispatch 함수를 담는 <code>counterContext</code> 를 정의하고 이를 <code>Counter</code> 라는 컴포넌트와 <code>DisplayCount</code> 라는 컴포넌트에게 <code>Context API</code> 를 통해  카운트 숫자와 증가 함수를 전달시키는 방식이다. 여기서 기존과는 다른 방식이 약간 존재한다.</p>
<p>우선 타입스크립트에서 <code>createContext</code> 를 하려고하면 초기화 할때 기본값을 넘겨주어야 할텐데 아래의 코드를 이용할 경우 불필요한 기본값을 정의할 필요없이 <code>undefined</code> 로 초기화 할 수 있다.</p>
<p>우선 타입스크립트에서 <code>createContext</code> 를 하려고하면 초기화 할때 기본값을 넘겨주어야 할텐데 아래의 코드를 이용할 경우 불필요한 기본값을 정의할 필요없이 <code>undefined</code> 로 초기화 할 수 있다.</p>
<pre><code class="language-tsx">export const counterContext = createContext&lt;counterContextValue | undefined&gt;(undefined);</code></pre>
<p>하지만 이 상태로 바로 <code>Counter</code> 컴포넌트에서 Context를 사용하려고 하면 다음과 같은 타입 오류가 발생한다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/2214748b-092b-485c-8ac3-e099b126605e/image.png" alt=""></p>
<p>이는 <code>Counter</code>  컴포넌트 입장에서는 <code>counterContext</code> 에서 넘어오는 값이  <code>counterContextValue | undefined</code> 로 정의 되어져 있고 둘 중 어느 타입이 올지 정확히 알 수 없기 때문이다. 여기서 <code>Counter</code> 컴포넌트는 <code>counterContext</code> 로 부터 타입이 <code>counterContextValue</code> 인 값만 받아오면 되기 때문에 타입가드를 적절히 활용하여 특정 타입만 오도록 타입을 제한하면 될 것이다.</p>
<br/>

<pre><code class="language-tsx">export const useCounterContext = () =&gt; {
  const context = useContext(counterContext);
  if(!context){
    throw new Error(&#39;useCounterContext must be used within a CounterContextProvider&#39;);
  }
  return context;
}</code></pre>
<p>이를 위해 위와 같은 커스텀 훅을 도입하게 되면 <code>Counter</code> 컴포넌트는 <code>undefined</code> 를 제외한 값들을 받아올 수 있게 된다. 뿐만 아니라 불필요하게 <code>useContext</code> 훅과 <code>counterContext</code> 를 import 할 필요없이 <code>useCounterContext</code> 만 import하면 <code>counterContext</code> 를 사용할 수 있게되어 코드량도 줄일 수 있다!</p>
<pre><code class="language-tsx">import { useCounterContext } from &quot;../App&quot;;

export default function Counter() {
    const { count, setCount } = useCounterContext();

    return (
        &lt;div&gt;
            &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
                Click me
            &lt;/button&gt;
        &lt;/div&gt;
    );
}</code></pre>
<br/>

<h2 id="🧐-기존-context-사용-방법의-문제점들">🧐 기존 context 사용 방법의 문제점들</h2>
<pre><code class="language-tsx">import { useState, createContext, Dispatch, useContext } from &#39;react&#39;;
import &#39;./App.css&#39;;
import Counter from &#39;./components/Counter&#39;;
import DisplayCount from &#39;./components/DisplayCount&#39;;

interface counterContextValue {
  count: number;
  setCount: Dispatch&lt;number&gt;;
}

export const counterContext = createContext&lt;counterContextValue | undefined&gt;(undefined);

export const useCounterContext = () =&gt; {
  const context = useContext(counterContext);
  if(!context){
    throw new Error(&#39;useCounterContext must be used within a CounterContextProvider&#39;);
  }
  return context;
}

function App() {
  const [count, setCount] = useState&lt;number&gt;(0);
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;counterContext.Provider value={{count, setCount}}&gt;
        &lt;DisplayCount/&gt;
        &lt;Counter/&gt;
      &lt;/counterContext.Provider&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<p>앞서 봤던 <code>App.tsx</code> 다시 한번 확인해보자. 크게 두가지 문제점들이 있다. 우선, <code>counterContext</code> 를 정의하기 위해 타이핑을 위한 <code>interface</code> 선언, <code>createContext</code> 를 통해 context를 생성하는 부분,  편의성을 위해 정의한 커스텀 훅 등 <code>App.tsx</code> 파일 내부에 너무 많은 코드들이 작성되어 있다. <code>App.tsx</code> 내부에 여러 컴포넌트들을 추가하면서 코드 길이가 길어진다면 읽기가 매우 힘들어질 것이다.</p>
<pre><code class="language-tsx">import { useCounterContext } from &quot;../App&quot;;

export default function Counter() {
    const { count, setCount } = useCounterContext();

    return (
        &lt;div&gt;
            &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
                Click me
            &lt;/button&gt;
        &lt;/div&gt;
    );
}</code></pre>
<p>또한 이를 <code>counterContext</code> 사용하기 위해 <code>App.tsx</code>와 <code>Counter.tsx</code> 서로가 import 하게 되는 순환형 참조가 일어나게 된다. 이를 방지 하기 위해서는 기존의 context 사용 방법을 개선해야될 필요가 있는데 이는 바로 context 자체를 <strong>컴포넌트화</strong> 하는 것이다.</p>
<br/>

<h2 id="🚧-context를-컴포넌트화-하기">🚧 Context를 컴포넌트화 하기</h2>
<ul>
<li><code>components/counterContext.tsx</code></li>
</ul>
<pre><code class="language-tsx">import { createContext, useState, ReactNode, Dispatch, useContext } from &quot;react&quot;

interface counterContextValue {
  count: number;
  setCount: Dispatch&lt;number&gt;;
}

const counterContext = createContext&lt;counterContextValue | undefined&gt;(undefined);

const CounterProvider = ({children}: {children: ReactNode}) =&gt; {
    const [count, setCount] = useState&lt;number&gt;(0);
    return (
        &lt;counterContext.Provider value={{count, setCount}}&gt;
            {children}
        &lt;/counterContext.Provider&gt;
    );
}

const useCounterContext = () =&gt; {
    const context = useContext(counterContext);

    if(!context){
        throw new Error(&#39;useCounterContext must be used within a CounterContextProvider&#39;);
    }
    return context;
}

export {CounterProvider, useCounterContext};</code></pre>
<p>기존의 <code>context</code> 관련 코드들을 모두 하나의 컴포넌트로 담는 것이 핵심이다. 또한 이를 <code>counterContext</code> 의 공급책인 <code>counterContext.Provider</code> 를 사용하는 대신 이를 포함하는 컴포넌트인 <code>CounterProvider</code> 를 정의하여 좀 더 간결하게 사용할 수 있도록 할 수 있다. 아래처럼 말이다.</p>
<ul>
<li><code>App.tsx</code></li>
</ul>
<pre><code class="language-tsx">import &#39;./App.css&#39;;
import Counter from &#39;./components/Counter&#39;;
import {CounterProvider} from &#39;./components/counterContext&#39;;
import DisplayCount from &#39;./components/DisplayCount&#39;;

function App() {
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;CounterProvider&gt;
        &lt;DisplayCount/&gt;
        &lt;Counter/&gt;
      &lt;/CounterProvider&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<ul>
<li><code>DisplayCount.tsx</code></li>
</ul>
<pre><code class="language-tsx">import { useCounterContext } from &quot;./counterContext&quot;;

export default function DisplayCount ()  {
    const {count} = useCounterContext();
    return &lt;p&gt;You clicked {count} times&lt;/p&gt;;
}</code></pre>
<ul>
<li><code>Counter.tsx</code></li>
</ul>
<pre><code class="language-tsx">import { useCounterContext } from &quot;./counterContext&quot;;

export default function Counter() {
    const { count, setCount } = useCounterContext();

    return (
        &lt;div&gt;
            &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
                Click me
            &lt;/button&gt;
        &lt;/div&gt;
    );
}</code></pre>
<p>기존에는 <code>Context API</code> 를 사용하면서 아쉬웠던 점이 코드가 굉장히 지저분해진다 였는데 이렇게 컴포넌트화 하여 사용하니 코드도 깔끔해지면서 굉장히 사용하기 편리해졌다. <code>Context API</code>가 필요하다면 이런식으로 컴포넌트화 하여 사용하는 것을 추천한다!</p>
<h1 id="🚩-출처와-참고자료">🚩 출처와 참고자료</h1>
<p><a href="https://kentcdodds.com/blog/how-to-use-react-context-effectively">How to use React context effectively</a></p>
<p><a href="https://react.vlpt.us/basic/22-context-dispatch.html">velopert - 22. Context API 를 사용한 전역 값 관리</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS와 ISR]]></title>
            <link>https://velog.io/@seungchan__y/NextJS%EC%99%80-ISR</link>
            <guid>https://velog.io/@seungchan__y/NextJS%EC%99%80-ISR</guid>
            <pubDate>Thu, 07 Jul 2022 14:59:28 GMT</pubDate>
            <description><![CDATA[<h1 id="😛-tldr">😛 tl;dr</h1>
<p><strong>ISR과 On-demand revlidation을 이용하면 정적생성으로도 사용자에게 실시간으로 업데이트된 페이지를 제공할 수 있다.</strong></p>
<h2 id="🥏-기존의-렌더링-방식">🥏 기존의 렌더링 방식</h2>
<p>일반적으로 NextJS를 활용하게 되면 크게 3가지 렌더링 방식을 사용할 수 있다.</p>
<ul>
<li><p><code>CSR</code> (클라이언트 사이드 렌더링) : <code>useEffect</code>훅을 이용하거나, <code>SWR</code> 같은 상태관리 툴을 이용해 렌더링의 책임을 사용자에게 전가하는 것. 화면 로딩이 사용자 눈에 보여 사용자 경험을 감소시키는 단점이 있다</p>
</li>
<li><p><code>SSR</code> (서버사이드 렌더링) : 렌더링의 책임이 프론트엔드 서버에게 주어지며, 웹사이트 사용자가 접속할때마다 새로운 페이지를 생성해내는 방식. 매번 최신 정보를 유지해야한다면 좋은 방식이긴 하지만, 성능상 이슈가 있고 화면 깜빡임 현상이 있다.</p>
</li>
<li><p><code>SSG</code> (정적 생성) : 렌더링의 책임이 역시 프론트엔드 서버에게 주어지지만, 프론트엔드 <code>build</code> 시간에 미리 화면에 대한 HTML을 미리 생성하여 사용자에게 미리 만들어진 화면을 제공한다. 이를 통해 성능상의 이점은 챙길 수 있으나, 미리 생성된 페이지를 제공하는 방식 이기 때문에 페이지 내 데이터가 변화하더라도  <strong>변화된 내용들을 전혀 제공해주지 못한다</strong>.</p>
</li>
</ul>
<p>이러한 상황 속에서 NextJS에서 성능상의 이점은 챙기면서도 변화된 내용에 대한 업데이트를 제공해줄 수 있는 방식이 바로 <code>ISR</code>(Incremental Static Regeneration) 방식이다. 오늘은 이 ISR 방식이 어떠한 구조로 이루어졌는지를 이해해보고 실제 사용해볼 것이다.</p>
<h2 id="📡-isr이란">📡 ISR이란?</h2>
<p><a href="https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration">공식문서</a>에 따르면 다음과 같이 작성되어 있다.</p>
<blockquote>
<p>Next.js allows you to create or update static pages <em>after</em>
 you’ve built your site. Incremental Static Regeneration (ISR) enables you to use static-generation on a per-page basis, <strong>without needing to rebuild the entire site</strong>. With ISR, you can retain the benefits of static while scaling to millions of pages.</p>
</blockquote>
<p>즉, 정적생성으로 미리 만들어놓은 사이트들도 필요하다면 업데이트가 가능하다는 이야기이다. 이를 이용한다면 정적생성의 장점을 취하되 단점을 보완할 수 있게 되는 것이다. ISR은 기존의 정적생성 방식에 몇가지 옵션들을 추가하면 바로 적용이 가능하다.</p>
<pre><code class="language-jsx">function Blog({ posts }) {
  return (
    &lt;ul&gt;
      {posts.map((post) =&gt; (
        &lt;li key={post.id}&gt;{post.title}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  )
}

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// the path has not been generated.
export async function getStaticPaths() {
  const res = await fetch(&#39;https://.../posts&#39;)
  const posts = await res.json()

  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) =&gt; ({
    params: { id: post.id },
  }))

  // We&#39;ll pre-render only these paths at build time.
  // { fallback: blocking } will server-render pages
  // on-demand if the path doesn&#39;t exist.
  return { paths, fallback: &#39;blocking&#39; }
}

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
  const res = await fetch(&#39;https://.../posts&#39;)
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 10 seconds
    revalidate: 10, // In seconds
  }
}

export default Blog</code></pre>
<p>위의 코드는 공식문서에서 제시하고 있는 ISR 예시이다. 우선 <code>build</code> 시간에 정적 생성할 경로들을 <code>getStaticPaths</code> 를 이용해 전달 받는데 기존과 다르게 여기서 <code>fallback: &#39;blocking&#39;</code> 옵션이 추가되어 있다.  <code>fallback</code> 옵션에 대한 자세한 설명은 <a href="https://nextjs.org/docs/api-reference/data-fetching/get-static-paths">공식문서</a>에도 정리되어 있고 한국어로 더 잘 정리해놓은 내용이 있어 해당 <a href="https://velog.io/@mskwon/next-js-static-generation-fallback">블로그 링크</a>로 설명을 대체한다. 따라서, 해당 예시에서는 <code>fallback: &#39;blocking&#39;</code> 이기 때문에 <code>build</code> 시간에 만들어지지 않은 경로는 SSR처럼 요청시 새롭게 생성된다는 의미이다.  </p>
<p><code>getStaticPaths</code> 가 정적생성할 경로 id들을 제공해줬다면, <code>getStaticProps</code> 에서는 해당 경로들에 대해서만 정적생성을 진행한다. 여기서 <code>revalidate: 10</code> 옵션이 추가되어 있는데, 이는 해당 페이지로 어느 사용자가 진입한 이후 10초 후에 해당 페이지에 대해서 정적생성을 진행한다는 의미이다. 이때 정적 생성된 페이지를 통해 다음 사용자에게 업데이트된 내용이 제공된다. 간혹 일부 블로그들에서 다음과 같이 옵션을 설정하면, 매 10초마다 정적생성을 진행하는 것으로 설명이 되어있는데 이는 잘못된 이야기다. 아래의 그림을 보면 이해하기 조금 더 수월해진다. </p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/a7b4619a-a5d7-4594-a755-b02f0a911b8f/image.png" alt=""></p>
<p>사용자1이 해당 페이지에 진입하면 이때 부터 60초 동안은 어느 사용자가 들어오더라도 미리 생성해두었던 페이지를 제공해 준다. 이후 60초가 지나면 NextJS 백그라운드에서 해당 페이지의 업데이트가 이루어지고 이 업데이트가 완료되면 앞으로 새롭게 만들어진 페이지를 사용자에게 제공해주는 방식이다. 이를 통해 <code>SSG</code> 의 성능상 이점을 챙기면서도 사용자에게는 업데이트된 내용을 제공해줄 수 있다. 이제 부터는 어떻게 실제로 사용하는지 작은 실습을 통해 알아볼 것이다.</p>
<h2 id="🕹-isr-실습해보기">🕹 ISR 실습해보기</h2>
<h2 id="1-api-구성">1. API 구성</h2>
<p>이를 위해서는 간단한 백엔드 API와 NextJS 프로젝트가 필요한데 API는 <a href="https://github.com/typicode/json-server">json-server</a> 를 이용해 간단하게 구동해볼 것이다. 이를 이용하면 json파일만으로도 간단하게 API를 구성해볼 수 있다.</p>
<p>우선, <code>json-server</code> 를 전역적으로 설치해준다.</p>
<pre><code class="language-bash">npm i -g json-server</code></pre>
<p>이후 <code>db.json</code> 라는 이름으로 파일을 하나 생성해준다.</p>
<pre><code class="language-json">{
  &quot;books&quot;: [
    { &quot;id&quot;: 1, &quot;title&quot;: &quot;book1&quot;, &quot;description&quot;: &quot;this is book1&quot; },
    { &quot;id&quot;: 2, &quot;title&quot;: &quot;book2&quot;, &quot;description&quot;: &quot;this is book2&quot; },
    { &quot;id&quot;: 3, &quot;title&quot;: &quot;book3&quot;, &quot;description&quot;: &quot;this is book3&quot; },
    { &quot;id&quot;: 4, &quot;title&quot;: &quot;book4&quot;, &quot;description&quot;: &quot;this is book4&quot; },
    { &quot;id&quot;: 5, &quot;title&quot;: &quot;book5&quot;, &quot;description&quot;: &quot;this is book5&quot; },
    { &quot;id&quot;: 6, &quot;title&quot;: &quot;book6&quot;, &quot;description&quot;: &quot;this is book6&quot; },
    { &quot;id&quot;: 7, &quot;title&quot;: &quot;book7&quot;, &quot;description&quot;: &quot;this is book7&quot; },
    { &quot;id&quot;: 8, &quot;title&quot;: &quot;book8&quot;, &quot;description&quot;: &quot;this is book8&quot; },
    { &quot;id&quot;: 9, &quot;title&quot;: &quot;book9&quot;, &quot;description&quot;: &quot;this is book9&quot; },
    {
      &quot;id&quot;: 10,
      &quot;title&quot;: &quot;book10&quot;,
      &quot;description&quot;: &quot;this is book10 which is last statically generated&quot;
    }
  ]
}</code></pre>
<p>그 다음 다음의 명령어를 이용하면 4000번포트에 API를 생성해 놓을 수 있다.</p>
<pre><code class="language-bash">json-server --watch db.json --port 4000</code></pre>
<p>이렇게 실행해놓으면 다음의 API가 자동으로 구성된다.</p>
<ul>
<li><code>http://localhost:4000/books</code> - 모든 books를 조회</li>
<li><code>http://localhost:4000/books/${id}</code> - id인 book을 조회</li>
</ul>
<h2 id="2-nextjs-구성하기">2. NextJS 구성하기</h2>
<ul>
<li><code>pages/books/index.tsx</code></li>
</ul>
<pre><code class="language-tsx">export default function Books({ data }: BooksProps) {
  return (
    &lt;&gt;
      {data?.map(({ id, title, description }) =&gt; (
        &lt;Link href={`/books/${id}`} key={id}&gt;
          &lt;div style={{ padding: &quot;10px&quot;, cursor: &quot;pointer&quot;, borderBottom: &quot;1px solid black&quot; }}&gt;
            &lt;span style={{ marginRight: &quot;10px&quot; }}&gt;{title}&lt;/span&gt;
            &lt;span&gt;{description}&lt;/span&gt;
          &lt;/div&gt;
        &lt;/Link&gt;
      ))}
    &lt;/&gt;
  );
}

export async function getStaticProps() {
  try {
    const { data } = await axios.get(&quot;http://localhost:4000/books&quot;);
    return {
      props: { data },
      revalidate: 6000,
    };
  } catch (err) {
    return {
      notFound: true,
    };
  }
}</code></pre>
<ul>
<li><code>pages/books/[id].tsx</code></li>
</ul>
<pre><code class="language-tsx">type GetSpecificBookResponse = {
  data: Book;
};

interface IdParams extends ParsedUrlQuery {
  id: string;
}

export default function SpecificBook({ data: { id, title, description } }: BookProps) {
  return (
    &lt;&gt;
      &lt;div key={id}&gt;
        &lt;span style={{ marginRight: &quot;10px&quot; }}&gt;{title}&lt;/span&gt;
        &lt;span&gt;{description}&lt;/span&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
}

export const getStaticPaths: GetStaticPaths = async () =&gt; {
  const { data } = await axios.get(&quot;http://localhost:4000/books&quot;);
  const paths = (data as Book[]).map(({ id }) =&gt; ({ params: { id: String(id) } }));
  return { paths, fallback: &quot;blocking&quot; };
};

export const getStaticProps: GetStaticProps = async (context) =&gt; {
  try {
    const { id } = context.params as IdParams;
    const { data } = await axios.get&lt;GetSpecificBookResponse&gt;(`http://localhost:4000/books/${id}`);
    return {
      props: { data },
      revalidate: 5,
    };
  } catch (err) {
    return {
      notFound: true,
    };
  }
};
</code></pre>
<p>위와 같이 구성한다면 사용자 진입 후 5초 뒤에 <code>revalidate</code>를 진행하여 해당 페이지의 업데이트된 내용을 제공한다. </p>
<h2 id="3-결과-확인하기">3. 결과 확인하기</h2>
<p>참고로 정적생성의 효과를 제대로 확인하려면 <code>development</code>로 실행하면 안되고 <code>production</code>으로 실행해야한다. 이는 <code>development</code> 모드로 실행할 경우 사용자가 진입할 때마다 <code>getStaticProps</code>가 실행되기 때문이다.</p>
<pre><code class="language-tsx">npm run build

npm run start</code></pre>
<p>실행하고 나서 데이터 베이스에 해당하는 <code>db.json</code> 내용을 변경했을때 변화가 반영되는지 아래화면을 통해 확인해 볼 수 있다. 특정 경로의 데이터가 업데이트 되면, 일정 시간 이후 업데이트 된 내용이 반영되는 모습이다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/24cf134c-3352-4ed4-b272-8ccb66f511a8/image.gif" alt=""></p>
<p>업데이트가 아니라 새로운 책 정보가 생성되는 경우, 일정 시간 이후 새로운 경로를 생성하는 것도 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/e6504dea-98bc-4196-9cf6-c5ef26458179/image.gif" alt=""></p>
<p>다만, 위에서 볼 수 있듯 갱신/생성 되는데 있어서 <code>revalidate</code> 옵션의 값만큼 딜레이가 발생하게 된다. 실제로 데이터가 업데이트 되었더라도, 일정시간동안은 사용자가 업데이트 되지 않은 내용을 확인하게 된다는 것이다. 또한 이러한 방식을 사용하게 될 경우, 실제 업데이트 여부와 관계없이 <code>revalidate</code> 가 이루어지기 때문에 다소 비효율적으로 프론트엔드 서버의 리소스가 이용되는 한계점이 있다. 이러한 한계점을 조금 극복할 수 있는 새로운 기능이 생겼는데 이는 <code>On-Demand Revaliation</code> 이다.</p>
<h2 id="🍏-on-demand-revalidation">🍏 On-demand Revalidation</h2>
<p>단어에서 추측해 볼 수 있듯, 필요할때에만 <code>Revalidation</code>을 진행하여 업데이트 시기를 효율적으로 설정할 수 있다.  그렇다면 어떻게 <code>Revalidation</code>이 필요한 때 인지를 프론트엔드서버가 알 수 있게 될까? 바로 NextJS 내 API를 이용해 실현해 낼 수 있다. 대략적인 과정은 다음과 같다.</p>
<ol>
<li>데이터베이스 내 데이터가 업데이트 된다.</li>
<li>프론트엔드에게 API 통신을 통해 <code>Revalidation</code> 이 필요함을 알린다.</li>
<li>이를 통해 업데이트가 필요한 페이지 컴포넌트들을 <code>Revalidate</code>를 한다.</li>
</ol>
<p>이를 직접 실습해보자.</p>
<blockquote>
<p>참고로 <strong>On-demand Revalidation</strong>은 안정화 버전이 <a href="mailto:next@12.2.0">next@12.2.0</a>부터 제공되니 package.json의 next 버전을 꼭 확인하자. 맞지 않는다면 12.2.0버전 이상으로 설치하는 것이 요구된다.</p>
</blockquote>
<ul>
<li><code>pages/api/revalidate-books.ts</code></li>
</ul>
<pre><code class="language-tsx">import { NextApiRequest, NextApiResponse } from &quot;next&quot;;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { method, query, body } = req;

  if (method !== &quot;POST&quot;) {
    return res.status(400).json({ error: &quot;Invalid HTTP method. Only POST method is allowed.&quot; });
  }

  // Unauthorized access as invalid token
  if (query.secret !== process.env.SECRET_REVALIDATE_TOKEN) {
    return res.status(401).json({ message: &quot;Invalid token&quot; });
  }

  try {
    if (!body) {
      res.status(400).send(&quot;Bad reqeust (no body)&quot;);
      return;
    }

    const idToRevalidate = body.id;

    if (idToRevalidate) {
      await res.revalidate(&quot;/books&quot;);
      await res.revalidate(`/books/${idToRevalidate}`);
      return res.json({ revalidated: true });
    }
  } catch (err) {
    return res.status(500).send(&quot;Error while revalidating&quot;);
  }
}</code></pre>
<p>pages 폴더 안에 api 폴더를 생성한 뒤 적절하게 api 경로를 생성한다. 위의 코드 내용을 대략적으로 설명하면,</p>
<ul>
<li>POST 요청이 아닌 경우 400에러를 발생시킨다.</li>
<li>query의 <code>secret</code>값이 환경 변수 <code>SECRET_REVALIDATE_TOKEN</code> 과 일치하지않으면 Unauthorized 401에러를 발생시킨다. (<code>.env</code> 파일에 <code>SECRET_REVALIDATE_TOKEN</code> 값을 적절히 설정해주어야한다.)</li>
<li>body 안에 <code>Id</code> 값이 없으면 400에러를 발생시킨다.</li>
<li>이러한 조건들을 통과한다면 <code>/books</code> 페이지와 <code>/books/${id}</code> 를 <code>revalidate</code> 시킨다.</li>
</ul>
<p>이를 제대로 확인해보려면, 위에서 설정했던 <code>revalidate</code> 값을 크게해야 한다(예를 들어 6000). 혹은 <code>revalidate</code> 옵션을 생략하는 것도 가능한데 이는 아래 사진처럼 직접 revalidate 시켜주지 않는한 사이트가 자동으로는 갱신이 이루어지지 않는 형태이다.</p>
<p>서버를 실행한뒤 데이터를 업데이트 하고 적절히 API요청을 보내주면 아래처럼 잘 갱신됨을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/87fdc78d-ee02-485c-9530-cd2350264b0a/image.gif" alt=""></p>
<p>API요청을 통해 <code>revalidate</code>가 필요함을 알림으로써 바로바로 갱신되는 것을 확인해 볼 수 있다. 다만, 이런식으로 수작업으로 API요청을 보내는 것은 매우 비효율적이기 때문에 <code>Webhook</code>을 이용하거나 백엔드에서 데이터 업데이트 이후 API요청을 보냄으로써 이를 자동화 할 수 있을 것으로 기대된다.</p>
<h2 id="🚩-출처와-참고자료">🚩 출처와 참고자료</h2>
<p><a href="https://github.com/tumetus/cooking-with-tuomo">https://github.com/tumetus/cooking-with-tuomo</a></p>
<p><a href="https://velog.io/@mskwon/next-js-static-generation-fallback">Next JS Static Generation - fallback</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSR & SSR]]></title>
            <link>https://velog.io/@seungchan__y/CSR-SSR</link>
            <guid>https://velog.io/@seungchan__y/CSR-SSR</guid>
            <pubDate>Fri, 17 Jun 2022 02:33:41 GMT</pubDate>
            <description><![CDATA[<p>이번 포스트에서는 프론트엔드의 대표적인 렌더링 방식인 <code>CSR(Client Side Renering)</code>과 <code>SSR(Server Side Rendering)</code>에 특징과 어떤 시기에 사용해야 가장 좋은지에 알아보겠다.</p>
<br/>

<h2 id="📄-페이지-구성-방식---spa-mpa">📄 페이지 구성 방식 - SPA, MPA</h2>
<p>본격적인 렌더링 방식을 알아보기 전에 웹어플리케이션을 페이지 구성 방식에 따라 크게 두가지로 나누어볼 수 있다. </p>
<h3 id="spasingle-page-application">SPA(Single Page Application)</h3>
<p>페이지 변경 없이 하나의 페이지 안에서 어플리케이션이 구동되는 방식 - ex) <code>React</code>, <code>Vue</code>, <code>Angular</code> 등</p>
<h3 id="mpamulti-page-application">MPA(Multi Page Application)</h3>
<p>사용자에 상호작용에 따라 여러개의 페이지가 제공되는 웹어플리케이션 - ex) <code>php</code>, <code>JSP</code> 등</p>
<p>이러한 웹어플리케이션은 각자의 특징때문에 과거에는 각자에게 필요한 렌더링 방식을 사용해야했다. 나중에 제대로 설명할 것이지만 렌더링 방식은 크게 3가지로 아래와 같은데,</p>
<ul>
<li><code>CSR - Client Side Rendering</code> : 클라이언트 측에서 렌더링을 하는 방식</li>
<li><code>SSR - Server Side Rendering</code> : 서버 측에서 렌더링을 하는 방식</li>
<li><code>SSG - Static Site Generation</code> : 빌드 타임에 미리 리소스들을 정적으로 생성하는 방식</li>
</ul>
<p>이러한 렌더링 방식과 결합시켜 아래와 같은 구성으로 웹어플리케이션을 구성하였다.</p>
<table>
<thead>
<tr>
<th></th>
<th>App 구성 #1</th>
<th>App 구성 #2</th>
</tr>
</thead>
<tbody><tr>
<td>페이지 구성 방식</td>
<td>SPA</td>
<td>MPA</td>
</tr>
<tr>
<td>렌더링 방식</td>
<td>CSR</td>
<td>SSR</td>
</tr>
</tbody></table>
<p>하지만, 최근들어 다양한 기술들과 프레임워크가 등장하면서부터는 <code>SPA</code> 상에서도 <code>SSR</code>을 구성할 수 있게되었다. 지금까지 웹어플리케이션의 페이지 구성방식에 대해 알아봤다면 이제부터는 <strong>정말로</strong> 렌더링 방식에 대해 알아볼 차례이다.</p>
<br/>
<br/>

<h2 id="🕹-csr의-동작-과정과-특징">🕹 CSR의 동작 과정과 특징</h2>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/c217f3a5-b57d-423b-bf30-4dd0025b8bef/image.png" alt=""></p>
<p><code>CSR</code>은 클라이언트 측에서 리소스를 다운로드 받아 직접 렌더링에 관여하는 방식인데 다음의 특징들을 가진다. </p>
<ul>
<li>JS 다운로드 후 브라우저 측에서 동적 DOM 생성하고 나서야 렌더링이 완료되기 때문에 초기 로딩속도가 매우 느리다. 다만, 모든 요소가 다운로드 된 후 렌더링이 진행되기에 화면 깜빡임은 없다.</li>
<li>첫 렌더링 이후에는 일부 요소 변경 시 해당되는 리소스만 요청하면 되기 때문에 초기 로딩을 제외한 나머지 시기의 구동 속도는 매우 빠르다.</li>
<li>빈 뼈대 HTML과 JS경로를 제공하는 것 외에는 하는일이 없어 서버 측 부하는 다소 적다.</li>
<li>웹 크롤러 입장에서는 인덱싱 과정에서 빈뼈대 HTML을 읽어드리기 때문에 적절한 색인 작업이 불가능하다.  이는 검색어 상단에 노출시키도록 하는 작업인  <strong>검색엔진최적화(SEO)</strong>에 불리하다.</li>
</ul>
<p>대표적으로 <code>React</code> 라이브러리를 이용한다면 아래와 같이 useEffect 훅을 이용해서 렌더링에 필요한 데이터를 백엔드로부터 불러오는 것이 대표적인 예이다.</p>
<pre><code class="language-jsx">useEffect(() =&gt; {
    axios
      .get(&#39;https://worldtimeapi.org/api/ip&#39;)
      .then((res) =&gt; {
        setDateTime(res.data.datetime);
      })
      .catch((error) =&gt; console.error(error));
  }, []);</code></pre>
<br/>
<br/>

<h2 id="📡-ssr의-동작-과정과-특징">📡 SSR의 동작 과정과 특징</h2>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/64ad5996-e38e-4099-a482-7dce3fcb0efe/image.png" alt=""></p>
<p><code>SSR</code>은 <code>CSR</code> 과 달리 서버가 HTML 뼈대와 JS 링크를 주는 것에 그치지 않고 직접 HTML페이지와 관련된 JS파일을 실행시켜 <strong>완성된 HTML</strong>과 브라우저 측에서 실행할 <strong>JS 파일링크</strong>를 넘기는 방식으로 렌더링을 진행한다. 이러한 <code>SSR</code> 방식은 다음의 특징들을 가진다.</p>
<ul>
<li><p>초기 렌더링시 HTML 뼈대만이 아니라 렌더링이 완료된 HTML과 이후 실행될 자바스크립트 코드가 제공되어 웹 크롤러 입장에서는 더 상세하게 인덱싱이 가능하여 <code>SEO(검색엔진 최적화)</code>에 매우 용이하다.</p>
</li>
<li><p>이미 렌더링이 완료된 상태로 페이지가 제공되기 때문에 초기 구동속도가 빠르다. 다만, HTML과 관련된 JS 로직들이 모두 연결되기 전까지는 사용자의 상호작용이 불가능하다. 이러한 경우 <code>TTV(Time To View) ≠ Time To Interact</code> 인 상황이라고도 한다.</p>
<ul>
<li>참고로 SSR 과정에서 서버측에서 먼저 렌더링한 HTML 파일이 전송된 후 이후에 클라이언트 측에서 실행할 JS가 다운로드되어야 웹페이지가 인터렉션이 가능해지는데 이러한 과정을 <code>Hydration</code> 이라고 한다.</li>
</ul>
</li>
<li><p>또한 서버가 매 요청시 필요한 리소스를 직접 만들어야하기 때문에 서버에 과부하가 생길 수 있다. 이러한 경우, 사용자가 페이지 최초 진입시 페이지가 보여지는데 오랜시간이 걸릴수도 있다.</p>
</li>
</ul>
<p>CSR과 SSR의 특징들을 정리하여 장점과 단점으로 분리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th></th>
<th>CSR</th>
<th>SSR</th>
</tr>
</thead>
<tbody><tr>
<td>장점</td>
<td>화면 깜빡임(≠ 화면전환)이 없음 <br/>초기 로딩 이후 구동 속도 빠름 <br/>TTV와 TTI 사이 간극이 없음 <br/> TTV와 TTI 사이 간극이 없음 <br/> 서버 부하 분산</td>
<td>초기 구동 속도가 빠름 <br/> SEO에 유리함</td>
</tr>
<tr>
<td>단점</td>
<td>초기 로딩 속도 느림 <br/> SEO에 불리함</td>
<td>화면 깜빡임이 존재 <br/> TTV와 TTI 사이 간극 존재 <br/> 서버 부하가 존재</td>
</tr>
</tbody></table>
<br/>
<br/>

<h2 id="🎱-csr의-단점-개선">🎱 CSR의 단점 개선</h2>
<p>위에서 살펴보았던 CSR 단점들은 다음의 해결책들을 통해 어느정도 해소될 수 있다.</p>
<ul>
<li><p><strong>초기 진입시 느린 로딩 속도 개선</strong></p>
<ul>
<li><p><code>Code-splitting</code> - SPA는 초기 실행시에 필요한 웹 리소스를 다운받는 특징이 있다. 이를 해소하기 위해 일부 리소스들에 대해서 <code>lazy loading</code> 을 적용하여 한번에 다운로드 받지 않도록 하는 방법이 있다. 리액트에서는 <code>React.lazy</code>와 <code>Suspense</code> 컴포넌트를 활용해 일부 리소스를 필요할 때 import 시킴으로써 최적화할 수 있다.</p>
<pre><code class="language-jsx">  const OtherComponent = React.lazy(() =&gt; import(&#39;./OtherComponent&#39;));

  function MyComponent() {
    return (
      &lt;div&gt;
        &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
          &lt;OtherComponent /&gt;
        &lt;/Suspense&gt;
      &lt;/div&gt;
    );
  }</code></pre>
<p>  <code>Code splitting</code> 에 대해서 자세하게 알아보고싶다면 아래를 참고하길 바란다.</p>
<p>  <a href="https://velog.io/@odini/Code-Splitting%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%94%8C%EB%A6%BF%ED%8C%85">Code-Splitting(코드 스플릿팅)</a></p>
</li>
<li><p><code>Tree-shaking</code> - 웹어플리케이션을 개발하다보면 다양한 모듈들을 설치해 사용하게 될텐데 이중에서 사용되지 않은 모듈들을 최대한 배제하는 기법이다. 나무를 흔들어서 죽은 나뭇잎들을 떨어뜨리는 데에서 유래 되었다고 한다.</p>
<pre><code class="language-jsx">  import * as util from &#39;../utilFile&#39;;</code></pre>
<p>  대표적으로 특정 파일에서 모듈을 위와 같은 방식으로 <code>import</code> 하게 될 경우, 사용하지 않는 모듈들도 같이 <code>import</code> 하게될텐데 이런식으로 불필요한 리소스가 누적되면 어플리케이션을 <code>build</code> 하면서 만들어지는 번들의 크기가 매우 커지게된다. 이로 인해 리소스를 로딩하는 시간을 지연시킬 수 있다. 이에 대한 자세한 설명은 아래를 참고하면 좋을것 같다.</p>
<p>  <a href="https://helloinyong.tistory.com/305">웹 성능 최적화를 위한 Tree Shaking 소개</a></p>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>SEO 개선</p>
<ul>
<li>페이지 미리 렌더링함으로써 검색엔진에서 해당 웹어플리케이션을 인덱싱하기 용이하기 하면 <code>SEO</code> 가 잘 안되던 기존의 문제점을 해결할 수 있다. 이처럼 페이지가 미리 렌더링되도록 해놓는 작업을 <code>Pre-rendering</code> 이라고 한다.</li>
</ul>
</li>
<li><p><strong>SSR/SSG를 부분 도입</strong></p>
<ul>
<li>렌더링의 부담을 모두 Client 측으로 돌리는 것이 아니라 일부 Server 측에게도 부여하는 방식이다. 이를 통해 초기 로딩 속도를 크게 감소시킬 수 있다. 또한 이를 도입하면 프리렌더링도 해줄 수 있어 SEO 개선도 가능하다. 이제부터는 SSR/SSG를 도입하는 방법에 대해서 알아보고자 한다.</li>
</ul>
</li>
</ul>
<br/>
<br/>

<h2 id="🥏-csr--ssrssg-도입-방법">🥏 CSR + SSR/SSG 도입 방법</h2>
<ul>
<li><p>프레임워크 없이 도입</p>
<p>  Express, Nest 등으로 백엔드 라이브러리를 이용해 별도의 서버를 운영하는 방법 : 진입장벽이 높고, 서버 환경 구성이 백엔드에 익숙하지 않은 프론트엔드 개발자에게는 다소 어렵다는 단점이 있어 실현되기 어렵다.</p>
</li>
</ul>
<ul>
<li><p>프레임워크를 이용해 도입</p>
<p>  프레임워크를 이용하면 쉽게 SSR/SSG를 도입할 수 있는데 프론트엔드 라이브러리 별로 도입할 수 있는 프레임워크는 다음과 같다.</p>
<ul>
<li><p><code>React</code> - <code>NextJS</code>, <code>Gatsby</code></p>
</li>
<li><p><code>Angular</code> - <code>Universal</code></p>
</li>
<li><p><code>Vue</code> - <code>Nuxt</code></p>
<p>대표적으로 <code>NextJS</code> 를 이용해 SSR를 할 경우 다음과 같은 방식으로 특정 페이지에 대하여 SSR을 진행할 수 있다. <code>getServerSideProps</code> 라는 함수가 프론트엔드 서버측에서 미리 실행되어 data를 패치한뒤 이를 페이지 컴포넌트의 props로 넘기는 방식이다.</p>
<pre><code class="language-jsx">function Page({ data }) {
// Render data...
}

// This gets called on every request
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch(`https://.../data`)
const data = await res.json()

// Pass data to the page via props
return { props: { data } }
}

export default Page</code></pre>
</li>
</ul>
</li>
</ul>
<p>다만, 프레임워크를 사용하게되면 당여히 프레임워크에 대한 이해가 필요하여 러닝커브가 존재하고 코드 복잡도도 상승하게 된다는 단점이 있다.</p>
<br/>
<br/>

<h2 id="🧐-csr-ssr-universalcsrssr의-선택-기준">🧐 CSR, SSR, Universal(CSR+SSR)의 선택 기준</h2>
<p>지금까지 렌더링하는 방식들의 특징들과 도입방법들에 대해서 알아보았다. 각각의 장단점들이 있으니 상황에 맞게 사용하는 것이 중요하다. 각 렌더링 방식에 대하여 사용해야될 경우를 특정해보면 다음과 같다.</p>
<ul>
<li><p><code>CSR</code>이 선호되는 경우</p>
<ul>
<li>유저와의 상호작용이 많은 경우</li>
<li>고객 개인 정보를 다뤄야 하는 페이지라 검색엔진 노출이 필요 없는 경우</li>
</ul>
</li>
<li><p><code>SSG/SSR</code>이 선호되는 경우</p>
<ul>
<li>회사 홈페이지 처럼 홍보가 필요해 검색어 입력시 상위페이지에 존재해야함(SEO 필요)</li>
<li>누구에게나 동일한 내용 노출</li>
<li>자주 업데이트가 이루어지는 페이지 ⇒ <code>SSR</code></li>
<li>자주 업데이트가 이루어지지 않는다 ⇒ <code>SSG</code></li>
</ul>
</li>
<li><p><code>Universal(CSR + SSR)</code>이 선호되는 경우</p>
<ul>
<li>사용자에 따라서 페이지가 달라짐 ⇒ SSR/CSR를 적절히 혼용</li>
<li>빠른 인터렉션이 중요한 경우 ⇒ 프리렌더링이 필요함 ⇒ <code>SSR</code></li>
<li>화면 깜빡임 X ⇒ 리소스를 다운받은 상태에서 렌더링이 이뤄져야함 ⇒ <code>CSR</code></li>
<li>검색어 상위 노출이 요구 ⇒ <code>SEO</code> 최적화 필요 ⇒ <code>SSR</code></li>
</ul>
</li>
</ul>
<p>위의 내용을 참고하되 정해진 정답은 없으니 프로젝트의 상황에 맞게 렌더링 방식을 선택하는 것이 좋겠다.</p>
<h2 id="🪢-같이-배워보면-좋은-키워드들">🪢 같이 배워보면 좋은 키워드들</h2>
<ul>
<li><code>Code-splitting</code></li>
<li><code>Tree-shaking</code></li>
<li><code>SEO(Search Engine Optimization)</code></li>
<li><code>Hydration</code></li>
</ul>
<h2 id="🚩-출처-및-참고자료">🚩 출처 및 참고자료</h2>
<p><a href="https://www.youtube.com/watch?v=YuqB8D6eCKE">[10분 테코톡] 🎨 신세한탄의 CSR&amp;SSR</a></p>
<p><a href="https://dzone.com/articles/client-side-vs-server-side-rendering-what-to-choos">Client-Side v/s Server-Side Rendering: What to Choose When? - DZone Web Dev</a></p>
<p><a href="https://dev.to/snickdx/understanding-rendering-in-web-apps-spa-vs-mpa-49ef">Understanding Rendering in Web Apps: SPA vs MPA</a></p>
<p><a href="https://velog.io/@odini/Code-Splitting%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%94%8C%EB%A6%BF%ED%8C%85"></a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Webstorm에서 ESLint + Prettier 적용하기]]></title>
            <link>https://velog.io/@seungchan__y/Webstorm%EC%97%90%EC%84%9C-ESLint-Prettier-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/Webstorm%EC%97%90%EC%84%9C-ESLint-Prettier-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 13 Jun 2022 07:29:50 GMT</pubDate>
            <description><![CDATA[<p>이번 포스트에서는 <strong>타입스크립트 기반의 NextJS</strong> 프로젝트에서 설정된 <code>ESLint</code>와 <code>Prettier</code>를 <code>Websotrm</code> 에디터에서도 적용할 수 있도록 할 것이다. <code>ESLint</code>와 <code>Prettier</code> 를 적용하는 방법은 해당 <a href="https://velog.io/@seungchan__y/NextJS-Typescript-Template">포스트</a>에서 확인해 볼 수 있다.</p>
<p>이제부터는 <code>Prettier</code>와 <code>ESLint</code>가 적용되었다는 가정하에 Webstorm 환경에서 적용방법을 보여주도록 하겠다.</p>
<h2 id="🦄-prettier-플러그인-설치-및-설정">🦄 Prettier 플러그인 설치 및 설정</h2>
<h3 id="1-prettier-플러그인-설치">1. Prettier 플러그인 설치</h3>
<p><code>Preferences &gt; Plugins</code> 선택 후 <code>Prettier</code>를 검색하여 설치한다. 맥북의 경우 왼쪽 상단 Webstorm을 선택하면 Preferences 메뉴를 확인할 수 있다. 아래는 설치가 끝난 후의 모습이다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/3cbbf52d-4982-4624-b7dc-850f8fb59919/image.png" alt=""></p>
<h3 id="2-prettier-경로-설정">2. Prettier 경로 설정</h3>
<p>Preferences 메뉴창에서 왼쪽 상단에 검색어로 <code>Prettier</code> 를 입력한 뒤 아래 사진 처럼 <code>언어 및 프레임 &gt; JavaScript &gt; Prettier</code> 를 선택한 뒤 Prettier 패키지의 경로를 현재 프로젝트의 <code>node_modules</code> 내 Prettier로 설정한다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/aaae21c1-541e-48c9-9a64-e49700535ac3/image.png" alt=""></p>
<h3 id="3--저장시-prettier-적용-설정">3.  저장시 Prettier 적용 설정</h3>
<p><code>Preferences &gt; Plugins</code> 선택 후 <code>File watchers</code>를 검색하여 설치한다. 기본적으로 이미 설치되어 있을 수도 있다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/7b8df226-cef8-4fb8-b6bc-60e698446a35/image.png" alt=""></p>
<h3 id=""></h3>
<p>이후 <code>Tools &gt; File watchers(파일 감시기)</code> 를 선택하여 Prettier 항목들을 추가해준다. 저장 시 Prettier가 적용되도록 하기 위함이다. 본 프로젝트에서는 Typescript 기반의 NextJS 프로젝트 이기 때문에, 3가지 확장자에 대해서 설정해줘야 한다. (<code>*.tsx</code>, <code>*.ts</code>, <code>*.js</code>)</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/f206439c-6cdb-4b32-97d9-64484f7ebe56/image.png" alt=""></p>
<p>파일 타입인 <code>TypeScript JSX</code>, <code>TypeScript</code>, <code>Javascript</code>   각각에 대해서 아래의 설정을 동일하게 적용해주면 된다. 파일타입 외에 설정해줘야 할 것은 첫번째로 <code>변경 시 실행할 도구</code> 중 다음의 항목들이다.</p>
<ul>
<li>프로그램 :  <code>$ProjectFileDir$/node_modules/.bin/prettier</code></li>
<li>인수 : <code>-write $FilePathRelativeToProjectRoot$</code></li>
<li>새로고침할 출력 경로 : <code>$FilePathRelativeToProjectRoot$</code></li>
<li>작업 디렉터리 : 프로젝트의 루트 디렉토리</li>
</ul>
<p>4번째 항목인 <strong>작업 디렉토리</strong> 항목은 현재 프로젝트의 루트 디렉토리를 적어주면 된다. 두번째로 <code>고급 옵션</code>  항목에는 <code>편집한 파일을 자동 저장하여 감시기 트리거</code> 항목을 체크해 주면 된다. 지금까지 말한 사항들을 적용하면 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/ff9a97c0-57eb-4d42-9d2d-b478c42750df/image.png" alt=""></p>
<p>아까 말한대로 위 작업을 동일하게 하되 <code>파일 타입</code>만 바꿔서 적용해 주면 된다.</p>
<p>마지막으로 <code>Code Cleanup(코드 정리)</code> 을 해주게되면 적용이 완료된다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/753c8a20-27a0-420e-a8a1-10ae1cd7671e/image.png" alt=""></p>
<p> 이제 각 파일의 저장버튼을 누르면 Prettier가 포맷팅 하는 것을 확인할 수 있을 것이다.</p>
<h2 id="👒-eslint-설정-및-적용">👒 ESLint 설정 및 적용</h2>
<p>다음은 <code>ESLint</code> 를 적용하는 방법이다.</p>
<p><code>Preferences</code> 를 선택한 뒤 왼쪽 상단에 검색 키워드로 <code>eslint</code> 를 입력해준다.  여기서 설정해줘야 항목들은 다음과 같다.</p>
<ul>
<li><code>수동 ESLint 설정</code>을 체크</li>
<li><code>ESLint 패키지</code>에 현재 프로젝트 <code>node_modules</code> 에 설치된 eslint 경로를 명시</li>
<li><code>작업 디렉터리</code>에 현재 프로젝트의 루트 경로를 명시</li>
<li>구성 파일 항목에 현재 프로젝트의 <code>.eslintrc.js</code>의 경로를 명시</li>
<li><code>저장 시 eslint —fix 실행</code> 옵션 체크</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/23862f4b-0c6b-49d5-8fa0-0dd9eb0f8eef/image.png" alt=""></p>
<p>항목들을 체크 하고 확인 버튼을 누른 뒤 다시 한번 <code>Code Cleanup(코드 정리)</code> 작업을 해준다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/15c3693e-6a51-4242-ad50-6fe2188ea880/image.png" alt=""></p>
<p>이렇게 ESLint 설정 역시 끝나게 된다.</p>
<h2 id="🚩-출처-및-참고자료">🚩 출처 및 참고자료</h2>
<p><a href="https://modipi.tistory.com/10">WebStorm 에서 ESLint + Prettier 자동 저장 설정 하기</a></p>
<p><a href="https://valuefactory.tistory.com/828">ESlint설치, 실행법, webstrom에서 ESlint적용</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS + Typescript Template]]></title>
            <link>https://velog.io/@seungchan__y/NextJS-Typescript-Template</link>
            <guid>https://velog.io/@seungchan__y/NextJS-Typescript-Template</guid>
            <pubDate>Sun, 12 Jun 2022 14:10:09 GMT</pubDate>
            <description><![CDATA[<h2 id="포스트-목적-소개">포스트 목적 소개</h2>
<p>프로젝트 개발을 위해서 NextJS + Typescript + Emotion 기반 템플릿을 만들면서 이에 대한 설명을 다룬 내용들을 공유하기 위해 이 포스트를 올립니다.</p>
<p>해당 포스트의 코드는 아래 <a href="https://github.com/Yangseungchan/nextts-emotion-template">레포지토리</a>를 참고해주세요.
<br/></p>
<h2 id="템플릿-기능-소개">템플릿 기능 소개</h2>
<p>본 템플릿은 아래의 기술 스택들을 포함하고 있습니다.</p>
<ul>
<li><code>NextJS</code></li>
<li><code>TypeScript</code></li>
<li><code>ESLint &amp; Prettier</code> (저장 시 포맷 자동 적용 + Google 스타일 가이드 반영)</li>
<li><code>Husky</code> (커밋 전 포맷 체크)</li>
<li><code>Emotion</code></li>
<li><code>Stylelint</code> - 본 포스트에서 다뤘으나 코드에는 반영되지 않았습니다.</li>
</ul>
<br/>

<h2 id="nextjs-app-생성">NextJS App 생성</h2>
<p><code>$ npx create-next-app code-like-google</code> 실행</p>
<p><code>$ cd code-like-google</code> 실행</p>
<p><code>$ code .</code> 실행</p>
<p>api 폴더 제거</p>
<p><em>app.js 제거</em></p>
<br/>

<h2 id="💎-typescript-적용google-ts-guide-적용">💎 TypeScript 적용(Google TS Guide 적용)</h2>
<p><code>tsconfig.json</code> 라는 이름의 빈 파일을 프로젝트에 생성</p>
<p><code>$ npm install --save-dev @types/react @types/node</code> 실행</p>
<p><code>$ npm run dev</code> 실행</p>
<p>자동으로 채워진 <code>tsconfig.json</code> 대신 아래의 내용으로 변경 (<strong>Google Style Guide 적용</strong>)</p>
<pre><code class="language-json">{
&quot;compilerOptions&quot;: {
    &quot;allowJs&quot;: true,
    &quot;allowUnreachableCode&quot;: false,
    &quot;allowUnusedLabels&quot;: false,
    &quot;declaration&quot;: true,
    &quot;esModuleInterop&quot;: true,
    &quot;forceConsistentCasingInFileNames&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;jsx&quot;: &quot;preserve&quot;,
    &quot;lib&quot;: [&quot;dom&quot;, &quot;dom.iterable&quot;, &quot;esnext&quot;],
    &quot;module&quot;: &quot;esnext&quot;,
    &quot;moduleResolution&quot;: &quot;node&quot;,
    &quot;noEmit&quot;: true,
    &quot;noFallthroughCasesInSwitch&quot;: true,
    &quot;noImplicitReturns&quot;: true,
    &quot;pretty&quot;: true,
    &quot;resolveJsonModule&quot;: true,
    &quot;skipLibCheck&quot;: true,
    &quot;sourceMap&quot;: true,
    &quot;strict&quot;: true,
    &quot;target&quot;: &quot;es2018&quot;
  },
  &quot;include&quot;: [&quot;next-env.d.ts&quot;, &quot;**/*.ts&quot;, &quot;**/*.tsx&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;]
}</code></pre>
<br/>

<h2 id="🟣-eslint-적용">🟣 ESLint 적용</h2>
<blockquote>
<p>ESLint statically analyzes your code to quickly find problems. Many problems ESLint finds can be automatically fixed. ESLint fixes are syntax-aware so you won&#39;t experience errors introduced by traditional find-and-replace algorithms.</p>
</blockquote>
<p>ESLint 에 설정해둔 사항이 알아서 수정되도록 이후 부분에서 설정할 것임</p>
<p><code>npm install eslint --save-dev</code> 실행</p>
<p><code>npx eslint --init</code></p>
<ul>
<li>아래와 같이 질문들에 대해 답변 선택 (<strong>Google 방식 대로 eslint 적용</strong>)</li>
</ul>
<pre><code class="language-bash">    # Interactive
    ? How would you like to use ESLint? ...
    &gt; To check syntax, find problems, and enforce code style

    ? What type of modules does your project use? ...
    &gt; JavaScript modules (import/export)

    ? Which framework does your project use? ...
    &gt; React

    ? Does your project use TypeScript? » Yes

    ? Where does your code run? ...
    √ Browser

    ? How would you like to define a style for your project? ...
    &gt; Use a popular style guide

    ? Which style guide do you want to follow? ...
    &gt; Google: https://github.com/google/eslint-config-google

    ? What format do you want your config file to be in? ...
    &gt; JavaScript

    eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest eslint-config-google@latest eslint@&gt;=5.16.0 @typescript-eslint/parser@latest
    ? Would you like to install them now with npm? » Yes</code></pre>
<p><code>.eslintrc.json</code>제거</p>
<ul>
<li><code>.eslintrc.js</code> 를 아래와 같이 수정</li>
</ul>
<pre><code class="language-jsx">    module.exports = {
      &quot;env&quot;: {
        &quot;browser&quot;: true,
        &quot;es2021&quot;: true,
      },
      &quot;extends&quot;: [
        &quot;plugin:react/recommended&quot;,
        &quot;google&quot;,
        &quot;next/core-web-vitals&quot;, // next에서 제공; 이게 없으면 모든 파일에 React 모듈을 import 해야됨
      ],
      &quot;parser&quot;: &quot;@typescript-eslint/parser&quot;,
      &quot;parserOptions&quot;: {
        &quot;ecmaFeatures&quot;: {
          &quot;jsx&quot;: true,
        },
        &quot;ecmaVersion&quot;: &quot;latest&quot;,
        &quot;sourceType&quot;: &quot;module&quot;,
      },
      &quot;plugins&quot;: [
        &quot;react&quot;,
        &quot;@typescript-eslint&quot;,
      ],
      &quot;rules&quot;: {
        &quot;require-jsdoc&quot;: &quot;off&quot;, // Google Guide로 인해 강제된 jsdoc 옵션 해제
        &quot;quotes&quot;: [&quot;error&quot;, &quot;double&quot;], // 문자열 들을 쌍따옴표로 감싸도록 강제
      },
    };</code></pre>
<p>여기서 <code>index.tsx</code> 상에서 에러가 생기고 있다면, <code>eslint</code>가 잘 적용되고 있는 것</p>
<p>이후 <code>index.tsx</code> 에서 세미콜론이나 쌍따옴표가 없어 발생하는 에러를 없앤 뒤, <code>$npm run dev</code> 실행 확인</p>
<br/>

<h2 id="🔴-prettier-적용하기">🔴 Prettier 적용하기</h2>
<blockquote>
<p>It removes all original styling and ensures that all outputted code conforms to a consistent style.</p>
</blockquote>
<p><code>$ npm install --save-dev --save-exact prettier</code> </p>
<p>여기서 <code>—save-exact</code>는 package.json에 정확한 버전(^4.0.0 말고 4.0.0 과 같은 형태로 설치)으로 prettier 설치 한다는 의미</p>
<p><code>$ npm install --save-dev eslint-config-prettier</code> 실행</p>
<p><code>eslint-config-prettier</code> 는 eslint와 prettier의 충돌을 막기 위해 설치</p>
<p><code>eslintrc.js</code> 중 <code>extends</code> 항목에 <code>prettier</code>를 추가하도록 다음과 같이 수정</p>
<pre><code class="language-jsx">extends: [&quot;plugin:react/recommended&quot;, &quot;google&quot;, &quot;prettier&quot;],</code></pre>
<p><code>.prettierrc</code> 파일 생성 후 다음의 내용으로 채우기(필요한 옵션들은 찾아서 알아서 추가하기)</p>
<pre><code class="language-jsx">{
    &quot;printWidth&quot;: 100,
    &quot;semi&quot;: true,
    &quot;singleQuote&quot;: false,
    &quot;tabWidth&quot;: 2,
    &quot;trailingComma&quot;: &quot;es5&quot;
}</code></pre>
<p><strong>⚠️  prettier 일부 옵션들은 eslint에서 설정한 옵션들과 충돌할 수 있으니 주의할 것.</strong></p>
<p>ex) Google Style에서는 semi 콜론을 강제하고 있으므로, prettier에서 <code>semi</code>를 <code>false</code> 로 할 경우 충돌 날 수 있음 </p>
<p><code>.prettierignore</code> 와 <code>.eslintignore</code>를 추가하고 아래의 내용을 적용하기</p>
<pre><code class="language-jsx">.next
next-env.d.ts
node_modules
yarn.lock
package-lock.json
public</code></pre>
<p>다음의 항목들은 <code>prettier</code>와 <code>eslint</code>의 규칙들을 적용 받지 않음.</p>
<br/>

<h2 id="🆚-vscode-extension-설치하기eslint-prettier">🆚 VSCode Extension 설치하기(ESLint, Prettier)</h2>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/63905b3f-0aff-46bb-8523-bd02684416c3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/c5862830-5875-40f8-9523-3729693d2979/image.png" alt=""></p>
<p>다음의 두 Extension을 설치 한 뒤, <strong>VSCode를 껏다가 다시 켜기</strong></p>
<br/>

<h2 id="📎--저장-시-format-자동-적용-설정하기">📎  저장 시 Format 자동 적용 설정하기</h2>
<p>프로젝트 루트 경로에 <code>.vscode</code> 추가하기</p>
<p><code>.vscode</code> 폴더 안에 <code>settings.json</code> 파일 생성 후 아래의 내용으로 작성</p>
<pre><code class="language-json">{
    &quot;editor.formatOnPaste&quot;: true,
    &quot;editor.formatOnSave&quot;: true, 
    &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;,
    &quot;editor.codeActionsOnSave&quot;: {
        &quot;source.fixAll.eslint&quot;: true,
        &quot;source.fixAll.format&quot;: true
    }
}</code></pre>
<p>잘 적용된다면 포맷 적용 안된 파일을 저장할 시 아래처럼 prettier 에서 설정한 포맷에 맞게 자동 수정됨</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/217a2248-161c-4477-a852-ae2ec4e6003e/image.gif" alt=""></p>
<br/>

<h2 id="🦊-husky">🦊 Husky</h2>
<p>Husky는 <code>commit</code> 이나 <code>push</code> 와 같은 Git 명령어를 시도할때 사용자가 설정한 규칙을 잘 지켰는지 확인하는 역할을 한다.</p>
<h3 id="husky-초기화">husky 초기화</h3>
<p>아래 예시의 설정에서는 다음의 사항들을 확인한다.</p>
<ul>
<li>Code 상에 <code>Prettier</code> 관련 경고 사항들은 없는지 확인한다.</li>
<li>Code 상에 <code>ESLint</code> 관련 경고 사항들은 없는지 확인한다.</li>
<li>Code 상에 <code>TypeScript</code>로 컴파일 시 관련 에러는 없는지 확인한다.</li>
<li><code>next build</code> 를 통해 프로젝트 빌드시 정상적으로 되는지 확인한다.</li>
</ul>
<p><code>package.json</code> 이 존재하는 곳에서 다음의 명령어를 실행하여 husky 관련 설정을 초기화 한다. </p>
<pre><code class="language-bash">npm install husky --save-dev</code></pre>
<p>이렇게하게 되면 <code>package.json</code>에는 아래의 명령어 스크립트가 생성될 것이다.</p>
<pre><code class="language-jsx">{
    &quot;scripts&quot; : {
        &quot;prepare&quot; : &quot;husky install&quot;,
        ...
    },
    ...
}</code></pre>
<p>이후엔 아래 명령어를 실행하여 <code>husky</code> 관련 초기화를 진행한다.</p>
<pre><code class="language-bash">npm run prepare</code></pre>
<br>

<h3 id="위와-같이-했을-때-오류가-발생한-경우">위와 같이 했을 때 <strong>오류가 발생한 경우</strong></h3>
<p>간혹 하나의 프로젝트에서 <code>frontend</code> 와 <code>backend</code> 로 나누어지고 <code>package.json</code>  역시 폴더별로 따로 관리되는 경우가 있다. </p>
<pre><code>```bash
│   
├── frontend                     # 프론트엔드 폴더
│   
├── backend                      # 백엔드 폴더
│
└── .git                         # git 관련 파일
```</code></pre><p>이 때문에 <code>/frontend</code> 나 <code>/backend</code> 디렉토리로 이동해 <code>npm install husky -—save-dev</code> 를 실행했을 것이다. 이때 에러가 발생하게 될텐데 이는 <code>husky</code> 가 <code>git</code>과 연동되기 위해서는 <code>.git</code> 폴더와 같은 디렉토리 안에서 초기화 되어야 하기 때문이다.  따라서 앞서 생성된 <code>package.json</code> 의 명령어를 다음의 명령어로 변경해 주어야 한다.</p>
<pre><code class="language-json">  {
    // .git가 있는 root 디렉토리로 이동후 실행
    &quot;prepare&quot;: &quot;cd ..&quot; &amp;&amp; husky install 
  },</code></pre>
<p>한편 위와 같이 <code>frontend</code> 와 <code>backend</code> 가 나뉘어져 있는 프로젝트의 경우라면 적용해야될 <code>husky</code> 작업이 다를 것이다. 이처럼 디렉토리 별로 다른 <code>husky</code> 를 생성하려면 한번 더 변형이 필요하다. 가령 <code>frontend</code> 만을 위한 <code>husky</code> 를 생성하려면 아래의 명령어로 바꾸면 된다.</p>
<pre><code class="language-json">{
    &quot;prepare&quot;: &quot;cd ..&quot; &amp;&amp; husky install frontend/.husky
}</code></pre>
<p>이는 <code>frontend</code> 의 폴더에만 <code>husky</code>를 초기화하겠다는 의미이다. 이렇게 수정해 준 뒤 <code>npm run prepare</code> 를 실행하면 정상적으로 초기화 된다.</p>
<h3 id="husky와-연동할-명령어-작성">Husky와 연동할 명령어 작성</h3>
<hr>
<p><code>husky</code> 와 연동해서 사용할 명령어를 생성하기 위해 <code>package.json</code>의 <code>scripts</code>를 아래와 같이 수정한다.</p>
<pre><code class="language-json">&quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;check-types&quot;: &quot;tsc --pretty --noEmit&quot;,
    &quot;check-format&quot;: &quot;prettier --check .&quot;,
    &quot;check-lint&quot;: &quot;eslint . --ext ts --ext tsx --ext js&quot;,
    &quot;format&quot;: &quot;prettier --write .&quot;,
    &quot;test-all&quot;: &quot;npm run check-format &amp;&amp; npm run check-lint &amp;&amp; npm run check-types &amp;&amp; npm run build&quot;,
    &quot;prepare&quot;: &quot;husky install&quot;
  },</code></pre>
<ul>
<li><p><strong><code>check-types</code></strong>  : 타입스크립트의 <strong><code>[tsc](https://www.typescriptlang.org/docs/handbook/compiler-options.html)</code></strong> CLI 명령어를 실행한 뒤 그 결과 존재하는 warnings/errors를 출력한다.</p>
</li>
<li><p><strong><code>check-format</code></strong>  : <code>Prettier</code>로 하여금 모든 파일에서 <code>.prettierrc</code> 에서 명시한 규칙들이 잘 적용되었는지 확인하도록 한다. (단, <code>.prettierignore</code> 에 명시된 파일 이름들은 제외)</p>
<p>  <img src="https://s3.us-west-2.amazonaws.com/secure.notion-static.com/992f1504-5a79-4263-b521-dd4f752393f3/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20221026%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20221026T125345Z&X-Amz-Expires=86400&X-Amz-Signature=f0724fbf9026f90190991a348c5aa75efc9b13f68e50538ae3a50cbe7d0236c2&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Untitled.png%22&x-id=GetObject" alt=""></p>
</li>
</ul>
<ul>
<li><strong><code>check-lint</code> :</strong> <code>ESLint</code> 로 하여금 Linting과 관련된 에러나 경고들을 표시한다.</li>
<li><strong><code>format</code></strong> : <code>Prettier</code> 로 하여금 자동으로 포맷팅을 적용하도록 함.</li>
<li><strong><code>test-all</code></strong> : 위에서 명시했던 모든 명령어들을 실행하도록 한다.</li>
<li><code>**prepare**</code> 는 husky 관련 설정들이 Git hooks에 연동하도록 함.</li>
</ul>
<br>

<h3 id="git-hook-작성하기">Git-hook 작성하기</h3>
<hr>
<p>이러한 명령어들이 <code>Git</code> 의 명령어와 연동하도록 작성하는 것이 <code>Hook</code>이다. 작성할 수 있는 Hook들은 아래와 같다.  </p>
<table>
<thead>
<tr>
<th>분류</th>
<th>훅</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>커밋 워크플로우 훅</td>
<td>pre-commit</td>
<td>commit을 실행하기 전에 실행</td>
</tr>
<tr>
<td></td>
<td>prepare-commit-msg</td>
<td>commit 메시지를 생성하고 편집기를 생성하기 전에 실행</td>
</tr>
<tr>
<td></td>
<td>commit-msg</td>
<td>commit 메시지를 완성한 후 commit을 완료하기 전에 실행</td>
</tr>
<tr>
<td></td>
<td>post-commit</td>
<td>commit을 완료한 후 실행</td>
</tr>
<tr>
<td>기타 훅</td>
<td>pre-rebase</td>
<td>rebase 하기 전에 실행</td>
</tr>
<tr>
<td></td>
<td>post-rewrite</td>
<td>git commit -amend, git rebase 와 같이 커밋을 변경하는 명령을 실행한 후 실행</td>
</tr>
<tr>
<td></td>
<td>post-merge</td>
<td>Merge가 끝나고 나서 실행</td>
</tr>
<tr>
<td></td>
<td>pre-push</td>
<td>git push 명령 실행 시 동작하며 리모트 정보를 업데이트하고 난 후 리모트로 데이터를 전송하기 전에 실행. push를 중단 시킬 수 있음.</td>
</tr>
</tbody></table>
<p>가령 <code>Commit</code> 이전에 특정 명령어들이 연동되도록 <code>pre-commit</code> 훅을 생성할 수 있다. 또한 <code>Push</code> 이전에 특정 명령어들이 실행되어 모두 만족할때에만 <code>push</code> 가 이뤄지도록 구성할 수 있다. </p>
<br>

<h3 id="pre-commit-훅-작성-예시">pre-commit 훅 작성 예시</h3>
<p><code>.husky</code> 폴더 안에 <code>pre-commit</code> 이란 파일을 생성한다.  이후 해당 파일에 연동할 명령어와 실패시 실행할 명령어들을 작성한다. 참고로 일반적인 명령어 형식은 아래와 같다.</p>
<pre><code class="language-bash">npm run format || # hook과 연동할 명령어
(
    echo &#39;Failure of formatting&#39; # 실패 시 띄울 메세지
    false;
)</code></pre>
<p>아래는 <code>pre-commit</code> 훅을 작성하는 예시이다. 아래와 같이 작성할 경우 <code>commit</code> 이루어지기 전에 <code>formatting</code>, <code>linting</code>, <code>type checking</code>, <code>build</code> 를 진행한다.</p>
<pre><code class="language-bash">#!/bin/sh
. &quot;$(dirname &quot;$0&quot;)/_/husky.sh&quot; # 이부분을 파일 앞에 붙여주는게 필요하다(아마도?)

echo &#39;🏗️👷 Styling, testing and building your project before committing&#39;

# Check Prettier standards
npm run check-format ||
(
    echo &#39;🤢🤮🤢🤮 Its F**KING RAW - Your styling looks disgusting. 🤢🤮🤢🤮
            Prettier Check Failed. Run npm run format, add changes and try commit again.&#39;;
    false;
)

# Check ESLint Standards
npm run check-lint ||
(
        echo &#39;😤🏀👋😤 Get that weak s**t out of here! 😤🏀👋😤 
                ESLint Check Failed. Make the required changes listed above, add changes and try to commit again.&#39;
        false; 
)

# Check tsconfig standards
npm run check-types ||
(
    echo &#39;🤡😂❌🤡 Failed Type check. 🤡😂❌🤡
            Are you seriously trying to write that? Make the changes required above.&#39;
    false;
)

# If everything passes... Now we can commit
echo &#39;🤔🤔🤔🤔... Alright... Code looks good to me... Trying to build now. 🤔🤔🤔🤔&#39;

npm run build ||
(
    echo &#39;❌👷🔨❌ Better call Bob... Because your build failed ❌👷🔨❌
            Next build failed: View the errors above to see why. 
    &#39;
    false;
)

# If everything passes... Now we can commit
echo &#39;✅✅✅✅ You win this time... I am committing this now. ✅✅✅✅&#39;</code></pre>
<br>

<h3 id="hook을-작성했는데-git-명령어와-연동이-안되는-경우"><code>Hook</code>을 작성했는데 <code>git</code> 명령어와 <strong>연동이 안되는 경우</strong></h3>
<p>이는 <code>pre-commit</code> 이라는 파일에 실행권한이 없어서 <code>git</code> 명령어가 실행됐음에도 같이 실행되지 않는 경우일 것이다. </p>
<pre><code class="language-bash">$ ls -al .husky

total 8
drwxr-xr-x   4 yangseungchan  staff  128 10 26 20:01 .
drwxr-xr-x  16 yangseungchan  staff  512 10 26 19:57 ..
drwxr-xr-x   4 yangseungchan  staff  128 10 26 19:57 _
-rw-r--r--   1 yangseungchan  staff  157 10 26 20:08 pre-commit # 실행 권한이 빠져있다.</code></pre>
<p>이를 해결하기 위해서는 아래의 명령어를 실행하여 <code>.husky</code> 폴더 내 파일들이 실행 권한을 갖도록 해야한다.</p>
<pre><code class="language-bash">$ chmod ug+x .husky/*</code></pre>
<p>실행 시 다음과 같은 결과가 나올 것이다.</p>
<p>1) 정상적인 경우</p>
<pre><code class="language-bash">$ git commit -m &quot;[Add] Husky setting&quot;
🏗️👷 Styling, testing and building your project before committing

&gt; next-ts-plate@0.1.0 check-format
&gt; prettier --check .

Checking formatting...
All matched files use Prettier code style!

&gt; next-ts-plate@0.1.0 check-lint
&gt; eslint . --ext ts --ext tsx --ext js

&gt; next-ts-plate@0.1.0 check-types
&gt; tsc --pretty --noEmit

🤔🤔🤔🤔... Alright... Code looks good to me... Trying to build now. 🤔🤔🤔🤔

&gt; next-ts-plate@0.1.0 build
&gt; next build

info  - Checking validity of types  
info  - Creating an optimized production build  
info  - Compiled successfully
info  - Collecting page data  
info  - Generating static pages (3/3)
info  - Finalizing page optimization  

Page                                       Size     First Load JS
┌ ○ /                                      6.26 kB        82.1 kB
├   └ css/955229c28454e75a.css             668 B
└ ○ /404                                   193 B            76 kB
+ First Load JS shared by all              75.8 kB
  ├ chunks/framework-1f10003e17636e37.js   45 kB
  ├ chunks/main-fc7d2f0e2098927e.js        28.7 kB
  ├ chunks/pages/_app-02d0f4839caa4a8e.js  1.36 kB
  └ chunks/webpack-69bfa6990bb9e155.js     769 B

○  (Static)  automatically rendered as static HTML (uses no initial props)

✅✅✅✅ You win this time... I am committing this now. ✅✅✅✅
[main e85d3dd] [Add] Husky setting
 10 files changed, 112 insertions(+), 58 deletions(-)
 rewrite .eslintrc.js (87%)
 create mode 100755 .husky/pre-commit</code></pre>
<p>2) 에러가 발생하는 경우</p>
<pre><code class="language-bash">🏗️👷 Styling, testing and building your project before committing

&gt; next-ts-plate@0.1.0 check-format
&gt; prettier --check .

Checking formatting...
All matched files use Prettier code style!

&gt; next-ts-plate@0.1.0 check-lint
&gt; eslint . --ext ts --ext tsx --ext js

/Users/yangseungchan/Desktop/SCG/next-ts-plate/pages/index.tsx
  2:8  error  &#39;styles&#39; is defined but never used  no-unused-vars

✖ 1 problem (1 error, 0 warnings)

😤🏀👋😤 Get that weak s**t out of here! 😤🏀👋😤 
                ESLint Check Failed. Make the required changes listed above, add changes and try to commit again.
husky - pre-commit hook exited with code 1 (error)</code></pre>
<br/>

<h2 id="😃-emotion-적용swc-기반--_apptsx-생성">😃 Emotion 적용(SWC 기반) + _app.tsx 생성</h2>
<h3 id="1-emotion-설치-및-global-css-적용하기">1. Emotion 설치 및 Global CSS 적용하기</h3>
<p><code>Emotion</code>은 css-in-js기반으로 css를 적용하는 라이브러리 입니다. 이전에는 SSR이 지원되지 않아 <code>babel</code> 설정이 필요했지만, v10부터 SSR이 지원되어 설정이 따로 필요없어졌다.</p>
<blockquote>
<p>To use emotion’s SSR with Next.js you need a custom <code>Document</code> component in <code>pages/_document.js</code> that renders the styles and inserts them into the <code>&lt;head&gt;</code> .
<strong><a href="https://github.com/vercel/next.js/tree/master/examples/with-emotion-vanilla">An example of Next.js with emotion can be found in the Next.js repo</a></strong>
This only applies if you’re using vanilla Emotion or a version of Emotion prior to v10. For v10 and above, SSR just works in Next.js.</p>
</blockquote>
<p>아래의 명령어를 실행하여<code>emotion</code> 관련 모듈을 설치한다.</p>
<pre><code class="language-bash">npm i --save @emotion/styled @emotion/react</code></pre>
<p>emotion연동을 위해 <code>next.config.js</code> 을 아래와 같이 수정한다.</p>
<pre><code class="language-jsx">const nextConfig = {
  reactStrictMode: true,
  compiler: {
    emotion: true,
  },
};

module.exports = nextConfig;</code></pre>
<p><code>Emotion</code> 은 전역스타일링을 지원한다. 아래의 코드를 작성하여 <code>Emotion</code> 을 이용해 전역 스타일링 컴포넌트를 생성해 줄 수 있다. 전역 스타일도 적용하고, css-in-js 방식으로 적용하기 위해 아래와 같이 수정한다.</p>
<ul>
<li>styles/global.tsx</li>
</ul>
<pre><code class="language-tsx">import { css, Global } from &quot;@emotion/react&quot;;

export const globalStyles = (
  &lt;Global
    styles={css`
      html,
      body {
        padding: 0;
        margin: 0;
        font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
          Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
      }

      a {
        color: inherit;
        text-decoration: none;
      }

      * {
        box-sizing: border-box;
      }
    `}
  /&gt;
);</code></pre>
<ul>
<li>styles/home.tsx</li>
</ul>
<pre><code class="language-tsx">import styled from &quot;@emotion/styled&quot;;

export const Container = styled.div`
  min-height: 100vh;
  padding: 0 0.5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
`;</code></pre>
<ul>
<li>styles내의 css 파일들은 제거하기</li>
<li>pages/_app.tsx</li>
</ul>
<pre><code class="language-tsx">import createCache from &quot;@emotion/cache&quot;;
import { CacheProvider } from &quot;@emotion/react&quot;;
import { AppProps } from &quot;next/app&quot;;

import { globalStyles } from &quot;../styles/global&quot;;

const cache = createCache({ key: &quot;next&quot; });

const App = ({ Component, pageProps }: AppProps) =&gt; (
  &lt;CacheProvider value={cache}&gt;
    {globalStyles}
    &lt;Component {...pageProps} /&gt;
  &lt;/CacheProvider&gt;
);

export default App;</code></pre>
<ul>
<li>pages/index.tsx</li>
</ul>
<pre><code class="language-tsx">import Head from &quot;next/head&quot;;
import { Container } from &quot;../styles/home&quot;;

export default function Home() {
  return (
    &lt;Container&gt;
      &lt;Head&gt;
        &lt;title&gt;Create Next App&lt;/title&gt;
        &lt;meta name=&quot;description&quot; content=&quot;Generated by create next app&quot; /&gt;
        &lt;link rel=&quot;icon&quot; href=&quot;/favicon.ico&quot; /&gt;
      &lt;/Head&gt;
      Next TypeScript Template
    &lt;/Container&gt;
  );
}</code></pre>
<br/>

<h3 id="2-emotion-기반-theme-주입하기">2. Emotion 기반 Theme 주입하기</h3>
<p>이외에도 일부 css 공통 요소들을 Theme화 할 수도 있다.  아래의 코드를 작성하여 Theme를 생성해주도록 한다.</p>
<ul>
<li>styles/theme.tsx</li>
</ul>
<pre><code class="language-tsx">import { Theme } from &quot;@emotion/react&quot;;

const theme: Theme = {
  colors: {
    primiary: &quot;hotpink&quot;,
  },
};

export default theme;</code></pre>
<p>_app.tsx가 <strong>프로젝트의 진입점</strong>이기 때문에  Theme를 하위 컴포넌트나 페이지에서 사용할 수 있도록 Provider를 부여하기 위해 아래와 같이 해당 파일을 수정해준다.</p>
<ul>
<li>pages/_app.tsx</li>
</ul>
<pre><code class="language-tsx">import createCache from &quot;@emotion/cache&quot;;
import { CacheProvider, ThemeProvider } from &quot;@emotion/react&quot;;
import { AppProps } from &quot;next/app&quot;;

import { globalStyles } from &quot;../styles/global&quot;;
import theme from &quot;../styles/theme&quot;; // 추가

const cache = createCache({ key: &quot;next&quot; });

const App = ({ Component, pageProps }: AppProps) =&gt; (
  &lt;CacheProvider value={cache}&gt;
    &lt;ThemeProvider theme={theme}&gt; 
      {globalStyles}
      &lt;Component {...pageProps} /&gt;
    &lt;/ThemeProvider&gt;
  &lt;/CacheProvider&gt;
);

export default App;</code></pre>
<p>주요 색상이 <code>hotpink</code> 인 테마를 제공하였으니 적용을 해보도록 할 것이다. </p>
<p><code>Title</code> 이라는 컴포넌트를 생성해 주어 Theme를 적용해 볼 것이다. 컴포넌트를 만들때에는 컴포넌트의 리액트 로직과 CSS로직을 코드상에서 분리시켜 놓기 위해 아래와 같은 패턴으로 컴포넌트를 만들어줄 것이다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/ab5556f7-04ce-4a3a-84b8-dd038c10043f/image.png" alt=""></p>
<p><code>index.tsx</code>에는 리액트 로직의 내용이 들어가게 되고 <code>styles.tsx</code>에는 css 로직들이 들어가게 될것이다.</p>
<p>기존에 <code>pages/index.tsx</code>  에서 <strong>Next TypeScript Template</strong> 라는 글자를 <code>Title</code> 라는 컴포넌트로 만들것이다. 아래와 같이 코드를 추가해준다.</p>
<ul>
<li>styles/theme.tsx - Theme 선언하기</li>
</ul>
<pre><code class="language-tsx">import { Theme } from &quot;@emotion/react&quot;;

const theme: Theme = {
  colors: {
    primary: &quot;hotpink&quot;,
  },
};

export default theme;</code></pre>
<p>다만, 현재 프로젝트는 타입스크립트 기반이므로, <strong>Theme 객체의 타입</strong>을 사용자가 정의해주어야 한다. 그렇지 않으면 Theme에 어떤 속성이 있는지 타입스크립트는 이해하지 못한다.  따라서, 다음과 같이 프로젝트 루트 디렉토리에 Theme 객체에 어떤 속성이 있는지 부여해준다.</p>
<ul>
<li>emotion.d.ts</li>
</ul>
<pre><code class="language-tsx">/* Theme 타입 설정 파일 */

import &quot;@emotion/react&quot;;

declare module &quot;@emotion/react&quot; {
  export interface Theme {
    colors: {
      primary: string;
    };
  }
}</code></pre>
<p> Theme에 속성을 추가적으로 부여하고 싶다면, <code>emotion.d.ts</code> 파일과 <code>styles/theme.tsx</code> 에 모두 해당 속성을 추가해 주어야 한다.</p>
<p>이제 선언된 테마를 적용해보기 위해 Title이라는 컴포넌트를 완성시키고 시작페이지에 렌더링할 것 이다. 아래의 코드를 따라해보자.</p>
<ul>
<li>components/Title/styles.tsx - 색상이 primary인 h1 컴포넌트</li>
</ul>
<pre><code class="language-tsx">import styled from &quot;@emotion/styled&quot;;

export const TitleText = styled.h1`
  color: ${(props) =&gt; props.theme.colors.primary};
`;</code></pre>
<ul>
<li>components/Title/index.tsx - title이 prop인 컴포넌트</li>
</ul>
<pre><code class="language-tsx">import { TitleText } from &quot;./styles&quot;;

interface TitleProps {
  title: string;
}

const Title = ({ title }: TitleProps) =&gt; {
  return &lt;TitleText&gt;{title}&lt;/TitleText&gt;;
};

export default Title;</code></pre>
<ul>
<li>pages/index.tsx - Title 컴포넌트 렌더링</li>
</ul>
<pre><code class="language-tsx">import Head from &quot;next/head&quot;;
import Title from &quot;../components/Title&quot;;
import { Container } from &quot;../styles/home&quot;;

export default function Home() {
  return (
    &lt;Container&gt;
      &lt;Head&gt;
        &lt;title&gt;Create Next App&lt;/title&gt;
        &lt;meta name=&quot;description&quot; content=&quot;Generated by create next app&quot; /&gt;
        &lt;link rel=&quot;icon&quot; href=&quot;/favicon.ico&quot; /&gt;
      &lt;/Head&gt;
      &lt;Title title=&quot;NextJS TypeScript Template&quot; /&gt;
    &lt;/Container&gt;
  );
}</code></pre>
<p>결과는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/2fbdc875-12e7-463b-ad6e-4bf480168b56/image.png" alt=""></p>
<p>또한 emotion의 css props 기능을 사용하려면 <code>tsconfig.json</code>을 아래와같이 수정해주어야 한다.</p>
<pre><code>{
  &quot;compilerOptions&quot;: {
    ...
    &quot;jsxImportSource&quot;: &quot;@emotion/react&quot;,
    ...
  },
  &quot;include&quot;: [&quot;next-env.d.ts&quot;, &quot;**/*.ts&quot;, &quot;**/*.tsx&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;]
}
</code></pre><br/>

<h2 id="🌟stylelint-적용하기---권장x-코드에-없음">🌟Stylelint 적용하기 - 권장x, 코드에 없음</h2>
<blockquote>
<p>Stylelint를 emotion과 같은 css-in-js 환경에서 사용하기 위해 사용되는 <a href="https://github.com/stylelint/postcss-css-in-js/issues/225">@stylelint/postcss-css-in-js가 depreceated 되면서</a>, <strong>css-in-js에서는 Stylelint를 사용하기 매우 까다로워졌다</strong>. 특히나, 자동 Fix 기능이 적용되지 않기 때문에, css에 통일된 규칙을 적용하고 싶은 경우에만 추가하는 것이 좋겠다.</p>
</blockquote>
<p>앞서 <a href="https://www.notion.so/VSCode-Extension-ESLint-Prettier-b876d9b0c64f4930877465afe6bd96a8">🆚 VSCode Extension 설치하기(ESLint, Prettier)</a> 에서 포맷팅을 적용하였지만, 이는 타입스크립트를 위한 포맷터라 내재된 emtion에 내재된 css코드는 포맷이 일정하지 않을 수 있다. 따라서 css코드의 포맷팅이 따로 필요하다.  <code>Stylelint</code>는 css 정적 분석 도구 중 하나로 css 코드의 포맷팅을 지원한다. </p>
<p>우선 필요한 모듈들을 아래의 명령어를 실행해 설치한다.</p>
<pre><code class="language-bash">npm install --save-dev stylelint stylelint-config-standard stylelint-config-prettier postcss-syntax @stylelint/postcss-css-in-js</code></pre>
<p>설치한 모듈들이 어떤 기능을 하는지 살펴보면 다음과 같다.</p>
<ul>
<li><code>stylelint</code> : stylelint를 사용하기 위한 기본 모듈</li>
<li><code>stylelint-config-standard</code> : stylelint에서 제시한 규칙들 중 일부를 모아 적용해 놓은 써드파티 모듈이다. 기본적으로 설정되있는 규칙들은 <a href="https://github.com/stylelint/stylelint-config-standard/blob/main/index.js">이곳</a>을 참고할 것.</li>
<li><code>stylelint-config-prettier</code> : 포맷팅 시 stylelint 규칙과 prettier 규칙이 충돌하지 않도록 방지해주는 모듈이다.</li>
<li><code>postcss-syntax</code>, <code>@stylelint/postcss-css-in-js</code> : css-in-js 모듈에서 stylelint가 작동하도록 하는 모듈이다.</li>
</ul>
<p>이후에는 루트 디렉토리에 .stylelintrc.js 라는 파일을 생성해 다음의 내용을 채워준다.</p>
<pre><code class="language-bash">module.exports = {
  extends: [&quot;stylelint-config-standard&quot;, &quot;stylelint-config-prettier&quot;],
  overrides: [
    {
      files: [&quot;**/*.tsx&quot;],
      customSyntax: &quot;@stylelint/postcss-css-in-js&quot;,
    },
  ],
};</code></pre>
<p>이후 작동되는지 확인하기 위해 package.json의 script를 다음과 같이 수정하고 실행해본다.</p>
<pre><code class="language-bash">...
&quot;scripts&quot;: {
    &quot;lint-css&quot;: &quot;stylelint --ignore-path .gitignore &#39;**/*.(css|tsx)&#39;&quot;,
}
...</code></pre>
<p>이처럼 stylelint에 의해서 css 코드 내에 규칙에 맞지 않는 것들을 지적해 내는 것을 확인할 수 있다. 다만, <code>function-no-unknown</code> 의 규칙의 경우 emotion의 코드방식과 충돌하는 것이므로 불필요한 규칙에 해당한다. 이처럼, 자신에게 불필요한 규칙은 없애거나 필요한 규칙은 추가해줄 수 있다. 다음과 같이 말이다.</p>
<ul>
<li>.stylelintrc.js</li>
</ul>
<pre><code class="language-bash">module.exports = {
  extends: [&quot;stylelint-config-standard&quot;, &quot;stylelint-config-prettier&quot;],
  overrides: [
    {
      files: [&quot;**/*.tsx&quot;],
      customSyntax: &quot;@stylelint/postcss-css-in-js&quot;,
    },
  ],
  rules: {
    &quot;function-no-unknown&quot;: null, // emotion styled와 충돌나서 배제
    &quot;color-hex-length&quot;: &quot;long&quot;, // 16진수 색상에 대해 표기법 지정
    &quot;unit-allowed-list&quot;: [&quot;em&quot;, &quot;rem&quot;, &quot;vh&quot;, &quot;vw&quot;], // 사용가능한 단위 특정하기
  },
};</code></pre>
<p>참고로, stylelint가 알아서 잘못된 내용들을 수정하기 원할텐데 이 경우 다음의 명령어를 사용하면 된다.</p>
<pre><code class="language-bash">npm run lint-css --fix</code></pre>
<p>다만, AutoFix 기능은 극히 일부 규칙(<code>Autofixable</code>로 명시된 규칙)만 적용된다고 한다. 그래서 자동으로 고쳐주는 부분은 eslint나 prettier 만큼은 효과적이지 못하다. <a href="https://stylelint.io/user-guide/rules/list">공식 문서</a>를 참고해 규칙들을 확인하며 적절히 커스텀하여 사용해야 한다.</p>
<br/>

<h2 id="🗂-폴더-구조-정리-및-import-개선">🗂 폴더 구조 정리 및 import 개선</h2>
<p>현재 프로젝트의 디렉토리 구조는 다음과 같이 되어있다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/fee0f255-1181-45fb-8909-61b1d2367a05/image.png" alt=""></p>
<p>이러한 상황에서 특정 컴포넌트나 파일을 import 해야하면 다음과 같은 상대경로를 작성해야 한다.</p>
<pre><code class="language-tsx">import Head from &quot;next/head&quot;;
import Title from &quot;../components/Title&quot;;
import { Container } from &quot;../styles/home&quot;;</code></pre>
<p>여기서 개선할 여지가 있는 두가지 문제점이 있다.</p>
<p><strong>1. 프로젝트 구조가 너무 복잡하여 가독성이 떨어진다.</strong></p>
<p><strong>2. import 시 상대경로를 사용하여 역시 가독성이 떨어진다. (현재는 단순하지만 경로가 복잡해지면, import경로가 굉장히 보기 싫어진다)</strong></p>
<p>이를 해결하기 위해 디렉토리 구조를 다음과 같이 수정할 것이다. </p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/1fbec01e-a3e6-4fb7-b7e9-b11bec364762/image.png" alt=""></p>
<p>수정한 디렉토리 구조를 바탕으로, import 시 상대경로 대신 절대경로를 사용하도록 다음과 같이 <code>tsconfig.json</code>을 수정해준다.</p>
<pre><code class="language-tsx">{
  &quot;compilerOptions&quot;: {
    ...
    &quot;baseUrl&quot;: &quot;src&quot;,
    ...
  },
  &quot;include&quot;: [&quot;next-env.d.ts&quot;, &quot;**/*.ts&quot;, &quot;**/*.tsx&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;]
}</code></pre>
<p>이렇게 수정하게 되면 import 경로가 얼마나 복잡하든 절대경로를 사용하여 다소 굉장히 깔끔해진다.</p>
<pre><code class="language-tsx">
/* 적용 전
import Head from &quot;next/head&quot;;
import Title from &quot;../components/Title&quot;;
import { Container } from &quot;../styles/home&quot;;
*/

// 적용 후
import Head from &quot;next/head&quot;;
import Title from &quot;@components/Title&quot;;
import { Container } from &quot;@styles/home&quot;;</code></pre>
<br/>

<h2 id="🚩-출처-및-참고자료">🚩 출처 및 참고자료</h2>
<p><a href="https://blog.jarrodwatts.com/nextjs-eslint-prettier-husky">NextJS Style Guide: Prettier, ESLint, Husky and VS Code</a></p>
<p><a href="https://github.com/vercel/next.js/tree/canary/examples/with-emotion-swc">NextJS with emotion with swc</a></p>
<p><a href="https://dev-yakuza.posstree.com/ko/linter/stylelint">Linter StyleLint</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Execution Context (실행 컨텍스트)]]></title>
            <link>https://velog.io/@seungchan__y/Execution-Context-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@seungchan__y/Execution-Context-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Thu, 09 Jun 2022 12:59:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 포스트는 <a href="https://youtu.be/EWfujNzSUmw">[10분 테코톡] 💙 하루의 실행 컨텍스트 - YouTube</a> 영상을 참고 및 요약하여 만들어졌습니다. 포스트 대신 위의 영상을 보시면 내용을 더 풍부하게 이해하실 수 있습니다.</p>
</blockquote>
<h2 id="execution-context과-lexical-environment">Execution Context과 Lexical Environment</h2>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/ddb2416a-4640-470c-a0d7-264b605c441d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/b33aa7ed-250a-4786-ace6-b092dc321338/image.png" alt=""></p>
<ol>
<li><p>Environment <strong>Record</strong>(환경 레코드) - 식별자와 식별자에 바인딩된 값을 기록. 여기서 <strong>식별자</strong>(Identifier)란 코드에서의 변수나 함수를 나타낸다.</p>
</li>
<li><p><strong>Outer</strong> Environment Reference(외부 환경 참조) - 바깥(이전) Lexcial Environment를 가리킨다.</p>
</li>
</ol>
<br/>
<br/>


<h2 id="🟡-environment-record-feat-호이스팅">🟡 Environment Record (feat. 호이스팅)</h2>
<p>Lexical Environment의 구성 요소 중 하나인 Environment Record를 알아보면서 함께 호이스팅에 대해서도 알아보고자 한다.</p>
<blockquote>
<p><strong>Hoisiting</strong> (호이스팅)</p>
<p>자바스크립트 엔진에 의해 코드에서 선언문이 마치 최상단으로 끌어올려진 듯한 현상</p>
</blockquote>
<p>Environment Record와 호이스팅에 대해 이해 하기 전에 잠~깐 자바스크립트 엔진이 코드를 어떻게 실행하는지 알아보고 넘어갈 것 이다.</p>
<blockquote>
<p>🧐 JS 엔진의 코드 실행 단계</p>
<ol>
<li><strong>생성 단계</strong>(Creation Phase) : 선언문만 우선 실행해서 Environment Record에 기록을 하는 단계</li>
<li><strong>실행 단계</strong>(Execution Phase) : 선언문 외 다른 코드들도 순차적으로 실행하는 단계. 이때에는 Environment Record를 참조하거나 업데이트 한다.</li>
</ol>
</blockquote>
<p>JS엔진의 실행 단계를 알아보았으니 이제 호이스팅에 대해 이해해 볼 수 있다. 호이스팅은 크게 3가지로 나뉘는데 첫번째는 Variable의 호이스팅, 두번째는 function expression, 세번째는 함수 선언의 호이스팅 이다. 각 호이스팅 별로 <strong>JS 엔진의 실행 단계</strong>와 함께 <strong>Environment Record</strong>가 어떻게 바뀌어 나가는지 살펴볼 것이다.</p>
<h3 id="1-variable-호이스팅">1. Variable 호이스팅</h3>
<h4 id="case-1-var-호이스팅---var로-선언된-변수의-호이스팅">Case 1) var 호이스팅 - var로 선언된 변수의 호이스팅</h4>
<h5 id="phase-1-생성-단계">Phase 1) 생성 단계</h5>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/476d03d1-2e50-4386-bbc7-b806c7304acc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/5987e9b6-9026-4442-b76b-cb059580c78d/image.png" alt=""></p>
<ol>
<li><p>Call Stack에 전역 환경 상에서의 Environment Context를 생성</p>
</li>
<li><p>선언문들만을 실행하여, Environment Record 만을 저장해둔다. 위의 예시에서는 TVChannel 이라고 선언된 변수를 읽어들인다. 이 경우 var형 변수이므로 undefined로 값을 초기화 해둔다.</p>
</li>
</ol>
<h5 id="phase-2-실행-단계">Phase 2) 실행 단계</h5>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/e03fe7d3-b726-4443-af4e-e820677c4fc3/image.png" alt=""></p>
<ol>
<li><p>선언문을 제외한 나머지 코드들을 읽어들인다. 첫번째 줄은 자바스크립트 엔진이 Call Stack을 참조하더라도 TVChannel이 &quot;undefined&quot;로 저장되어 있고 이를 출력한다.</p>
</li>
<li><p>두번째 줄은 TVChannel에 &quot;Netflix&quot;값을 할당하는 코드이다. 이를 위해 전역 Context내의 Record속에 저장된 TVChannel의 값을 &quot;Netflix&quot;로 변경해준다.</p>
</li>
<li><p>세번째 줄은 TVChannel을 출력하는 코드이므로 전역 Context 내의 Record를 참조하여 TVChannel의 값을 확인하고 이를 출력한다.</p>
</li>
</ol>
<h4 id="case-2-const혹은-let-호이스팅---const혹은-let로-선언된-변수의-호이스팅">Case 2) const(혹은 let) 호이스팅 - const(혹은 let)로 선언된 변수의 호이스팅</h4>
<pre><code class="language-js">console.log(TVChannel);

const TVChannel = &quot;Netflix&quot;;

console.log(TVChannel);</code></pre>
<h5 id="phase-1-생성-단계-1">Phase 1) 생성 단계</h5>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/96378f21-390b-4043-a6a3-1a597751e62e/image.png" alt=""></p>
<ol>
<li>TVChannel이란 변수의 선언문을 실행한다. 이때 const형 변수이기에 전역 Context 내의 Record에는 TVChannel을 기록해두기는 하지만 var형인 경우와는 다르게 바로 초기화 되지 않는다.</li>
</ol>
<h5 id="phase-2-실행-단계-1">Phase 2) 실행 단계</h5>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/cbbaded8-80a2-4607-b2ee-f95731bcadfa/image.png" alt=""></p>
<p>선언문을 제외한 나머지 코드들을 실행한다. TVChannel이란 변수를 출력하려고 시도하지만 Context내의 Record를 확인했을때 TVChannel은 어떠한 값도 띄지 않고 있기에 어떠한 값을 출력할 수 없어서 Reference Error가 발생하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/fd43a75b-0810-4008-aacb-fa0de5ed55bd/image.png" alt=""></p>
<p>위와 같이 const(let)으로 선언했을때 선언하기 이전에 식별자를 참조할 수 없는 구역이 존재하게 되는데 이러한 구역을 일시적 사각지대(Temporal Dead Zone)이라고 한다. 이는 let, const로 선언한 변수의 경우 선언이 되었을 때 초기화는 따로 이루어지기 때문에 발생하게 된다.</p>
<h3 id="2-function-expression-호이스팅">2. Function Expression 호이스팅</h3>
<p>Function Expression(함수 표현식)은 함수를 특정 변수에 담아두는 방식을 의미</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/706dc792-d577-48f9-a0fc-c94d00a9cbd1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/b1b40754-4c3f-4292-ba9e-c56d00c83d93/image.png" alt=""></p>
<p><strong>Function Expression</strong>은 함수를 변수에 담아둔 형태이기에 <strong>JS엔진에게는 변수로 취급</strong>되므로 호이스팅은 변수와 동일한 방식으로 이루어진다. (var 함수표현식은 undefined로 초기화되고, let과 const는 어떤 값으로도 초기화 되지 않는다.)</p>
<h3 id="3-function-호이스팅">3. Function 호이스팅</h3>
<pre><code class="language-js">study();

function study(){
  // ...  
};</code></pre>
<h4 id="phase-1-생성-단계-2">Phase 1) 생성 단계</h4>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/36ab8345-8955-49df-b7a4-f204aba67260/image.png" alt=""></p>
<p>Context 생성 후 Record 내부에 study라는 함수 객체를 생성 후 기록해 둔다. 이 때에는 JS엔진이 함수 선언과 동시에 함수 객체를 생성해 둔다.</p>
<h4 id="phase-2-실행-단계-2">Phase 2) 실행 단계</h4>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/ad813632-e848-4f98-a9a3-126ee652a1d4/image.png" alt=""></p>
<p>JS엔진이 Context내 Record를 참조하여 study 함수를 정상적으로 실행한다.</p>
<p>이처럼 함수 선언의 경우에는 선언과 동시에 함수객체가 생성되기 때문에 호이스팅이 일어나도 정상적으로 실행될 수 있다.</p>
<h2 id="🟣-outer-environment-reference">🟣 Outer Environment Reference</h2>
<p>Outer Environment Refernece는 외부 환경을 참조하는 역할을 하며 이전에 실행된 Lexcial Environment를 가리킨다. 이는 <strong>식별자 결정</strong> (Identifier Resolution)이 필요한 상황에서 사용된다. 아래의 상황을 생각해보자</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/cf08b6a1-f05e-45a1-8c33-97de9ed1d2d6/image.png" alt=""></p>
<p>다음의 코드를 예시로 들어보면, Call Stack에 전역 Execution Context가 생성된 뒤, goTo2F 함수가 실행되면서 goTo2F 함수에 대한 Execution Context가 생성된 것을 볼 수 있다. 이 상황에서 goTo2F 함수가 lamp 변수를 출력해야 한다면, 어떤 변수를 출력해야할까? 이 상황을 JS엔진은 Outer를 이용하여 해결하려고 한다.</p>
<pre><code class="language-js">let lamp = false;

function goTo2F() {
    let lamp = true;
    function goTo3F(){
        let pet = &#39;puppy&#39;;
        console.log(pet);  // puppy
        console.log(lamp); // ????
    }
    goTo3F();
}

goTo2F();</code></pre>
<p>다음의 코드를 예시로 들어 JS엔진이 코드를 실행하면서 <strong>Outer Environment Reference</strong>를 어떻게 사용하는지 알아보려고 한다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/ed3c87b6-d700-4c93-bd92-3ba3a6009db0/image.png" alt=""></p>
<p>다음은 goTo2F 함수가 실행되는 시점을 가리킨다. 전역에 선언된 lamp와 goTo2F 함수가 전역 Execution Context 내부에 생성되어 있는 모습이다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/74ca08f8-6342-4f72-ac2f-79c502f42fb4/image.png" alt=""></p>
<p>이 상황에서 goTo2F 함수를 실행하면 goTo2F 함수에대한 Execution Context를 생성한 뒤 새로 생성된 Exeuction Context의 Outer에는 전역 Lexcial Environment(Record + Outer)로 되돌아갈 수 있는 Outer를 생성해 둔다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/9df40ea7-0d0d-41b5-ab4e-99c5702f4c53/image.png" alt=""></p>
<p>위의 그림은 goTo2F 함수가 실행되고 났을때의 상황을 보여준다. lamp 변수와 goTo3F 함수 객체가 Record에 기록된다. 이후에는 goTo3F 함수가 실행될 것이다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/88a0fd8f-4805-499b-ad47-006f5d2cbd1f/image.png" alt=""></p>
<p>위의 그림은 goTo3F 함수가 실행되는 시점을 나타낸다. 이전과 마찬가지로 goTo3F라는 새로운 함수가 실행되었으므로 새로운 Exeuction Environment를 생성하고 여기에는 goTo2F 함수의 Execution Environment로 돌아갈 수 있는 outer를 생성해 놓은 모습이다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/ac8b71cc-2e34-4cec-88ef-17f285591786/image.png" alt=""></p>
<p>위 그림은 goTo3F가 본격적으로 실행되고 났을 때의 모습을 나타낸다. Record에 변수 pet이 기록된 모습이다. 이 상황에서 pet의 값을 출력해야 한다면, 현재 실행중인 Context(그림 상 제일 위에 위치한 Context)를 먼저 참조하여 pet의 값을 출력해낸다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/8b61e939-690c-4300-86bb-90e8f9b233bb/image.png" alt=""></p>
<p>이외에 corona라는 변수를 출력해야 한다면 어떻게 될까? JS 엔진은 마찬가지로 현재 실행중인 Execution Context 부터 살펴보며 corona가 어떤 값을 가지는지 찾아본다. 여기서 현재 찾아보고 있는 Context 안에 corona 변수 값이 존재하지 않는다면 outer를 통해 이전 Execution Context로 이동하여 탐색과정을 진행한다. 이러한 과정을 전역 Exeuction Context에 도달할 때 까지 반복해 낸다. 결국 찾아내지 못한다면, corona 변수는 어떤 값인지 알 수가 없기에 Reference Error를 뿜어낸다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/2573eab8-c1e3-4874-a2f1-48c6bf429fbd/image.png" alt=""></p>
<p>다음은 lamp라는 변수의 값을 출력해야 되는 경우이다. 이 경우도 마찬가지로 Execution Context들을 찾아보다가 가운데 Execution Context에서 lamp의 값을 발견하게 되고 이 때에는 탐색 과정을 종료하게 된다. 한편, 이러한 경우 동일한 식별자인 전역 Execution Context에 위치한 lamp의 값은 알 수가 없게 되는데 이러한 상황을 <strong>변수 섀도잉</strong>(Variable Shadowing) 이라고 한다.</p>
<blockquote>
<p><strong>변수 섀도잉</strong>(Variable Shadowing)</p>
<p>동일한 식별자로 인하여 상위 스코프에서 선언된 식별자의 값이 가려지는 현상</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/2aff75a9-771f-4d1c-a221-ddfe3fb1253c/image.png" alt=""></p>
<p>이전에 선언된 코드의 Context들을 정리해보면 다음과 같다. 위에서 언급했듯 JS 엔진은 코드 실행 시 식별자의 값을 찾아내기 위해 Call Stack 내부의 선언된 Execution Context들을 Outer를 통해 둘러보는 과정을 진행한다. 이처럼 식별자의 값을 결정할 때 사용하는 Context 또는 스코프들의 연결 리스트를 <strong>스코프 체인</strong>이라고 하고, 식별자를 결정할 때 스코프를 이동하는 과정 자체를 <strong>스코프 체이닝</strong> 이라고 한다.</p>
<h2 id="🟡--🟣-execution-context">🟡 + 🟣 Execution Context</h2>
<p>지금까지 Execution Context내 Lexcial Environment를 구성하는 <strong>Lexical Environment Record</strong>와 <strong>Outer Environment Reference</strong>의 의미와 그 기능에 대해 알아보았다. Execution Context의 기능을 한마디로 요약한다면 다음과 같다.</p>
<blockquote>
<p><strong>Exeuction Context</strong>(실행 컨텍스트)</p>
<p>코드를 실행하는데 필요한 <strong>환경</strong>을 제공하는 객체</p>
</blockquote>
<p>여기서 <strong>환경</strong>이란 코드 실행에 영향을 주는 조건이나 상태를 의미한다.</p>
<p>이처럼 자바스크립트가 Execution Context라는 객체를 통해 식별자를 판별할 수 있는 것은 ES5이후 부터였다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/dc7074d8-8d99-4e9c-bea3-e99c2cf519cb/image.png" alt=""></p>
<p>이전에는 함수가 어디에서 호출되느냐에 따라 스코프가 결정되는 동적 스코프 방식이였다면 ES5 이후로는 Execution Context를 이용해 함수가 어디에 선언되어있는지에 따라 스코프가 결정되는 정적 스코프 방식을 채택하게 되었다. 이를 통해 자바스크립트는 좀 더 효율적으로 식별자의 값을 결정해낼 수 있게 되었다.</p>
<h2 id="🚩출처-및-참고자료">🚩출처 및 참고자료</h2>
<p><a href="https://www.youtube.com/watch?v=EWfujNzSUmw&amp;t=9s">[10분 테코톡] 💙 하루의 실행 컨텍스트 - YouTube</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CORS 에러와 해결법]]></title>
            <link>https://velog.io/@seungchan__y/CORS-%EC%97%90%EB%9F%AC%EC%99%80-%ED%95%B4%EA%B2%B0%EB%B2%95</link>
            <guid>https://velog.io/@seungchan__y/CORS-%EC%97%90%EB%9F%AC%EC%99%80-%ED%95%B4%EA%B2%B0%EB%B2%95</guid>
            <pubDate>Wed, 08 Jun 2022 14:13:56 GMT</pubDate>
            <description><![CDATA[<h1 id="🚫-corscross-origin-resouce-sharing-에러">🚫 CORS(Cross Origin Resouce Sharing) 에러</h1>
<h2 id="👀-intro">👀 Intro</h2>
<p>프론트엔드에서 백엔드서버로 api 요청을 보낼 때 흔히 볼 수 있는 에러들 중 하나이다.</p>
<p><strong>개발하면서는 서로 오리진이 다르니 프론트나 백엔드에서 CORS 에러를 해결해주도록 하면 된다.</strong></p>
<p>라고 대충 알고 넘어갔지만, 이번을 계기로 확실하게 이해하고 정확히 무슨 요인으로 발생하는지 알아보고자 한다.</p>
<h2 id="📪-origin">📪 Origin</h2>
<p><strong>CORS</strong> 에러를 이해하려면 <strong>Origin</strong> 에 대해 알아볼 필요가 있다.</p>
<p><img src="https://ko.javascript.info/article/url/url-object.svg" alt="URL objects"></p>
<p>[출처] : <a href="https://ko.javascript.info/url">모던 자바스크립트 튜토리얼</a></p>
<p>위 그림에서 볼 수 있듯이 <strong>Origin</strong> 이란 <mark><em>Protocol, Hostname, Port</em></mark>를 한데 묶은 것을 의미한다. 다음의 간단한 예제를 통해 같은 오리진인지 아닌지 확인해보자</p>
<h4 id="❓-다음-중-httplocalhost와-동일-origin인-url은-무엇일까"><strong>❓ 다음 중 <a href="http://localhost%EC%99%80">http://localhost와</a> 동일 Origin인 URL은 무엇일까?</strong></h4>
<ul>
<li><p><input disabled="" type="checkbox">  <a href="https://localhost">https://localhost</a></p>
</li>
<li><p><input checked="" disabled="" type="checkbox">  <a href="http://localhost:80">http://localhost:80</a></p>
</li>
<li><p><input disabled="" type="checkbox">  <a href="http://127.0.0.1">http://127.0.0.1</a></p>
</li>
<li><p><input checked="" disabled="" type="checkbox">  <a href="http://localhost/api/cors">http://localhost/api/cors</a></p>
</li>
</ul>
<h4 id="✅-정답을-확인해봅시다">✅ <strong>정답을 확인해봅시다.</strong></h4>
<ol>
<li><p>첫번째 URL은 프로토콜이 <strong>https</strong>로 다른 프로토콜을 가지고 있으므로 <strong>다른 Origin</strong> 으로 취급된다.</p>
</li>
<li><p>두번째 URL은 포트번호가 <strong>80</strong>이라서 다른 오리진이라고 생각하겠지만 이는 잘못된 생각이다. 우선 <strong>http 프로토콜</strong>의 경우 포트번호가 생략될 경우 기본적으로 포트 80을 배정받는다고 한다. 따라서 두번째 URL은 <strong>동일한 Origin</strong>으로 취급된다. ([참고로 https 프로토콜의 경우 443 포트를 기본적으로 배정받는다.](<a href="https://johngrib.github.io/wiki/why-http-80-https-443/#:~:text=http%EC%9D%98%20%EA%B8%B0%EB%B3%B8%20%ED%8F%AC%ED%8A%B8%EB%8A%94,%EA%B8%B0%EB%B3%B8%20%ED%8F%AC%ED%8A%B8%EB%8A%94%20443%20%EC%9D%B4%EB%8B%A4.">http의 기본 포트가 80, https의 기본 포트가 443인 이유는 무엇일까? - 기계인간 John Grib</a>))</p>
</li>
<li><p>세번째 URL은 Hostname이 <strong>127.0.0,1</strong> 로 통상적으로 <strong>localhost</strong> 를 의미하는 IP주소이다. 따라서 같은 오리진으로 보일 수도 있다. 다만, <strong>브라우저의 경우</strong> 에는 같은 오리진인지 판단할때 문자열 값 자체를 기준으로 판단하기 때문에, <strong>다른 Origin 으로 판단</strong> 한다.</p>
</li>
<li><p>네번째 URL의 경우 localhost뒤에 <strong>/api/cors 라는 pathname이 붙었을 뿐</strong>, 같은 Protocol, Hostname, Port를 가지기 때문에 <strong>같은 Origin으로 판단</strong> 한다.</p>
</li>
</ol>
<h2 id="⚠️-sopsame-origin-policy">⚠️ SOP(Same Origin Policy)</h2>
<p>Origin에 대해 이해한 내용을 바탕으로 웹에서 규정되어 있는 SOP에 대해 알아볼 필요가 있다 공식문서에 따르면, SOP는 다음과 같이 정의된다.</p>
<blockquote>
<p><strong>동일 출처 정책</strong>(same-origin policy)은 어떤 <a href="https://developer.mozilla.org/ko/docs/Glossary/Origin">Origin</a>에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식입니다. 동일 출처 정책은 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여줍니다.</p>
</blockquote>
<p>즉 보안상의 이유로, <strong>웹 상에서는 다른 Origin으로 부터 요청 받는 리소스 접근을 제한하는 정책</strong>이라고 볼 수 있다. 따라서 위의 URL들 중 첫번째와 세번째 URL이 <a href="http://localhost%EC%9D%98">http://localhost의</a> 리소스에 접근하려 할 경우 SOP에 위반되어 리소스 접근에 제한 받는다. 이러한 상황은 프론트엔드와 백엔드 개발 중에 흔히 직면할 수 있는 상황이다.</p>
<p>예를 들어, 프론트엔드가 <strong>localhost:3000</strong>으로 개발 중인 상황에서, 백엔드 서버는 <strong>localhost:5000</strong>에서 호스팅 중인 상황이라면, 이 둘은 서로 포트번호가 달라 다른 Origin으로 취급되어 SOP에 위반되게 되는 것이다. 이러한 상황을 우리가 흔히 말하는 <strong>CORS 에러</strong>라고 하는 것이다. 그럼, 이렇게 Origin이 다른 상황에서는 어떻게 원하는 리소스에 접근할 수 있을까? 여기서 이제 <strong>CORS</strong>에 대해 알아갈 필요가 있는 것이다.</p>
<h2 id="🌙-corscross-origin-resource-sharing">🌙 CORS(Cross Origin Resource Sharing)</h2>
<p>MDN 공식문서에 따르면, CORS는 다음과 같이 정의된다.</p>
<blockquote>
<p><strong>교차 출처 리소스 공유</strong>(Cross-Origin Resource Sharing, <a href="https://developer.mozilla.org/ko/docs/Glossary/CORS">CORS</a>)는 추가 <a href="https://developer.mozilla.org/ko/docs/Glossary/HTTP">HTTP</a> 헤더를 사용하여, 한 <a href="https://developer.mozilla.org/ko/docs/Glossary/Origin">출처</a>에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.</p>
</blockquote>
<p>즉, SOP에 위반 되는 상황 속에서도 리소스에 접근하려면, CORS 헤더를 도입하여 이러한 상황을 해결해야 한다. 여기서 CORS헤더를 도입한 경우는 아래와 같은 상황을 의미한다.</p>
<p>Origin이 <strong><a href="https://foo.example">https://foo.example</a></strong>인 클라이언트에서 <strong><a href="https://bar.other">https://bar.other</a></strong> 도메인의 리소스를 접근하는 시나리오를 생각해보자. foo.example에서 리소스를 호출하는 코드는 다음과 같다.</p>
<pre><code class="language-javascript">const xhr = new XMLHttpRequest();
const url = &#39;https://bar.other/resources/public-data/&#39;;

xhr.open(&#39;GET&#39;, url);
xhr.onreadystatechange = someHandler;
xhr.send();</code></pre>
<p>이런식으로 리소스 접근 호출을 할 경우, 브라우저가 서버로 전송하는 내용의 헤더는 다음과 같다.</p>
<pre><code>GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
**Origin: https://foo.example**</code></pre><p>요청 헤더의 경우 어느 Origin에서 보내는 요청인지 항상 포함되어 있다. CORS가 도입된 서버가 리소스 요청에 보내는 응답은 다음과 같다.</p>
<pre><code>HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[…XML Data…]</code></pre><p>눈여겨 봐야할 정보는 <strong>Access-Control-Allow-Origin</strong> 가 *로 설정되어 있으며 이는 어느 Origin에서든 bar.other의 리소스에 접근할 수 있다는 것을 의미한다. 특정 Origin만 접근할 수 있도록 하려면, 다음과 같이 헤더를 변경하면 된다. 아래는 <a href="https://foo.example%EC%9D%B4%EB%9D%BC%EB%8A%94">https://foo.example이라는</a> 오리진을 가진 URL만 접근할 수 있음을 의미한다.</p>
<pre><code>Access-Control-Allow-Origin: https://foo.example</code></pre><h2 id="👂-cors를-도입하는-방법">👂 CORS를 도입하는 방법</h2>
<p>지금 까지는 CORS 에러에 대해서 알아봤다면 이제는 해결하는 방법에 대해서 알아보려고 한다. 이 문제를 해결하는 방법은 프론트엔드, 백엔드 양 측에게 모두 존재한다.</p>
<h3 id="프론트엔드---proxy-server-도입">프론트엔드 - Proxy Server 도입</h3>
<p>Proxy란 어떠한 대상의 기본적인 동작의 작업을 가로챌 수 있는 객체를 의미한다. 이를 이용한 Proxy Server는 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있도록 하는 응용 프로그램을 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/seungchan__y/post/89fddacb-af4d-41c9-ad4d-403f008425b3/image.png" alt=""></p>
<p>[출처] : 면접을 위한 CS 전공지식 노트</p>
<p>위의 그림에서 볼 수 있듯, 프론트엔드 서버 말단에 프록시 서버를 도입하여, /api, /api2로 보내지는 요청의 Origin을 API 서버의 Origin과 동일하게 하여 SOP를 위반하지 않도록 할 수 있다.</p>
<p>리액트의 경우 http-proxy-middleware 모듈을 이용하여 이를 구현해 낼 수 있다.</p>
<pre><code class="language-javascript">const { createProxyMiddleware } = require(&#39;http-proxy-middleware&#39;);

module.exports = function(app) {
  app.use(
    &#39;/api&#39;,
    createProxyMiddleware({
      target: &#39;http://localhost:5000&#39;,
      changeOrigin: true,
    })
  );
};</code></pre>
<p>[출처] - <a href="https://create-react-app.dev/docs/proxying-api-requests-in-development/">https://create-react-app.dev/docs/proxying-api-requests-in-development/</a></p>
<p>위의 코드를 src 폴더안에 setupProxy.js라는 파일로 작성하면 리액트 서버에 프록시 서버를 도입할 수 있다고 한다.</p>
<h3 id="백엔드---1-access-control-allow-origin-응답-헤더-추가">백엔드 - (1) Access-Control-Allow-Origin 응답 헤더 추가</h3>
<p>응답 헤더의 Access-Control-Allow-Origin 항목을 해당하는 Origin으로 설정해주면 CORS를 도입할 수 있다. Express의 경우는 다음과 같이 추가해 줄 수 있다.</p>
<pre><code class="language-js">app.use((req, res) =&gt; {
    res.header(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;); // 모든 도메인 허용
    res.header(&quot;Access-Control-Allow-Origin&quot;, &quot;http://localhost:3000&quot;); // 특정 도메인 허용
});</code></pre>
<p>다만 &quot;*&quot;를 이용해 모든 도메인을 허용해 놓는 것은 보안상 좋지 못하니 Origin을 특정해 주는 것이 좋다.</p>
<h3 id="백엔드---2-cors-미들웨어-사용하기">백엔드 - (2) CORS 미들웨어 사용하기</h3>
<p>Express의 경우, cors 모듈을 사용하여 CORS를 도입할 수 있다.</p>
<pre><code class="language-js">import express from &quot;express&quot;;
import cors from &#39;cors&#39;;

const app = express();

app.use(cors({ origin: [
  &#39;http://localhost:3000&#39;,
  &#39;https://nomalog.netlify.app&#39;
]));</code></pre>
<p>위와 같이 CORS를 설정한다면, localhost:3000과 nomalog.netlify.app라는 Origin에 대해 CORS를 허용할 수 있다.</p>
<h2 id="🚩-출처-및-참고자료">🚩 출처 및 참고자료</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/CORS">교차 출처 리소스 공유 (CORS) - HTTP | MDN</a></p>
<p><a href="https://developer.mozilla.org/ko/docs/Web/Security/Same-origin_policy">동일 출처 정책 - 웹 보안 | MDN</a></p>
<p><a href="https://www.youtube.com/watch?v=-2TgkKYmJt4&amp;t=1101s">[10분 테코톡] 🌳 나봄의 CORS - YouTube</a></p>
<p><a href="https://velog.io/@wiostz98kr/React-Express-CORS-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0">React + Express | CORS 설정하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS에서 ToastUI Viewer 적용하기]]></title>
            <link>https://velog.io/@seungchan__y/NextJS%EC%97%90%EC%84%9C-ToastUI-Viewer-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/NextJS%EC%97%90%EC%84%9C-ToastUI-Viewer-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 23 Jul 2021 14:12:22 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@seungchan__y/NextJS%EC%97%90%EC%84%9C-Toastui-Editor-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0">지난 포스트</a>에 이어서 ToastUI Viewer를 적용해보려고 한다. 여기서 Viewer란 Editor에서 만든 게시글의 내용을 보여주는 도구이다. Editor에서 소개한 방법과 비슷하게 dynamic import를 이용해서 적용하면 된다! 간단하지만 공식문서만 보았을때는 다소 난해한 경험을 겪어서 이것을 읽는 분들은 나처럼 삽질을 하지 않기를 바란다.</p>
<p>Editor에 대한 공식 <a href="https://github.com/nhn/tui.editor/blob/master/docs/ko/viewer.md">Github 문서</a>에 따르면 크게는 두 가지 방법으로 적용할 수 있다고 나와있다.</p>
<h3 id="1-viewer를-직접-적용">1. Viewer를 직접 적용</h3>
<h3 id="2-editor의-factory를-활용해-적용하는-방법">2. Editor의 factory를 활용해 적용하는 방법</h3>
<p>이번 포스트에서는 리액트 및 NextJS에서 적용하기 쉽게 하기 위해서 <strong>Viewer를 직접 적용하는 방법</strong>으로 할 것이다.(사실 Editor의 factory를 React 컴포넌트로 어떻게 적용하는지 몰라서이다..=_=)</p>
<p><img src="https://images.velog.io/images/seungchan__y/post/db4e9daf-f9bd-46eb-8cf9-d0a70110b66b/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-07-23%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.37.43.png" alt=""></p>
<p><img src="https://images.velog.io/images/seungchan__y/post/fcba455a-5dd1-4500-8003-b4cfa36c9b09/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-07-23%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.37.58.png" alt=""></p>
<p>공식 문서에 따르면 Viewer 모듈과 해당 css 파일을 불러오면 된다고 한다. 그래서 나도 이에 따라 컴포넌트 파일에는 Viewer를 불러오고 Viewer를 불러와야 하는 Page에는 dynamic import를 적용해 봤다. </p>
<p>또한 뷰어에 대한 <a href="https://github.com/nhn/tui.editor/blob/master/docs/ko/viewer.md#%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0">공식문서</a>에 따르면 Viewer는 내용값으로 HTML이 아닌 <strong>MarkDown 만을 받는다고 하니, 주의하도록 하자!!</strong></p>
<h3 id="viewer-component">Viewer Component</h3>
<pre><code>import Viewer from &#39;@toast-ui/editor/dist/toastui-editor-viewer&#39;;
import &#39;@toast-ui/editor/dist/toastui-editor-viewer.css&#39;;

const TestView = () =&gt; {
  return &lt;Viewer height=&quot;600px&quot; initialValue=&quot;# hello&quot; /&gt;;
};

export default TestView;</code></pre><h3 id="viewer-page">Viewer Page</h3>
<pre><code>import dynamic from &#39;next/dynamic&#39;;

const Viewer = dynamic(() =&gt; import(&#39;../../../components/TestView/TestView&#39;), {
  ssr: false,
});

const ProfessorStatus = () =&gt; {
  return (
    &lt;Viewer /&gt;
  );
};

export default ProfessorStatus;</code></pre><p>이대로 무난하게 잘 되는줄 알았으나... 에러가 발생하고 말았다.</p>
<p><img src="https://images.velog.io/images/seungchan__y/post/222e258e-f4db-4201-8044-8f41f90b7a7a/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-07-23%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.54.02.png" alt=""></p>
<p>분명 dynamic import를 적용했음에도 에러가 발생한 것 때문에 많이 절망적이었다... 그래도 공식 Github를 살펴보면서 해결의 실마리를 찾을 수 있었다.</p>
<p>우선 이전의 Editor 경우도 <strong>@toast-ui/react-editor</strong> 에서 import 해왔다는 점에서 <strong>@toast-ui/react-editor</strong>를 자세히 살펴보았다. 그리고 Viewer에 대한 코드를 살펴보니 아래 사항을 확인할 수 있었다.</p>
<pre><code>import Editor from &#39;./editor&#39;;
import Viewer from &#39;./viewer&#39;;

export { Editor, Viewer };</code></pre><p>이를 통해서 <strong>@toast-ui/react-editor</strong> 모듈에서 Editor와 Viewer 모두 import하는 것이 가능하다는 것을 확인할 수 있다. 이를 바탕으로 Viewer Component를 아래와 같이 수정해 봤다.</p>
<h3 id="viewer-component-수정">Viewer Component 수정!</h3>
<pre><code>import Viewer from &#39;@toast-ui/react-editor&#39;;
import &#39;@toast-ui/editor/dist/toastui-editor-viewer.css&#39;;

const TestView = () =&gt; {
  return &lt;Viewer height=&quot;600px&quot; initialValue=&quot;# hello&quot; /&gt;;
};

export default TestView;</code></pre><p>그러고 결과를 확인해보면 정상적으로 작동함을 확인할 수 있다!!</p>
<p><img src="https://images.velog.io/images/seungchan__y/post/7cc5736e-8d37-4b60-be65-16b581344cec/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-07-23%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.08.50.png" alt=""></p>
<p>공식문서가 React와 NextJS에 대해서 적용하는 법에 대해 친절히 설명해 줬으면 이런 고생들을 하지 않았겠지만.. 이런 고생들을 하면서 많은 것들을 배운 기회가 된 것 같다. 이제 프로젝트 내 게시판 기능 구현에 한 단계 앞으로 나아간 것 같다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS에서 Toastui Editor 적용하기]]></title>
            <link>https://velog.io/@seungchan__y/NextJS%EC%97%90%EC%84%9C-Toastui-Editor-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seungchan__y/NextJS%EC%97%90%EC%84%9C-Toastui-Editor-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 21 Jul 2021 08:19:48 GMT</pubDate>
            <description><![CDATA[<p>NextJS와 React를 사용하는 프로젝트를 진행하면서 게시글을 작성하고 읽는 기능이 필요해졌다. 이를 위해 찾아본 결과 ToastUI에서 제공하는 <a href="https://github.com/nhn/tui.editor/tree/master/apps/react-editor">Editor</a>가 있어서 사용해보았다. 그런데 공식문서대로 적용해보았으나 오류(~ is not defined)가 발생하며 적용이 되지 않았다. Github issue에서 이에 관해 찾아보았으나 별로 마땅한 해결책이 나오지 않아 구글링을 시도했다. 역시나, 어느 똑똑하신 분께서 <a href="https://yoon-dumbo.tistory.com/m/38?category=969772">해결책</a>을 내려주셨다.</p>
<p>이런 문제가 발생한 것은 바로 ToastUI Editor에서는 NextJS에서 적용되는 SSR(Server Side Rendering)을 지원하지 않기 때문이었다. 이러한 충돌을 막기 위해서는 <strong>해당 에디터를 dynamic하게 import할 필요가 있다</strong>. 위 블로그 링크에서 해결책을 확인할 수 있으니, 자세한 설명은 생략하겠다.</p>
<p><img src="https://images.velog.io/images/seungchan__y/post/71681e6c-45f2-4ce8-8424-be590f46a975/%E1%84%85%E1%85%A9%E1%84%83%E1%85%A5%20%E1%84%8C%E1%85%A5%E1%86%A8%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%8C%E1%85%A5%E1%86%AB.gif" alt="로더 적용 전"></p>
<p>그런데 한 가지 문제가 있다. 정상적으로 적용된 에디터 페이지를 들어가보면, 잘 나오는데 에디터만 유독 느리게 나온다. 아마도 런타임에 로드를 해야 되는 것이다 보니 다른 요소들에 비해서 느리게 나오는 것 같다. 이를 완화 시키고자 dynamic import시 <strong>loading</strong> 옵션을 추가했다. Dynamic Import의 loading 옵션에 대한 자세한 설명을 보고싶다면 <a href="https://nextjs.org/docs/advanced-features/dynamic-import#with-custom-loading-component">공식문서</a>를 참고하기를 바란다.</p>
<h3 id="editor를-dynamic-import로-불러오는-부분"><strong>Editor를 Dynamic import로 불러오는 부분</strong></h3>
<pre><code>const PostEditor = dynamic(() =&gt; import(&#39;../PostEditor/PostEditor&#39;), {
  ssr: false,
  loading: () =&gt; &lt;EditorLoader /&gt;,
});
</code></pre><h3 id="editorloader-컴포넌트">EditorLoader 컴포넌트</h3>
<pre><code>import CircularProgress from &quot;@material-ui/core/CircularProgress&quot;;

import { makeStyles } from &quot;@material-ui/core/styles&quot;;

const useStyles = makeStyles((theme) =&gt; ({
  loaderContainer: {
    height: &quot;600px&quot;,
    width: &quot;100%&quot;,
    border: `0.5px solid ${theme.palette.grey[400]}`,
    borderRadius: &quot;4px&quot;,
  },
  loaderItem: {
    position: &quot;relative&quot;,
    top: &quot;50%&quot;,
    left: &quot;50%&quot;,
  },
}));

const EditorLoader = () =&gt; {
  const classes = useStyles();
  return (
    &lt;div className={classes.loaderContainer}&gt;
      &lt;div className={classes.loaderItem}&gt;
        &lt;CircularProgress color=&quot;primary&quot; /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default EditorLoader;
</code></pre><p>EditorLoader의 구성요소 중 하나인 CircularProgress라는 요소는 material-ui에서 제공해주는 컴포넌트인 점을 참고하길 바란다. 꼭 위와 같이 material-ui를 적용할 필요 없이 적용한 editor의 크기로 자기 스타일에 맞는 Loader를 적용하면 된다.</p>
<p><img src="https://images.velog.io/images/seungchan__y/post/04ba75d0-8af9-4632-9f8c-597d06a4f8b3/%E1%84%85%E1%85%A9%E1%84%83%E1%85%A5%E1%84%8C%E1%85%A5%E1%86%A8%E1%84%8B%E1%85%AD%E1%86%BC.gif" alt="로더 적용"></p>
<p>위 사진에서 볼 수 있듯이 loading 옵션을 적용하니 Editor를 가져오는 동안 EditorLoader 컴포넌트가 그 자리를 차지하면서 이전과 다르게 에디터의 로딩을 기다리더라도 이질감은 덜 느껴진다. </p>
<p>원래는 Editor와 Viewer를 같이 다룰려고 하였으나 너무 길어진 관계로 다음글에서 Viewer 적용을 소개하려고 한다. </p>
]]></description>
        </item>
    </channel>
</rss>