<?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, 16 Oct 2022 08:10:43 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. 멋진홍홍. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/awesome-hong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Safari - Invalid regular expression: invalid group specifier name 솔루션]]></title>
            <link>https://velog.io/@awesome-hong/Safari-Invalid-regular-expression-invalid-group-specifier-name-%EC%86%94%EB%A3%A8%EC%85%98</link>
            <guid>https://velog.io/@awesome-hong/Safari-Invalid-regular-expression-invalid-group-specifier-name-%EC%86%94%EB%A3%A8%EC%85%98</guid>
            <pubDate>Sun, 16 Oct 2022 08:10:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/182346fe-152f-4d44-a4fc-961265ab2355/image.png" alt=""></p>
<h2 id="safari-브라우저에서는-정규표현식의-lookbehind-기능을-사용할-수-없다">safari 브라우저에서는 정규표현식의 lookbehind 기능을 사용할 수 없다.</h2>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/a1e20519-c29f-4c4e-bae9-4f40ca609ef0/image.png" alt=""></p>
<h3 id="lookbehind">Lookbehind</h3>
<p>lookbehind 라는 정규식 문법은 모든 브라우저에서 support하지 않는다.</p>
<p><code>X(?&lt;!Y)</code> 와 같은 형태의 문법으로, 
X중에서 Y라는 패턴에 매칭되지 않는 문자만 포함해주는 문법이다.</p>
<h3 id="예시">예시</h3>
<p>숫자를 3자리 단위로 끊어서 <code>,</code>를 찍어주어 돈 단위를 표현해주는 기능을 위해 
아래처럼 정규식을 사용했다.</p>
<pre><code class="language-typescript">return money.toString().replace(/\B(?&lt;!\.\d*)(?=(\d{3})+(?!\d))/g, &quot;,&quot;);</code></pre>
<p>위 코드에서 <code>\B(?&lt;!\.\d*)</code> 에 해당하는 부분이 지원하지 않는 부분이다.</p>
<p>돈이 소수점을 가질 때, 
소수부말고 <strong>정수부</strong>만 구분하기 위해 추가된 코드이다.</p>
<p>그래서 lookbehind 문법을 사용하지 않기 위해서
아예 split으로 정수부, 소수부를 나누어버리고 </p>
<p>다음과 같이 수정한다.</p>
<pre><code class="language-typescript">const parts = money.toString().split(&#39;.&#39;);

parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, &#39;,&#39;);

return parts.join(&#39;.&#39;);</code></pre>
<hr>
<p>+)
개발할때마다, 일일히 찾아보기는 힘드니</p>
<p><a href="https://regexr.com/">https://regexr.com/</a></p>
<p>자주 사용하는 유용한 정규식 사이트에서 확인하면 좋을 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/bc862949-062c-4011-8e6e-d8c5db1d0314/image.png" alt="">
이것처럼 모든 브라우저에서 지원하지 않는 문법을 보여준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Vercel 배포 중 오류 해결하기 2 - Client_fetch_error]]></title>
            <link>https://velog.io/@awesome-hong/vercel</link>
            <guid>https://velog.io/@awesome-hong/vercel</guid>
            <pubDate>Thu, 07 Jul 2022 13:36:06 GMT</pubDate>
            <description><![CDATA[<p>또...  Client_fetch_error 등장....으윽 그만..ㅠ</p>
<p><code>Unexpected token &lt; in JSON at position 0</code>
이게 또 나왔다.</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/40f7d085-0293-4a49-b074-c05e3cfbcbb6/image.png" alt=""></p>
<p>여기 콘솔에러의 링크로 들어가면 환경변수로 NEXTAUTH_URL 를 설정해주라고만 하는데, 이미 잘 했다... 물론 NEXTAUTH_SECRET 이것도 잘 해놨다.
(이건 저번 오류포스팅에서 해결함)</p>
<p>그래서 deployment status에 들어가보니,
이전버전 배포에는 없었던 빌드 이슈가 등장....!</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/225e6a7b-5aa1-4287-8933-c013f1218315/image.png" alt=""></p>
<p><a href="https://nextjs.org/docs/api-routes/introduction">api 라우트 관련 공식문서</a>를 보니,
next.js에서 서버사이드 함수를 동작할 때, api 엔드포인트로 pages/api에 있는걸 /api/* 로 처리하게 되는데,
pages와 같은 레벨에 api 폴더가 존재하면 여기로 매핑이 바뀌어서 next-auth 사용에 있어서 경고가 뜬 것 같다.</p>
<hr>
<p>그래서 api 폴더를 apis로 변경하여 해결함!
<img src="https://velog.velcdn.com/images/awesome-hong/post/4ba27e9c-8c0c-4aae-b41e-c07f593e7813/image.png" alt=""></p>
<p><a href="https://community.auth0.com/t/auth0-with-next-auth-running-locally-but-not-on-vercel/58635">여기</a>에서 어떤 분이 알려주심.
런던사는 필립님 감사합니다....
<img src="https://velog.velcdn.com/images/awesome-hong/post/ea7b3c95-0dde-498e-80d1-e4fc39e55812/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[넘블] 1회차 로그인 모듈 만들기 회고록]]></title>
            <link>https://velog.io/@awesome-hong/%EB%84%98%EB%B8%94-1%ED%9A%8C%EC%B0%A8-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%AA%A8%EB%93%88-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@awesome-hong/%EB%84%98%EB%B8%94-1%ED%9A%8C%EC%B0%A8-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%AA%A8%EB%93%88-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Thu, 30 Jun 2022 09:38:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/30432885-82e5-4a3e-b1f1-63382174362e/image.png" alt="">
<img src="https://velog.velcdn.com/images/awesome-hong/post/efde978b-bdd5-4d14-9135-dfaaa67e2e48/image.png" alt=""></p>
<p>넘블에서 &quot;로그인 모듈 만들기&quot; 이라는 주제로 챌린지를 시작했다.
실무와 가장 근접하게 <strong>좋은 코드</strong> 작성이 포커스라서 도움이 될것같아서 신청했다.</p>
<h2 id="1회차-챌린지-목표">1회차 챌린지 목표</h2>
<p>우선, 1회차는 로그인 관련된 로직을 클래스로 묶어서 프론트엔드에서의 객체지향에 대해 고민해보는 회차이다.
보통 프론트엔드 챌린지는 눈에 보이는 컴포넌트를 만드는게 대부분이라, 모듈설계에 자신이 없었다.</p>
<p>그렇다 보니, 어떤 프로젝트를 해도 비동기 처리하는 부분을 설계할때가 가장 어렵게 느껴졌다ㅠ</p>
<p>그래서 이번 챌린지에서는 인터페이스와 클래스, 메서드를 정의하여 지속가능한 설계를 배워보는 것!이 목표였다.
리액트 컴포넌트 관련 레퍼런스는 많지만, 이런 레퍼런스는 적용하기 어려워서 냉큼 신청함.</p>
<p>그리고 챌린지 종료 후, 상위 참가자에게 코드리뷰를 통해 피드백을 준다고 하는데
내가 되지 않더라고 코드리뷰를 보고 공부할 수 있기 때문에 기대가 된다....</p>
<h2 id="적용하려-노력한-부분코드리뷰-후-내용추가예정">적용하려 노력한 부분(+코드리뷰 후 내용추가예정)</h2>
<p>꽤 추상적이어서 막막했다 ㅠㅠ
그래서 챌린지 호스트님이 주신 힌트로 몇가지 컨셉을 익히고 설계했다.</p>
<h3 id="1-solid">1. SOLID</h3>
<ol>
<li>AuthService, UserService 클래스의 공통적인 부분 추상화.</li>
<li>모듈(파일, 클래스, 메서드 등)이 하나의 역할만 하도록.</li>
<li>범용적인 기능 (ex. CRUD)를 추가했을 때, 구조에 문제가 없는지 파악, 개선</li>
</ol>
<p>이런 힌트를 얻었다.</p>
<p>레퍼런스로 주신 컴포넌트의 응집도 관련 영상도 보고,
프론트엔드에서의 <strong>SOILD 원칙</strong> 을 찾아보며 공부했다.</p>
<p>내가 한 작업은, 라이브러리(<code>js-cookie</code>, <code>axios</code>)와 강한 의존성을 띄는 부분을 없애기 위해 중개모듈을 추가하여 의존성을 제거하기!
이 부분은 SOLID의 Liskov 역전 원칙을 적용했다.</p>
<p>이렇게 하니, 라이브러리를 사용하는 부분은 중계모듈이 담당하도록 역할을 분리하게 되었다.
자연스럽게 SOLID의 Single Responsibility 원칙이 해결되었다.</p>
<p>그리고 해당 라이브러리는 내가 생각한 Service Layer의 역할인 &quot;서버와 Fetch하는 비동기를 담당&quot;에서 계속 쓰일 것이라고 생각했다.
따라서 Service Layer에서 계속 사용할 부모 클래스를 만들어서 이 라이브러리 중계모듈을 연결해주었다.
부모 클래스를 상속받은 중계모듈을 사용하여, 해당클래스의 역할이 아닌 부분은 역할 위임을 통해 해결했다,</p>
<p>그런데 조금 염려되는 부분은 SOLID의 Interface 분리 원칙에 위배되는 것은 아닐지? 하는 점이다.
쿠키가 사용되지 않는 모듈도 있을 것같아서.. </p>
<p>그래서 코드리뷰에서 제일 궁금한 점은 <strong>부모모듈을 어떻게 설계하는지</strong> 이다.</p>
<h3 id="2-클래스와-메서드-정적타입-인터페이스-추가하기">2. 클래스와 메서드 정적타입, 인터페이스 추가하기</h3>
<p>일단, 각 fetch담당하는 함수마다 필요한 파라미터가 다르고 리턴값도 달랐기 떄문에 정의가 필요했다.
컴포넌트에서 필요한 값을 적절히 주고 받기 위해서 함수의 파라미터와 axios 리턴값에 대한 타입정의를 추가했다.</p>
<p>그리고 클래스의 interface를 추가하여, 해당 클래스가 어떤 역할을 하는지 보여주었다.</p>
<h3 id="3-커스텀-훅-userequest-제작">3. 커스텀 훅 useRequest 제작</h3>
<p>useQuery를 사용할 중계모듈이라고 생각하고 제작했다.</p>
<p>컴포넌트에서 직접적으로 리액트 쿼리를 불러오지 않도록, useRequest를 인터페이스로 사용하도록 했다.</p>
<p>아쉬운 점은, 서비스 레이어에 있는 각 메서들을 컴포넌트에서 직접 불러와야 하는 점이다.
useRequest 내에서 미리 불러오는 방식으로 해보려고 했으나, 그럴려면 모드 메서드에 대한 타입을 제네릭으로 지정해주는 방식으로 해야할 것 같다.
확신이 없었고 시간이 부족해서 충분히 고민해보지 못한 것 같다.</p>
<p>이 부분도 코드리뷰에서 궁금한 점 중 하나이다.</p>
<hr>
<p>사실 객체지향에 대해 잘 모르고, 리액트 쿼리도 익숙치않아서 좋은 코드인지 잘 모르겠다.ㅜㅜ
아마 좋은 코드에 대한 인풋이 많이 부족해서 라고 생각한다.</p>
<p>하지만 이 시간이 없었다면 좋은 코드와 프론트엔드에서의 객체지향에 대해 생각해보지 못했을 것이다.
이 챌린지를 계기로 나의 코드 퀄리티에 대해 반성(...)하게 되었다.</p>
<p><del>리팩토링과 좋은 설계에 대해 공부하기로 함(인강 결제 완..)</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 렌더링 최적화, 리팩토링으로 성능향상🎯 시켜보자]]></title>
            <link>https://velog.io/@awesome-hong/React-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%EC%9C%BC%EB%A1%9C-%EC%84%B1%EB%8A%A5%ED%96%A5%EC%83%81-%EC%8B%9C%EC%BC%9C%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@awesome-hong/React-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%EC%9C%BC%EB%A1%9C-%EC%84%B1%EB%8A%A5%ED%96%A5%EC%83%81-%EC%8B%9C%EC%BC%9C%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 26 Jun 2022 18:21:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/0fc17120-724d-4c2c-a9fc-d9ddbd86536b/image.png" alt=""></p>
<p>프로젝트로 만든 &quot;칭찬이 필요해&quot; 웹사이트의 첫 페이지에서 여러 사람의 글을 모아볼 수 있다. 
<img src="https://velog.velcdn.com/images/awesome-hong/post/76ee54ea-3329-4580-9d9f-6b5c1e1a3635/image.png" alt=""></p>
<p>그리고 이렇게 맘에 드는 기록에 칭찬버튼을 누를 수 있다.
<img src="https://velog.velcdn.com/images/awesome-hong/post/2d61ab2b-a875-4282-9378-7b3fcb7f0b98/image.gif" alt=""></p>
<p>그런데 한번 누를때마다 너무 느려서 Profiler를 통해 성능을 측정해보았다.</p>
<h1 id="리팩토링-전-성능측정">리팩토링 전 성능측정</h1>
<p>크롬 익스텐션인 Profiler를 사용하여 측정한다.</p>
<p>새로운 칭찬을 반영하는데 총 75.7ms가 걸렸다....</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/e70c64bf-0a3e-428b-9ab8-e511927069a4/image.png" alt="">
부드러운 웹 어플리케이션을 위해서 60fps가 권장되는데, 
이는 한 동작에 약 16~17ms가 소요되어야 한다.
지금은... 느리게 느껴질만하다.</p>
<p>그리고 칭찬을 교체하는데에는 무려 139.4ms가 소요되었다....
<img src="https://velog.velcdn.com/images/awesome-hong/post/1a00374a-fb21-4709-a77a-12333d7c5c85/image.png" alt=""></p>
<p>프로파일러를 보면, 
하나의 버튼을 누르는데도,
버튼이 눌리지 않은 요소들까지 <strong>모두 리렌더링</strong> 시키는 것이 근본적인 문제이다.</p>
<p><strong>버튼을 누르지 않은 요소들은 업데이트되지 않아도 되므로, 
<code>React Memo</code>를 사용하여 리렌더링에 조건을 걸어주는 방식으로 최적화</strong> 를 진행할 것이다.</p>
<p>그리고 이렇게 최적화하기에 앞서, 
컴포넌트를 제대로 설계하지 못하였음을 깨닫고....
유지보수에 편하도록 리팩토링을 먼저 진행하기로 했다.</p>
<h1 id="리팩토링-시작">리팩토링 시작</h1>
<p>현재 컴포넌트는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/79ee8916-2fe3-42b2-96f0-2a120190e94f/image.png" alt=""></p>
<p><code>FeedPublic</code> 컴포넌트에서 map 함수를 통해 순회하며 <code>FeedItem</code> 컴포넌트를 생성해주는 방식이다.</p>
<p>우선 <strong>하나로 뭉쳐져있는 컴포넌트를 역할별로 분리</strong>하여 로직을 깔끔히 하고,
최적화를 하기로 했다.</p>
<p><strong>응집도있는 컴포넌트</strong>에 관한 영상을 봐서, 
한 컴포넌트는 한가지의 역할만 하도록 리팩토링해보고 싶었다. ^-^</p>
<h2 id="컴포넌트-분리---한-컴포넌트는-한가지-역할만">컴포넌트 분리 - 한 컴포넌트는 한가지 역할만!</h2>
<h3 id="1-컴포넌트가-여러역할을-한다면">1. 컴포넌트가 여러역할을 한다면?</h3>
<p>기존의 <code>FeedItem</code> 컴포넌트는 보여줘야할 정보를 props로 받아서 한번에 보여주었다.</p>
<p><strong>하나의 컴포넌트가 너무 다양한 정보를 다루고 있기 때문에 
새로운 기능의 추가가 어렵고, 에러 발생 시 추적이 쉽지 않았다. 
코드가 읽기 어렵게 되어 가독성이 좋지않다.</strong></p>
<h3 id="2-분리하기">2. 분리하기</h3>
<p>따라서 다음과 같이 <strong>작은 컴포넌트로 분리하여 하나의 역할</strong> 을 담당하도록 했다.
<img src="https://velog.velcdn.com/images/awesome-hong/post/d8a51085-5aaa-4609-bba4-4cc82fc6129a/image.png" alt="">
왼쪽부터 차례로</p>
<ol>
<li><p><code>goal</code> 데이터를 받아 <code>goal.name</code>, <code>goal.color</code>를 보여주는 컴포넌트</p>
</li>
<li><p><code>task</code> 데이터를 받아 <code>task.title</code>을 보여주는 컴포넌트</p>
</li>
<li><p><code>compliments</code> 데이터, <code>onEmojiClick</code> 핸들러, 클릭된 타입을 알려주는<code>clickedType</code>을 받아 보여주는 컴포넌트</p>
</li>
<li><p><code>task</code> 데이터를 받아 <code>task.createAt</code>을 보여주는 컴포넌트
이렇게 나누었다.  </p>
</li>
</ol>
<h3 id="3-더-작게-쪼갤-필요가-있다">3. 더 작게 쪼갤 필요가 있다.</h3>
<p>여기서 가장 중요한 컴포넌트는 <code>3번 FeedItemCompliment</code> 컴포넌트이다. </p>
<p>칭찬버튼이 눌릴때마다 직접적으로 인터렉션을 담당하고, 
빠르게 변경사항을 반영해야 하기 때문이다.</p>
<p>따라서 그림과 같이  컴포넌트를 <strong>더 작게</strong> 나누어 리팩토링한다.
<img src="https://velog.velcdn.com/images/awesome-hong/post/f9afeccb-160c-4a66-88db-f9097edf01f7/image.png" alt=""></p>
<ol>
<li><p>추가 기능 확장을 쉽게 하도록, 개수를 보여주는 부분을 별도의 <code>ComplimentCounter</code> 컴포넌트로 관리한다.</p>
</li>
<li><p>그리고 내부적으로 4개의 버튼이 존재하기 때문에, 버튼을 <code>ComplimentButton</code> 컴포넌트로 만들어 관리한다.
버튼 타입 배열 [👏🏻 , 🎉 ,❤️ ,👍🏻] 을 map으로 순회하며 <code>ComplimentButton</code> 컴포넌트를 생성한다. 
이렇게 하면, <strong>새로운 버튼 타입이 추가되거나 수정되어도 버튼 타입 배열만 수정해주면 쉽게 반영할 수 있다.</strong></p>
</li>
</ol>
<h3 id="4-응집도있는-버튼-컴포넌트">4. 응집도있는 버튼 컴포넌트</h3>
<p>이 코드는 <code>ComplimentButton</code> 컴포넌트의 props이다.</p>
<pre><code class="language-ts">type ComplimentButtonProps = {
  type: ComplimentData[&quot;type&quot;],
  clicked: boolean,
  onClick: (type: ComplimentData[&quot;type&quot;]) =&gt; void;
}</code></pre>
<ol>
<li><code>type</code><ul>
<li>자신이 표현해야 할 타입</li>
<li>👏🏻 , 🎉 ,❤️ ,👍🏻 중 하나를 의미</li>
</ul>
</li>
<li><code>clicked</code><ul>
<li>클릭되었는지 아닌지 여부</li>
<li>버튼 내부적으로 어떻게 표시할지는 알아서 하고, 부모 컴포넌트는 클릭 여부만 전달한다.</li>
<li>상위 컴포넌트에서 useMemo를 사용하여 계산하여 전달한다.</li>
</ul>
</li>
<li><code>onClick</code><ul>
<li>버튼을 눌렀을 때 동작할 콜백함수</li>
</ul>
</li>
</ol>
<p>이렇게 하면 버튼 역할만! 하는 컴포넌트가 된다.</p>
<ol>
<li>버튼컴포넌트가 <code>clicked</code>에 대해 내부적으로 알아서 표시</li>
<li><code>onClick</code> 핸들러에 자신에게 할당된 <code>type</code>을 전달</li>
</ol>
<p><strong>즉, 주어진 데이터 사이의 연관관계가 강한 응집도있는 컴포넌트가 되었다!</strong></p>
<h2 id="react-memo-적용---리렌더링이-필요없는-컴포넌트를-골라내자">React memo 적용 - 리렌더링이 필요없는 컴포넌트를 골라내자!</h2>
<h3 id="1-memo란">1. memo란?</h3>
<p>리액트는 렌더링한 뒤, 이전 렌더결과와 비교하여 DOM업데이트를 결정한다. 
이전 렌더결과와 다르다면 DOM업데이트를 진행한다.</p>
<p>리액트는 가상돔을 가지고 있으므로 빠르다. 
하지만 특정 상황에서 이 속도를 더 높일 수 있다!</p>
<p><strong><code>React memo</code>를 사용하면 된다.</strong></p>
<p>memo는 기존 컴포넌트를 한번더 감싸는 HOC패턴으로, 
memo가 적용된 컴포넌트의 props가 변경되지 않는다면, 다음 렌더링에 메모이징된 렌더결과를 그대로 사용한다.</p>
<p>memo를 적용하면 다음과 같이 동작한다.</p>
<blockquote>
</blockquote>
<ol>
<li>이전 props와 비교를 위해 비교함수를 실행한다.</li>
<li>비교함수에서 true를 반환한다면, 메모이징된 이전 렌더결과를 재사용한다.✨</li>
<li>비교함수에서 false를 반환한다면, 새로 렌더링하고 이전 렌더결과와 다음 렌더결과를 비교하여 DOM업데이트를 결정한다.</li>
</ol>
<p><strong>메모이징된 렌더결과를 재사용함</strong>으로서 리액트에서 리렌더링할 때, 
<strong>가상돔에서 달라진 부분을 비교할 필요가 없으므로 속도를 높일 수 있다.</strong></p>
<h3 id="2-memo를-사용하는-경우">2. memo를 사용하는 경우</h3>
<p>memo를 효과적으로 적용할 수 있는 경우는 
*<em>같은 props로 자주 렌더링이 일어나는 컴포넌트로 예상될 때 *</em>이다.</p>
<ul>
<li>부모컴포넌트에 의해 자식컴포넌트가 같은 props로 자주 리렌더링 되는 경우</li>
<li>혹은 무겁고 비용이 큰 연산이 있는 경우</li>
</ul>
<p>다만, 메모를 적용해도 내부 state가 변경되면 당연히 리렌더링되므로 
내부 state가 자주 변경되는 경우는 memo가 필요없을 가능성이 높다.</p>
<p>주의할 점은 props를 비교할 때 <strong>얕은(shallow)비교</strong> 를 한다는 점이다.</p>
<p>따라서 객체형태로 주어지는 props에서 실제값을 비교하길 원한다면, 
memo의 두번째 인자로 비교 함수를 직접 작성하여 넣어줄 수 있다. 
(아래에 나의 코드예시가 있다.)</p>
<h3 id="3-적절한-컴포넌트에-memo를-적용하기">3. 적절한 컴포넌트에 memo를 적용하기!</h3>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/9e54b1c6-e428-4e98-aa74-92cd6e32a751/image.png" alt=""></p>
<ol>
<li><p>Header
우선 props가 존재하지 않는다. 
오른쪽 사이드바를 열고 닫는 용도의 boolean값 하나만을 state로 가지고 있다. 
따라서 사이드바를 열고닫는 동작이 아니라면 memo를 적용하여 메모이징을 적용하도록 한다.</p>
</li>
<li><p>feedItem
칭찬 버튼이 눌린 컴포넌트만 제외하고 나머지 전부 memo로 재사용한다.
렌더링시간을 줄이면, 버튼을 눌렀다는 표시를 더 빠르게 적용시킬 수 있다.
(gif로 설명: ❤️를 클릭해서, 표시가 👍🏻에서 ❤️로 바뀜)
<img src="https://velog.velcdn.com/images/awesome-hong/post/2d61ab2b-a875-4282-9378-7b3fcb7f0b98/image.gif" alt="">
중요한 핵심 로직은
칭찬버튼을 누르면 <strong><code>칭찬 type</code>이 변경된다</strong> 는 것 이다.</p>
</li>
</ol>
<ul>
<li>새로 칭찬하면?
<code>undefined</code>에서 👏🏻 , 🎉 ,❤️ ,👍🏻 중 하나로 type이 바뀜</li>
<li>삭제하면? 
👏🏻 , 🎉 ,❤️ ,👍🏻 중 하나에서  <code>undefined</code>로 바뀜</li>
<li>교체하면? 
👏🏻 , 🎉 ,❤️ ,👍🏻 중 하나에서 다른 type으로 바뀜</li>
</ul>
<p>이렇게 DB에 저장된 칭찬 타입이 변경될 것이다.</p>
<p>이 점을 활용하여, memo의 두번째 비교함수에 적용했다.</p>
<pre><code class="language-tsx">//feedItem.tsx
const FeedItem = ()=&gt;{
    //component..
}

FeedItem.displayName=&quot;FeedItem&quot;

const areEqual = (prevProps:FeedItemProps,nextProps:FeedItemProps)=&gt;{
  // 1.현재 로그인 사용자가 같다
  if(prevProps.loggedInUserId !== nextProps.loggedInUserId) return false;

  const {loggedInUserId}=nextProps;

  // 2.현재 로그인 사용자가 눌렀던 칭찬버튼 `type`이 같다
  if(prevProps.task.compliments.find(compliment =&gt; compliment.author === loggedInUserId)?.type === nextProps.task.compliments.find(compliment =&gt; compliment.author === loggedInUserId)?.type){
    return true;
  }
  return false;
}

export default memo(FeedItem,areEqual)

//index.ts
export {default as FeedItem} from &quot;./feedItem.tsx&quot;;</code></pre>
<p>이 예시는 내가 적용한 코드이다.</p>
<ol>
<li>현재 로그인 사용자가 같고,</li>
<li>현재 로그인 사용자가 눌렀던 칭찬버튼 <code>type</code>이 같으면,</li>
</ol>
<p>true를 반환한다! 
즉, 새로 렌더링할 필요가 없다.</p>
<h2 id="전후-성능-비교-🎉">전,후 성능 비교 🎉</h2>
<p>메모이징이 적용된 모습을 Profiler에서 확인할 수 있다.
보는것처럼 memo가 의도한대로 적용되었다.
<img src="https://velog.velcdn.com/images/awesome-hong/post/1e381a2d-dc44-4727-a078-c75f952ba76a/image.png" alt=""></p>
<p>버튼이 눌린 컴포넌트만 재사용하지 않고,
나머지 컴포넌트는 memo가 적용되어 리렌더링하지 않고 있다!</p>
<p>Header, FeedItem 두 컴포넌트에 memo를 적용한 결과, 
동일한 작업에 대해 렌더링 시간이 <strong>18.4ms</strong>로 줄었다.</p>
<p>리팩토링 전 139ms에 비해 120.6ms가 줄었다.
렌더링 시간이 *<em>86.762% 감소 🎉 *</em>한 것을 알 수 있다.</p>
<h2 id="조금만-더-추가-리팩토링">조금만 더...? 추가 리팩토링</h2>
<p>약 87% 렌더링 시간 감소! 
정말 뿌듯하다.
하지만 보다보니 영 아쉬운점이 보인다.</p>
<p>전체 렌더링시간은 확연히 줄었고, 그만큼 빠르게 리렌더링된다.</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/9d941afc-3ead-4d4e-b0d3-26e4292a3104/image.gif" alt=""></p>
<p>(gif: 👏🏻누르고, 딜레이 후에 👏🏻에 표시되는 모습....)
하지만 서버와 통신 후 반영이 되는거라
통신하는 시간만큼 화면에 보여지는데에 딜레이가 생기게 된다.</p>
<p> <img src="https://velog.velcdn.com/images/awesome-hong/post/db8514f1-3988-4995-8261-1186dadf7821/image.png" alt="">
Profiler를 보고 계산해보니, 대략 클릭하고 <strong>12ms 후</strong>에 변경된 내용이 화면에 그려진다.</p>
<p>통신 상태에 따라서 더~ 느려질 수도 있을 것 이다.</p>
<p><strong>통신이 어떻든지간에, 
유저입장에서 누른 버튼이 당장❗️ 반영되었다고 느껴지도록 만들고 싶다.</strong></p>
<h3 id="렌더링시간보다-중요한건-사용자-경험">렌더링시간보다 중요한건 사용자 경험!</h3>
<p>useState에 당장 변경된 내용을 저장하여 이 내용을 반영시키도록 리팩토링을 했다.</p>
<p>렌더링시간이 조금 늘어나더라도, 
유저에게 바로 변화를 보여주는 방식을 택했다!</p>
<pre><code class="language-tsx">    &lt;ComplimentButton 
          key={type}
          type={type}
          clicked={clickedComplimentType ? clickedComplimentType===type : false}
          onClick={handleClickedEmoji}&gt;    
    &lt;/ComplimentButton&gt;</code></pre>
<p>각 칭찬 버튼에서는 현재 클릭된 <code>clickedComplimentType</code> 을 가지고 클릭된지 아닌지를 판단한다.</p>
<p>이 점을 이용하여, </p>
<ol>
<li>버튼의 상위 컴포넌트에서 <code>clickedComplimentType</code>을 <code>useState</code>로 저장하여 사용한다. 
counter 컴포넌트에도 똑같이 적용하려고 <code>complimentsCount</code>도 <code>useState</code>로 저장한다.<pre><code class="language-tsx">const clicked = useMemo(()=&gt;{
 return compliments.find(compliment =&gt; compliment.author === loggedInUserId);
},[compliments,loggedInUserId])
</code></pre>
</li>
</ol>
<p>const [clickedComplimentType, setClickedComplimentType] = useState(clicked?.type);
const [complimentsCount, setComplimentsCount] = useState(compliments.length);</p>
<pre><code>
2. 클릭하자마자 클릭된 버튼타입을 `clickedComplimentType`에 저장하고 이 값을 버튼에게 전달한다.
그리고 삭제,추가 될 때 `complimentsCount`도 변경해준다!
```tsx

  const handleClickedEmoji = useCallback((emoji:ComplimentData[&quot;type&quot;])=&gt;{
    if(!loggedInUserId) {
      handleSnackbarShow();
      return;
    }

    if(clickedComplimentType === undefined){
      // 새로 생성할 때
      setComplimentsCount((state)=&gt;state+1);
      handleCreate(emoji);
    }else if(clickedComplimentType !== emoji){
      // 지우고 새로만든다. 즉, 교체할 때
      handleDelete();
      handleCreate(emoji);
    }else{
      // 삭제할 때
      setComplimentsCount((state)=&gt;state-1);
      handleDelete();
    }

  },[clickedComplimentType,handleSnackbarShow,loggedInUserId,handleDelete,handleCreate]) </code></pre><p>지금 누른 버튼타입을 일단 화면에 보여준 후에, 
디비에 저장된 내용을 가져와 뒤늦게 반영하는 방식이다.
<img src="https://velog.velcdn.com/images/awesome-hong/post/6c396227-ee4f-453f-b41c-815ab4ce5eb7/image.gif" alt=""></p>
<p>(gif: 누르자마자 표시되는 모습!! 카운트도 바로 반영!!! )</p>
<p>이렇게 하면,
유저입장에서는 당장❗️ 반영되는 것 같지만
사실 실제 저장된 데이터는 여전히 조금 딜레이된 후에 적용된다.</p>
<p>하지만 성공적으로 저장되었다면 이미 적용된 화면에 변화가 없을것이다!! 후후후</p>
<p>저장에 실패했다면 다시 누르기 전 화면으로 돌아갈 것이다.</p>
<h2 id="🎯-최종-렌더링-시간-비교와-회고">🎯 최종 렌더링 시간 비교와 회고</h2>
<p>이런 방식으로 바꿈으로서,
기존의 memo를 사용하여 18ms로 줄였던 렌더링시간이 다시 22ms로 늘어났다.
(그래도 84.172% 감소이다! 지독했던 이전 속도 🤣)
하지만 숫자보다 중요한건 UX라고 들었다.</p>
<p>기존방식은 렌더링 시간은 더 짧지만, 유저가 바뀐 화면을 보기까지 약 12ms가 걸렸다. 
하지만 지금 방식은 약 0.5ms만에 볼 수 있다.
누르자마자 변경되었다고 느껴진다.</p>
<p><strong>프론트엔드 개발자로서 숫자보다 유저의 더 나은 경험이 중요하다❗️</strong></p>
<p>따라서 렌더링시간이 좀 늘어나고, 컴포넌트가 조금 복잡해졌지만
유저는 매우 빠른 사이트라고 경험할 것이다!</p>
<p>추가 리팩토링을 안했다면, 
숫자에 집중하여 얼마나 전체 렌더링 시간을 감소시켰는지만 집착(?)했을 것이다.</p>
<p>관점을 조금 바꿔 유저경험을 위주로 생각하니, 이런 좋은 결과가 나왔
다고 생각한다.
뿌듯하다!</p>
<hr>
<p>쓰다보니 글이 예상보다 너무 길어졌다....🙃...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HTTP] Options 요청은 뭘까? ]]></title>
            <link>https://velog.io/@awesome-hong/HTTP-Options-%EC%9A%94%EC%B2%AD%EC%9D%80-%EB%AD%98%EA%B9%8C</link>
            <guid>https://velog.io/@awesome-hong/HTTP-Options-%EC%9A%94%EC%B2%AD%EC%9D%80-%EB%AD%98%EA%B9%8C</guid>
            <pubDate>Sun, 26 Jun 2022 10:28:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/2b0a4c90-f8eb-4c27-a1da-a110649ce9d7/image.png" alt=""></p>
<p>프로젝트에서 API서버를 따로 생성하여 프론트엔드와 통신했다.
<img src="https://velog.velcdn.com/images/awesome-hong/post/a89aca3c-170f-43e7-8ab3-6e206296537f/image.png" alt="">
그런데 요청한 메소드 이전에 <code>Request Method: OPTIONS</code> 요청이 네트워크탭에 있는 것을 확인했다.
<img src="https://velog.velcdn.com/images/awesome-hong/post/02b25a92-b56f-4c16-9c0b-b1a60f6953bf/image.png" alt=""></p>
<p>통신 Type도 <code>XMLHttpRequest (XHR)</code> 가 아닌 <code>preflight</code>으로 되어있다.</p>
<p>확인해보면 응답값은 없는데, 뭐하는거지?</p>
<h2 id="preflight-request-method-options">Preflight: Request Method: OPTIONS</h2>
<p>preflight인 OPTIONS 요청은 서버와 브라우저가 통신하기 위한 통신 옵션을 확인하기 위해 사용한다.</p>
<p>서버가 어떤 method, header, content type을 지원하는지를 알 수 있다.</p>
<p>브라우저가 요청할 메서드와 헤더를 허용하는지 미리 확인한 후, 서버가 지원할 경우에 통신한다. 좀 더 효율적으로 통신할 수 있다.</p>
<p>터미널에서 확인해볼 수 있다.</p>
<pre><code>curl -X OPTIONS https://API서버 -i</code></pre><p>라는 요청을 서버에 보내면 다음과 같은 응답이 나온다.</p>
<pre><code>HTTP/1.1 204 No Content
...
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
...</code></pre><p>서버에서 허용하는 메소드를 알수있다.</p>
<p>CORS 동작에서도 Options요청이 사용되는데, 먼저 Options요청을 보낸 뒤 응답 정보를 사용 가능한지 파악한다. 서버의 &quot;허가&quot;가 떨어지면 실제 요청을 보내도록 요구하고, 또한 서버는 클라이언트에게 요청에 &quot;인증정보&quot;(쿠키, HTTP 인증)를 함께 보내야 한다고 알려줄 수도 있습니다.
허용되지 않는 요청의 경우, <code>405(Method Not Allowed)</code> 에러를 발생시키고 실제 요청은 전송하지 않게된다.</p>
<h3 id="발생-조건">발생 조건</h3>
<ol>
<li><code>GET</code>, <code>HEAD</code>, <code>POST</code> 요청이 아닌 경우</li>
<li>Custom HTTP Header가 존재하는 경우 <ul>
<li>유저 에이전트가 자동으로 설정 한 헤더 (예를들어, Connection, User-Agent (en-US), Fetch 명세에서 “forbidden header name”으로 정의한 헤더)외에, 수동으로 설정할 수 있는 헤더는 오직 Fetch 명세에서 “CORS-safelisted request-header”로 정의한 헤더(Accept, Accept-Language, Content-Language, Content-Type) 뿐입니다.</li>
</ul>
</li>
<li><code>Content-Type</code>이 <code>application/x-www-form-urlencoded</code>, <code>multipart/form-data</code>, <code>text/plain</code> 이 아닌 경우</li>
</ol>
<p>preflight를 발생하지 않는다면, simple requests를 요청할 수 있다.</p>
<h3 id="options-요청-없애기">Options 요청 없애기?</h3>
<p>우선 preflight은 보안을 위한 절차이지만, 경우에 따라 응답속도가 중요하다면 발생하지 않도록 하는 것이 좋다.</p>
<ol>
<li>CORS상황이 되지 않도록 웹서버와 동일한 오리진을 사용한다.<ul>
<li>보통 API서버와 프론트엔드가 분리된 경우가 많다. 이런 경우, 브라우저에서 보내는 요청을 웹서버와 동일한 서버에서 받아주는 중간 서버를 두어서 실제 API서버로 전달해주면 된다.</li>
</ul>
</li>
<li>preflight 발생조건을 없앤다.<ul>
<li>하지만 나의 경우, GET/POST 외의 PATCH, DELETE 메서드도 사용할 뿐더러 Content-type이 <code>application/json</code>를 사용하기 때문에... 발생하게 된다.</li>
<li>또한 인증관련해서 헤더에 추가해야할게 있는 경우에도 발생할 수 밖에 없다.</li>
</ul>
</li>
</ol>
<hr>
<p>참고 : <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/CORS">MDN CORS</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[응집도있는 컴포넌트 설계란?]]></title>
            <link>https://velog.io/@awesome-hong/%EC%9D%91%EC%A7%91%EB%8F%84%EC%9E%88%EB%8A%94-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84%EB%9E%80</link>
            <guid>https://velog.io/@awesome-hong/%EC%9D%91%EC%A7%91%EB%8F%84%EC%9E%88%EB%8A%94-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84%EB%9E%80</guid>
            <pubDate>Tue, 21 Jun 2022 19:36:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/aed9420c-05ca-43df-beae-4dbf7cf5745c/image.png" alt="">
<img src="https://velog.velcdn.com/images/awesome-hong/post/2dc5421d-2e24-41b5-8e93-216b11321300/image.png" alt=""></p>
<p><a href="https://www.youtube.com/watch?v=aSAGOH2u2rs">[B3] 우리는 응집도에 대하여 이야기할 필요가 있다</a></p>
<p>넘블 챌린지 중, 챌린지 호스트님께서 방향을 잡는데 좋은 영상을 추천해주셨다.</p>
<p>그리고 내가 리액트로 개발한 페이지의 성능측정을 해보았는데, 컴포넌트가 제대로 나눠져 있지 않아서 성능저하가 일어나고 있었다. 
성능개선을 위해 리팩토링이 필요했다.
좋은 공부가 될 것 같군!</p>
<p>추천해주신 넘블 호스트님과 좋은 영상을 제공해주신 마켓컬리에게 감사합니다 👍🏻</p>
<hr>
<h2 id="응집도를-나누는-기준">응집도를 나누는 기준</h2>
<p>상위의 기준을 가질수록 좋은 응집도</p>
<ol>
<li><code>기능적 응집도</code><ul>
<li>모듈이 하나의 기능만 하고, 작은단위로 기능할 때. 단일책임원칙과 비슷.</li>
</ul>
</li>
<li><code>순차적 응집도</code><ul>
<li>루틴이 특정순서로 수행되어야하고, 단계마다 정보를 공유하며, 동시에 수행되면 완전한 기능을 제공하지 못할 때. 하나의 로직을 분리하여 하나의 일만 수행하도록 하면 기능적 응집도를 갖게할 수 있다.</li>
</ul>
</li>
<li><code>통신적 응집도</code><ul>
<li>각 모듈이 같은 데이터를 사용하지만, 서로 관련성이 없을 때. </li>
</ul>
</li>
<li><code>시간적 응집도</code><ul>
<li>여러 연산이 동시에 수행되어야 해서 하나의 로직으로 결합될 때. </li>
</ul>
</li>
<li><code>절차적 응집도</code><ul>
<li>특정 순서대로 실행.</li>
</ul>
</li>
<li><code>논리적 응집도</code><ul>
<li>여러 기능을 하나의 루틴에서 수행할 때, 루틴에 전달되는 조건에 따라 수행하는 기능이 다른 경우. 이름과 번호를 받는 컴포넌트에서, 번호만 -(dash)를 지우는 작업이 필요하다면 플래그를 사용해서 한 함수로 사용하지 않고, 함수를 분리해야한다. 예측이 쉽고, 한 함수가 하나의 일을 하도록</li>
</ul>
</li>
<li><code>우연적 응집도</code><ul>
<li>놓을곳을 몰라서 util에 마구 넣어진 경우.. 반드시 피해야 함.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="컴포넌트-예시로-알아보기">컴포넌트 예시로 알아보기</h2>
<h3 id="페이지네이션-컴포넌트의-응집도를-높여보자">페이지네이션 컴포넌트의 응집도를 높여보자!</h3>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/e8916015-0537-4270-8806-e670e163405a/image.png" alt=""></p>
<p>페이지를 클릭하면 <code>onChangePage</code>이벤트가 발생한다.</p>
<p>페이지 당 갯수를 변경하면 <code>onChangeSize</code>이벤트가 발생한다.</p>
<hr>
<h3 id="기능별로-분리해보자">기능별로 분리해보자</h3>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/5b312134-c99b-4b9a-92ae-6d4ec95fbcf4/image.png" alt=""></p>
<ul>
<li><strong>페이지네이션 <code>정보를 보여주는</code> 컴포넌트</strong> <strong>Pagination</strong></li>
<li><strong>페이지 당 <code>개수를 변경</code>하는 컴포넌트 PagingSize</strong></li>
</ul>
<p>각 컴포넌트가 하나의 일만 하고있다.</p>
<p>데이터와 메서드가 끈끈하게 연결되어있다.</p>
<p>→ <strong>높은 응집도를 가진 컴포넌트가 될 것이다.</strong></p>
<hr>
<h3 id="pagingsize-컴포넌트에서-onchangesize를-실행한-뒤-현재-페이지가-1로-초기화되어야한다면"><strong>PagingSize 컴포넌트</strong>에서 <code>onChangeSize</code>를 실행한 뒤, 현재 페이지가 1로 초기화되어야한다면?</h3>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/93e60072-8acf-4fb9-be9f-6676626c8cf3/image.png" alt=""></p>
<p>그림처럼 onChangeSize 이벤트가 page에도 의존하므로, 외부의 page에 의존하게 된다.</p>
<p><strong>따라서 PagingSize는 응집도가 낮은 컴포넌트가 된다.</strong> </p>
<hr>
<h3 id="어떻게-개선할-수-있을까">어떻게 개선할 수 있을까?</h3>
<p>그래서 size, page, total을 더 작게 나눌수없다고 판단한다면, paging이라는 데이터로 표현할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/2ecdb19c-f485-45a0-a8dd-acf9f8dbb9f4/image.png" alt=""></p>
<p>이제 Pagination컴포넌트는 paging 데이터를 받고, 페이징이 변경되었을 때 onChange가 발생한다.</p>
<p><strong>데이터가 메서드가 밀접하게 응집되어있으므로 응집도가 높은 컴포넌트이다.</strong></p>
<hr>
<h3 id="데이터와-메서드가-연관이-있다고-해서-응집도가-높은-컴포넌트일까">데이터와 메서드가 연관이 있다고 해서 응집도가 높은 컴포넌트일까?</h3>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/8ff25499-0baf-4527-9144-2dcae650e314/image.png" alt=""></p>
<p>새로고침을 하더라도 이전에 선택한 페이지를 기억하기 위해서 Pagination정보를 수정할 때마다 URL 쿼리를 수정하는 기능을 추가하려고 한다.</p>
<p>onChange가 발생할 때, 브라우저의 location API를 사용하여 쿼리를 수정한다.</p>
<ul>
<li>페이지를 바꾸면? onChange함수에서 paging.page를 변경, 쿼리 수정</li>
<li>사이즈를 바꾸면? onChange함수에서 paging.size를 바꾸고 page를 1로 변경, 쿼리 수정</li>
</ul>
<p>그럼 이 때의 의문.</p>
<h4 id="이-컴포넌트의-목적이-뭔가">이 컴포넌트의 목적이 뭔가?</h4>
<ol>
<li>페이지네이션을 수정하는 것</li>
<li>URL 쿼리를 수정하는 것</li>
</ol>
<p>데이터와 메서드의 연관관계를 보면 응집도가 높다고 할 수 있다.</p>
<p><strong>하지만 목적이 두 가지이기 때문에 기능적 응집도가 낮아지게 된다.</strong></p>
<p>높은 응집도의 컴포넌트를 만들려면, 컴포넌트의 목적을 명확히 하고 이 목적을 위해서 데이터와 메서드가 똘똘 뭉치게 해야한다.</p>
<hr>
<h3 id="왜-높은-응집도가-중요할까">왜 높은 응집도가 중요할까?</h3>
<ol>
<li><p>이해하기 쉽다.
필요한 데이터를 한번에 파악하여 이해하기 쉬워 일처리를 빠르게 한다.</p>
</li>
<li><p>의도파악이 쉽다.
PR 목적에 맞지않는 commit이 있을 때. 이걸 왜 했지? 싶음. 그리고 PR을 분리해야 나중에 코드리뷰 후 수정하기도 수월하다.</p>
</li>
<li><p>테스트하기 쉽다.
페이지네이션을 담당하는 컴포넌트에서 쿼리를 조작한다면? 쿼리 조작이 어디에서 이루어지는지 추적하기 쉽지않다. 테스트도 URL관련한 테스트를 분산되게 해야한다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/b63d65ea-0930-4d36-a388-d0ae6fad87e2/image.png" alt=""></p>
<hr>
<h3 id="pagination-컴포넌트를-테스트한다면-어떨까">Pagination 컴포넌트를 테스트한다면 어떨까?</h3>
<p>새로운 쿼리를 추가하거나, 페이징 관련 버튼을 추가하면 이 컴포넌트의 로직을 봐가며 수정해야한다. 테스트도 수정해야한다.</p>
<p>즉, 페이징 변경과 쿼리 변경 두가지를 수행하므로 응집도가 낮아진다.</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/475ee24c-eb41-4bc8-a7fb-3cc3cff07f97/image.png" alt=""></p>
<p>컴포넌트 내부에서 쿼리 변경 함수와 페이징 변경 함수를 나눈다고 하더라도, 같은 데이터를 사용하는 통신적 응집도가 존재하게 된다.
동시에 컴포넌트가 두 가지 일을 하므로 기능적 응집도는 낮다.</p>
<p>컴포넌트가 하나의 일만 하도록 URL 쿼리를 갱신하는 로직을 바깥으로 뺀다면? </p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/07f645ad-7ec4-4e02-9a2d-78e62f5f3979/image.png" alt=""></p>
<p>Pagination 컴포넌트는 페이징을 변경하는 일과 페이지를 보여주는 일만 담당하게 된다.</p>
<p>테스트도 간단해졌다. 테스트가 많아지고 복잡해질 수록 응집도에 대해 의심해보는것도 좋다.</p>
<p>대부분의 문제는 내가 만든 코드에 어떤 변경 사항이 일어나거나, 다른 사람이 사용할 떄 일어난다.</p>
<p>다른사람이 내 코드를 볼 때, 이해하고 사용해야 하는데 응집도가 낮으면 사용하기 어려워진다.</p>
<p>테스트 코드를 작성하면, 내 코드를 사용하는 설명서 역할을 하므로 이해하기 쉽게 만들어준다. 그런데 테스트가 아주 많다면, 100장짜리 설명서는 아무도 읽고싶지 않을 것…. 내 코드를 사용하기 어려운건 아닐지..</p>
<p>코드 작성 전, 먼저 테스트를 작성한다면 어떻게 사용할지 미리 고려할 수 있기 때문에 처음부터 사용하기 좋은지 설계할 수 있다.</p>
<p>테스트는 단순히 원하는대로 동작하는 것 뿐 아니라 응집도있는 설계, 사용하기 좋은 코드를 만드는데 도움이 되는 도구로 활용할 수 있다.</p>
<p>하지만 무조건 높은 응집도가 좋은 것은 아니고 목적에 맞게 필요한 만큼만 응집되어 있어야 한다.</p>
<hr>
<h3 id="응집도는-컴포넌트를-나누는-기준이-된다">응집도는 컴포넌트를 나누는 기준이 된다!</h3>
<p>높은 응집도를 설계하는 것도 중요하지만</p>
<p><strong>낮은 응집도를 발견💡 하는 것이 중요하다.</strong></p>
<blockquote>
<p>마치 리팩토링하는 방법은 모두 알지만, 
언제 리팩터링해야할지 모르는 것과 같다.</p>
</blockquote>
<hr>
<h3 id="paging-컴포넌트를-응집도있는-코드로-만들어보자">Paging 컴포넌트를 응집도있는 코드로 만들어보자</h3>
<p>테스트로 검증해야할 내용은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/a6a280f3-3191-4477-8075-cdc7614c0a10/image.png" alt=""></p>
<p>테스트가 늘어간다… 
→ 컴포넌트를 더 작게 나눠야하지 않나? 로 생각해볼 수 있다.</p>
<p>그래서 더 작게 나눠본 컴포넌트는 이렇게 된다.</p>
<p><img src="https://velog.velcdn.com/images/awesome-hong/post/8d883782-7dd7-4bb8-8b56-d99f1296e0d9/image.png" alt=""></p>
<ul>
<li>PagingSizeSelect<ul>
<li>사이즈를 전달받고, 변경된 사이즈를 전달하는 역할</li>
</ul>
</li>
<li>PreviousButton<ul>
<li>클릭했을 때 실행할 <code>onClick</code>과 첫페이지일 때 비활성화하는 <code>disabled</code></li>
<li>비활성화하는 부분은 컴포넌트 내부에서 처리한다. Pagination컴포넌트는 관심없기 때문…!</li>
</ul>
</li>
<li>PageButton<ul>
<li>몇번째 page인지 받고, 선택되었다면 어떻게 표시될지 내부에서 결정하여 보여준다.</li>
<li>클릭되면 클릭된 페이지를 넘겨준다.</li>
</ul>
</li>
<li>NextButton</li>
</ul>
<p>와우~~
각 모듈이 기능이 명확하고, 하나의 일만을 담당하고 있다!
데이터와 메서드도 적절한 응집도를 가지게 되었다.</p>
<hr>
<h2 id="정리">정리</h2>
<ul>
<li><p>응집도는 모듈의 목적을 위해 요소들이 밀접하게 연관되어 있는 것을 말한다.</p>
</li>
<li><p>높은 응집도 모듈은 이해하기 쉽고, 의도파악이 쉽고, 테스트하기 쉽게 만들어 준다.</p>
</li>
<li><p>테스트를 작성하여 응집도가 높은지 낮은지 파악할 수 있으며 응집도에 따라 컴포넌트를 분리할 수 있다.</p>
</li>
</ul>
<hr>
<p>그동안 컴포넌트를 나누는 기준이 없었던 것 같다.
반성....
이런 기준으로 나누는거구나.. 
적당히 재사용할 수 있으면서 시각적으로 나눌 수 있으면 나눴는데 이런 시각에서는 생각하지 못했다.</p>
<p>현재 내 프로젝트에서 리팩토링이 필요한 컴포넌트가 위 예시에서 Pagination과 비슷하다. 언뜻 보기에 페이지네이션 하나의 기능을 담당하지만, 분석해보면 그 안에서도 여러가지 일을 하고있어서 분리가 필요하다.
<del>(물론 분리안하고 짰음..반성ㅠ)</del></p>
<p>내일 리팩토링 하러간닷!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] Tower of Hanoi]]></title>
            <link>https://velog.io/@awesome-hong/JavaScript-Tower-of-Hanoi</link>
            <guid>https://velog.io/@awesome-hong/JavaScript-Tower-of-Hanoi</guid>
            <pubDate>Mon, 20 Jun 2022 11:49:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/80ba6ac6-a097-4ffa-bf44-d8cf45d85950/image.png" alt=""></p>
<p>라이브 코테에서 하노이탑을 구현해보라는 요구를 받았다.</p>
<p>재귀연습에 아주 기초적인 문제인데 생각해보니 한번도 구현해보지 않았던.. 이 점이 실수였다.
항상 기초에 충실하자!</p>
<hr>
<h2 id="solution">Solution</h2>
<p>문제는 워낙 유명하니, <a href="https://programmers.co.kr/learn/courses/30/lessons/12946?language=javascript">여기 프로그래머스 문제</a>에서 확인할 수 있다.
전혀 감이 오지않는다면, <a href="https://www.youtube.com/watch?v=rf6uf3jNjbo">이 유튜브 영상</a>을 추천한다.</p>
<p>가장 중요한 인사이트는 다음 두가지라고 생각한다.</p>
<p>**</p>
<ol>
<li>가장 큰 원판을 제외한 N-1개를 통째로 <code>mid rod</code>에 옮겨야한다.</li>
<li>1번을 수행했다면, 가장 큰 원판을 <code>end rod</code>에 가져다두어야 한다.</li>
<li><code>mid rod</code>에 옮겨둔 N-1개를 통째로 <code>end rod</code>에 옮겨야한다.</li>
</ol>
<p>**</p>
<p>이 과정을 보면, 재귀적으로 구현해야하는 부분을 알 수 있다.
1번을 수행하기 위해서는 결국, N-1개의 하노이탑을 mid rod로 주어진 end rod에 가져다놓는 수행이 된다.</p>
<p>이 과정을 자바스크립트로 구현하면 다음과 같다.</p>
<pre><code class="language-js">function solution(n) {
    const answer=[];
    const start=1;
    const mid=2;
    const end=3;

    const hanoi= (curN, curS, curM, curE)=&gt;{
        if(curN===1){
            answer.push([curS,curE])
            return;
        }
        hanoi(curN-1, curS, curE, curM);
        answer.push([curS,curE]);
        hanoi(curN-1, curM, curS, curE);
    }
    hanoi(n,start,mid,end)

    return answer;
}</code></pre>
<h2 id="시간-복잡도-공간-복잡도">시간 복잡도, 공간 복잡도</h2>
<p>answer에 필요한 어레의 크기는 시간복잡도와 비례할 것이다.
옮기는 횟수만큼 저장되기 때문이다.</p>
<p>hanoi 함수를 재귀적으로 호출하는 방법또한 시간복잡도와 비례할 것이다.</p>
<p>그럼 이 함수가 몇번 호출되는지 생각해보면 된다.
우선 1개의 하노이탑이 주어지면  O(1)로 끝날것이다.</p>
<p>중요한 점은 <strong>T(n)   = 2T(n-1) + 1</strong> 이다.</p>
<blockquote>
<p>T(1)= 1
T(2) = 2T(1)+1  = 2 + 1
T(3) = 2T(2)+1 = 2(2+1)+1 = 2^2 + 2^1 + 2^0</p>
<p>이를 일반화해보면
T(N) = 2^(n-1) + 2^(n-2) + ... + 2^0 = <strong>2^n -1</strong> 임을 알 수 있다.</p>
</blockquote>
<p>즉, O(2^n-1)이므로 <strong>O(2^n)의 시간복잡도를</strong> 가진다.</p>
<p>따라서 이만큼의 정답어레이와 재귀 콜스택이 필요하므로 <strong>공간복잡도도 O(2^n)</strong>만큼 필요하다고 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 155. Min Stack]]></title>
            <link>https://velog.io/@awesome-hong/JavaScript-155.-Min-Stack</link>
            <guid>https://velog.io/@awesome-hong/JavaScript-155.-Min-Stack</guid>
            <pubDate>Mon, 20 Jun 2022 09:55:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/429b3d67-456d-4d24-82b5-f58e20012ede/image.png" alt=""></p>
<p>오늘 라이브코딩을 했는데, 알고리즘 구현을 요구하는 문제가 아닌 좀 창의적인 문제 해결을 원하는 문제였다. </p>
<p>개인적으로 참신한 발상이 맘에 들어 포스팅한다.</p>
<hr>
<h2 id="problem">Problem</h2>
<p><a href="https://leetcode.com/problems/min-stack/submissions/">Min stack 문제</a>는 단순히 Stack클래스를 구현하면 된다.
다만, 새로운 메서드가 추가되는데 <code>getMin()</code> 을 Constant time에 구현해야 한다.</p>
<h2 id="initial-approach">Initial Approach</h2>
<p>처음에는 생각한 방법은 다음과 같다.
min변수를 설정하여 push되는 값마다 갱신해주려고 했으나, pop하면 min값을 처음부터 다시 찾아야한다.
그래서 min, secondMin 두 변수를 저장하면 어떨까? 했으나 이것도 결국 두 변수가 pop되면 다시 찾아야하는 점이 여전히 문제로 남게된다.</p>
<h2 id="solution">Solution</h2>
<p>그래서 보완한 최종로직은 다음과 같다.
스택에 push했을 때 저장하는 형태를 객체로 다음과 같은 저장한다.</p>
<pre><code class="language-js">{
  value: &quot;현재 push된 데이터&quot;,
  min: &quot;스택의 최소값과 현재 데이터 중 더 작은 값&quot;
}
</code></pre>
<p><strong>min을 저장할 때 스택의 마지막 인덱스의 min과 비교하여 저장한다.
그러면 항상 stack의 가장 마지막 인덱스의 min값이 스택전체의 min값이 된다. 
**
이 후에 들어오는 데이터에 상관없이 나와 내 이전데이터만 의존하게 된다. 
그렇기 때문에, 어떤 값이 pop되든 영향을 받지 않게 된다.
**그러므로 min을 얻기 위해서 단순히 가장 마지막 데이터의 min값을 참조하면 된다.</strong></p>
<p>space를 조금 더 사용하는 대신, sorting해야하는 시간을 보완하는 컨셉이다.</p>
<p>전체 코드는 다음과 같다.</p>
<pre><code class="language-js">
var MinStack = function() {
    this.stack=[];
};

/** 
 * @param {number} val
 * @return {void}
 */
MinStack.prototype.push = function(val) {
    this.stack.push({
        value: val,
        min : this.stack.length ? Math.min(val,this.getMin()) : val
    })
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function() {
    return this.stack.pop();
};

/**
 * @return {number}
 */
MinStack.prototype.top = function() {
    return this.stack[this.stack.length-1].value;
};

/**
 * @return {number}
 */
MinStack.prototype.getMin = function() {
    return this.stack[this.stack.length-1].min;
};

/** 
 * Your MinStack object will be instantiated and called as such:
 * var obj = new MinStack()
 * obj.push(val)
 * obj.pop()
 * var param_3 = obj.top()
 * var param_4 = obj.getMin()
 */</code></pre>
<hr>
<p>이렇게 데이터에 데이터와 관련된 정보를 추가하는 형태는 Queue에서 많이 사용했다. 
BFS에서 최단거리를 찾을 때 사용하는 queue에서 해당 데이터가 얼마만큼 거리가 있는지 함께 저장하고, 현재 최단거리보다 큰 데이터가 들어오면 바로 continue하는 로직을 주로 구현했다.</p>
<p>stack을 이용한 문제는 많이 풀어보지 못했고, queue에서는 자연스럽게 사용했을 법한 형태를 생각못했다는게 아쉬웠다.ㅠㅠ</p>
<p>다음에는 맞출수있기를!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 743. Network Delay Time]]></title>
            <link>https://velog.io/@awesome-hong/743.-Network-Delay-Time</link>
            <guid>https://velog.io/@awesome-hong/743.-Network-Delay-Time</guid>
            <pubDate>Sat, 07 May 2022 19:00:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/83c8c068-680c-4348-a515-5f7722a8e9d9/image.png" alt=""></p>
<h1 id="743-network-delay-time"><a href="https://leetcode.com/problems/network-delay-time/">743. Network Delay Time</a></h1>
<h2 id="풀이">풀이</h2>
<p>다익스트라를 사용하여, 출발노드(k)에서 모든 노드로가는 최소값을 구하는 문제이다. 각 노드로 가는 최소값을 dp에 저장하고, 이 중에서 가장 큰 값이 가장 오래걸리는 시간이 될것이다.</p>
<h4 id="1-times-인풋이-2d-array주어지기-때문에-map-형태로-변환해준다">1. times 인풋이 2d array주어지기 때문에, map 형태로 변환해준다.</h4>
<pre><code class="language-js">{
     출발지: [[목적지, time]],
    출발지: [[목적지, time]],
    ...
}</code></pre>
<p>이런 자료구조가 되게 만들어준다.</p>
<h4 id="2-queue에-출발지0을-넣고-시작한다">2. queue에 [출발지,0]을 넣고 시작한다.</h4>
<p>이 때, 출발지에 이어진 모든 노드들까지 가는 최소비용을 dp에 갱신한다.
그리고 visited아닌 애들만 queue에 넣는다.</p>
<p>중요한 점은 다익스트라를 구현하기 위해, 우선순위 큐를 사용하거나 minHeap을 사용하거나 하면되는데 자바스크립트에는 둘다없다...
그래서 따로 구현해주어야 하는데, 난 <del>(귀찮아서)</del> 구현하는 대신 큐에 있는 애들을 sorting 하면서 진행해주었다. </p>
<p>이 부분때문에 전형적인 다익스트라 시간복잡도와 차이가 생긴다. 이부분은 밑에서 또 설명함.</p>
<h4 id="3-queue에서-하나씩-빼면서-방문했는지-확인한다">3. queue에서 하나씩 빼면서 방문했는지 확인한다.</h4>
<p>다익스트라는 한번 방문한 노드는 다시 방문하지 않느다.
따라서, 이미 방문했으면 continue 처리하고 방문안한 노드가 나오면 방문처리를 한 후, 이어져있는 edge를 보며 최소비용을 dp에 갱신을 반복한다.</p>
<h2 id="time-space-complexity">Time, Space Complexity</h2>
<p>N: number of Nodes
E: number of Edges</p>
<p>연결되어있는 모든 간선을 확인하며 큐에 넣어주기 때문에 큐에 들어가는 최대개수는 E개가 된다.
따라서 O(E)만큼 반복하면서, 큐에 push, shift를 진행한다.</p>
<p>전형적인 다익스트라라면 우선순위 큐에 push, shift하는데에 드는 시간복잡도가 각각 O(logE)가 될 것이다. 하지만 난 우선순위 큐를 구현하지 않고 sorting으로 진행했기 때문에 O(ElogE)가 될 것이다.</p>
<p>그리고 정답로직에서 dp에 저장된 N개 배열 중 최댓값을 찾아야 하므로 O(N)가 소요된다.</p>
<p>따라서, 전형적인 다익스트라라면 <code>time O(N + ElogE)</code> 만큼 소요되겠지만, 내가 짠 코드는 <code>time O(N + E * ElogE)</code> 만큼 소요된다.</p>
<p>map에 사용되는 공간복잡도는 O(E), dp, visited는 O(N), 큐는 최대길이가 O(E)이므로 <code>space O(N+E)</code> 가 소요된다.</p>
<pre><code class="language-js">/**
 * @param {number[][]} times
 * @param {number} n
 * @param {number} k
 * @return {number}
 */
var networkDelayTime = function(times, n, k) {
    const map = new Map();
    const dp=Array(n+1).fill(Number.MAX_VALUE);
    const visited = Array(n+1).fill(0);

    dp[0]=0;
    dp[k]=0;

    for(let [now,next,time] of times){
        if(map.has(now)) map.get(now).push([next,time]);
        else map.set(now,[[next,time]]);
    }

    const q=[[k,0]];

    while(q.length){
        const [now, time]=q.shift();

        if(visited[now]) continue;
        visited[now]=1;

        if(!map.has(now)) continue;

        for(let [next,nextTime] of map.get(now)){
            dp[next]=Math.min(dp[next], dp[now]+nextTime);

            if(visited[next]) continue;
            q.push([next,dp[next]]);
        }
        q.sort((a,b)=&gt;a[1]-b[1]);
    }

    const max = Math.max(...dp);
    return max===Number.MAX_VALUE ? -1 : max;
};</code></pre>
<hr>
<p>자바스크립트에서 간단하게 우선순위 큐를 구현하는 코드를 추가해서 수정하겠음!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 72. Edit Distance]]></title>
            <link>https://velog.io/@awesome-hong/JavaScript-72.-Edit-Distance</link>
            <guid>https://velog.io/@awesome-hong/JavaScript-72.-Edit-Distance</guid>
            <pubDate>Sat, 07 May 2022 17:39:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/b677ffae-caa8-4752-a1a9-15e5af1fdbd8/image.png" alt=""></p>
<h1 id="72-edit-distance"><a href="https://leetcode.com/problems/edit-distance/">72. Edit Distance</a></h1>
<h2 id="풀이">풀이</h2>
<p>두 스트링일 같게 만드는데 최소한의 연산 횟수를 구하는 문제이다.
추가, 삭제, 교체 이렇게 3가지 연산이 가능하다.</p>
<p>잘 생각해보면 이것도 순차적으로 char을 하나씩 추가해가며 이전 결과를 활용할 수 있다.</p>
<h4 id="1-이전-결과를-활용하기-위해-word1word2를-가지고-2d-array를-만든다-base-case를-위해-역시-각각-스트링앞에-쓰레기값으로-패딩을-넣었다">1. 이전 결과를 활용하기 위해 word1,word2를 가지고 2d array를 만든다. base case를 위해 역시 각각 스트링앞에 &quot;#&quot;(쓰레기값)으로 패딩을 넣었다.</h4>
<p>dp[0][3]이 의미하는 것은 빈 스트링을 &quot;hor&quot;으로 만들려면 몇번의 연산이 필요한지 이다. 3번의 추가가 필요하니 3이 저장된다.</p>
<p>아래처럼 초기값을 세팅해준다.
<img src="https://velog.velcdn.com/images/awesome-hong/post/9f61dbce-7044-4e50-b54f-bc7f5be474b7/image.png" style="width:100%" /></p>
<figcaption align="center">출처: https://leetcode.com/problems/edit-distance/discuss/662931/EDIT-DISTANCE-or-CPP-or-C%2B%2B-or-Pictorial-representation-or-Easy-to-understand-or</figcaption>


<ol start="2">
<li>순차적으로 테이블을 채워나가는데, 추가,삭제, 교체가 좀 특이하다. 
예시를 보면 이해할수있다.<blockquote>
<p>Input: word1 = &quot;horse&quot;, word2 = &quot;ros&quot;
Output: 3
Explanation: 
horse -&gt; rorse (replace &#39;h&#39; with &#39;r&#39;)
rorse -&gt; rose (remove &#39;r&#39;)
rose -&gt; ros (remove &#39;e&#39;)</p>
</blockquote>
</li>
</ol>
<p>h를 r로 만들어야 한다. 마지막 char이 다르니까 추가든 삭제든 교체는 어떤 작업이 필요하다. </p>
<ul>
<li>h 삭제하는 케이스: &quot;h&quot;에서 &quot;h&quot;를 삭제해서 빈스트링으로 만들고, 빈 스트링을 &quot;r&quot;로 만들면 된다.<ul>
<li>즉, <code>h 삭제 + 빈 스트링을 r로 만드는 작업횟수 1 = 2</code></li>
</ul>
</li>
<li>r을 추가하는 케이스: &quot;h&quot;를 빈 스트링으로 만들고 r을 추가하면 된다. <ul>
<li>즉, <code>h를 빈스트링으로 만드는 작업횟수 1 + r 추가 = 2</code></li>
</ul>
</li>
<li>h를 r로 교체하는 케이스: 각 케이스에서 마지막 char을 지우고, 둘다 r를 추가한다고 생각한다.<ul>
<li>즉, <code>빈 스트링을 빈스트링으로 만드는 작업횟수 0 + r 추가 = 1</code></li>
</ul>
</li>
</ul>
<h4 id="위-세가지중에서-가장-적은-연산횟수는-마지막-교체이므로-h-r-연산횟수는-1이-된다">위 세가지중에서 가장 적은 연산횟수는 마지막 교체이므로, h-&gt;r 연산횟수는 1이 된다.</h4>
<p>헷갈릴때에는, 이 전 계산해놓은 값의 결과에서 어떤연산을 추가로해놓으면 되는지 생각해본다.
(ex. h를 빈스트링으로 만드는데 1개연산이 들었네? 그럼 빈스트링이 결과이니까 여기에 r만 추가하면 되는거네.)</p>
<p>ref: <a href="https://youtu.be/MiqoA-yF-0M">https://youtu.be/MiqoA-yF-0M</a></p>
<h2 id="time-space">Time, Space</h2>
<p>time O(N^2) space O(N^2)</p>
<pre><code class="language-js">/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function(w1, w2) {
    if(w1===w2) return 0;

    w1=&quot;#&quot;+w1;
    w2=&quot;#&quot;+w2;
    const dp = [...Array(w1.length)].map(row =&gt; Array(w2.length).fill(0));

    for(let i=0; i&lt;w1.length; i++){
        for(let j=0; j&lt;w2.length; j++){
            if(i===0){
                dp[0][j]=j;
                continue;
            }
            if(j===0){
                dp[i][0]=i;
                continue;
            }

            if(w1[i]===w2[j]){
                dp[i][j]=dp[i-1][j-1];
            }else{
                dp[i][j]=Math.min(dp[i][j-1],dp[i-1][j],dp[i-1][j-1])+1;
            }
        }
    }
    return dp[w1.length-1][w2.length-1];
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 583. Delete Operation for Two Strings]]></title>
            <link>https://velog.io/@awesome-hong/JavaScript-583.-Delete-Operation-for-Two-Strings</link>
            <guid>https://velog.io/@awesome-hong/JavaScript-583.-Delete-Operation-for-Two-Strings</guid>
            <pubDate>Sat, 07 May 2022 16:10:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/1906e5a2-9834-4bf9-bc12-17ab63101f21/image.png" alt=""></p>
<h1 id="583-delete-operation-for-two-strings"><a href="https://leetcode.com/problems/delete-operation-for-two-strings/">583. Delete Operation for Two Strings</a></h1>
<h2 id="풀이">풀이</h2>
<p>LCS를 구할 수 있다면 바로 풀수있는 문제다!
두 스트링이 같아지기 위해, 최소한만 삭제하라는 문제인데, 두 스트링의 LCS를 구하고, 각각 LCS와 얼마나 차이가 나는지만 계산하면 된다.</p>
<p>이전 포스팅 <a href="https://velog.io/@awesome-hong/JavaScript-1143.-Longest-Common-Subsequence">[JavaScript] 1143. Longest Common Subsequence</a> 에 나온 방법과 똑같다.</p>
<h2 id="time-space">Time, Space</h2>
<p>LCS를 구하는데에 드는 비용과 같다. 따라서 time O(N^2), space O(N^2)이다.</p>
<pre><code class="language-js">var minDistance = function(w1, w2) {
    if(w1===w2) return 0;

    w1 = &quot;#&quot;+w1;
    w2 = &quot;#&quot;+w2;
    const dp = [...Array(w1.length)].map(row =&gt; Array(w2.length).fill(0));

    for(let i=1; i&lt;w1.length; i++){
        for(let j=1; j&lt;w2.length; j++){
            if(w1[i]===w2[j]){
                dp[i][j] = 1 + dp[i-1][j-1];
            }else{
                dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
            }
        }
    }

    const LCS = dp[w1.length-1][w2.length-1];

    return (w1.length-1)-LCS + (w2.length-1)-LCS;
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 1143. Longest Common Subsequence]]></title>
            <link>https://velog.io/@awesome-hong/JavaScript-1143.-Longest-Common-Subsequence</link>
            <guid>https://velog.io/@awesome-hong/JavaScript-1143.-Longest-Common-Subsequence</guid>
            <pubDate>Sat, 07 May 2022 15:58:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/ce1ede9e-9dd9-4221-a656-cc8e28d91edc/image.png" alt=""></p>
<h1 id="1143-longest-common-subsequence"><a href="https://leetcode.com/problems/longest-common-subsequence/">1143. Longest Common Subsequence</a></h1>
<h2 id="dp">DP</h2>
<img src="https://velog.velcdn.com/images/awesome-hong/post/5898087d-1125-4b26-bf8e-f91e31afcfde/image.png" style="width:100%"/>
<figcaption align = "center">출처: https://leetcode.com/problems/longest-common-subsequence/discuss/348884/C%2B%2B-with-picture-O(nm)</figcaption>  

<p>위와 같은 어레이를 만들어서 가장 끝에 위치한 값이 답이 될것이다.</p>
<p>예를들어, dp[2][3]이 의미하는 것은 &quot;ac&quot;와 &quot;ab&quot;의 LCS이다.
위 테이블에서 dp[2][3]=1 이 나오는 이유는 a 하나만 공통이기 때문이다.</p>
<h4 id="1-base-case는-제일-위-row-왼쪽-column이다이-패딩을-만들기위해-string-제일앞쪽에-각각-쓰레기값을-넣어주고-시작한다">1. base case는 제일 위 row, 왼쪽 column이다.이 패딩을 만들기위해 string 제일앞쪽에 각각 쓰레기값(#)을 넣어주고 시작한다.</h4>
<p>인덱스 실수를 하지않기 위함이다.</p>
<pre><code class="language-js">t1 = &quot;#&quot;+t1;
t2 = &quot;#&quot;+t2;</code></pre>
<br>

<h4 id="2-dp를-순회하면서-마지막-char이-같은-경우와-다른경우를-구분하여-저장해준다">2. dp를 순회하면서, 마지막 char이 같은 경우와 다른경우를 구분하여 저장해준다.</h4>
<ul>
<li><p>같은경우: <code>&quot;ac&quot;, &quot;abc&quot;</code></p>
<ul>
<li>c는 서로 같기 때문에, &quot;a&quot;와 &quot;ab&quot; 결과에 1이 추가되는 것과 같다. 이때에는 &quot;a&quot;와 &quot;ab&quot;의 LCS가 1이므로 2가 dp에 저장된다.</li>
</ul>
</li>
<li><p>다른경우: <code>&quot;ac&quot;, &quot;ab&quot;</code></p>
<ul>
<li>c-b가 다르기 때문에, &quot;a&quot;와 &quot;ab&quot;, &quot;ac&quot;와 &quot;a&quot; 중 더 큰값을 dp에 저장한다.</li>
<li>왜냐하면 새로 추가된 마지막문자가 다르기 때문에, 이전 LCS에 길이를 추가할 수 없고, 이전에 만들어진 결과 중 가장 큰 결과를 가져와서 LCS를 저장해야하기 때문이다.<ul>
<li>&quot;c&quot;가 추가되기 이전인 &quot;a&quot;-&quot;ab&quot; 결과 1</li>
<li>&quot;b&quot;가 추가되기 이전인 &quot;ac&quot;-&quot;a&quot; 결과 1</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="time-space">Time, Space</h3>
<p>이렇게 dp테이블을 모두 채우면 time O(N^2), space O(N^2)에 수행할 수 있다.</p>
<pre><code class="language-js">var longestCommonSubsequence = function(t1, t2) {
    if(t1===t2) return t1.length;

    t1 = &quot;#&quot;+t1;
    t2 = &quot;#&quot;+t2;
    const dp=[...Array(t1.length)].map(row =&gt; Array(t2.length).fill(0));

    for(let i=1; i&lt;t1.length; i++){
        for(let j=1; j&lt;t2.length; j++){
            if(t1[i]===t2[j]){
                dp[i][j] = 1 + dp[i-1][j-1];
            }else{
                dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
            }
        }
    }

    return dp[t1.length-1][t2.length-1];
};</code></pre>
<hr>
<p><a href="https://leetcode.com/problems/longest-common-subsequence/discuss/351689/JavaPython-3-Two-DP-codes-of-O(mn)-and-O(min(m-n))-spaces-w-picture-and-analysis">https://leetcode.com/problems/longest-common-subsequence/discuss/351689/JavaPython-3-Two-DP-codes-of-O(mn)-and-O(min(m-n))-spaces-w-picture-and-analysis</a>
<img src="https://velog.velcdn.com/images/awesome-hong/post/b0f74b22-7b73-4e10-8cc3-666c66becda2/image.png" alt=""></p>
<p>Discuss에서 본 마음따뜻한 댓글 ㅠ 
space 최적화를 하셨던데.. 일단 머리아파서 안보긴했는데.. 언젠간 보고싶다.. ^-^..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 300. Longest Increasing Subsequence]]></title>
            <link>https://velog.io/@awesome-hong/300.-Longest-Increasing-Subsequence</link>
            <guid>https://velog.io/@awesome-hong/300.-Longest-Increasing-Subsequence</guid>
            <pubDate>Sat, 07 May 2022 12:59:53 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/d22637ad-030c-470a-bd70-151953fd135f/image.png" alt=""></p>
<h1 id="300-longest-increasing-subsequence"><a href="https://leetcode.com/problems/longest-increasing-subsequence/">300. Longest Increasing Subsequence</a></h1>
<h2 id="풀이1-dp">풀이1. DP</h2>
<p>숫자들을 순회하면서, 해당값까지 가능한 LIS길이를 dp어레이에 저장한다.
해당 인덱스 이전값을 순회하면서 가장 긴 값을 찾아 1 증가시켜준다. </p>
<p>나보다 작은값이 없어서 LIS갱신이 불가능한 경우에도 &quot;나 자신&quot;으로 이루어진 길이1짜리 LIS가 존재하므로 max초기값 0에 1을 더한 1로 dp에 저장하게 된다.</p>
<p>정답으로 반환할 가장 긴 LIS를 갱신하며 진행한다.</p>
<h3 id="time-space">Time, Space</h3>
<p>이중반복으로 time O(N^2)가 소요되고, space O(N)이 소요된다.</p>
<pre><code class="language-js">var lengthOfLIS = function(nums) {
    const dp = Array(nums.length).fill(1);
    let ans = 1;

    for(let i=1; i&lt;nums.length; i++){
        let max=0;
        for(let j=i-1; j&gt;=0; j--){
            if(nums[j] &lt; nums[i]) max = Math.max(max, dp[j]);
        }
        dp[i] = max+1;
        ans = Math.max(ans, dp[i]);
    }

    return ans;
};
</code></pre>
<h2 id="풀이-2-binary-searchdp">풀이 2. Binary Search+DP</h2>
<p>위 풀이에서, 앞 요소들을 순회하는 반복문을 개선하기 위한 방식이다.
dp와 count를 사용하지않고 sub을 저장해가면서, 이 sub를 이진탐색하여 길이를 구할 수 있다.</p>
<p>(참고로, 문제풀이에 사용되는 sub는 LIS가 아니다. 정확히 말하면, LIS의 길이를 측정하기 위해 사용되는 어레이이다. 밑에 풀이를 보면 이해할 수 있다.)</p>
<p>sub에 수를 추가할 수 있는 조건은 딱 한가지이다. sub의 가장 큰수, 즉 가장 끝에있는 수보다 큰 수가 나타났을때에만 추가할 수 있다.</p>
<p>다음 세가지 경우로 나눌 수 있다.</p>
<h4 id="1-sub의-가장-큰-수보다-큰-수가-나타났을-때">1. sub의 가장 큰 수보다 큰 수가 나타났을 때</h4>
<p>sub끝에 추가한다.</p>
<h4 id="2-sub의-가장-큰-수와-같은-수가-나타났을-때">2. sub의 가장 큰 수와 같은 수가 나타났을 때</h4>
<p>그냥 넘어간다.</p>
<h4 id="3-sub의-가장-큰-수보다-작은-수가-나타났을-때">3. sub의 가장 큰 수보다 작은 수가 나타났을 때</h4>
<p> <code>binary search</code>의 lower bound 방식을 통해, sub에서 해당 수가 들어갈 자리를 찾아 그 자리에 덮어씌워 교체시킨다.</p>
<p>정답은 sub의 길이가 된다.</p>
<p>이해하기 위해, 예시를 보면 다음과 같다.</p>
<pre><code class="language-js">nums = [1,2,7,8,3,4,5,9,0]

value -&gt; sub
    1 -&gt; [1]
    2 -&gt; [1,2]
    7 -&gt; [1,2,7]
    8 -&gt; [1,2,7,8]
    3 -&gt; [1,2,3,8] 
    // 7을 3으로 교체한다. 
    // 여기서 sub이 LIS가 아닌 의미가 나타난다. 
    // [1,2,3,8]은 순서가 안맞기 때문에 LIS가 아니다.
    // 하지만 [1,2,7,8]은 LIS가 맞는데, 우리는 [1,2,7,8]의 길이를 그대로 유지하면서 
    // [1,2,7]대신 [1,2,3]이라는 새로운 sequence를 저장하게 된 셈이다. 
    // 왜이렇게 하냐면 [1,2,7]보다 [1,2,3]이 더 작은수로 끝나기 때문에 더 긴 시퀀스를 만들 기회가 있기에 3으로 교체해준다.
    // 8은 3보다 크고 8보다 작은수가 나타난다면 사라지게 될 것을 알수있다.

    4 -&gt; [1,2,3,4] 
    // 8을 4으로 교체한다.
    // 역시 [1,2,7,8] 길이를 그대로 유지하면서 새로운 4라는 숫자가 포함된 시퀀스를 저장하게 되었다.

    5 -&gt; [1,2,3,4,5] 
    // 5를 추가한다. 길이를 갱신할 수 있는 것은 sub의 가장 큰수보다 클 때뿐이다.

    9 -&gt; [1,2,3,4,5,9]
    0 -&gt; [0,2,3,4,5,9] 
    // 1을 0으로 교체한다.</code></pre>
<h3 id="time-space-1">Time, Space</h3>
<p>이런식으로 진행하면 time O(NlogN)으로 수행할 수 있다. sub은 최대길이가 N이 될수있으므로 space O(N)이다.</p>
<pre><code class="language-js">var lengthOfLIS = function(nums) {
    if(nums.length===1) return 1;

    const sub=[nums[0]];

    for(let i=1; i&lt;nums.length; i++){
        if(sub[sub.length-1] === nums[i]) continue;
        if(sub[sub.length-1] &lt; nums[i]){
            sub.push(nums[i]);
            continue;
        }
        let l=0;
        let r=sub.length-1;

        while(l&lt;r){
            const m=(l+r)/2&lt;&lt;0;

            if(sub[m] &lt; nums[i]) l=m+1;
            else r=m;

        }
        sub[l]=nums[i];
    }
   return sub.length;
};</code></pre>
<p>ref: <a href="https://leetcode.com/problems/longest-increasing-subsequence/discuss/74848/9-lines-C%2B%2B-code-with-O(NlogN)-complexity">https://leetcode.com/problems/longest-increasing-subsequence/discuss/74848/9-lines-C%2B%2B-code-with-O(NlogN)-complexity</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 673. Number of Longest Increasing Subsequence]]></title>
            <link>https://velog.io/@awesome-hong/673.-Number-of-Longest-Increasing-Subsequence</link>
            <guid>https://velog.io/@awesome-hong/673.-Number-of-Longest-Increasing-Subsequence</guid>
            <pubDate>Sat, 07 May 2022 11:46:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/db2e6a2c-3794-40dd-860c-1dbd087a775e/image.png" alt=""></p>
<h1 id="673-number-of-longest-increasing-subsequence"><a href="https://leetcode.com/problems/number-of-longest-increasing-subsequence/">673. Number of Longest Increasing Subsequence</a></h1>
<h2 id="풀이-1-on2-dp">풀이 1. O(N^2) DP</h2>
<p>DP에는 해당인덱스를 포함한 LIS 길이를 저장하고,
count에는 그 LIS와 같은 길이의 LIS가 몇개가 있는지 저장한다.</p>
<p>max에는 가장 긴 LIS 길이를 저장하고, 이 LIS길이를 가지고 있는 인덱스의 count의 총합이 정답이 된다.</p>
<p>count의 총 합을 정답으로 하는것을 잊지말자. 3가지의 오답이 나와었다.</p>
<ul>
<li><p><code>[1,2,4,3,5,4,7,2]</code>
현재 인덱스와 LIS가 1개차이나면서 LIS로 이어질수있는 값을 만나면, 그 값의 count값을 흡수해야한다.</p>
</li>
<li><p><code>[1,1,1,2,2,2,3,3,3]</code>, <code>[1,2,3,1,2,3,1,2,3]</code>
정답로직에 최대LIS를 가진 값들의 count총합을 구하지않아서 오답처리됨.</p>
</li>
</ul>
<h2 id="time-space">Time, Space</h2>
<p>일단 이중반복문을 통해 처리하므로 Time O(N^2)가 소요되고, dp와 count어레이를 사용하므로 O(N)의 공간이 소요된다.</p>
<pre><code class="language-js">var findNumberOfLIS = function(nums) {
    const dp=Array(nums.length).fill(1);
    const count=Array(nums.length).fill(1);
    let max = 0;
    let ans = 0;

    for(let i=nums.length-1; i&gt;=0; i--){
        for(let j=i+1; j&lt;nums.length; j++){
            if(nums[i]&gt;=nums[j]) continue;

            if(dp[i] === dp[j]+1) count[i]+=count[j];
            else if(dp[i] &lt; dp[j]+1){
                dp[i] = dp[j]+1;
                count[i] = count[j];
            }
        }
        max=Math.max(max, dp[i]);
    }

    for(let i=0; i&lt;dp.length; i++){
        if(dp[i]===max) ans+=count[i];
    }
    return ans;
};</code></pre>
<p>찾아보니 time을 O(NlogN)로 줄일수있는 방법이 있다고해서 공부한 후 추가할 예정</p>
<p>ref: 
<a href="https://leetcode.com/problems/number-of-longest-increasing-subsequence/discuss/107295/9ms-C%2B%2B-Explanation%3A-DP-%2B-Binary-search-%2B-prefix-sums-O(NlogN)-time-O(N)-space">https://leetcode.com/problems/number-of-longest-increasing-subsequence/discuss/107295/9ms-C%2B%2B-Explanation%3A-DP-%2B-Binary-search-%2B-prefix-sums-O(NlogN)-time-O(N)-space</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 139. Word Break]]></title>
            <link>https://velog.io/@awesome-hong/139.-Word-Break</link>
            <guid>https://velog.io/@awesome-hong/139.-Word-Break</guid>
            <pubDate>Sat, 07 May 2022 04:20:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/2629a30a-6228-4a6c-baea-f1567782617b/image.png" alt="">
<del>문제푸는데 화가났으므로 칙칙한 배경ㅠ</del></p>
<h1 id="139-word-break"><a href="https://leetcode.com/problems/word-break/">139. Word Break</a></h1>
<h2 id="dp-풀이">DP 풀이</h2>
<p>우선 s길이가 1이거나 </p>
<h2 id="time-space-complexity">Time, Space complexity</h2>
<p>S: s.length
W: wordDict.length</p>
<p>slice와 indexOf하는데에 s길이만큼 시간이 소요되므로 <code>O(SW) * O(S)</code> 만큼 소요된다.</p>
<p>time <code>O(S^2 * W)</code>, space <code>O(S)</code></p>
<pre><code class="language-js">var wordBreak = function(s, wordDict) {
    if(s.length===1 &amp;&amp; wordDict.indexOf(s) === -1) return false;

    const dp = Array(s.length+1).fill(0);

    for(let i=0; i&lt;s.length; i++){
        if(i&gt;0 &amp;&amp; !dp[i]) continue;

        for(let word of wordDict){
            if(s.slice(i).indexOf(word)===0) dp[i+word.length]=1;
        }
    }

    return dp[dp.length-1];
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 413. Arithmetic Slices]]></title>
            <link>https://velog.io/@awesome-hong/413.-Arithmetic-Slices</link>
            <guid>https://velog.io/@awesome-hong/413.-Arithmetic-Slices</guid>
            <pubDate>Sat, 07 May 2022 03:56:37 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/218db3dd-7400-45f2-a46d-ebe52e46bb3a/image.png" alt=""></p>
<h1 id="413-arithmetic-slices"><a href="https://leetcode.com/problems/arithmetic-slices/">413. Arithmetic Slices</a></h1>
<p>DP문제인데 DP테이블까지 만들건없고 변수 두개로 가능하다.</p>
<h2 id="dp">DP</h2>
<p>길이 3이상의 등차수열(?)의 갯수를 알아내야하는 문제이다.</p>
<p>예외처리로 숫자가 3개가 안되면 바로 0으로 끝내버리고 시작함</p>
<p>3개가 기준이니까 한개전꺼랑 두개전꺼를 보면서 확인하고, 맞다고 확인하면 count를 증가시키고 정답에 count를 추가한다. 
이 카운트는 현재 수열이 허용됨으로서 증가하는 길이 3이상의 수열의 갯수이다.
정답은 3이상의 누적수열 개수이다.</p>
<p>예를들어,</p>
<ul>
<li>[1,2,3]이 수열이 맞으니까 cnt=1</li>
<li>[1,2,3,4]에서 2,3,4도 수열이고 1,2,3,4도 수열이다. 저장된 cnt값을 하나 증가시켜서 cnt=2가 되고, 이 cnt를 정답에 추가한다.</li>
</ul>
<p>왜냐하면 수열 길이가 1씩 증가될수록 3개짜리 수열의 증가값도 1씩 증가하기 때문이다.</p>
<p>[1,2,3]은 길이 3이상 수열이 1개(1추가), 
[1,2,3,4]는 길이 3이상 수열이 3개(2추가), 
[1,2,3,4,5]는 길이 3이상 수열이 6개(3추가)... 이런식이다.</p>
<pre><code class="language-js">var numberOfArithmeticSlices = function(nums) {
    if(nums.length &lt; 3) return 0;

    let ans = 0;

    for (let i = 2, cnt = 0; i &lt; nums.length; i++){
        if(nums[i] - nums[i-1] === nums[i-1] - nums[i-2]){
            cnt++;
            ans+=cnt;
        }else cnt=0;
    }

    return ans;
};
</code></pre>
<h2 id="time-space-complexity">Time, space complexity</h2>
<p>nums 어레이 한번순회하고 변수만으로 가능하므로 시간 O(N), 공간 O(1)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 5. Longest Palindromic Substring ]]></title>
            <link>https://velog.io/@awesome-hong/Longest-Palindromic-Substring</link>
            <guid>https://velog.io/@awesome-hong/Longest-Palindromic-Substring</guid>
            <pubDate>Sat, 07 May 2022 03:36:47 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/awesome-hong/post/f412ce60-65fa-47a7-b9b8-faf1ff2cea1a/image.png" alt=""></p>
<h1 id="5-longest-palindromic-substring"><a href="https://leetcode.com/problems/longest-palindromic-substring/">5. Longest Palindromic Substring</a></h1>
<p>dp로 풀수있고, dp의 공간복잡도 개선을 위해 투포인터로 풀수있다.</p>
<h2 id="풀이-1-dynamic-programming">풀이 1. Dynamic Programming</h2>
<p>2d 배열을 DP 저장소로 사용한다.
다만, 대각선 오른쪽만 사용한다. 공간절반이 버려지는 셈이라 비효율적이다.</p>
<ol>
<li>모두 초기값 0으로 설정해두고 오른쪽 아래부터 왼쪽위로 순회한다.</li>
<li>현재 인덱스 i와 j에 대해 <code>s[i]</code>, <code>s[j]</code>가 같으면 <code>s[i+1][j-1]</code>가 같은지 보면된다. 다르면 볼것도없이 그냥 0인거 건드리지않는다.</li>
<li>같다면 정답을 갱신해야하는데, 현재 max길이보다 크면 <code>index[i,j]</code>로 정답을 갱신한다.</li>
</ol>
<p>다만, aa와 같이 길이가 2개인 경우에는 <code>s[i+1][j-1]</code>를 보면 엇갈려져서 정답로직에 예외가 생기므로 둘의 인덱스차이가 1인경우는 따로 빼주었다.</p>
<p>코드가독성을 위해 정답인 경우, <code>getAnswer()</code>로 로직을 분리해주었다.</p>
<h3 id="time-space-complexity">Time, Space complexity</h3>
<p>시간복잡도 <code>O(N^2)</code>가 소요되고, 이를 저장할 어레이때문에 공간도 <code>O(N^2)</code>가 소요된다.</p>
<pre><code class="language-js">var longestPalindrome = function(s) {
    if(s.length===1) return s;

    let max=1;
    let index=[0,0];

    const dp=[...Array(s.length)].map(r=&gt;Array(s.length).fill(0));

    const getAnswer=(i,j)=&gt;{
        max=j-i+1;
        index[0]=i;
        index[1]=j;
    }

    for(let i=s.length-1; i&gt;=0; i--){
        for(let j=s.length-1; j&gt;=i; j--){
            if(i===j){
                dp[i][j]=1;
                continue;
            }
            if(s[i]!==s[j]) continue;

            if(j-i===1){
                dp[i][j]=1;
                if(max&lt;j-i+1) getAnswer(i,j);
            }

            if(!dp[i+1][j-1]) continue;
            dp[i][j]=1;

            if(max&lt;j-i+1) getAnswer(i,j);
        }
    }

    return s.substring(index[0],index[1]+1);
};</code></pre>
<h2 id="풀이-2-two-pointer">풀이 2. Two Pointer</h2>
<p>dp테이블을 안쓰고 동적으로 하나씩 살펴보며 정답인지 체크하는 방식이다.</p>
<p>이 방식은 한계가 있는데, Palindrom이 홀수길이인지 짝수길이인지에 따라 다르게 생각해야한다.</p>
<ul>
<li><p>aba 케이스(홀수)
가운데지점 b에서 시작해서 양옆으로 한칸씩 확장하며 비교하면 된다.</p>
</li>
<li><p>abba 케이스(짝수)
첫번째 b에서 시작해서 한칸씩 확장하면 팰린드롬인데도 아니라고 판단된다. 이 경우에는 시작하려는 인덱스의 한칸앞이 같은지도 확인해주고, 같다면 bb에서부터 양옆으로 한칸씩 확장하면 된다.</p>
</li>
</ul>
<p>문제는 우리는 팰린드롬의 길이를 모르는 점인데, 그래서 홀짝케이스를 모두 가정하고 계산을 돌려버리고 두 방식의 결과 중 더 긴 걸 정답으로 치면 된다.</p>
<p>인덱스 0부터 시작점이라고 가정하고 양옆으로 뻗어나가고, 중간에 팰린드롬이 끊기면 시작인덱스를 증가시켜서 다시 뻗어나가길 반복한다. 결국 이중반복문으로 순회할 수 있다.</p>
<h3 id="time-space-complexity-1">Time, Space complexity</h3>
<p>시간복잡도 O(N^2), 공간은 constant이므로 O(1)</p>
<pre><code class="language-js">var longestPalindrome = function(s) {
    if(s.length===1) return s;

    const getEven = (arr)=&gt;{
        const index=[0,0];
        let max=1;

        for(let i=0; i&lt;arr.length; i++){
            let l=i;
            let r=i+1;
            while(l&gt;=0 &amp;&amp; r&lt;arr.length){
                if(arr[l]!==arr[r]) break;

                if(max &lt;= r-l+1){
                    max=r-l+1;
                    index[0]=l;
                    index[1]=r;
                }
                l--;
                r++;
            }    
        }

        return [index,max];
    }

    const getOdd = (arr)=&gt;{
        const index=[0,0];
        let max=1;

        for(let i=1; i&lt;arr.length-1; i++){
            let l=i;
            let r=i;
            while(l&gt;=0 &amp;&amp; r&lt;arr.length){
                if(arr[l]!==arr[r]) break;

                if(max &lt;= r-l+1){
                    max=r-l+1;
                    index[0]=l;
                    index[1]=r;
                }
                l--;
                r++;
            }    
        }

        return [index,max];
    }

    const [evenIndex,evenLen] = getEven(s);
    const [oddIndex,oddLen] = getOdd(s);

    if(evenLen &gt; oddLen) return s.substring(evenIndex[0],evenIndex[1]+1);
    return s.substring(oddIndex[0],oddIndex[1]+1);
};</code></pre>
<hr>
<p>처음풀었을땐 손도못대로 솔루션보고 3일을 낑낑대고도 이해못했는데 이제 두가지 방식으로도 풀수있다니ㅠ감격ㅠ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[vercel] fork한 레포 자동최신화 + vercel로 자동배포하기]]></title>
            <link>https://velog.io/@awesome-hong/vercel-%EC%9E%90%EB%8F%99%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@awesome-hong/vercel-%EC%9E%90%EB%8F%99%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 07 Apr 2022 10:17:07 GMT</pubDate>
            <description><![CDATA[<h2 id="vercel로-프로젝트-배포하기">vercel로 프로젝트 배포하기</h2>
<p>프로젝트를 진행했던  깃허브 레포지토리는 organization 레포지토리였다.</p>
<p>초기에 배포가 쉽고 간단한 vercel을 사용해 배포하기로하고, 프로젝트를 진행했는데 organization레포지토리는 유료제(정확히는 정해진 기간만 무료)인걸 나중에 알았다.</p>
<h2 id="문제해결---다른-문제발생">문제해결 -&gt; 다른 문제발생</h2>
<p>그래서 개인 레포지토리로 fork한 후, 최종 프로덕션 브랜치로 사용했던 master브랜치를 vercel에 연결하여 개인레포지토리 인 척(?)하여 배포에 성공했다.</p>
<p>그런데 프로젝트를 진행하면서, 자잘한 기능수정과 버그 수정 등이 생겨서 <code>fork</code>된 레포지토리를 매번 <code>fetch and merge</code>하여 싱크를 맞추기가 귀찮아졌다.</p>
<p>그래서 git actions를 활용하여 자동화 싱크 기능을 추가하였다.</p>
<h2 id="깃-액션-사용하기">깃 액션 사용하기</h2>
<p>ref: <a href="https://stackoverflow.com/questions/23793062/can-forks-be-synced-automatically-in-github">https://stackoverflow.com/questions/23793062/can-forks-be-synced-automatically-in-github</a></p>
<p>fork해온 개인 레포지토리의 액션 탭으로 들어가서 새로운 액션을 만들었다.
코드는 스택오버플로우를 참고했다.</p>
<pre><code class="language-yml"># org레포지토리에서 fork해온 현 레포지토리를 최신상태로 fetch and merge하는 스크립트입니다.

name: Sync and merge upstream repository
on:
  workflow_dispatch:
  schedule: 
  - cron: &quot;0 8 * * *&quot; #Runs at 8:00 UTC(5:00 in Korea) every day.

jobs:
  merge:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Merge upstream
        run: |
          git config --global user.name &#39;Hong-been&#39;
          git config --global user.email &#39;redbean-2@naver.com&#39;
          # &quot;git checkout master&quot; is unnecessary, already here by default
          git pull --unshallow  # this option is very important, you would get
                                # complains about unrelated histories without it.
                                # (but actions/checkout@v2 can also be instructed
                                # to fetch all git depth right from the start)
          git remote add upstream https://github.com/Tinkerbell-Green/need-compliments.git
          git fetch upstream
          git checkout master
          git merge -Xtheirs upstream/master
          git push origin master
          # etc</code></pre>
<ul>
<li>cron에서 얼마의 시간주기로 or 매번 언제 실행할건지 정할 수 있다. </li>
<li>fork해오는 master브랜치를 덮어쓰기로 머지하도록 함.</li>
</ul>
<p><img src="https://velog.velcdn.com/cloudflare/awesome-hong/8c564765-0c2b-42db-afb3-63ea4d1cbcd9/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-04-07%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%207.16.25.png" alt="">
성공~! 내일 아침에도 잘 되었는지 확인해봐야지.</p>
<h2 id="여담">여담</h2>
<h3 id="현재-프로젝트-진행해서-개선할-점">현재 프로젝트 진행해서 개선할 점</h3>
<p>지금 프로젝트 방식은 </p>
<blockquote>
<ol>
<li>기능을 나누어 개발자들이 구현하고
<code>각자 구현한 브랜치</code> --<code>merge</code>--&gt; <code>develop</code>브랜치</li>
</ol>
</blockquote>
<blockquote>
<ol start="2">
<li>배포가능한 단위가 되었다고 판단할 때
<code>develop</code> 브랜치 --<code>merge</code>--&gt; <code>master</code> 브랜치 </li>
</ol>
</blockquote>
<blockquote>
<ol start="3">
<li>vercel에 연결된 개인 레포지토리를 통해 배포하기 위해
<code>master</code> 브랜치 --<code>fork</code>--&gt; 개인레포 <code>master</code> 브랜치 --&gt; vercel로 자동배포 </li>
</ol>
</blockquote>
<p>위와 같다.
배포자동화 등의 키워드를 찾아보다가 무중단 배포, CI/CD 등이 자주 나오는걸 보았다. 
포스팅한 글은 단순히 3번에 해당하는 <code>fork한 레포를 자동 최신화 + vercel을 통해 자동 배포</code>하는 부분이다. (vercel자동배포도 vercel에서 알아서 해줌)</p>
<h3 id="테스트-코드의-부재-불편함">테스트 코드의 부재, 불편함</h3>
<ol>
<li><p>일단 테스트 코드가 없어서 각 기능을 머지할 때, develop브런치에 끼치는 영향도를 알 수 없었고 각자 구현한 정확한 기능을 확인할 방법이 없었다. (머지할 때 사람이 코멘트로 설명을 남기는 수동작업 뿐)
그러다보니 코드를 여러번 읽어보고 머지해도 에러가 안날거라는 확신도 부족했고, 합치고보니 develop브런치가 예상과 다르게 동작한 경험도 있다.</p>
</li>
<li><p>서로 부족한 점을 보완하기 위해 머지할 때마다, 코드리뷰를 진행했지만 역시 테스트 코드가 없으니 구현사항을 정확히 정의하여 공유하기 힘들었고 그에따라 리뷰할 때 드는 시간도 오래걸렸다. 중복된 의사소통도 많아짐.</p>
</li>
</ol>
<p>테스트 코드의 필요성을 매우 느끼게되었고, 기능마다? 테스트 코드를 추가한다면 이 코드를 자동으로 실행시키고 통과되었을 때 개발브런치에 머지시키는 등을 자동화할 수 있는 CI/CD 프로세스가 필요하게 될것임을 예상할 수 있었다.</p>
<p>자꾸 되게 멋지고 유용한 기능을 <code>구현</code> 하는 데에 욕심이 난다. 하지만 다음 프로젝트를 진행한다면, 멋진 기능을 구현하기보다 구현한 기능의 신뢰성을 높일 수 있는 테스트 코드와 이 과정을 위한 CI/CD를 경험해보고싶다.</p>
<p>P.S.) 역시 선배 개발자들이 만들어 놓은 문화(개발 프로세스)는 다 필요에 의해 나온것임을.....</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] redux store hydrate 중복 저장 이슈]]></title>
            <link>https://velog.io/@awesome-hong/Next.js-redux-store-hydrate-%EC%A4%91%EB%B3%B5-%EC%A0%80%EC%9E%A5-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@awesome-hong/Next.js-redux-store-hydrate-%EC%A4%91%EB%B3%B5-%EC%A0%80%EC%9E%A5-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Mon, 04 Apr 2022 11:55:17 GMT</pubDate>
            <description><![CDATA[<p>전체글 가져오는 페이지에서 중복이슈 발견
<img src="https://media.vlpt.us/images/awesome-hong/post/2fb51f62-f17a-4619-80f6-25f853015f66/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-04-04%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.54.41.png" alt=""></p>
<p><img src="https://media.vlpt.us/images/awesome-hong/post/95b3391a-7b4b-4168-a9e9-9e652885c9f3/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-04-04%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.18.47.png" alt="">
리덕스 로거를 보면, 액션에서는 분명
데이터가 <code>data/GET_PUBLIC_TASKS</code>가 38개, <code>data/GET_GOALS_BY_IDS</code>가 9개로 배열에 저장되어있다.</p>
<p>원하는 동작은 비어있는 <code>initial store</code>에, 이 값을 저장하는 것 이었는데 prev state를 보니 이미 38개, 9개로 저장이 되어있었다.
<img src="https://media.vlpt.us/images/awesome-hong/post/f5c0da19-8211-4e54-82d6-29b474c3af32/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-04-04%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.20.47.png" alt="">
그래서 같은 데이터가 2번 중복으로 들어가있는 이슈가 발생했다.</p>
<hr>
<p>22줄에 rootReducer를 저장하고있는 코드를 보면
<img src="https://media.vlpt.us/images/awesome-hong/post/2be48baf-39ad-405c-b21d-0c94ae2466e8/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-04-04%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.28.54.png" alt="">
액션이 <code>HYDRATE</code>인 경우에, 기존 state와 새로 들어온 incomingState를 deepmerge라이브러리를 통해 merge시킨다.</p>
<p>그런데 25줄에서 로그를 찍어보니 기존 state에 이미 원하는 값이 들어있었다. 그래서 결국 같은 값을 merge하고 있었던 것이다.</p>
<pre><code class="language-tsx">const rootReducer = (state = initialRootState, action: any) =&gt; {
  if (action.type === HYDRATE) {
    const incomingState = action.payload as RootState

    const nextState: RootState = merge(state, incomingState)

    return incomingState;
  } else {
    return combinedReducer(state, action);
  }
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Vercel 배포 중 오류 해결하기]]></title>
            <link>https://velog.io/@awesome-hong/Next.js-%EB%B0%B0%ED%8F%AC-%EC%A4%91-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@awesome-hong/Next.js-%EB%B0%B0%ED%8F%AC-%EC%A4%91-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 25 Mar 2022 07:56:47 GMT</pubDate>
            <description><![CDATA[<p>^^...;; 나름 신중히 검토하고 1차배포했다고 생각했는데 빌드과정에서 오류폭탄남
하나씩 해결해보자...</p>
<h2 id="❌-pages폴더-내에-styled파일이-있는-경우">❌ pages폴더 내에 styled파일이 있는 경우</h2>
<p>next.js 프레임워크의 경우, pages파일안에 있는 루트대로 페이지를 생성해주는 아주 편한 기능을 가지고 있다. 그에 맞춰서 룰을 잘 지켜주어야 원하는대로 빌드가 가능하다.</p>
<p>하지만 이번 프로젝트에서 <code>sylted-components</code>를 사용중이기 때문에 별 생각없이 페이지와 같이 <code>styled.ts</code> 파일을 넣어주었다.</p>
<p><img src="https://images.velog.io/images/awesome-hong/post/c1c4ae5e-5749-4fcc-8201-5cf40ff5d587/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-23%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.29.50.png" alt=""></p>
<p>빌드 failed 🥲</p>
<h3 id="✅-해결완료">✅ 해결완료</h3>
<p>기존 styles디렉토리 하위에 pages 디렉토리를 생성하고, 이 위치로 styled.ts파일들을 모두 빼주었다.</p>
<h2 id="❌-next-auth-로그인-페이지가-오류인-경우">❌ next-auth 로그인 페이지가 오류인 경우</h2>
<p><img src="https://images.velog.io/images/awesome-hong/post/9f2680b6-f084-4fd8-b1bc-581b7eb15b89/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-23%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.56.26.png" alt="">
로그인 페이지에서 <code>next-auth</code>를 사용하여, 카카오, 구글, 네이버, 페이스북의 간편로그인 기능을 사용했다. 그런데 화면이 나오지않아서, 콘솔창의 네트워크탭을 보았다.
여기에서는 500 error라고만 알려주고 자세히 어떤 에러인지 알수없다.</p>
<p>이럴때에는 vercel 사이트에서 자세히 확인해볼 수 있다.(콘솔창에도 나오긴 하는데 콘솔보다 보기편함)</p>
<p><img src="https://images.velog.io/images/awesome-hong/post/20f772c1-95ee-4140-a637-16b99a2c4851/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-25%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%204.09.26.png" alt="">
프로젝트를 배포하고보면 위 사진처럼 Functions 탭에 들어갈 수 있다.
이 탭에 들어간 상태에서 에러를 발생시키면 자세한 로그를 볼 수 있다.
아쉽게 캡쳐를 안해놔서 ㅠㅠ 지금은 없어졌는데, </p>
<pre><code>[next-auth][error][CLIENT_FETCH_ERROR]
https://next-auth.js.org/errors#client_fetch_error invalid json response body at https://EXAMPLE.com/api/auth/session reason: Unexpected token I in JSON at position 0 { error: ...</code></pre><p>검색해서 비슷한 예시를 찾았다.
나도 <code>[next-auth][error][CLIENT_FETCH_ERROR]</code> 가 나왔었다.</p>
<p>일단 이 부분은 환경변수 설정이 잘 되어있는지 먼저 확인해주어야 한다.</p>
<p>프로젝트를 작업할 때 <code>.env</code> 파일에 환경변수를 넣고 작업했을텐데, </p>
<p><img src="https://images.velog.io/images/awesome-hong/post/e59f99fa-6fd3-4131-8bad-401a16b7b59d/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-25%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%204.01.37.png" alt="">
각자의 프로젝트에서 위 페이지의 상단 <code>Settings/Environment Variables</code> 탭으로 들어가서 환경변수를 추가, 삭제, 수정할 수 있다.
당연히 <code>.env</code> 파일에 있는 환경변수들을 모두 정확히 입력해주어야 한다.
하지만 분명히 확인했는데도 오류가 난다면 ...</p>
<h3 id="✅-해결-방법">✅ 해결 방법</h3>
<p>중요한 점은, 환경변수에 <code>NEXTAUTH_URL</code>와 <code>NEXTAUTH_SECRET</code>를 꼭!!! 넣어주어야 한다.</p>
<h4 id="nextauth_url"><code>NEXTAUTH_URL</code></h4>
<ul>
<li>배포완료하고 사용될 도메인. </li>
<li>나의 경우에는 <code>https://need-compliments-sandy.vercel.app/</code>
난 이 URL에서 auth/signin으로 이동하여 로그인을 하도록 페이지를 구성했는데, 이 루트가 아닌 커스텀 패스를 추가하였다면, 다르게 지정해주어야한다.
아래 <a href="https://next-auth.js.org/configuration/options#nextauth_url">공식문서</a>를 참고했다.
<img src="https://images.velog.io/images/awesome-hong/post/f03170b1-833c-442a-8535-8b4305907425/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-25%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%204.16.18.png" alt=""></li>
</ul>
<h4 id="nextauth_secret"><code>NEXTAUTH_SECRET</code></h4>
<ul>
<li>이게 진짜 ㅠㅠ 삽질했음. </li>
<li>왜냐하면 아래 터미널창에서 볼 수 있듯이, devleopment에서는 오류가 안나고 Warning이라고만 뜬다...배포해서 프로덕트 레벨에서 꼭 있어야 함.
<img src="https://images.velog.io/images/awesome-hong/post/7044cc79-2d40-4209-a324-47668a7e40d1/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-25%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%205.23.28.png" alt="">
하.. ㅠㅠ 로그 제대로 확인하는 습관을 들여야겠다.</li>
</ul>
<p>이 부분 역시 <a href="https://next-auth.js.org/configuration/options#nextauth_secret">공식문서</a>를 참고했다.
<img src="https://images.velog.io/images/awesome-hong/post/d284cde8-11b0-4dcf-8a74-7aa9e5094341/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-25%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%204.19.12.png" alt="">
시크릿키를 어떻게 만드냐면 이 부분도 <a href="https://next-auth.js.org/configuration/options#secret">공식문서</a> 참고.
<img src="https://images.velog.io/images/awesome-hong/post/372ff21d-c6f4-4a58-aa02-734efba4ee57/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-25%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%204.21.32.png" alt=""></p>
<p>위 문서에서 말하듯이 <code>openssl rand -base64 32</code>를 터미널에서 실행시키고 나온 값을 <code>NEXTAUTH_SECRET</code> 값으로 사용하면 된다.</p>
<hr>
<p>(프로젝트가 진행중이므로 배포관련 오류가 나면 또 추가하겠음)</p>
<ul>
<li>추가: <a href="https://velog.io/@awesome-hong/vercel-%EC%9E%90%EB%8F%99%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD-%EB%A7%8C%EB%93%A4%EA%B8%B0">fork한 레포 자동최신화 + vercel로 자동배포하기</a></li>
</ul>
<hr>
<p>ref</p>
<ul>
<li><a href="https://next-auth.js.org/configuration/options">https://next-auth.js.org/configuration/options</a></li>
<li><a href="https://effectivesquid.tistory.com/entry/Base64-%EC%9D%B8%EC%BD%94%EB%94%A9%EC%9D%B4%EB%9E%80">https://effectivesquid.tistory.com/entry/Base64-%EC%9D%B8%EC%BD%94%EB%94%A9%EC%9D%B4%EB%9E%80</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>