<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ssooo_kk_77.log</title>
        <link>https://velog.io/</link>
        <description>기록을 습관처럼</description>
        <lastBuildDate>Sat, 19 Apr 2025 07:38:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ssooo_kk_77.log</title>
            <url>https://velog.velcdn.com/images/ssooo_kk_77/profile/d9eb7240-3bbe-4779-8c84-32e5a73bfc54/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ssooo_kk_77.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ssooo_kk_77" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[비전공자 개발자의 CS 공부기록(2)]]></title>
            <link>https://velog.io/@ssooo_kk_77/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-CS-%EA%B3%B5%EB%B6%80%EA%B8%B0%EB%A1%9D2</link>
            <guid>https://velog.io/@ssooo_kk_77/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-CS-%EA%B3%B5%EB%B6%80%EA%B8%B0%EB%A1%9D2</guid>
            <pubDate>Sat, 19 Apr 2025 07:38:49 GMT</pubDate>
            <description><![CDATA[<h3 id="📌-웹-애플리케이션-아키텍처의-구조와-역할">📌 웹 애플리케이션 아키텍처의 구조와 역할</h3>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/3da8adae-2d13-461c-ace6-cf71d5c3488e/image.png" alt=""></p>
<p><code>웹 클라이언트</code> </p>
<ul>
<li>사용자가 웹에 접근할 때 사용하는 프로그램</li>
<li>크롬, 사파리, 엣지 등의 웹 브라우저</li>
</ul>
<p><code>웹 서버</code></p>
<ul>
<li>웹 클라이언트로부터 HTTP 요청을 받아 데이터를 전달하는 프로그램</li>
<li>아파치 웹 서버, IIS(Internet Information Services), 엔진엑스(Nginx) 등</li>
</ul>
<p><code>WAS(Web Application Server)</code></p>
<ul>
<li>웹 서버로부터 주로 동적 콘텐츠를 받아 이를 처리한 뒤, 그 결과를 다시 웹 서버에 전달하는 중간자 역할</li>
<li>웹 서버가 해야 하는 일을 나눠 가지며 서버의 부담을 줄이고 전체 웹 통신의 효율을 높이는 조력자 역할</li>
<li>톰캣, 제우스, IBM 웹스피어 등</li>
</ul>
<p><code>데이터베이스(DB)</code></p>
<ul>
<li>웹 서비스에 필요한 다양한 데이터를 체계적으로 저장하는 곳이며 WAS에서 동적 콘텐츠를 제공하기 위해 접근하는 장소</li>
<li>MySQL, 오라클, MongoDB 등</li>
</ul>
<br />
<br />


<h3 id="📌-http란">📌 HTTP란?</h3>
<blockquote>
<p>HTTP(Hypertext Transfer Protocol) 란 웹 브라우저와 웹 서버 간에 데이터를 주고 받기 위해 사용하는 클라이언트/서버 모델을 따르는 프로토콜이다. </p>
</blockquote>
<h4 id="특징">특징</h4>
<ul>
<li><p>한 번 통신을 주고받으면 연결을 끊는 비연결성 프로토콜이다.</p>
</li>
<li><p>헤더
 · &#39;헤더명: 헤더값&#39;의 형식으로 작성하며 대소문자를 구분하지 않는다.
 · 헤더 종류</p>
<pre><code> ㄴ 공통 헤더 : Date / Cache-control
 ㄴ 요청 헤더 : Host / User-Agent
 ㄴ 응답 헤더 : Server / Location
 ㄴ 엔티티 헤더(메시지 본문에 대한 정보를 포함하는 헤더) : Content-Length / Content-Type
 ![](https://velog.velcdn.com/images/ssooo_kk_77/post/486e0b01-ddfd-488c-9d35-2b82f7aa828a/image.png)</code></pre></li>
<li><p>한계
  ㄴ 정보를 암호화하지 않은 상태로 보내기 때문에 정보 유출의 위험에 노출될 가능성이 높다.
  ㄴ 통신 상대를 확인하지 않기 때문에 누가 요청을 보냈는지, 통신 중인 상대가 허가된 상대인지 파악할 방법이 없다.</p>
</li>
<li><p>상태 코드
1️⃣XX : 웹 서버가 현재 요청을 받았으며 작업을 진행하고 있다.
<code>100(Continue)</code> : 서버가 요청을 받았으며 추가 요청을 기다리고 있음.</p>
<p>2️⃣XX : 클라이언트가 요청한 작업을 서버가 성공적으로 처리했다.
<code>200(Ok)</code> : 요청이 성공적으로 되었습니다.
<code>201(Created)</code> : 요청이 성공적이었으며 그 결과로 새로운 리소스가 생성되었습니다.
<code>204(No Content)</code> : 요청은 성공적으로 처리되었지만 응답 본문이 없습니다. </p>
<p>3️⃣XX : 리다이렉션 완료 응답으로 요청을 완료하기 위해 재전송이 필요하다.
<code>301(Moved Permanently)</code> : 추가로 요청한 페이지로 영구 이동되었습니다.
<code>302(Found)</code> : 추가로 요청한 페이지로 일시적으로 이동되었습니다.
<code>304(Not Modified)</code> : 리소스가 수정되지 않았으므로 캐시된 데이터를 사용해도 됩니다.</p>
<p>4️⃣XX : 클라이언트측에 오류가 있다.
<code>400(Bad Request)</code> : 클라이언트의 요청 내용에 문제가 있습니다.
<code>401(Unauthorized)</code> : 인증되지 않은 사용자입니다.
<code>404(Not Found)</code> : 요청에는 문제가 없으나 요청한 데이터가 없습니다.</p>
<p>5️⃣XX : 서버가 요청을 수행하지 못했음.
<code>500(Internal Server Error)</code> : 서버 내부적으로 오류가 발생해 응답에 실패했습니다.
<code>502(Bad Gateway)</code> : 서버가 다른 서버로부터 잘못된 응답을 받는 등 서버간의 네트워크에 문제가 생겨 통신이 제대로 되지 않습니다.
<code>503(Service Unavailable)</code> : 서버가 일시적으로 과부하 상태이거나 유지보수 중입니다.
<code>504(Gateway Timeout)</code> : 게이트웨이 또는 프록시 서버가 응답을 기다리다 시간이 초과되었습니다.</p>
</li>
</ul>
<br />
<br />

<h3 id="📌-https란">📌 HTTPS란?</h3>
<blockquote>
<p>HTTP 계층 아래 SSL이라는 보안 계층이 추가된 프로토콜이다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/286d95b7-22cb-4a38-877c-79cc32528ea1/image.png" alt=""></p>
</blockquote>
<p>🔆 SSL의 동작 과정
SSL은 공개키 기법과 대칭키 기법이라는 두 암호화 기법을 함께 사용한다.
(<a href="https://velog.io/@ssooo_kk_77/AES-%EC%95%94%ED%98%B8%ED%99%94%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B5%EB%AA%85-%ED%88%AC%ED%91%9C">관련 포스팅 보기</a>)
SSL은 크게 핸드셰이크, 세션, 세션 종료의 세 단계로 이루어진다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/7488c036-302b-4a68-ba5e-c414b3337619/image.png" alt=""></p>
<p><code>1단계</code> 
: 클라이언트가 랜덤한 데이터와 함께 현재 지원할 수 있는 암호화 방식을 서버에 전달한다.
<code>2단계</code>
: 서버가 클라이언트에게 랜덤 데이터와 지원 가능한 암호화 방식, 인증서(서버가 공식적으로 인증된 기관인 CA에서 발급받은 문서)를 전달한다.
<code>3단계</code>
: 인증서를 발급받은 클라이언트는 이 인증서가 제대로 된 문서인지 검증하기 위해 CA가 발급한 인증서 목록 중에서 서버가 전달한 인증서가 있는지 확인한다. 인증서가 목록에 있다면 한 번 더 확인하기 위해 CA에서 공유하는 공개키를 가지고 인증서를 복호화한다. 복호화에 성공한다면 이 인증서는 서버가 자신의 비밀키로 암호화했다는 것이 검증되니 서버를 신뢰할 수 있게 된다.
<code>4단계</code>
: 본격적으로 키를 주고받기 위해 클라이언트는 실제 데이터 통신에서 사용할 대칭키를 임시로 만든다.(이 때 대칭키는 클라이언트와 서버가 서로 주고받은 랜덤한 데이터를 조합한 임시키이다.)임시키는 대칭키이기 때문에 앞서 갖고 있던 공개키로 암호화해 서버에 전달한다.
<code>5단계</code>
: 키를 받은 서버는 자신이 갖고 있던 비밀키로 암호를 해독해 임시 키를 전달받는다.
<code>6단계</code>
: 클라이언트와 서버의 임시 키는 일련의 과정을 거쳐 세션 키로 바뀌고 이 세션키를 이용해 본격적으로 클라이언트와 서버가 통신을 할 수 있게 된다.</p>
<br />
<br />
<br />

<p>[출처] - 그림으로 쉽게 이해하는 웹/HTTP/네트워크</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DHTMX 간트차트 구현기]]></title>
            <link>https://velog.io/@ssooo_kk_77/DHTMX-%EA%B0%84%ED%8A%B8%EC%B0%A8%ED%8A%B8-%EA%B5%AC%ED%98%84%EA%B8%B0</link>
            <guid>https://velog.io/@ssooo_kk_77/DHTMX-%EA%B0%84%ED%8A%B8%EC%B0%A8%ED%8A%B8-%EA%B5%AC%ED%98%84%EA%B8%B0</guid>
            <pubDate>Sat, 12 Apr 2025 05:24:56 GMT</pubDate>
            <description><![CDATA[<p>진행하는 프로젝트에서 간트차트로 일정관리하는 기능 개발을 담당하게 되어 여러 라이브러리를 비교하던 중 </p>
<ol>
<li>UI가 깔끔하고</li>
<li>React를 지원하며</li>
<li>문서화가 잘 되어있는</li>
</ol>
<p>DHTMLX(<a href="https://dhtmlx.com/docs/products/dhtmlxGantt/">공식문서</a>) 라이브러리를 사용하기로 했다.</p>
<br />

<p>주요 기능들과 커스텀한 내용을 소개해보자면</p>
<h3 id="1️⃣-커스텀-스크롤">1️⃣ 커스텀 스크롤</h3>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/c1cd2468-488f-4ebb-8966-47fed06f22d7/image.gif" alt=""></p>
<p>원래 기본 스크롤은 왼쪽 그리드 영역은 고정이고 오른쪽 차트 영역을 스크롤하는 것이었다. 하지만 위와같이 그리드 영역이 좁아질 경우 데이터를 제대로 보여주지 못하는 문제가 있어 그리드 영역과 차트 영역 각각에 스크롤을 설정해 컨트롤할 수 있게 개선이 필요했다.</p>
<pre><code>          gantt.config.layout = {
                css: &quot;gantt_container&quot;,
                cols: [
                    {
                        width: 400,
                        min_width: 400,
                        rows: [
                            {view: &quot;grid&quot;, scrollable: true, scrollX: &quot;gridScrollHor&quot;, scrollY: &quot;scrollVer&quot;},
                            {view: &quot;scrollbar&quot;, id: &quot;gridScrollHor&quot;, height: 20},
                        ],
                    },
                    {
                        rows: [
                            {view: &quot;timeline&quot;, scrollable: true, scrollX: &quot;timelineScrollHor&quot;, scrollY: &quot;scrollVer&quot;, width: 1},
                            {view: &quot;scrollbar&quot;, scroll: &quot;x&quot;, id: &quot;timelineScrollHor&quot;, height: 20},
                        ],
                    },
                    {view: &quot;scrollbar&quot;, id: &quot;scrollVer&quot;},
                ],
            };</code></pre><p>왼쪽 그리드 영역은 width를 400px로 고정하고 그 외의 영역은 timeline이 차지하도록 width : 1을 설정했다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/be0dc7c2-4a6c-43a3-8f6c-abe77246a3f3/image.png" alt=""></p>
<br />

<h3 id="2️⃣-locale-ko로-수정">2️⃣ locale ko로 수정</h3>
<pre><code>gantt.locale = {
                date: {
                    month_full: [&quot;1월&quot;, &quot;2월&quot;, &quot;3월&quot;, &quot;4월&quot;, &quot;5월&quot;, &quot;6월&quot;, &quot;7월&quot;, &quot;8월&quot;, &quot;9월&quot;, &quot;10월&quot;, &quot;11월&quot;, &quot;12월&quot;],
                    month_short: [&quot;1월&quot;, &quot;2월&quot;, &quot;3월&quot;, &quot;4월&quot;, &quot;5월&quot;, &quot;6월&quot;, &quot;7월&quot;, &quot;8월&quot;, &quot;9월&quot;, &quot;10월&quot;, &quot;11월&quot;, &quot;12월&quot;],
                    day_full: [&quot;일요일&quot;, &quot;월요일&quot;, &quot;화요일&quot;, &quot;수요일&quot;, &quot;목요일&quot;, &quot;금요일&quot;, &quot;토요일&quot;],
                    day_short: [&quot;일&quot;, &quot;월&quot;, &quot;화&quot;, &quot;수&quot;, &quot;목&quot;, &quot;금&quot;, &quot;토&quot;],
                },
                labels: {
                    new_task: &quot;새일정&quot;,
                    dhx_cal_today_button: &quot;오늘&quot;,
                    day_tab: &quot;일&quot;,
                    week_tab: &quot;주&quot;,
                    month_tab: &quot;월&quot;,
 ...</code></pre><p> 한국어로 date 표시 및 ui 요소(버튼명, 폼 필드명 등)에 표시되는 라벨명 수정</p>
<p>++ 라벨 추가는  </p>
<pre><code>gantt.locale.labels.section_modification_reason = &quot;일정 변경 사유&quot;;</code></pre> <br />
 <br />


<h3 id="3️⃣-date-순서-변경">3️⃣ date 순서 변경</h3>
<p>기본 date format은 dd-mm-yy 순</p>
<p> <img src="https://velog.velcdn.com/images/ssooo_kk_77/post/b57d6706-f647-4b38-8f66-873a2c310fd8/image.png" alt=""></p>
<p>✔️ yy-mm-dd순으로 변경</p>
<pre><code> gantt.config.date_format = &quot;%Y-%m-%d %H:%i&quot;;</code></pre><br />
<br />

<h3 id="4️⃣-onbeforelightbox">4️⃣ onBeforeLightbox</h3>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/df6fd035-dac0-4554-a44c-219a06d6041e/image.png" alt=""></p>
<p>일정 등록/수정/삭제를 위한 모달을 lightbox라고 하는데 이 모달이 열리기전에 하고자 하는 작업을 설정할때 사용하는 함수이다.</p>
<pre><code>gantt.attachEvent(&quot;onBeforeLightbox&quot;, function(id) {
    const task = gantt.getTask(id);
    task.my_template = `&lt;span id=&#39;title1&#39;&gt;Holders: &lt;/span&gt;${task.users}
    &lt;span id=&#39;title2&#39;&gt;Progress: &lt;/span&gt;${task.progress*100}%`;
    return true;
});</code></pre><p>나는 여기에 따로 외부에서 만든 셀렉트 박스 컴포넌트를 주입하기 위해 </p>
<pre><code>gantt.config.buttons_right = [&quot;dhx_delete_btn&quot;];
gantt.config.lightbox.sections = [
                                {
                                    name: &quot;food&quot;, 
                                    height: 35,
                                    map_to: &quot;food&quot;,
                                    type: &quot;textarea&quot;,
                                },
                                {
                                    name: &quot;drink&quot;, 
                                    height: 20,
                                    map_to: &quot;drink&quot;,
                                    type: &quot;custom&quot;,
                                },</code></pre><p>lightbox.sections의 type을 custom으로 설정하고 </p>
<pre><code> const addMuntoContainer: LightboxControl = {
                render: function (sns: any) {
                    return `&lt;div class=&#39;custom_muntno_container&#39; style=&quot;width: 100%;&quot;&gt;&lt;/div&gt;`;
                },

                set_value: function (node: HTMLElement, value: string, task: any, section: any) {
                    ...
                },

                get_value: function (node: HTMLElement, task: any, section: any) {
                    return task.muntno || &quot;&quot;;
                },
            };

  gantt.form_blocks[&quot;custom&quot;] = addMuntoContainer;</code></pre><p>form_blocks의 type이 custom인 영역에 div태그를 추가해주었다.</p>
<br />

<h3 id="5️⃣-onlightbox">5️⃣ onLightbox</h3>
<p>모달이 열렸을 때 실행하는 함수로 onBeforeLightbox에서 생성한 div 태그를 찾아 외부에서 만든 컴포넌트를 렌더링 해주었다.</p>
<pre><code>            gantt.attachEvent(&quot;onLightbox&quot;, function (id: number) {
                const task = gantt.getTask(id);

                const lightbox = document.querySelector(&quot;.custom_muntno_container&quot;);

                if (lightbox) {
                    if (rootInstance) {
                        rootInstance.unmount();
                    }

                    rootInstance = createRoot(lightbox);
                    rootInstance.render(
                        &lt;CutomComponents
                        /&gt;,
                    );
                }
            });</code></pre><br />
<br />

<h3 id="6️⃣-onlightboxsave">6️⃣ onLightboxSave</h3>
<p>모달에서 save 버튼 클릭 시 실행되는 함수로 api 연동이나 validation check 로직을 정의했다.</p>
<pre><code>gantt.attachEvent(&quot;onLightboxSave&quot;, function(id, task, is_new){
    //any custom logic here
    return true;
})</code></pre><br/>
<br />

<h3 id="7️⃣-lightbox-header-name-설정">7️⃣ lightbox header name 설정</h3>
<pre><code> gantt.templates.lightbox_header = function (start: Date, end: Date, task: GanttTask) {
                let headerName;
                if (currentLevelRef.current === 1) {
                    if (task.text !== &quot;새 일감&quot;) {
                        headerName = &quot;일감 수정&quot;;
                    } else {
                        headerName = &quot;일감 등록&quot;;
                    }
                }
                return headerName;
            };</code></pre><br />
<br />

<h3 id="8️⃣-timeline-영역-헤더-커스텀">8️⃣ timeline 영역 헤더 커스텀</h3>
<pre><code>  // timeline 헤더 설정
            gantt.config.scales = [
                {
                    unit: &quot;day&quot;,
                    step: 1,
                    format: function (date: Date) {
                        const monthNames = [&quot;01&quot;, &quot;02&quot;, &quot;03&quot;, &quot;04&quot;, &quot;05&quot;, &quot;06&quot;, &quot;07&quot;, &quot;08&quot;, &quot;09&quot;, &quot;10&quot;, &quot;11&quot;, &quot;12&quot;];
                        const day = date.getDate();
                        const month = date.getMonth();
                        const dayString = day &lt; 10 ? `0${day}` : day;
                        return `${monthNames[month]}.${dayString}`;
                    },
                },
            ];</code></pre><p>기본 timeline 헤더 
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/916e4b01-a876-4d52-9148-feacebdad7d2/image.png" alt="">
커스텀한 timeline 헤더
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/4f233084-42a1-478f-a62f-084992373b7a/image.png" alt=""></p>
<br />
<br />

<h3 id="9️⃣-레벨에-따라-일감을-스타일링하고-싶을-때">9️⃣ 레벨에 따라 일감을 스타일링하고 싶을 때</h3>
<pre><code> gantt.templates.task_class = function (start: Date, end: Date, task: GanttTask) {
                // 최상위 일감
                if (!task.parent) {
                    return &quot;highlight-parent-task&quot;;
                }

                // 하위 일감
                const parentTask = gantt.getTask(task.parent);
                if (parentTask) {
                    return &quot;highlight-child-task&quot;;
                }
                return &quot;&quot;;
            };</code></pre><p>클래스를 다르게 주어 </p>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/fdb2fd5d-a0d0-43ce-b9e6-d1af552617c8/image.png" alt="">
부모일감과 하위일감의 색을 커스텀할 수 있다.</p>
<br />
<br />

<h3 id="🔟-onlightboxdelete">🔟 onLightboxDelete</h3>
<p>lightbox에서 삭제 버튼을 클릭했을때 실행되는 함수이다. true를 return하면 정상정으로 삭제를 진행, false를 return하면 삭제가 진행되지 않는다.</p>
<pre><code>gantt.attachEvent(&quot;onLightboxDelete&quot;, function(id){
    const task = gantt.getTask(id);
    if (task.duration &gt; 60){
        alert(&quot;The duration is too long. Please, try again&quot;);
        return false;
    }
    return true;
})</code></pre><br />
<br />

<h3 id="1️⃣1️⃣-onaftertaskupdate">1️⃣1️⃣ onAfterTaskUpdate</h3>
<p>일감이 수정된 후 실행되는 함수</p>
<pre><code>gantt.attachEvent(&quot;onAfterTaskUpdate&quot;, function(id,task){
    //any custom logic here
});</code></pre><br />
<br />

<h3 id="1️⃣2️⃣-드래그-관련-함수">1️⃣2️⃣ 드래그 관련 함수</h3>
<ul>
<li><p>onTaskDrag
: 드래그할 때 실행되는 함수</p>
<pre><code>gantt.attachEvent(&quot;onTaskDrag&quot;, function(id, mode, task, original){
  //any custom logic here
});</code></pre></li>
<li><p>onAfterTaskDrag
: 드래그 완료 후 실행되는 함수</p>
<pre><code>gantt.attachEvent(&quot;onAfterTaskDrag&quot;, function(id, mode, e){
  //any custom logic here
});</code></pre></li>
<li><p>onBeforeTaskDrag
: autoScehduling을 직접 구현하다보니 최상위 일감의 드래그를 막는 로직이 추가로 필요했다.(최상위 일감은 하위 일감들의 일정에 따라 계산되는 일정)
일정을 드래그하여 조정하기 전 최상위 일감인지 먼저 판단 후 최상위면 false를 return하여 드래그를 방지했다.</p>
</li>
</ul>
<pre><code>    gantt.attachEvent(&quot;onBeforeTaskDrag&quot;, function (id: string, mode: string, e: MouseEvent) {
                const task = gantt.getTask(id);

                // 부모 Task 드래그 방지
                if (task.type === &quot;project&quot; || gantt.hasChild(id)) {
                    return false;
                }

                return true;
            });</code></pre><br />
<br />

<p>➕ 이 외에도 다양한 커스텀 가능한 이벤트들이 정의되어 있고 설명과 예시도 문서화로 잘 구성되어 있어서 다음에도 간트 차트를 구현한다면 dhtmlx 라이브러리를 사용할 것 같다.
🧷 <a href="https://docs.dhtmlx.com/gantt/api__refs__gantt_events.html">이벤트 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AES 암호화를 활용한 익명 투표]]></title>
            <link>https://velog.io/@ssooo_kk_77/AES-%EC%95%94%ED%98%B8%ED%99%94%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B5%EB%AA%85-%ED%88%AC%ED%91%9C</link>
            <guid>https://velog.io/@ssooo_kk_77/AES-%EC%95%94%ED%98%B8%ED%99%94%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B5%EB%AA%85-%ED%88%AC%ED%91%9C</guid>
            <pubDate>Sat, 22 Mar 2025 06:54:43 GMT</pubDate>
            <description><![CDATA[<p>프로젝트의 기능 중 하나인 투표를 담당하면서 db에서 추적되지 않는 <code>익명</code>투표를 위해 암호화에 대해 공부하며 기록을 남기게 되었다. </p>
<p>전체적인 프로세스는 다음과 같다.
1️⃣ 투표를 익명으로 생성 시 db에서 추적이 불가능하게 하기 위해 먼저 사용자 id를 해시하고 
2️⃣ 이 값의 일부로 고유한 AES키를 생성해 서버에 전달한다. 
3️⃣ 이를 해당 투표의 고유 id로 정의하고 
4️⃣ 사용자가 투표 답변, 수정, 삭제 시 동일한 방법으로 서버에 고유한 키를 보내면 
5️⃣ 서버에 저장되어 있던 암호화된 투표 고유 id와 비교해 자신이 생성한 투표인지 아닌지를 판단할 수 있게 하였다.</p>
<p>따라서 사용자의 원래 id를 서버에 전달하지 않고 db에는 사용자를 식별할 수 있는 정보가 없으므로 익명성이 보장된다.</p>
<p>코드를 살펴보자면</p>
<pre><code>import forge from &quot;node-forge&quot;;

function getSignature(publicKey: string, content: string): string {
  const md = forge.md.sha256.create();
  md.update(publicKey);
  const key = md.digest().getBytes(16); // AES-128 사용</code></pre><p>먼저 사용자 ID를 해시하여 AES 키를 생성하고</p>
<pre><code>  const iv = &quot;0000000000000000&quot;;</code></pre><p>동일키에 동일 암호문이 나와야 하기 때문에 IV (초기화 벡터)를 고정시킨다.</p>
<pre><code>const cipher = forge.cipher.createCipher(&quot;AES-CBC&quot;, key);
cipher.start({ iv: iv });
cipher.update(forge.util.createBuffer(content));
cipher.finish();

const encrypted = cipher.output.getBytes();</code></pre><p>forge 라이브러리를 사용하여 AES-CBC 암호화 객체를 생성하고 위에서 만든 AES 키를 설정한다. 이 후 암호화된 데이터를 바이트 배열로 변환하여 저장한다.</p>
<pre><code> return forge.util.encode64(encrypted);</code></pre><p>최종으로 IV와 암호화된 데이터를 Base64로 인코딩하여 반환하는 함수를 만들었다.</p>
<p>이후 테스트를 진행하였는데 url로 이 키를 전달할 시 기호(?*^% 등)가 빈 문자열로 들어가는 이슈로 인해 투표를 생성한 사람과 동일함에도 매칭이 안되는 문제가 생겼다. </p>
<p>url-safe 문자열로 변환하기 위해 encodURIComponent로 한번 감싸서 보내 해결할 수 있었다. </p>
<pre><code> return encodeURIComponent(forge.util.encode64(encrypted));</code></pre><br />
<br />

<p>➕ 추가로 알게 된 것 ➕</p>
<p>AES는 대칭 키 암호화 방식으로 고정된 키와 초기화 벡터(IV)를 사용해 데이터를 암호화한다. </p>
<p>여기서 대칭키 암호 알고리즘과 비대칭키 암호 알고리즘을 비교하며 좀 더 알아보자면 다음과 같은 특징이 있다.</p>
<p>✔️ <strong>대칭키 암호 알고리즘</strong>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/1298f6bd-4bd0-46b2-b8bc-6858983a38c9/image.png" alt=""></p>
<ul>
<li>하나의 키로 암호화/복호화를 모두 수행하는 알고리즘</li>
<li>암호화하는 암호키와 복호화하는 해독키가 같다.</li>
<li>따라서 이 키는 절대 외부에 노출되면 안되고 해당 키를 Secret Key라고 부른다.</li>
<li>프론트엔드와 백엔드 간의 동일한 키를 공유해야하기 때문에 키 관리에 대한 어려움이 있고, 잦은 키 변경이 있는 경우에는 불편함을 초래할 가능성이 있다.</li>
<li>비대칭키 암호 알고리즘보다 속도가 빠르다.</li>
<li>키가 노출되면 암호화된 데이터를 복호화할 수 있다는 취약성이 있다.</li>
</ul>
<p> 
<br /></p>
<p>✔️ <strong>비대칭키 암호 알고리즘</strong>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/83dea08a-2a7c-4282-8eeb-fbd5de7a8e96/image.png" alt=""></p>
<p>- 암호화/복호화 시의 키가 서로 다른 키를 의미한다.</p>
<ul>
<li>공개키와 개인키를 사용한다.</li>
<li>한 쌍의 키가 존재하며 하나는 특정 사람만이 가지는 개인키이며 하나는 누구나 가질 수 있는 공개키이다.</li>
<li>예를들어 공개키로 암호화한 데이터는 개인키로만 복호화할 수 있고 반대로 개인키로 암호화한 데이터는 공개키로만 복호화할 수 있다.</li>
<li>누군가 공개키를 가로채더라도 개인키를 모르기 때문에 데이터를 온전히 복호화할 수 없다.</li>
<li>주로 키 교환이나 인증에 사용한다.</li>
<li>대칭키 암호 알고리즘에 비해 느리다.</li>
</ul>
<br />
<br />

<p><a href="https://choijying21.tistory.com/entry/CryptoJS%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A0%95%EB%B3%B4-%EC%95%94%ED%98%B8%ED%99%94%EB%B3%B5%ED%98%B8%ED%99%94%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8">참고</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[간트차트 빌드 에러 ]]></title>
            <link>https://velog.io/@ssooo_kk_77/%EA%B0%84%ED%8A%B8%EC%B0%A8%ED%8A%B8%EB%A5%BC-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94%EB%8F%84%EC%A4%91-%EB%A7%8C%EB%82%9C-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@ssooo_kk_77/%EA%B0%84%ED%8A%B8%EC%B0%A8%ED%8A%B8%EB%A5%BC-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94%EB%8F%84%EC%A4%91-%EB%A7%8C%EB%82%9C-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Mon, 17 Mar 2025 23:23:58 GMT</pubDate>
            <description><![CDATA[<h3 id="💥-에러-발생">💥 에러 발생</h3>
<p>처음 간트차트를 구현할 때 다른 라이브러리들 처럼 상단에 import를 해서 useEffect 내에서 초기화 후 렌더링하는 방식으로  로직을 짰다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/255eae3e-79c6-4c90-9831-0fd00c5de29d/image.png" alt=""></p>
<pre><code>const GanttChart: React.FC = () =&gt; {
    const ganttContainer = useRef&lt;HTMLDivElement&gt;(null);

 useEffect(() =&gt; {
        // Gantt 초기화
        if (ganttContainer.current) {

        ...

        gantt.init(ganttContainer.current);

          return () =&gt; {
          gantt.clearAll();
       };
    }
}, []);

return (
        &lt;div&gt;
            &lt;div ref={ganttContainer} style={{width: &quot;100%&quot;, height: &quot;400px&quot;}} /&gt;</code></pre><p>로컬에서는 문제가 없었는데 빌드를 하니 다음과 같은 에러가 발생하였다.</p>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/b779b4e8-b73a-409d-8941-2096cf4b8698/image.png" alt=""></p>
<br />

<h3 id="❓-에러-원인">❓ 에러 원인</h3>
<p>해당 프로젝트는 <code>Next</code> 프레임워크를 사용하고 있다.</p>
<p>dhtmlx-gantt는 DOM을 직접 조작하는 라이브러리이고 Next는 서버사이드 렌더링(SSR)을 수행하므로 DOM이 없는 서버에서 dhtmlx-gantt를 실행하려고 했던 것이 원인이었다.</p>
<br />

<h3 id="❗에러-해결">❗에러 해결</h3>
<p>1️⃣ <code>useEffect</code>와 <code>typeof window</code>를 사용하여 클라이언트 환경에서만 실행되도록 한다.
2️⃣ Next는 import 구문을 SSR 단계에서 처리하므로 클라이언트 환경에서 동적으로 로드하기 위해 <code>required</code>를 사용한다.</p>
<p>최종 코드</p>
<pre><code>import React, { useEffect, useRef } from &quot;react&quot;;
import &quot;dhtmlx-gantt/codebase/dhtmlxgantt.css&quot;;

const GanttChart: React.FC = () =&gt; {
    const ganttContainer = useRef&lt;HTMLDivElement&gt;(null);

    useEffect(() =&gt; {
        if (typeof window !== &quot;undefined&quot; &amp;&amp; ganttContainer.current) {
            const gantt = require(&quot;dhtmlx-gantt&quot;);
            if (ganttContainer.current) {
                gantt.init(ganttContainer.current);
                gantt.parse({
                    data: [
                        { id: 1, text: &quot;Task #1&quot;, start_date: &quot;01-04-2023&quot;, duration: 5, progress: 0.6 },
                        { id: 2, text: &quot;Task #2&quot;, start_date: &quot;06-04-2023&quot;, duration: 3, progress: 0.4, parent: 1 },
                    ],
                });
            }
        }
    }, []);

    return &lt;div ref={ganttContainer} style={{ width: &quot;100%&quot;, height: &quot;400px&quot; }} /&gt;;
};

export default GanttChart;</code></pre><br />

<p>수정 후 다시 시도하니 빌드가 자알 된다!😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[비전공자 개발자의 CS 공부기록(1)]]></title>
            <link>https://velog.io/@ssooo_kk_77/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-CS-%EA%B3%B5%EB%B6%80%EA%B8%B0%EB%A1%9D1</link>
            <guid>https://velog.io/@ssooo_kk_77/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-CS-%EA%B3%B5%EB%B6%80%EA%B8%B0%EB%A1%9D1</guid>
            <pubDate>Mon, 17 Feb 2025 01:10:31 GMT</pubDate>
            <description><![CDATA[<h2 id="클라이언트-기반-데이터-저장-방식">클라이언트 기반 데이터 저장 방식</h2>
<h3 id="📌-쿠키">📌 쿠키</h3>
<ul>
<li><p>사용자의 브라우저에 저장되는 작은 데이터 조각으로 필요할 때 쉽게 데이터를 받아 사용 가능하다.</p>
</li>
<li><p>만료 기간이 정해져 있지 않은 쿠키인 <code>세션 쿠키</code>(Session cookie)와 만료 기간이 존재하는 <code>영구 쿠키</code>(persistent cookie)가 있다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/7abe2f6b-0941-4c5c-b640-ee78ffb448a5/image.png" alt="">
(위 사진에서 theme 세션이 세션 쿠키, refresh_token이 영구 쿠키에 해당한다.)</p>
</li>
<li><p>세션 쿠키는 브라우저가 종료될 때 함께 사라지는 반면, 영구 쿠키는 만료기간이 끝나면 삭제된다.</p>
</li>
<li><p>동작
  1️⃣ 클라이언트가 서버에 웹 페이지를 요청하면 서버는 요청에 대한 응답과 함께 쿠키정보를 전송한다.
  2️⃣ 응답을 받은 클라이언트는 메시지 안에 있던 쿠키 정보를 브라우저 공간에 저장한다.
  3️⃣ 이후부터 클라이언트에서 서버로 보내는 모든 요청에 쿠키 정보를 담아 전달한다.
  4️⃣ 서버는 이러한 요청 메시지의 쿠키 정보를 확인해 사용자를 식별하거나 적절한 데이터를 보내준다.</p>
</li>
<li><p>단점</p>
<ul>
<li>누구나 쉽게 접근이 가능하고 조작할 수 있기 때문에 탈취되거나 변조될 위험이 크다.</li>
<li>요청마다 메시지에 쿠키 정보가 함께 전송되기 때문에 오가는 데이터의 양도 더 커져 웹 통신의 성능이 떨어지는 원인이 될 수 있다.</li>
<li>사용자가 방문한 웹 사이트에서 직접 발행하는 <code>퍼스트 파티 쿠키</code>와 제삼자가 발행한 <code>서드 파티 쿠키</code>로 구분할 수 있다.</li>
<li><code>서드파티 쿠키</code>는 사용자로부터 충분한 동의를 받지 않고 웹 브라우징 활동을 추적해 표적 광고에 이용하기 때문에 개인 정보 침해의 소지가 있다.</li>
<li>개인 정보와 같은 민감한 데이터보다는 팝업 다시 보지 않기, 다크 모드 여부 등의 용도로 쿠키를 사용할 수 있다.</li>
</ul>
</li>
</ul>
<br />

<h3 id="📌-로컬-스토리지-vs-세션-스토리지">📌 로컬 스토리지 vs 세션 스토리지</h3>
<ul>
<li><p><strong>로컬 스토리지</strong></p>
<ul>
<li>한 번 브라우저에 저장되면 의도적으로 삭제하지 않는 이상 데이터가 유지된다.</li>
<li>자동 로그인 등의 용도로 사용할 수 있다.</li>
</ul>
</li>
<li><p><strong>세션 스토리지</strong></p>
<ul>
<li>브라우저 탭이 닫힐 때 데이터도 함께 삭제된다.</li>
<li>일회성 정보의 데이터 저장 용도로 활용할 수 있다.</li>
</ul>
</li>
</ul>
<br />

<h3 id="📌-쿠키와-웹-스토리지-비교">📌 쿠키와 웹 스토리지 비교</h3>
<table>

  <thead>
    <th>
    쿠키
    </th>
    <th>웹 스토리지</th>
  </thead>
  <tbody>
    <tr>
      <td>
        4KB까지 저장이 가능하다.
      </td>
      <td>
        5MB ~ 10MB까지 저장이 가능하다.
      </td>
    </tr>
       <tr>
      <td>
        클라이언트와 서버 양쪽 모두 값에 접근 가능하다.
      </td>
      <td>
        클라이언트 단에서만 접근 가능하다.
      </td>
    </tr>
      <tr>
      <td>
        HTTP 헤더에 담겨 서버로 전송된다.
      </td>
      <td>
        따로 서버 전송을 하지 않기 때문에 네트워크 통신량이 줄어든다.
      </td>
    </tr>
  </tbody></table>

<br />

<h3 id="📌-index-db">📌 Index DB</h3>
<ul>
<li>데이터의 형태가 문자열로 한정된 쿠키나 웹 스토리지에 반해, Key Value 한 쌍을 저장하며 , File/Blob 포함 다양한 자바스크립트 데이터 타입 지원한다. </li>
<li>용량 제한이 특별히 없다.</li>
<li>복잡한 구조의 데이터나 큰 규모의 데이터를 괸리해야할 때 유용하게 사용할 수 있다.</li>
<li>same origin policy 를 따르며 온라인/오프라인 환경 모두에서 쿼리 지원한다.</li>
</ul>
<br />
<br />

<h2 id="서버-기반-데이터-저장-방식">서버 기반 데이터 저장 방식</h2>
<h3 id="📌-세션">📌 세션</h3>
<ul>
<li>정보를 서버에서 관리하기 때문에 보안이 우수하다.</li>
<li>클라이언트별로 고유 ID를 부여해 관리하기 때문에 각 클라이언트의 요구에 맞는 맞춤형 서비스를 제공할 수 있다.</li>
<li>사용자가 많아지거나 관리해야 하는 데이터의 양이 많아지면 서버의 부담이 커진다는 단점이 있다.</li>
<li>브라우저에서 값을 보여주는 것보다 상대적으로 데이터 제공 속도가 느릴 수 있다.</li>
<li>동작
  1️⃣ 로그인한 사용자의 정보를 서버에 전달한다.
  2️⃣ 서버는 요청을 보낸 클라이언트가 이전에 접근한 적이 있는지 확인하고 없다면 고유한 값인 세션 ID를 발급해 자신의 메모리에 저장하고 요청에 대한 응답과 함께 세션 ID를 클라이언트에 전달한다.
  3️⃣ 클라이언트는 전달받은 세션 ID를 쿠키에 저장한다.
  <img src="https://velog.velcdn.com/images/ssooo_kk_77/post/a6b9d6c8-8389-41e5-bcfa-81a4ef24d02b/image.jpg" alt="">
  4️⃣ 가령 사용자가 장바구니 api를 요청하면 클라이언트는 요청과 함께 쿠키 안의 세션ID를 함께 전달한다.
  5️⃣ 요청을 받은 서버는 메모리에서 자신이 받은 세션ID에 해당하는 클라이언트가 있는지 확인하고 있다면 장바구니 정보를 조회해 응답 데이터로 전송한다.
  <img src="https://velog.velcdn.com/images/ssooo_kk_77/post/cd1615e2-1d7f-4f2a-a29d-d8257bf8d10d/image.jpg" alt=""></li>
</ul>
<br />
<br />

<p><a href="https://product.kyobobook.co.kr/detail/S000201603021">이미지 출처</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[엑셀 파일 업로드를 구현해보자]]></title>
            <link>https://velog.io/@ssooo_kk_77/%EC%97%91%EC%85%80-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@ssooo_kk_77/%EC%97%91%EC%85%80-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 02 Jun 2024 07:23:29 GMT</pubDate>
            <description><![CDATA[<p>이번에 구현하게 된 기능은 엑셀 파일 업로드이다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/68c2a098-774c-47c9-8157-9066f065595d/image.gif" alt=""></p>
<br />
<br />


<p>먼저 input을 정의해주고 onChange 함수 작성</p>
<pre><code>&lt;input type=&quot;file&quot; accept=&quot;.xlsx, .xls&quot; onChange={handleFileChange} /&gt;</code></pre><pre><code>const uploadedFileRefPut = useRef&lt;File | null&gt;(null);</code></pre><pre><code>const handleFileChange = useCallback(async (event, fileRef) =&gt; {
    const selectedFile = event.target.files?.[0];

    if (selectedFile) {
       fileRef.current = selectedFile;
       const reader = new FileReader();
       reader.onload = async (e: ProgressEvent&lt;FileReader&gt;) =&gt; {
           if (e.target &amp;&amp; e.target.result) {
              const data = new Uint8Array(e.target.result as ArrayBuffer);
              const workbook = XLSX.read(data, { type: &quot;array&quot;, bookVBA: true });
              const worksheet = workbook.Sheets[workbook.SheetNames[0]];
              const jsonData: ExcelData[] = XLSX.utils.sheet_to_json(worksheet);
          }
      };

        reader.readAsArrayBuffer(selectedFile);
   }
}, []);</code></pre><br />

<p>버튼 클릭 시 업로드를 수행 할 함수 작성</p>
<pre><code>// 엑셀 파일로 일괄 수정
    const handlePutExcel = (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
        e.preventDefault();

        if (!uploadedFileRefPut.current) {
            toast.error(&quot;엑셀 파일을 업로드 해주세요.&quot;);
            return;
        }

        const formData = new FormData();

        formData.append(&quot;file&quot;, uploadedFileRefPut.current);
        uploadUserExcelPut(formData);
    };</code></pre><br />
<br />

<hr>
<br />

<p>최종 구현된 코드는 위와 같지만 초기에 upload할 파일을 useState로 상태를 관리하고 있었다.</p>
<pre><code>const [uploadedFile, setUploadedFile] = useState&lt;File | null&gt;(null);</code></pre><pre><code>  const jsonData: ExcelData[] = XLSX.utils.sheet_to_json(worksheet);
  setUploadedFile(jsonData);
}</code></pre><p>이렇게 onChange 됐을 때 setUploadedFile로 상태를 업데이트 해 주었음에도 업로드 버튼을 클릭하면 계속 엑셀 파일이 없다는 toast 에러가 뜨는 문제가 발생했다.</p>
<p>디버깅을 해보니 uploadedFile이 null로 찍히고 있었다...
왜 null이지? 나는 분명 setUploadedFile로 업데이트를 해주었는데...</p>
<br />

<p>한참 헤맨 끝에 찾은 원인은 uploadedFile이 <code>클로저</code>이기 때문이었다.</p>
<p>❔ <strong>원인</strong>
엑셀 파일 업로드라는 버튼을 클릭하면 input이 있는 모달이 뜨는데 모달이 열린 시점에는 uploadedFile 상태를 클로저로 기억하고 있어서 상태가 업데이트된 이후에도 null 값을 유지하고 있었다.</p>
<br />

<p>두 가지의 해결 방법이 있다.</p>
<br />

<p>❕<strong>방법 1</strong> </p>
<p>uploadedFile 상태가 변경될 때마다 handleUpload 함수를 새로 정의하기 위해 useCallback으로 감싸고 의존성 배열에 uploadedFile을 추가하기</p>
<pre><code>    const handleUpload = useCallback(async () =&gt; {
        if (!uploadedFile) {
            toast.error(&quot;엑셀 파일을 업로드 해주세요.&quot;);
            return;
        }

        const formData = new FormData();
        formData.append(&quot;file&quot;, uploadedFile);

        await uploadUserExcel(formData);
    }, [uploadedFile]);</code></pre><br />

<p>❕<strong>방법 2</strong> </p>
<p>useRef를 사용하여 파일 상태를 저장하고 이를 참조하여 항상 최신 상태를 유지하는 방법</p>
<pre><code>위의 최종 코드와 동일</code></pre><p>두 방법 모두 상태가 변경될 때마다 handleUpload 함수가 최신 상태를 참조하게 되어 uploadedFile 상태의 동기화를 유지할 수 있게 된다!</p>
<p>나는 상태변경 이후 추가적인 UI의 변경이 없고 리렌더링이 필요하지 않았기 때문에 두 번째 방법으로 구현하기로 했다!</p>
<p>자바스크립트의 기본기가 중요하다는 것을 다시금 느끼게 된 과정이었다.</p>
<br />
<br />

<p>🧷 <a href="https://poiemaweb.com/js-closure">참고</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Sortablejs를 이용한 Drag and Drop 구현]]></title>
            <link>https://velog.io/@ssooo_kk_77/Sortablejs%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Drag-and-Drop-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@ssooo_kk_77/Sortablejs%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Drag-and-Drop-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 02 Jun 2024 06:21:46 GMT</pubDate>
            <description><![CDATA[<br />

<p>처음 구현하는 기능은 언제나 떨리고 조금 설렌다🤓</p>
<p>프론트엔드에서 흔하게 접할 수 있는 <strong>Drag and Drop</strong> 기능과 새로 정렬된 리스트의 순서를 서버에 바로 반영해 새로고침해도 정렬이 유지되는 기능 구현해보자!</p>
<p>내가 사용한 라이브러리는 <strong>react-sortablejs</strong>이다.</p>
<br />

<h3 id="1️⃣-설치">1️⃣ 설치</h3>
<pre><code>npm i react-sortablejs</code></pre><br />

<h3 id="2️⃣-drag-and-drop-구현">2️⃣ Drag and Drop 구현</h3>
<p>깃헙 공식 문서에 나와있는 기본형은 아래와 같다.</p>
<pre><code>import React, { FC, useState } from &quot;react&quot;;
import { ReactSortable } from &quot;react-sortablejs&quot;;

interface ItemType {
  id: number;
  name: string;
}

export const BasicFunction: FC = (props) =&gt; {
  const [state, setState] = useState&lt;ItemType[]&gt;([
    { id: 1, name: &quot;shrek&quot; },
    { id: 2, name: &quot;fiona&quot; },
  ]);

  return (
    &lt;ReactSortable list={state} setList={setState}&gt;
      {state.map((item) =&gt; (
        &lt;div key={item.id}&gt;{item.name}&lt;/div&gt;
      ))}
    &lt;/ReactSortable&gt;
  );
};</code></pre><p>오 생각보다 단순하다..!</p>
<pre><code>import { ReactSortable } from &quot;react-sortablejs&quot;;</code></pre><pre><code>const [list, setList] = useState(lists);

&lt;ReactSortable tag={&quot;ul&quot;} animation={100} list={list.map((item) =&gt; ({ ...item, id: item.tid }))} setList={setList} className=&quot;flex list-none flex-col gap-[20px]&quot; onEnd={handleDragEnd}&gt;
            {list?.map((item, index) =&gt; (
                &lt;TopicList
                    index={index}
                /&gt;
            ))}
&lt;/ReactSortable&gt;</code></pre><br />

<p>전체 리스트 목록을 ReactSortable 컴포넌트로 감싸고 필요한 속성 값을 지정해주면 끝!</p>
<p>다양한 옵션을 제공하는데 그 중 사용한 것에 대해 간략히 정리를 하자면</p>
<ul>
<li><p><strong>tag</strong>
ReactSortable 컴포넌트가 어떤 HTML 태그나 컴포넌트로 렌더링될지를 지정하는 속성이다. 나는 TopicList가 li태그이기 때문에 ul로 지정해주었다.</p>
</li>
<li><p><strong>animation</strong>
항목을 정렬할 때 이동하는 애니메이션의 속도를 밀리초(ms) 단위로 설정.
만약 animation={0}으로 설정하면 애니메이션 없이 즉시 이동된다.</p>
</li>
<li><p><strong>list</strong>
ReactSortable에서 사용할 항목 배열로 필수로 지정해주어야 하는 속성이다.</p>
</li>
<li><p><strong>setList</strong>
항목 목록의 상태를 설정하는 함수로 정렬된 후의 새로운 상태를 받아 컴포넌트의 상태를 업데이트해준다. list와 마찬가지로 필수로 지정해주어야 한다.</p>
</li>
<li><p><strong>onEnd</strong>
<code>onEnd?: ((evt: Sortable.SortableEvent, sortable: Sortable | null, store: Store) =&gt; void)</code></p>
<p>ReactSortable 컴포넌트에서 드래그 앤 드롭 작업이 끝났을 때 호출되는 콜백 함수를 지정하고자 할 때 사용하는 속성이다. 이 콜백 함수는 SortableEvent 객체를 매개변수로 받아 드래그된 항목과 그 위치 변경에 대한 정보를 제공한다.</p>
<p>나는 드래그가 끝나고 변경된 위치를 서버에 반영해 새로고침해도 정렬이 유지되도록 구현하고 싶었기 때문에 handleDragEnd라는 콜백 함수를 지정해주었다. </p>
</li>
</ul>
<br />
<br />

<h3 id="3️⃣-정렬-구현">3️⃣ 정렬 구현</h3>
<pre><code>const handleDragEnd = (event) =&gt; {
    const updatedList = [...list];
    const movedItem = updatedList.splice(event.oldIndex, 1);
    updatedList.splice(event.newIndex, 0, movedItem);
    setList(updatedList);
};</code></pre><p>SortableEvent 객체의 주요 속성은 다음과 같다.</p>
<ul>
<li><p>item : 드래그된 DOM 요소</p>
</li>
<li><p>from : 드래그 시작 위치의 DOM 요소</p>
</li>
<li><p>to : 드롭된 위치의 DOM 요소</p>
</li>
<li><p>oldIndex : 드래그 시작 전의 요소 인덱스</p>
</li>
<li><p>newIndex : 드래그 후의 요소 인덱스</p>
<ol>
<li>먼저 리스트 배열의 복사본을 만들고 </li>
<li>복사본을 splice메소드를 사용해 oldIndex 속성으로 드래그 한 요소를 제거한다.</li>
<li>복사본을 다시 splice 메소드로 드래그한 후 인덱스 위치에 제거한 요소를 삽입한 후 setList로 상태를 업데이트 해준다. </li>
</ol>
</li>
</ul>
<br />
<br />

<h3 id="4️⃣-최종">4️⃣ 최종</h3>
<p>새로 정렬된 리스트를 서버에 반영하면 새로고침해도 정렬된 리스트가 유지된다!</p>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/a01c311d-712a-47e7-a8de-b90a18b38852/image.gif" alt=""></p>
<br />
<br />


<h4 id="🧷-참고">🧷 참고</h4>
<ul>
<li><a href="https://github.com/SortableJS/react-sortablejs">react-sortablejs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[브라우저 렌더링 과정에 대해 알아보자]]></title>
            <link>https://velog.io/@ssooo_kk_77/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@ssooo_kk_77/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 07 Mar 2024 04:03:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/74d6787c-66a5-45cf-9f59-4d14337b01e2/image.png" alt=""></p>
<h3 id="1-dom-트리-빌드">1. DOM 트리 빌드</h3>
<p>이전 단계에서 통신을 통해 받아온 html 파일들은 바이트 형태로 전달된다.
바이트 &gt; 문자 &gt; 토큰 &gt; 노드 &gt; 객체 모델로 전환하는 작업이 수행되고 최종 트리 형태의 DOM이 출력된다.
    + 만약 html 파싱 중 link 태그나 style 태그를 만날 경우 블로킹되어 CSSOM 생성으로 넘어간다.
    <img src="https://velog.velcdn.com/images/ssooo_kk_77/post/3294a624-4236-4ffa-be37-8f9d9eaba8c4/image.png" alt=""></p>
<h3 id="2-cssom-트리-빌드">2. CSSOM 트리 빌드</h3>
<p>css 파일도 html 파일과 마찬가지로 트리 형태의 객체 모델로 전환하는 과정을 거친다. css 파싱이 완료되면 html 파싱이 중단된 시점으로 돌아가 다시 html을 파싱한다.
    <img src="https://velog.velcdn.com/images/ssooo_kk_77/post/5d67e6c5-8679-4c63-92d4-a18c716aa538/image.png" alt=""></p>
<h3 id="3-render-tree-생성">3. Render Tree 생성</h3>
<p>기존에 제작된 DOM과 CSSOM을 결합하여 Render Tree를 생성한다.
    + 화면에 렌더링되는 노드만으로 구성된다. (display : none이나 meta 태그 등은 렌더트리를 구성하지 않음)
    <img src="https://velog.velcdn.com/images/ssooo_kk_77/post/4b12c002-2bb2-4dad-b110-d0cd203a3a6e/image.png" alt=""></p>
<h3 id="4-js-파싱-후-ast-생성">4. JS 파싱 후 AST 생성</h3>
<p>html 파싱 중 js 즉, script 태그를 만나게 되면 blocking되어 제어권을 JS엔진에게 넘겨주어 JS 파싱으로 넘어간다. 
소스코드를 토큰으로 분해하고 파싱해 AST라는 추상적 구문 트리로 생성한 뒤 인터프리터가 읽을 수 있도록 바이트코드를 생성하여 실행한다. </p>
<p>이후 js 파싱과 실행이 종료되면 렌더링 엔진으로 다시 제어권이 넘어가 html 파싱이 중단된 시점부터 다시 시작하여 dom 생성을 재개한다.
    <img src="https://velog.velcdn.com/images/ssooo_kk_77/post/82bbcd36-5896-4ccb-92f3-6adb486eb061/image.png" alt=""></p>
<h3 id="5-layout">5. Layout</h3>
<p>Render Tree의 노드들의 위치와 크기를 계산하는 단계</p>
<h3 id="6-paint">6. Paint</h3>
<p>계산된 값들을 기반으로 화면에 필요한 요소들을 실제로 그리는 작업을 실행한다.</p>
<h3 id="7-reflow--repaint">7. Reflow &amp; Repaint</h3>
<p>아래 3가지의 경우와 같이 html 요소의 크기나 위치를 변경해야 하는 경우가 발생하면 렌더링 트리의 요소의 크기와 위치를 다시 계산하는 reflow 과정을 거쳐 다시 페인팅하는 repaint 가 발생한다. 
    + 리렌더링 조건
    1. 자바스크립트에 의한 노드 추가 또는 삭제
    2. 브라우저 창 리사이징에 의한 뷰포트의 크기 변경
    3. html 요소의 레이아웃에 변경을 발생시키는 width, height 등의 스타일 변경</p>
<h4 id="-composition">++ Composition</h4>
<p>레아아웃과 페인트를 수행하지 않고 레이어의 합성만 실행시키는 단계
Transform, opacity와 같은 요소들을 의미한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 브라우저에 해당 도메인 주소를 입력했을 때 데이터를 받아오는 과정]]></title>
            <link>https://velog.io/@ssooo_kk_77/%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%97%90-%ED%95%B4%EB%8B%B9-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%A3%BC%EC%86%8C%EB%A5%BC-%EC%9E%85%EB%A0%A5%ED%96%88%EC%9D%84-%EB%95%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%B0%9B%EC%95%84%EC%98%A4%EB%8A%94-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@ssooo_kk_77/%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%97%90-%ED%95%B4%EB%8B%B9-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%A3%BC%EC%86%8C%EB%A5%BC-%EC%9E%85%EB%A0%A5%ED%96%88%EC%9D%84-%EB%95%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%B0%9B%EC%95%84%EC%98%A4%EB%8A%94-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Wed, 06 Mar 2024 10:33:19 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/12abf2a9-9bd8-44b2-b8f8-e8dfbd15daa3/image.png" alt=""></p>
<ol>
<li><p>주소창에 url을 입력하면 DNS서버(domain name system의 약자로 도메인 이름과 IP 주소를 매핑해주는 서버)에 요청하기 전에 먼저 CDN에 캐싱된 dns 기록들을 확인한다.</p>
<ul>
<li>CDN : <code>콘텐츠 전송 네트워크</code>로 여러 개의 서버를 이용해 웹 콘텐츠를 사용자와 가까운 서버에서 전송함으로써 전송 속도를 높인다는 장점을 가진다. AWS나 SK브로드밴드, KT 등의 예가 있다.</li>
</ul>
</li>
<li><p>만약 캐싱된 기록이 있으면 요청을 보내지 않고 바로 ip 주소를 반환하고 없으면 dns 서버 요청으로 넘어간다.</p>
</li>
<li><p>가장 가까운 dns 서버에서 도메인 이름에 해당하는 IP 주소를 찾기 위해 dns query를 전달한다. </p>
</li>
<li><p>dns query는 현재 dns 서버에서 원하는 ip 주소가 존재하지 않으면 다른 dns 서버를 방문하는 과정을 원하는 IP 주소를 찾을 때까지 반복한다.</p>
</li>
<li><p>찾던 IP 주소를 전달 받으면 이 IP 주소를 이용하여 웹 브라우저는 웹 서버에게 해당 웹 사이트에 맞는 html 문서를 요청한다.</p>
</li>
<li><p>해당 HTTP 요청 메세지는 TCP/IP 프로토콜을 사용하여 서버로 전송된다.</p>
</li>
<li><p>전송 제어 프로토콜인 TCP는 데이터의 전송을 제어하고 데이터를 어떻게 보낼지 어떻게 맞출지? 정한다.</p>
</li>
<li><p>IP의 특징인 비신뢰성과 비연결성으로 인해 IP 프로토콜 만으로는 통신을 할 수 없기 때문에 신뢰성과 연결성을 책임지는 TCP를 활용하여 통신한다.</p>
</li>
<li><p>TCP는 <code>3-웨이 핸드셰이크</code>라는 작업을 진행하면서 연결 및 데이터를 수신받는다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/d6cd1d34-18ed-4ddf-828a-a44f41750852/image.jpg" alt=""></p>
<ol>
<li>SYN단계 : 클라이언트는 서버에 클라이언트의 ISN을 담아 SYN을 보낸다. </li>
<li>SYN + ACK 단계 : 서버는 클라이언트의 SYN을 수신하고 서버의 ISN을 보내며 승인번호로 클라이언트의 ISN + 1을 보낸다.</li>
<li>ACK 단계 : 클라이언트는 서버의 ISN + 1한 값인 승인번호를 담아 ACK를 서버에 보낸다.
이렇게 3-웨이 핸드셰이킹 과정 이후 신뢰성이 구축되고 데이터 전송을 시작한다.</li>
</ol>
<blockquote>
<ul>
<li>SYN : SYNchronization의 약자, 연결 요청 플래그<ul>
<li>ACK : Acknowledgement의 약자, 응답 플래그</li>
<li>ISN : Initial Sequence Numbers의 약자로 초기 네트워크 연결을 할 때 할당된 32비트의 고유 시퀀스 번호</li>
</ul>
</li>
</ul>
</blockquote>
</li>
<li><p><code>4-웨이 핸드셰이크</code>라는 과정을 통해 연결을 종료한다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/73798016-e437-4b31-8d80-fd2f53a8acbf/image.jpg" alt=""></p>
<ol>
<li>클라이언트가 연결을 닫으려고 할 때 FIN으로 설정된 세그먼트를 보낸다.</li>
<li>서버는 클라이언트로 ACK라는 승인 세그먼트를 보내고 자신의 통신이 끝날 때까지 기다린다.</li>
<li>서버는 일정 시간 이후 클라이언트에 FIN이라는 세그먼트를 보낸다.</li>
<li>클라이언트는 확인했다는 메세지인 ACK를 보내고 서버는 CLOSED 상태가 된다. 이후 클라이언트는 어느 정도의 시간을 대기한 후 연결이 닫히고 클라이언트와 서버의 모든 자원의 연결이 해제된다.</li>
</ol>
</li>
<li><p>특정 데이터 요청을 브라우저로부터 받게되면 웹 서버는 페이지의 로직이나 데이터베이스 연동을 위해 WAS에게 이들의 처리를 요청한다. 웹 서버의 과부하 방지를 위한 WAS(사용자의 컴퓨터나 장치에 웹 어플리케이션을 수행해주는 미들웨어)는 해당 요청을 통해 동적인 페이지 처리를 담당하고 DB에서 필요한 데이터 정보를 받아 그에 맞는 파일을 생성한다.</p>
<blockquote>
<p>웹 서버 : 정적인 파일(HTML, CSS, 이미지 파일)을 처리
WAS : 동적인 파일(JS, TS)을 처리</p>
</blockquote>
</li>
<li><p>WAS에서의 작업 처리 결과들을 웹 서버로 전송하고, 웹 서버는 웹 브라우저에게 html 문서 결과를 전달한다.
전달 과정에서 status code를 통해 서버 요청에 따른 결과 및 상태를 전달한다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/9bb6ef05-87de-402f-8377-0ec6e22c72c9/image.webp" alt=""></p>
</li>
</ol>
<ul>
<li>1xx : 정보가 담긴 메세지</li>
<li>2xx : response 성공</li>
<li>3xx : 클라이언트를 다른 URL로 리다이렉트</li>
<li>4xx : 클라이언트 측에서 에러 발생</li>
<li>5xx : 서버 측에서 에러 발생</li>
</ul>
<br />

<p>💡</p>
<ul>
<li><a href="https://velog.io/@tnehd1998/%EC%A3%BC%EC%86%8C%EC%B0%BD%EC%97%90-www.google.com%EC%9D%84-%EC%9E%85%EB%A0%A5%ED%96%88%EC%9D%84-%EB%95%8C-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-%EA%B3%BC%EC%A0%95">참고</a></li>
<li><a href="https://thebook.io/080326/0079/">참고</a></li>
<li><a href="https://brunch.co.kr/@danni/5">참고</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[requestAnimationFrame을 사용해 ProgressBar 만들기]]></title>
            <link>https://velog.io/@ssooo_kk_77/requestAnimationFrame%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4-ProgressBar-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@ssooo_kk_77/requestAnimationFrame%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4-ProgressBar-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 18 Jan 2024 14:31:25 GMT</pubDate>
            <description><![CDATA[<h2 id="⚒️-setinterval과-requestanimationframe">⚒️ setInterval과 requestAnimationFrame</h2>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/bd8d7445-19e5-4fca-a18f-045e1f842264/image.gif" alt=""></p>
<p>첫 번째, 두 번째 막대는 각각 setInterval과 requestAnimationFrame을 이용해서 너비값을 증가시키는 애니메이션이다.</p>
<p>setInterval을 이용한 막대 그래프는 requestAnimationFrame을 이용한 막대 그래프에 비해 일정한 간격으로 실행이 되지 않고 프레임이 유연하지 못한 것을 확인할 수 있다.</p>
<ul>
<li><p>setInterval을 사용한 애니메이션</p>
<pre><code>const startInterval = () =&gt; {
  const interval = document.querySelector(&#39;#interval&#39;) as HTMLDivElement;

  const id = setInterval(() =&gt; {
    interval.style.width = `${intervalWidth}px`;
    intervalWidth += 1;

    if (intervalWidth &gt; window.innerWidth) clearInterval(id);
  }, 1000 / 60);
};</code></pre></li>
<li><p>requestAnimationFrame을 사용한 애니메이션</p>
<pre><code>const startAnimationFrame = () =&gt; {
  const animationFrame = document.querySelector(
    &#39;#animationFrame&#39;
  ) as HTMLDivElement;

  animationFrame.style.width = `${requestWidth}px`;
  requestWidth += 1;
  const id = requestAnimationFrame(startAnimationFrame);
  if (requestWidth &gt; window.innerWidth) cancelAnimationFrame(id);
};</code></pre></li>
</ul>
<p>이렇게 다르게 동작하는 이유는 setInterval로 width를 자주 변경하게 되면 자바스크립트가 실행되는 동안(브라우저에서 다른 작업이 처리되는 동안) 레이아웃 변경이 지정된 간격보다 더 늦게 또는 더 빠르게 실행될 수 있다. 즉 간격이 일정하지 못하게 되고 실제 화면에 렌더링 되는 프레임의 수가 줄어드는 frame drop 현상으로 이어지게 된다. 이로인해 버벅이는 듯한 애니메이션으로 이어지게 되는 것이다.</p>
<p>반면 <code>requestAnimationFrame은 브라우저의 렌더링 주기와 동기화 돼서 실행</code>이된다. requestAnimationFrame 콜백에 DOM을 변경하는 함수를 넣어주면 해당 변경은 브라우저의 렌더링 주기에 최적화된 방식으로 반영이 되기 때문에 setInterval보다 requestAnimation을 사용할 때 더 유연한 애니메이션을 제공할 수 있게된다.</p>
<br />
<br/ >

<h2 id="✔️-requsetanimationframe으로-scroll에-따른-progessbar-구현하기">✔️ requsetAnimationFrame으로 scroll에 따른 progessBar 구현하기</h2>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/fe0849ae-1861-4a6c-8b00-d3cb1ff8a68a/image.gif" alt=""></p>
<p>비용이 많이 드는 레이아웃 변경 대신 transform의 scaleX로 progressBar의 너비값이 변하도록 적용했다. </p>
<ul>
<li><p>전체 코드</p>
<pre><code>function ScrollProgressBar() {
  const [progess, setProgress] = useState(0);
  const rafRef = useRef&lt;number | null&gt;(null);

  useEffect(() =&gt; {
      const scroll = () =&gt; { 
          const scrollTop = document.documentElement.scrollTop
          // 문서 전체 높이 - 뷰포트 높이 값
          const height = document.documentElement.scrollHeight - document.documentElement.clientHeight

          // 중복된 작업이 반복적으로 일어나지 않도록 방지코드
          if (rafRef.current) {
              cancelAnimationFrame(rafRef.current)    
          }

          rafRef.current = requestAnimationFrame(() =&gt; {
              setProgress(scrollTop / height)
          })
      }

      window.addEventListener(&#39;scroll&#39;, scroll)

      return () =&gt; {
          if (rafRef.current) {
              cancelAnimationFrame(rafRef.current)    
          }

          window.removeEventListener(&#39;scroll&#39;, scroll)
      }
  },[])
  return (
      &lt;div style={{ transform : `scaleX(${progess})`, transformOrigin : &#39;left&#39;, 
      backgroundColor:&#39;&#39;, height : 8, position :&#39;sticky&#39;, 
      top : 62,  zIndex:999, background: &#39;rgb(255 196 35)&#39;}}&gt;

      &lt;/div&gt;
  )
}</code></pre><p>💡</p>
</li>
<li><p><a href="https://stackblitz.com/edit/stackblitz-starters-ht4nty?description=React%20%20%20TypeScript%20starter%20project&amp;file=src%2FApp.tsx&amp;title=React%20Starter">참고</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트를 최적화하는 다양한 방법 - lazy loading]]></title>
            <link>https://velog.io/@ssooo_kk_77/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@ssooo_kk_77/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 17 Jan 2024 08:54:00 GMT</pubDate>
            <description><![CDATA[<h2 id="✔️-코드-스플리팅code-splitting">✔️ 코드 스플리팅(Code-Splitting)</h2>
<p>: 커다란 번들을 작은 여러개의 청크로 나누는 과정으로 코드 스플리팅을 통해 유저가 필요하지 않은 코드들을 다운 받지 않도록 할 수 있다. 
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/4cec549f-a3ee-4e98-b44f-e84840682bfc/image.webp" width="600" /></p>
<br />

<h3 id="➕-코드-스플리팅-적용-시-이점">➕ 코드 스플리팅 적용 시 이점</h3>
<p>: 어플리케이션 규모가 커질수록 번들 사이즈는 증가하게 되고 초기 로딩 속도가 증가할수 밖에 없다. 이때 코드 스플리팅을 적용하면 <code>현재 필요한 코드만 다운 받을 수 있게 되어 첫 페이지 랜딩 속도를 더 빠르게 향상</code>시켜 사용자 경험을 향상시킬 수 있다.</p>
<blockquote>
<p>&lt;공식 문서&gt;
앱의 코드 양을 줄이지 않고도 사용자가 필요하지 않은 코드를 불러오지 않게 하며 앱의 초기화 로딩에 필요한 비용을 줄여줍니다.</p>
</blockquote>
<br />

<h2 id="➕-reactlazy-적용하기">➕ React.lazy 적용하기</h2>
<h4 id="1️⃣-import-하기">1️⃣ import 하기</h4>
<pre><code>import {lazy, Suspense} from &quot;react&quot;</code></pre><h4 id="2️⃣-const-컴포넌트명변수명--lazy--importcomponenetsauthprivateroute">2️⃣ const 컴포넌트명(변수명) = lazy(() =&gt; import(&#39;@componenets/auth/PrivateRoute&#39;))</h4>
<pre><code>const OtherComponent = React.lazy(() =&gt; import(&#39;./OtherComponent&#39;));</code></pre><h4 id="3️⃣-suspense로-컴포넌트들-감싸주기">3️⃣ Suspense로 컴포넌트들 감싸주기</h4>
<p>: <code>lazy 컴포넌트는 Suspense 컴포넌트 하위에서 렌더링</code>되어야 한다. </p>
<pre><code>     &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
        &lt;section&gt;
          &lt;OtherComponent /&gt;
          &lt;AnotherComponent /&gt;
        &lt;/section&gt;
      &lt;/Suspense&gt;</code></pre><h4 id="4️⃣-fallback-옵션-지정하기">4️⃣ fallback 옵션 지정하기</h4>
<p>:  Suspense는 컴포넌트가 lazy loading되는 동안 보여줄 ui를 만드는 fallback props를 제공한다.</p>
<pre><code>import React, { Suspense } from &#39;react&#39;;

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

function MyComponent() {
  return (
    &lt;div&gt;
      &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
        &lt;OtherComponent /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;
  );
}</code></pre><h4 id="5️⃣-프로젝트에-적용해보기">5️⃣ 프로젝트에 적용해보기</h4>
<p><code>lazy 적용 전</code> 유저가 첫 페이지를 만나기까지 걸리는 시간 &gt; 1.27s</p>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/3cafc3af-3657-4375-aaba-3410465ef72a/image.png" alt="">
<code>lazy 적용 후</code> 유저가 첫 페이지를 만나기까지 걸리는 시간 &gt; 407.17ms
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/dc9ea020-aa6f-4548-93d3-46a2097c9208/image.png" alt=""></p>
<p>대략 <code>0.87s</code>가 더 빨라진 것을 확인할 수 있다!!</p>
<br />
<br />

<p>💡</p>
<ul>
<li><a href="https://bhanu.io/easy-code-splitting-using-react-lazy-and-suspense-ec5295b8082e">참고</a></li>
<li><a href="https://ko.legacy.reactjs.org/docs/code-splitting.html">참고</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next를 Next 답게 사용하는 방법]]></title>
            <link>https://velog.io/@ssooo_kk_77/Next%EB%A5%BC-Next-%EB%8B%B5%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@ssooo_kk_77/Next%EB%A5%BC-Next-%EB%8B%B5%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 10 Jan 2024 11:28:41 GMT</pubDate>
            <description><![CDATA[<h2 id="✔️-image-컴포넌트-사용">✔️ Image 컴포넌트 사용</h2>
<ul>
<li><p><strong>lazy loading</strong> : <code>이미지 로드하는 시점을 지연시키는 기능</code>을 제공한다. 따라서 스크린 밖에 있는 이미지들은 로딩을 지연시키고 스크린 안에 있는 이미지만을 로드해  불필요한 대역폭 사용을 줄이고 필요한 이미지만 빠르게 로드할 수 있도록 최적화 할 수 있다. (우선적으로 로딩 시키고 싶다면 priority 속성을 추가하면 된다!)</p>
</li>
<li><p>*<em>이미지 사이즈 최적화 *</em></p>
<ul>
<li>사용자의 디바이스에 맞는 이미지 사이즈를 만들어 다운로드 할 수 있게 제공 </li>
<li>webp와 같은 용량이 작은 포맷으로 변환하여 제공(이미지에 대한 최초 요청 시에 Next.js 서버에서 진행)</li>
<li>이후 요청에는 캐시가 만료될 때까지 캐시 된 이미지가 제공되기 때문에 첫 번째 요청보다 훨씬 빠르게 이미지를 보여줄 수 있게 된다. </li>
</ul>
</li>
<li><p>*<em>placeholder 제공 *</em>: 레이아웃이 흔들리는 현상을 방지하기 위해 placeholder를 제공 해 <code>이미지가 로드되기 전에도 이미지 높이만큼 영역을 표시</code>해서 이미지가 로드된 후에 레이아웃이 흔들리지 않도록 할 수 있다.</p>
</li>
</ul>
<pre><code>import logoImg from &#39;@/assets/logo.png&#39;

export default function Header() {
    return(
        &lt;header className={classes.header}&gt;
           &lt;img src={logoImg.src} alt=&quot;logoImg&quot; /&gt;
</code></pre><pre><code>                ▽ ▽ ▽ ▽ ▽ ▽ ▽</code></pre><pre><code>import logoImg from &#39;@/assets/logo.png&#39;
import Image from &quot;next/image&quot;;

export default function Header() {
    return(
        &lt;header className={classes.header}&gt;
            &lt;Image src={logoImg} alt=&quot;logo image&quot; width={80} height={100} priority /&gt;</code></pre><p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/5b5ed662-bd4b-450c-b8db-d0c6ffca3aea/image.png" alt="">
<code>Network/Headers/Content-Type에서 확인 가능</code>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/6c566e81-ac84-4312-a22e-bc6de25d0b3d/image.png" alt=""></p>
<br />
<br />

<p>💡</p>
<ul>
<li><a href="https://fe-developers.kakaoent.com/2022/220714-next-image/">참고</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[전역 상태 관리 라이브러리인 Recoil에 대해 알아보자]]></title>
            <link>https://velog.io/@ssooo_kk_77/recoil</link>
            <guid>https://velog.io/@ssooo_kk_77/recoil</guid>
            <pubDate>Wed, 03 Jan 2024 05:07:20 GMT</pubDate>
            <description><![CDATA[<h2 id="❓recoil-이란">❓Recoil 이란?</h2>
<ul>
<li><p>React 애플리케이션 <code>상태 관리를 위한 라이브러리</code></p>
</li>
<li><p>Facebook에서 개발한 상태 관리 라이브러리로 <code>atom 이라는 단위로 상태를 정의</code>하고 이를 이용해 컴포넌트 사이에서 데이터를 공유하며 상태를 업데이트한다.</p>
</li>
</ul>
<br />

<h2 id="❗recoil-사용시-장점">❗Recoil 사용시 장점</h2>
<ul>
<li><p><strong>간편한 상태관리</strong>: 간편하게 상태를 정의하고 관리할 수 있다.</p>
</li>
<li><p><strong>최적화된 리렌더링</strong>: 내부적으로 최적화되어 있기 때문에 필요한 경우에만 리렌더링된다.</p>
<blockquote>
<p>서로 다른 selector가 같은 atom을 참조하고 setter의 결과로 원본 atom에 변화가 있더라도, 또다른 selector가 참조하고 있는 필드가 변경되지 않았다면 recoil이 이를 판단해서 영리하게 리렌더링을 일으키지 않는다.</p>
</blockquote>
</li>
<li><p><strong>복잡한 애플리케이션에 적합</strong>: 복잡한 상태 관리를 효과적으로 다루는데 적합하다.</p>
</li>
</ul>
<br />

<h2 id="📌-atom과-selector">📌 Atom과 Selector</h2>
<ul>
<li><h3 id="atom"><strong>Atom</strong></h3>
<p>: 앱의 상태를 정의하는 단위로 atom 함수를 통해 생성한다. atom의 값을 읽는 컴포넌트들은 암묵적으로 atom을 구독하기 때문에 atom에 어떤 변화가 있으면 그 atom을 구독하는 모든 컴포넌트가 재 렌더링 되는 결과가 발생한다.</p>
<blockquote>
<p><strong>기본형</strong>  </p>
</blockquote>
<pre><code>function atom&lt;T&gt;({
    key: string, // 내부적으로 atom을 식별하는데 사용되는 고유한 문자열
    default: T | Promise&lt;T&gt; | RecoilValue&lt;T&gt;, // atom의 초깃값 또는 Promise 또는 동일한 타입의 값을 나타내는 다른 atom이나 selector
}): RecoilState&lt;T&gt;</code></pre><p>ex)</p>
<pre><code>import { atom } from &quot;recoil&quot;;

const userState = atom({
    key : &#39;userState&#39;,
    default : null,
})</code></pre></li>
<li><h3 id="selector"><strong>Selector</strong></h3>
<p>: 상태 변화를 만들어 내는 함수로 다른 atom이나 selector로부터 값을 계산한다.</p>
<blockquote>
<p><strong>기본형</strong>  </p>
</blockquote>
<pre><code>  function selector&lt;T&gt;({
    key: string, // 내부적으로 atom을 식별하는데 사용되는 고유한 문자열
    get: ({
      get: GetRecoilValue // selector는 읽기만 가능한 상태를 반환한다.
    }) =&gt; T | Promise&lt;T&gt; | RecoilValue&lt;T&gt;,
    set?: ( // selector는 쓰기 가능한 상태를 반환한다.
      {
        get: GetRecoilValue,
        set: SetRecoilState,
        reset: ResetRecoilState,
      },
      newValue: T | DefaultValue,
    ) =&gt; void,
  })</code></pre><p>ex)</p>
<pre><code>import { selector } from &quot;recoil&quot;;

const userNameState = selector({
    key : &#39;userNameState&#39;,
    get : ({get}) =&gt; {
        const user = get(userState);
        return user ? user.name : &#39;Guest&#39;;
    }
})</code></pre></li>
</ul>
<br />

<h2 id="📌-recoil에서-자주-사용하는-hook">📌 Recoil에서 자주 사용하는 Hook</h2>
<ul>
<li><h3 id="userecoilstatestate">useRecoilState(state)</h3>
<ul>
<li><code>Atom 값을 읽고 업데이트하는데 사용</code>되는 훅으로 useState과 비슷한 구조이지만 전역 상태를 다룬다는 점에서 차이가 있다. 상태가 업데이트 되었을 때 리렌더링을 하도록 컴포넌트를 구독한다.<pre><code>import {atom, useRecoilState, RecoilRoot} from &quot;recoil&quot;;
</code></pre></li>
</ul>
<p>const countState = atom({</p>
<pre><code>key : &#39;countState&#39;,
default : 0,</code></pre><p>});</p>
<p>function Counter() {</p>
<pre><code>const [count, setCount] = useRecoilState(countState);

return(
    &lt;div&gt;
        &lt;p&gt;Count : {count} &lt;/p&gt;
        &lt;button onClick={() =&gt; setCount(count + 1)}&gt;Increment&lt;/button&gt;
        &lt;button onClick={() =&gt; setCount(count - 1)}&gt;Decrement&lt;/button&gt;
    &lt;/div&gt; 
);</code></pre><p>}</p>
<pre><code></code></pre></li>
</ul>
<br />

<ul>
<li><h3 id="userecoilvaluestate">useRecoilValue(state)</h3>
<ul>
<li><code>단순히 Atom 값을 읽는데에 사용</code>되며 상태 업데이트를 트리거하지 않는다. <pre><code>import {selector, useRecoilValue, RecoilRoot} from &#39;recoil&#39;;
</code></pre></li>
</ul>
<p>const userNameState = selector({</p>
<pre><code>key : &#39;userNameState&#39;,
get : ({get}) =&gt; {
    const user = get(userState);
    return user ? user.name : &#39;Guest&#39;;
},</code></pre><p>});</p>
<p>function UserInfo() {</p>
<pre><code>const userName = useRecoilValue(userNameState);

return &lt;p&gt;Welcome, {userName}!&lt;/p&gt;</code></pre><p>}</p>
<pre><code></code></pre></li>
</ul>
<br />

<ul>
<li><h3 id="usesetrecoilstatestate">useSetRecoilState(state)</h3>
<ul>
<li><code>Atom 값을 업데이트</code>하는 setter 함수를 반환한다.</li>
</ul>
<pre><code>import {atom, useSetRecoilState, RecoilRoot} from &#39;recoil&#39;;

const countState = atom({
    key : &#39;countState&#39;,
    default : 0,
});

function Counter() {
    const setCount = useSetRecoilState(countState);

    return(
        &lt;div&gt;
            &lt;button onClick={() =&gt; setCount((prevCount) =&gt; prevCount + 1)}&gt;Increment&lt;/button&gt;
        &lt;/div&gt;
    );
}</code></pre></li>
</ul>
<br />

<ul>
<li><h3 id="useresetrecoilstate">useResetRecoilState</h3>
<ul>
<li><code>Atom 값을 초기값으로 리셋</code>하는 함수를 반환한다.<pre><code>import {atom, useResetRecoilState, RecoilRoot} from &#39;recoil&#39;;
</code></pre></li>
</ul>
<p>const countState = atom({</p>
<pre><code>key : &#39;countState&#39;,
default : 0,</code></pre><p>});</p>
<p>function ResetButton() {</p>
<pre><code>const resetCount = useResetRecoilState(countState);

return(
    &lt;div&gt;
        &lt;button onClick={resetCount}&gt;Reset Count&lt;/button&gt;
    &lt;/div&gt;
);</code></pre><p>}</p>
<pre><code></code></pre></li>
</ul>
<br />

<hr>
<br />

<h2 id="📌-그렇다면-프로젝트에-적용해보자">📌 그렇다면 프로젝트에 적용해보자!</h2>
<h3 id="1️⃣-설치">1️⃣ 설치</h3>
<pre><code>npm install recoil
yarn add recoil</code></pre><h3 id="2️⃣-recoilroot로-컴포넌트-감싸기">2️⃣ RecoilRoot로 컴포넌트 감싸기</h3>
<pre><code>export const NextProvider = ({ children }: Props) =&gt; {
  return (
    &lt;RecoilRoot&gt;
      &lt;QueryClientProvider client={queryClient}&gt;
        ...
      &lt;/QueryClientProvider&gt;
    &lt;/RecoilRoot&gt;
  );
};</code></pre><h3 id="3️⃣-지도-전역-상태-설계">3️⃣ 지도 전역 상태 설계</h3>
<p>: 기존 Map, Marker에 전달하던 상태 및 상태 관리 함수는 모두 삭제 후 전역으로 상태 관리하도록 적용</p>
<blockquote>
<p>• mapState : 지도의 기본 상태를 저장
• currentStoreState : 현재 선택한 맛집 상태를 저장
• locationState : 현재 위치 및 zoom 상태를 저장</p>
</blockquote>
<ul>
<li><h4 id="atom-값-설계">Atom 값 설계</h4>
<pre><code>// atom/index
</code></pre></li>
</ul>
<p>export const mapState = atom<any>({
  key: &quot;map&quot;,
  default: null,
  dangerouslyAllowMutability: true,
});</p>
<p>export const currentStoreState = atom&lt;StoreType | null&gt;({
  key: &quot;store&quot;,
  default: null,
});</p>
<p>export const locationState = atom<LocationType>({
  key: &quot;location&quot;,
  default: {
    lat: DEFAULT_LAT,
    lng: DEFAULT_LNG,
    zoom: DEFAULT_ZOOM,
  },
});</p>
<pre><code>
- #### Map 컴포넌트에 적용
</code></pre><p>// components/map
export default function Map({ lat, lng, zoom }: MapProps) {
  const setMap = useSetRecoilState(mapState);
  const location = useRecoilValue(locationState);</p>
<pre><code>- #### Marker 컴포넌트에 적용</code></pre><p>export default function Marker({ store }: MarkerProps) {
  const map = useRecoilValue(mapState);</p>
<pre><code>
- #### Markers 컴포넌트에 적용</code></pre><p>// components/markers
export default function Markers({ stores }: MarkerProps) {
  const map = useRecoilValue(mapState);
  const setCurrentStore = useSetRecoilState(currentStoreState);
  const [location, setLocation] = useRecoilState(locationState);</p>
<pre><code>
&lt;br /&gt;

### 4️⃣ 검색 기능 전역 상태 설계
: 기존 SearchFilter에 전달하던 상태 및 상태 관리 함수는 모두 삭제 후 전역으로 상태 관리하도록 적용

&gt; searchState : 검색 상태를 저장

- ####  Atom 값 설계</code></pre><p>// atom/index</p>
<p>export const searchState = atom&lt;SearchType | null&gt;({
  key: &quot;search&quot;,
  default: null,
});</p>
<pre><code>
- ####  SearchFilter 컴포넌트에 적용</code></pre><p>export default function SearchFilter() {
  const [search, setSearch] = useRecoilState(searchState);</p>
<p>  return (
    ...
        &lt;input
          type=&quot;search&quot;
          onChange={(e) =&gt; setSearch({ ...search, q: e.target.value })}
          ...
        /&gt;
      </div>
      &lt;select
        onChange={(e) =&gt; setSearch({ ...search, district: e.target.value })}
        ...
      &gt;</p>
<p>````</p>
<br />
<br />

<p>💡 </p>
<ul>
<li><a href="https://recoiljs.org/ko/docs/introduction/getting-started">참고</a></li>
<li><a href="https://velog.io/@2ast/React-Recoil-selector%EB%A1%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94%EC%97%90-%EA%B8%B0%EC%97%AC%ED%95%98%EA%B8%B0">참고</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[pages 라우터에서 app 라우터로 마이그레이션]]></title>
            <link>https://velog.io/@ssooo_kk_77/pages-%EB%9D%BC%EC%9A%B0%ED%84%B0%EC%97%90%EC%84%9C-app-%EB%9D%BC%EC%9A%B0%ED%84%B0%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@ssooo_kk_77/pages-%EB%9D%BC%EC%9A%B0%ED%84%B0%EC%97%90%EC%84%9C-app-%EB%9D%BC%EC%9A%B0%ED%84%B0%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</guid>
            <pubDate>Sun, 24 Dec 2023 03:02:46 GMT</pubDate>
            <description><![CDATA[<h3 id="❔-app-router로-변환하는-방법">❔ App router로 변환하는 방법</h3>
<ul>
<li>app 디렉토리 안에 <code>page.js 파일</code>을 만들어서 공개적으로 접근 가능한 URL 경로를 만든다.
(app router는 page router보다 우선시 되며 디렉토리 간에 라우팅이 겹칠 경우 충돌을 방지하기 위해 빌드 시 에러를 발생시킨다.) </li>
<li>기본적으로 app 디렉토리 내부의 컴포넌트는 리액트의 <code>서버 컴포넌트로 동작</code>한다.(성능 최적화 적용 가능) <ul>
<li>클라이언트 컴포넌트로 사용하고 싶다면 최상단에 &quot;<code>use client</code>&quot;로 정의하면 된다.</li>
</ul>
</li>
</ul>
<br />

<h3 id="📝-파일-규칙">📝 파일 규칙</h3>
<table class="table">
        <thead>
<tr>
<th>파일명(.js, jsx, .tsx)</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td>layout</td>
<td>세그먼트와 그 자식들에 대한 공유하는 UI. 레이아웃 파일</td>
</tr>
  <tr>
<td>page</td>
<td>라우트의 고유한 UI(페이지)를 만들고 공개적으로 접근 가능하게 만드는 파일</td>
</tr>
  <tr>
<td>loading</td>
<td>세그먼트와 그 자식들에 대한 로딩 UI</td>
</tr>
  <tr>
<td>not-found</td>
<td>세그먼트와 그 자식들에 대한 404 UI</td>
</tr>
  <tr>
<td>error/global-error</td>
<td>세그먼트와 그 자식들에 대한 에러 UI. 글로벌 에러 UI</td>
</tr>
  <tr>
<td>route</td>
<td>서버 측 API 엔드포인트(기존 pages의 api 폴더 역할)</td>
</tr>
    <tr>
<td>template</td>
<td>커스텀 된(리렌더링) 레이아웃 UI(상태유지X)</td>
</tr>
</tbody>
</table>

<br />
<br />

<h3 id="페이지와-레이아웃">페이지와 레이아웃</h3>
<ul>
<li><strong>Page.js</strong> : Route의 유일하게 UI를 보여줄 수 있는 페이지<ul>
<li>기본적으로 서버 컴포넌트로 구성되며 따로 클라이언트 컴포넌트 설정이 가능하다.</li>
</ul>
</li>
<li><strong>Layouts</strong> : 여러 pages 간에 공유되는 UI<ul>
<li>상태 보존, 인터렉티브 유지, 리렌더링 되지 않는다.</li>
<li>모든 페이지에 공유되는 Root Layout는 필수로 생성해야 한다.</li>
<li>child layout이나 child page가 있는 경우에는 항상 children props를 설정해주어야 한다.</li>
</ul>
</li>
<li><strong>Templates</strong> : 템플릿은 레이아웃과 비슷하게 layout, page를 감싸지만 상태 유지는 안됨.(새로운 인스턴스 생성)<ul>
<li>특별한 상황이 아닌 경우 , Layout 사용이 권장된다. </li>
</ul>
</li>
<li>layout.js와 page.js 파일은 같은 폴더에 정의 가능하며, layout.js는 항상 page.js를 감싸는 구조<pre><code>export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
  &lt;html lang=&quot;en&quot;&gt;
    &lt;body&gt;{children}&lt;/body&gt;
  &lt;/html&gt;
)
}</code></pre></li>
</ul>
<br />
<br />

<h3 id="라우트-그룹">라우트 그룹</h3>
<ul>
<li>사이트의 목적별로 또는 기능별로 경로를 구성하고 싶은데 URL 경로에는 포함되지 않도록 하고자 할 때 폴더를 Route 그룹으로 표시하여 사용한다.</li>
<li>레이아웃만 다르게 적용하고 url은 변경하고 싶지 않은 경우에 사용한다.</li>
<li>컨벤션 : 폴더 이름을 괄호로 묶음으로써 생성(name)<ul>
<li>각각의 Route Group 마다 같은 URL 계층을 가져도 다른 layout을 적용</li>
</ul>
</li>
<li>(marketing), (shop)은 app 하단의 최상위 루트지만 Route Group을 이용해서 별개의 레이아웃 구성
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/4a611bb7-a0dd-492a-a160-2c9726b8ab42/image.avif" alt=""></li>
</ul>
<br />
<br />

<hr>
<br />
<br />

<h3 id="프로젝트에-적용하기">프로젝트에 적용하기</h3>
<p>그렇다면 이제 제 프로젝트를 app router로 마이그레이션 해보겠습니다!</p>
<h4 id="1-nodejs와-nextjs-최신-버전으로-업그레이드하기">1. node.js와 next.js 최신 버전으로 업그레이드하기</h4>
<pre><code>    &gt; yarn add next@latest</code></pre><h4 id="2-src-폴더-하위에-app-디렉토리-생성">2. src 폴더 하위에 app 디렉토리 생성</h4>
<br />

<h4 id="3-app-디렉토리-하위에-root-layouttsx-파일-생성">3. app 디렉토리 하위에 Root Layout.tsx 파일 생성</h4>
<pre><code>  export default function RootLayout({
    children,
  }: {
    children: React.ReactNode;
  }) {
    return (
      &lt;html lang=&quot;en&quot;&gt;
        &lt;body&gt;
          &lt;GoogleAnalytics GA_TRACKING_ID={process.env.NEXT_PUBLIC_GA_ID} /&gt;
          &lt;NextProvider&gt;
            &lt;NextLayout&gt;{children}&lt;/NextLayout&gt;
          &lt;/NextProvider&gt;
        &lt;/body&gt;
      &lt;/html&gt;
    );
  }</code></pre><h4 id="4-_documenttsx-파일을-마이그레이션-해주기-위해-metadata-정의">4. _document.tsx 파일을 마이그레이션 해주기 위해 Metadata 정의</h4>
<pre><code>  import { Metadata } from &quot;next&quot;;

   export const metadata: Metadata = {
     title: &quot;Hole in the wall&quot;,
     description: &quot;Next.js 13 로컬 맛집 앱&quot;,
   }; </code></pre><h4 id="5-기존-_apptsx-파일과-_docuemnttsx-파일-삭제">5. 기존 _app.tsx 파일과 _docuemnt.tsx 파일 삭제</h4>
<br />

<h4 id="6-기존-index-파일을-app의-pagetsx-파일로-변경">6. 기존 index 파일을 app의 page.tsx 파일로 변경</h4>
<br />

<h4 id="7-데이터-패칭-변경">7. 데이터 패칭 변경</h4>
<pre><code>    // 기존 getServersideProps로 하던 패칭 방법
    export async function getServerSideProps() {
    const stores = await axios(`${process.env.NEXT_PUBLIC_API_URL}/api/stores`);

    return {
      props: { stores: stores.data },
    };
  }</code></pre><p>  ⬇⬇⬇  ⬇⬇⬇  ⬇⬇⬇  ⬇⬇⬇  ⬇⬇⬇</p>
<pre><code>  // fetch api를 사용한 패칭 방법
  // Home 내부에서 호출
  const stores: StoreType[] = await getData();

  async function getData() {
    try {
      const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/stores`, {
        cache: &quot;no-store&quot;,
      });

      if (!res.ok) {
        throw new Error(&quot;Failed to fetch data&quot;);
      }

      return res.json();
    } catch (e) {
      console.log(e);
    }
  }</code></pre><br />

<h4 id="8-서버-컴포넌트로-작동하는-app-폴더-내에서-usestate나-useeffect와-같은-훅을-사용하는-컴포넌트를-작동시키기-위해-클라이언트-사이드에서-use-client-명시">8. 서버 컴포넌트로 작동하는 app 폴더 내에서 useState나 useEffect와 같은 훅을 사용하는 컴포넌트를 작동시키기 위해 클라이언트 사이드에서 &quot;use client&quot; 명시</h4>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/7ba1c7c4-ca70-4a03-ab68-8e02fac44cd2/image.JPG" width="700" />


<pre><code>// Markers.tsx

&quot;use client&quot;;
import { currentStoreState, locationState, mapState } from &quot;@/atom&quot;;
import { StoreType } from &quot;@/interface&quot;;
import { useCallback, useEffect } from &quot;react&quot;;
import { useSetRecoilState, useRecoilValue, useRecoilState } from &quot;recoil&quot;;</code></pre><br />

<h4 id="9-userouter-usepathname-usesearchparams-변경">9. <code>useRouter()</code>, <code>usePathname()</code>, <code>useSearchParams()</code> 변경</h4>
<ul>
<li>기존에 next/router에서 import하던 useRouter 훅을 next/navigation으로 변경</li>
<li>새로운 useRouter는 더이상 문자열 pathname을 반환하지 않기 때문에 usePathname 훅으로 변경</li>
<li>새로운 useRouter는 쿼리를 반환하지 않기 때문에 useSearchParams 훅으로 변경</li>
</ul>
<pre><code>&#39;use client&#39;

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

export default function ExampleClientComponent() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()

  // ...
}</code></pre><pre><code>  // 기존의 useRouter를 사용해 url로 쿼리를 받음.
  const router = useRouter();
  const { page = &quot;1&quot; }: any = router.query;

  // 새로운 useSearchParams 훅으로 parameters를 받음.
  const searchParams = useSearchParams();
  const page: any = searchParams?.get(&quot;page&quot;) || &quot;1&quot;;</code></pre><br />

<h4 id="10-url-경로에-따라-폴더-생성하고-pagetsx-파일로-변환">10. URL 경로에 따라 폴더 생성하고 page.tsx 파일로 변환</h4>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/b3a1870a-e700-4a3b-8be6-be464f24ace5/image.JPG" alt=""></p>
<br />

<h4 id="11-global-error-파일-생성">11. global-error 파일 생성</h4>
<pre><code>  &quot;use client&quot;;

  export default function GlobalError({
    reset,
  }: {
    error: Error &amp; { digest?: string };
    reset: () =&gt; void;
  }) {
    return (
      &lt;html&gt;
        &lt;body&gt;
          &lt;div&gt;
            다시 시도해주세요.
            &lt;button
              onClick={() =&gt; reset()}&gt;
              Try again
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/body&gt;
      &lt;/html&gt;
    );
  }</code></pre><br />

<h4 id="12-app-디렉토리-내에-apiauthnextauthroutets-파일-생성">12. app 디렉토리 내에 api/auth/[...nextauth]/route.ts 파일 생성</h4>
<p>/app/api/auth/[...nextauth]/route.ts</p>
<pre><code>import NextAuth from &quot;next-auth&quot;

const handler = NextAuth({
  ...
})

export { handler as GET, handler as POST }</code></pre><p>💥 해당 과정 중 생긴 오류 </p>
<blockquote>
<p><a href="https://velog.io/@ssooo_kk_77/build-error">오류 해결 포스팅</a></p>
</blockquote>
<br />

<h4 id="13-기존-pages-폴더-안의-nextauthroutets-삭제">13. 기존 pages 폴더 안의 [...nextauth]/route.ts 삭제</h4>
<br />

<h4 id="14-stores의-route-http-메서드에-따라-함수화">14. stores의 route HTTP 메서드에 따라 함수화</h4>
<p>// 기존의 POST 메서드</p>
<pre><code>import type { NextApiRequest, NextApiResponse } from &quot;next&quot;;
import { StoreApiResponse, StoreType } from &quot;@/interface&quot;;
import prisma from &quot;@/db&quot;;
import axios from &quot;axios&quot;;

import { getServerSession } from &quot;next-auth&quot;;
import { authOptions } from &quot;./auth/[...nextauth]&quot;;

interface Responsetype {
  page?: string;
  limit?: string;
  q?: string;
  district?: string;
  id?: string;
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse&lt;StoreApiResponse | StoreType[] | StoreType | null&gt;
) {
  const { page = &quot;&quot;, limit = &quot;&quot;, q, district, id }: Responsetype = req.query;
  const session = await getServerSession(req, res, authOptions);

  if (req.method === &quot;POST&quot;) {
    const formData = req.body;
    const headers = {
      Authorization: `KakaoAK ${process.env.KAKAO_CLIENT_ID}`,
    };

    ...


    return res.status(200).json(result);
</code></pre><p>⬇⬇⬇ ⬇⬇⬇ ⬇⬇⬇ ⬇⬇⬇ ⬇⬇⬇</p>
<p>// NextResponse로 변경한 POST 메서드</p>
<pre><code>import { NextResponse } from &quot;next/server&quot;;
import prisma from &quot;@/db&quot;;
import axios from &quot;axios&quot;;

import { getServerSession } from &quot;next-auth&quot;;
import { authOptions } from &quot;@/app/utils/authOptions&quot;;

export async function POST(req: Request) {
  // 데이터 생성을 처리한다.
  const formData = await req.json();

  const headers = {
    Authorization: `KakaoAK ${process.env.KAKAO_CLIENT_ID}`,
  };

 ...


  return NextResponse.json(result, { status: 200 });
}</code></pre><br />
<br />
<br />

<p>💡</p>
<ul>
<li><a href="https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration#migrating-_documentjs-and-_appjs">공식 문서 참고</a></li>
<li><a href="https://next-auth.js.org/configuration/initialization">next auth route handler</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트에 Google Analytics 적용하기]]></title>
            <link>https://velog.io/@ssooo_kk_77/Google-Analytics-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@ssooo_kk_77/Google-Analytics-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Sat, 23 Dec 2023 08:16:06 GMT</pubDate>
            <description><![CDATA[<h2 id="❔-google-analytics란">❔ Google Analytics란?</h2>
<blockquote>
<p>웹 사이트 및 앱의 트래픽, 사용자의 행동을 분석하는 데 사용되는 google의 무료 웹 분석 도구(<a href="https://analytics.google.com/">https://analytics.google.com/</a>)</p>
</blockquote>
<br />

<h2 id="❕-google-analytics를-사용했을-때-장점">❕ Google analytics를 사용했을 때 장점</h2>
<ul>
<li>트래픽 소스, 사용자의 경로, 전환률 등을 상세하게 분석할 수 있어 <code>웹 사이트나 앱의 성능 향상</code>에 도움을 준다.</li>
<li>사용자가 웹 사이트나 앱에서 어떻게 상호 작용하는지 이해할 수 있고 이를 바탕으로 <code>사용자 경험을 개선</code>할 수 있다.</li>
<li><code>트래픽 분석</code> : 웹 사이트나 앱에 어떤 트래픽 소스에서 사용자가 유입되는지 추적할 수 있다,</li>
<li><code>페이지 분석</code> : 어떤 페이지나 화면이 사용자에게 가장 많이 방문되는지 어떤 컨텐츠가 인기 있는지 파악할 수 있다.</li>
<li><code>전환률 분석</code> : 원하는 동작(가입, 다운로드 등)을 완료한 사용자의 비율을 계산하고 개선 방법을 고려할 수 있다. </li>
</ul>
<br />

<h2 id="❕-세팅">❕ 세팅</h2>
<h4 id="1-관리--만들기--계정">1. 관리 &gt; 만들기 &gt; 계정</h4>
<p><a href="https://analytics.google.com/analytics">https://analytics.google.com/analytics</a></p>
<h4 id="2-계정-이름-설정">2. 계정 이름 설정</h4>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/facbabc0-2ca7-43fb-91fd-353b43ebf1f6/image.JPG" width="600"/>

<br />

<h4 id="3-속성-이름-보고-시간대-통화-설정">3. 속성 이름, 보고 시간대, 통화 설정</h4>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/780d0011-5d3d-4b71-a6e1-1ba3dc420cbd/image.JPG" width="600"/>

<br />

<h4 id="4--업종-카테고리-비지니스-규모-선택">4.  업종 카테고리, 비지니스 규모 선택</h4>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/f1521f6b-e58f-4868-9782-84fc6f25870e/image.JPG" width="300"/>

<br />

<h4 id="5-비지니스-목표-선택">5. 비지니스 목표 선택</h4>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/94af9895-7c1b-4a66-9822-f7b263f65aaa/image.JPG" width="500"/>

<br />

<h4 id="6-약관-동의-후-플랫폼-선택">6. 약관 동의 후 플랫폼 선택</h4>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/350f4985-d843-44c4-93c6-b460f1f81017/image.JPG" width="800"/>

<br />

<h4 id="7-배포한-url과-스트림-이름-설정">7. 배포한 URL과 스트림 이름 설정</h4>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/98b742c1-623b-493b-b39a-e8a3085250ca/image.JPG" width="700"/>

<br />

<ol start="8">
<li>측정 ID를 .env 파일에 NEXT_PUBLIC_GA_ID로 저장<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/ef566c50-641d-4ecf-89d9-786bb21ac916/image.JPG" width="700"/>

</li>
</ol>
<br />

<hr>
<br />

<h2 id="❕-적용">❕ 적용</h2>
<h4 id="1-설치">1. 설치</h4>
<pre><code>yarn add -D @types/gtag.js</code></pre><h4 id="2-tsconfigjson에-gtag-추가해-타입-선언-파일-지정">2. tsconfig.json에 gtag 추가해 타입 선언 파일 지정</h4>
<pre><code>  &quot;types&quot;: [&quot;@types/gtag.js&quot;],</code></pre><h4 id="3-이전-페이지에서-저장한-측정-id를-env-파일에-저장">3. 이전 페이지에서 저장한 측정 ID를 .env 파일에 저장</h4>
<h4 id="4-srclibgtagts-파일-생성-후-구글-애널리틱스-관련-타입-및-함수-정의">4. /src/lib/gtag.ts 파일 생성 후 구글 애널리틱스 관련 타입 및 함수 정의</h4>
<ul>
<li><p>pageview 함수</p>
<ul>
<li>page path를 넘겨받아 사용자가 페이지를 이동할 때마다 해당 path를 저장</li>
</ul>
</li>
<li><p>event 함수</p>
<ul>
<li>사용자가 찜 버튼을 누를 때마다 해당 이벤트가 감지되도록 정의<pre><code>export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;
</code></pre></li>
</ul>
</li>
</ul>
<p>export const pageview = (url: URL | string) =&gt; {
  if (process.env.NODE_ENV !== &quot;development&quot;) {
    window.gtag(&quot;config&quot;, GA_TRACKING_ID as string, {
      page_path: url,
    });
  }
};</p>
<p>type gtagEvent = {
  action: string;
  category: string;
  label: string;
  value: number;
};</p>
<p>export const event = ({ action, category, label, value }: gtagEvent) =&gt; {
  if (process.env.NODE_ENV !== &quot;development&quot;) {
    window.gtag(&quot;event&quot;, action, {
      event_category: category,
      event_label: label,
      value: value,
    });
  }
};</p>
<pre><code>
#### 5. src/app/googleAnalytics.tsx 파일 생성 후 구글 스크립트 로드
- pathname이 변경될 때 마다 pageview 함수 호출
</code></pre><p>import { pageview } from &quot;@/lib/gtag&quot;;
import Script from &quot;next/script&quot;;
import { useEffect } from &quot;react&quot;;
import { usePathname } from &quot;next/navigation&quot;;</p>
<p>function GoogleAnalytics({ GA_TRACKING_ID }: { GA_TRACKING_ID?: string }) {
  const pathname = usePathname();</p>
<p>  useEffect(() =&gt; {
    if (pathname) {
      pageview(pathname);
    }
  }, [pathname]);</p>
<p>  if (process.env.NODE_ENV !== &quot;production&quot;) {
    return null;
  }
  return (
    <div className="container">
      &lt;Script
        strategy=&quot;afterInteractive&quot;
        src={<code>https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}</code>}
      /&gt;
      <Script id="google-analytics" strategy="afterInteractive">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());</p>
<pre><code>      gtag(&#39;config&#39;, &#39;${GA_TRACKING_ID}&#39;);
    `}
  &lt;/Script&gt;
&lt;/div&gt;</code></pre><p>  );
}</p>
<p>export default GoogleAnalytics;</p>
<pre><code>
#### 6. /src/app/layout.tsx에서 앞서 만든 googleAnalytics 컴포넌트 로드</code></pre><p>export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <GoogleAnalytics GA_TRACKING_ID={process.env.NEXT_PUBLIC_GA_ID} />
        <NextProvider>
          <NextLayout>{children}</NextLayout>
        </NextProvider>
      </body>
    </html>
  );
}</p>
<pre><code>
#### 7. 찜 하기 버튼을 클릭했을 때 event 함수 호출</code></pre><p>const toggleLike = async () => {
    // 찜하기 / 찜 취소
    if (session?.user && store) {
      try {
        ...생략</p>
<pre><code>    event({
      action: &quot;click_like&quot;,
      category: &quot;like&quot;,
      label: like.status === 201 ? &quot;create_like&quot; : &quot;delete_like&quot;,
      value: storeId,
    });

    refetch();
  } catch (e) {
    console.log(e);
  }
} else if (status === &quot;unauthenticated&quot;) {
  toast.warn(&quot;로그인 후 이용해주세요.&quot;);
  event({
    action: &quot;click_like&quot;,
    category: &quot;like&quot;,
    label: &quot;need_login_like&quot;,
    value: storeId,
  });
}</code></pre><p>  };</p>
<p>```</p>
<h4 id="8-vercel의-환경-변수-추가-후-재배포">8. Vercel의 환경 변수 추가 후 재배포</h4>
<p><img src="https://velog.velcdn.com/images/ssooo_kk_77/post/2f55565a-7fd6-4566-b635-40d79a550ca8/image.JPG" alt=""></p>
<br />
<br />

<p>여기까지 설정은 끝이났고 적용이 잘 되었는지 확인해봅시다!</p>
<p>먼저 event를 적용한 찜 하기 기능을 테스트 하기 위해 가게 하나를 찜하고 찜을 삭제한 후</p>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/efafae37-b470-4abb-8896-036bf689c578/image.JPG" width="600"/>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/523d86e9-3c47-4921-b11f-2f80d8cf3a17/image.JPG" width="600"/>

<br />

<p>Google 애널리틱스의 프로젝트 보고서를 확인하니 사용자 수가 측정된 것을 확인할 수 있었습니다.</p>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/c9d3641c-5e68-4db1-965a-8e395368c7dc/image.JPG" width="600"/>

<br />

<p>찜하기와 찜삭제 기능이 몇 번 발생했는지 측정되었습니다! </p>
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/02a508d5-d0f8-4d7a-8c8f-e1576f81bf3f/image.JPG" width="500"/>



<br />
<br />

<p>💡
<a href="https://nextjs.org/docs/messages/next-script-for-ga">공식 문서 참고</a>
<a href="https://developers.google.com/analytics/devguides/collection/gtagjs/pages">페이지 조회수 측정 참고</a>
<a href="https://developers.google.com/analytics/devguides/collection/gtagjs/events">Google 애널리틱스 이벤트 측정 참고</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next-auth 소셜 로그인 구현 과정(2)]]></title>
            <link>https://velog.io/@ssooo_kk_77/Next-auth-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%952</link>
            <guid>https://velog.io/@ssooo_kk_77/Next-auth-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%952</guid>
            <pubDate>Sat, 23 Dec 2023 07:50:56 GMT</pubDate>
            <description><![CDATA[<h3 id="📌-google-login-세팅">📌 Google Login 세팅</h3>
<p>✅ <a href="https://console.cloud.google.com">https://console.cloud.google.com</a> 에서 새 프로젝트 생성 후 
프로젝트 이름 설정 &gt; 만들기
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/a1d67bd2-6d67-4b08-acec-96b31b036849/image.JPG" width="600" /></p>
<br />

<p>✅ 생성한 프로젝트 선택 후 사용자 인증 정보  &gt; API 키 생성
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/87604a68-0f52-4e25-9919-90b9ba5e757e/image.JPG" width="600" /></p>
<br />

<p>✅ OAuth 동의 화면에서 User Type에 따른 선택 (Hole-in-the-wall의 경우 외부로 진행)</p>
<br />

<p>✅ 앱 이름과 사용자 지원 이메일, 이메일 주소 등 필수 값 입력
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/2e53f7fd-3169-49c5-a6c2-50cad178cac6/image.JPG" width="600" /></p>
<p>✅ 범위 추가 또는 삭제 클릭 후 범위 설정 &gt; 업데이트
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/82b31bc7-5f70-4f60-915b-b84cd81e3a64/image.JPG" alt=""></p>
<p>✅ 테스트 할 사용자 이메일 추가 후 대시보드로 돌아가기
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/63146fd1-b1b5-45e1-8667-2272bc79862b/image.JPG" alt=""></p>
<p>✅ 사용자 인증 정보 &gt; OAuth 클라이언트 ID
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/3414ea34-a335-44a6-8da6-28fd70dbb47f/image.JPG" width="600" /></p>
<p>✅ 애플리케이션 유형과 이름 승인된 자바스크립트 원본(테스트할 로컬 주소), 리디렉션 URI 입력 &gt; 만들기</p>
<ul>
<li><a href="https://next-auth.js.org/providers/google">https://next-auth.js.org/providers/google</a> 공식 문서에 따라 리디렉션 URI 설정<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/44514687-6e79-48d9-9d49-68ce10f01a4f/image.JPG" width="600" />
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/a67e9b91-cae8-4510-8550-cfe90de4e64d/image.JPG" width="600" />

</li>
</ul>
<p>✅ 생성된 클라이언트 ID와 클라이언트 보안 비밀번호(클라이언트 시크릿) .env 파일에 저장
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/e71440b6-3e20-440d-ad3b-6ed1c26b467d/image.JPG" width="600" /></p>
<p>✅ [...nextauth].tsx 파일에서 GoogleProvider 설정</p>
<br />
<br />

<hr>
<br />
<br />

<h3 id="📌-naver-login-세팅">📌 Naver Login 세팅</h3>
<p>✅ <a href="https://developers.naver.com/main/">https://developers.naver.com/main/</a> 의 네이버 로그인 &gt; 오픈 API 이용 신청</p>
<p>✅ 애플리케이션 이름과 사용 API 설정
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/b398070e-6898-4929-9f68-285c126cbbe5/image.JPG" width="600" /></p>
<p>✅ 서비스 환경, URL, Callback URL 설정
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/fa2830ef-c6a3-4c96-8471-da7495803add/image.JPG" width="600" /></p>
<p>✅ 생성된 클라이언트 ID와 클라이언트 보안 비밀번호(클라이언트 시크릿) .env 파일에 저장
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/1cd5375c-1653-44fc-92f3-f23864a185f3/image.JPG" width="600" /></p>
<p>➰<strong>(추가)</strong> 네이버는 name 값이 필수로 오지 않기 때문에 prisma schema의 User model의 name을 optional로 변경하는 과정이 필요했다.</p>
<pre><code>  name      String?</code></pre><ul>
<li>[...nextauth].tsx 파일에서 NaverProvider 설정</li>
</ul>
<br />
<br />

<hr>
<br />
<br />

<h3 id="📌-kakao-login-세팅">📌 Kakao Login 세팅</h3>
<p>✅ <a href="https://developers.kakao.com/">https://developers.kakao.com/</a> 에서 생성한 프로젝트의 REST API 키를 클라이언트 아이디로 .env 파일에 저장</p>
<p>✅ 카카오 로그인 &gt; 활성화 ON으로 설정
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/28a3cc08-c686-41f8-ba47-72c45dc678d5/image.JPG" width="400" /></p>
<p>✅ Redirect URI 설정
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/4f8852a9-59f3-4249-8e57-3a9aae1d898b/image.JPG" width="500" /></p>
<p>✅ 보안 &gt; 클라이언트 시크릿 발급 후 .env 파일에 저장
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/616017f9-e217-4cf5-a4b0-5436e2767314/image.JPG" alt=""></p>
<p>✅ 동의항목 설정
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/93c1fc28-7714-48de-aa58-56340187b6a3/image.JPG" alt=""></p>
<p>✅ [...nextauth].tsx 파일에서 KakaoProvider 설정</p>
<br />

<p>➰<strong>(추가)</strong> 카카오 API로 로그인할 경우 prisma에서 refresh_token_expires_in (Int) 라는 데이터가 넘어오기 때문에 schema 변경이 필요했다.
(Account model에서 해당 필드를 optional로 추가 후 migrate)</p>
<pre><code>  refresh_token_expires_in Int?</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Next-auth 소셜 로그인 구현 과정(1)]]></title>
            <link>https://velog.io/@ssooo_kk_77/Next-auth-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@ssooo_kk_77/Next-auth-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Fri, 22 Dec 2023 07:46:04 GMT</pubDate>
            <description><![CDATA[<h2 id="next-auth-세팅">Next-auth 세팅</h2>
<h3 id="1️⃣-설치">1️⃣ 설치</h3>
<pre><code>    &gt; yarn add next-auth</code></pre><h3 id="2️⃣-env-파일에-nextauth_url-nextauth_secret-환경변수-추가">2️⃣ .env 파일에 NEXTAUTH_URL, NEXTAUTH_SECRET 환경변수 추가</h3>
<pre><code>    &gt; NEXTAUTH_URL=http://localhost:3000

    &gt; NEXTAUTH_SECRET은 랜덤한 string 값으로 설정 : openssl rand -base64 24</code></pre><h3 id="3️⃣-api-route-추가">3️⃣ API Route 추가</h3>
<ul>
<li>발급받은 인증 공급자의 client_id와 client_secret을 .env파일에 설정</li>
<li>api/auth/[...nextauth].ts 파일 생성 후 원하는 인증 공급자(Provider) 및 옵션 설정</li>
</ul>
<pre><code>import NextAuth from &quot;next-auth&quot;
import GoogleProvider from &quot;next-auth/providers/google&quot;

export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    // ...add more providers here
  ],
}
export default NextAuth(authOptions)</code></pre><br />

<h3 id="4️⃣-최상위-파일에서-session-provider로-전체-앱-감싸기">4️⃣ 최상위 파일에서 Session Provider로 전체 앱 감싸기</h3>
<pre><code>// pages/_app.tsx
import { SessionProvider } from &quot;next-auth/react&quot;
export default function App({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    &lt;SessionProvider session={session}&gt;
      &lt;Component {...pageProps} /&gt;
    &lt;/SessionProvider&gt;
  )
}</code></pre><br />

<h3 id="5️⃣-usesession-및-singin-또는-signout과-같은-인증-훅-적용">5️⃣ useSession() 및 singIn 또는 signOut과 같은 인증 훅 적용</h3>
<pre><code>export default function LoginPage() {
  const { status, data: session } = useSession();
  const router = useRouter();

  useEffect(() =&gt; {
    if (status === &quot;authenticated&quot;) {
      router.replace(&quot;/&quot;);
    }
  }, [router, status]);

  return (
     &lt;button
         type=&quot;button&quot;
         className=&quot;text-white flex gap-2 bg-[#4285f4] hover:bg-[#4285f4]/90 font-medium rounded-lg w-full px-5 py-4 text-center items-center justify-center&quot;
          onClick={() =&gt; signIn(&quot;google&quot;, { callbackUrl: &quot;/&quot; })}
      &gt;
          &lt;AiOutlineGoogle className=&quot;w-6 h-6&quot; /&gt;
          Sign in With Google
      &lt;/button&gt;</code></pre><br />
<br />


<hr>
<br />
<br />


<h2 id="prisma-adapter-세팅">Prisma Adapter 세팅</h2>
<h3 id="1️⃣-설치-1">1️⃣ 설치</h3>
<pre><code>&gt; yarn add @auth/prisma-adapter</code></pre><h3 id="2️⃣-next-auth-파일에-정의">2️⃣ next-auth 파일에 정의</h3>
<p>➰ Prisma Adapter란 ?
Next-auth로 로그인 또는 회원가입한 사용자들의 계정 정보와 세션/토큰 정보를 저장할 수 있도록 연동하는 것 <code>(자동으로 prisma에 맞게 데이터 생성 가능)</code></p>
<pre><code>import NextAuth from &quot;next-auth&quot;
import Providers from &quot;next-auth/providers&quot;
import { PrismaAdapter } from &quot;@next-auth/prisma-adapter&quot;
import { PrismaClient } from &quot;@prisma/client&quot;

const prisma = new PrismaClient()

export default NextAuth({
  providers: [
    Providers.Google({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  adapter: PrismaAdapter(prisma),
})</code></pre><br />

<h3 id="3️⃣-prismaschema-파일에-스키마-정의">3️⃣ prisma.schema 파일에 스키마 정의</h3>
<p>model Account {
  id                 String  @id @default(cuid())
  userId             Int
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?  @db.Text
  refresh_token_expires_in Int?
  access_token       String?  @db.Text
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?  @db.Text
  session_state      String?</p>
<p>  user User @relation(fields: [userId], references: [id], onDelete: Cascade)</p>
<p>  @@unique([provider, providerAccountId])
}</p>
<p>model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  likes         Like[]
    comments      Comment[]
}</p>
<p>model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       Int
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}</p>
<p>model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime</p>
<p>  @@unique([identifier, token])
}</p>
<h3 id="3️⃣5️⃣-migrate">3️⃣.5️⃣ migrate</h3>
<pre><code>&gt; npx prisma migrate dev</code></pre><br />

<h3 id="4️⃣-middleware-세팅">4️⃣ Middleware 세팅</h3>
<p><a href="https://next-auth.js.org/configuration/nextjs#middleware">https://next-auth.js.org/configuration/nextjs#middleware</a></p>
<ul>
<li>특정 경로에서 항상 로그인을 해야하는 경우 middleware를 통해서 해당 페이지에 접근 권한을 제한시킬 수 있다.(보안 향상)</li>
<li>.env 파일에 NEXTAUTH_SECRET을 생성 후 반드시 로그인이 필요한 페이지 경로를 Middleware를 통해 정의(/src/middleware.ts)</li>
</ul>
<pre><code>export { default } from &quot;next-auth/middleware&quot;

export const config = { matcher: [&quot;/users/mypage&quot;, &quot;/stores/new&quot;, &quot;/stores/:id/edit&quot;, &quot;/users/likes&quot;] }</code></pre><br />


<p>💡
<a href="https://next-auth.js.org/getting-started/example">공식문서 참고</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[에러로그] 빌드 중 만난 error]]></title>
            <link>https://velog.io/@ssooo_kk_77/build-error</link>
            <guid>https://velog.io/@ssooo_kk_77/build-error</guid>
            <pubDate>Thu, 21 Dec 2023 07:00:37 GMT</pubDate>
            <description><![CDATA[<h3 id="💥-에러-발생">💥 에러 발생!</h3>
<p>app route로 변경하던 중 로컬에서는 문제 없이 작동되는데 빌드 하려고 하니 다음과 같은 에러가 발생하면서 실패😭</p>
<pre><code>Type error: Type &#39;OmitWithTag&lt;typeof import(&quot;C:/Users/kimsooin/hole-in-the-wall/src/app/api/auth/[...nextauth]/route&quot;), &quot;GET&quot; | &quot;DELETE&quot; | &quot;HEAD&quot; | &quot;OPTIONS&quot; | &quot;POST&quot; | &quot;PUT&quot; | &quot;PATCH&quot; | &quot;config&quot; | &quot;generateStaticParams&quot; | ... 6 more ... | &quot;maxDuration&quot;, &quot;&quot;&gt;&#39; does not satisfy the constraint &#39;{ [x: string]: never; }&#39;.

  Property &#39;authOptions&#39; is incompatible with index signature.
    Type &#39;AuthOptions&#39; is not assignable to type &#39;never&#39;.

   6 |
   7 | // Check that the entry is a valid entry
&gt;  8 | checkFields&lt;Diff&lt;{
     |             ^
   9 |   GET?: Function
  10 |   HEAD?: Function
  11 |   OPTIONS?: Function
- info Linting and checking validity of types ...</code></pre><br />

<h3 id="💦-과정">💦 과정</h3>
<p>/src/app/api/auth/[...nextauth]/route.ts</p>
<pre><code>export const authOptions: NextAuthOptions = {
  session: {
    strategy: &quot;jwt&quot; as const,
    maxAge: 60 * 60 * 24,
    updateAge: 60 * 60 * 2,
  },
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID || &quot;&quot;,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET || &quot;&quot;,
    }),
    NaverProvider({
      clientId: process.env.NAVER_CLIENT_ID || &quot;&quot;,
      clientSecret: process.env.NAVER_CLIENT_SECRET || &quot;&quot;,
    }),
    KakaoProvider({
      clientId: process.env.KAKAO_CLIENT_ID || &quot;&quot;,
      clientSecret: process.env.KAKAO_CLIENT_SECRET || &quot;&quot;,
      allowDangerousEmailAccountLinking: true,
    }),
  ],
    (...생략...)

};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };</code></pre><p>기존의 route.ts 파일에서 authOptions을 export 하고 있었다.</p>
<br />
<br />


<h3 id="❓❗-그러다-찾은-나와-같은-에러가-발생했다는-이슈글-발견">❓❗ 그러다 찾은 나와 같은 에러가 발생했다는 이슈글 발견!</h3>
<p><a href="https://stackoverflow.com/questions/76388994/next-js-13-4-and-nextauth-type-error-authoptions-is-not-assignable-to-type-n">https://stackoverflow.com/questions/76388994/next-js-13-4-and-nextauth-type-error-authoptions-is-not-assignable-to-type-n</a>
위의 글에서 원인과 해결방법을 찾을 수 있었다.</p>
<blockquote>
<p> authOptions을 다른 파일로 분리하고 route 파일 내에서 import하여 </p>
</blockquote>
<br />
<br />


<h3 id="✅-해결">✅ 해결</h3>
<p>스택오버플로우에서 찾은 대로 utils라는 폴더에 
/src/app /utils/authOptions 파일을 분리한 다음</p>
<p>/src/app/api/auth/[...nextauth]/route.ts</p>
<pre><code>import { authOptions } from &quot;@/app/utils/authOptions&quot;;
import NextAuth from &quot;next-auth/next&quot;;

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };</code></pre><p>이렇게 import한 뒤 다시 빌드를 해보니 정상적으로 빌드가 되었다!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[에러로그] Next-auth 소셜 로그인 구현 중 만난 Login error]]></title>
            <link>https://velog.io/@ssooo_kk_77/prisma-next-auth-login-error</link>
            <guid>https://velog.io/@ssooo_kk_77/prisma-next-auth-login-error</guid>
            <pubDate>Tue, 19 Dec 2023 14:50:22 GMT</pubDate>
            <description><![CDATA[<h3 id="💥-에러-발생">💥 에러 발생!</h3>
<p>구글과 네이버는 정상적으로 로그인이 되는데 카카오로 로그인을 시도하면 메인페이지로 리디렉션 되는 대신 url에 다음과 같이 표시되면서 로그인이 되지 않는 문제 발생
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/843fc70b-2cac-436b-89ea-75a7ac71226e/image.JPG" alt=""></p>
<h3 id="💦-과정">💦 과정</h3>
<p>터미널에서는 다음과 같은 에러로그가 찍히고 있었다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/cc696941-5c47-4251-99ba-4cd54eedb3d2/image.JPG" alt=""></p>
<p>로그에 찍힌 에러 관련 안내 url을 클릭하니
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/634d030e-f4d8-4a04-8572-d42e2a2f4776/image.JPG" alt=""></p>
<p><strong>1.</strong> 이메일 콜백 프로세스 중 발생한 문제라면 <code>콜백 url에 오타가 있었나?</code> 살펴도 봤고
(문제는 없었다.)
<strong>2.</strong> <code>카카오톡 email 동의</code>도 잘 설정 해 놓았고
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/65486b71-a243-41f8-b035-1a4b15ca1f7d/image.JPG" alt="">
<strong>3.</strong> NEXTAUTH_URL과 로컬 주소 모두 localhost:3000으로 같게 설정 했는데 문제가 뭘까...?</p>
<p>하다가 url의 <code>http://localhost:3000/users/login?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F&amp;error=OAuthAccountNotLinked</code> 로 구글링을 다시 해보았다.</p>
<br />
<br />


<h3 id="❓❗-그러다-찾은-나와-같은-에러가-발생했다는-이슈글-발견">❓❗ 그러다 찾은 나와 같은 에러가 발생했다는 이슈글 발견!</h3>
<p><a href="https://github.com/nextauthjs/next-auth/issues/519">https://github.com/nextauthjs/next-auth/issues/519</a>
위의 글에서 원인을 찾을 수 있었다.</p>
<blockquote>
<p>OAuthAccountNotLinked 동일한 이메일 주소와 연결된 다른 제공업체에 이미 로그인했다는 의미입니다. </p>
</blockquote>
<p>로그인 시 자동 계정 연결을 사용하면 악의적인 행위자가 이를 악용하여 다른 사용자의 이메일 주소와 연결된 OAuth 계정을 생성하여 계정을 탈취할 수 있습니다.</p>
<blockquote>
</blockquote>
<p>이러한 이유로 로그인 시 임의 공급자 간의 계정을 자동으로 연결하는 것은 안전하지 않습니다. 이것이 바로 이 기능이 일반적으로 인증 서비스에서 제공되지 않으며 NextAuth.js에서도 제공되지 않는 이유입니다.</p>
<p>나의 경우 카카오 로그인 시 네이버 이메일을 사용해서 로그인하고 있었는데 이메일이 동일해 문제가 되었다는 것을 알 수 있었다. </p>
<br />
<br />


<h3 id="✅-해결">✅ 해결</h3>
<p>KakaoProvider의 옵션 중 allowDangerousEmailAccountLinking라는 옵션을  true로 설정하니 로그인 성공!!</p>
<pre><code> KakaoProvider({
      clientId: process.env.KAKAO_CLIENT_ID || &quot;&quot;,
      clientSecret: process.env.KAKAO_CLIENT_SECRET || &quot;&quot;,
      allowDangerousEmailAccountLinking: true,
    }),</code></pre><p>해당 옵션은 <code>KakaoProvider가 내 계정과 자동 연결을 허용</code>하도록 하는 옵션인데 설명글에서도 에러가 생겼던 원인을 파악할 수 있었다.
<img src="https://velog.velcdn.com/images/ssooo_kk_77/post/234eafd6-5ae0-4f08-90b2-08be41f3a3f9/image.png" alt=""></p>
<blockquote>
<p>일반적으로 OAuth 제공업체로 로그인하고 동일한 이메일 주소를 가진 다른 계정이 이미 존재하는 경우 해당 계정은 자동으로 연결되지 않습니다. 로그인 시 자동 계정 연결은 임의의 공급자 간에 안전하지 않으며 기본적으로 비활성화되어 있습니다( 보안 FAQ 참조 ). 그러나 관련 제공업체가 계정과 연결된 이메일 주소를 안전하게 확인했다고 신뢰하는 경우 자동 계정 연결을 허용하는 것이 바람직할 수 있습니다. allowDangerousEmailAccountLinking: true자동 계정 연결을 활성화하려면 공급자 구성을 설정하기만 하면 됩니다 .</p>
</blockquote>
<br />
<br />

<p>Next-auth로 로그인 구현 시 같은 에러를 마주친분들께 도움이 되셨길 바랍니다.😊😉</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 13 변경된 내용]]></title>
            <link>https://velog.io/@ssooo_kk_77/Next.js-13-%EB%B3%80%EA%B2%BD%EB%90%9C-%EB%82%B4%EC%9A%A9</link>
            <guid>https://velog.io/@ssooo_kk_77/Next.js-13-%EB%B3%80%EA%B2%BD%EB%90%9C-%EB%82%B4%EC%9A%A9</guid>
            <pubDate>Mon, 11 Dec 2023 04:10:10 GMT</pubDate>
            <description><![CDATA[<h2 id="❔-nextjs-13-변경된-내용을-알아봅시다">❔ Next.js 13 변경된 내용을 알아봅시다!</h2>
<h3 id="❕-라우팅-방식">❕ 라우팅 방식</h3>
<p>: 기존의 pages 폴더 기반 파일 시스템 라우터를 <strong><code>app 디렉토리로 변경하여 라우팅</code></strong> 및 레이아웃 개선</p>
<br />

<h3 id="❕-layout">❕ Layout</h3>
<p>: app 디렉토리 루트의 RootLayout 필수적으로 생성 (하위에 layout.js라는 커스텀 레이아웃 파일도 생성 가능)
: 복잡한 상태를 더 쉽게 관리할 수 있고 <code>리렌더링 방지</code>하면서 경로간 <code>공통 UI를 쉽게 공유</code>할 수 있음
: 레이아웃을 중첩하거나 라우트, 컴포넌트, 테스트 및 스타일과 함께 앱 코드를 배치할 수 있음
: 여러 페이지들이 동일한 UI를 공유할 때 레이아웃 기능 사용
ex) 네비게이션에 레이아웃을 적용하면 상태를 유지하면서 상호작용을 하고 리렌더링 방지</p>
<pre><code>export default function RootLayout({children}) {
   return (
     &lt;html lang=&quot;en&quot;&gt;
         &lt;body&gt;{children}&lt;/body&gt;
     &lt;/html&gt;
   )
}</code></pre><br />

<h3 id="❕-server-component">❕ Server Component</h3>
<p>: app 디렉토리 내 파일은 기본적으로 모든 컴포넌트가 서버 컴포넌트로 동작한다.
따라서 클라이언트 컴포넌트를 사용하려면 파일 상단에 &#39;use client&#39;라는 directive 명시해야 한다. </p>
<blockquote>
<p><strong>Server Component를 사용할 때 이점</strong></p>
</blockquote>
<p>✅ 서버 컴포넌트를 사용하면 클라이언트로 전송되는 JS 양을 줄여 <strong>초기 페이지 로드 속도를 줄일 수 있음</strong>
✅ <strong>성능 최적화</strong> : 렌더링하는 데 필요한 데이터만 서버에서 가져오고 이를 브라우저로 전달(로딩 속도 개선)
✅ <strong>SSR 및 SEO</strong> : 서버에서 초기 페이지 렌더링을 수행하므로 검색 엔진 최적화(SEO)와 성능 향상
✅ <strong>실시간 업데이트</strong> : 클라이언트 측 JS가 필요하지 않으므로 실시간 업데이트와 같은 기능을 구현하기 쉬움
✅ 경로가 로드될 때 next.js나 react 런타임이 로드되기 때문에 <strong>캐시가 가능하고 크기 예측 가능</strong>
✅ <strong>서버측 데이터 흐름 관리</strong> : 데이터 흐름을 더욱 효율적으로 관리</p>
<br />
<br />

<h3 id="❕-streaming-app">❕ Streaming app</h3>
<p>: loading.js 파일을 생성하면 React Suspense와 함께 <strong>로딩 UI</strong>를 생성할 수 있음</p>
<p>✅ 페이지 내용을 로드하는 동안 서버로부터 즉시 로딩 상태를 표시하고 렌더링이 완료되면 컨텐츠 표시
✅ page.js 파일과 그 아래의 모든 파일을 자동으로 wrapping해서 원하는 로딩화면을 보여줌
✅ 렌더링된 단위 별 UI 클라이언트에 점진적으로 렌더링하고 스트리밍할 수 있는 기능 제공
✅ 페이지의 HTML을 더 작은 청크로 분할하고 서버에서 점진적으로 클라이언트로 전송</p>
<br />
<br />

<h3 id="❕-data-fetching-지원">❕ Data Fetching 지원</h3>
<p>: fetch() Web API를 사용할 수 있게 되어 <strong>컴포넌트 레벨에서도 SSR 적용 가능</strong>(중복 제거와 캐싱, 재요청 처리까지 지원)</p>
<pre><code>export default async function Page() {
  // This request should be cached until manually invalidated.
  // Similar to `getStaticProps`.
  // `force-cache` is the default and can be omitted.
  const staticData = await fetch(`https://...`, { cache: &#39;force-cache&#39; })

  // This request should be refetched on every request.
  // Similar to `getServerSideProps`.
  const dynamicData = await fetch(`https://...`, { cache: &#39;no-store&#39; })

  // This request should be cached with a lifetime of 10 seconds.
  // Similar to `getStaticProps` with the `revalidate` option.
  const revalidatedData = await fetch(`https://...`, {
    next: { revalidate: 10 },
  })

  return &lt;div&gt;...&lt;/div&gt;
}</code></pre><br />

<h3 id="❕-new-nextimage-stable">❕ New next/image (stable)</h3>
<p>✅ 레이아웃 변경 없이 이미지를 쉽게 표시하고 on-demand 방식으로 파일을 최적화하여 성능을 향상시킬 수 있는 컴포넌트 제공
✅ 클라이언트 측 JS가 더 적게 포함됨
✅ 스타일링 및 구성이 용이함
✅ 기본적으로 alt 태그를 내장하여 접근성을 향상
✅ 네이티브 lazy-loading을 사용하기 때문에 더 빠름(hydration 필요하지 않음)</p>
<br />

<h3 id="❕-new-nextfontbeta">❕ New @next/font(Beta)</h3>
<p>✅ 레이아웃 이동이 필요없는 자체 호스팅 폰트 제공
✅ 커스텀 폰트를 포함하여 모든 폰트를 자동으로 최적화
✅ 개인 정보 보호 및 성능 향상을 위한 외부 네트워크 요청 제거
✅ 모든 폰트 파일에 대한 자동 self-hosting 내장
✅ CSS size-adjust 속성을 자동으로 적용</p>
<br />

<h3 id="❕-향상된-nextlink">❕ 향상된 next/link</h3>
<p>: 업데이트 된 next/link에서는 더 이상 수동으로 <code>&lt;a&gt;</code>를 하위 항목을 추가할 필요가 없음
✅ <code>항상 &lt;a&gt;를 렌더링</code>하며 기본 태그로 props를 전달할 수 있음</p>
<pre><code>import Link from &#39;next/link&#39;

// Next.js 12 : &#39;&lt;a&gt;&#39; has to be nested otherwise it&#39;s excluded
&lt;Link href=&quot;/about&quot;&gt;
   &lt;a&gt;About&lt;/a&gt;
&lt;/Link&gt;

// Next.js 13 : &#39;&lt;Link&gt;&#39; always renders &#39;&lt;a&gt;&#39;
&lt;Link href=&quot;/about&quot;&gt;
   About
&lt;/Link&gt;</code></pre>]]></description>
        </item>
    </channel>
</rss>