<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>🔥🔥🔥</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 꿈나무, 탐구자.</description>
        <lastBuildDate>Sun, 12 Jan 2025 10:57:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>🔥🔥🔥</title>
            <url>https://images.velog.io/images/code-bebop/profile/8ffbb377-055f-46dc-81d8-5ad006c00ae0/KakaoTalk_20200925_173940005.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 🔥🔥🔥. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/code-bebop" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Typescript의 Satisfies 연산자]]></title>
            <link>https://velog.io/@code-bebop/Typescript%EC%9D%98-Satisfies-%EC%97%B0%EC%82%B0%EC%9E%90</link>
            <guid>https://velog.io/@code-bebop/Typescript%EC%9D%98-Satisfies-%EC%97%B0%EC%82%B0%EC%9E%90</guid>
            <pubDate>Sun, 12 Jan 2025 10:57:31 GMT</pubDate>
            <description><![CDATA[<p>인터넷에서 본 코드에서, <code>satisfies</code> 라고 하는 연산자를 발견했다. 나는 처음 봤음;
검색해보니 Typescript의 연산자인데, 뭔가 아주 편리한 연산자일 것 같다는 생각이 들어서 알아보았다는 겁니당</p>
<h1 id="개요">개요</h1>
<pre><code>type UserState = StateName | StateCordinates;
type StateName = &#39;Seoul&#39; | &#39;Busan&#39; | &#39;Daegu&#39;;
type StateCordinates = { x: number, y: number };

type User = { birthState: MyState, currentState: MyState, }</code></pre><p>예시로 이런 타입이 있다.</p>
<p><code>UserState</code> 인터페이스는 유저의 도시 정보를 가리키는 인터페이스고,
도시 정보는 &#39;Seoul&#39; &#39;Busan&#39; &#39;Daegu&#39; 문자열이거나 { x: number, y: number}  객체다.</p>
<p>아니 무슨 이런 인터페이스가 다 있어! 싶지만 예시니깐..</p>
<pre><code>const user: User = {
    birthState: &#39;Busan&#39;,
    currentState: { x: 255, y: 100 }
};</code></pre><p><code>User</code> 인터페이스를 갖는 <code>user</code> 객체를 만들었다.
<code>user</code>의  <code>birthState</code>는 string이니까, <code>toUpperCase</code> 메서드를 호출해보자.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/c58f8d5a-779e-43ad-8220-fff4c5c4f88a/image.png" alt=""></p>
<p>그럼 타입스크립트 에러가 발생한다.
<code>user.birthState</code>는 { x: number, y: number} 객체일 수도 있는데, 왜 <code>toUpperCase</code> 를 호출하려고 하시죠?? 
이런 말을 하고 있는 거다.</p>
<p>아니! <code>user</code> 객체를 선언할 때 <code>birthState</code>가 <code>&#39;Busan&#39;</code> 이라고 했잖아!
이런 생각도 들지만, <code>user</code> 의 인터페이스는 <code>User</code> 이기 때문에.</p>
<p>그럼 우리는 뭘 해줘야할까?? 당연히 <code>birthState가</code> string 타입이라는 걸 타입스크립트에게 알려줘야한다.</p>
<p><code>if</code> 문과 <code>typeof</code> 연산자로 타입스크립트에게 알려주자.
<strong>&quot;<code>birthState</code>가 string인 걸 확인했어!!!!&quot;</strong>
<img src="https://velog.velcdn.com/images/code-bebop/post/e275e690-2fa6-4f29-b355-87ff98bea1a7/image.png" alt=""></p>
<p>그럼 타입스크립트는 에러를 발생시키지 않는다.</p>
<p><code>satisfies</code> 연산자는 이 코드를 더 간단하고 합리적으로 만들어줄 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/de720a5f-197d-47dc-82e8-393fa84dd811/image.png" alt=""></p>
<p>ㅋㅋ? 어떻게 이런 일이?</p>
<p>검색해보니 <code>satisfies</code> 연산자는 미리 타입 검사를 한 뒤, 타입을 제한? Narrowing? 해주는 역할을 한다는듯 하다.</p>
<p><code>user</code> 객체를 선언할 때, 뒤에 <code>satisfies User</code> 를 붙여놓으면, <code>user</code> 객체가 <code>User</code> 인터페이스를 만족하는지 미리 검사해서, 만족하지 못 하면 컴파일 에러를 띄워버린다는 거다.</p>
<p>아니 근데 그게 <code>const user: User</code> 랑 뭐가 다르지? 
<code>const user: User</code> 도 <code>user</code> 객체가 <code>User</code> 인터페이스를 만족하지 않으면 컴파일 에러를 발생시킨다.</p>
<p>일단은 뭔가.. 상위호환인 것 같다. <code>const user: User</code> 가 하는 일을 다 해주는데, 거기에 더해 추가적으로 뭔가를 더 해주는 걸로 보인다.</p>
<hr>
<p>ㅋㅋ 저녁밥 먹으면서 골똘히 생각하고 있었는데, <code>satisfies</code> 연산자가 뭘 하는지 정확히 알아버렸다!!</p>
<p><code>user</code> 객체 선언문 뒤에 <code>satisfies User</code> 를 붙이면, <code>user</code> 객체가 <code>User</code> 인터페이스를 만족하는지 아닌지를 컴파일 단계에서 검사한다. 있어야 하는 prop이 없거나, prop의 인터페이스가 일치하지 않는다면 컴파일 에러를 발생시킨다.
<strong>그러나, 이렇게 선언된 <code>user</code> 객체는 <code>User</code> 인터페이스가 아니라, <code>User</code> 인터페이스를 만족하는 독립적인 객체인 것이다.</strong></p>
<p>무슨 말이냐고? 자, 이게 <code>statisfies User</code> 로 선언한 객체의 인터페이스다.
타입스크립트는 이 객체를 <code>User</code>로 추론하는 게 아니라, 마치 <code>as const</code> 처럼 타입을 추론해주고 있다. 근데 <code>as const</code> 보다는 좀 더 느슨하게 추론한다.
<img src="https://velog.velcdn.com/images/code-bebop/post/6a36a652-e715-4e96-96ba-689fb0c28de0/image.png" alt=""></p>
<p>그리고 이게 <code>User</code> 인터페이스를 타입으로 지정한 객체의 인터페이스다.
<code>birthState</code>를 <code>&#39;Busan&#39;</code> 으로 선언했든 말든, <code>currentState</code> 를 <code>x</code>와 <code>y</code>를 prop으로 가진 객체로 선언했든 상관없이 인터페이스는 그냥 <code>User</code> 다.
<img src="https://velog.velcdn.com/images/code-bebop/post/fd3a527c-8582-4823-8368-befae59feabd/image.png" alt=""></p>
<p>이해가 쏙쏙 되잖아!</p>
<h1 id="레퍼런스">레퍼런스</h1>
<p><a href="https://mycodings.fly.dev/blog/2023-07-14-understanding-typescript-satisfies-operator">https://mycodings.fly.dev/blog/2023-07-14-understanding-typescript-satisfies-operator</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Svelte를 배워보자 - 1]]></title>
            <link>https://velog.io/@code-bebop/Svelte%EB%A5%BC-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-1</link>
            <guid>https://velog.io/@code-bebop/Svelte%EB%A5%BC-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-1</guid>
            <pubDate>Fri, 10 Jan 2025 15:16:17 GMT</pubDate>
            <description><![CDATA[<h1 id="동기">동기</h1>
<p>프론트엔드 개발자로 취업한지도 2년하고 5개월이 지났다.
SPA로 구독형 서비스를 제공하는 SAAS 제품을 판매하는 회사에 취직했고, 프로젝트는 Next.js를 사용하고 있다.</p>
<p>프로젝트 하나를 가지고 계속해서 업데이트/유지보수하는 양상이기 때문에, 완전 새로운 기술을 실무에서 배울 일은 없다.</p>
<p>그래서 뭔가 사이드 프로젝트를 해야겠구나 싶은 마음을 먹은 게 2년 전이구요..
이제 실천하는겁니당</p>
<p>궁금한 것도 많고 배우고 싶은 것도 많은데, 일단 Svelte를 배워보려고 한다.
Svelte를 배우려는 가장 큰 이유는, 가볍고 생산성이 높다는듯 하여 사이드 프로젝트를 만들기 적합하다고 생각했다.</p>
<h1 id="개요">개요</h1>
<p>React도 그렇고 Vue도 그렇고..
웹앱을 만드는 라이브러리를 배울 때, 가장 좋은 방법이 무엇이나면, 뭔가를 만들어보는 것이다.</p>
<p>나는 블로그를 만들어보면서 Svelte를 배우려고 한다.</p>
<p>그치만 Svelte에 대해 아무것도 모르는 상태로 블로그를 만들 수는 없으니까, 기본적인 튜토리얼은 읽어보려고 합니당..</p>
<p>공식 튜토리얼 문서가 있는데, 영어로 되어 있다.
찾아보니 한국어 번역 버전이 있긴한데, 아직 번역이 진행중이라는 것 같다..</p>
<p>영어버전/한국어버전 간에 내용의 차이는 없는 것 같으니, 일단 한국어버전으로 배워보려고 한다.</p>
<blockquote>
</blockquote>
<p><a href="https://learn.svelte.kr/tutorial/welcome-to-svelte">https://learn.svelte.kr/tutorial/welcome-to-svelte</a></p>
<h1 id="svelte가-정확히-뭐하는-라이브러리일까요">Svelte가 정확히 뭐하는 라이브러리일까요?</h1>
<p>나의 사전 지식에 의하면</p>
<ul>
<li>Svelte는 컴파일러이다.</li>
<li>Virtual DOM을 통해 실제 DOM을 변경하는 React와 다르게 그냥 실제 DOM을 수정한다.</li>
</ul>
<p>정도를 알고 있다.</p>
<p>이제 공식문서에선 자기소개를 어떻게 하고 있는지 살펴보자.</p>
<blockquote>
<p>스벨트는 웹 애플리케이션을 구축하기 위한 도구입니다. 다른 사용자 인터페이스 프레임워크와 마찬가지로 마크업, 스타일 및 동작을 결합한 구성 요소로 앱을 선언적으로 빌드할 수 있습니다.
이러한 컴포넌트는 작고 효율적인 자바스크립트 모듈로 컴파일 되어 기존 UI 프레임워크와 관련된 오버헤드를 제거합니다.
이 튜토리얼에서 다룰 스벨트킷과 같은 애플리케이션 프레임워크를 사용하여 전체 앱을 스벨트로 빌드하거나 기존 코드베이스에 점진적으로 추가할 수 있습니다. 또한 컴포넌트를 어디서나 작동하는 독립형 패키지로 제공할 수도 있습니다.</p>
</blockquote>
<p>요약하면</p>
<ul>
<li>웹 앱을 만들기 위한 도구</li>
<li>선언적으로 개발 가능</li>
<li>각 컴포넌트는 자바스크립트 모듈로 컴파일됨</li>
<li><strong>스벨트킷</strong>이라고 하는 프레임워크도 제공하는듯 함</li>
<li>각 컴포넌트가 자바스크립트 모듈로 컴파일되기 때문에, 이들을 어디에서나 작동 가능한 독립 패키지로 제공할 수도 있다!</li>
</ul>
<p>이다.</p>
<h1 id="컴포넌트-처음-알아보기">컴포넌트 처음 알아보기</h1>
<blockquote>
<p><a href="https://learn.svelte.kr/tutorial/your-first-component">https://learn.svelte.kr/tutorial/your-first-component</a></p>
</blockquote>
<p>Svelte에서의 컴포넌트는 React에서의 컴포넌트와 개념이 같은듯 하다.</p>
<blockquote>
<p>컴포넌트는 재사용 가능한 독립적인 코드 블록으로, 함께 속해 있는 HTML, CSS 및 JavaScript를 &#39;.svelte&#39; 파일로 캡슐화하여 작성합니다.</p>
</blockquote>
<p>문서를 읽고서, 아래와 같은 지식을 알게 되었다!</p>
<ul>
<li>컴포넌트 파일은 <code>*.svelte</code> 로 생성한다.</li>
<li>컴포넌트 파일은 HTML, CSS, JS로 구성된다. 바꿔 말하자면 독립적인 성격의 HTML, CSS, JS 코드 블럭을 캡슐화한다는 겁니당,,</li>
<li><code>script</code> 내부에서 선언한 변수는 HTML 블럭 내부에서 <code>{변수}</code> 와 같이 불러올 수 있다.</li>
<li>중괄호 안에서는 JS 코드를 사용할 수 있다.</li>
</ul>
<p>파트를 다 읽고나니 요런 결과물이 나왔다.
<img src="https://velog.velcdn.com/images/code-bebop/post/35a7267c-ee62-4750-9d2d-0f0b6b9951af/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/745f793f-3fd8-403a-8e57-2177d56ff664/image.png" alt=""></p>
<p>문법이 React와 매우 흡사해서 배우기 아주 편하다!</p>
<h1 id="동적-어트리뷰트">동적 어트리뷰트</h1>
<blockquote>
<p><a href="https://learn.svelte.kr/tutorial/dynamic-attributes">https://learn.svelte.kr/tutorial/dynamic-attributes</a></p>
</blockquote>
<ul>
<li><code>script</code> 블록에서 선언한 변수는 HTML 태그의 어트리뷰트에서 접근할 수도 있다.</li>
<li>어트리뷰트 이름과 변수의 이름이 같다면, <code>src={src}</code> 를 <code>{src}</code> 로 줄일 수도 있다. 이것을 <strong>Shorthand attributes</strong>라고 부른다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/0a803b33-98d0-4d00-bbe4-72bd839b958f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/35d84ab5-b5c3-484b-ae51-55a4c37fb031/image.png" alt=""></p>
<h1 id="스타일링">스타일링</h1>
<blockquote>
<p><a href="https://learn.svelte.kr/tutorial/styling">https://learn.svelte.kr/tutorial/styling</a></p>
</blockquote>
<ul>
<li><code>styled</code> 블록 내에 css 코드를 선언할 수 있다.</li>
<li>컴포넌트의 <code>styled</code> 에 선언된 css 코드는, 컴포넌트의 HTML 코드에만 적용된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/fbb9980b-dc33-4f9b-87aa-b48f8d3cf366/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/3b1c2925-59d4-4870-a2bd-5c14416f6908/image.png" alt=""></p>
<h1 id="중첩된-컴포넌트">중첩된 컴포넌트</h1>
<blockquote>
<p><a href="https://learn.svelte.kr/tutorial/nested-components">https://learn.svelte.kr/tutorial/nested-components</a></p>
</blockquote>
<ul>
<li>컴포넌트는 다른 컴포넌트를 불러와 마크업으로 사용할 수 있다.</li>
</ul>
<p>이 역시 React에서 JSX 컴포넌트를 다루는 방식과 같다.</p>
<p>만지작거리던 코드를 <code>Bangboo.svelte</code> 파일으로 분리한다.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/dce2b655-c8bb-482e-81e1-7a58d4a998ca/image.png" alt=""></p>
<p>실제로 렌더링하는 컴포넌트인 <code>App.svelte</code> 에서, 분리했던 <code>Bangboo.svelte</code> 컴포넌트를 불러온다.
그리고 <code>&lt;Bangboo /&gt;</code>  와 같이 마크업으로 호출한다. </p>
<p>분리된 컴포넌트가 잘 불러와지는지 확인해볼 겸, 정말로 <code>Bangboo.svelte</code> 내부의 <code>h1</code> 선택자 css 코드가, 컴포넌트 바깥에 영향을 끼치지 않는지 확인하기 위해 <code>h1</code> 블록도 추가해봤다.
<img src="https://velog.velcdn.com/images/code-bebop/post/a448b013-ea8d-40a6-a9aa-c7f5e1c10056/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/563d81e3-2213-440a-b19f-3e457dfd2109/image.png" alt=""></p>
<p>굿,,</p>
<h1 id="html-태그">HTML 태그</h1>
<blockquote>
<p><a href="https://learn.svelte.kr/tutorial/html-tags">https://learn.svelte.kr/tutorial/html-tags</a></p>
</blockquote>
<p>불러온 변수가 문자열이고, 문자열 내에 HTML 코드가 있는 경우, 그냥 변수를 불러오면 HTML 코드 문자열은 실행되지 않는다.
그야 그렇겠지. 그냥 문자열이니까..</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/971db114-fd20-4e6a-bcc7-ce4b57918ba0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/7690733e-af5b-4e4a-ab94-cdfcaae36b6b/image.png" alt=""></p>
<p>만약 문자열 내의 HTML 코드도 실행시키고 싶다면, 문자열 앞에 <code>@html</code> 을 붙이면 된다.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/f00dcce4-4baa-428b-a026-615f9b3755f9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/08ac272d-1e90-445f-98ab-bad6ab1ce275/image.png" alt=""></p>
<p>당연하지만 아무 HTML 코드를 실행시키면 어떤 식으로든 악용될 수 있기 때문에 막 쓰면 안 된다.
신뢰할 수 있는 출처의 문자열일 때만 사용하자..</p>
<h1 id="마무리">마무리</h1>
<p>여기까지가 공식문서의 <strong>Part1/소개</strong> 절 까지의 내용이다.</p>
<p>리액트랑 크게 다른 내용이 없어서 막힘 없는 느낌이 좋았고, 그러면서도 좀 더 원본에 가까운 JS, CSS, HTML 을 사용하는 느낌이 싫지 않달까??</p>
<p>음.. 근데 블로그에 기록하면서 배우니까 속도가 많이 떨어져서 튜토리얼 배우는 걸 계속 기록할지 말지는 좀 고민해볼 문제다.</p>
<p>러닝 커브가 이정도라면 튜토리얼을 빠르게 배우고 블로그 만드는 과정부터 기록해도 될 것 같다는 생각이다.</p>
<p>이렇게 블로그에 글 쓰면서 배웠던 건 취직 이래 처음이라, 취준생 때 생각도 나면서,, 좋네요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SWR 심층탐구 - useSWRImmutable]]></title>
            <link>https://velog.io/@code-bebop/SWR-%EC%8B%AC%EC%B8%B5%ED%83%90%EA%B5%AC-useSWRImmutable</link>
            <guid>https://velog.io/@code-bebop/SWR-%EC%8B%AC%EC%B8%B5%ED%83%90%EA%B5%AC-useSWRImmutable</guid>
            <pubDate>Tue, 16 Aug 2022 01:06:52 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>오늘은 SWR 라이브러리의 세 가지 hook 중 마지막 hook인 <code>useSWRImmutable</code>에 대해 알아보자.</p>
<h2 id="useswrimmutable">useSWRImmutable</h2>
<p><code>useSWRImmutable</code>은 처음 validate 된 이후로 다시는 revalidate를 하지 않는다. <code>useSWRImmutable</code>이 반환한 <code>data</code>는 말 그대로 immutable 하다는 것이다.</p>
<p><code>useSWR</code>의 <code>revalidateIfStale</code>, <code>revalidateOnFocus</code>, <code>revalidateOnReconnect</code> option을 false로 준다면 <code>useSWRImmutable</code>과 완전히 똑같이 동작한다. 이는 <a href="https://swr.vercel.app/ko/docs/revalidation#%EC%9E%90%EB%8F%99-%EA%B0%B1%EC%8B%A0-%EB%B9%84%ED%99%9C%EC%84%B1%ED%99%94%ED%95%98%EA%B8%B0">공식 문서</a>에도 나와있다.</p>
<p>이게 <code>useSWRImmutable</code>의 전부다. 뭔가 기능이 많은 줄 알았는데 아니었다.</p>
<h1 id="🧶">🧶</h1>
<p>이제 SWR에서 제공하는 세 가지 hook과 mutate 등에 대해 모두 알아보았다.
혹시 뭔가 더 포스팅 할만한 내용이 발견된다면 이 시리즈에 추가하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SWR 심층탐구 - useSWRInfinite]]></title>
            <link>https://velog.io/@code-bebop/SWR-%EC%8B%AC%EC%B8%B5%ED%83%90%EA%B5%AC-useSWRInfinite</link>
            <guid>https://velog.io/@code-bebop/SWR-%EC%8B%AC%EC%B8%B5%ED%83%90%EA%B5%AC-useSWRInfinite</guid>
            <pubDate>Mon, 15 Aug 2022 14:00:25 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>저번 포스팅에선 useSWR과 mutate에 대해 알아보았다.</p>
<p>사실 useSWR이 기본형태이고, 좀 더 다양한 상황에 맞게 쓸 수 있게 파생된 useSWR hook들이 몇 가지 있다.</p>
<p>오늘은 그 중 <code>useSWRInfinite</code>에 대해 알아보자.</p>
<h2 id="useswrinfinite">useSWRInfinite</h2>
<p>위에서 이야기 한 것처럼 특수한 상황에서 사용하는 <code>useSWR</code>이 몇 가지 있는데, 개중에서 가장 빈번하게 쓰이는 것이 <code>useSWRInfinite</code> 이다. 주로 pagination을 적용해야하는 상황에서 사용된다.</p>
<p><a href="https://swr.vercel.app/ko/docs/pagination#%EA%B3%A0%EA%B8%89-%EC%82%AC%EB%A1%80">공식 문서</a></p>
<p>우선 <code>useSWRInfinite</code>의 문법을 알아보자.</p>
<pre><code class="language-jsx">const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)</code></pre>
<p><code>getKey</code> 인자는 <code>pageIndex</code>, <code>previousPageData</code>를 인자로 받고, <code>key</code>를 반환하는 함수다. 만일 <code>getKey</code> 함수가 null 을 반환한다면 페이지의 요청이 일어나지 않는다.
여기서 <code>pageIndex</code>는 말 그대로 현재 page의 Index, <code>previousPageData</code>는 이전 페이지에 담긴 <code>data</code>이다.</p>
<p><code>fetcher</code>와 <code>options</code>는 <code>useSWR</code>과 같다. 다만 <code>options</code> 객체에는 <code>useSWRInfinite</code>에만 있는 prop이 몇 개 있다.</p>
<p>반환값 또한 <code>useSWR</code>과 같다. 다만 <code>size</code>, <code>setSize</code>라는 반환값이 추가로 있다.</p>
<p><code>size</code> 반환값은 가져올 페이지 및 반환될 페이지의 수 이다. 쉽게 말하자면 현재 반환된 <code>data</code>가 몇 페이지까지의 데이터인지를 나타내는 수이다. <code>size</code>가 1이라면 현재 <code>data</code>는 1페이지까지의 데이터인 것이고, 2라면 현재 <code>data</code>는 2페이지 까지의 데이터라는 것이다.</p>
<p><code>setSize</code> 반환값은 함수이다. <code>setSize</code> 함수는 인자로 <code>pageIndex</code>를 받는다. <code>setSize(3)</code>을 호출하면, 해당 <code>useSWRInfinite</code>의 <code>getKey</code> 함수가 호출되고, <code>getKey</code> 함수의 <code>pageIndex</code> 매개변수가 3이 된다. 그럼 새로운 <code>pageIndex</code>로 revalidate 되고 data가 갱신된다.
<strong>쉽게 말하자면 <code>setSize(3)</code>을 호출하면 해당 <code>useSWRInfinite</code>의 <code>data</code>가 3페이지까지의 데이터로 갱신된다는 것이다.</strong></p>
<p>한 번에 이해했다면 똑똑한 것이다. 잘 이해가 안 됐다면 정상이다.</p>
<h3 id="좀-더-상세히">좀 더 상세히</h3>
<p><code>useSWRInfinite</code>의 동작을 좀 더 상세히 설명하자면...</p>
<ol>
<li><code>getKey</code> 함수가 실행된다. <code>getKey</code> 함수가 최초 호출됐을 때의 <code>pageIndex</code> 매개변수는 <code>0</code>. API 0번째 데이터를 요청하는 URL을 <code>key</code>로써 반환한다.(이 부분은 개발자가 해야함)</li>
<li><code>fetcher</code> 함수가 실행된다. <code>fetcher</code> 함수의 <code>key</code> 매개변수는 <code>getKey</code> 함수의 반환값. 0번째 페이지 데이터를 요청했고, 응답 데이터를 반환했다고 하자. 반환값은 <code>data</code> 배열에 담김</li>
</ol>
<p>이제 <code>setSize(size =&gt; size + 1)</code> 함수가 호출됐다고 치자.</p>
<ol>
<li><code>getKey</code> 함수가 실행된다. <code>pageIndex</code> 매개변수는 <code>0</code>.</li>
<li><code>fetcher</code> 함수가 실행된다. 역시 0번째 페이지 데이터를 요청하고 반환값이 <code>data</code> 배열에 push됨</li>
<li><code>getKey</code> 함수가 다시 실행된다. 이번엔 <code>pageIndex</code> 매개변수가 1이다.</li>
<li><code>fetcher</code> 함수가 실행된다. 1번째 페이지 데이터를 요청하고 반환값이 <code>data</code> 배열에 push됨</li>
</ol>
<p>그럼 아마 data 배열은 다음과 같을 것이다.</p>
<pre><code class="language-js">[
  [ // 0번째 페이지에 담긴 user 정보. &quot;/users?page=0&quot; API의 반환값
    { name: &#39;Alice&#39;, ... },
    { name: &#39;Bob&#39;, ... },
    { name: &#39;Cathy&#39;, ... },
    ...
  ],
  [ // 1번째 페이지에 담긴 user 정보. &quot;/users?page=1&quot; API의 반환값
    { name: &#39;John&#39;, ... },
    { name: &#39;Paul&#39;, ... },
    { name: &#39;George&#39;, ... },
    ...
  ],
  ...
]</code></pre>
<p>이제 느낌이 올 것이다. <code>setSize(4)</code>를 호출하면 <code>getKey</code> 함수가 <code>pageIndex</code> 매개변수로 <code>0</code> 을 받고 <code>key</code>를 반환, <code>fetcher</code> 함수가 실행, 반환값이 <code>data</code> 배열에 push, <code>getKey</code> 함수가 <code>pageIndex</code> 매개변수로 <code>1</code> 을 받고 <code>fetcher</code>함수가 실행... <code>2</code>를 받고... <code>3</code>을 받고...
<code>fetcher</code> 함수가 실행될 때 마다 반환값이 차례차례 <code>data</code> 배열에 push된다는 것이다.</p>
<p>그 후 우리는 <code>/users?page=0</code> 부터 <code>/users?page=4</code> 까지 의 응답값이 담긴 <code>data</code> 배열을 얻게 될 것이다</p>
<hr>
<p>이번엔 공식문서의 실제 사용 예시 코드를 관찰해보자.</p>
<pre><code class="language-jsx">/ 각 페이지의 SWR 키를 얻기 위한 함수,
// `fetcher`에 의해 허용된 값을 반환합니다.
// `null`이 반환된다면, 페이지의 요청은 시작되지 않습니다.
const getKey = (pageIndex, previousPageData) =&gt; {
  if (previousPageData &amp;&amp; !previousPageData.length) return null // 끝에 도달
  return `/users?page=${pageIndex}&amp;limit=10`                    // SWR 키
}

function App () {
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
  if (!data) return &#39;loading&#39;

  // 이제 모든 users의 수를 계산할 수 있습니다
  let totalUsers = 0
  for (let i = 0; i &lt; data.length; i++) {
    totalUsers += data[i].length
  }

  return &lt;div&gt;
    &lt;p&gt;{totalUsers} users listed&lt;/p&gt;
    {data.map((users, index) =&gt; {
      // `data`는 각 페이지의 API 응답 배열입니다.
      return users.map(user =&gt; &lt;div key={user.id}&gt;{user.name}&lt;/div&gt;)
    })}
    &lt;button onClick={() =&gt; setSize(size + 1)}&gt;Load More&lt;/button&gt;
  &lt;/div&gt;
}</code></pre>
<p>곰곰히 읽어보면 이해가 될 것이다.</p>
<p>이게 <code>useSWRInfinite</code>의 기본적인 것의 전부다. 나머지는 본인이 응용해서 어떻게 잘 써야한다.</p>
<h3 id="useswrinfinite의-key">useSWRInfinite의 key</h3>
<p><code>useSWRInfinite</code>의 <code>key</code>는 일반 <code>useSWR</code>의 <code>key</code>와는 그 생김새가 좀 다르다.
정확히는  우리가 <code>getKey</code>에서 반환한 문자열 앞에 <code>$inf$</code> 라는 접두가 붙는다.</p>
<p><code>useSWRInfinite</code>를 위한 <code>key</code>를 만들기 위해선 <code>unstable_serialize</code>라는 함수를 <code>swr/infinite</code>로부터 import 해서 사용해야한다.
그런데 이름에서부터 알 수 있듯이 현재(1.3.0버전)로썬 이 함수가 불안정한 모양이다. 뭔가 생각한대로 동작하질 않는다. 그래서 그냥 <code>useSWRInfinite</code>가 반환한 바인딩된 <code>mutate</code>를 사용하는 것을 추천한다.</p>
<h1 id="🥀">🥀</h1>
<p>다음 포스트는 <code>useSWRImmutable</code>에 대해 알아볼 생각이다. 이건 내용이 좀 짧다.
pagination 형식의 UI는 단순히 게시판을 떠올릴 수도 있지만, 웹 페이지의 스크롤이 끝에 닿을 때 쯤 <code>setSize</code> 함수의 인자로 <code>size + 1</code>을 넘기고 호출함으로써 무한 스크롤 UI을 쉽게 만들 수도 있다. 주로 그 두 가지가 사용되는 모양이다.</p>
<p>SWR을 공부하다보면 머리를 잘 굴려서 더 재밌는 UI도 만들 수 있겠다는 자신감마저 생긴다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SWR 심층탐구 - useSWR, mutate]]></title>
            <link>https://velog.io/@code-bebop/SWR-%EC%8B%AC%EC%B8%B5%ED%83%90%EA%B5%AC</link>
            <guid>https://velog.io/@code-bebop/SWR-%EC%8B%AC%EC%B8%B5%ED%83%90%EA%B5%AC</guid>
            <pubDate>Mon, 15 Aug 2022 13:10:32 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>요새 SWR에 대해서 배우고 있다. 이전에 SWR, react-query 등을 적용하고, 실제로 사용해보긴 했지만 사실 옵션이 어떻게 되어 있고 반환하는 값에 무엇이 있고, 정확히 어떻게 작동하는가에 대해서는 잘 모르고 있었다.
오늘은 그런 고급 사용법에 대해 알아보자.</p>
<h1 id="swr이란">SWR이란</h1>
<p>SWR은 <code>stale-while-revalidate</code>의 약자다. 무슨 뜻인가 풀어보자면 stale(cache) while revalidate, 즉 서버로의 요청을 하는 동안에 캐시를 나타낸다.. 뭐 그런 뜻을 의도한 것 같다.</p>
<h2 id="낙관적인-ui">낙관적인 UI</h2>
<p>SWR에 대해 본격적으로 알아보기 전에 알아두면 좋은 개념이 하나 있는데, 그것이 바로 낙관적인 UI이다.
어떤 것이냐하면, 유저가 서버에서 온 데이터를 로컬에서 수정했다고 치자. 그럼 어떤 일이 일어날까?</p>
<ol>
<li>서버에 업데이트 요청을 보냄</li>
<li>요청이 끝날 때 까지 기다림</li>
<li>요청이 완료되면 화면에 업데이트된 데이터가 반영된 UI를 나타냄</li>
</ol>
<p>이런 흐름일 것이다. 그런데 데이터가 너무 방대한 나머지 요청이 끝나기까지 2초나 3초 정도가 걸린다면 어떨까? 유저는 화면에 업데이트된 데이터가 UI에 반영되는 것을 확인할 때 까지 기다려야 한다.</p>
<p>이런 현상을 해결하기위해 낙관적인 UI라는 개념이 나왔다. 낙관적인 UI를 적용하면 위의 과정이 다음과 같이 바뀐다.</p>
<ol>
<li>로컬에서 데이터를 업데이트 시키고, 반영된 UI를 즉시 나타냄</li>
<li>서버에 업데이트 요청을 보냄</li>
<li>요청이 끝날 때 까지 기다림</li>
<li>요청이 완료되면 서버 데이터와 로컬 데이터가 일치됨</li>
</ol>
<p>즉 요청이 아무리 오래 걸려도 유저의 눈에는 업데이트한 데이터가 즉시 반영된 것 처럼 보이는 것이다.</p>
<p>이것은 아래에서 알아볼 SWR의 mutate에서 나오는 개념이다. 이제 SWR에 대해 알아보자.</p>
<h2 id="swr의-역할">SWR의 역할</h2>
<p>SWR의 역할은 크게 두 가지이다.</p>
<ol>
<li><strong>서버로의 데이터 요청 시점 컨트롤</strong>
데이터를 언제 요청할 것인지 컨트롤 하기가 용이하고, 같은 API를 요청하는 컴포넌트가 여러 개 있으면 해당 API로의 요청이 단 한 번만 이루어진다.</li>
<li><strong>반환된 데이터를 전역으로 관리하기 용이</strong>
또한 같은 API로 요청한 컴포넌트들은 반환된 데이터를 서로 공유한다. 내부적으로 어딘가에 응답 데이터를 저장해놓고 공유하는 것으로 보인다.</li>
</ol>
<p>이를 통해 로컬에서만 사용하는 전역 상태와 서버 상태를 분리할 수 있다. 이전에는 <code>redux</code>와 <code>redux-saga</code>등을 이용해서 서버 상태를 관리했는데, 그게 좀 복잡하기도 했고 반복되는 코드가 많았다. 그런 것들을 좀 더 나은 방법으로 해결해주는 역할을 한다고 평가할 수 있겠다.</p>
<h2 id="useswr">useSWR</h2>
<p>기본적으로 <code>useSWR</code>이라는 hook을 사용해서 데이터를 요청한다.
<code>useSWR</code>의 문법은 다음과 같다.</p>
<pre><code class="language-js">const { data, error, isValidating, mutate } = useSWR(key, fetcher, option)</code></pre>
<p><code>key</code> 인자는 해당 <code>useSWR</code>을 식별하기위한 문자열(혹은 함수, 배열, null)이다. 같은 <code>key</code>를 사용하는 <code>useSWR</code>들은 모두 같은 요청을 보내는 것으로 간주하고 최적화를 적용한다.
<code>fetcher</code> 인자는 매개변수로 <code>key</code>를 받는 함수이다. 이 함수의 반환값이 <code>useSWR</code>의 반환값 중 <code>data</code>에 해당한다.
<code>option</code> 인자는 해당 <code>useSWR</code>의 옵션을 담은 객체다. 옵션은 <a href="https://swr.vercel.app/ko/docs/options">공식 문서</a>에서 확인할 수 있다.</p>
<p>말로만 설명하면 좀 복잡할 수 있다. 아래는 실제 사용할 때의 모습이다.</p>
<pre><code class="language-jsx">// card.tsx

const fetcher = async url =&gt; {
    const response = await axios.get(`someServer${url}`);
     return response.data;
}

const { data, error } = useSWR(&#39;/cards&#39;, fetcher);

return (
    &lt;Fragment&gt;
        {
            data.map(card =&gt; &lt;p key={card.id}&gt;{card.title}&lt;/p&gt;)
        }
    &lt;/Fragment&gt;
);</code></pre>
<p>이런 느낌으로. 대충 어떻게 사용하는지 감을 잡을 수 있을 것이다.</p>
<p>옵션을 따로 설정하지 않았다면 여러가지 편리한 기능들이 기본값으로 적용된다. 윈도우 창이 포커싱 되었을 때 재요청 해주기도 하고, 브라우저의 네트워크가 끊겼다가 다시 연결되었을 때 재요청 해주기도 하고, 같은 <code>key</code>를 사용하는 <code>useSWR</code>의 호출이 2초 이내에 여러번 일어났을 경우 1번만 요청하기 등등... 이것저것 편리하다.</p>
<h3 id="useswr과-cache">useSWR과 cache</h3>
<p>조금만 더 자세히 <code>useSWR</code>에 대해 알아보자. <code>useSWR</code>이 호출되고 <code>fetcher</code>가 실행된 뒤, <code>fetcher</code>가 반환된 값은 해당 <code>key</code>를 사용하는 <code>useSWR</code>의 cache가 된다.</p>
<p><strong>무슨 말이냐면 <code>fetcher</code>가 반환한 <code>data</code>가 로컬 어딘가에 저장된다는 것이다. 그리고 해당 <code>key</code>를 사용하는 <code>useSWR</code>에 별도의 revalidate 요청이 없는 한, 해당 <code>key</code>를 사용하는 <code>useSWR</code>의 <code>data</code>는 caching된 상태를 유지하는 것이다.</strong></p>
<p>이것이 <code>SWR</code> 라이브러리의 핵심 개념이다. 데이터를 요청하고, 캐싱해서 재사용한다. 그리고 새로운 revalidate 요청이 들어오면 서버로 데이터를 요청하고, 다시 캐싱하고 재사용한다. 이것의 반복이다.</p>
<p>이상의 개념만 알고 있으면 <code>SWR</code>의 80%는 알았다고 해도 과언이 아니다.</p>
<h3 id="useswr과-revalidate">useSWR과 revalidate</h3>
<p><strong>revalidate라는 단어가 의미하는 것은, 특정 <code>useSWR</code>의 <code>fetcher</code>를 호출해서 서버에서 반환된 <code>data</code>로 해당 <code>useSWR</code>의 cache를 갱신한다는 것이다.</strong> 이것도 중요한 개념이다.</p>
<h2 id="mutate">mutate</h2>
<p><code>useSWR</code> 의 반환값 중 <code>mutate</code> 라는 함수가 있다. <strong><code>mutate</code>는 언제 데이터를 최신화 할 것인지를 컨트롤 할 수 있게 해주는 역할이라고 생각한다.</strong></p>
<p><code>mutate</code>의 사용방법은 좀 여러가지이고, 헷갈리기 쉬우니 잘 구분해야한다.
일단 <code>mutate</code>의 문법을 살펴보자.</p>
<pre><code class="language-js">const data = mutate(key, data, option);</code></pre>
<p><code>key</code> 인자는 최신화하고자 하는 <code>useSWR</code>의 <code>key</code>이다. 내가 <code>/cards</code>라는 <code>key</code>를 사용하는 <code>useSWR</code>을 최신화하려 한다면 <code>/cards</code>를 <code>key</code> 인자로 주면 된다.
<code>data</code> 인자는 해당 <code>useSWR</code>을 최신화하기 위한 data다. <strong>정확히는 해당 <code>useSWR</code>의 cache를 <code>data</code>로 바꾸는 것이다.</strong>
<code>option</code> 인자는 해당 <code>mutate</code>의 옵션 객체다. 이 또한 <a href="https://swr.vercel.app/ko/docs/mutation#optimistic-updates">공식 문서</a>에서 확인할 수 있다.</p>
<p>반환값인 <code>data</code>는 2번째 인자인 <code>data</code>와 동일하다.</p>
<p>나는 <code>mutate</code> 함수에서 굉장히 헷갈렸다. 하지만 위에서 설명한 <code>useSWR</code>과 cache 개념만 잘 이해했다면, 덜 헷갈릴 것이라고 단언할 수 있다.</p>
<p><strong><code>mutate</code>는 정확히는 해당 <code>key</code>를 사용하는 <code>useSWR</code>의 cache를 <code>data</code>로 최신화 시켜준다.</strong>
<strong>그리고 <code>option</code> 객체를 따로 넘겨주지 않았다면 해당 <code>key</code>를 사용하는 <code>useSWR</code>를 revalidate한다.</strong> &lt;&lt;&lt;&lt;&lt; 이게 정말 중요하다!!!!</p>
<p><strong>내가 <code>mutate</code>로 cache를 최신화 했다고 해도, 서버의 데이터를 최신화 시키지 않았다면 해당 <code>useSWR</code>이 revalidate 되면서 서버에 있는 최신화 이전의 <code>data</code>가 cache로 갱신된다.</strong></p>
<h3 id="mutate-option">mutate option</h3>
<p>그리고 이런... <code>mutate</code> 이후의 revalidate를 <code>mutate</code>의 <code>option</code> 객체로 어느정도 컨트롤 할 수 있다.
<strong><code>option</code> 객체에 그냥 falsy한 값을 주면 <code>mutate</code> 이후에 해당 <code>useSWR</code>을 revalidate 하지 않는다.</strong> 그래서 기본값은 true인 것 같다.
객체를 통해 좀 더 세세한 옵션을 지정할 수 있긴 한데, 그랬더니 내가 생각한대로 잘 동작하질 않아서... 그에 대해선 좀 더 알아봐야 한다.</p>
<h3 id="mutate와-key-바인딩">mutate와 key 바인딩</h3>
<p><code>mutate</code> 는 두 가지 경로로 접근할 수 있다. </p>
<ol>
<li><code>useSWRConfig</code>의 반환값 <code>mutate</code>
<code>useSWRConfig</code>이라는 hook의 반환값으로써의 <code>mutate</code>는 반드시 <code>key</code> 인자를 넘겨주어야 한다.</li>
<li><code>useSWR</code>의 반환값 <code>mutate</code></li>
</ol>
<p><strong>어떤 <code>useSWR</code>의 반환값으로써의 <code>mutate</code>는 해당 <code>useSWR</code>의 <code>key</code>가 이미 바인딩되어 있는 상태이다.</strong> 그래서 굳이 <code>key</code> 인자를 넘겨주지 않아도 된다.</p>
<h3 id="mutate와-data-인자">mutate와 data 인자</h3>
<p><code>mutate</code>의 <code>data</code> 인자는 비워놓을 수 있다. 만일 <code>data</code> 인자를 넘겨주지 않은 채 <code>mutate</code>를 호출한다면 해당 <code>useSWR</code>을 revalidate 하는 행동만을 한다.</p>
<h1 id="🍀">🍀</h1>
<p>이 글만 보고 SWR을 손바닥 뒤집듯이 다루기는 쉽지 않을 것이다. 이 글을 보고, 직접 API 요청을 해보고, 캐시를 다루고, 개발자 도구 - 네트워크에서 어떻게 요청이 가는지 관찰하고... 하다 보면 어느새 SWR을 내 생각대로 다룰 수 있게 될 것이다.</p>
<p>다음 포스트에선 useSWRInfinite에 대해서 알아볼 계획이다. 이걸 다 배우고 나면 SWR으로 멋지게 서버 상태를 전역으로 관리하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react-beautiful-dnd 이슈]]></title>
            <link>https://velog.io/@code-bebop/react-beautiful-dnd-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@code-bebop/react-beautiful-dnd-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Mon, 15 Aug 2022 03:41:15 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>최근 Drag And Drop(이하 dnd) 기능을 구현해야 할 일이 있어 <code>react-beautiful-dnd</code> 라는 라이브러리를 배우고 사용 중에 있다.
이 라이브러리는 현재 나와있는 dnd 라이브러리 중에서는 가장 사용하기 쉽고 필요한 기능도 다 있는 라이브러리라고 평가된다.</p>
<p>이 라이브러리를 사용중에 이슈가 발생했는데, 컴포넌트가 랜더링 되고 나서 처음엔 dnd 기능이 정상적으로 작동하는데, 두 번째 부터는 dnd 기능이 작동하지 않는 것이다.
콘솔로그엔 다음과 같은 에러 메시지가 출력되고 있었다.
<img src="https://velog.velcdn.com/images/code-bebop/post/1f761e63-bbe3-4845-a034-c6d29d72a406/image.PNG" alt="1">
<em>unable to find draggable with id</em>
아마도 dnd를 통해 리스트 컴포넌트의 순서를 바꾼 뒤에 리스트 컴포넌트를 추적하지 못 하고 있는 것 같다.</p>
<pre><code class="language-tsx">        &lt;DragDropContext onDragEnd={onDragEnd}&gt;
          &lt;Droppable droppableId=&quot;interviewTemplate&quot;&gt;
            {provided =&gt; (
              &lt;div {...provided.droppableProps} ref={provided.innerRef}&gt;
                {templates.map((template, index) =&gt; {
                  return (
                    &lt;Draggable
                      draggableId={template.interviewTemplateId}
                      index={index}
                    &gt;
                      {provided =&gt; {
                        return (
                          &lt;Container
                            ref={provided.innerRef}
                            {...provided.draggableProps}
                            {...provided.dragHandleProps}
                          &gt;
                            &lt;p&gt;{template.title}&lt;/p&gt;
                          &lt;/Container&gt;
                        );
                      }}
                    &lt;/Draggable&gt;
                  );
                })}
                {provided.placeholder}
              &lt;/div&gt;
            )}
          &lt;/Droppable&gt;
        &lt;/DragDropContext&gt;</code></pre>
<p>모든 게 공식문서대로 적용되었는데, 뭐가 문제였을까?</p>
<h1 id="map-함수가-반환하는-reactelement의-key-prop">map 함수가 반환하는 ReactElement의 key prop</h1>
<p>정답은 바로 <strong>map 함수가 반환하는 ReactElement의 key prop을 넣지 않아서</strong>이다.
나같은 경우 map 함수가 반환하는 Draggable 컴포넌트에 key prop을 넣었더니 제대로 작동했다.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/dca233c3-f697-4b43-80fe-98a961b5a515/image.gif" alt="2">
<em>이제 잘 작동한다</em></p>
<p>왜 key prop을 주지 않은 것 만으로 이런 이슈가 발생했을까?
그걸 알기 위해선 <a href="https://ko.reactjs.org/docs/reconciliation.html#recursing-on-children">react의 list에서 key prop의 역할</a> 에 대해 알 필요가 있다.</p>
<h1 id="key-prop의-역할">key prop의 역할</h1>
<p>위의 링크로 접속해보면, react 공식문서에서 이에 대해 설명하고 있다.</p>
<pre><code class="language-jsx">&lt;ul&gt;
  &lt;li&gt;Duke&lt;/li&gt;
  &lt;li&gt;Villanova&lt;/li&gt;
&lt;/ul&gt;

&lt;ul&gt;
  &lt;li&gt;Connecticut&lt;/li&gt;
  &lt;li&gt;Duke&lt;/li&gt;
  &lt;li&gt;Villanova&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<p>예를 들어 위의 리스트에서 아래의 리스트처럼 순서를 변경했을 때, React는 rerendring을 할 것이다. 그래야 뷰포트에 바뀐 리스트의 순서가 반영될테니까.
그런데 위와 같이 <strong>리스트의 첫 번째 순서에 새로운 컴포넌트가 추가된 경우</strong>에, 리스트의 순서도 바뀌게 되고, 결과적으로 React는 모든 컴포넌트를 rerendering 한다. 실상은 그저 리스트의 순서만 바뀐 것인데 말이다.</p>
<p><strong>이런 비효율적인 상황을 해결하기 위해서  key prop이 사용된다.</strong></p>
<pre><code class="language-jsx">&lt;ul&gt;
  &lt;li key={2015}&gt;Duke&lt;/li&gt;
  &lt;li key={2016}&gt;Villanova&lt;/li&gt;
&lt;/ul&gt;

&lt;ul&gt;
  &lt;li key={2014}&gt;Connecticut&lt;/li&gt;
  &lt;li key={2015}&gt;Duke&lt;/li&gt;
  &lt;li key={2016}&gt;Villanova&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<p>이렇게 key prop을 추가하면, 리스트의 순서가 바뀌었을 때 React는 먼저 key prop을 통해서 리스트 컴포넌트를 식별한다. 리스트 순서가 바뀌기 전과 바뀐 후의 컴포넌트의 key prop을 비교하고, 일치한다면 같은 컴포넌트로 판단하고 rerendering 하지 않고 컴포넌트의 순서를 이동만 시킨다는 것이다.</p>
<p><strong>그렇기 때문에 key prop은 고유해야한다.</strong> 간혹 map 함수의 콜백함수의 2번째 인자인 index를 key prop의 값으로 주는 코드도 있는데, 그랬다간 코드가 예상치 못한 방향으로 작동할 수 있다. index 값은 리스트의 순서가 변경되면 바뀔 수 있기 때문에.</p>
<hr>
<p>이제 key prop이 <code>react-beautiful-dnd</code> 라이브러리의 작동방식에서 어떤 역할을 하는지 대충은 알 수 있다.
아마도 key prop을 주지 않았을 때, 내가 위의 gif처럼 dnd를 통해 리스트의 순서를 바꾸면 리스트 전체가 rerendering 되면서 <code>react-beautiful-dnd</code> 라이브러리가 리스트 컴포넌트를 추적하지 못 하게 되는 것 같다. 비록 기본적으로 <code>draggableId</code> prop을 통해 컴포넌트를 추적하더라도 말이다.</p>
<h1 id="✔">✔</h1>
<p>리스트에서의 key prop의 역할에 대해서는 이미 이전에 알아보고 배운 적이 있다. 그럼에도 불구하고 그 중요성에 대해 간과했기 때문에 일어난 이슈다.
key prop이 중요하지만, <strong>특히 리스트의 순서를 바꾸는 때에는 훨씬 더 중요하다</strong>는 걸 주의해야겠다고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react 17에서 18으로 마이그레이션]]></title>
            <link>https://velog.io/@code-bebop/react-17%EC%97%90%EC%84%9C-18%EC%9C%BC%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/@code-bebop/react-17%EC%97%90%EC%84%9C-18%EC%9C%BC%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</guid>
            <pubDate>Wed, 13 Jul 2022 09:25:01 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>사이드 프로젝트 작업물이 많아짐에 따라 포트폴리오의 작업물 파트의 UI도 바꿔야겠다는 생각이 들었다.
<img src="https://velog.velcdn.com/images/code-bebop/post/7573470a-0d5e-4665-8579-13c6cb42a1b3/image.PNG" alt="1">
이 부분이 작업물 파트인데 작업물 하나당 100vh의 높이를 가지고 있다. 처음엔 작업물이 별로 없었으니 좀 풍성하게 보이려고 크기를 크게 만들었는데, 작업물이 많아짐에 따라 보는 게 불편해지기 시작했다.
그래서 카드, 그리드, 슬라이드 같은 디자인으로 바꿀 생각을 했고, 이 오래된 프로젝트를 업데이트 할 계획이다.</p>
<p>이 프로젝트의 react 버전은 <code>17.0.1</code> 이고, 현재 최신 버전은 <code>18.2.0</code> 이다. 나는 <code>package</code>의 모든 라이브러리를 최신화했는데, react 코드에 문제가 생겼다. 때문에 &#39;어떻게 react의 버전을 마이그레이션 할 수 있을까?&#39; 라는 포스트를 작성하면서 고치기로 했다.</p>
<h1 id="react-마이그레이션">react 마이그레이션</h1>
<h2 id="패키지-업데이트">패키지 업데이트</h2>
<p>우선 패키지 업데이트다. 라이브러리를 업데이트 하려면 <code>npm</code> 명령어를 사용하면 되지만, <code>package</code> 전체를 업데이트 하기 위해선 어떻게 해야할까? 바로 <a href="https://github.com/raineorshine/npm-check-updates">npm-check-updates</a> 라이브러리를 사용한다.
이 라이브러리를 설치하고 <code>ncu -u</code> 명령어로 해당 프로젝트의 <code>package</code>의 모든 라이브러리의 버전을 최신화 할 수 있다.</p>
<h2 id="lint-수정">lint 수정</h2>
<p>일단 lint 오류부터 없애자.</p>
<pre><code class="language-json">{
  &quot;rules&quot;: {
    &quot;react/jsx-uses-react&quot;: &quot;off&quot;,
    &quot;react/react-in-jsx-scope&quot;: &quot;off&quot;
  },
}</code></pre>
<p><code>eslintrc.json</code> 에 이 부분만 추가해주면 된다.</p>
<h2 id="webpack-설정-변경">webpack 설정 변경</h2>
<p>React 17 릴리즈 버전부터는 <code>import React from &#39;react&#39;</code> 를 달지 않아도 jsx문법을 사용할 수 있게 바뀌었다. 그래서 그 부분을 제거했는데</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/788616c7-7422-44f6-9b37-7721499078a5/image.PNG" alt="2">
<em>원래 코드</em></p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/2c67c595-89b8-4166-9291-8ef3794e5af1/image.PNG" alt="3">
<em>수정 했을 때 문법오류</em></p>
<p>이 모양이다. 그냥 문법 오류겠거니 생각하고 컴파일 했더니 컴파일 오류가 발생했다. 뭐가 문제일까?
React 공식문서를 뒤져보다가 <a href="https://ko.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html">How to Upgrade to the New JSX Transform?</a> 이라는 문단을 발견했다. 이걸 보고 따라해보자.</p>
<p>기존의 <code>@babel/preset-react</code>의 <code>runtime</code> 옵션의 기본값은 <code>classic</code>이었고, 이걸 <code>automatic</code>으로 바꾸어 주라고 한다. 또한 babel 8 버전부터는 이 옵션의 기본값은 <code>automatic</code>으로 바뀐다고 한다.</p>
<pre><code class="language-js">// If you are using @babel/preset-react
{
  &quot;presets&quot;: [
    [&quot;@babel/preset-react&quot;, {
      &quot;runtime&quot;: &quot;automatic&quot;
    }]
  ]
}</code></pre>
<p>근데 babel 설정을 바꾸어 보아도 해결이 안 된다.</p>
<h2 id="codemode-명령어-사용">codemode 명령어 사용</h2>
<p><a href="https://ko.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports">react 공식문서</a>에 의하면 <code>npx react-codemod update-react-imports</code>를 실행하면 환경에 대한 몇 가지 질문 후에 자동으로 해당 프로젝트에 대한 JSX transform 업데이트가 진행된다고 한다.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/e0ff9df8-22af-4d47-a511-0d573b032963/image.PNG" alt="4"></p>
<ol>
<li>어떤 디렉토리에 해당 업데이트를 수행할지</li>
<li>현재 어떤 JS 언어환경을 사용중인지</li>
<li>업데이트를 진행하면서 해당 업데이트에 해당하는 필요없는 import를 모두 없앨 것인지</li>
</ol>
<p>상기의 세가지 질문에 답하고 나면 업데이트가 진행된다.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/3013b614-e19d-45ec-bea7-55cf7f00151e/image.PNG" alt="5">
뭔가 업데이트가 적용되긴 했지만 여전히 JSX transform 문제는 해결되지 않았다.</p>
<h2 id="tsconfigjson-수정">tsconfig.json 수정</h2>
<p>자꾸 ts오류가 생기니까 <code>tsconfig.json</code>을 수정할 생각이 들었다.</p>
<p>아무도! 구글에서 찾은 어느 포스트에서도 이 이야기를 해주지 않았다. react 공식문서에도</p>
<blockquote>
<p>TypeScript supports the new JSX transform in v4.1 and up.</p>
</blockquote>
<p>라고만 해놔서 Typescript는 안 건드려도 되는 줄 알았다.</p>
<pre><code class="language-json">{
  &quot;compilerOptions&quot;: {
    &quot;jsx&quot;: &quot;react&quot;,
  }
}
</code></pre>
<p><code>tsconfig.json</code>의 이 부분을 <code>react-jsx</code>로 바꿔주면 된다.
<img src="https://velog.velcdn.com/images/code-bebop/post/b6a03672-6ff8-4e6d-8b4a-55b54c7e64a2/image.PNG" alt="6">
<em>문법 오류 없음</em>
<img src="https://velog.velcdn.com/images/code-bebop/post/32b1c6e9-30b3-4697-84d9-7509f6e97803/image.PNG" alt="7">
<em>컴파일 잘 됨</em></p>
<p>다른 오류도 발생하지 않는다.
<strong>내가 이겼다!</strong></p>
<h1 id="🍞">🍞</h1>
<p>오늘은 때 늦은 React 마이그레이션 포스팅을 했다. 이런 작은 프로젝트의 환경을 업데이트 하는 것에 5시간이나 걸렸다. 하지만 배운 것이 많다. 다음에 이런 업데이트를 하면 좀 더 능숙하게 해결할 수 있을 것이다.</p>
<p>이제 옛날 코드에 새 코드를 용접할 차례다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 Next.js 인가?]]></title>
            <link>https://velog.io/@code-bebop/%EC%99%9C-Next.js-%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@code-bebop/%EC%99%9C-Next.js-%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Thu, 16 Jun 2022 17:19:40 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>요새 Next.js에 대해서 공부하다가, &#39;이거는 React 처음 배울 때 같이 배우고 썼어야 했구나&#39; 같은 생각을 하고 있다.
물론 Next.js라는 이름을 들은적은 있다. 왜 이걸 배워야 하는가? 하는 글도 읽었었다. 근데 그 때의 나는 별 관심이 없었나보다.</p>
<p>그래서 Next.js 프로젝트를 시작하기 전에 이것을 배워야 하는 이유를 정리하는 시간을 가져볼 것이다.</p>
<h1 id="왜-nextjs-인가">왜 Next.js 인가?</h1>
<h2 id="1-seo를-잘-한다">1. SEO를 잘 한다.</h2>
<h3 id="11-seo는-무엇인가">1.1. SEO는 무엇인가?</h3>
<p>SEO가 뭐냐면 Search Engine Optimization 의 약자로, 검색 엔진 최적화라는 뜻이다. 구글이든 뭐든 검색엔진이 웹사이트를 검색 결과에 띄우기 전에, <strong>웹사이트는 분석을 당한다.</strong> 그리고 <strong>분석 할 때에 가장 크게 참고하는 것이 HTML 코드</strong>이다.</p>
<h3 id="12-csr은-seo에-약하다">1.2. CSR은 SEO에 약하다.</h3>
<p>React로 만든 웹은 HTML 코드 분석을 못 한다. 정확히는 CSR은 못 한다. (빌드 도구의 플러그인을 사용하면 가능하긴 하지만, 너무나 비효율적이다.)
React 웹을 빌드하면 body태그 내용물이 없는 HTML 파일 하나에 번들된 css와 js가 있을 뿐이다. 사용자는 이 HTML 파일을 열면 css와 js가 실행되어 DOM을 만드는 흐름이 이어진다.</p>
<p>문제는 <strong>모든 검색엔진이 이 HTML을 분석할 때, js 코드를 실행하지는 않는다</strong>는 것이다. (구글 검색엔진은 js코드도 고려하여 분석한다.)</p>
<h3 id="13-ssr은-seo에-강하다">1.3. SSR은 SEO에 강하다.</h3>
<p>SSR은 서버에서 렌더링을 다 한 다음에 완성된 HTML 파일을 사용자에게 준다. 이것은 <strong>검색엔진이 SSR방식의 웹페이지를 분석할 때에 또한 완성된 HTML 파일을 보게 되는 것을 의미</strong>한다.
그것은 분명히 SEO에 유리하게 작용한다.</p>
<h2 id="2-nextjs는-프레임워크다">2. Next.js는 프레임워크다.</h2>
<h3 id="21-react는-라이브러리다">2.1. React는? 라이브러리다.</h3>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/8f548b74-9239-467a-9951-c85884bd333e/image.PNG" alt="1">
<em>오피셜이다</em></p>
<p>React를 처음 사용할 때가 생각나는가? 나는 HTML, CSS, JS만으로 웹페이지를 만들다 React를 처음 썼을 때 너무 편해서 너무 좋았다.
엄청나게 긴 HTML 코드를 뒤적거리며 마크업을 추가하지 않아도 되고, 반복되는 구조의 마크업을 일일이 복사 붙여넣기 하지 않아도 되며, 데이터를 기반으로 동적으로 반응하는 마크업을 만들 수 있었다. 그 이외에도 엄청나게 편한 코딩이 가능해서 감동했다.</p>
<p>근데 이런 React는 라이브러리다. <code>react-router</code>, <code>redux</code>, <code>styled-components</code> 같은 라이브러리들을 더 붙여서 프레임워크처럼 사용이 가능하지만, <code>react</code>는 라이브러리다.</p>
<h3 id="22-nextjs는-react를-사용하는-프레임워크이다">2.2. Next.js는 React를 사용하는 프레임워크이다.</h3>
<p>무슨 말이 하고 싶냐면, <strong>프레임워크가 라이브러리보다 더 개발이 편하다</strong>는 것이다. 물론, 보통 정확한 사용법을 익혀야하는 프레임워크가 러닝커브가 더 심하겠지만 그것을 고려하더라도 프레임워크를 익혔을 때의 간편함이 압도적이다.</p>
<p>게다가 Next.js는 <code>react</code> 라이브러리를 사용하는 프레임워크이기 때문에, <code>react</code>를 배웠고 사용해온 개발자라면 러닝커브가 매우매우 짧아진다. 라우팅 방식도 <code>react-router</code>와 매우 유사하기 때문에 더더욱 쉽다.</p>
<p>정말 정말 개발이 편해진다. <code>react</code>로 웹 개발을 해봤다면, 꼭 배워보길 바란다.</p>
<h2 id="3-ssr과-csr를-선택할-수-있다">3. SSR과 CSR를 선택할 수 있다.</h2>
<h3 id="31-nextjs에서도-csr을-쓸-수-있다">3.1. Next.js에서도 CSR을 쓸 수 있다.</h3>
<p>SSR도 장점과 단점이 있다. 어떤 상황에서는 CSR이 더 유리하고, 또 다른 상황에서는 SSR이 더 유리하다. <strong>CSR과 SSR, 무슨 방법을 사용할지 유동적으로 선택할 수 있다면 굉장히 좋을 것</strong>이다.
<strong>그게 Next.js에서 가능하다.</strong></p>
<p>Next.js에서는 CSR을 사용하고 싶을 때, SSR을 사용하고 싶을 때를 개발자가 구분하여 다르게 웹페이지를 제공할 수 있다.</p>
<h2 id="4-url-마스킹이-가능하다">4. URL 마스킹이 가능하다.</h2>
<p>웹을 개발하다보면 어떤 요청 URL에서 API KEY 같은 것을 숨기고 싶을 수도 있고, 라우트 URL에 담긴 정보를 사용자에게 보이고 싶지 않을 때가 있을 것이다.
그럴 때에 Next.js에서는 실제로 요청하는 URL과 사용자에게 보여지는 URL을 다르게 할 수 있다. 아주 간편하게 말이다.</p>
<p>매우 소소한 부분이라고 생각할 수 있지만, 나는 이것을 React와 비교했을 때 굉장히 장점이라고 생각한다.
<code>react</code>에서도 다른 라이브러리를 사용해서 URL 마스킹이 가능하겠지만, 그게 다 빌트인 되어있는 프레임워크가 훨씬 편하다. 그것은 비교적 개발에 온전히 집중할 수 있다는 뜻이다.</p>
<h1 id="그러니까-꼭-한-번-배워보십시오">그러니까 꼭 한 번 배워보십시오.</h1>
<p><strong><code>react</code>를 써봤다면 꼭! Next.js를 배워보라.</strong> 나는 왜 이제 이걸 배웠을까 조금 후회중이다.
나는 이제 배우는 중이라 나열할 수 있는 장점이 적지만, 분명 더욱 더 많은 장점이 있다.</p>
<h1 id="🛠">🛠</h1>
<p>이것을 계기로 다음 프로젝트는 <code>프레임워크</code>를 사용해서 진행하려고 한다.
CSS도 <code>tailwindcss</code> 라는 프레임워크를 알아보고 있는데, 이것도 굉장하다. 개발시간의 대부분을 차지하는 CSS를 작성하는 시간이 줄어들면, 개발에 집중할 수 있는 시간도 늘어나고 프로젝트 진행 기간도 대폭 단축될 것이다. 사용법에 익숙해지고 나면 꼭 포스팅하려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[vite에서 favicon 적용하기(vite-plugin-favicon)]]></title>
            <link>https://velog.io/@code-bebop/vite%EC%97%90%EC%84%9C-favicon-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@code-bebop/vite%EC%97%90%EC%84%9C-favicon-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 07 Jun 2022 10:18:51 GMT</pubDate>
            <description><![CDATA[<p>vite로 빌드한 웹에 favicon을 적용하고 싶은데, vite 자체 config에는 관련한 설정이 없다. (작성일 기준)
더 알아보니 관련 플러그인을 찾았는데, 이상하게 사용 방법에 관한 포스트나 글이 하나도 없는 것이다. 그래서 내가 포스팅했다.</p>
<h1 id="vite-plugin-favicon">vite-plugin-favicon</h1>
<p>일단 <code>vite-plugin-favicon</code> 이라는 vite plugin이 필요하다.
공식문서는 <a href="https://github.com/josh-hemphill/vite-plugin-favicon">여기</a>에서 볼 수 있다.</p>
<h2 id="viteconfig-설정">vite.config 설정</h2>
<p><code>vite-plugin-favicon</code>을 설치했다면 <code>vite.config</code>에서 설정을 해 줄 차례이다.</p>
<pre><code class="language-ts">// vite.config.ts

import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;
import { ViteFaviconsPlugin } from &quot;vite-plugin-favicon&quot;;

export default defineConfig({
  plugins: [
    react(),
    ViteFaviconsPlugin({
      logo: &quot;public/assets/logo.png&quot;,
    }),
  ],
  base: &quot;/lostark_character_search/&quot;,
});
</code></pre>
<p>기본적인 사용법은 이거다. <code>ViteFaviconPlugin</code>에 파라미터를 주지 않고 그냥 호출하면 <code>assets/logo.png</code> 를 자동으로 찾아서 favicon으로 사용한다. 다른 폴더에 아이콘 사진이 있다면 위와 같이 <code>logo</code>에 아이콘 파일의 경로를 지정해주면 된다.</p>
<p>아마 png와 svg 확장자만 받는 것 같다. ico 파일으로 넣으면  안 된다.</p>
<p>이제 빌드를 하면 이런 html이 나온다.
<img src="https://velog.velcdn.com/images/code-bebop/post/908846f9-7ecd-4ea3-85de-71ba5931805a/image.PNG" alt="1">
<em>으악 뭐야 이거</em></p>
<p><code>head</code>에 뭔가 잔뜩 들어있다. 이게 뭔가하면 모든 환경에 대응해서 최적화된 favicon을 싹 다 설정한 것이다.
build된 폴더의 assets 폴더를 열어보면 모든 환경에 1:1로 대응하는 favicon이 잔뜩 들어있다.
<img src="https://velog.velcdn.com/images/code-bebop/post/197dd412-2127-4b45-bde8-f61095a7f50e/image.PNG" alt="2"></p>
<p><strong>근데 이러고 배포하면 favicon을 못 찾는다.</strong>
왜냐면 <code>link</code> 태그의 <code>href</code> 속성을 보면 그냥 문자열의 나열만 있기 때문이다. 아이콘 파일 명의 일부이긴 한데, 왜 파일명의 일부분만 <code>href</code>에 들어있는지 모르겠다.</p>
<p>이 문제를 해결하기 위해서는 <code>faviconsConfig</code>을 따로 설정해줘야한다.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/8c8d8d7c-cfa1-4c0d-b8c7-438a6feb3cea/image.PNG" alt="3"></p>
<p><em>이 부분에 나와 있다.</em></p>
<p><code>faviconsConfig</code> 문서는 <a href="https://github.com/itgalaxy/favicons#usage">여기</a>에 나와 있다.</p>
<pre><code class="language-ts">var favicons = require(&quot;favicons&quot;),
  source = &quot;test/logo.png&quot;, // Source image(s). `string`, `buffer` or array of `string`
  configuration = {
    path: &quot;/&quot;, // Path for overriding default icons path. `string`
    appName: null, // Your application&#39;s name. `string`
    appShortName: null, // Your application&#39;s short_name. `string`. Optional. If not set, appName will be used
    appDescription: null, // Your application&#39;s description. `string`
    developerName: null, // Your (or your developer&#39;s) name. `string`
    developerURL: null, // Your (or your developer&#39;s) URL. `string`
    dir: &quot;auto&quot;, // Primary text direction for name, short_name, and description
    lang: &quot;en-US&quot;, // Primary language for name and short_name
    background: &quot;#fff&quot;, // Background colour for flattened icons. `string`
    theme_color: &quot;#fff&quot;, // Theme color user for example in Android&#39;s task switcher. `string`
    appleStatusBarStyle: &quot;black-translucent&quot;, // Style for Apple status bar: &quot;black-translucent&quot;, &quot;default&quot;, &quot;black&quot;. `string`
    display: &quot;standalone&quot;, // Preferred display mode: &quot;fullscreen&quot;, &quot;standalone&quot;, &quot;minimal-ui&quot; or &quot;browser&quot;. `string`
    orientation: &quot;any&quot;, // Default orientation: &quot;any&quot;, &quot;natural&quot;, &quot;portrait&quot; or &quot;landscape&quot;. `string`
    scope: &quot;/&quot;, // set of URLs that the browser considers within your app
    start_url: &quot;/?homescreen=1&quot;, // Start URL when launching the application from a device. `string`
    preferRelatedApplications: false, // Should the browser prompt the user to install the native companion app. `boolean`
    relatedApplications: undefined, // Information about the native companion apps. This will only be used if `preferRelatedApplications` is `true`. `Array&lt;{ id: string, url: string, platform: string }&gt;`
    version: &quot;1.0&quot;, // Your application&#39;s version string. `string`
    pixel_art: false, // Keeps pixels &quot;sharp&quot; when scaling up, for pixel art.  Only supported in offline mode.
    loadManifestWithCredentials: false, // Browsers don&#39;t send cookies when fetching a manifest, enable this to fix that. `boolean`
    manifestMaskable: false, // Maskable source image(s) for manifest.json. &quot;true&quot; to use default source. More information at https://web.dev/maskable-icon/. `boolean`, `string`, `buffer` or array of `string`
    icons: {
      // Platform Options:
      // - offset - offset in percentage
      // - background:
      //   * false - use default
      //   * true - force use default, e.g. set background for Android icons
      //   * color - set background for the specified icons
      //
      android: true, // Create Android homescreen icon. `boolean` or `{ offset, background }` or an array of sources
      appleIcon: true, // Create Apple touch icons. `boolean` or `{ offset, background }` or an array of sources
      appleStartup: true, // Create Apple startup images. `boolean` or `{ offset, background }` or an array of sources
      favicons: true, // Create regular favicons. `boolean` or `{ offset, background }` or an array of sources
      windows: true, // Create Windows 8 tile icons. `boolean` or `{ offset, background }` or an array of sources
      yandex: true, // Create Yandex browser icon. `boolean` or `{ offset, background }` or an array of sources
    },
    shortcuts: [
      // Your applications&#39;s Shortcuts (see: https://developer.mozilla.org/docs/Web/Manifest/shortcuts)
      // Array of shortcut objects:
      {
        name: &quot;View your Inbox&quot;, // The name of the shortcut. `string`
        short_name: &quot;inbox&quot;, // optionally, falls back to name. `string`
        description: &quot;View your inbox messages&quot;, // optionally, not used in any implemention yet. `string`
        url: &quot;/inbox&quot;, // The URL this shortcut should lead to. `string`
        icon: &quot;test/inbox_shortcut.png&quot;, // source image(s) for that shortcut. `string`, `buffer` or array of `string`
      },
    ],
    // more shortcuts objects
  },
  callback = function (error, response) {
    if (error) {
      console.log(error.message); // Error description e.g. &quot;An unknown error has occurred&quot;
      return;
    }
    console.log(response.images); // Array of { name: string, contents: &lt;buffer&gt; }
    console.log(response.files); // Array of { name: string, contents: &lt;string&gt; }
    console.log(response.html); // Array of strings (html elements)
  };

favicons(source, configuration, callback);</code></pre>
<p>뭐가 많은데 빌드 파일에서도 아이콘이 나오게 하려면 그냥 맨 위에 있는 <code>path</code>만 설정해주면 된다.
<code>path</code>를 어떻게 설정해줘야 하냐면, <strong>빌드 폴더에서 아이콘 파일이 들어있는 폴더</strong>를 path로 설정해주면 된다.</p>
<p>나는 빌드 폴더 내에서 <code>assets</code> 폴더에 아이콘 파일이 들어있으니까 <code>assets/</code> 이라고 입력해주면 된다.</p>
<pre><code class="language-ts">import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;
import { ViteFaviconsPlugin } from &quot;vite-plugin-favicon&quot;;

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    ViteFaviconsPlugin({
      logo: &quot;public/assets/logo.png&quot;,
      favicons: {
        path: &quot;assets/&quot;,
      },
    }),
  ],
  base: &quot;/lostark_character_search/&quot;,
});</code></pre>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/490255d4-5eab-45ff-8fcc-c77a885e9d88/image.PNG" alt="4">
<img src="https://velog.velcdn.com/images/code-bebop/post/af9ae813-013b-4ce1-9c59-137613c7853e/image.PNG" alt="5"></p>
<p>그럼 이렇게 아이콘 파일 명도 잘 찾고, favicon도 적용된 모습을 볼 수 있다.</p>
<h2 id="manifestjson">manifest.json</h2>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/c0031851-9bdc-4eb1-aeca-1b47010c059e/image.PNG" alt="6">
근데 <code>manifest.json</code> 파일을 못 찾고 있다. 왜냐하면 <code>manifest.json</code>은 assets 폴더 밖에 있기 때문이다.</p>
<blockquote>
<p><code>manifest.json</code>은 이 웹사이트가 웹 앱으로써 실행될 때 필요한 정보들을 담고 있는 파일이다.</p>
</blockquote>
<p>아쉽지만 manifest 관련 파일만 경로를 지정하는 config는 찾을 수가 없었다. 혹시 알고 계신다면 댓글로 알려주시면 감사하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/3c47e182-b626-4012-870a-12a1b4bfd6ca/image.PNG" alt="7"></p>
<p><strong>그냥 얘네를 assets 폴더 내로 옮겨주자.</strong></p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/caa11fbd-9837-42b2-8964-5eea383df8a5/image.PNG" alt="8"></p>
<p>이제 잘 응답받는다.</p>
<h1 id="🌱">🌱</h1>
<p>나는 <code>vite</code>를 사용하면서 거의 모든 면에서 <code>webpack</code>보다 쾌적했다. favicon 적용하는 부분만 빼고.
그렇지만 favicon을 다양한 플랫폼에 대응하게끔 해주는 부분은 인상깊었다. 덕분에 잘 모르던 favicon에 대해서 좋은 공부했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JS 임의의 순서로 배열 재정렬]]></title>
            <link>https://velog.io/@code-bebop/JS-%EC%9E%84%EC%9D%98%EC%9D%98-%EC%88%9C%EC%84%9C%EB%A1%9C-%EB%B0%B0%EC%97%B4-%EC%9E%AC%EC%A0%95%EB%A0%AC</link>
            <guid>https://velog.io/@code-bebop/JS-%EC%9E%84%EC%9D%98%EC%9D%98-%EC%88%9C%EC%84%9C%EB%A1%9C-%EB%B0%B0%EC%97%B4-%EC%9E%AC%EC%A0%95%EB%A0%AC</guid>
            <pubDate>Tue, 03 May 2022 05:04:53 GMT</pubDate>
            <description><![CDATA[<h1 id="계기">계기</h1>
<p>제목대로 임의의 순서로 배열을 재정렬 해야 할 일이 생겼다.
<img src="https://velog.velcdn.com/images/code-bebop/post/5786e619-d48f-464f-aa3d-cea130f5b6dc/image.PNG" alt="1">
위는 구현해야 할 장비의 상세정보의 일부이다.
현재 구현 중인 부분은 해당 장비의 세트 효과를 가진 장비의 장착 현황을 나타내는 부분이다. API에 정보를 요청하면 다음과 같은 형태로 응답이 온다.
<img src="https://velog.velcdn.com/images/code-bebop/post/ccbae112-6820-4c73-bc2c-da39399e78b4/image.PNG" alt="2">
여기서 문제와 마주쳤다.
세트 효과 장비의 순서는 <code>머리, 어깨, 상의, 하의, 장갑, 무기</code> 이다. 그럼 위의 배열을 어떻게 임의의 순서로 정렬한단 말인가?</p>
<h1 id="어떻게">어떻게?</h1>
<ol>
<li><p>결국 결과값은 하나의 배열이어야 하니깐 일단은 두 배열을 합쳐주었다. </p>
<pre><code class="language-js">[
 ...equipment.set.setItemEnableList,
 equipment.set.setItemDisableList,
].flat();</code></pre>
</li>
<li><p>그 다음 정렬을 해야하는데, 나는 1시간 고민한 뒤 다음과 같이 정렬에 성공했다.</p>
<pre><code class="language-js">[
...equipment.set.setItemEnableList,
equipment.set.setItemDisableList,
]
.flat()
.sort((a, b) =&gt; {
 let aOrder = 0;
 let bOrder = 0;

 if (a.includes(&quot;머리&quot;)) aOrder = 0;
 else if (a.includes(&quot;어깨&quot;)) aOrder = 1;
 else if (a.includes(&quot;상의&quot;)) aOrder = 2;
 else if (a.includes(&quot;하의&quot;)) aOrder = 3;
 else if (a.includes(&quot;장갑&quot;)) aOrder = 4;
 else aOrder = 5;

 if (b.includes(&quot;머리&quot;)) bOrder = 0;
 else if (b.includes(&quot;어깨&quot;)) bOrder = 1;
 else if (b.includes(&quot;상의&quot;)) bOrder = 2;
 else if (b.includes(&quot;하의&quot;)) bOrder = 3;
 else if (b.includes(&quot;장갑&quot;)) bOrder = 4;
 else bOrder = 5;

 return aOrder - bOrder;
})</code></pre>
<p>각각의 요소를 if문으로 구별해낸 뒤 임의의 순서를 매겨줘서 정렬에 성공했다.</p>
</li>
</ol>
<p>좀 더 우아한 방법을 상상하고 이것저것 해보았는데, 하면 할 수록 점점 코드가 더러워졌다. sort 메서드를 사용하지 않고 배열의 순서를 바꾸려면 splice 메서드를 사용해야 하는데, 그렇게 하니 코드가 점점 더 알아보기 힘들어졌다. splice 메서드를 사용한 배열 재정렬은 지금과 같은 상황에선 좋지 못했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTML 비어있는 script 태그 체크]]></title>
            <link>https://velog.io/@code-bebop/HTML-%EB%B9%84%EC%96%B4%EC%9E%88%EB%8A%94-script-%ED%83%9C%EA%B7%B8-%EC%B2%B4%ED%81%AC</link>
            <guid>https://velog.io/@code-bebop/HTML-%EB%B9%84%EC%96%B4%EC%9E%88%EB%8A%94-script-%ED%83%9C%EA%B7%B8-%EC%B2%B4%ED%81%AC</guid>
            <pubDate>Wed, 27 Apr 2022 11:29:02 GMT</pubDate>
            <description><![CDATA[<h1 id="계기">계기</h1>
<p>로스트아크 캐릭터 정보 검색 프로젝트를 진행하는 중에, 처음 겪는 문제를 맞딱뜨렸다.
<img src="https://velog.velcdn.com/images/code-bebop/post/f44303a0-dbc7-4d8d-953c-4c3622b241c5/image.PNG" alt="1">
바로 이거다.</p>
<p>내가 만든 API는 로스트아크 전투정보실에서 크롤링을 해서 데이터를 긁어온다.
보통은 저 script 태그 안에 캐릭터의 정보가 JSON 형태로 담겨있고, 그걸 <code>JSON.parse()</code>한 다음 가공해서 정보를 뱉는 형식이다.
그런데 이 script 태그가 비어있는 케이스가 있는데, 해당 캐릭터를 만들기만 하고 아무것도 하지 않은 경우다. 아무것도 장착되어 있지 않고, 스킬도 배우지 않았고, 그냥 아무 정보가 안 담겨 있다.
이런 script 태그를 긁어오면 바로 에러가 난다.
<img src="https://velog.velcdn.com/images/code-bebop/post/474ceeed-96dc-40ec-b407-db4b34b34c7a/image.PNG" alt="2">
JSON 객체를 파싱해야하는데 JSON 객체가 입력되지 않기 때문이다.
그럼 뭘 해주어야 하느냐? <code>JSON.parse()</code>가 실행되기 전에 이 script 태그의 내용물을 체크해주고, JSON 객체가 들어있지 않으면 ealry return 해주면 된다.</p>
<p><strong>여기서 문제다. 저 빈 script 태그 안에 들어있는 게 무엇인가?</strong></p>
<h1 id="빈-script-태그의-내용물">빈 script 태그의 내용물</h1>
<p><strong>결과부터 말하자면 개행문자 <code>\n</code>가 들어있었다.</strong> 이것을 어떻게 확인하는지 지금부터 설명하겠다.
<img src="https://velog.velcdn.com/images/code-bebop/post/56d95330-250a-4b1d-8fe8-85f9849f3e71/image.PNG" alt="4">
우선 이렇게해서 안에 뭐가 들었는지 확인하려고 했다.
<img src="https://velog.velcdn.com/images/code-bebop/post/04898144-88d2-4f39-ac19-ffdccda081ba/image.PNG" alt="5">
?</p>
<p>아무 것도 안 나온다. 그럼 이 값이 빈 문자열이냐?</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/b82dd9fa-36ba-4102-8224-386ea190a932/image.PNG" alt="6">
<img src="https://velog.velcdn.com/images/code-bebop/post/b840bdb3-141b-4521-80f4-380c38ec2be0/image.PNG" alt="7">
그것도 아니랜다.</p>
<p>여기서 생각난 것은 cheerio 라이브러리의 <code>$.html()</code> 메소드의 반환값은 string type이다. 그리고 <code>console.log()</code>로 아무것도 출력되지 않았으니 아마도 공백문자가 담겨있으리라 생각했다. 공백문자를 확인하기 위해 사용한 방법은 <code>String.match()</code> 메소드를 사용하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/d466ca73-ac94-4b33-a4b2-b54711f5d9bf/image.PNG" alt="8">
<img src="https://velog.velcdn.com/images/code-bebop/post/485b5b6c-a656-46a5-b4d2-ca67a03bc402/image.PNG" alt="9">
결과가 나왔다!  그렇다. 개행문자 <code>\n</code>이다.
그럼 이제 <code>$(&quot;#profile-ability &gt; script&quot;).html().match(/\s/).input === &quot;\n&quot;</code> 이 true면 early return 하면 된다.</p>
<h1 id="🎆">🎆</h1>
<p>이렇게 해결했지만, 이것보다 더 편한 방법이 있을 것 같다. 혹시 찾게 되면 이 글에 덧붙이려 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react-router v6 useLocation ts2558 오류]]></title>
            <link>https://velog.io/@code-bebop/react-router-v6-useLocation-ts2558-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@code-bebop/react-router-v6-useLocation-ts2558-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Mon, 25 Apr 2022 11:24:10 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>프로젝트를 진행하면서 v6로 업데이트된 react-router를 처음 사용 중이다.
프로젝트 중 useLocation hook으로 현재 라우트의 정보를 받아오려고 했는데, <code>useLocation().state</code> 의 type이 unknown 이라서 프로퍼티에 접근이 안 되는 것이다.
그럼 useLocation에 state 타입을 선언할 수 있는 제네릭이 있겠지? 싶었지만?</p>
<p><img src="https://velog.velcdn.com/images/code-bebop/post/7082d47e-2161-42f3-9eaa-f58e280f1afa/image.PNG" alt="1">
<img src="https://velog.velcdn.com/images/code-bebop/post/38d23685-88f0-4042-b6b6-c309c9e0be2c/image.PNG" alt="2"></p>
<p>없다! 구글링 해서 본 useLocation 사용법에는 제네릭이 붙어 있었는데... 이거 아주 난감하다.</p>
<h1 id="해결">해결</h1>
<p>그리고 발견한 해결 방법이 <a href="https://github.com/reach/router/issues/414#issuecomment-859406190">이것</a>이다.
요컨대 <code>useLocation().state</code>의 타입이 unknown이니 타입 단언을 통해서 <code>state</code>의 타입을 정하면 된다는 것이다.</p>
<pre><code class="language-js">const location = useLocation();
const state = location.state as { nickname: string };</code></pre>
<p>이런 식으로 말이다.</p>
<h1 id="왜">왜?</h1>
<p><a href="https://github.com/remix-run/history/blob/main/docs/api-reference.md#location">react-router의 Location 타입 정의</a>를 보면 다음과 같다. (여기서 Location은 useLocation() hook의 반환값이다.)
<img src="https://velog.velcdn.com/images/code-bebop/post/e35d766a-07ca-4bb0-9f3e-e1d94a780040/image.PNG" alt="3">
굳이 <code>Location.state</code>의 타입을 unknown으로 선언한 것은, &#39;사용자가 알아서 타입을 좁혀서 사용 해라&#39; 정도의 의도라고 생각한다.</p>
<p>그러나 왜 useLocation의 제네릭이 없어졌는지는 모르겠다. 현재 <code>react-router-dom^6.3.0</code> 을 사용중인데, 적어도 이 버전엔 제네릭이 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DNS는 무엇인가?]]></title>
            <link>https://velog.io/@code-bebop/DNS%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@code-bebop/DNS%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Sat, 02 Apr 2022 08:27:08 GMT</pubDate>
            <description><![CDATA[<h1 id="글을-쓰게-된-계기">글을 쓰게 된 계기</h1>
<p>이틀 전, lostark의 캐릭터 정보 API를 개발하다가 갑자기 오류가 나는 일이 있었다. 바로 어제까지만 해도 잘 되던 요청인데, 갑자기 요청이 안 되는 것이다.
원인은 바로 내 서버 도메인이 만료되었기 때문이다. 작년 21년 3월 31일에 freenom에서 1년짜리 무료 도메인을 구해서 서버 도메인으로 쓰고 있었는데, 까맣게 모른 채 연장 신청을 안 했고 영영 사라졌다.
다시 무료로 구매하면 그만이야<del>~</del> 인 줄 알았는데 freenom은 이전에 한 번이라도 구매된 도메인은 사전적 의미가 깃든 도메인이라고 판단하는지 1년에 12000원을 내고 구입하라고 한다.</p>
<p><img src="https://media.vlpt.us/images/code-bebop/post/ce4f1e09-ad35-4b8a-9e47-793478c4c028/1.PNG" alt="1"></p>
<p>그냥 밑에 무료 도메인 쓰면 되는 일이지만, 서버 도메인 명을 바꾸면 나에게 귀찮은 일이 두 가지 생긴다.</p>
<ol>
<li>codebebop.tk 로 요청하는 코드가 포함된 프로젝트들을 싹 다 바꿔야 한다.</li>
<li>Oracle Cloud DNS Zone을 다시 생성해야한다.</li>
</ol>
<p>1번 같은 경우 프로젝트 파일을 하나하나 수정해서 깃허브에 올리고 배포하면 끝이다.
그러나 문제가 되는 건 2번의 경우다.</p>
<p>원래는 Oracle Cloud DNS를 도메인의 DNS 레코드와 어떻게 어떻게 해서 접속 가능하게 만들어 두었는데, 나는 그걸 원리도 모르고 인터넷 게시글을 보고 따라 한 것이다. 그 마저도 원활하지 못 해 3일정도 만지다가 겨우 됐었다.
그래서 내가 생각한 해결책은 Oracle Cloud DNS zone을 하나 더 만들어서 원래 쓰던 Oracle Cloud DNS zone 설정을 도메인 명만 변경해서 적용하는 것이었다. 하지만 그게 안 된다. 원래 Oracle Cloud DNS Service는 유료 등급의 계정만 사용할 수 있는 기능인데, 나는 무료 등급 계정이라서다.
내가 원래 쓰던 Oracle Cloud DNS zone을 만들 적에는 Oracle Cloud에 가입하고 30일이 지나기 전이라서 유료 서비스를 사용할 수 있었는데, 지금은 안 되는 것이다.
그래서 한 달만 유료 계정으로 업그레이드 하려고 했는데, Oracle Cloud의 계정 업그레이드 방식은 내가 느끼기에 상당히 불친절했다. 나같은 일반인이 주 고객층이 아니기 때문이니 당연한 일이다.</p>
<p>다른 DNS 서비스를 구매해야하나 찾아보려던 찰나...
문득 이런 생각이 든 것이다.</p>
<h1 id="dns가-뭐지">DNS가 뭐지?</h1>
<p>대관절 나는 어째서 DNS가 뭔지도 모르고 이런 발버둥을 치고 있단 말인가? DNS랑 도메인이 무슨 차이지? 우선 DNS가 무엇인지 이해하고 나면 내가 뭘 해야하는지 명확하게 알 수 있고, 나중에 또 도메인을 구매해서 DNS 레코드 등의 설정을 할 때에도 혼자서 해결할 수 있을 것이다.</p>
<p>결과부터 말하자면 <a href="https://gentlysallim.com/dns%EB%9E%80-%EB%AD%90%EA%B3%A0-%EB%84%A4%EC%9E%84%EC%84%9C%EB%B2%84%EB%9E%80-%EB%AD%94%EC%A7%80-%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC/">이 게시글</a>을 읽고 문제를 해결했다.</p>
<h2 id="dnsdomain-name-system">DNS(Domain Name System)</h2>
<p>요약컨대 <strong>DNS란 웹사이트의 IP주소와 도메인명을 연결시켜주는 환경/시스템</strong> 이다.</p>
<p>이 설명에서 Oracle Cloud DNS는 사용할 필요 없는 게 아닌가? 하는 생각이 들었다. 이것이 정말이라면 그냥 내 서버 IP랑 구매한 도메인을 이어놓기만 하면 되는 것이 아닌가.</p>
<p>아래는 내가 글을 읽고 이해한 DNS의 작동 방식을 그림으로 나타낸 것이다.</p>
<p><img src="https://media.vlpt.us/images/code-bebop/post/d09ebb6a-ee85-4160-91c2-51d21a3abd22/2.png" alt="2"></p>
<ol>
<li>웹 브라우저에 WhatEver.com 이라는 도메인을 브라우저의 URL 창에 검색한다.</li>
<li>이 요청은 DNS로 전송된다.
DNS 서버는 WhatEver.com 이라는 도메인의 DNS 레코드를 확인한다.
DNS 레코드 중 A 레코드 타입의 값으로 123.456.7.89 라는 IP가 등록 된 것을 확인한다.
웹 브라우저를 이 IP로 리다이렉트 시킨다.</li>
<li>웹 브라우저는 123.456.7.89로 리다이렉트 된다.</li>
<li>무사히 응답을 받게 된다.</li>
</ol>
<p>허허... DNS 레코드는 또 뭐고 A 레코드 타입은 뭡니까?
이제부터 알아보자.</p>
<h3 id="dns-레코드">DNS 레코드</h3>
<p>역시 간단히 요약하자면 <strong>DNS 레코드란 DNS 서버가 받은 요청을 어떻게 처리해야할지에 대한 정보가 담긴 곳</strong> 이다.
즉, DNS 서버는 어떤 도메인명에 대한 요청을 받을 때 마다 DNS 레코드를 참조하고, 해당 도메인명에 대한 정보를 찾아 요청을 처리한다. 대충 이해하겠는가?</p>
<p>그리고 이 DNS 레코드에는 여러게의 레코드 타입이 존재한다.
비록 영어이긴 하나 <a href="https://constellix.com/news/dns-record-types-cheat-sheet">여기</a>에 DNS 레코드 타입에 대한 설명이 매우 잘 되어 있다.</p>
<p>DNS 레코드 타입은 적어도 20개는 존재하는 것 같다. 정확히 몇 개인지는 모르겠다.</p>
<p>여기 내가 도메인을 구입한 hosting.kr의 DNS 레코드 설정 창이다.
<img src="https://media.vlpt.us/images/code-bebop/post/338afe05-2bb4-40df-a75d-43478edbab87/3.PNG" alt="3">
여기서는 5가지의 레코드 타입에 대한 설정을 지원하고 있다. 사진에도 설명이 간략하게 나와있지만 좀 더 자세히 설명해보겠다.</p>
<h4 id="a-레코드-타입">A 레코드 타입</h4>
<p><strong>IP 주소와 도메인 주소를 매핑 할 때 사용되는 레코드이다. 하나의 도메인 주소에 여러 개의 IP 주소를 매핑할 수도 있다.</strong></p>
<p>정확히는 도메인 주소와 IPv4_(123.456.7.89 같은 형태)<em>를 매핑할 수 있다. IPv6</em>(ACDD:BA98:3654:3210:ADBF:BBFF:2922:FFFF 같은 형태)_를 매핑할 수 있는 AAAA 레코드 타입이 따로 있기 때문이다.</p>
<p>즉 이것이 내가 찾던 것이다. 이 도메인명과 내 서버 IP를 매핑하면 된다는 것 아닌가? 저기 저 IP주소 칸에 내 서버의 IP만 입력하면 되는 것이다.</p>
<h4 id="mx-레코드-타입">MX 레코드 타입</h4>
<p><strong>메일서버 레코드이다. 해당 도메인명과 연결되어 있는 메일서버를 확인할 때에 사용된다.</strong></p>
<p>예를 들어 누군가 &#39;myID@WhatEver.com&#39; 으로 인터넷 메일을 발신했다. <em>인터넷 메일 요청도 DNS 서버로 향하게 되는 모양이다.</em> DNS 서버는 해당 도메인명의 DNS 레코드 중 MX 레코드 타입이 있는지 확인하고, 해당 IP주소 혹은 레코드로 리다이렉트 시킨다.</p>
<h4 id="txt-레코드-타입">TXT 레코드 타입</h4>
<p><strong>이름대로 텍스트를 입력할 수 있는 레코드이며, 주로 메모를 남기기 위해 사용된다.</strong></p>
<p>메모라니? 무슨 메모? 누가 굳이 여기에 메모를 남긴단 말인가?
찾아본 결과 보통 어떤 사이트에서 제공하는 서비스를 이용하기 위해 그 가이드에 맞게 TXT 레코드를 설정한다.</p>
<p>예를 들면 &#39;myID@WhatEver.com&#39; 에서 &#39;myID@gmail.com&#39; 으로 인터넷 메일을 발신했다고 치자. gmail 서버는 WhatEver.com 이 신뢰할만한 도메인인지 아닌지 판단하기 위해 WhatEver.com 의 DNS 레코드 중 TXT 레코드 타입에 <a href="https://support.google.com/a/answer/2716802?hl=ko">Gmail 가이드 라인</a>에 명시된 보안 코드가 서식에 맞게 포함되어 있는지 확인한다. 있다면 받은 메일함에 도착할 것이고, 없다면 스팸 메일함에 도착할 것이다.</p>
<p>물론 이것은 하나의 사용례에 불과하고 TXT 레코드는 무궁무진한 활용 가능성이 있다.</p>
<h4 id="cname-레코드-타입">CNAME 레코드 타입</h4>
<p><strong>하나의 도메인명을 다른 도메인명으로 매핑시키는 레코드 타입이다.</strong></p>
<p>간단히 이야기 하자면 어떤 도메인을 다른 도메인으로 리다이렉션 하는 역할을 하는 것이다.</p>
<p>&#39;WhatEver.com&#39; 도메인이 CNAME 레코드의 값으로 &#39;SomeDomain.com&#39; 을 가지고 있다고 가정하자. 웹 브라우저가 WhatEver.com 를 검색하면  DNS 서버는 WhatEver.com의 DNS 레코드를 확인하다가 CNAME 레코드를 발견할 것이고, SomeDomain.com의 DNS 레코드를 확인한다.
SomeDomain.com의 DNS 레코드에서 A 레코드를 발견하면 그 IP로 리다이렉트 하고, 또 다른 CNAME 레코드가 있다면 다시 해당 CNAME 레코드의 도메인의 DNS 레코드를 확인하고... 같은 흐름이 이어질 것이다.</p>
<h4 id="srv-레코드-타입">SRV 레코드 타입</h4>
<p><strong>이메일, HTTP 통신 등 여러 서비스를 이용하기 위해 필요한 레코드 타입이다.</strong> DNS가 서비스의 위치(포트, 혹은 호스트 이름 등)을 저장하기 위해 필요한 레코드 타입이다.</p>
<p>이라고는 하나 사실 정확히 이해하지 못 했다. 처음듣는 단어가 많고, 그러다보니 설명이 추상적으로 다가온다. 현재 나로써는 이에 대해서 자세히 설명할 수가 없다.</p>
<h1 id="결말">결말</h1>
<p>DNS에 대해 대략 이해하고서 내가 뭘 해야하는 지 알게 되었다. 내가 구입한 도메인의 DNS 레코드에 내 서버 IP를 A 레코드 타입으로 매핑하면 되는 것이다. 그리고 생각대로 잘 작동했다.</p>
<p>이전에 내가 Oracle Cloud DNS로 도메인을 연결한 것은 어떻게 된 것일까?
내가 구입한 도메인의 CNAME 레코드로 만들어 둔 Oracle Cloud DNS Zone을 매핑했고, Oracle Cloud DNS Zone은 A 레코드로 내 서버의 IP를 매핑한 것이다.</p>
<p>나는 Oracle Cloud DNS의 기능을 이용하지도 않았으니, 이제 와서 보면 무지함이 낳은 삽질이었다.</p>
<p>이제 서버도 잘 작동하니 다시 개발을 해야지.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JS로 웹 크롤링]]></title>
            <link>https://velog.io/@code-bebop/JS%EB%A1%9C-%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%A7%81</link>
            <guid>https://velog.io/@code-bebop/JS%EB%A1%9C-%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%A7%81</guid>
            <pubDate>Fri, 11 Mar 2022 13:00:15 GMT</pubDate>
            <description><![CDATA[<h1 id="계기">계기</h1>
<p>이전부터 하고 싶은 프로젝트가 있었다. 게임 LostArk의 캐릭터 정보를 얻어와서 뭔가 하는 프로젝트다.
그런데 LostArk는 제공하는 OpenAPI가 없다. 대신 홈페이지에서 캐릭터의 닉네임을 검색해서 캐릭터 정보를 볼 수 있는데, 이 URL을 크롤링해서 캐릭터 정보를 얻을 수 있겠다 싶은 생각이 들었다.</p>
<p>최근에 cheerio 라는 JS 웹 크롤링 라이브러리를 알게 되었고, 이걸로 웹 크롤링을 해 보자는 마음이 들었다.</p>
<h1 id="cheerio">cheerio</h1>
<p><strong>cheerio는 마크업을 구문 분석해서 결과 데이터 구조를 탐색하고 조작하는 API를 제공하는 라이브러리다.</strong> 설명만 들으면 알기 어렵지만, 사용 예를 보면 금방 이해할 수 있다.
이런 API를 사용해 정제된 데이터를 추출해서 사용하는 것이 내 목적이다.</p>
<h2 id="사용법">사용법</h2>
<p><img src="https://images.velog.io/images/code-bebop/post/d54c169b-27ca-4c28-9bb3-12c7f34a4054/1.PNG" alt="1">
여기 이 웹 사이트를 크롤링 해보자.</p>
<p><code>npm init</code> 으로 프로젝트를 생성한 다음, <code>npm i axios cheerio</code> 로 axios와 cheerio 라이브러리를 설치한다. 그럼 준비는 끝이다.</p>
<p>우선 axios로 해당 웹 사이트의 URL을 요청해서 응답을 받아오자. 이 응답의 data 프로퍼티에는 해당 URL의 HTML 문서의 마크업이 담겨 있다.</p>
<pre><code class="language-js">const getHtml = async () =&gt; {
  try {
    return await axios.get(&#39;https://lostark.game.onstove.com/Profile/Character/%EB%AA%A8%EC%BD%94%EC%BD%94%EB%B3%BC%EB%94%B0%EA%B5%AC%EB%B9%A0%EB%8A%94%EC%86%8C%EB%A6%AC&#39;);
  } catch (error) {
    console.error(error);
  }
};</code></pre>
<p><img src="https://images.velog.io/images/code-bebop/post/bd083a9e-72cd-419d-aa71-0f05dabfd6e9/2.PNG" alt="2"></p>
<p>그 다음 cheerio의 load 메소드의 인자로 응답받은 데이터를 넘겨준다.</p>
<pre><code class="language-js">getHtml()
  .then((html) =&gt; {
    const $ = cheerio.load(html);
  })</code></pre>
<p>이 load 메소드가 뱉어낸 것이 무엇이냐? 이게 바로 <strong>CheerioAPI</strong> 라는 것이다.</p>
<p><a href="https://cheerio.js.org/interfaces/CheerioAPI.html">CheerioAPI 명세</a> 를 보면, CheerioAPI는 함수이고 첫 번째 인자로 Selector를 받는다. 이 Selector는 CSS에서 사용하는 Selector와 동일한 것이다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/5389fe24-715c-4660-8417-a8ad59c4ffb2/3.PNG" alt="3">
우선 해당 캐릭터의 장착 아이템 레벨을 얻어보자. 그러려면 이 장착 아이템 레벨을 표시하는 마크업의 Selector가 필요하다.
어떤 요소의 Selector를 얻으려면 개발자 도구에서 Selector 복사를 하기만 하면 된다.
<img src="https://images.velog.io/images/code-bebop/post/f17e973f-b63b-4423-8da1-4e5545f7dfbf/4.PNG" alt="4">
여기 이 녀석의 Selector를 복사해서 CheerioAPI 함수의 인자로 주고 함수를 호출한다. 그리고 함수의 출력값을 log해보자.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/5cd7b451-b490-4a36-9079-c3a71ce18ac3/5.PNG" alt="5">
그럼 뭔가 DOM 같은 느낌의 데이터가 나온다. <a href="https://cheerio.js.org/classes/Cheerio.html">명세</a>에 의하면 이것이 바로 <strong>Cheerio의 Main Object</strong> 이다. 이 Cheerio의 다양한 메소드를 통해 Cheerio object를 탐색하고 조작하여 원하는 데이터를 얻는 것이다. 우리가 찾은 이 span 안에 캐릭터의 장착 아이템 레벨이 들어있으니, text 메소드를 이용해 마크업 안의 텍스트만 얻어보자.</p>
<pre><code class="language-js">getHtml()
  .then((html) =&gt; {
    const $ = cheerio.load(html.data);
    const data = $(&#39;#lostark-wrapper &gt; div &gt; main &gt; div &gt; div.profile-ingame &gt; div.profile-info &gt; div.level-info2 &gt; div.level-info2__expedition &gt; span:nth-child(2)&#39;);
    const dataText = data.text();
    return dataText;
  })
  .then((res) =&gt; log(res));</code></pre>
<p>Cheerio object의 text 메소드를 호출하기만 하면 된다. 반환값을 log해보면?</p>
<p><img src="https://images.velog.io/images/code-bebop/post/b1c4fd17-86c0-45b2-b4e1-cf189ffca5eb/6.PNG" alt="6">
짠. 장비 아이템 레벨이 나왔다.</p>
<h1 id="🧹">🧹</h1>
<p>명세에 나와있는 Cheerio object의 메소드를 잘 조합하기만 하면 웹 사이트에서 원하는 데이터는 뭐든 얻을 수 있다. 이제 웹 크롤링을 이용해서 캐릭터의 정보가 담긴 데이터를 얻어 제공하는 API들을 직접 만든 뒤, API를 프론트에서 호출해서 뭔가 보여주는 웹을 만들어 볼 예정이다.</p>
<p>이번 프로젝트에선 Vite와 SWR을 써볼 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CORS의 Cookie]]></title>
            <link>https://velog.io/@code-bebop/CORS%EC%9D%98-Cookie</link>
            <guid>https://velog.io/@code-bebop/CORS%EC%9D%98-Cookie</guid>
            <pubDate>Tue, 27 Jul 2021 07:56:04 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>서버와 클라이언트의 통신을 이용한 로그인 기능을 구현하는 것에 쿠키를 사용하기로 했다.
왜냐하면 httpOnly 쿠키 헤더를 활성화하면 XSS 공격은 어느 정도 방어가 가능하고, 쿠키는 클라이언트에서 코드로 조작하는 등의 수고로움이 없기 때문이다.</p>
<p>그런데 CORS의 세계는 너무나 냉혹했다. CORS 정책을 따르지 않는 쿠키는 HTTP 통신을 할 수 없었다.
지금부터 3일 밤낮을 헤매고 4일째 아침에 찾은 답을 포스팅하겠다.</p>
<h1 id="cors-cookie">CORS Cookie</h1>
<h2 id="same-site-속성">Same-Site 속성</h2>
<p>일단 쿠키 헤더의 Same-Site 속성에 대해 알아보자.</p>
<p>Same-Site 속성은 <strong>다른 도메인 간의 통신에 대한 보안에 대한 설정</strong>이고, 3가지 설정값이 있다.</p>
<ol>
<li>Strict
말 그대로 엄격하게 쿠키를 제한한다. <strong>서로 다른 도메인간의 쿠키 교환을 절대 금지</strong>한다.</li>
<li>Lax
Strict보다 느슨하게 쿠키를 제한한다. <strong>서로 다른 도메인간의 쿠키 교환이라 할지라도 top-level navagation 에서 일어나는 안전한 HTTP Method 일 때에는 쿠키를 허용한다.</strong>
top level navigation은 브라우징 컨텍스트에 관한 것인데, 간단히 말하자면 해당 탭의 URL이 바뀌는 요청에 Lax 쿠키가 포함된다.
이를태면 a태그와 link태그를 클릭했을 때 발생하는 GET 요청에 쿠키가 포함되고, 현재 탭의 URL을 바꾸진 않으나 form GET 요청에도 Lax 쿠키가 포함된다.</li>
<li>None</li>
</ol>
<p><strong>서로 다른 도메인간의 쿠키 교환을 제한하지 않는다.</strong> <strong>Same-Site가 None인 쿠키는 반드시 Secure가 true여야 한다.</strong>
Secure 속성이 활성화되면 HTTPS 프로토콜 간의 통신에만 쿠키가 포함될 수 있다. 아마 Same-Site를 None으로 할 거면 최소한의 보안 수단으로 Secure 속성을 활성화하라는 의도인 것 같다.</p>
<h2 id="그래서-발생하는-문제">그래서 발생하는 문제</h2>
<p>내 프로젝트의 HTTP 통신에서 <code>/login</code> 요청은 POST이고, 요청의 응답에는 token이 쿠키로 포함되어야 한다. 그래서 Same-Site는 None이어야 하고, Secure를 활성화 한다.
하지만 개발을 하는 localhost는 기본적으로 HTTP 프로토콜을 사용하기 때문에, localhost의 프로토콜을 HTTPS로 바꿔야했다.</p>
<p><a href="https://web.dev/how-to-use-local-https/">How to use HTTPS for local development</a> 포스트를 참고함으로써 이 문제는 해결했다.</p>
<h2 id="credentials">Credentials</h2>
<p>두 번째는 CORS 통신에 포함 할 쿠키에 관한 설정이다. 쿠키는 요청을 할 때에 자동으로 요청에 포함되지만, CORS 요청에서는 아니다. </p>
<h3 id="클라이언트">클라이언트</h3>
<p>클라이언트의 XHR 객체에는 withCredentials이라는 옵션이 있다. (<a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials">XHR.withCredentials 문서</a>)
이는 쿠키, Authorization header 같은 user Credentials를 요청에 포함할 것인지에 대한 설정이다. true로 설정하면 user Credentials를 요청에 포함할 수 있다.</p>
<p>하지만 이대로만 요청을 보내면 Request error가 발생한다.
<img src="https://images.velog.io/images/code-bebop/post/78f1d965-a278-4a2e-a1ad-8b8813f8b643/1.PNG" alt="1"></p>
<h3 id="서버">서버</h3>
<p>에러 로그를 읽어보면 Credentials request에 대한 Response의 헤더에 <code>Access-Control-Allow-Credentials</code>가 <code>true</code>여야 한다고 한다.</p>
<p>나는 express를 사용하고 있으니 express 기준으로 설명하겠다.
<a href="http://expressjs.com/en/resources/middleware/cors.html#configuration-options">express cors middleware 문서의 config option 문단</a>을 보면 해당 옵션에 대한 설명이 있다.</p>
<pre><code class="language-js">const express = require(&quot;express&quot;);
const server = express();
const cors = require(&quot;cors&quot;);

...

server
  .use(cors({ origin: true, credentials: true }))
  .use(express.json())
  .use(cookieParser());

...</code></pre>
<p>이렇게 설정해주면 해당 서버의 모든 응답에 <code>Access-Control-Allow-Credentials</code> 헤더가 붙는다.
cors의 config 객체를 보면 <code>origin: true</code>도 설정해줬는데, 이유는 다음과 같다.</p>
<p><code>Access-Control-Allow-Credentials</code>가 <code>true</code>인 응답은 반드시 <code>Access-Control-Allow-Origin</code>이 명시되어 있어야 한다. 와일드카드( * )는 사용할 수 없고, origin을 명시해야한다.
cors cofing의 <code>origin</code>을 <code>true</code>로 설정하면 요청을 보내온 origin이 <code>Access-Control-Allow-Origin</code>의 값으로 설정된다.</p>
<h2 id="마침내">마침내</h2>
<p><img src="https://images.velog.io/images/code-bebop/post/8c4b3cdc-040c-43fb-9c91-98fa8f043f18/2.PNG" alt="2"></p>
<p><img src="https://images.velog.io/images/code-bebop/post/5527be61-27de-42f0-9cf6-435704e8876a/3.PNG" alt="3"></p>
<p>이 모든 문제가 해결되었다면, 이렇게 쿠키가 포함된 응답을 받을 수가 있다.
여기까지 내가 이 문제를 접한지 1일차에 해결한 부분이다.</p>
<h2 id="그런데">그런데</h2>
<p>그런데 나는 Chrome 개발자 도구의 Application 탭의 Cookies storage에는 저장된 쿠키가 안 보이는데, 왜 이러는지 모르겠다.</p>
<p>거기에 나는 쿠키가 필요한 API에 요청을 보냈는데, 자꾸 에러가 떴다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/0c0668ee-6b53-4417-8923-d81b31c73cf5/4.PNG" alt="4"></p>
<p><img src="https://images.velog.io/images/code-bebop/post/e6528289-de5e-4a51-8a35-01e9cbe8c634/5.PNG" alt="5"></p>
<p>서버에 요청은 제대로 갔는데, refreshToken 쿠키가 없다고 에러를 던지고 있다. 이것 때문에 3일 밤낮을 헤맸다.</p>
<p>그리고 해결방법은 어처구니 없게 간단했고, 나는 어처구니 없게 멍청했다.</p>
<h2 id="진짜-해결">진짜 해결</h2>
<pre><code class="language-js">...

  const requestRefresh = async () =&gt; {
    const response = await axios.get(
      &quot;쿠키가.필요한/API/EndPoint&quot;
    );

    console.log(response);
  };

...</code></pre>
<p>이게 쿠키가 필요한 API로 보내는 요청 코드였는데, 여기에도 <code>withCredentials</code> 옵션이 필요했다.</p>
<p><strong>그러니까, 쿠키를 받을 요청 뿐만 아니라 쿠키를 보낼 요청 또한 withCredentials 옵션을 true로 설정해야한다!</strong></p>
<p>아니, 지금 와서 생각해보면 분명히 이치에 맞는 말이다.
하지만 그 당시에는 Application의 Cookie Storage에 쿠키가 나타나지 않아서 쿠키를 제대로 못 받아와서 생긴 일이라고 생각했다. 그래서 해결이 더뎠다.</p>
<p>그러니까 위의 요청에 <code>withCredentials</code> 옵션을 <code>true</code>로 설정하면..</p>
<pre><code class="language-js">...

  const requestRefresh = async () =&gt; {
    const response = await axios.get(
      &quot;쿠키가.필요한/API/EndPoint&quot;,
      {
          withCredentials: true
      }
    );

    console.log(response);
  };

...</code></pre>
<p><img src="https://images.velog.io/images/code-bebop/post/52809289-4263-4dae-b17a-2a7522eca7b8/6.PNG" alt="6"></p>
<p><img src="https://images.velog.io/images/code-bebop/post/24793568-3a2c-4336-85da-f556b7b3f0b6/7.PNG" alt="7">
뭐야... 잘 되잖아...</p>
<p><img src="https://images.velog.io/images/code-bebop/post/739635c7-b188-4cc3-99fb-11852b964a09/1.jpg" alt="8"></p>
<h2 id="요약">요약</h2>
<p>클라이언트</p>
<p>쿠키가 필요한 모든 요청에 withCertification 옵션 활성화( 쿠키를 받는 요청에도, 주는 요청에도 모두 )
https 프로토콜 사용해야 함 ( Secure 쿠키는 HTTPS 간의 통신에만 전송 돼서 )</p>
<hr>
<p>서버</p>
<p>응답 Access-Control-Allow-Credentials 헤더 활성화
응답 Access-Control-Allow-Origin 헤더에 도메인 명시( 와일드 카드 사용 불가 )
쿠키 헤더에 Same-Site: &quot;None&quot; 설정( 다른 Origin간의 통신에도 전송되게 ) , Secure 설정( Same-Site가 None이면 Secure 활성화 되어야함 )</p>
<h1 id="🐬">🐬</h1>
<p>그래도 시간 들이고 해결 못 하는 것 보단 해결을 한 것이 낫다. 어쨌든 내가 이겼다.
그리고 오류를 해결하면서 HTTP 통신과 쿠키, 뭐 이런 것들에 대해서도 여러가지 배웠고, CORS 이슈에 대해 서버와 클라이언트 둘 다 관심을 기울여야 한다는 것을 느꼈다. 실제로도 그렇고.</p>
<p>나는 그냥 로그인 기능이 있는 CRUD 리액트 웹앱을 만들고 싶었는데, 생각 이상으로, 너무나도 생각 이상으로 알아야 할 것이 많다. 배우는 것이 많아서 좋기는 하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT 사용하기]]></title>
            <link>https://velog.io/@code-bebop/JWT-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@code-bebop/JWT-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 22 Jul 2021 12:27:30 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>이전 포스트에선 JWT를 왜 쓰는지, 뭐하는 친구인지, 어떻게 쓸지에 대해 이야기했다.
이번 포스트에선 실제로 express 서버에 적용해보는 시간을 가져보려고 한다.</p>
<h1 id="구현">구현</h1>
<h2 id="로그인">로그인</h2>
<p>클라이언트가 Token을 발급받기 위해서는 인증을 해야한다. 여기서는 인증 수단으로 ID와 Password를 입력받는 로그인을 사용하겠다.</p>
<pre><code class="language-js">router.post(&quot;/login&quot;, [
        check(&quot;userId&quot;).notEmpty().isLength({ min: 5, max: 12 }).isAlpha().isLowercase(),
        check(&quot;password&quot;).notEmpty().isLength({ min: 8, max: 16 })
    ], async (req, res) =&gt; {
    try {
        console.group(&quot;detected GET request to /auth/login&quot;);

              // 유저 인증 로직, 인증에 실패하면 throw됨
              ...

        const accessToken = user.generateAccessToken();
        const refreshToken = user.generateRefreshToken();

        user.refreshToken = refreshToken;
        await user.save();

        if (req.body.automaticLogin) {
            const refreshToken = user.generateRefreshToken();

            user.refreshToken = refreshToken;
            await user.save();

            res.cookie(&quot;refresh_token&quot;, refreshToken, {
                maxAge: 1000 * 60 * 60 * 24 * 14,
                httpOnly: true
            });    
        }
        res.send({
            accessToken
        });
        console.groupEnd();
    } catch (e) {
        console.log(`Error: ${e.message}`);
        console.groupEnd();
        res.status(500).send(e.message);
    }
});</code></pre>
<p><code>/login</code> Endpoint로 POST요청이 오면 유저 인증 로직(reqeust.body와 DB의 ID/Password를 비교하는 로직이다)을 거친 뒤 accessToken을 req.body에 포함하고, refreshToken을 선택적으로 cookie에 포함하여 응답한다.
기본적으로 accessToken을 res.body에 포함하고, req.body.automaticLogin이 true라면 refreshToken을 res.cookie에 포함한다.</p>
<p>POST 인 이유는 refreshToken이 DB에 추가 될 수 있기 때문이다. 이것은 이 요청이 Resource를 생성할 수 있으며 멱등성이 보장되어있지 않다는 의미를 내포하기 때문에 POST 요청이 적절하다고 판단했다.</p>
<pre><code class="language-js">...

userSchema.methods.generateAccessToken = function() {
    const token = jwt.sign(
        {
            userId: this.userId
        },
        process.env.JWT_ACCESS_SECRET,
        {
            expiresIn: &quot;1h&quot;
        }
    );
    return token;
}

userSchema.methods.generateRefreshToken = function() {
    const token = jwt.sign(
        {
            userId: this.userId
        },
        process.env.JWT_REFRESH_SECRET,
        {
            expiresIn: &quot;14d&quot;
        }
    );
    return token;
}

...</code></pre>
<p>나는 accessToken과 refreshToken의 Secret key를 다르게 설정했는데, 그 이유는 다음과 같다.
내가 해커라면 accessToken의 Secret key를 손에 넣고 나서 같은 Secret key로 refreshToken도 뚫리는지 확인해볼 것이기 때문이다.</p>
<p>그리고 accessToken의 만료 시간을 1시간으로 지정했는데, 내 생각에 이런 Flow도 괜찮을 것 같다.</p>
<ol>
<li>5분이나 10분단위로 accessToken의 만료 시간을 정하고,</li>
<li>서버에 유효한 accessToken이 담긴 요청을 받고 새로운 accessToken을 응답하는 로직을 만들고,</li>
<li>프론트엔드 코드에서 accessToken의 만료 시간이 1분 이하로 남으면(setTimeout 같은 메소드로 시간을 재는 게 좋겠다.) 2번의 API Endpoint로 요청을 보내고 accessToken을 갱신</li>
</ol>
<p>하는 것이다. 그럼 accessToken의 보안도 향상시키고, 유저는 웹사이트를 종료하지 않는 이상 계속 로그인 되어 있는 상태를 유지할 수 있다.</p>
<hr>
<p><em>... 코드를 짤 당시에는 그렇게 생각했었다. 근데 다시 생각해보니 accessToken의 Secret key가 노출되면 어짜피 Secret key를 바꿔야하는데, 그럼 refreshToken과 accessToken이 동일한 Secret Key를 써도 되는 게 아닐까?
게다가 동일한 Secret key를 쓰면 두 Token의 Secret key가 새로운 것으로 교체되니 오히려 보안에 좋다.
처음엔 accessToken의 Secret key를 알게 되면 유효기간을 늘려 사용할 수도 있겠다 생각했는데, 이 문제는 코드로 접근할 수 없는 곳에 Token을 둠으로써 예방할 수 있다.</em></p>
<hr>
<p>refreshToken은 해당 User DB의 refreshToken 프로퍼티에 저장한다. </p>
<ol>
<li>나중에 refreshToken을 검증할 때에 현재 DB에 해당 Token을 가지고 있는 User가 존재하는지 검증하기 위함이고,</li>
<li>해당 refreshToken이 탈취되어 악용되어지고 있다 판단되면 DB에서 삭제함으로써 실시간으로 통제를 하기 위함이다.</li>
<li>또 해당 유저가 Logout 요청을 보내면 DB에서 refreshToken을 삭제해 해당 유저가 가진 refreshToken을 무효화 할 수도 있겠다.</li>
</ol>
<p>refreshToken은 cookie로, accessToken은 res.body로 전송한다.
왜냐하면 accessToken은 클라이언트의 state에 저장해놓았다가 Authorization header로 요청받을 것이기 때문이다. 그렇게 하면 사용자가 브라우저를 종료했을 때에 accessToken은 삭제되니 불특정다수가 사용하는 컴퓨터에서의 해킹 위험이 적어진다.</p>
<hr>
<p><em>공공장소에서의 JWT에 대해 생각하다가 든 생각이 있다.
클라이언트는 accessToken이 유효하나 만료되었을 때에만 Cookie에 저장된 refreshToken과 함께 /refresh API로 요청을 보내고, 새로운 accessToken을 응답받는다.
그런데 이런 시나리오가 떠올랐다. 공공장소에서 웹사이트에 로그인하여 Cookie가 사용자의 컴퓨터에 저장되고, 나중에 누군가가 refreshToken cookie가 유효할 때에 /refresh API로 접근하면 보안이 뚫리는 것 아닌가?
그럼 자동 로그인 같은 옵션을 만들어서 옵션이 활성화 되었을 때에만 refreshToken을 발급하는 것은 어떤가? 현재 내가 떠올릴 수 있는 방법은 이게 최선이라고 판단했다.
구글링을 하며 본 내용 중 refreshToken과 함께 userID, userSecret을 요청하는 flow도 봤었는데, 이게 정확히 어떻게 이루어지는 건지를 몰라 포스팅을 하고 나서 알아볼 예정이다.</em></p>
<hr>
<h2 id="accesstoken-검증">AccessToken 검증</h2>
<p>이제 인가가 필요한 API에서 accessToken을 체크하는 기능을 만들어보자.
가장 적합한 방식은 미들웨어를 사용해서 체크하는 방식이라고 생각한다.</p>
<pre><code class="language-js">...

router.get(&quot;/check&quot;, checkAccessTokenMiddleware, async (req, res) =&gt; {
    console.group(&quot;detected GET request to /auth/check&quot;);
    try {
        const { userId } = res.user;
        if (!userId) {
            console.log(&quot;There is not userId&quot;);
            throw new Error(&quot;There is not userId&quot;);
        }
        res.send(&quot;check allow&quot;);
        console.groupEnd();
    } catch (e) {
        console.log(e);
        console.groupEnd();
        res.send(&quot;check disallow&quot;);
    }
});

...</code></pre>
<p>이렇게 accessToken을 체크하는 미들웨어를 인가가 필요한 API마다 꽂아준다.
해당 미들웨어는 이렇게 생겼다.</p>
<pre><code class="language-js">...

const checkAccessTokenMiddleware = async (req, res, next) =&gt; {
    try {
        console.group(&quot;request passes through jwtMiddleware&quot;);
        const accessToken = req.headers.authorization.split(&quot; &quot;)[1];
        if (!accessToken) {
            res.sendStatus(400);
        }
        jwt.verify(accessToken, process.env.JWT_ACCESS_SECRET, (err, user) =&gt; {
            res.user = { userId: user.userId };

            console.groupEnd();
            return next();
        })
    } catch (e) {
        console.log(`Error: ${e.message}`);
        console.groupEnd();
        if (e.name === &quot;TokenExpiredError&quot;) {
            res.status(403).send(e.message);
        }
        res.sendStatus(403);
    }
}

...</code></pre>
<p>Authorization header에서 accessToken을 찾아 검증한다. Token이 없거나 검증에 실패하면 err를 던지거나 적절한 status를 응답한다.</p>
<h2 id="refreshtoken으로-accesstoken-재발급">RefreshToken으로 AccessToken 재발급</h2>
<pre><code class="language-js">...

router.get(&quot;/refresh&quot;, async (req, res) =&gt; {
    console.group(&quot;detected GET request to /auth/refresh&quot;);
    try {
        const refreshToken = req.cookies.refresh_token;    
        if (!refreshToken) {
            throw new Error(&quot;There is not refreshToken&quot;);
        }

        jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET, (e, user) =&gt; {
            if (e) {
                if (e.name === &quot;TokenExpiredError&quot;) {
                    throw new Error(&quot;It&#39;s Expired refreshToken&quot;);
                }
                throw new Error(&quot;It&#39;s invalid refreshToken&quot;);
            }
        });

        const user = await User.findOne({ refreshToken }).exec();
        if (!user) {
            throw new Error(&quot;It&#39;s invalid refreshToken&quot;);
        }

        const accessToken = user.generateAccessToken();
        console.log(user);

        console.groupEnd();
        res.send({ accessToken });
    } catch (e) {
        console.log(e);
        console.groupEnd();
        res.sendStatus(400);
    }
});

...</code></pre>
<p><code>/refresh</code> Endpoint로 GET 요청이 오면 cookie에 있는 refreshToken을 검증하고 AccessToken을 응답한다.</p>
<ol>
<li>Token이 있는지 체크</li>
<li>Token이 만료되었는지 체크</li>
<li>Token이 유효한지 체크</li>
<li>DB에 해당 Token을 가지고 있는 유저가 있는지 체크 (중요함)</li>
</ol>
<p>DB에 해당 Token을 가지고 있는 유저가 있는지 체크하는 게 특히 중요하다. 로그아웃 요청이 들어오면 DB에서 해당 유저의  refreshToken을 지우기 때문이다.</p>
<p>모든 체크를 통과하면 새로운 accessToken을 응답한다.</p>
<hr>
<p>클라이언트에서</p>
<ol>
<li>accessToken이 없거나</li>
<li>API를 요청했는데 403 status를 받게 되면</li>
</ol>
<p>이 <code>/refresh</code> Endpoint로 API를 요청한다.</p>
<p>&quot;서버 로직에서 accessToken의 유무를 체크하는데 왜 굳이 클라이언트에서도 accessToken의 유무를 체크하느냐?&quot; 는 의문이 생길 수 있을텐데, 그에 대한 나의 대답은 이렇다.</p>
<p>클라이언트에서 요청을 보내기 전에 Token의 유무를 체크하고 <code>/refresh</code>로 요청을 보내는 것이 요청을 보내고 403 status를 받은 뒤 <code>/refresh</code>로 다시 요청을 보내는 것 보다 비용이 더 적게 든다고 판단했기 때문이다.</p>
<p>서버 로직에 accessToken의 유무를 다시 체크하는 이유는 2중 보안의 의미이다.</p>
<h2 id="로그아웃">로그아웃</h2>
<pre><code class="language-js">...

router.post(&quot;/logout&quot;, async (req, res) =&gt; {
    try {
        const refreshToken = req.cookies.refresh_token;    
        if (!refreshToken) {
            throw new Error(&quot;There is not refreshToken&quot;);
        }
        jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET, (e, user) =&gt; {
            if (e) {
                if (e.name === &quot;TokenExpiredError&quot;) {
                    throw new Error(&quot;It&#39;s Expired refreshToken&quot;);
                }
                throw new Error(&quot;It&#39;s invalid refreshToken&quot;);
            }
        });

        const user = await User.findOne({ refreshToken }).exec();
              if (!user) {
            throw new Error(&quot;There is not user&quot;);
        }
        user.refreshToken = null;
        await user.save();

        res.sendStatus(200);
    } catch (e) {
        res.sendStatus(400);
    }
});

...</code></pre>
<p><code>/logout</code> Endpoint로 POST 요청이 들어오면 cookie에서 refreshToken을 얻어 해당 refreshToken을 들고 있는 User를 DB에서 찾아서 해당 User의 refreshToken을 삭제한다.</p>
<p>이러면 이후에 사용자가 클라이언트에 접속했을 때 로그인이 해제된 상태의 뷰를 볼 것이다. 로그아웃을 했는데도 로그인이 풀리지 않는 것은 이치에 어긋나지 않는가?</p>
<h1 id="🐾">🐾</h1>
<p>이상의 코드가 내가 생각한 AccessToken / RefreshToken을 이용한 로그인 API의 Flow다.</p>
<p>조금(많이) 코드가 더럽기도하고 어설픈 구석도 있으나 PostMan으로 API를 테스트했을 때는 의도대로 잘 작동한다. 아마 프론트엔드 코드를 작성하면서 무언가 헛점을 발견할 것이라 생각한다.</p>
<p>쉽게 이야기하자면 이 Flow(인증 프로세스? 뭐라고 불러야 할까)에서는 클라이언트의 AccessToken의 유무가 로그인 상태를 결정짓는 것이고, RefreshToken의 유무가 브라우저를 종료해도 로그인 상태를 유지할 것인지 말 것인지를 결정짓는 것이다.</p>
<hr>
<p>JWT를 사용한 유저 인증/인가 로직도 Flow도 공식 사이트의 예제나 로직이 없어서 혼란스러웠다. Access Token이나 Refresh Token의 개념만 추상적으로 나와 있었다.
그래서 며칠동안 구글링을 했는데, 그냥 개념만 따르면 되는 것으로 판단해 내 생각대로 로직을 짰다.</p>
<p>그리고 구글링하면서 지속적으로 나온 키워드가 OAuth다.
나는 OAuth가 그냥 &quot;Google이나 Facebook같은 곳의 계정으로 로그인을 할 수 있게 해주는 기술&quot;정도로 알고 있었다. 그것도 맞긴 한데, OAuth에서 사용하는 인증 방식이 JWT를 사용한 인증 방식인 것으로 판단된다.
지금은 그 정도만 파악했고 더 자세한 내용은 배워야한다. 배우고나면 포스팅 할 생각이다.</p>
<hr>
<p>어설프게나마 코드를 작성하고 있으니 백엔드 프로그래밍도 재밌게 느껴진다. JWT를 이해하느라 시간이 좀 많이 걸리긴 했지만...</p>
<p>아무튼 그렇다. 다음 포스팅에서는 이 API에 기반해 프론트엔드의 로그인 로직을 작성할 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT 이해하기]]></title>
            <link>https://velog.io/@code-bebop/JWT-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@code-bebop/JWT-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 16 Jul 2021 12:21:31 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>이전 포스트에서는 회원가입과 로그인 API를 만들었다.</p>
<p>이제 유저가 로그인이 필요한 API를 요청할 때에, 해당 유저가 로그인이 되었는지 안 되었는지를 확인하는 기능이 필요하다.</p>
<h2 id="인증과-인가">인증과 인가</h2>
<p>우선 다루고 있는 것이 <strong>인증</strong> 과 <strong>인가</strong> 에 대한 부분이니만큼 둘의 차이에 대해 인지하고 있어야한다.</p>
<p>회원가입 / 로그인은 <strong>인증</strong>에 관한 기능을 한다. 유저는 회원가입 / 로그인을 통해 자신이 이 서버의 회원임을 인증할 수 있다.
하지만 유저가 권한이 필요한 기능에 접근하려고 할 때마다 로그인을 통해 인증을 해야한다면 UX는 저하된다. 생각해보라, 보통 로그인을 한 번 하고 나면 권한이 필요한 기능에 접근할 때에 다시 로그인을 하지 않아도 접근이 가능했지 않은가? 반면 매번 로그인을 해야한다면 상당히 불편할 것이다.</p>
<p>이것이 어떻게 가능할까? 그것은 클라이언트든 서버든 기능에 접근하려는 유저가 권한을 이미 가지고 있음을 알고 있기 때문이다. 즉, 이 유저가 <strong>인가</strong> 되었음을 알고 있기 때문이다.</p>
<p>그리고 JWT는 <strong>인가</strong>에 관한 기능을 한다.</p>
<h2 id="왜-jwt인가">왜 JWT인가?</h2>
<p>HTTP 통신은 <strong>Connectionless이며 Stateless</strong>다. 무슨 말인고 하면 서버는 어떤 요청에 대해 응답하고 나면 그걸로 연결이 종료되며, 요청을 보내온 상대를 식별할 수 없다.</p>
<p>식별을 하기 위해서는 클라이언트가 인증을 하면 서버에서 해당 클라이언트를 식별할 수 있는 일종의 ID를 주고, 클라이언트에서는 인가가 필요한 API에 접근할 때에 해당 ID를 서버에 전송함으로써 인가된 클라이언트임을 서버에 알리는 것이 현재의 보편화된 인가 방식이다.</p>
<h2 id="과거에는-세션이-대세였다">과거에는 세션이 대세였다</h2>
<p>과거에는 인가를 위해서 세션을 보편적으로 사용했었다.
잠시 세션은 어떤 원리로 인가를 하는지 가볍게 살펴보자.
<img src="https://images.velog.io/images/code-bebop/post/bd3828aa-50b8-4140-a34e-a4b2f407f71e/2.png" alt="2"></p>
<ol>
<li>클라이언트가 인증에 성공하여 Session ID를 요청한다.</li>
<li>서버는 Session ID를 Cookie로 발급하고 <strong>해당 Session ID를 서버 메모리에 저장한다.</strong></li>
<li>클라이언트가 인가가 필요한 API로 요청을 보낸다. Session ID와 함께.</li>
<li>서버는 Session ID를 검증하고 API를 응답한다.</li>
</ol>
<p>이후에 인가를 취소하는 요청(예를 들면 로그아웃)이 들어오면 해당 Session ID를 지운다. Session ID가 저장된 메모리에는 현재 인가된 상태의 유저들의 Session ID가 저장되어 있는 것이다. 그래서 실시간으로 인가된 유저들을 관리하기에 용이하다.</p>
<p>허나 Session ID를 서버 메모리에 저장한다는 것이 치명적인 단점이다.</p>
<ol>
<li>첫째로 접속 유저가 많아지면 서버에 점점 부담이 갈 것이고, 서버가 수용할 수 있는 메모리를 넘어서면 서버가 다운되거나 처리 속도가 느려져 제대로 서비스를 제공하지 못 하게 된다.</li>
<li>둘째는 로드밸런서에 관한 문제다.
로드밸런서는 서버에 가해지는 부하를 분산하기 위해 요청을 각각 다른 서버로 분산시키는데, 요청이 Session ID를 발급하고 저장한 서버가 아니라 다른 서버로 분산되면 오류가 생긴다.
이를 해결하기 위해 Session ID를 발급할 때 Cookie에 해당 유저가 요청할 때에 어느 서버로 로드밸런싱 될지 명시하는 방법을 사용하는데, 이 또한 문제가 될 수 있는 부분은 한 서버에 유저가 몰려서 서버에 부하가 일어날 수 있다. 로드밸런서의 의미가 퇴색되는 부분이다.</li>
<li>셋째는 쿠키와 웹뷰에 관한 문제다. 갑자기 왠 웹뷰 이야기가 나오는 것인가?
세션은 쿠키를 통한 통신이 강제된다. 그런데 웹뷰에서는 쿠키를 사용할 때에 저장되었던 쿠키가 삭제되는 둥의 이슈가 간헐적으로 발생한다. 물론 이 이슈는 웹뷰와 쿠키를 동기화하는 방법으로 해결이 가능하긴 하다.</li>
</ol>
<p>이런 단점들을 해결하기 위해 나온 것이, 바로 JWT다.</p>
<h2 id="jwt는-어떻게-작동하는가">JWT는 어떻게 작동하는가?</h2>
<p>큰 흐름은 세션과 같다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/c5ba81fe-6470-4dbd-a40a-15c29cca5c5d/1.png" alt="1"></p>
<ol>
<li>클라이언트가 인증에 성공하여 Access Token을 요청한다.</li>
<li>서버는 Access Token을 발급한다. JWT는 굳이 Cookie가 아니더라도 어디에나 저장 가능하다. <a href="https://jwt.io/introduction/">JWT 공식문서</a>에는 Bearer schema의 Authorization header</li>
<li>클라이언트가 인가가 필요한 API를 요청한다. Access Token과 함께.</li>
<li>서버는 해당 Access Token을 검증 후 적절한 응답을 한다.</li>
</ol>
<p>세션 방식과 다른 점은 서버에 뭔가 저장하지 않는다는 것이다. 이것이 어떻게 가능한지 알아보자.</p>
<h3 id="jwt의-구성요소">JWT의 구성요소</h3>
<p>JWT는 Header, Paylaod, Signature로 구성되어있고, 각 요소는 . 으로 구분된다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/3c30289e-6405-4b96-92d1-265de3341bff/3.PNG" alt="3">
실제 JWT의 모습이다.</p>
<p>Signature를 제외한 각 요소는 JSON 객체이다. 이렇게 문자열의 형태를 띄고 있는 것은 base64 방식으로 인코딩되었기 때문이다.
이 문자열의 인코딩 되기 이전의 모습은 이렇다.
<img src="https://images.velog.io/images/code-bebop/post/e4ce208d-7ec1-4073-aa5e-3f63ec5cd6c9/4.PNG" alt="4">
이제 각 구성요소가 갖는 의미에 대해 알아보자.</p>
<h4 id="header">Header</h4>
<p>JWT의 Header에는 해싱 알고리즘 방식을 명시하는 alg와 토큰 타입을 명시하는 typ가 들어있어야 한다.</p>
<h4 id="payload">Payload</h4>
<p>Payload는 data를 담는 곳이다. 그리고 이 부분이 JWT가 서버에 정보를 따로 저장하지 않고 사용자를 식별하고 HTTP 통신 환경을 개인화 할 수 있는 핵심적인 부분이다.</p>
<p>Payload에 담기는 Key-Value 한 쌍을 Claim이라고 부른다.
그리고 Claim에는 세 종류가 있다.</p>
<ul>
<li>Registered claim
토큰에 대한 정보를 담기위한 claim으로, 이미 이름이 정해져 있다.
Registered claim은 <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1">RFC 7519 4.1</a>에 명세되어 있다. 총 7개이다.</li>
<li>Public claim
JWT를 사용하는 유저들에 의해 임의로 지정될 수 있으나, 이름의 충돌을 방지하기 위해 <a href="https://www.iana.org/assignments/jwt/jwt.xhtml">IANA JWT 레지스트리</a>에 이름을 정의하거나, UUID, OID, URL 형식의 이름으로 지정해야한다.
라고 소개되어 있는데, 왜 그렇게 해야하는지, Private와 정확히 무엇이 다른 것인지 어디에 쓰이는 것인지를 모르겠다.</li>
<li>Private claim
Registered claim도 아니고, Public claim도 아닌 claim을 Private claim이라고 한다.
Private claim은 서버와 클라이언트가 Key-Value를 정해놓고 자유롭게 사용 가능하다.</li>
</ul>
<h4 id="signature">Signature</h4>
<p>Signature는 Header와 Payload를 각각 base64 방식으로 인코딩 하고 하나의 문자열로 합친 다음 임의의 Sceret key를 사용해 HMAC SHA-256 방식으로 암호화한 값이다.
그래서 Payload의 내용이 한 끗만 바뀌어도 Signature값은 송두리째 바뀐다.</p>
<p>JWT는 Token을 검증할 때에 Header와 Payload를 해당 Token을 발급할 때 사용했던 Secret key를 이용해 Signature 값을 만들고, 이 값과 해당 Token의 Signature 값이 일치하지 않는다면 검증은 실패한다.(<em>나는 처음에 이 과정을 잘 이해 못 했었는데, 어떻게 잘 설명이 되었길 바란다.</em>)</p>
<p>그러니 악의적인 유저가 Token의 Payload를 decode해서 임의로 값을 바꾼 다음 encode하는 둥의 조작을 가해도 Secret key를 모른다면 Signature 값을 일치시킬 수 없어 검증을 통과할 수 없다.</p>
<h3 id="jwt는-token-자체가-데이터다">JWT는 Token 자체가 데이터다</h3>
<p>이렇듯 <strong>JWT는 Token 자체에 데이터가 모두 담겨있기 때문에, 서버에 따로 정보를 저장해두지 않아도 요청을 보낸 사용자에 대해 어느정도 식별이 가능</strong>하다. 이것이 세션 방식과의 차이점이다.</p>
<h2 id="jwt-token을-어디에-저장해야-할까">JWT Token을 어디에 저장해야 할까?</h2>
<p>클라이언트에서 서버로 Token을 전달 할 때에 사용할 수 있는 경로가 3개 정도 있다.</p>
<ol>
<li>request.body로 Token 전달 (권장되는 방법은 아니다. 왜냐하면 request.body를 사용할 수 없는 HTTP METHOD가 몇 있기 때문이다.)</li>
<li>요청의 query parameter (그 길다란 문자열이 parameter에 그대로 노출되는 건 별로 보기 좋은 모습은 아니라고 생각한다.)</li>
<li>Cookie로 Token 전달 (생각보다 많이 쓰이는 방법이다.)</li>
<li>HTTP Authorization Header로 전달 (RFC 7519 명세에 따르면 이 방법이 권장되고 있다. 그러니까 이 방법이 표준이라는 이야기이다.)</li>
</ol>
<p>보통 Cookie 혹은 Authorization header에 Token을 담아 요청을 보낸다.(<em>항상 그렇지만 내가 모르는 방법이 있을 수도 있다. Authorization header의 존재도 이 포스트를 쓰다가 알았다.</em>)</p>
<p>JWT를 사용할 때에 Access token과 Refresh token을 발급하고 Access Token의 만료기간을 1시간 혹은 그보다 더 짧게 설정하고 Refresh token의 만료기간은 1주 혹은 2주 정도로 설정한다.
그리고 서버에서는 Access token이 만료되었으나 Refresh token이 유효하다면 Access token을 다시 발급해주는 로직을 짜놓는다.
이렇게 하면 Access token이 탈취당해도 만료기간이 짧으므로 해킹으로 인한 피해를 줄일 수 있다는 이야기이다. 물론 Refresh token이 탈취당하면 그게 그거 아닌가 싶긴 하지만..</p>
<p>아무튼 갑자기 Access token과 Refresh token을 설명하는 이유는 두 Token의 저장 위치를 달리하는 방법을 설명하고 싶어서다. <strong>Access token은 Authorization header에, Refresh token은 Cookie에 저장하는 방법</strong>이다. 자세한 시나리오를 보자.</p>
<p>서버에서 인증된 유저에게 응답할 때에 Access token은 request.body로, Refresh token은 Cookie로 발급한다. 클라이언트는 Access token를 받으면 인가가 필요한 API 요청의 Authorization header에 즉시 삽입한다. Refresh token은 Cookie에 저장되어 있으니 자동으로 요청에 포함된다. 그러므로 Access token이 만료되거나 사용자가 브라우저를 껐다 키거나 새로고침 해서 Access token이 사라진다고 해도 Refresh token에 의해 Access token이 재발급 된다.</p>
<p>처음에는 &quot;로컬 변수에 Token을 저장하면 JS코드로 접근 가능한 거 아닌가?&quot; 싶었는데, 이런 보안에 민감한 부분을 다루는 웹을 순수 JS로 만들 리는 없고, 순수 JS를 사용한다 하더라도 클로저로 변수를 보호하면 되고, React나 Vue같은 프레임워크를 사용한다면 JS코드로 변수에 접근할 수 없으니 그 부분은 안전하다고 생각한다.</p>
<p>보안에 관한 부분은 여기서 다루자면 끝이 없을 것 같다. JWT 사용하기 편에서 어떤 보안책을 적용했고 왜 그랬는지 이야기를 해 볼 생각이다.</p>
<h1 id="🌟">🌟</h1>
<p>솔직히 이렇게 길어질 줄 몰랐다! 코드는 보안에 관련되기만 하면 배워야 할 게 너무 많아진다.</p>
<p>이 포스트를 쓰기전에는 </p>
<ol>
<li>유저 인증하고</li>
<li>인증 된 유저에게 JWT를 발급하고</li>
<li>API 요청시 JWT 검증</li>
</ol>
<p>여기까지 구현하고 끝인줄 알았다. 근데 포스팅하려고 알아보기 시작하니 너무 빙산의 일각만을 알고 있었다.</p>
<p>다음 포스트에선 실제로 서버에 JWT를 적용시키는 부분을 다룰 예정이다.</p>
<hr>
<p>이틀 전에 면접을 봤다. 처음 보는 면접이라 긴장을 했는지 잘못된 대답을 두 어개 정도 했다... 면접 내용을 복기하고 있으려니 기술적인 질문도 그렇고 인적인 질문에도 내가 이상하게 대답을 했었다는 걸 깨달아서 내가 긴장을 하긴 했구나 싶었고, 다음에 면접을 볼 때는 정신을 더욱 집중해야겠다 느꼈다.</p>
<p>결과는 어땠냐고? 떨어졌다! 자가진단을 하기로는 아무래도 포트폴리오에 문제가 있다고 생각한다. 포트폴리오 하나하나의 개발기간이 되게 긴데, 왜 길어졌는지 정확히 설명을 하지 못 하기도 했고 나 자신을 모두 포트폴리오에 녹이지 못 했다고 생각한다. 질문에 이상하게 답을 한 것도 원인이라고 생각한다.</p>
<p>아쉽지만 어쩌겠는가! 그럼에도 앞으로 나아가야한다. 하지만 구직기간이 너무 길어져서 단기 아르바이트라도 병행해야겠다 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[회원가입 / 로그인 API 만들기]]></title>
            <link>https://velog.io/@code-bebop/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EB%A1%9C%EA%B7%B8%EC%9D%B8-API-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@code-bebop/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EB%A1%9C%EA%B7%B8%EC%9D%B8-API-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 10 Jul 2021 15:09:31 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>1년 전에 CRUD 기능을 가진 게시판 웹의 Front와 Back을 만들어서 배포한 적이 있다.
그 때에는 로그인 기능을 넣지 않았는데, &quot;어짜피 만들 줄 알아!&quot; 라는 생각과 &quot;이 웹에는 로그인 기능이 필요없다.&quot; 라는 생각이었다. 근데 이제 와서 냉정히 생각해보면 그냥 만들기 어렵고 귀찮으니 자기합리화를 했던 것 뿐이었다.</p>
<p>요 근래에 SWR이라는 라이브러리도 알게 되어서, 이 기회에 로그인 기능도 있는 CRUD 웹 앱을 제대로 만들고 싶은 생각이 들었다.</p>
<hr>
<p>그래서 이번 포스트에선 Backend 에서 어떻게 회원가입 / 로그인 API를 구현하는 과정을 써내려갈 생각이다.</p>
<h1 id="개발-환경">개발 환경</h1>
<p>나는 아래와 같은 환경에서 Backend 서버를 개발하고 배포하고 있다.</p>
<ul>
<li>OS: Oracle Cloud의 ubuntu VM</li>
<li>nginx</li>
<li>node.js</li>
<li>express</li>
<li>MongoDB</li>
</ul>
<h1 id="전체-코드">전체 코드</h1>
<h2 id="indexjs">index.js</h2>
<p>먼저 express 서버를 여는 index.js를 살펴보자.</p>
<pre><code class="language-js">// index.js

const express = require(&quot;express&quot;);
const server = express();
const mongoose = require(&quot;mongoose&quot;);

const authRouter = require(&quot;./router/auth.js&quot;);

mongoose.connect(&quot;mongodb://dbUser:myPassword@SERVER_IP:DB_PORT, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() =&gt; {
    console.log(&quot;MONGO DB CONNECT&quot;);
  })

server.use(express.json());

server.use(&quot;/auth&quot;, authRouter);

server.listen(3001, () =&gt; {
    console.log(&quot;Express server listen at 3001&quot;);
});</code></pre>
<p>먼저 mongoose로 mongodb에 연결한다. 최신 버전의 mongoose(여기선 5.13.2 버전이다)에선 <code>useNewUrlParser</code>와 <code>useUnifiedTopoplogy</code> 옵션을 사용할 것을 권장한다. 이 옵션을 사용하지 않으면 mongoose에서 경고를 띄운다.</p>
<p>다음으로 <code>express.json()</code> 미들웨어를 express에 적용한다. 이 미들웨어는 bodyParser 역할을 하는데, express 자체에 내장되어 있어 따로 bodyParser plugin을 설치하지 않아도 되서 좋다.</p>
<p>다음으로 <code>/auth</code>로 요청이 들어오면 authRouter로 라우팅 시켜준다. authRouter의 코드는 바로 아래에 나온다.</p>
<p>이제 3001 port에 서버를 Listening 시킨다. 나는 3000 port에 다른 서버가 Listening 중이라서 3001 port를 선택했다.</p>
<h2 id="authjs">auth.js</h2>
<p>이제 auth.js 를 살펴보자.</p>
<pre><code class="language-js">// auth.js

const express = require(&quot;express&quot;);
const router = express.Router();
const { check, validationResult } = require(&quot;express-validator&quot;);

const User = require(&quot;../models/user.js&quot;);
const { encodePassword, decodePassword } = require(&quot;../modules/cryptoModule.js&quot;);

router.post(&quot;/register&quot;, [
        check(&quot;userId&quot;).notEmpty().isLength({ min: 5, max: 12 }).isAlpha().isLowercase(),
        check(&quot;password&quot;).notEmpty().isLength({ min: 8, max: 16 })
    ], async (req, res) =&gt; {
    try {
        console.group(&quot;detected POST request to /auth/register&quot;);
        const errors = validationResult(req);

        if(!errors.isEmpty()) {
            throw new Error(&quot;Invalid request body&quot;);
        }

        const { userId, password: plainPassword } = req.body;
        console.log(`userId: ${userId}, password: ${plainPassword}`);

        const { hashedPassword, salt } = await encodePassword(plainPassword);
        console.log(`hashedPassword: ${hashedPassword}, salt: ${salt}`);

        await User.create({ userId, hashedPassword, salt });
        res.send(&quot;Sign up complete&quot;);

        console.groupEnd();
    } catch (e) {
        console.log(`Error: ${e.message}`);
        console.groupEnd();
        res.status(500).send(e.message);
    }
});

router.get(&quot;/login&quot;, [
        check(&quot;userId&quot;).notEmpty().isLength({ min: 5, max: 12 }).isAlpha().isLowercase(),
        check(&quot;password&quot;).notEmpty().isLength({ min: 8, max: 16 })
    ], async (req, res) =&gt; {
    try {
        console.group(&quot;detected GET request to /auth/login&quot;);
        const errors = validationResult(req);

        if(!errors.isEmpty()) {
            throw new Error(&quot;Invalid request body&quot;);
        }

        const { userId, password: plainPassword } = req.body;
        const requestUserHashedPassword = await decodePassword(userId, plainPassword);

        const { hashedPassword } = await User.findOne({ userId }).exec();
        console.log(hashedPassword);
        console.log(requestUserHashedPassword);

        if (!(requestUserHashedPassword === hashedPassword)) {
            throw new Error(&quot;Isn&#39;t match password&quot;);
        }

        res.send(&quot;Sign in complete&quot;);
        console.groupEnd();
    } catch (e) {
        console.log(`Error: ${e.message}`);
        console.groupEnd();
        res.status(500).send(e.message);
    }

});

module.exports = router;</code></pre>
<p>뭐가 많으니까 나눠서 찬찬히 뜯어보자.</p>
<h3 id="post-register-router">POST /register router</h3>
<p>/auth/register 로 POST 요청이 들어오면 이 Router로 라우팅된다.</p>
<pre><code class="language-js">const express = require(&quot;express&quot;);
const router = express.Router();
const { check, validationResult } = require(&quot;express-validator&quot;);

const User = require(&quot;../models/user.js&quot;);
const { encodePassword, getUserHashedPassword } = require(&quot;../modules/cryptoModule.js&quot;);

router.post(&quot;/register&quot;, [
        check(&quot;userId&quot;).notEmpty().isLength({ min: 5, max: 12 }).isAlpha().isLowercase(),
        check(&quot;password&quot;).notEmpty().isLength({ min: 8, max: 16 })
    ], async (req, res) =&gt; {
    try {
        console.group(&quot;detected POST request to /auth/register&quot;);
        const errors = validationResult(req);

        if(!errors.isEmpty()) {
            throw new Error(&quot;Invalid request body&quot;);
        }

        const { userId, password: plainPassword } = req.body;
        console.log(`userId: ${userId}, password: ${plainPassword}`);

        const { hashedPassword, salt } = await encodePassword(plainPassword);
        console.log(`hashedPassword: ${hashedPassword}, salt: ${salt}`);

        await User.create({ userId, hashedPassword, salt });
        res.send(&quot;Sign up complete&quot;);

        console.groupEnd();
    } catch (e) {
        console.log(`Error: ${e.message}`);
        console.groupEnd();
        res.status(400).send(e.message);
    }
});

...</code></pre>
<h4 id="request-body-검증">Request body 검증</h4>
<p>Router의 두 번째 인자로 왠 배열이 들어오는데, 이건 <code>express-validator</code> 에서 참조하는 validation이다.
<a href="https://github.com/express-validator/express-validator">express-validator</a> 는 <a href="https://github.com/validatorjs/validator.js">validator</a> 라이브러리를 기반으로 만들어졌는데, request의 body값을 검증하는 것을 도와주는 라이브러리이다. 사용자가 일일이 정규표현식으로 request를 검증하는 수고로움을 덜어준다.
<code>validationResult()</code>의 인자로 <code>request</code>를 넘겨주면 <code>errors</code>를 반환하는데, 검증이 통과되었다면 <code>errors</code>는 비어있다. 그러므로 <code>errors</code>가 비어있지 않다면 ealry return을 통해 new Error를 throw해서 catch문으로 바로 넘기고 status 400과 함께 에러 메시지를 response한다.</p>
<h4 id="user-password-암호화">User password 암호화</h4>
<p>DB에 User의 Password를 Plain text상태로 저장하는 것은 얼핏 생각해봐도 보안에 좋지 못 하다. 그래서 개발자들은 여러가지 암호화 방법을 생각해냈는데, 나는 개중에서도 PBKDF2 방식의 암호화를 채택했다.</p>
<blockquote>
<p>이 글에서 암호화 방식들에 대해 설명하기엔 그 양이 너무 방대하다. PBKDF2 방식에 대한 정보는 <a href="https://d2.naver.com/helloworld/318732">NAVER D2 - 안전한 패스워드 저장 게시물</a>에서 잘 설명되어 있다.</p>
</blockquote>
<p>암호화를 시켜주는 <code>encodePassword()</code> 함수를 살펴보자.</p>
<pre><code class="language-js">// cryptoModule.js

const crypto = require(&quot;crypto&quot;);
const User = require(&quot;../models/user.js&quot;);

const createSalt = () =&gt; {
        return new Promise((resolve, reject) =&gt; {
            crypto.randomBytes(64, (err, buf) =&gt; {
                if (err) reject(err);
                resolve(buf.toString(&quot;base64&quot;));
            });
        });
}

const encodePassword = (plainPassword) =&gt; {
    return new Promise(async (resolve, reject) =&gt; {
        const salt = await createSalt();
        crypto.pbkdf2(plainPassword, salt, 121687, 64, &quot;sha512&quot;, (err, key) =&gt; {
            if (err) reject(err);
            resolve({ hashedPassword: key.toString(&quot;base64&quot;), salt });
        });
    });
}

...</code></pre>
<p><code>createSalt()</code> 함수로 Salt 값을 만든다. <code>crypto.randomBytes()</code> 함수는 말 그대로 임의의 buffer를 반환한다. 이 buffer를 base64로 인코딩하고, 이 값을 Salt 값으로써 Plain text 상태의 Password를 암호화해서 Slat 값과 함께 반환한다. 나는 <code>pdkdf2()</code> 함수의 3번짜 인자인 Key stretching count를 노출시켜놓았는데, 상수이고 보안에 관련된 값인만큼 어디 <code>.env</code> 파일에 저장해놓고 불러오는 게 좋다.</p>
<h4 id="db에-저장">DB에 저장</h4>
<p>그 뒤 userId, hashedPassword, salt 를 DB의 User collection에 저장한다.
Salt를 같이 저장하는 것은 login 요청을 받을 때에 사용하기 때문이다.</p>
<p>코드가 잘 작동하는지 Postman으로 확인해보자.
<img src="https://images.velog.io/images/code-bebop/post/fad727ee-3b41-4f6d-b6ce-694c5ebe46f6/1.PNG" alt="1">
<img src="https://images.velog.io/images/code-bebop/post/ced99796-4a0f-4411-b7d1-3c6e02199913/2.PNG" alt="2">
<img src="https://images.velog.io/images/code-bebop/post/2c4f231f-d5e8-4fe6-a369-917309da1d1a/3.PNG" alt="3">
아주 잘 동작한다.
이로써 회원가입 API 구현에 성공했다.</p>
<h2 id="get-login-router">GET /login router</h2>
<p>/auth/login으로 GET 요청이 들어오면 이 Router로 라우팅된다.</p>
<pre><code class="language-js">// auth.js

...

router.get(&quot;/login&quot;, [
        check(&quot;userId&quot;).notEmpty().isLength({ min: 5, max: 12 }).isAlpha().isLowercase(),
        check(&quot;password&quot;).notEmpty().isLength({ min: 8, max: 16 })
    ], async (req, res) =&gt; {
    try {
        console.group(&quot;detected GET request to /auth/login&quot;);
        const errors = validationResult(req);

        if(!errors.isEmpty()) {
            throw new Error(&quot;Invalid request body&quot;);
        }

        const { userId, password: plainPassword } = req.body;
        const requestUserHashedPassword = await getUserHashedPassword(userId, plainPassword);

        const { hashedPassword } = await User.findOne({ userId }).exec();
        console.log(hashedPassword);
        console.log(requestUserHashedPassword);

        if (!(requestUserHashedPassword === hashedPassword)) {
            throw new Error(&quot;Isn&#39;t match password&quot;);
        }

        res.send(&quot;Sign in complete&quot;);
        console.groupEnd();
    } catch (e) {
        console.log(`Error: ${e.message}`);
        console.groupEnd();
        res.status(500).send(e.message);
    }
});

...</code></pre>
<h4 id="request-body-검증-1">Request body 검증</h4>
<p>이 Router도 /register POST router와 같은 절차로 Request body를 검증한다.</p>
<h4 id="login-password로-hashedpassword-얻기">Login password로 hashedPassword 얻기</h4>
<p>우선 <code>getUsersHashedPassword()</code> 함수를 살펴보자.</p>
<pre><code class="language-js">// cryptoModule.js

...

const getUserHashedPassword = (userId, plainPassword) =&gt; {
    return new Promise(async (resolve, reject) =&gt; {
        const { salt } = await User.findOne({
            userId
        }).exec();

        crypto.pbkdf2(plainPassword, salt, 121687, 64, &quot;sha512&quot;, (err, key) =&gt; {
            if (err) reject(err);
            resolve(key.toString(&quot;base64&quot;));
        });
    });
}

...</code></pre>
<p>userId와 Plain text상태의 Password를 매개변수로 받아온다.
그리고 Users collection에서 같은 userId를 가진 Document를 찾아서 해당 User가 회원가입 할 때에 사용한 Salt 값을 얻어온다.
그 후 회원가입 할 때에 사용한 Key stretching count를 같이 인자로 넣어 암호화시키고 반환한다.</p>
<h4 id="db의-hashedpassword와-대조-후-로그인-여부-결정">DB의 hashedPassword와 대조 후 로그인 여부 결정</h4>
<p>이렇게 얻은 HashedPassword가 같은 UserId를 가진 Document의 HashedPassword와 대조해서 일치한다면 회원가입을 했을 때 사용한 비밀번호와 현재 로그인을 할 때에 사용한 비밀번호가 같다는 뜻이 된다. 그럼 성공적으로 로그인에 성공한 것이다.</p>
<p>다시 Postman으로 login 요청을 보내보자.
<img src="https://images.velog.io/images/code-bebop/post/04b389af-e2cb-4bf0-bac4-bb97f0c84bf9/4.PNG" alt="4">
<img src="https://images.velog.io/images/code-bebop/post/d40dfff2-617d-49bb-baaf-2e9cb8248427/5.PNG" alt="5">
잘 작동한다.</p>
<h2 id="왜-이렇게-귀찮은-과정을-거치는가">왜 이렇게 귀찮은 과정을 거치는가?</h2>
<p>그것은 <strong>PBKDF2 방식이 단방향 암호화 방식의 단점을 보완하기 위해 등장했기 때문</strong>이다.</p>
<p>단방향 암호화 방식은 암호화는 가능하지만 복호화는 할 수가 없다. 이런 회원가입 / 로그인 기능을 구현할 때에는 굳이 복호화 할 필요가 없기 때문에(비밀번호를 잊어버렸을 때 기존 비밀번호를 알려주지 않고 새 비밀번호를 만들면 되기 때문) 단방향 암호화 방식을 보편적으로 채택한다.</p>
<p>이 PBKDF2 방식의 등장 배경을 살펴보면, 옛날에는 해쉬 알고리즘(예를 들어 SHA)으로 비밀번호를 해쉬화 한 다음 base64 문자열로 인코딩해서 암호화했는데, 이런 방식으로 암호화를 하면 같은 비밀번호에 대해 항상 같은 문자열을 가지게 된다는 단점을 안게 된다.</p>
<p>해커가 어떤 방법으로든 어느 사용자의 아이디와 비밀번호를 알아내게 되었고, 서버의 DB까지 접근하게 되면 해당 유저와 같은 비밀번호를 사용하는 모든 유저들을 해킹할 수 있는 이야기가 된다.</p>
<p>그래서 암호화를 할 때에 임의로 정해진 Salt 라고 부르는 값을 Plain text 상태의 Password 앞에 붙인 다음, 해쉬 알고리즘을 임의의 숫자만큼 반복해서 적용시킨다. 이 숫자가 위에서 보았던 Key stretching count이다. 숫자가 크면 클 수록, 예측하기 어려우면 어려울 수록 해킹의 위험이 감소한다고 볼 수 있다.</p>
<blockquote>
<p>여기서 Salt는 말 그대로 소금이라는 의미로 사용된다. Password에 랜덤한 문자열을 붙여 예측을 어렵게 하는 것을 &quot;Password에 소금을 친다&quot; 라는 은유적인 표현으로 나타내는 듯 하다.</p>
</blockquote>
<p>이러면 같은 비밀번호를 암호화 시켜도 다른 문자열이 나오며, 복호화 하는 것 또한 힘들어진다.</p>
<h1 id="🌃">🌃</h1>
<p>이번 포스팅에선 회원가입 / 로그인 API를 만드는 방법 중 하나에 대해 알아보았다.
가장 기본적이라고도 할 수 있는 기능인데도 쉽게 보고 다가갔다간 큰 코 다치는 기능 중 하나다.
나는 꽤 복잡하다는 것을 알고 있었으나, 막상 구현하고 나니 나의 걱정만큼 복잡하진 않았다. 물론 복잡해지려면 끝도 없이 복잡해질 수 있는 로직이 보안에 관한 로직이지만...</p>
<p>이 다음 단계는 로그인에 성공하면 JWT를 발급해서 로그인이 필요한 API에 접근할 때마다 JWT를 검증하는 것을 구현할 예정이다.</p>
<p>그럼 또 다음 포스팅에서...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github - merge / squash merge / rebase merge]]></title>
            <link>https://velog.io/@code-bebop/Github-merge-squash-merge-rebase-merge</link>
            <guid>https://velog.io/@code-bebop/Github-merge-squash-merge-rebase-merge</guid>
            <pubDate>Thu, 01 Jul 2021 06:06:31 GMT</pubDate>
            <description><![CDATA[<h1 id="이-글의-목적">이 글의 목적</h1>
<p>최근 Github의 Branch 분기와 Pull request merge를 배우고 활용하면서 Merge의 종류가 다양하다는 걸 알게 되어 이번 포스팅에서 알아보려고 한다. </p>
<p>merge의 모든 속성을 알아볼 것은 아니고, Pull request를 할 때에 Github가 권장하는 merge 세 가지(merge, sqaush merge, rebase merge)에 대해 알아볼 것이다.</p>
<h1 id="pull-request">Pull request</h1>
<p><strong>Git으로 작업을 하면서, 어느 Branch에서 작업한 내용을 다른 Branch에 대한 Merge의 승인을 해줄 것을 요청하는 행위를 Pull request(이하 PR)라고 한다.</strong></p>
<p>PR 요청을 받은 Repository의 관리자는 PR 내용을 보고 해당 Branch에 대한 Merge의 승인 여부를 정할 수 있다. 문제가 있다고 생각되면 PR을 반려할 수 있고, 문제가 없다고 판단되면 Merge를 승인하고, 해당 Branch에 PR commit code가 합쳐진다.</p>
<p>PR 요청이 승인 되어 Merge되면, PR 요청을 보낸 사람은 기여를 인정받아 해당 Repository의 Contributor가 된다. 명망있는 Repository의 Contributor가 되면 일종의 자기 증명이 가능하니 PR이 활발히 일어난다. 오픈 소스의 순 기능이라고 생각한다.</p>
<h2 id="pull-request-merge">Pull request merge</h2>
<p><img src="https://images.velog.io/images/code-bebop/post/dafd99b1-8685-4754-8bb8-85defb84f0f8/2.PNG" alt="1">
Github 웹사이트에서 PR 요청을 승인해서 Merge하려고 할 때에, Github는 Merge에 대해 세 가지 선택지를 준다. 바로 Merge, Squash merge, Rebase merge이다.</p>
<h3 id="merge">Merge</h3>
<p>Merge는 그냥 일반 Merge다. Branch와 Branch는 이어지고 Merge된 Repository의 History에서 어느 Branch에서 Merge되었는지, 이 Branch에서 어떤 Commit들이 있었는지 모두 볼 수 있다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/ea3f5015-dea8-4e4d-a303-1b8b64402fcc/3.PNG" alt="2"></p>
<p>파란 게 <code>develope branch</code>고 노란 게 <code>#8-Chat.vue/$socket.id branch</code>다.
이 그래프는 <code>develope</code>에서 <code>#8-Chat.vue/$socket.id</code>을 merge했을 때의 그래프이다. <code>#8-Chat.vue/$socket.id</code>으로부터의 Pull request를 Merge했다는 설명이 명시되어 있고, branch도 이어져 있어서 그 사실을 직관적으로 알 수 있다.</p>
<ul>
<li><strong>이 방식의 장점</strong>
Git history에서 해당 Pull request에 대해 상세하게 확인할 수 있다.</li>
<li><strong>단점</strong>
그러나 Branch가 많아지고, Commit도 많아지고 하면 Git history는 알아보기가 힘들어진다. 프로젝트의 규모가 크면 클 수록 이 방식은 선호되지 않는다.</li>
</ul>
<h3 id="rebase-merge">Rebase merge</h3>
<p>Rebase merge는 Merge할 Branch의 Commit 내역들을 그대로 옮긴다. Branch의 Base를 옮기는 일이니 말 그대로 Rebase다. 그래서 Branch는 이어지지 않는다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/5bd7485c-4495-41e9-bfb4-5b0625eb381a/4.PNG" alt="3"></p>
<p>초록색이 <code>main</code>, 분홍색이 <code>1-Caht.vue/getCss</code>다.
이 그래프는 <code>main</code>에서 <code>1-Caht.vue/getCss</code>의 Pull request를 Rebase and merge 했을 때의 그래프다.<code>1-Caht.vue/getCss</code>의 <code>fix #1</code>이라는 commit이 <code>main</code>의 <code>fix #1 (#12)</code>라는 이름의 commit으로 그대로 옮겨진 것이다. #1은 Issue link, (#12)는 Pull request의 번호다.
commit의 base를 <code>1-Caht.vue/getCss</code>에서 <code>main</code>으로 옮긴 것이기 때문에 branch는 이어져 있지 않다. 보통 이 이후에 rebase를 한 branch는 삭제해서 그래프를 깔끔하게 유지한다.</p>
<ul>
<li><strong>이 방식의 장점</strong>
Git history를 볼 때 깔끔하게 볼 수 있다. 여러 개의 Branch가 복잡하게 얽혀있지 않고 그냥 default branch 하나의 Histroy만 쭉 읽으면 해당 프로젝트의 이력을 이해할 수 있다.</li>
<li><strong>단점</strong>
여러 개의 Commit을 Rebase merge했는데 Commit conflict가 일어나면 Merge하려는 모든 Commit에서 Conflict가 일어난다.
<a href="https://www.youtube.com/watch?v=J1euNW6q8R8&amp;list=WL&amp;index=37&amp;ab_channel=%EC%83%9D%ED%99%9C%EC%BD%94%EB%94%A9">생활코딩님의 rebase 충돌과 원인 해결 영상</a>을 보면 좀 더 쉽게 이해할 수 있다.
또한 거미줄처럼 이어지는 Branch는 사라지지만, 여전히 의미가 없다시피 하는 Commit도 History에 남는다.</li>
</ul>
<h3 id="squash-merge">Squash merge</h3>
<p>사실 Squash는 Rebase merge의 Option이다. Squash 옵션을 적용한 Rebase merge를 Squash merge라고 부르는 모양이다.</p>
<p>Squash merge는 Rebase하되 Merge할 Commit들을 하나의 Commit으로 뭉친다. Squash는 사전적 의미로 으깨다, 짓누르다 라는 뜻을 가지고 있다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/9d599a8d-06d9-4ec0-a878-67c59db18e0b/5.PNG" alt="4">
<code>develope</code>로부터 분기한 <code>16-ChatUiImprove</code>가 있다. <code>16-ChatUiImprove</code>에는 2개의 commit이 있다. 여기서 Squash merge를 사용해보겠다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/5d172a17-92ba-4f76-acce-9d570a0d2762/6.PNG" alt="5">
<code>16-ChatUiImprove</code>의 Commit들이 하나로 합쳐진 다음 <code>develope</code>로 rebase되었다. 자잘한 CSS 변경, 오타 수정 같은 Commit까지 History에 남기지 않고 의미 있는 Commit들으로만 History를 유지할 수게 된다.</p>
<ul>
<li><strong>이 방식의 장점</strong>
Git history의 Branch도 깔끔하게 유지하되 자잘한 Commit들은 없애고 의미있는 Commit으로만 History를 이룰 수 있다.</li>
<li><strong>단점</strong>
나는 아직 이 방식의 단점을 느끼지 못 했다. 다른 사람들은 Merge에 대한 정보가 부족한 점을 단점으로 여기고 있으나, 해당 Merge Commit의 설명에 정보를 자세히 명시해놓으면 될 일이다.</li>
</ul>
<h1 id="🌞">🌞</h1>
<p>이번 포스트에선 Merge의 종류에 대해 간략하게 알아보았다.
이 의외에도 Merge option은 굉장히 다양하다.</p>
<p>사실 1인 개발에 있어서는 그냥 main branch 하나 develope branch 하나면 충분하지만, 나는 미리미리 Git 협업에 익숙해지고 싶은 마음이기도 하고 1인 개발에서도 Issue tarcker와 Project board를 사용하면 개발이 좀 더 잘 되어서 기능 개발을 할 때마다 branch를 분기하고 PR을 하고 있다.</p>
<hr>
<p>당초 취업을 생각하고 있던 것이 이번 년도의 초였는데, 벌써 매미 울음소리가 들려오는 계절이 왔다. 내 포트폴리오에 실린 프로젝트들은 그리 입맛이 당기지 않는 모양이다.</p>
<p>그것은 그렇다 하더라도 부산에는 정말 구인공고가 없다. 정말 빈손으로라도 서울으로 올라가야겠다는 생각이 점점 커진다. 답답한 나날이다. 구인공고가 없는 건 어쩔 수가 없는 일이고 공부는 계속해야하지 않겠는가.</p>
<p>그래서 React 말고도 Vue를 배우고 있다. 주니어일 때에는 T자 모양으로 기술 스택을 관리하라는 모양이다. 한 언어를 깊게 파는 것도 좋으나, 다른 언어도 기본적인 부분 정도는 배워서 지원할 수 있는 회사의 범위를 넓히고 식견도 넓히라는 이야기다.</p>
<p>아무튼 그렇다. 어서 취업이 하고 싶은 마음이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github / Issue Tracker와 Project Board]]></title>
            <link>https://velog.io/@code-bebop/Github-Issue-Tracker%EC%99%80-Project-Board</link>
            <guid>https://velog.io/@code-bebop/Github-Issue-Tracker%EC%99%80-Project-Board</guid>
            <pubDate>Sun, 27 Jun 2021 14:47:06 GMT</pubDate>
            <description><![CDATA[<h1 id="포스트-작성-계기">포스트 작성 계기</h1>
<p>깃허브를 접하고 사용한 지도 1년이 다 되어간다.
그런데 아직 깃허브를 사용한 팀 단위의 개발을 접해보질 못 해서 깃허브로 하는 거라곤 Commit이랑 어줍잖은 Branch와 Merge뿐이다. 사실상 코드 저장소 정도로 사용하고 있었다.
잘 모르지만서도 어렴풋이 느끼기엔 깃허브를 잘 못 사용하고 있다고 느껴져 구글링으로 배우게 된 것이 Issue Tracker 기능과 Project Board 기능이다.</p>
<h1 id="issue-tracker">Issue Tracker</h1>
<p>깃허브 레포지토리 탭 메뉴를 보면 <code>Issue</code>라는 메뉴가 있다.
이전에는 그냥 &quot;프로젝트를 배포한 뒤, 사용자들이 그에 대한 버그나 건의사항을 적는 곳&quot; 정도로 인식하고 있었는데, 그건 Issues의 기능 중 하나였고 더 많은 기능을 가지고 있었다.</p>
<p>Issue의 명사로써의 사전적 의미는 다음과 같다.</p>
<blockquote>
<ol>
<li>(논의논쟁의 중요한) 주제[안건], 쟁점, 사안</li>
<li>(걱정거리가 되는) 문제</li>
</ol>
</blockquote>
<p>내가 생각했던 Issues의 기능은 2번의 의미로써 뿐이었고, 사실은 더 나아가 1번의 의미로써의 기능까지 제공하고 있었다.</p>
<p>좀 추상화시켜 이야기하자면 현재 내가 생각하는 Issues의 가장 주요한 기능은 <strong>TodoList로써의 기능</strong>이다.</p>
<h2 id="사용법">사용법</h2>
<p>이 개념을 처음 접할 때 글만으로는 이해하기 힘드니 사진과 함께 알아보자.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/efe1612d-8cbc-45e2-a39d-2d0de7d20ebf/github.com_code-bebop_vue-messenger-web_issues.png" alt="1"></p>
<p>처음 Issues 메뉴를 열어보면 이런 화면이다. New Issue 버튼으로 새 Issue를 만들어보자.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/85a775bc-19c1-4b0f-8102-a17fa63fbefa/github.com_code-bebop_vue-messenger-web_issues_new%20(1).png" alt="2"></p>
<p>짠, 새로운 이슈를 만드는 화면이다.
UI를 살펴보면, 일단 이슈의 제목과 본문을 입력한다. 본문은 마크다운으로 입력할 수 있고 마크다운 도구도 지원된다.</p>
<p>그리고 오른쪽에 있는 메뉴가 핵심이다. 하나하나 살펴보자.</p>
<h3 id="assignees">Assignees</h3>
<p>Assignees는 이 이슈를 처리해야할 담당자를 지정하는 메뉴다.
<img src="https://images.velog.io/images/code-bebop/post/234f8bbb-8f6c-45ef-b0e7-a5ea6528b278/github.com_code-bebop_vue-messenger-web_issues_new%20(2).png" alt="3">
혼자 하는 프로젝트라 나 밖에 없는게 어쩐지 쓸쓸하다.</p>
<h3 id="labels">Labels</h3>
<p><strong>이 부분이 중요하다.</strong></p>
<p>이슈를 하나 둘씩 등록하다보면 제목만으로는 이슈들의 구분이 힘들어져서 우선순위, 혹은 이슈를 해결해야할 역할군을 지정하기가 힘들어진다. 프로젝트가 오래되었고 규모가 크다면 이슈가 2000개, 10000개씩 쌓이는 일도 있다.</p>
<p>그래서 이슈에 Label을 붙여서 구별이 용이하게 만드는 것이다.
<img src="https://images.velog.io/images/code-bebop/post/524bf4a7-3304-4938-b63e-3958d1facb4d/github.com_code-bebop_vue-messenger-web_issues_new%20(3).png" alt="4"></p>
<p>깃허브가 기본으로 등록해놓은 9개의 Label들이 있고, 맨 밑의 Edit labels 버튼으로 사용자 정의 Label을 만들 수 있다.</p>
<h3 id="projects">Projects</h3>
<p><img src="https://images.velog.io/images/code-bebop/post/9e39dcf8-93ac-40f0-a784-e496833a039f/github.com_code-bebop_vue-messenger-web_issues_new%20(4).png" alt="5">
처음으로 이걸 열어봤다면 아무 것도 없을 것이다. 나는 General Board라는 이름의 Project Board를 만들어 놓아서 목록에 있다.</p>
<p>Projects 메뉴는 해당 이슈를 어느 Project Board에 놓을 지 정하는 메뉴다.</p>
<p>Project Board에 대해선 아래에서 마저 설명할 것이다.</p>
<h3 id="milestone">Milestone</h3>
<p><img src="https://images.velog.io/images/code-bebop/post/5f394c2a-53e1-4273-9621-e598aec88691/github.com_code-bebop_vue-messenger-web_issues_new%20(5).png" alt="6">
이 메뉴도 Projects와 마찬가지로 처음 열면 아무것도 없다.
Milestone 메뉴는 해당 이슈를 어느 Milestone에 포함시킬 것인지 정하는 메뉴다.</p>
<p>Milestone은 간단히 말하자면 여러 개의 이슈를 포함하는 이슈의 상위 개념이다. 이 또한 아래에서 자세히 설명하겠다.</p>
<h2 id="이슈-등록">이슈 등록</h2>
<p>이제 이슈를 등록해보자. Submit new issue 버튼을 누르면 등록된다.
<img src="https://images.velog.io/images/code-bebop/post/a39ab6e4-c849-4606-a83e-5802e440c6ef/github.com_code-bebop_vue-messenger-web_issues%20(1).png" alt="7">
와우! 이슈가 등록되었다. 등록된 이슈들은 Issues 탭 메뉴에 등록된 순서대로 쌓인다.
이슈 리스트 상단에는 이슈들을 필터링 할 수 있는 메뉴도 제공되어 있어 이슈들을 일목요연하게 볼 수 있다. 이슈가 많으면 많을 수록 빛을 발하는 기능이다.</p>
<p>이슈를 등록했으니 이 이슈를 해결하는 법에 대해서도 알아보자.</p>
<h2 id="이슈-닫기">이슈 닫기</h2>
<p>이슈를 열어보면 번호가 붙어 있는 걸 볼 수 있다.
<img src="https://images.velog.io/images/code-bebop/post/ab559a74-ba74-48ea-b2df-afcf8e9f4c8a/github.com_code-bebop_vue-messenger-web_issues%20(2).png" alt="8">
이 이슈는 #4 라는 번호가 붙어 있다.</p>
<p>이슈에 관한 작업을 하고나서 <strong>이슈 아래에 해당 커밋을 나타나게 하고 싶으면, 커밋을 할 때에 커밋 제목에 해당 이슈의 번호를 붙여주면 된다.</strong>
이것을 <code>Issue linking</code>이라고 한다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/4d0b55ff-790f-4ad4-b1bd-6589e8554f4c/github.com_code-bebop_vue-messenger-web_commit_aefc4ec2f5dc887bfd0086f7174cf1b16f7fbf8a.png" alt="9">
여기 #2 이슈에 관한 코드가 담긴 커밋이 있다. 제목에 #2 를 붙여놓았는데, 이 부분에 링크가 생겼고 마우스를 올려보면 해당 이슈의 요약정보가 나타난다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/57965725-eb97-4618-956f-9804ebe44325/github.com_code-bebop_vue-messenger-web_issues_2.png" alt="10">
클릭해보면 해당 이슈 페이지로 이동하고, 이슈의 타임라인에 해당 커밋이 추가된 것이 보인다.</p>
<p>이제 이슈를 닫는 방법에 대해 알아보자.</p>
<p>이슈를 해결하는 행위를 Github에선 <code>Close issue</code> 라고 한다. 이슈를 닫는다 라고 해석하겠다.
이슈를 닫기 위해선 일반적으로 두 가지 방법을 사용하는데,</p>
<ol>
<li>이슈 페이지 맨 아래에 있는 Close issue 버튼을 누른다.</li>
<li>커밋할 때에 닫고자 하는 이슈의 번호와 함께 접두사를 붙인다.</li>
</ol>
<p>가 그 두가지 방법이다.</p>
<h3 id="close-issue-버튼-누르기">Close issue 버튼 누르기</h3>
<p><img src="https://images.velog.io/images/code-bebop/post/733c10a8-42da-4474-9eae-5462d7208679/github.com_code-bebop_vue-messenger-web_issues_2%20(1).png" alt="11">
이슈 페이지 맨 아래를 보면 Close issue 버튼이 있다. 이걸 누르면 이슈가 닫힌다.</p>
<h3 id="커밋-제목에-이슈-번호와-함께-접두사-붙이기">커밋 제목에 이슈 번호와 함께 접두사 붙이기</h3>
<p><a href="https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue">Github Docs</a>에 가장 자세히 나와있다.</p>
<blockquote>
<ul>
<li>close</li>
</ul>
</blockquote>
<ul>
<li>closes</li>
<li>closed</li>
<li>fix</li>
<li>fixes</li>
<li>fixed</li>
<li>resolve</li>
<li>resolves</li>
<li>resolved</li>
</ul>
<p>이 9가지 단어 중 하나를 Issue link의 앞에 붙이면 해당 이슈에 Issue linking 됨과 동시에 Issue closed 된다.</p>
<p><code>KEYWORD</code>와  <code>#ISSUE_NUMBER</code>가 커밋 제목에 포함되어 있으면 인식되는 모양이다.</p>
<p>다만, 올바른 문자열이 커밋 제목에 포함되어 있어도 레포지토리의 default branch에 커밋된 게 아니라면 이슈 링킹은 작동하지 않는다.
또한 다른 branch에서 커밋되었더라도 해당 커밋이 default branch로 merge되면 이슈 링킹이 작동한다.</p>
<table>
<thead>
<tr>
<th align="left">Linked issue</th>
<th align="center">Syntax</th>
<th align="right">Example</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Issue in the same repository</td>
<td align="center">KEYWORD #ISSUE-NUMBER</td>
<td align="right">Closes #10</td>
</tr>
<tr>
<td align="left">Issue in a different repository</td>
<td align="center">KEYWORD OWNER/REPOSITORY#ISSUE-NUMBER</td>
<td align="right">Fixes octo-org/octo-repo#100</td>
</tr>
<tr>
<td align="left">Multiple issues</td>
<td align="center">Use full syntax for each issue</td>
<td align="right">Resolves #10, resolves #123, resolves octo-org/octo-repo#100</td>
</tr>
</tbody></table>
<p>Github Docs에 의하면, 다른 레포지토리에도 이슈 링킹이 가능하며 하나의 커밋을 여러개의 이슈에 링킹하거나 닫는 것이 가능하다.</p>
<p><code>fix #11, #13, #17</code> 같이 커밋 제목을 작성해도 #11, #13, #17 이슈가 전부 닫히는 것 같다.</p>
<hr>
<h1 id="project-board">Project Board</h1>
<p>Project Board는 기본적으로 <a href="https://ko.wikipedia.org/wiki/%EA%B0%84%EB%B0%98_%EB%B3%B4%EB%93%9C">칸반 보드</a> 레이아웃을 가지고 있다. 이 안에는 이슈들에 대한 전체적인 진행 상황이 담겨 있고, 진행 상황을 컨트롤 할 수 있다.</p>
<h2 id="사용법-1">사용법</h2>
<p><img src="https://images.velog.io/images/code-bebop/post/ef4270d9-8d7f-4554-962d-96ceda2b814e/github.com_code-bebop_vue-messenger-web_projects_new.png" alt="12">
Projects 페이지에서 New Project 버튼을 누르면 이런 화면이 된다.
Project의 제목, 설명을 입력하고 Project template를 선택한 뒤 Create Project 버튼을 누르면 Project가 생성된다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/f4b6ce34-9c06-45d1-b2e5-ee3e99197f6f/github.com_code-bebop_vue-messenger-web_projects_new%20(1).png" alt="13">
Project template는 이것저것 있지만, 1인 프로젝트에 적합하다고 생각되는 Automated kanban을 선택했다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/997ff92e-a387-480e-b6ce-4252318d876f/github.com_code-bebop_vue-messenger-web_projects_2_add_cards_query=is%253Aopen.png" alt="14">
짠, 멋지게 생긴 Project Board가 완성됐다. 사용법을 잘 알고 사용하면 개발 효율에 있어 굉장한 이점이 있으니(1인 프로젝트라 할지라도) 하나하나 알아보자.</p>
<p>먼저 가장 큰 부분을 차지하는 To do - In progress - Done 으로 구성된 Board 부분을 보자. 이런 레이아웃을 칸반 보드라고 한다.</p>
<p>이 칸반 보드는 현재는 세 Column으로 나뉘어져 있는데, 사용자가 원한다면 Add Column 버튼을 눌러서 새로운 Column을 계속 만들 수 있다.</p>
<p>Column 안에는 여러 개의 Card가 포함되고, Card는 왼쪽 Column에서 오른쪽 Column으로 이동된다. 일반적으로 맨 왼쪽에 TodoList Card가 들어오고, 해당 Todo Card에 대한 개발이 이루어지고 있으면 In progress Column로 카드를 옮기고, Todo Card가 해결되면 Done Column으로 옮긴 뒤 종결한다.</p>
<p><strong>Card는 Note card와 Issue card, Pull request card로 구분된다.</strong></p>
<h3 id="note-card">Note card</h3>
<p>Note card는 Column의 위에 있는 <code>+ 버튼</code>을 눌러 해당 Column에 추가할 수 있다.
<img src="https://images.velog.io/images/code-bebop/post/6cb69562-ea2a-4e55-acd1-4d7b0238d4a3/github.com_code-bebop_vue-messenger-web_projects_2_add_cards_query=is%253Aopen%20(1).png" alt="15">
화면의 Todo column을 보면 Project를 만들면 자동으로 추가되는 Note Card들도 보인다.</p>
<h3 id="issue-card">Issue card</h3>
<p>그리고 화면 오른쪽을 보면 슬라이드 메뉴 안에 Issue card가 있다.
기본적으로 모든 이슈들을 선택할 수 있고, Qualifier를 통해 Issue를 검색할 수 있다. Qualifier에 대해선 <a href="https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests">Github Docs의 해당 항목</a>을 살펴보라.</p>
<p>화면에선 is:open Qualifier로 현재 열려있는 이슈들만 보여진다. 이 Issue card를 클릭 후 드래그해서 Column으로 옮길 수 있다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/6316577c-9b0b-4216-8aed-61e4df8d81eb/github.com_code-bebop_vue-messenger-web_projects_2_add_cards_query=is%253Aopen%20(2).png" alt="16">
이렇게 말이다.</p>
<p>이후 개발 진척도에 따라 Card를 다른 Column으로 이동시켜주면 된다.</p>
<h3 id="pull-request-card">Pull request card</h3>
<p>이 레포지토리에서 Test branch를 분기해서 Test Commit을 main branch에 PR했다.
<img src="https://images.velog.io/images/code-bebop/post/7c8e86e1-b7ff-46b8-a289-4f00694bab54/github.com_code-bebop_vue-messenger-web_projects_2.png" alt="17">
화면 오른쪽 메뉴를 보라. Test Commit PR card가 생겼다.</p>
<p>이 또한 Issue card와 마찬가지로 개발 진척도에 맞게 Card를 옮겨주면 된다.</p>
<h3 id="automate-manage">Automate manage</h3>
<p>각 Column의 아랫쪽을 보면 <code>Manage 버튼</code>이 있다.
Card가 자동으로 등록되고 이동하는 것을 Manage하는 옵션이다.</p>
<p>일단 버튼을 클릭해보자.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/4cc1ac4f-b3ce-468f-95d7-382fb10bf60e/1.PNG" alt="18"></p>
<p>첫 번째로 Preset이다. 현재는 To do로 선택되어 있는데 클릭해보자.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/1bcbc6de-ab2c-4b59-b83c-da3d7314c9d2/2.PNG" alt="19"></p>
<p>Preset은 해당 Column이 어떤 Automate preset을 사용할지에 대한 Preset이다.</p>
<ul>
<li>To do: 할 계획이지만 아직 시작되지 않은 Card가 있는 Column에 대한 Preset이다.</li>
<li>In progress: 현재 처리하고 있는 Card가 있는 Column에 대한 Preset이다.</li>
<li>Done: 완료된 Card가 있는 Column에 대한 Preset이다.</li>
<li>None: 어느 Automate 옵션도 사용하지 않을 Column에 대한 Preset이다.</li>
</ul>
<p>앞서 Project를 생성할 떄에 Template를 선택하던 것이 기억나는가? 선택한 Template에 따라 적절한 Automate 옵션이 설정된 Column들을 자동으로 생성해준다.</p>
<h4 id="to-do">To do</h4>
<p><img src="https://images.velog.io/images/code-bebop/post/4627a023-06bd-4e50-a6a8-40bb6c5b69e5/3.PNG" alt="20">
To do Preset의 옵션부터 살펴보자.</p>
<ul>
<li>Move issues here when...<ul>
<li>Newly added: 이 프로젝트에 이슈가 등록되면 이 Column에 해당 이슈 Card가 등록된다.</li>
<li>Reopened: 이 프로젝트에서 닫혔던 이슈가 reopen되면 이 Column에 해당 이슈 Card가 등록된다.</li>
<li>Move pull requests here when...<ul>
<li>Newly added: 이 프로젝트에 Pull request가 발생하면 이 Column에 해당 Pull request card가 등록된다.</li>
<li>Reopened: 이 프로젝트에서 닫혔던 Pull request가 reopen되면 이 Column에 해당 Pull request card가 등록된다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>현재 프로젝트에서는 Reopened 옵션을 In progress column에서 사용하고 있기 때문에 To do column에서 선택이 되지 않는 모습이다.</p>
<h4 id="in-progress">In progress</h4>
<p><img src="https://images.velog.io/images/code-bebop/post/3400abf9-874f-4b15-bd63-0dfe65cdc85b/4.PNG" alt="21"></p>
<p>처음 나온 옵션만 설명하겠다.</p>
<ul>
<li>Move pull requests here when...<ul>
<li>Approved by reviewer: 해당 Pull request card가 최소 필수 Approving review를 충족하면 이 Coulmn에 자동으로 등록된다.</li>
<li>Pending approval by reviewer: 해당 Pull request card가 최소 필수 Approving review를 충족하지 못 하거나, Request changes(Pull request 거절)되면 이 Coulmn에 자동으로 등록된다.</li>
</ul>
</li>
</ul>
<p>적혀있기로는 Approved by reviewer 옵션과 Pending approval by reviewer 옵션은 서로 다른 Column에서 각각 활성화 시켜놓는 것을 권장하고 있다.
승인된 Pull request와 거절된 Pull request를 분리시키고 그에 따른 적절한 절차를 적용하기 위한 것으로 생각된다.</p>
<h4 id="done">Done</h4>
<p><img src="https://images.velog.io/images/code-bebop/post/88b9415f-70b5-45c2-ad12-b7ba55b90820/5.PNG" alt="22"></p>
<ul>
<li>Move issues here when...<ul>
<li>Closed: 해당 Issue card가 닫히면 이 Column에 자동으로 등록된다.</li>
</ul>
</li>
<li>Move pull requests here when...<ul>
<li>Merged: 해당 Pull request가 Merge되면 이 Column에 자동으로 등록된다.</li>
<li>Closed with unmerged commits: 해당 Pull request가 Merge되지 않은 채로 닫히면 이 Column에 자동으로 등록된다.</li>
</ul>
</li>
</ul>
<hr>
<h1 id="milestones">Milestones</h1>
<p>마지막으로 알아볼 메뉴는 Milestones이다. Milestone은 말 그대로 이정표인데, <strong>Issue보다는 크며 Project보다는 작은 개발 단위</strong> 라고 생각한다.</p>
<p>일단 만들어보며 알아보자.</p>
<h2 id="사용법-2">사용법</h2>
<p><img src="https://images.velog.io/images/code-bebop/post/9edd9510-c50b-4981-9dd6-24a41066f6d0/github.com_code-bebop_vue-messenger-web_issues%20(3).png" alt="23"></p>
<p>Issues 혹은 Pull requests 페이지의 상단을 보면 Milestones 메뉴가 있다. 페이지에 진입하면 이런 모습을 하고 있다. 새로운 Milestone을 만들어 보자.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/8f2d2868-e19d-417e-92c9-02928acbfdda/github.com_code-bebop_vue-messenger-web_issues%20(4).png" alt="24"></p>
<p>제목과 설명, 그리고 마감일을 정할 수 있다. 마감일은 optional이다.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/7707fe23-b5c3-4950-94cb-21234dd05737/github.com_code-bebop_vue-messenger-web_milestones_with_issues=no.png" alt="25"></p>
<p>Milestone이 만들어졌다.</p>
<p>Milestone에는 Issue들이 등록되는데, Issue를 만들 때에 해당 Issue를 어느 Milestone에 등록할지 정할 수 있다. Issue를 등록하러 가보자.
<img src="https://images.velog.io/images/code-bebop/post/ce269f6a-ae9f-4de1-943e-c4480fe9ee29/github.com_code-bebop_vue-messenger-web_issues_new%20(6).png" alt="26">
Milestones 옵션을 열어보면 방금 만든 Milestone이 있다.
<strong>Issue를 등록할 때에는 두 개 이상의 Milestone을 지정할 수 없다.</strong></p>
<p>이 Milestone로 지정하고 Issue를 등록해보자.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/866f0122-663e-4055-bd8a-9613908616be/github.com_code-bebop_vue-messenger-web_milestones.png" alt="27">
짠, Milestone에 Issue가 등록되었다. 이렇게 Issue를 만들 때 마다 관련된 Milestone에 등록해서 특정 기능에 대한 Issue들을 한 곳에 모아 볼 수 있다.</p>
<p>그리고 Milestone에 등록된 Issue가 닫히면 Milestone의 진척도가 올라간다. Test Issue를 닫아보자.</p>
<p><img src="https://images.velog.io/images/code-bebop/post/c42fee19-e1f0-40ff-a193-70dd4e6420ab/github.com_code-bebop_vue-messenger-web_milestone_1_closed=1.png" alt="28">
Issue가 총 1개 등록되었는데 1개가 닫혀서 진척도가 100%가 된 모습이다.</p>
<h1 id="🍉">🍉</h1>
<p>이번 포스팅에선 Github의 레포지토리 기능 중 Issue tracker, Project board, Milestone에 대해 알아보았다.</p>
<p>전체적으로 체계적인 개발을 도와 개발 효율을 높여주는 역할을 한다.
이것이 팀에서 사용된다면 프로젝트의 진척도, 마감일 등을 가늠하며 팀원 개개인의 어떤 역량등을 점쳐볼 수 있을 것이고, 1인 프로젝트에 적용된다면 개발 동기를 부여하며 개발에 대한 긴장감을 유지할 수 있는 장치가 될 것이라 생각한다. </p>
<p><strong>이런 좋은 기능들이 있는데 도대체 왜 여지껏 알아보지도 않고 사용하지도 않았는지 억울한 마음까지 든다.</strong> 늦었지만 지금이라도 알아서 다행이지 않은가.</p>
<p>이 포스팅을 쓰면서 Pull Request에 대해서도 알게 되었고, Git에 대해 여러가지 알게 되었다.
<code>Git Hook</code>이라는 것도 접했는데, 얼핏 보기로는 Issue Tracking이나 Commit에 대한 부분을 자동화 시켜주는 기능을 하는듯 하다. 이에 대해서도 포스팅을 할 예정이다.</p>
<p>그럼 다들 Github를 잘 활용해서 즐거운 개발을 하기 바란다. 안뇽~</p>
]]></description>
        </item>
    </channel>
</rss>