<?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>Fri, 31 Mar 2023 10:17:19 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>개발자 왜?전</title>
            <url>https://velog.velcdn.com/images/_woogie/profile/f0075c17-443c-4505-a146-201aa8b6d1e8/image.webp</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 개발자 왜?전. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/_woogie" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[React에서 Next.js로 Migration(EP4: 조금만 더 빨리 너에게 닿기를)]]></title>
            <link>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP4-%EC%A1%B0%EA%B8%88%EB%A7%8C-%EB%8D%94-%EB%B9%A8%EB%A6%AC-%EB%84%88%EC%97%90%EA%B2%8C-%EB%8B%BF%EA%B8%B0%EB%A5%BC</link>
            <guid>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP4-%EC%A1%B0%EA%B8%88%EB%A7%8C-%EB%8D%94-%EB%B9%A8%EB%A6%AC-%EB%84%88%EC%97%90%EA%B2%8C-%EB%8B%BF%EA%B8%B0%EB%A5%BC</guid>
            <pubDate>Fri, 31 Mar 2023 10:17:19 GMT</pubDate>
            <description><![CDATA[<p>이번 EP4은 Next.js에서 SSR을 어떻게 더 빠르게 처리할 수 있는지에 관한 이야기입니다.</p>
 <br/>

<h2 id="고민">고민</h2>
<blockquote>
<p>혹시 <code>getStaticProps</code> 사용해보셨나요?</p>
</blockquote>
<h3 id="getstaticprops">GetStaticProps</h3>
<p><code>getStaticProps</code>, SSG가 필요한 페이지에 해당 메소드를 사용합니다. 근데 사용해보신적이 있으신가요? 블로그를 개발할 때도 “SSG할거면 차라리 gatsby를 쓰지”라며 저는 사용해 본 적이 없습니다. 프로덕트에서 사용한다면 소개페이지 정도에 알맞겠군요. <a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-props">getStaticProps</a>에 관해 읽다보면 구미가 확 당기는 기능이 있습니다. 바로 <a href="https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration">Incremental Static Regeneration</a>이란 기능입니다.</p>
<h3 id="isrincremental-static-regeneration">ISR(Incremental Static Regeneration)</h3>
<p>해당 기능은 정적페이지를 만들거나 업데이트 하는 기능입니다. ISR을 사용하면 전체 사이트를 재구성할 필요 없이 페이지 단위로 정적 생성을 사용할 수 있습니다. 정적페이지에 그릴 데이터가 변경되었을 때 빌드와 배포를 다시하는 것이 아니라 ISR로 처리하면 자동으로 다시 <code>re-generate</code>하게됩니다.</p>
<blockquote>
<p>굉장히 편리한 기능이군요..?!</p>
</blockquote>
<p>코드는 단순히 getStaticProps return시에 <code>revalidate: number</code>만 넣어주면 됩니다.</p>
<pre><code class="language-tsx">export async function getStaticProps() {
  const res = await fetch(&#39;/someting&#39;)
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 10 seconds
    revalidate: 10, // In seconds
  }
}</code></pre>
<p>그러니까 요청(위의 코드기준으로)이 들어올 때 10초에 1번 re-generate를 하는겁니다. </p>
<blockquote>
<p>SSR에서 generate를 하지 못해도 fetching결과를 캐싱하는 건 어떨까요?</p>
</blockquote>
<br/>

<h2 id="getcachedserversideprops">getCachedServerSideProps</h2>
<p>fetching결과를 캐싱해봅시다.</p>
<h3 id="node-cache">node-cache</h3>
<p>먼저 <a href="https://www.npmjs.com/package/node-cache">node-cache</a>라는 라이브러리를 사용할겁니다. Node.js 내부 캐싱 라이브러리입니다.</p>
<p>이 때 사용하기에 앞서 TTL에 대해 알아두면 좋습니다. TTL은 Time To Live의 약자로 생존시간 정도로 생각해두시면 됩니다. 캐시된 데이터의 유효시간인거죠.</p>
<h3 id="code">Code</h3>
<p>일단 아주 단순하게 코드를 짜볼겁니다. 먼저 NodeCache를 생성합니다.</p>
<pre><code class="language-tsx">import NodeCache from &quot;node-cache&quot;;

const globalCache = new NodeCache({
  stdTTL: 60 * 10,
  checkperiod: 600,
});</code></pre>
<p>option을 살펴보면 <code>stdTTL</code>은 standard TTL을 의미합니다. 기본 TTL값인거죠. <code>checkpreiod</code>는 자동으로 만료된 캐시들을 삭제하는 기간을 의미합니다. 즉 10분마다 돌면서 만료된 캐시를 삭제하는 것이죠.</p>
<p>다음으로는 <code>getServerSideProps</code>에 쓰일 고차함수를 만들겠습니다.</p>
<pre><code class="language-tsx">import { GetServerSidePropsContext, GetServerSidePropsResult } from &quot;next&quot;;
import NodeCache from &quot;node-cache&quot;;

const DEFAULT_TTL = 60 * 10; // 10분

const globalCache = new NodeCache({
  stdTTL: DEFAULT_TTL,
  checkperiod: 100,
});

const getCachedServerSideProps = ({
  getServerSideFunc,
  key,
  ttl = DEFAULT_TTL,
}: {
  getServerSideFunc: (ctx: GetServerSidePropsContext) =&gt; Promise&lt;GetServerSidePropsResult&lt;{ [key: string]: any }&gt;&gt;;
  key: string;
  ttl?: number;
}) =&gt; async (ctx: GetServerSidePropsContext) =&gt; {
  const cache = globalCache.get(key);

  if (cache) {
    return cache;
  }

  const ret = await getServerSideFunc(ctx);  
  globalCache.set(key, ret, ttl);

  return ret;
};

export default getCachedServerSideProps;</code></pre>
<p>SSR시에 fetch할 함수를 받고, 캐시의 key값 그리고 해당 캐시의 ttl을 별도로 받을 수 있게 만들었습니다. 코드가 실행되면 먼저 cache가 있는지 확인하고 있으면 return, 만약에 없다면 fetch를 하고 해온 값을 캐싱해줍니다. 이후 return.</p>
<p>페이지에서 사용할 때는 다음과 같이 사용하면 됩니다.</p>
<pre><code class="language-tsx">export const getServerSideProps = getCachedServerSideProps({
  key: &#39;dog&#39;,
  getServerSideFunc: async () =&gt; {
    const res = await fetch(&#39;https://dog.ceo/api/breeds/image/random&#39;);
    const data = await res.json();
    return {
      props: {
        image: data.message,
      },
    };
  },
  ttl: 60,
});</code></pre>
<p>이렇게 처리하면 60초동안 캐싱된 데이터를 계속 불러오게 됩니다.</p>
<h3 id="짜잔">짜잔!</h3>
<p><img src="https://velog.velcdn.com/images/_woogie/post/a9daf4f9-de6b-4a69-9609-9a8938ee7762/image.png" alt=""></p>
<p>랜덤한 강아지 사진을 가져오는 <a href="https://dog.ceo/api/breeds/image/random">api</a>인데요, 보시다시피 계속 같은 값을 가져옵니다!</p>
<br/>

<h2 id="개선사항">개선사항</h2>
<p>단순한 코드이니만큼 개선사항이 있습니다. </p>
<ol>
<li><a href="https://www.npmjs.com/package/node-cache#get-ttl-getttl">getTtl</a> 메소드를 활용해서 현재 시간과 비교해 <code>checkperiod</code>를 기다리는게 아니라 만료된 경우 바로 새로운 값으로 바꿔주는 겁니다. 개별 캐시마다 만료시간이 지나면 업데이트를 하도록 할 수 있을 것 같습니다.</li>
<li>key를 단순 string이 아닌 함수로 만들어 <code>dynamic routing</code>에 대응하도록 할 수 있습니다. <code>GetServerSidePropsContext</code> 를 매개변수로 넘겨주면 됩니다.</li>
</ol>
<h2 id="회고">회고</h2>
<p>SSR시 fetch data에 대해 조금이라도 속도를 개선할 수 있게 되었습니다. 그러나 알맞은 TTL을 데이터에 따라 정의하는 점에서 어려움이 여전히 존재할 것 같습니다.
그리고 사용자 입장에서는 이런 경우도 발생하겠죠.</p>
<blockquote>
<p>Q: 아 데이터 수정했는데 왜 안바뀌죠?
A: 반영되는 시간이 10분정도 걸립니다.
Q: 왜요?</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서 Next.js로 Migration(EP3: 빠른 실패가 오랜 고민보단 낫다)]]></title>
            <link>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP3-%EB%B9%A0%EB%A5%B8-%EC%8B%A4%ED%8C%A8%EA%B0%80-%EC%98%A4%EB%9E%9C-%EA%B3%A0%EB%AF%BC%EB%B3%B4%EB%8B%A8-%EB%82%AB%EB%8B%A4</link>
            <guid>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP3-%EB%B9%A0%EB%A5%B8-%EC%8B%A4%ED%8C%A8%EA%B0%80-%EC%98%A4%EB%9E%9C-%EA%B3%A0%EB%AF%BC%EB%B3%B4%EB%8B%A8-%EB%82%AB%EB%8B%A4</guid>
            <pubDate>Thu, 30 Mar 2023 12:18:03 GMT</pubDate>
            <description><![CDATA[<p>이번 EP3은 Next.js로 넘어가며 상태관리를 어떻게 했는가에 관한 이야기입니다.</p>
<br/>

<h2 id="분석">분석</h2>
<p>기존 React App에서는 상태관리를 자체적으로 만들어 사용했습니다.  <code>redux</code>를 래핑해서 devTool의 장점을 살린 <code>ORM</code>이었는데요. 굉장히 파워풀했습니다. history를 구독해 페이지별 index를 관리하면서도 데이터를 normalize하여 불필요한 fetch또한 제어할 수 있었습니다. 마치 웹을 모바일 앱처럼 작동시킬 수 있었죠.</p>
<p>그런데 Next.js로 변경하면서 <a href="https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP2-%EA%B3%BC%EC%97%B0-%EC%9D%B4%EA%B2%8C-%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%84%A0%ED%83%9D%EC%9D%BC%EA%B9%8C-feat.-Auth#getserversideprops">대부분의 페이지를 SSR로 처리</a>하기에 이릅니다. 그러다보니 매 페이지마다 fetch를 해야해 결국 기존의 자체상태관리 라이브러리는 더 이상 필요가 없다고 느껴졌습니다. </p>
<blockquote>
<p>애초에 해당 라이브러리는 오픈소스가 아니어서 유지보수나 DOCS 관리 등에 어려움이 있었습니다.</p>
</blockquote>
<p>더불어 백엔드가 <code>REST</code> 기반에서 <code>GraphQL</code>기반으로 web API가 변화함에 따라 프론트 상태관리에도 변화의 바람이 불게됩니다.</p>
<br/>

<h2 id="graphql">GraphQL</h2>
<h3 id="라이브러리">라이브러리</h3>
<p>먼저 GQL로 변경됨에 따라 GQL라이브러리에 대해 고민하게됩니다. 모두가 그렇듯 Apollo, Relay, urql 3가지를 놓고 고민했습니다. 아무래도 relay나 urql은 자료가 적었는데요, 반면 Apollo에서는 정규화도 지원하고 정보도 많이 있었기에 Apollo로 결정되는 듯 했습니다.</p>
<p>그러나 여전히 REST형태로 처리되는 API가 있다는 점에서 상태관리에 둘 이상의 라이브러리를 사용하는데에 반감을 가지게 되었습니다. 그래서 <code>react-query</code>로 눈을 돌리게 됩니다. 또한 정규화도 필요하지 않았구요.</p>
<h3 id="react-query">React-query</h3>
<p>react-query는 REST와 GQL모두 처리가 가능했습니다. GQL은 <a href="https://www.npmjs.com/package/graphql-request">graphql-request</a>를 사용하긴 했지만 꼭 필요하진 않습니다.(POST method로 올바른 query를 담아 보내면 됩니다.)</p>
<blockquote>
<p>추후에는 <a href="https://the-guild.dev/graphql/config">graphQL config</a>와 <a href="https://the-guild.dev/graphql/codegen">codegen</a>을 사용해서 타입을 관리했습니다.</p>
</blockquote>
<p>또한 data캐싱과 서버상태 업데이트의 편의성 등 좋은 개발경험으로 인해 선택하게 되었습니다. 또한 <a href="https://tanstack.com/query/v4/docs/react/examples/react/nextjs">Next.js와 함께 사용하는 법</a>등 DOCS를 통해 많은 정보를 제공하고 있었습니다.</p>
<p>추가적으로 react-query를 전역상태관리로 사용하기로 결정했습니다. 앞선 상태관리를 위해 여러개의 라이브러리를 사용하지 않는 결정이 이어진 것이죠. 값을 관리하기 위한 도구가 하나로 통일되면 편하지 않느냐는 의견이었습니다. 또한 react-query내에서도 <a href="https://tanstack.com/query/v4/docs/react/guides/does-this-replace-client-state">전역상태관리로 사용하는데 문제는 없다</a>고 설명하기도 했기에 일단 빠르게 시도해봤습니다.</p>
<br/>

<h2 id="문제점">문제점</h2>
<h3 id="전역상태관리">전역상태관리</h3>
<p>전역상태관리로 react-query는 문제가 있었습니다. 먼저 프로덕트 전반에 필요한 메타데이터를 받아오는 작업일 경우 페이지 접근 당 한 번만 필요합니다. 만약에 SSR이 필요한 페이지라면 <code>getServerSideProps</code>에서 처리하게됩니다. 그리고 이 때 <code>staleTime</code>과 <code>cacheTime</code>을 <code>Infinity</code>로 설정했습니다. 왜냐면 한 번만 요청하면 되는 데이터고 페이지가 동작하는 동안 바뀔 일이 없는 데이터니까요. 그런데 페이지가 정상적으로 그려지고 react-query devtool로 해당 상태를 살펴보면 5분으로 초기화됩니다. </p>
<p>이 때 클라이언트에서 useQuery를 통해 올바른 <code>key</code>와 <code>queryFunction</code>을 넣는다면 문제없이 작동할 겁니다. 그런데 개념상 한 번 만 호출해도 존재할 거라는 생각에 <code>getQueryData</code>를 사용했는데요, 이 때 페이지 유지 기간이 5분 지나면 에러가 발생했습니다. 제가 생각하기에 페이지의 사이클동안 전역상태로 관리하는 것과 react-query와는 많이 달랐습니다.</p>
<blockquote>
<p>나중에 찾아보니 react-query의 maintainer인 Tkdodo는 전역상태관리로의 사용을 원하지 않더군요. 😢</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/_woogie/post/c07335e1-d33d-43ca-acbd-7949afa8f533/image.png" alt=""></p>
<br/>

<h2 id="전역상태관리-도입zustand">전역상태관리 도입(zustand)</h2>
<p>전역상태관리가 필요함을 깨닫고 라이브러리 테스트했습니다. <del>redux는 절대 안됩니다.</del> <code>zustand</code>, <code>recoil</code>, <code>jotai</code> 3가지 라이브러리를 비교했습니다. 기존에 사용하던 redux와 비슷하고 devtool을 사용할 수 있는 zustand로 선택하였습니다. 다른 라이브러리는 일단 atom store에 대한 이해도가 적었고, recoil의 메이저 버전 등의 이유로 금방 결정되었습니다.</p>
<blockquote>
<p>추후에 연재할 현업에서 채팅기능을 개발한 이야기에서 zustand를 어떻게 사용했는지 다뤄보겠습니다.</p>
</blockquote>
<br/>

<h2 id="결론">결론</h2>
<p>이로써 Next.js를 사용하며 어떻게 전역관리를 했는지까지 다뤄보았습니다. REST와 GraphQL을 함께 사용했기 때문에 react-query는 좋은 선택이었습니다. 전역 상태관리를 추후에 필요에 의해서 도입한 것 또한 좋은 선택이었다고 생각합니다. 덕분에 server-data, client-data에 대한 이해와 react-query에 대한 컨셉을 더욱 제대로 이해할 수 있었습니다.</p>
<blockquote>
<p>일단 시도해봅시다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서 Next.js로 Migration(EP2-1: 생각보다 괜찮을지도)]]></title>
            <link>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP2-1-%EC%83%9D%EA%B0%81%EB%B3%B4%EB%8B%A4-%EA%B4%9C%EC%B0%AE%EC%9D%84%EC%A7%80%EB%8F%84</link>
            <guid>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP2-1-%EC%83%9D%EA%B0%81%EB%B3%B4%EB%8B%A4-%EA%B4%9C%EC%B0%AE%EC%9D%84%EC%A7%80%EB%8F%84</guid>
            <pubDate>Wed, 29 Mar 2023 13:09:16 GMT</pubDate>
            <description><![CDATA[<p>이번 EP2-1은 <a href="https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP2-%EA%B3%BC%EC%97%B0-%EC%9D%B4%EA%B2%8C-%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%84%A0%ED%83%9D%EC%9D%BC%EA%B9%8C-feat.-Auth">EP2</a>에서 다뤘던 “Next.js에서 어떻게 Auth를 처리했는지”에서 <strong>어떻게 보완했는지</strong>에 관한 이야기입니다.</p>
<blockquote>
<p>필요한 동작은 하지만, 이게 맞을까?</p>
</blockquote>
<h2 id="분석">분석</h2>
<p>앞선 EP2에서 언급했던 것 처럼 <code>prepareServerSideProps</code>는 많은 역할을 담당했습니다. 심지어 매 페이지 접근시 SSR으로 처리되며 사용자 경험 저하에 영향을 주었습니다. 
먼저 고민점을 해결하기 전에 먼저 이 고차함수에 대해 분석을 했습니다. </p>
<blockquote>
<p>prepareServerSideProps은 어떤 필요에 의해 만들었을까?</p>
</blockquote>
<h3 id="routing">Routing</h3>
<p>페이지에 권한별 접근을 제한하는 것에 관한 기능이었습니다.</p>
<blockquote>
<p>꼭 SSR시에 페이지 접근을 제한해야 하나요? 다른 방법은요?</p>
</blockquote>
<p>접근을 제한한 페이지라면 SEO를 걱정할 필요가 없었습니다. 더욱이 서버에서 데이터를 fetching하여 미리 페이지를 그려줄 필요가 없었죠. 그러면 page내에서 <code>useEffect</code>에서 권한을 확인 후 <code>replace</code>처리를 해도 충분했습니다. </p>
<blockquote>
<p>필요하지 않겠군요!</p>
</blockquote>
<h3 id="user">User</h3>
<p>user정보 fetching은 <code>routing</code>을 위해서 필요한 동작이었습니다. 그런데 위에서 설명드린대로 <code>routing</code>이 꼭 필요한 기능이 아니니 해당 동작또한 불필요했습니다. 프로덕트상 GNB에 user정보가 꼭 미리 그려져야한다는 조건 또한 없었습니다. Suspense 등의 적당한 UI처리를 통해 충분히 보완할 수 있는 UI기도 하고요.</p>
<blockquote>
<p>필요하지 않겠군요!</p>
</blockquote>
<h3 id="cookie">Cookie</h3>
<p>해당 함수를 만든 가장 중요한 이유였습니다. httpOnly쿠키를 꺼내와야 했기 때문이죠. </p>
<blockquote>
<p>그렇다면 꼭 SSR에 가져와야 하는 이유가 있었나?</p>
</blockquote>
<p>만약 node환경을 찾는다면? 만들어둔 <code>prepareServerSideProps</code>는 필요하지 않게될겁니다. 정말 SSR이 필요한 페이지에서는 <code>getServerSideProps</code>로 동작하겠죠.</p>
<h3 id="결국-cookie를-꺼내올-수만-있으면-이-함수는-필요하지-않을겁니다">결국 Cookie를 꺼내올 수만 있으면 이 함수는 필요하지 않을겁니다.</h3>
<br/>


<h2 id="고민">고민</h2>
<p>cookie에 대해 고민하기전에 먼저 Next.js를 다시금 돌아봤습니다.</p>
<p><img src="https://velog.velcdn.com/images/_woogie/post/62bf8811-36ce-48cb-83ec-cab490c9d35d/image.png" alt=""></p>
<p>먼저 위의 그림처럼 흐름을 그려봤습니다.</p>
<ol>
<li>Next.js 서버로부터 클라이언트(브라우저)는 source를 받습니다. 미리 그려진 HTML일 수도 JS일 수도 image일 수도 있죠.</li>
<li>Next.js 서버는 data를 가져오기 위해 backend server에 요청합니다. 예를 들어 SSR 때를 상상해보세요!</li>
<li>브라우저는 필요한 data를 가져오기 위해 backend server에 요청합니다.</li>
</ol>
<blockquote>
<p>어? 다시금 생각해보니 Next.js <strong>서버</strong>, <strong>서버</strong>입니다?!</p>
</blockquote>
<p>맞습니다. node환경이면 cookie를 가져올 수 있었습니다.</p>
<h3 id="api-routes">API Routes</h3>
<p>API Routes는 Next.js로 API를 구축할 수 있는 기능입니다. pages/api 내부에 있는 모든 파일은 /api/*에 매핑되며 페이지가 아닌 API endpoint로 처리됩니다. 또한 해당 API는 server-side bundle이기에 클라이언트 bundle사이즈를 증가시키지 않습니다.</p>
<p><img src="https://velog.velcdn.com/images/_woogie/post/6be612cb-ee6e-4a7b-917c-550e468dce1c/image.png" alt=""></p>
<p>즉, 위의 그림처럼 브라우저가 Next.js 서버로 api요청을 하고 response를 받을 수 있습니다. Cookie를 꺼낼 수 있겠군요!</p>
<br/>

<h2 id="해결">해결</h2>
<p>해당 방법을 통해 제가 생각한 방법은 2가지 입니다.</p>
<h3 id="쿠키를-가져오기">쿠키를 가져오기</h3>
<p>쿠키를 직접 받아오는 방식입니다. </p>
<p><img src="https://velog.velcdn.com/images/_woogie/post/dd270c8d-53bc-406a-a019-89142f76ff64/image.png" alt=""></p>
<ol>
<li>페이지에 접근했을 때 먼저 API Routes를 통해 자신의 token을 리턴해줍니다. </li>
<li>리턴 받은 token을 저장해두고 Bearer Auth가 필요할 때 실어서 보내는 방식입니다.</li>
</ol>
<p>예시 코드는 다음과 같습니다.</p>
<pre><code class="language-tsx">// pages/api/getToken.ts
import type { NextApiRequest, NextApiResponse } from &#39;next&#39;;

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = req.cookies?.[&#39;token&#39;];
  res.status(200).json(JSON.parse(token));
}</code></pre>
<p>API route에서는 req에서 쿠키를 꺼내 리턴해줍니다.</p>
<p>실제로 요청하는 부분은 아래와 같습니다.</p>
<pre><code class="language-tsx">const token = await axios.get(&#39;/api/getToken&#39;);</code></pre>
<h3 id="모든-요청을-서버로-하기">모든 요청을 서버로 하기</h3>
<p>Bearer Auth가 필요한 모든 요청을 Next.js서버로 요청하는 방식입니다.</p>
<p><img src="https://velog.velcdn.com/images/_woogie/post/b3effd08-6496-4f8a-8b9e-4bbaacf6abf2/image.png" alt=""></p>
<ol>
<li>클라이언트에서 필요한 data를 API Route를 통해 요청한다.</li>
<li>Next.js서버에서 req의 cookie를 꺼낸다.
 a. 진짜 data를 요청해야하는 api에 call한다.
 b. 가져온 data를 클라이언트에 return해준다.</li>
</ol>
<p>예시 코드는 다음과 같습니다. 해당 예시는 <code>GET method</code>일 때 입니다. </p>
<pre><code class="language-tsx">// pages/api/get.ts
import type { NextApiRequest, NextApiResponse } from &#39;next&#39;;

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = req.cookies?.[&#39;token&#39;];
    const targetUrl = req.query.url as string;
    const data = await axios(targetUrl, {
            headers: { Authorization: token },
        });
  res.status(200).json(data);
}</code></pre>
<br/>

<h2 id="결론">결론</h2>
<p>1번 해결법은 요청 타이밍에 관한 문제, token을 클라이언트단에서 계속 관리해야한다는 점에서 2번 방법으로 처리하였습니다. 이로써 Bearer Authentication를 처리할 수 있었습니다.</p>
<p>프론트엔드 개발에 있어서 브라우저는 큰 제약으로 다가올 때가 많습니다. 이 때 위의 방법처럼 우회하여 여타 다른 문제도 해결할 수 있을겁니다.(마치 <a href="https://velog.io/@_woogie/Open-AI%EB%A1%9C-%EB%82%98%EB%A7%8C%EC%9D%98-%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0#api-routes">Open AI로 비서 만들기</a>에서 처럼요!)</p>
<br/>

<h2 id="추가작업">추가작업</h2>
<h3 id="ssr">SSR</h3>
<p>그러나 여전히 SSR시에는 context argument에서 쿠키를 직접 꺼내서 Bearer에 실어서 보내야합니다. </p>
<h3 id="middleware">Middleware</h3>
<p>페이지 접근 권한을 막는 방법은 <a href="https://nextjs.org/docs/advanced-features/middleware">middleware</a>를 통해 처리할 수 있었습니다. 접근이 제한이 필요한 URL을 리스트업해두고 해당 페이지에 접근하면 user정보를 가져와 페이지의 접근을 막았습니다. 페이지 접근 후 <code>useEffect</code>처리보다 우아하게 처리할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서 Next.js로 Migration(EP2: 과연 이게 올바른 선택일까? feat. Auth)]]></title>
            <link>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP2-%EA%B3%BC%EC%97%B0-%EC%9D%B4%EA%B2%8C-%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%84%A0%ED%83%9D%EC%9D%BC%EA%B9%8C-feat.-Auth</link>
            <guid>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP2-%EA%B3%BC%EC%97%B0-%EC%9D%B4%EA%B2%8C-%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%84%A0%ED%83%9D%EC%9D%BC%EA%B9%8C-feat.-Auth</guid>
            <pubDate>Tue, 28 Mar 2023 09:08:10 GMT</pubDate>
            <description><![CDATA[<p>이번 EP2은 Next.js에서 어떻게 Auth를 처리했는지에 관한 이야기입니다.</p>
<br/>

<h2 id="auth-분석">Auth 분석</h2>
<p>Next.js를 기반으로하는 새로운 프로젝트 V2가 시작되고 제가 진행한 일은 Auth였습니다. 당장 유저정보를 불러와 GNB에 그리고 싶었거든요.</p>
<blockquote>
<p>내 유저 정보를 어떻게 알죠?
내가 누군지 정보를 담아서 Call하면 되죠!
그러면 정보를 어떻게 담죠?</p>
</blockquote>
<h3 id="bearer-authentication">Bearer Authentication</h3>
<p>프로덕트는 Bearer Authentication방식으로 개발되었습니다. 인증에 필요한 token은 로그인시에 쿠키로 발급받습니다. Login page는 V1에서 유지했는데요. 도메인이 같기 때문에 발급받은 token이 담긴 Cookie를 어떻게 다룰 것인가가 가장 중요한 문제였습니다. 왜냐하면 Cookie는 <code>httpOnly</code> 거든요.</p>
<blockquote>
<p>참고로 서브도메인에 대한 보안상의 이유로 credential로 처리하지 않았습니다.</p>
</blockquote>
<h3 id="httponly🍪">HttpOnly🍪</h3>
<p>다들 알고 계시듯이 httponly쿠키는 <strong>브라우저에서 접근이 불가</strong>능합니다. 이 말은 다시 생각해보면 클라이언트단에서 유저가 무언가 요청할 때 쿠키를 가져와 Bearer에 실을 수 없다는 말과 같습니다. 그러면 유저 정보를 가져오거나 혹은 권한이 필요한 요청을 할 수 없습니다.</p>
<br/>

<h2 id="쿠키를-가져오자">쿠키를 가져오자!</h2>
<blockquote>
<p>브라우저 환경에서 가져올 수 없다면 node환경에서는 되겠구나!</p>
</blockquote>
<h3 id="node">Node</h3>
<p>Next.js로 개발하고 있었기 때문에 node환경을 찾는건 어렵지 않았습니다. <a href="https://nextjs.org/docs/basic-features/data-fetching/overview">Data-Fetching</a>시점에 context를 활용하면 request속에 담긴 쿠키를 가져오는 건 어렵지 않았기 때문이죠. 그러면 여러 data-fetching중 어떤 것을 사용해야할지 고민했습니다.</p>
<h3 id="getinitialprops">getInitialProps</h3>
<p>가장 먼저 고민했던 부분은 <code>어떤 페이지에 접속하든</code>이었습니다. 해당 페이지가 SSR이든 CSR이든 쿠키를 가져와 클라이언트단에서 사용할 수 있어야했거든요. 그래서 처음 생각이 난 곳은 <code>_app</code>이었습니다. 모든 페이지를 initial하는 곳이기 때문이죠. 
<code>_app</code>에서는 data-fetching-methods 중 유일하게 <code>getInitialProps</code>만을 사용할 수 있었습니다. 해당 메소드에서 제공되는 context argument에서 httpOnly쿠키를 가져올 수 있었습니다.</p>
<blockquote>
<p>뭐야 끝난건가?</p>
</blockquote>
<p>그러나 문제가 있었습니다.
일단 <code>getInitialProps</code>는 번들링될 때 컴포넌트와 분리되지 않습니다. 다시말해 클라이언트단에서도 해당 코드가 실행됩니다. 그래서 클라이언트단에서 실행될 때는 예외처리가 필요합니다(쿠키를 가져올 수 없을테니까요). 또한 어떤 페이지에서 SSR이 필요해 <code>getServerSideProps</code>를 사용했다면 <code>_app</code>에서 빼낸 token(getInitialProps로부터 가져온)을 전달할 수 없습니다. 만약 SSR페이지에서 <code>getInitialProps</code>를 사용하면 처리할 수 있지만 위에서 언급한대로 번들링 때 포함돼 불필요하게 파일 사이즈가 커집니다. 그럼에도 <code>getServerSideProps</code>를 사용하려면 따로 ctx에서 쿠키를 가져오는 로직이 필요하게되어 코드가 중복 존재하게됩니다. 또한 <a href="https://nextjs.org/docs/advanced-features/automatic-static-optimization">ASO</a>를 사용할 수 없는 이유 등으로 다른 방법을 찾아보게 되었습니다.</p>
<h3 id="getserversideprops">getServerSideProps</h3>
<p><code>getInitialProps</code>의 문제를 봤을 때 node환경(server)에서만 실행되어야 한다는 조건이 붙으면 좋겠다는 생각을 했습니다. 그렇다면 접근하기 가장 쉬운방법은 <code>getServerSideProps</code>를 사용하는 방안이었습니다. 그런데 <code>_app</code>에서는 <code>getServerSideProps</code>를 사용할 수 없습니다. 그리하여 조금은 괴랄(?)하지만 모든 페이지에 <code>getServerSideProps</code>를 사용하면 되는거 아니야?라는 생각을 하게됩니다. </p>
<br/>

<h2 id="prepareserversideprops">PrepareServerSideProps</h2>
<p>이제 getServerSideProps를 감싼 HoF을 제작하기에 이릅니다.</p>
<blockquote>
<p>당시에는 최선의 고민이었습니다.
이어서 연재할 &quot;어떻게 수정했는지&quot;까지 지켜봐주세요!</p>
</blockquote>
<p>해당 함수에는 여러 기능이 포함되었습니다. 다음과 같은 작업순서를 가집니다.</p>
<h3 id="cookie">Cookie</h3>
<p>원래의 목표 httpOnly쿠키를 먼저 가져와야 했습니다. 그래야 해당 쿠키로 Bearer에 실어서 요청할 수 있을테니까요. <code>getServerSideProps</code>의 argument로 부터 쿠키를 너무나 쉽게 가져올 수 있었습니다.</p>
<blockquote>
<p>쿠키로부터 가져온 토큰을 저장할 store는 추후에 작성할 예정입니다. 
이번 글은 httpOnly쿠키를 어떻게 꺼낼까에 초점을 맞춰주시면 됩니다.</p>
</blockquote>
<h3 id="user">User</h3>
<p>쿠키를 가져왔으니 그 다음은 유저정보를 가져왔습니다. 유저정보를 get해온 이유는 프로덕트에 유저가 가질 수 있는 여러 역할이 있었기 때문입니다. 이 역할이 페이지의 접근을 제한하기 때문입니다. </p>
<h3 id="routing">Routing</h3>
<p>이제는 가져온 유저정보를 바탕으로 routing을 처리해줬습니다. 해당 페이지에 접근할 수 있는지 없는지, 만약 접근할 수 없다면 <code>redirect</code>객체를 early return하여 페이지의 접근을 막았습니다. 이를 통해 접근할 수 없는 정보에 대해서도 미리 처리하면서 동시에 페이지의 접근을 막았습니다.</p>
<h3 id="fetch">Fetch</h3>
<p>이제 코드가 해당 부분까지 진행되면 페이지를 구성하는데에 필요한 정보를 가져오는 fetch를 진행합니다. fetch가 완료되면 불러온 정보를 props로 내려주게됩니다.</p>
<h3 id="code">Code</h3>
<p>이로써 <code>prepareServerSideProps</code>함수는 <code>getServerSideProps</code>에 알맞은 return 객체를 반환하는 함수가 됩니다. 개략적인 코드는 다음과 같습니다.</p>
<pre><code class="language-tsx">const prepareServerSideProps =
  ({ getServerSidePropsFunc, accessibleRoles }) =&gt;
    async (ctx) =&gt; {
        const token = getCookie(ctx);
        const user = await getUser(token);
        if (!accessibleRoles.includes(user.role)) {
            return {
                redirect: {
          destination: &#39;/403&#39;,
          permanent: false,
        },
            }
        }
        const data = await getServerSidePropsFunc(token);
        return {
            props: {
                data,
                token,
            },
        }
    };

// 사용부 pages/index.tsx
const HomePage = () =&gt; {
    return &lt;HomeComponent /&gt;
};

export const getServerSideProps = prepareServerSideProps({
    accessibleRoles: [&#39;admin&#39;],
});</code></pre>
<h2 id="이게-최선이니">이게 최선이니?</h2>
<p>작업을 하면서 매 순간 고민했습니다. 
이게 맞을까?
이렇게 하면 next서버에 부하가 크지 않을까?
꼭 모든 페이지가 SSR이어야 할까?
그래서 여러 고민을 통해 조금 더 나은 방식으로 수정했습니다. 어떻게 수정했는지는 다음화에 이어 작성하겠습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서 Next.js로 Migration(EP1: 누구나 피치 못할 사정이 있다)]]></title>
            <link>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP1-%EB%88%84%EA%B5%AC%EB%82%98-%ED%94%BC%EC%B9%98-%EB%AA%BB%ED%95%A0-%EC%82%AC%EC%A0%95%EC%9D%B4-%EC%9E%88%EB%8B%A4</link>
            <guid>https://velog.io/@_woogie/React%EC%97%90%EC%84%9C-Next.js%EB%A1%9C-MigrationEP1-%EB%88%84%EA%B5%AC%EB%82%98-%ED%94%BC%EC%B9%98-%EB%AA%BB%ED%95%A0-%EC%82%AC%EC%A0%95%EC%9D%B4-%EC%9E%88%EB%8B%A4</guid>
            <pubDate>Mon, 27 Mar 2023 09:12:20 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>현업에서 <a href="https://react.dev/">React</a>에서 <a href="https://nextjs.org/">Next.js</a>로 어떻게 migration을 했는지에 대해 연재를 시작합니다.
그 때 당시의 상황설명과 의사결정 과정에 초점을 맞춰 작성했습니다. 
누군가의 의사결정에 조금의 도움이 되길 바라며.</p>
</blockquote>
<p>이번 EP1은 <strong>어떻게</strong> Next.js로 migration하게 되었는지에 관한 이야기입니다.</p>
<br/>

<h2 id="발단">발단</h2>
<p><img src="https://velog.velcdn.com/images/_woogie/post/e5a1591c-5d5e-40b2-93cd-e1c72daad9ef/image.jpg" alt=""></p>
<h3 id="기존-react-app-분석">기존 React app 분석</h3>
<p>꽤 오래전 Next.js가 대세가 아닐 시절에 프로덕트는 시작되었습니다. 그러다보니 React로만 SSR을 직접 처리했습니다. 동작방식은 글로 작성하니 꽤 간단해보이는데요,</p>
<p>Next.js처럼 먼저 express 서버를 띄웁니다.
요청이 들어오면 페이지 구성에 필요한 데이터를 fetch 후 redux store 형태에 알맞게 가공합니다.
가공된 데이터를 encode하고 string화합니다.
해당 데이터를 변수로 가지고 있는 <code>script tag</code>를 만드는데요, 이 때 <code>text HTML</code>로 만들어둡니다.
또한 별도의 변수를 두어 해당 페이지가 SSR이 필요한지 HTML에 함께 내려주게됩니다.
이후에 클라이언트에서는 해당 파일을 받고 SSR 변수를 통해 true일 경우 ReactDOM의 <code>hydrate</code>를 사용하고 아닐경우에 ReactDOM의 <code>render</code> 메소드를 통해 그리게 됩니다.</p>
<p><del>간단하죠?</del></p>
<p>단순한 설명으로도 벌써 문제가 많이 있을 것 같습니다. 그래서 데이터가 외부에 직접 노출되지 않도록 난독화 부터 코드 스플리팅, cache, static file처리 등 웹페이지 서빙에 관한 모든 작업을 직접 처리했습니다.</p>
<h3 id="회사의-성장">회사의 성장</h3>
<p>이렇게 React만으로도 꽤나 멋진 SSR환경을 제공했습니다. 그러나 회사가 성장하고 투자를 받으며 많은 프론트엔드 개발자가 입사하게 됩니다. 8명의 프론트엔드 팀이 되었고, 나중에는 각기 다른 스프린트에 참여하게 되었습니다. 이러다보니 자체적으로 만든 React App의 동작방식에 대한 이해의 시간은 부족하게 되었고 자연스레 프레임워크에 대한 수요가 생길 수 밖에 없었습니다. </p>
<Br/>

<h2 id="전개">전개</h2>
<p>Next.js의 선택은 당연한 수순이었습니다. 신규입사자들이 대부분 다뤄봤고 문제 발생시 많은 정보를 찾을 수 있었고 무엇보다 강력한 Docs가 있었기에 Next.js로의 migration은 결정되었습니다. </p>
<h3 id="어떻게-migration을-할까요">어떻게 migration을 할까요?</h3>
<p>먼저 현재의 app에 곧바로 Next.js를 이식해봤습니다. install하고 <code>_app</code>과 <code>_document</code>파일에 필요한 코드를 모두 옮겼습니다. <code>redux</code>부터 기존에 SSR시에 style을 위해 사용하던 <code>ServerStyleSheet</code>까지 최대한 옮겨보았습니다.</p>
<blockquote>
<p>메인페이지를 띄우는 것까지는 대성공!</p>
</blockquote>
<p>그러나 기존에 render를 위해 express server에서 사용되던 코드에는 페이지를 구성하는데에 필수적인 코드가 많았는데요, 이에따라 여러 복합적인 문제가 발생했습니다. 이후에 여러 시도를 했지만 여전히 문제가 있었고  migration하는 동안 sprint가 진행될 수 없다는 점, 프론트 팀 전원이 참여할 필요가 없다는 이유 아래 새로운 레포지토리를 파는 방향으로 결정됩니다.</p>
<h3 id="v2">V2</h3>
<p>이로써 Next.js를 기반으로 하는 새로운 version, V2가 시작되었습니다. <strong>기존 V1(react app)을 유지하면서 개발될 페이지들은 V2(Next.js)에서 개발되는 형식입니다.(V1페이지가 개발될 경우 V2로 Migration 후 개발)</strong> 이렇게되면 하나의 도메인에서 2종류의 서버가 연결되어 있는 상태입니다. 그래서 임의의 URL이 들어왔을 때 알맞은 app에서 처리되도록 작업을 해줘야 했습니다. 기본적으로 CSR환경일 때는 만약 다른 app의 route일 경우 location.href를 변경해 완전히 새로운 페이지가 렌더되도록 처리했습니다. SSR환경일 때는 React에서는 기존에 express로 서버를 띄우고 있었기에 <a href="https://www.npmjs.com/package/http-proxy-middleware">http-proxy-middleware</a>를 활용해 V2로 요청이 되도록 처리했으며, Next.js에서 또한 <a href="https://nextjs.org/docs/advanced-features/custom-server">Custom Server</a>를 활용하여 V1으로 proxy되도록 처리했습니다.</p>
<p>사실 migration은 이제 시작입니다. </p>
<br/>

<h2 id="커밍쑨">커밍쑨!</h2>
<p>다음 에피소드는 Authentication과 Authorization에 관한 이야기입니다. 새로운 프로젝트에서 어떻게 권한을 확인하여 auth 요청을 보냈을까요?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[body-scroll-lock 사용법(feat. 모달 내부에서 원하는 요소는 스크롤 가능하도록)]]></title>
            <link>https://velog.io/@_woogie/body-scroll-lock-%EC%82%AC%EC%9A%A9%EB%B2%95feat.-%EB%AA%A8%EB%8B%AC-%EB%82%B4%EB%B6%80%EC%97%90%EC%84%9C-%EC%9B%90%ED%95%98%EB%8A%94-%EC%9A%94%EC%86%8C%EB%8A%94-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B0%80%EB%8A%A5%ED%95%98%EB%8F%84%EB%A1%9D</link>
            <guid>https://velog.io/@_woogie/body-scroll-lock-%EC%82%AC%EC%9A%A9%EB%B2%95feat.-%EB%AA%A8%EB%8B%AC-%EB%82%B4%EB%B6%80%EC%97%90%EC%84%9C-%EC%9B%90%ED%95%98%EB%8A%94-%EC%9A%94%EC%86%8C%EB%8A%94-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B0%80%EB%8A%A5%ED%95%98%EB%8F%84%EB%A1%9D</guid>
            <pubDate>Thu, 23 Mar 2023 08:09:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>리액트를 기반으로 작성된 글입니다.</p>
</blockquote>
<h2 id="문제">문제</h2>
<h3 id="ios">IOS</h3>
<p>우리는 흔히 모달을 띄울 때 모달 뒤 body영역을 dim 처리를 하고 스크롤이 되지 않도록 처리합니다. 해당 처리는 꽤 많은 방법을 가집니다. 그러나 이러한 처리방법은 IOS에서 문제가 발생합니다. 모달내의 컨텐츠가 길어져 스크롤이 필요할 때 스크롤이 되지 않는 문제가 바로 그것입니다. </p>
<br/>

<h2 id="목표">목표</h2>
<p>그렇다면 우리가 해결해야할 문제는</p>
<ol>
<li>모달이 띄워졌을 때 <strong>body가 스크롤 되지 않도록</strong> 처리한다.</li>
<li>IOS에서 띄워진 모달 내부의 컨텐츠가 길어질 경우 <strong>모달 내부가 스크롤</strong>이 되도록 한다.</li>
</ol>
<br/>

<h2 id="해결">해결</h2>
<h3 id="body-scroll-lock">body-scroll-lock</h3>
<p>body를 막기위한 방법은 많지만 라이브러리로 간단히 해결 가능하기에 <a href="https://www.npmjs.com/package/body-scroll-lock">body-scroll-lock</a>이라는 라이브러리를 사용합니다.</p>
<blockquote>
<p>어? 이거 사용하면 IOS에서 스크롤 안되는데..?</p>
</blockquote>
<p>네 맞습니다. 아마 해당 라이브러리 사용법중 allowTouchMove를 처리하시지 않으셨을겁니다.</p>
<h3 id="allowtouchmove">allowTouchMove</h3>
<p><a href="https://github.com/willmcpo/body-scroll-lock#allowtouchmove">Readme</a>설명을 읽어봅시다. 
<img src="https://velog.velcdn.com/images/_woogie/post/d1710fd4-64c1-48dc-a0a9-39f8e480304b/image.png" alt=""></p>
<blockquote>
<p>iOS에서 스크롤을 막기 위해 <code>disableBodyScroll</code> 을 처리하면 <code>touchmove</code> 또한 막힙니다. 그러나 element에 대해 <code>disableBodyScroll</code>을 호출했지만 element의 자식은 <code>touchmove</code>가 필요한 경우가 있습니다.</p>
</blockquote>
<p>딱 저희에게 필요한 내용이네요. disableBodyScroll을 처리하여 body를 막았지만 자식요소에서 스크롤이 필요한 경우(modal 내부의 스크롤)말이죠!</p>
<p>이제 Readme의 코드를 보면</p>
<p><img src="https://velog.velcdn.com/images/_woogie/post/55f161e3-2784-4f16-ac85-34bc72d8addb/image.png" alt=""></p>
<p>disableBodyScroll함수를 실행할 때 <code>body-scroll-lock-ignore</code> 라는 attribute가 있는지 없는지 체크해서 touchmove를 허용하는지 결정하고있네요!</p>
<blockquote>
<p>아 참고로 touchmove는 자바스크립트 event의 한 종류입니다.
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/touchmove_event">touchmove event 링크</a></p>
</blockquote>
<p><code>body-scroll-lock-ignore</code> 라는 attribute를 모달 내부에 스크롤이 필요한 요소에 넣어주면 되겠군요! </p>
<h3 id="codehook">Code(hook)</h3>
<p>먼저 <code>body-scroll-lock-ignore</code> 라는 attribute를 overflow때 반환하는 훅을 만들어주겠습니다.</p>
<p>그런데 리액트를 다뤄보신 분이라면 아시겠지만, <code>unknow DOM attributes</code>를 리액트는 무시해버립니다. 그래서 <code>data-</code> 혹은 <code>aria-</code> prefix가 붙은 attribute 명을 사용해야 합니다. <a href="https://ko.reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html#should-i-keep-data-in-custom-attributes">링크</a></p>
<pre><code class="language-tsx">export const BODY_SCROLL_LOCK_IGNORE = &#39;data-body-scroll-lock-ignore&#39;;</code></pre>
<p>이제 요소의 스크롤 필요 여부를 체크하여 attribute를 반환하는 훅을 작성하겠습니다. 스크롤 필요 여부를 체크하고 알맞은 attribute(<code>BODY_SCROLL_LOCK_IGNORE</code>)를 내려주기위해 hook 사이클에 알맞는 <a href="https://legacy.reactjs.org/docs/refs-and-the-dom.html#callback-refs">callbackRef</a>로 처리했습니다.</p>
<pre><code class="language-tsx">import {
  useCallback,
  useState,
} from &#39;react&#39;;

export const BODY_SCROLL_LOCK_IGNORE = &#39;data-body-scroll-lock-ignore&#39;;

function useAllowTouchMove&lt;T extends HTMLElement&gt;() {
  const [overflow, setOverflow] = useState(false);

  const measuredRef = useCallback((node: T) =&gt; {
    if (node !== null) {
      if (node.getBoundingClientRect().height &lt; node.scrollHeight
                || node.getBoundingClientRect().width &lt; node.scrollWidth) {
        setOverflow(true);
      } else {
        setOverflow(false);
      }
    }
  }, []);
}

export default useAllowTouchMove;</code></pre>
<p><code>measuredRef</code>라는 callbackRef를 만들었습니다. 해당 Ref는 이제 원하는 component ref에 넣어주면 해당 요소가 overflow상태 인지 체크하는 역할을 할 것입니다. 이제 attribute를 overflow에 알맞게 만들어주면 됩니다.</p>
<pre><code class="language-tsx">import {
  useCallback,
  useMemo,
  useState,
} from &#39;react&#39;;

export const BODY_SCROLL_LOCK_IGNORE = &#39;data-body-scroll-lock-ignore&#39;;

function useAllowTouchMove&lt;T extends HTMLElement&gt;() {
  const [overflow, setOverflow] = useState(false);

  const measuredRef = useCallback((node: T) =&gt; {
    if (node !== null) {
      if (node.getBoundingClientRect().height &lt; node.scrollHeight
                || node.getBoundingClientRect().width &lt; node.scrollWidth) {
        setOverflow(true);
      } else {
        setOverflow(false);
      }
    }
  }, []);

  const allowTouchMove = useMemo(() =&gt; {
    if (overflow) {
      return { [BODY_SCROLL_LOCK_IGNORE]: &#39;true&#39; };
    }
    return {};
  }, [overflow]);

  return [measuredRef, allowTouchMove];
}

export default useAllowTouchMove;</code></pre>
<p>이제 hook을 만들었는데 어떻게 쓰나요? 아직 앞으로 2가지를 더 해야합니다. 먼저 앞서 말씀드렸던 disableBodyScroll함수가 실행될 때 allowTouchmove를 처리해줘야합니다.</p>
<h3 id="codedisablebodyscroll-func">Code(disableBodyScroll func)</h3>
<p>보통 <code>body-scroll-lock</code> 라이브러리를 사용할 때 다음과 같이 사용하셨을 겁니다.</p>
<pre><code class="language-tsx">const body = document.querySelector(&#39;body&#39;) as HTMLElement;
useEffect(() =&gt; {
  disableBodyScroll(body);

  return () =&gt; {
    enableBodyScroll(body);
  };
}, []);</code></pre>
<p>이제 이 코드에 allowTouchMove를 입혀줍시다.</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
  disableBodyScroll(,body {
    allowTouchMove: (el) =&gt; {
      while (el &amp;&amp; el !== document.body) {
        if (el.getAttribute(BODY_SCROLL_LOCK_IGNORE) !== null) {
          return true;
        }
        if (el.parentElement) {
          el = el.parentElement;
        }
      }
    },
  });
  return () =&gt; clearAllBodyScrollLocks();
}, []);</code></pre>
<p>아까 hook에서 만든 <code>BODY_SCROLL_LOCK_IGNORE</code> attribute를 체크해서 값이 있다면 return true를 하여 touchmove를 허용하는겁니다. 그리고 unmount시에는 <code>clearAllBodyScrollLocks</code> 을 처리하여 모든 bodyscrolllock을 해소해줍니다.</p>
<p>자 이제 다왔습니다.</p>
<h3 id="codecomponent">Code(Component)</h3>
<p>아까 만든 hook을 이제 원하는 요소에 넣어야겠죠?</p>
<pre><code class="language-tsx">const ExampleComponent = () =&gt; {
    // 여러개의 데이터가 들어있는 데이터 리스트
    const data = [&#39;chris&#39;, &#39;chris2&#39;, &#39;chris3&#39;, ...];
    const [ref, attribute] = useAllowTouchMove&lt;HTMLDivElement&gt;();
    return (
        &lt;div ref={ref} {...attribute}&gt;
            {data.map((v) =&gt; &lt;span&gt;{v}&lt;/span&gt;)
        &lt;/div&gt;
    )
}</code></pre>
<p>이렇게 스크롤이 필요할 요소에 hook에서 반환된 값을 넣어주면 됩니다!</p>
<h2 id="정리">정리</h2>
<p>이렇게 사용하면 IOS에서 모달이 띄워졌을 때에도 모달 내부는 스크롤이 가능하게됩니다. 사실상 라이브러리를 어떻게 사용하는가에 맞춰진 글이었습니다. 문제를 해결했지만 근본적으로 IOS에서 스타일이 적용되는 방식이 여타 다른 기기 혹은 브라우저와 어떻게 다른지 알아봐야할 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Open AI로 나만의 비서 만들기]]></title>
            <link>https://velog.io/@_woogie/Open-AI%EB%A1%9C-%EB%82%98%EB%A7%8C%EC%9D%98-%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@_woogie/Open-AI%EB%A1%9C-%EB%82%98%EB%A7%8C%EC%9D%98-%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 22 Mar 2023 10:18:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>나에 대해서 알아서 대답해주는 비서가 있으면 얼마나 좋을까?</p>
</blockquote>
<pre><code>💡 GPT에게 인격을 부여하면 되겠구나!</code></pre><p>GPT처럼 서로 대화를 주고 받는 채팅 형태로 기획했습니다.
빠르게 완성하기위해 Next.js로 개발하고 Vercel로 배포해보자! 목표가 정해졌습니다.</p>
<p><em>gpt와 관련이 적은 theme를 구현한 방식 또는 PWA에 관한 얘기는 이후에 다시 써보겠습니다.</em></p>
<br/>

<p><a href="https://gpt-secretary.vercel.app/">비서 미리보기</a></p>
<h2 id="open-ai">Open AI</h2>
<h3 id="api-key-발급">API key 발급</h3>
<p>먼저 Chat GPT를 사용하려면 <code>API key</code>가 필요합니다. <del>개발자라면 당연히 회원가입은 되어있겠죠?</del> 먼저 <a href="https://platform.openai.com/account/api-keys">api</a>를 발급받아줍니다. </p>
<p><img src="https://velog.velcdn.com/images/_woogie/post/38fd09d9-50d9-4a74-bb90-160229bc18c7/image.png" alt=""></p>
<p>Create new secret key를 누르면 발급이 됩니다. 이 때 대부분의 시크릿키가 그러하듯 한 번 밖에 볼 수 없기에 복사하셔서 사용하셔야합니다. 자 이제 모든 준비는 끝났습니다. 해당 키코드를 프로젝트 루트폴더에 <code>.env</code> 파일을 생성해 원하시는 이름의 <code>key=value</code>형태로 넣어줍니다.</p>
<blockquote>
</blockquote>
<p>💡 next.js에서는 브라우저(클라이언트) 단에서 해당 환경변수를 사용하게 될 경우에 앞에 <code>NEXT_PUBLIC</code> 이라는 prefix가 붙어야 합니다. <a href="https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser">관련링크</a></p>
<br />

<h3 id="open-ai-가이드">Open AI 가이드</h3>
<p>open ai를 통해 Text completion, Code completion, Chat completion 등의 작업을 할 수 있습니다. <code>completions</code> 이라함은 주어진 prompt를 통해 모델은 하나 이상의 예측 완료를 반환하는 작업을 말합니다. 대화를 주고받는 앱을 개발중이기에 Chat completion을 해볼겁니다. Chat models는 여러 메시지 인풋을 받고 모델이 만들어낸 메시지를 리턴하는 모델입니다. </p>
<blockquote>
<p>그러면 Text completion을 쓰면 안되는건가요?</p>
</blockquote>
<p>써도 됩니다. chat은 Multi-turn 대화를 쉽게하도록 설계되었습니다. <del>그리고 Text에 쓰이는 <code>text-davinci-003</code>이 10배 비쌉니다.</del></p>
<blockquote>
<p>네? 10배 비싸다뇨? 돈을 내야 하나요?</p>
</blockquote>
<p>네! Free trial usage가 존재합니다. 총 5달러가 credit으로 주어지고 해당 사용량 이상으로 사용하면 지불해야 사용가능합니다. Chat에 쓰일 모델인 <code>gpt-3.5-turbo</code> 은 1000토큰 당 $0.002의 아주 적은 비용이 발생합니다.</p>
<blockquote>
<p>토큰은 또 뭔가요?</p>
</blockquote>
<p>토큰은 단어조각으로 생각하시면 됩니다. API가 Prompt를 처리하기전에 input을 토큰으로 분류합니다. 토큰은 정확하게 단어조각으로 잘리지 않아 단어조각과 항상 일치하지 않습니다. 토큰에 대해 더 궁금하시다면 <a href="https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them">링크</a>를 살펴보세요!</p>
<p>*<em>💡 그러면 gpt-3.5-turbo 모델을 사용해봅시다!
*</em></p>
<br />

<h3 id="open-ai-라이브러리">Open AI 라이브러리</h3>
<p>먼저 <a href="https://www.npmjs.com/package/openai">open AI라이브러리</a>를 사용해 개발을 시작해봅시다. Readme에도 설명이 잘 되어있습니다. </p>
<pre><code class="language-jsx">const { Configuration, OpenAIApi } = require(&quot;openai&quot;);

const configuration = new Configuration({
  apiKey: process.env.위에서env파일에 작성하신 key,
});
const openai = new OpenAIApi(configuration);

const completion = await openai.createCompletion({
  model: &quot;text-davinci-003&quot;,
  prompt: &quot;Hello world&quot;,
});
console.log(completion.data.choices[0].text);</code></pre>
<p>뭐야 라이브러리를 사용하니까 벌써 끝났네..? 
그러나 Readme에 보이는 예시는 Text completion입니다. <code>gpt-3.5-trubo</code>를 사용하는 Chat Completion 형식에 맞게 아래처럼 코드가 작성됩니다.</p>
<pre><code class="language-tsx">const { Configuration, OpenAIApi } = require(&quot;openai&quot;);

const configuration = new Configuration({
  apiKey: process.env.위에서env파일에 작성하신 key,
});
const openai = new OpenAIApi(configuration);

const response = await openai.createChatCompletion({
    model: &quot;gpt-3.5-turbo&quot;,
    max_tokens: 2048,
    messages: [
      {
        role: &quot;user&quot;,
        content: &quot;질문&quot;,
      }],
});

console.log(response.data.choices[0].message.content);</code></pre>
<p>이제 위의 코드를 fetch하면 될 것 같습니다. 벌써 해치웠나?</p>
<blockquote>
</blockquote>
<p>💡 기타 createChatCompletion 파라미터 값은 <a href="https://platform.openai.com/docs/api-reference/chat">링크</a>에서 보실 수 있습니다.</p>
<br />

<h3 id="refused-to-set-unsafe-header-user-agent">Refused to set unsafe header &quot;User-Agent&quot;</h3>
<p>분명히 답변이 잘 옵니다… 근데 왜 콘솔에 에러가?
이유는 당연했습니다.</p>
<p><img src="https://velog.velcdn.com/images/_woogie/post/2b6dbd0d-1abb-40a7-9346-4a4bdc254b1f/image.png" alt=""></p>
<p>server-side에서만 사용했어야했습니다. API키가 노출되거든요. Next.js를 사용하기에 <code>API Routes</code> 기능을 빠르게 도입했습니다.</p>
<br/>

<h3 id="api-routes">API Routes</h3>
<p>먼저 pages 폴더 내부에 api폴더를 만든 후 원하는 api명으로 파일을 만들어줍니다.</p>
<pre><code class="language-tsx">// pages/api/openai.ts

import type { NextApiRequest, NextApiResponse } from &#39;next&#39;

import { CreateChatCompletionRequest } from &#39;openai/api&#39;;
const { Configuration, OpenAIApi } = require(&#39;openai&#39;);

const configuration = new Configuration({
  apiKey: process.env.위에서env파일에 작성하신 key,
});
const openai = new OpenAIApi(configuration);

const getChatCompletion = async (content: string): Promise&lt;string&gt; =&gt; {
  const completionParams: CreateChatCompletionRequest = {
    model: &#39;gpt-3.5-turbo&#39;,
    max_tokens: 2048,
      messages: [
          {
            role: &quot;user&quot;,
            content: `${content}`,
          }],
  };

  const response = await openai.createChatCompletion(completionParams);
  return response.data.choices[0].message.content;
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse&lt;string&gt;
) {
  try {
    const reqBody = req.body;
    const reply = await getChatCompletion(JSON.parse(reqBody).content as string)
    res.status(200).json(reply); 
  } catch (error) {
    res.status(500);
  }
}</code></pre>
<p>이제 에러가 안뜨는군요! 해치웠다!</p>
<blockquote>
</blockquote>
<p>💡 이제 node에서 환경변수가 사용되기 때문에 위에서 설명드린 <code>NEXT_PULBIC</code> prefix는 안붙이셔도 됩니다!</p>
<br/>

<h3 id="system--assistant--user">System | Assistant | User</h3>
<p>근데 나를 알고 있는 상태에서 답변을 해야하는데 어떻게 제 자신이 누군지, 뭘 좋아하는지, 뭐에 관심있는지를 어떻게 알려줄 수 있을까요?</p>
<p>이제 messages 파라미터의 <code>role</code>에 대해 알아봐야할 차례입니다. messages는 message 객체의 리스트로 구성되어야 합니다. 이 때 객체는 <code>role</code>과 <code>content</code>를 가질 수 있는데요. 이 때 role에는 system, assistant, user로 지정할 수 있습니다. 일반적으로 assistant 메시지와 user 메시지가 번갈아가며 대화 형태가 구성됩니다.</p>
<p>먼저 <code>user 메시지</code>는 end user가 생성한 메시지입니다. <code>assistant 메시지</code>는 원하는 대답이 나오도록 정보를 제공하는데 도움이 되는 메시지입니다. <code>system role</code>은 <code>assistant</code>의 행동에 영향을 줍니다. <strong>그러나 항상 시스템 메시지를 강력하게 따르진 않습니다!</strong> (미래의 모델은 시스템 메시지를 잘 따르도록 트레이닝 된답니다!)</p>
<p>아하 그러면 system메시지를 이용해서 제 자신에 대해 알려주면 되겠군요?</p>
<pre><code class="language-tsx">const getChatCompletion = async (content: string): Promise&lt;string&gt; =&gt; {
  const completionParams: CreateChatCompletionRequest = {
    model: &#39;gpt-3.5-turbo&#39;,
    max_tokens: 2048,
      messages: [
            {
                role: &#39;system&#39;,
                content: &#39;Your name is Jeong Jae-wook. You are a third year front-end developer.&#39;,
            }, {
            role: &quot;user&quot;,
            content: `${content}`,
          }],
  };

  const response = await openai.createChatCompletion(completionParams);
  return response.data.choices[0].message.content;
};</code></pre>
<blockquote>
</blockquote>
<p>💡 근데 막상 작업을 해보니 role을 system과 assistant로 처리했을 때에 따른 결과물에 큰 차이를 느끼긴 어려웠습니다. 그래서 아직 해당 사항은 여러 아티클을 찾아보고 여러 방법을 시도중에 있습니다.</p>
<br />
<br />

<h2 id="배포">배포</h2>
<h3 id="vercel">vercel</h3>
<p>Next.js로 개발을 했으니 빠르게 vercel로 배포를 진행했습니다. repository를 <a href="https://vercel.com/new">import</a>하고 배포를 해줍니다. 그리고 <a href="https://vercel.com/jaewook-jeong/gpt-secretary/settings/environment-variables">환경변수도</a> 잊지 않고 설정해줍니다!</p>
<blockquote>
<p><strong>그런데!</strong></p>
</blockquote>
<p>가끔 요청을 보내면 <code>504에러</code>가뜹니다.
<code>Uncaught (in promise) SyntaxError: Unexpected token &#39;A&#39;, &quot;An error o&quot;... is not valid JSON</code></p>
<p>Vercel에서 배포했을 때 timeout과 관련된 문제였습니다. 왜냐하면 vercel hobby 티어에서 설정할 수 있는 <code>maxDuration</code> 은 1~10초 사이의 값만 입력할 수 있었습니다. 그래서 간혹 답변이 늦어져 10초가 넘어가는 경우에 에러가 발생했던 것입니다.</p>
<p><code>그러면 어떻게 해결할 수 있을까?</code></p>
<blockquote>
</blockquote>
<p>💡 Edge function을 사용해보자!</p>
<br />
<br />


<h2 id="edge-api-routes">Edge API Routes</h2>
<h3 id="edge">Edge?</h3>
<p><a href="https://nextjs.org/docs/api-routes/edge-api-routes">Edge API Routes</a>는 <a href="https://nextjs.org/docs/api-reference/edge-runtime">Next.js Edge Runtime</a>을 이용하여 높은 퍼포먼스를 보여주는 APIs입니다. 먼저 Edge Runtime을 이해하기 위해서는 Edge Computing에 대해 알아야 하는데요. <a href="https://www.redhat.com/ko/topics/edge-computing">Edge Computing이란 사용자에게 가까운 곳(네트워크 &#39;엣지&#39;)에서 컴퓨팅을 수행하여 대기 시간을 줄이고 대역폭을 절약하는 방식입니다.</a> Edge Computing을 이용하면 실시간으로 데이터 소스에서 문제를 해결할 수 있는 장점이 있습니다. 즉 Edge Runtime이란 ‘엣지’라는 서버리스 컴퓨팅 환경을 채택하여 개발자에게 웹표준에 알맞게 설계된 Next.js의 환경입니다.</p>
<p>특히 이러한 Edge는 streaming과 같이 사용하면 응답 전체를 기다리는 것이 아니라 응답을 작은 청크로 분할하여 좋은 사용자 경험을 이끌어낼 수 있습니다.</p>
<p>마침 Open AI에서도 <a href="https://platform.openai.com/docs/api-reference/chat/create#chat/create-stream">stream</a>을 지원하는데요. stream으로 처리하면 ChatGPT처럼 분절된 메시지가 전달됩니다. stream이 종료될 때에는 <code>data: [DONE]</code>의 형태로 메시지가 내려옵니다. 이를 통해 다음과 같이 코드를 작성할 수 있습니다.</p>
<br/>

<h3 id="nextjs">Next.js</h3>
<p>Next.js에서 edge api를 사용하려면 기존에 만들었던 API Routes에 아래와 같은 코드만 추가하면 됩니다.</p>
<pre><code class="language-tsx">export const config = {
  runtime: &quot;edge&quot;,
};</code></pre>
<br/>


<h3 id="stream-설정">Stream 설정</h3>
<p>이제 기존의 createChatCompletion에 stream을 끼얹어 봅시다.</p>
<pre><code class="language-jsx">const res = await openai.createChatCompletion({
        model: &#39;gpt-3.5-turbo&#39;,
    max_tokens: 2048,
        stream: true,
      messages: [
            {
                role: &#39;system&#39;,
                content: &#39;Your name is Jeong Jae-wook. You are a third year front-end developer.&#39;,
            }, {
            role: &quot;user&quot;,
            content: `${content}`,
          }],
}, { responseType: &#39;stream&#39; });
let returnText = &#39;&#39;,
res.data.on(&#39;data&#39;, data =&gt; {
    const lines = data.toString().split(&#39;\n&#39;).filter(line =&gt; line.trim() !== &#39;&#39;);
    for (const line of lines) {
        const message = line.replace(/^data: /, &#39;&#39;);
        if (message === &#39;[DONE]&#39;) {
            return; // Stream finished
        }
        try {
            const parsed = JSON.parse(message);
                        returnText += parsed.choices[0].text; 
        } catch(error) {
            console.error(&#39;Could not JSON parse stream message&#39;, message, error);
        }
    }
});

return returnText;</code></pre>
<p>stream의 처리는 다음과 같습니다. 청크된 데이터가 들어올 때마다 해당 형태를 가공하여 message를 뽑아내 최종 답변 string을 만들어내는 과정입니다.</p>
<br/>
<br/>

<h2 id="여전히-미완성">여전히 미완성.</h2>
<p><img src="https://velog.velcdn.com/images/_woogie/post/67e12016-0b5f-4a4d-baf0-0c72e82b98ad/image.png" alt=""></p>
<blockquote>
<p>판사님.. 저는 제 외모에 대해 assistant나 system메시지를 보낸적이 없습니다😭</p>
</blockquote>
<p>아직 불완전한 모습을 보입니다. 계속 같은 답변을 보인다거나, 입력받은 언어 타입대로 답변을 해주지 않는다거나 여전히 해결할 부분은 많습니다. 토큰 제한을 걸어두지 않아서 제한 토큰을 넘어가는 요청에 대한 에러 처리도 필요하겠군요. 그리고 아직 system과 assistant의 뚜렷한 차이도 모르겠구요. 
그래도 이러한 개발과정을 통해 ChatGPT 더 나아가 이러한 인공지능이 어떻게 만들어지고 이것을 어떻게 유저에게 제공하는지 이해하는 시간이 되었습니다.</p>
<p>GPT4가 나왔는데, 다음에는 4를 사용하며 더 나은 답변을 얻는 모델을 사용하여 더 완성도 높은 앱을 구현해보고 싶습니다. 또한 여러 AI회사에서 AI에게 인격을 부여하려는 노력을 기울이고 있는데 이를 통한 재미있고 참신한 프로덕트가 많아졌으면 좋겠다는 생각을 해봅니다.</p>
<p>문제가 있다면 <a href="https://github.com/jaewook-jeong/gpt-secretary/issues">이슈</a>에 남겨주세요~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 또는 Next.js에서 라우팅을 감지하는 법(feat. modal)]]></title>
            <link>https://velog.io/@_woogie/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%98%90%EB%8A%94-Next.js%EC%97%90%EC%84%9C-History-api-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%9D%84-%EA%B0%90%EC%A7%80%ED%95%98%EB%8A%94-%EB%B2%95feat.-modal</link>
            <guid>https://velog.io/@_woogie/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%98%90%EB%8A%94-Next.js%EC%97%90%EC%84%9C-History-api-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%9D%84-%EA%B0%90%EC%A7%80%ED%95%98%EB%8A%94-%EB%B2%95feat.-modal</guid>
            <pubDate>Wed, 15 Mar 2023 12:31:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 event 감지를 통해 새로고침이나 탭 닫기 등의 상황이 아닌 브라우저 라우팅에 관한 이야기입니다.</p>
</blockquote>
<p>예를 들어 페이지에 form이 있다고 가정을 해봅시다. 이 form은 중요한 정보라서 꼭 submit(혹은 저장)을 해야하는 상황이라면요? 이 때 submit 하지 않은 상태에서 유저가 이탈하려고 할 때 react-router의 <a href="https://v5.reactrouter.com/core/api/Prompt">prompt</a>, Nextjs에서는 useRouter의 <a href="https://nextjs.org/docs/api-reference/next/router#routerevents">event</a>를 통해 navigate를 막을 수 있습니다.</p>
<p>먼저 React app입니다.</p>
<p>react-router(v5기준, <a href="https://reactrouter.com/en/main/upgrading/v5#prompt-is-not-currently-supported">v6</a> 아직 미지원)에서 Prompt라는 컴포넌트를 제공합니다. 컴포넌트의 props의 타입을 보면 다음과 같습니다.</p>
<pre><code class="language-tsx">interface PromptProps {
    message: string | ((location: H.Location, action: H.Action) =&gt; string | boolean);
    when?: boolean | undefined;
}</code></pre>
<p>값을 살펴보면 when을 통해 navigation 허용 여부를 체크합니다. 그리고 message의 타입을 보면 2가지가 있는데, 먼저 string의 경우 <code>confirm</code>창의 message로 쓰일 string을 받습니다. 또한 함수가 올 수 있는데 해당 함수는 Location객체를 첫번째 파라미터로 가지고 string 또는 boolean을 리턴합니다. 이 함수타입의 message prop을 통해 <code>confirm</code>창이 아닌 다른 UI를 보여줄 수 있는 코드를 작성할 수 있습니다.</p>
<p>아래와 같은 코드처럼 작성될 수 있습니다.</p>
<pre><code class="language-tsx">const message = (nextLocation: Location): boolean =&gt; {
        // navigation block이 필요할 경우
    if (hasToBlock) {
            // 원하는 작업
      return false;
    }
    return true;
};</code></pre>
<p>false를 반환하면 라우팅이 되지 않습니다. 이 때 인자로 받은 location은 라우팅 될 location에 관한 정보를 가지고 있기 때문에 해당 정보를 저장해뒀다가 submit과 같은 작업 처리 후 사용할 수도 있습니다.</p>
<p>만약 모달을 띄운다면 모달 렌더링에 관한 state를 위의 코드 <code>if</code>내에서 처리하는 등의 작업을 통해 원하는 UI를 그려낼 수 있습니다.</p>
<p>다음은 Next.js입니다.</p>
<p>next에서 현재의 pathName을 가져오거나 페이지를 이동시킬 때 사용하는 useRouter에는 events라는 object가 있습니다.  이 object에는 on, off, emit이라는 메소드를 가지고 있습니다. on은 특정 이벤트를 감지하여 원하는 동작을 처리할 때 사용되고 off는 unmount시에 on 메소드를 정리하는 함수라 할 수 있습니다. emit은 특정한 이벤트를 동작시키게 하는 메소드입니다. events object를 사용하여 아래 코드와 같이 페이지 이동을 감지하여 처리할 수 있습니다.</p>
<pre><code class="language-tsx">const { events } = useRouter();
const [forcePush, setForcePush] = useState(false);
useEffect(() =&gt; {
  const handleBrowseAway = (url: string) =&gt; {
        if (forcePush) return;
        if (!hasToBlock) return;
        // ui와 관련된 처리
    events.emit(&#39;routeChangeError&#39;);
    throw &#39;routeChange aborted.&#39;;
  };
  events.on(&#39;routeChangeStart&#39;, handleBrowseAway);
  return () =&gt; {
    events.off(&#39;routeChangeStart&#39;, handleBrowseAway);
  };
}, [forcePush, hasToBlock]);</code></pre>
<p>먼저 route가 변경되는 시점을 알 수 있는 <code>routeChangeStart</code> 를 활용하여 해당 이벤트가 발생했을 때 페이지 이동을 막아야(hasToBlock) 한다면 routeChangeError를 emit함과 동시에 throw 처리를 통해 페이지가 변경되지 않도록 처리할 수 있습니다. </p>
<p>또한 forcePush라는 state가 존재하는데 해당 state를 제어하여 submit이 된다거나 혹은 UI에서 무언가 처리되고 navigate될 때 다시 정상적으로 처리되도록 동작하게 할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[취업, 그 7주간의 일정]]></title>
            <link>https://velog.io/@_woogie/%EC%B7%A8%EC%97%85-%EA%B7%B8-7%EC%A3%BC%EA%B0%84%EC%9D%98-%EC%9D%BC%EC%A0%95</link>
            <guid>https://velog.io/@_woogie/%EC%B7%A8%EC%97%85-%EA%B7%B8-7%EC%A3%BC%EA%B0%84%EC%9D%98-%EC%9D%BC%EC%A0%95</guid>
            <pubDate>Sat, 20 Feb 2021 16:09:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>두려운 모든 신입취준생을 위하여.</p>
</blockquote>
<p>취준의 과정은 끊임없는 자기파괴의 연속이었다. <code>&quot;나&quot;</code>에 대한 반추는 줄곧 자신을 비판하는 것에 이르러 더 나아가 자신을 짓밟기에 이르렀다.</p>
<p>계속된 서류탈락, 이후 가끔의 서류합격에 이은 면접탈락. 이러한 과정은 나의 눈높이를 낮추고 자신을 비하하는 과정에 빠져들게 만든다. </p>
<p>그럼에도 계속 지원했으면 한다. 나를 알아줄, 나와 맞을 회사는 당신을 기다리고 있을테니까.</p>
<p>결론적으로 60지원 10서류합격 6최종합격을 했다. </p>
<blockquote>
<p>서류합격, 나를 보여줄 수 있겠다!</p>
</blockquote>
<h2 id="과제">과제</h2>
<p>10서류합격 중 5곳에서 과제를 줬다. 1시간 라이브 코딩도 있었고 대부분 3일정도의 기한이 있는 과제였다. 과제는 어렵지 않다. 그런데 그만큼 코드의 질이나 구조를 파고든다. <code>Components</code>폴더에 다 <del>때려박는</del> 나의 코딩방식으로는 문제가 있었다. </p>
<p>공부를 해야했다. 아마 과제를 해결하며 공부했던 이 시간이 정말 나에게 중요했던 시간이었다. 좀 더 나은구조를 생각하고, Trendy한 github를 살펴보고 고민했다. </p>
<p>Skill을 사용할 줄 알아서 멋진 개발물을 만들어 내는 것도 좋지만, 진짜 제대로 깊게 공부해야 뽑고 싶은 개발자가 된다. </p>
<p>그런 노력을 알아주었는지 과제도 통과했고 면접이 나를 기다리고 있었다.</p>
<h2 id="면접">면접</h2>
<p>면접은 대부분 <strong>기술면접</strong> 겸 인성(?)면접이었다. 면접에서는 아래와 같은 내용이 진행된다.</p>
<ol>
<li>간단한 자기소개</li>
<li>앞선 자기소개와 관련된 질문</li>
<li>이력서나 자소서와 관련된 질문</li>
<li>개인이 진행한 프로젝트에 관련된 질문</li>
<li>과제가 있었다면 과제에서 작성한 코드 질문</li>
<li>개발과 관련한 질문(기술질문)</li>
<li>기타 인성(?) 질문</li>
</ol>
<p>짧게는 30분부터 길게는 90분도 진행했다. 질문은 정말 다양했다. 이미 잘 정리된 <a href="https://velog.io/@honeysuckle/%EC%8B%A0%EC%9E%85-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-%EB%AA%A8%EC%9D%8C"><strong>프론트엔드</strong> 질문모음</a>외에 정말 다양한 것(백엔드, 네트워크 등)에 대해 준비해야 했다.</p>
<p>그래서 받았던 질문들중 모음자료에 없는 내용들을 정리해봤다.</p>
<h3 id="면접-질문">면접 질문</h3>
<h4 id="기술관련">기술관련</h4>
<ul>
<li>리액트가 어떻게 작동되나요?</li>
<li>virtual Dom은 무엇인가요? 어떻게 만들어지나요?</li>
<li>Hook의 조건은 무엇이 있나요?</li>
<li>리액트 github에서 소스를 살펴보셨나요?<ul>
<li>리액트 작동방식에 대해 설명해주세요<br/></li>
</ul>
</li>
<li>redux-thunk와 redux-saga의 차이점은 무엇인가요?</li>
<li>redux-saga에서 generator에 대해 설명해주세요</li>
<li>immer와 같은 불변성라이브러리의 원리는 무엇인가요?</li>
<li>immer와 redux의 shallowEqual을 같이 사용했을 때 얻는 이점은 무엇인가요?</li>
<li>context api를 통해 redux를 대체할 수 있는데 왜 사용하셨나요?</li>
<li>front에서 CORS를 어떻게 해결할 수 있을까요?<ul>
<li>Back-end에서 처리할 수 없을 때 front에서 어떤 방식을 사용해야 할까요?<br/></li>
</ul>
</li>
<li>렉시컬 스코프와 다이나믹 스코프의 차이점에 대해서 알려주세요</li>
<li>크로스 브라우징이란 무엇인가요? 해보셨나요?</li>
<li>css-in-js에서 왜 00를 사용하셨나요?</li>
<li>ES5, ES6, Typescript를 연결해서 설명해주세요</li>
<li>빌드된 파일이 너무 크다면, 줄이기 위한 방식은 어떤 것이 있나요?</li>
</ul>
<h4 id="기술-외-질문">기술 외 질문</h4>
<ul>
<li>3년 후 당신은 어떤 개발자가 되어있을건가요?</li>
<li>시니어 개발자란 무엇일까요?</li>
<li>부당한 대우를 받았을 때 어떻게 처리할건가요?</li>
<li>혼자서 개발을 하게될 텐데 어떻게 할 건가요?</li>
<li>어떤식으로 개발공부를 하셨나요?</li>
<li>왜 개발자가 되고 싶은가요?</li>
<li>디자이너와 어떻게 협업할 것인가요?</li>
<li>본인이 무엇이 지금 부족하다고 생각하시나요?</li>
</ul>
<p>추가적으로 나중에 생각나면 덧붙일 예정이다.</p>
<h3 id="컬쳐핏-면접">컬쳐핏 면접</h3>
<p>기술과 인성(?)면접을 보고 컬쳐핏 면접도 봤다. 조직에 얼마나 알맞을지에 관한 면접이었다. 대부분 예시 상황을 부여하고 어떻게 해결해 나갈지에 관한 질문이었다. 또한 개발자로서의 철학에 관한 질문이 많았다. 관련된 질문들을 추후에 추가할 예정이지만, 돌이켜 생각해보면 모든 맥락은 비슷했다.</p>
<blockquote>
<p>왜?</p>
</blockquote>
<h2 id="왜🤨">왜?🤨</h2>
<p><em>면접관들은</em>
개발을 <strong>왜</strong> 시작했는지 궁금해했다. 
개발을 <strong>왜</strong> 하는지 궁금해했다.
<strong>왜</strong> 특정한 라이브러리, 프레임워크 등을 사용했는지 궁금해했다.
<strong>왜</strong> 이러한 코드를 작성했는지 궁금해했다.</p>
<p>돌이켜본 <em>나는</em>
<strong>왜</strong>라는 정답이 없는 나의 질문에 답을 더해가는 중이었다.
<strong>왜</strong>라는 질문에 나의 철학이 생겼고, 개발에 관한 생각의 깊이가 깊어졌다.</p>
<blockquote>
<p>function 진짜개발자();</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[E2E테스트 with Cypress]]></title>
            <link>https://velog.io/@_woogie/E2E%ED%85%8C%EC%8A%A4%ED%8A%B8-with-Cypress</link>
            <guid>https://velog.io/@_woogie/E2E%ED%85%8C%EC%8A%A4%ED%8A%B8-with-Cypress</guid>
            <pubDate>Sat, 09 Jan 2021 11:16:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/_woogie/post/c483320d-f5c0-4ef2-94a9-5dae3e5f1f39/8NtBfj6HITU093EGaYhfynsMecih3civAcEY.gif" alt=""></p>
<blockquote>
<p>나의 앱의 기능들이 짤처럼 작동한다면?</p>
</blockquote>
<h2 id="e2e가-뭔데">E2E가 뭔데?</h2>
<p>E2E(End to End) 테스트는 개발물을 사용자 관점에서 테스트 하는 방법이다. 페이지에서 원하는 텍스트가 제대로 출력이 되었는지, 버튼을 클릭 했을 때 올바른 동작을 수행하는 지 등을 테스트한다. </p>
<h2 id="왜">왜?</h2>
<blockquote>
<p>왜 E2E Test를 해야할까?</p>
</blockquote>
<p><a href="https://www.browserstack.com/guide/end-to-end-testing#:~:text=E2E%20testing%20determines%20if%20various,communicated%20between%20multiple%20system%20components.">Why is End to End Testing necessary?</a>에서 발췌하였습니다.</p>
<p>모든 애플리케이션은 다양한 시스템, DB와 연결되고 통합되어있다. 따라서 앱의 <strong>Workflow</strong>는 복잡해졌다. 그래서 이러한 앱의 올바른 작동을 사용자 관점에서 확인할 수요가 늘어났다.</p>
<p>E2E테스트는 다양한 앱의 의존관계가 정확히 작동하는지 확인한다. 또한 정확한 정보가 다양한 시스템 컴포넌트 사이에서 전달하는지 체크할 수 있다.</p>
<p>E2E테스트는 진짜 사용자의 시나리오대로 시뮬레이션하고, 본질적으로 사용자가 어떻게 앱을 사용할지 테스트한다.</p>
<h2 id="어떤걸-사용해야할까">어떤걸 사용해야할까.</h2>
<p><img src="https://images.velog.io/images/_woogie/post/3e08e192-03b0-4cba-995c-c366dfdc8b56/1_I2X-aBITwXcar5_y1Effkw.png" alt="">
<a href="https://medium.com/@aswinkumar4018/which-e2e-testing-framework-to-use-for-js-based-client-applications-fbcac9aab680">출처</a></p>
<p><del>Cypress가 정보가 많고 Mocha를 기반으로 만들어졌기에 좀 더 친숙한 부분이 있었다.</del></p>
<p>먼저 여러 프레임워크를 Selenium기반을 기준으로 나눌 수 있다. Selenium은 웹 어플리케이션을 위한 테스팅 프레임워크로 자동화 테스트를 위한 여러가지 강력한 기능을 지원해준다. Selenium기반 프레임워크는 웹드라이버를 사용하여 브라우저와 상호작용을 하지만 non-selenium 프레임워크는 브라우저와 직접 상호작용한다. 이러한 점에서 Cypress와 TestCafe를 후보군으로 올렸고, 최종적으로는 다운로드 수가 많은 Cypress를 사용하게 되었다.
<img src="https://images.velog.io/images/_woogie/post/4d6f94ac-be4f-4a4c-a3e2-888eb021cedd/Screen%20Shot%202021-01-09%20at%205.49.20%20PM.png" alt=""></p>
<h2 id="cypress-e2e-test">Cypress E2E test</h2>
<h3 id="설치">설치</h3>
<p>먼저 cypress를 해당 프로젝트에 설치하자</p>
<blockquote>
<p>npm install --save-dev cypress</p>
</blockquote>
<p>정상적으로 설치가 완료되었다면 script에 아래와 같은 내용을 추가한다.</p>
<pre><code>{
  &quot;scripts&quot;: {
     ...
    &quot;e2e&quot;: &quot;cypress run&quot;,
        &quot;e2e:open&quot;: &quot;cypress open&quot;
  }
}</code></pre><p><em>cypress <a href="https://docs.cypress.io/guides/guides/command-line.html#Commands">run</a>은 모든 테스트를 실행하는 명령어이고, <a href="https://docs.cypress.io/guides/guides/command-line.html#cypress-open">open</a>은 Cypress Test Runner를 실행하는 명령어이다.</em></p>
<p>이후 <code>npm run e2e:open</code> 명령어를 실행하면 자동으로 cypress.json 파일과 cypress Directory가 생성된다. cypress.json에는 설정을 할 수 있다. 설정은 다음과 같이 진행했다.</p>
<pre><code>{
    &quot;baseUrl&quot;: &quot;https://futchall.com&quot;,
    &quot;videoRecording&quot;: false
}</code></pre><p>cypress는 프로젝트를 테스트하는 도구라서 Cypress로 애플리케이션에 접속하기 전에 먼저 애플리케이션을 실행해야 합니다. 이러한 기준이 되는 url값을 설정해줘야한다. 필자는 배포중이기에 현재 배포중인 사이트를 넣었다. 로컬에서 개발중이라면 &quot;<a href="http://localhost:3000&quot;%EA%B3%BC">http://localhost:3000&quot;과</a> 같이 설정해주면 된다. 로컬에서 개발할 경우 로컬에서 서버를 시작한 후 cypress를 해야한다. <del><a href="https://github.com/bahmutov/start-server-and-test">다음</a>과 같은 로컬 서버를 먼저 켜주는 라이브러리도 있다.</del>
videoRecording설정값은 테스트를 실행하면 video폴더에 저장이 되는데 false를 통해 저장되지 않도록 했다.</p>
<p>다양한 설정값은 <a href="https://docs.cypress.io/guides/references/configuration.html#Options">공식문서</a>를 참고하면 된다.</p>
<h3 id="테스트-작성">테스트 작성</h3>
<p>테스트는  cypress/integration 폴더에 작성하면 된다. <code>App.js</code>와 같이 파일을 작성하자. 내용은 다음과 같다.</p>
<pre><code>describe(&#39;Main Test&#39;, () =&gt; {
  it(&#39;Click into main&#39;, () =&gt; {
    cy.visit(&#39;/&#39;);
  })
})</code></pre><p>describe는 테스트 설명이고 it은 테스트 제목이라고 생각하면 된다.
<code>cy.visit(&#39;/&#39;)</code>은 cypress.json에 설정한  baseurl경로로 이동한다는 뜻이다. </p>
<p>이제 <code>npm run e2e:open</code>을 통해 실행하면 아래와 같은 화면이 뜬다.
<img src="https://images.velog.io/images/_woogie/post/c17d08a8-1f3b-443f-97b0-25eaa59d1d8f/Screen%20Shot%202021-01-09%20at%208.00.16%20PM.png" alt=""> 이후 만들었던 app.js를 클릭하면 브라우저 새 창이 뜨면서 설정해두었던 사이트로 이동된다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/762408e1-d615-4325-b3f2-6cb1e50fbdb4/Screen%20Shot%202021-01-09%20at%208.01.27%20PM.png" alt=""></p>
<h3 id="cygetselector-이벤트">cy.get(selector) 이벤트</h3>
<p>이제 사이트에서 버튼 클릭과 같은 테스트를 하려면 버튼이 무엇인지 알아내야하는데 방법은 쉽다. 
<img src="https://images.velog.io/images/_woogie/post/d45762d4-1644-432c-a4ab-4d205f681262/Screen%20Shot%202021-01-09%20at%208.03.54%20PM.png" alt="">
아까와 같은 창에서 주황색 상자로 표시된 해당 버튼을 누른 뒤 원하는 버튼으로 커서를 옮기면 <img src="https://images.velog.io/images/_woogie/post/2b34c5f9-daa6-4a84-a55f-1d457dc29231/Screen%20Shot%202021-01-09%20at%208.05.51%20PM.png" alt="">
연두색 상자에 해당 selector가 뜬다. <del>주황색 상자는 원하는 버튼이다</del> 이제 연두색 상자의 selector를 복사해서 다시 App.js를 변경해보자</p>
<pre><code>describe(&#39;Main Test&#39;, () =&gt; {
  it(&#39;Click into main&#39;, () =&gt; {
    cy.visit(&#39;/&#39;);
    cy.get(&#39;:nth-child(2) &gt; :nth-child(2) &gt; a&#39;).click();
  })
})</code></pre><p>이렇게 cy.get()을 통해 selector를 찾고 이후 click()함수를 통해 버튼을 클릭한 모습이다. </p>
<p>다시 실행해보면 왼쪽에 로그가 잘 출력되고, 오른쪽 영역에서 실제로 실행되는 화면을 볼 수 있다.</p>
<h2 id="마무리">마무리</h2>
<p>테스트에 여러 테스트가 많지만 E2E테스트를 통해 사용자의 관점에서 서비스를 실제와 같이 테스트 한다는 점에서 필요하다고 느꼈다. 더불어 이전에 진행한 Jest와 Enzyme을 활용한 상태관리 테스트를 통합해서 사용자의 시각에서 결과물을 확인하는 테스트라 생각되어 재밌기도 하고 효율적이라고 느껴졌다.
다른 여러 command를 활용해서 좀 더 실제와 같은 시나리오를 테스트해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[배포를 자동화해보자! (feat. Next js, pm2, Nginx)]]></title>
            <link>https://velog.io/@_woogie/%EB%B0%B0%ED%8F%AC%EB%A5%BC-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%B4%EB%B3%B4%EC%9E%90-feat.-Next-js-pm2-Nginx</link>
            <guid>https://velog.io/@_woogie/%EB%B0%B0%ED%8F%AC%EB%A5%BC-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%B4%EB%B3%B4%EC%9E%90-feat.-Next-js-pm2-Nginx</guid>
            <pubDate>Tue, 29 Dec 2020 09:03:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>매번 <code>sudo git pull</code> &gt; <code>sudo npm run build</code> &gt; <code>sudo npx pm2 reload all</code> 매우 귀찮은걸?</p>
</blockquote>
<h2 id="왜">왜?</h2>
<p>AWS로 배포를 진행하면서 Local에서 테스트한 결과와 배포한 어플리케이션과는 꽤나 큰 차이가 있었다. 그러면서 매번 같은 코드를 쳐가며 확인하고 테스트했다. 이러한 귀찮음과 더불어 무중단 배포의 욕심에 Travis-CI와 docker, Nginx 그리고 AWS를 이용해 배포자동화 환경을 구축하기로 했다.</p>
<p>이 글은 그 과정을 적은 포스트이다.</p>
<blockquote>
<p>AWS EC2를 통해 웹애플리케이션이 배포중인 상태입니다.
또한 Travis CI를 통해 <code>Jest</code> 테스트하였습니다.
Nginx설정이나 Dockerfile등 각각의 프로젝트에 맞게 수정하시면 됩니다.</p>
</blockquote>
<h2 id="구조">구조</h2>
<p>앞으로 진행할 구조는 다음과 같다.
<img src="https://images.velog.io/images/_woogie/post/3c598cbc-7d1a-4127-8c62-a500a72ef653/%E1%84%8C%E1%85%A6%E1%84%86%E1%85%A9%E1%86%A8%20%E1%84%8B%E1%85%A5%E1%86%B9%E1%84%82%E1%85%B3%E1%86%AB%20%E1%84%91%E1%85%B3%E1%84%85%E1%85%A6%E1%84%8C%E1%85%A6%E1%86%AB%E1%84%90%E1%85%A6%E1%84%8B%E1%85%B5%E1%84%89%E1%85%A7%E1%86%AB.jpg" alt=""></p>
<ol>
<li>먼저 IDE에서 작업한 결과를 git push한다.</li>
<li>travis-ci에서 commit을 감지한다.</li>
<li>travis-ci가 미리 작성한 테스트를 Jest와 Enzyme을 통해 검사<del>(생략가능)</del> 후 빌드한다.</li>
<li>travis는 스스로 배포할 수 없기에 빌드한 파일은 S3버킷으로 보낸다.</li>
<li>배포과정은 Code deploy가 작업하며 과정에 대해 스크립트 또한 travis에서 정의하여 처리한다.</li>
<li>Code deploy가 S3의 파일을 해당 인스턴스에 보내고, 인스턴스 내부에 정의된 배포 스크립트를 실행한다.</li>
<li>스크립트를 통해 Dockerize 배포된다. 이 과정은 Nginx 로드밸런싱을 통해 무중단으로 진행된다.</li>
</ol>
<h2 id="설정하기">설정하기</h2>
<p><code>travis연결은 생략하였습니다.</code></p>
<h3 id="빌드한-코드를-저장할-s3-만들기">빌드한 코드를 저장할 S3 만들기</h3>
<p><a href="https://s3.console.aws.amazon.com/s3/home?region=ap-northeast-2">S3</a> 페이지에 접속하면 버킷 console이 뜬다. 버킷은 S3에 저장되는 데이터의 컨테이너로 쉽게말해 저장공간이다.
해당 페이지에서 주황색 버튼인 <strong>버킷 만들기</strong>를 클릭하면 된다.
<img src="https://images.velog.io/images/_woogie/post/6bb3c02f-a26f-4362-908c-7c8e07e44046/Screen%20Shot%202020-12-14%20at%205.51.15%20PM.png" alt="">
이후 버킷 이름을 설정하고 버킷만들기를 클릭하면 된다. 다른 설정은 건드리지 않아도 된다.
<img src="https://images.velog.io/images/_woogie/post/489ec04c-2b43-4fbf-8808-4d09edae59e2/Screen%20Shot%202020-12-14%20at%205.55.55%20PM.png" alt=""></p>
<h3 id="배포할-aws사용자-추가하기">배포할 AWS사용자 추가하기</h3>
<p>추가할 AWS사용자는 추후에 Travis CI에서 배포할 수 있도록 사용된다.</p>
<p><a href="https://console.aws.amazon.com/iam/home?region=ap-northeast-2#/home">IAM console</a>에서 액세스관리 &gt; 사용자에 들어가면 <a href="https://console.aws.amazon.com/iam/home?region=ap-northeast-2#/users$new?step=details">사용자 추가</a>라는 파란색 버튼이 있다.
<img src="https://images.velog.io/images/_woogie/post/4003df64-71b9-454a-be57-280ffcfff155/Screen%20Shot%202020-12-14%20at%205.57.54%20PM.png" alt="">
사용자 추가를 클릭 후 사용자 이름을 적고 <strong>다음:권한</strong> 버튼을 클릭한다.
<img src="https://images.velog.io/images/_woogie/post/5dce57e9-1d64-4c19-a233-0bde4a5aac90/Screen%20Shot%202020-12-14%20at%205.58.27%20PM.png" alt="">
이후 <strong>기존 정책 직접 연결</strong>을 클릭한 뒤 <strong>AmazonS3FullAccess</strong>와 <strong>CodeDeployFullAccess</strong>를 검색한 후 해당 항목을 체크하고 다음 버튼을 누른다.
<img src="https://images.velog.io/images/_woogie/post/e6cbfc8c-32f4-4009-9838-328625cc472d/Screen%20Shot%202020-12-14%20at%205.58.48%20PM.png" alt="">
필요한 태그가 있다면 설정한 후 검토에 사용자 이름을 <strong>제외</strong>한 나머지 항목이 같으면 된다.
<img src="https://images.velog.io/images/_woogie/post/1f427cf9-ee23-44ae-ae01-86be30446ff9/Screen%20Shot%202020-12-14%20at%206.00.35%20PM.png" alt="">
사용자 만들기를 하면 아래와 같은 화면이 나오는데, 이 때 <strong>.csv 다운로드</strong>를 클릭하여 csv 파일을 다운받아야한다. 해당 파일이 있어야만 증명을 통해 해당 사용자를 사용할 수 있다.
<img src="https://images.velog.io/images/_woogie/post/09955778-3686-48c9-b34c-b98fb5d086fd/Screen%20Shot%202020-12-14%20at%206.00.43%20PM.png" alt=""></p>
<h3 id="aws-역할-생성하기">AWS 역할 생성하기</h3>
<p>IAM역할은 다음과 같다.<img src="https://images.velog.io/images/_woogie/post/ab85198a-0e53-45b9-a428-68da4c12942b/Screen%20Shot%202020-12-18%20at%205.19.36%20PM.png" alt="">다시말해서 배포에 사용할 EC2와 CodeDeploy에게 올바른 역할로 권한을 부여하여 배포할 수 있다.</p>
<p>EC2와 CodeDeploy, 각각의 서비스에 맞는 역할 2개를 설정해야 한다. 먼저 EC2이다.</p>
<p><a href="https://console.aws.amazon.com/iam/home?region=ap-northeast-2#/roles$new?step=type">IAM역할</a>만들기를 클릭하면 다음과 같은 화면이 나온다. 사용사례는 아래와 같이 EC2를 클릭한 후 권한을 넘어간다.
<img src="https://images.velog.io/images/_woogie/post/c86afe43-3f41-4cbe-9b51-e644106d419d/Screen%20Shot%202020-12-18%20at%205.23.16%20PM.png" alt="">
권한에서는 <code>AmazonEC2RoleforAWSCodeDeploy</code>를 검색 후 해당 권한을 클릭하면 된다. 정책이름에서 알 수 있듯이 codedeploy를 위한 EC2에 대한 접근을 허용하는 내용이다.
<img src="https://images.velog.io/images/_woogie/post/92f6a104-1785-430b-b53a-5a3e1b09e916/Screen%20Shot%202020-12-14%20at%206.04.50%20PM.png" alt="">
이후 태그는 건너뛰고 이름설정에 본인이 이해할 수 있도록 설정 후 완료하면 된다.</p>
<p>이어서 Codedeploy완 관련된 역할을 설정해줘야한다. 이것도 EC2 역할설정과 비슷하다.
똑같이 <a href="https://console.aws.amazon.com/iam/home?region=ap-northeast-2#/roles$new?step=type">역할 만들기</a>에 들어간 후 사용사례 선택에서 <strong>CodeDeploy</strong>를 선택한다.
<img src="https://images.velog.io/images/_woogie/post/0ba81d51-aa0f-45ac-b763-e1f5838f4a35/Screen%20Shot%202020-12-14%20at%206.06.01%20PM.png" alt="">
이어서 <code>다음:권한</code>을 클릭하면 이전과 같이 정책이 뜨는데 정책이 하나(AWSCodeDeployRole)뿐이라 해당 정책을 클릭 후 넘어가면 된다. 해당 역할의 이름을 잘 설정하면 역할 생성이 마무리된다.</p>
<h3 id="ec2에-이전단계의-역할-적용하기">EC2에 이전단계의 역할 적용하기</h3>
<p><a href="https://ap-northeast-2.console.aws.amazon.com/ec2/v2/home?region=ap-northeast-2#Instances:instanceState=running">EC2</a>콘솔에서 인스턴스에 이전에 만들었던(첫번째, 사용사례: EC2) 역할을 설정해줘야 한다.</p>
<p>ec2콘솔에서 원하는 인스턴스 우클릭 후 &gt; 보안 &gt; IAM역할 수정에서 할 수 있다.
<img src="https://images.velog.io/images/_woogie/post/e6a24d54-8379-480e-b5c1-916ed3623f55/Screen%20Shot%202020-12-18%20at%205.34.35%20PM.png" alt=""></p>
<p>역할 수정 페이지에서는 아까 만들어두었던 역할을 설정하면 된다.
<img src="https://images.velog.io/images/_woogie/post/94f06045-67d6-4f6f-bd1a-20d2f2973466/Screen%20Shot%202020-12-18%20at%205.37.01%20PM.png" alt=""></p>
<h3 id="ec2에-codedeploy-agent설치하기">EC2에 CodeDeploy Agent설치하기</h3>
<p>모든 설정이 끝났으면 이전단계에서 역할을 설정한 인스턴스를 터미널로 접속한다.
접속 후 먼저 <code>awscli</code>를 설치한다.  설치방법은 아래와 같다.</p>
<blockquote>
<p><code>sudo apt-get install awscli</code></p>
</blockquote>
<p>설치를 시작하면 설치과정이 나오고 도중에 <code>y</code>를 눌러  continue를 하면 설치가 완료된다.</p>
<p>설치 후 <code>sudo aws configure</code>명령어를 터미널에 입력한다. 입력하면 아래와 같은 화면이 보인다. 이 때 이전에 받았던 <strong>.csv파일</strong>에는 id와 key값이 적혀있는데 터미널에 해당 값을 입력해주면 된다.
<img src="https://images.velog.io/images/_woogie/post/7f9a634d-8cf3-4ec8-98d8-bdd6768511de/Screen%20Shot%202020-12-18%20at%205.49.36%20PM.png" alt=""></p>
<p>2가지 값을 입력하면 <code>Default region name [None]</code>이 뜨는데 해당 리전에 맞게 입력하면 된다. (서울은 <code>ap-northeast-2</code>) 이어서 <code>Default output format [None]</code>이 뜨는데 <code>json</code>으로 입력하면 된다.</p>
<p>설정 후 <code>codeDeploy</code>에 관한 파일을 받아야 하는데 이는 <code>aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2</code> 명령어를 터미널에 입력하면 된다. 그러면 해당 파일이 다운받아져 있는 것을 확인할 수 있다.
<img src="https://images.velog.io/images/_woogie/post/55e49c26-76c6-4f43-8542-58525d5e51c1/Screen%20Shot%202020-12-18%20at%205.54.55%20PM.png" alt="">
위의 캡쳐화면에서 알 수 있듯이 x권한이 빠져있는 것을 볼 수 있다. 따라서 <code>chmod +x ./install</code>명령어를 통해 권한을 부여하면 된다. 또한 CodeDeploy Agent를 설치하기 위해서는 ruby가 필요하기 때문에(없는 경우에 설치하면 <code>/usr/bin/env: &#39;ruby&#39;: No such file or directory</code>가 뜬다) <code>sudo apt-get install ruby</code>를 통해 ruby를 설치한다. <del>설치 때 y를 눌러 continue하면 된다.</del></p>
<p>Ruby설치가 완료되었으면 <code>sudo ./install auto</code>명령어를 통해 CodeDeploy Agent설치하면된다.</p>
<p>설치완료 후 <code>sudo service codedeploy-agent start</code>를 통해 CodeDeploy Agent를 실행하고 <code>sudo service codedeploy-agent status</code>를 통해 현재 실행되고 있는지 확인할 수 있다. 정상적으로 완료되면 아래와 같은 화면을 볼 수 있다.
<img src="https://images.velog.io/images/_woogie/post/a3fdef05-8f02-4900-925f-b7a841ab21c8/Screen%20Shot%202020-12-18%20at%206.02.28%20PM.png" alt=""></p>
<h3 id="docker-설치">Docker 설치</h3>
<p>먼저 리눅스에 Docker를 설치하고 도커이미지 파일을 만들어 배포할 예정이다. 그 첫번째 Docker를 EC2에서 Docker를 설치한다. 명령어는 다음과 같다.
<code>curl -fsSL https://get.docker.com/ | sudo sh</code>
설치 이후 <code>docker -v</code>를 명령어를 치면 현재 설치된 버전이 나온다.
<img src="https://images.velog.io/images/_woogie/post/68d8f147-a7c3-434b-9ba4-242ed0911e8b/Screen%20Shot%202020-12-21%20at%204.05.59%20PM.png" alt=""></p>
<h3 id="dockerfile만들기">Dockerfile만들기</h3>
<p>예시로 쓰이는 <a href="https://futchall.com">futchall프로젝트</a>은 <code>Next js</code>프레임워크를 사용하였습니다. 특히 <code>Nginx</code>를 활용하여 https를 구현하였고 애플리케이션을 <code>pm2</code>를 통해 백그라운드 실행하였습니다.</p>
<blockquote>
<p>Docker Image는 해당 프로젝트에 맞게 설정하시면 됩니다.</p>
</blockquote>
<p>지금의 자동배포화 이전에 Nginx와 Let&#39;s encrypt를 통해 https통신을 하고 있었다. 이에 nginx 또한 Docker로 컨테이너로 만들어 운영하려 했다. 그러나 추후에 진행할 무중단 배포에서 nginx컨테이너의 설정값을 next app컨테이너에 따라 유동적으로 변경하는데에 어려움을 느꼈다. <del><a href="https://github.com/jwilder/docker-gen">docker-gen</a>을 통해 blue-green deployment를 구현할 수 있을 것 같지만, 시간 제약이 있었다.</del> 따라서 nginx는 컨테이너화 하지 않기로 했다. </p>
<p>Next의 구조는 다음과 같다. 그리고 괄호친 항목이 앞으로 만들 파일이다.</p>
<pre><code>-- app/
    |-- .next/
    |-- pages/
        ㄴ-- index.js
    |-- node_modules/
    |-- package.json
    |-- docker-compose.yml(앞으로 만들 파일)
    ㄴ-- dockerfile(앞으로 만들 파일)</code></pre><p>nginx설치와 https 같은 경우는 이전에 작성한 <a href="https://velog.io/@_woogie/%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%ED%98%84%EC%9E%AC%EC%9C%84%EC%B9%98%EB%A5%BC-%EC%95%8C%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94#aws%EC%97%90-nginx-%EC%84%A4%EC%B9%98">글</a>을 통해 확인할 수 있다.</p>
<p>nginx설정은 Web app컨테이너와 통신하기 위해 리버스 프록시 설정만 해주면 된다. 아래 코드는 Web app컨테이너를 <code>ports 3000:3000</code>으로 열어둘 것이기 때문에 3000포트를 통해 리버스 프록시하는 코드 예시이다. 해당 코드는 server블럭내에 넣으면 된다.</p>
<pre><code>location / {
        proxy_set_header HOST$host;
        proxy_pass http://127.0.0.1:3000;
        proxy_redirect off;
    }</code></pre><p>dockerfile은 <code>next</code> build 후 port 3000으로 <code>listen</code>하고 <strong>pm2</strong>를 통해 백그라운드 실행에 관한 설정파일이다.</p>
<pre><code>FROM node:alpine

WORKDIR /usr/app

RUN npm install --global pm2

COPY ./package*.json ./

RUN npm install --production

COPY ./ ./

RUN npm run build

EXPOSE 3000

USER node

CMD [ &quot;pm2-runtime&quot;, &quot;start&quot;, &quot;npm&quot;, &quot;--&quot;, &quot;start&quot; ]
</code></pre><p>from, copy와 같은 기본적인 <a href="https://www.daleseo.com/dockerfile/">문법</a>은 별로 어렵지 않다.</p>
<p>이제 docker-compose파일을 만들어줘야 하는데 그전에 먼저 docker-compose를 설치하자</p>
<h3 id="docker-compose">Docker-Compose</h3>
<p>Docker compose는 yaml 파일로 여러 개의 도커컨테이너의 정의를 작성하여 한 번에 많은 컨테이너들을 작동시키고 관리할 수 있는 툴이다. docker-compose설치 명령어는 다음과 같다.</p>
<pre><code>sudo curl -L &quot;https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose

sudo chmod +x /usr/local/bin/docker-compose</code></pre><p>이후 아래와 같이 docker-compose.yml파일을 만든다.</p>
<pre><code>version: &#39;3&#39;
services:
  nextjs: 
      build: ./
      restart: unless-stopped
      ports:
             - 3000:3000</code></pre><p><em>next앱의 루트폴더에 docker-compose.yml파일이 있다. 또한 도커이미지를 만들기위한 Dockerfile이 루트폴더에 있기 때문에 build의 value를 &quot;./&quot;와 같이 설정했다. 본인의 dockerfile에 따라 혹은 사용하는 도커 이미지에 따라 설정을 바꾸면 된다.</em></p>
<p>이후 <code>docker-compose up</code> 명령어를 입력하면 만든 이미지가 실행된다. <del>필자는 .dockerignore파일을 만들어서 node_modules를 등록해야 용량부족 없이 실행되었다.</del>
참고로 <code>docker-compose up -d</code>를 통해 백그라운드에서 실행할 수 있다.</p>
<h3 id="무중단-배포">무중단 배포</h3>
<p>무중단 배포에는 주로 사용되는 방식이 Rolling방식과 Blue-green방식이 있다. 전자는 2대의 서버를 작동시키면서 하나씩 수정된 사항을 배포하는 방식이다. 후자는 수정 사항을 새로운 서버를 통해 배포하고 신규서버가 완료되면 기존의 서버를 제거하는 방식이다. 이 글에서는 후자의 방식인 <strong>Blue-green</strong>방식으로 무중단 배포를 한다.</p>
<pre><code>-- app/
    |-- .next/
    |-- pages/
        ㄴ-- index.js
    |-- node_modules/
    |-- package.json
    |-- docker-compose.green.yml(앞으로 만들 파일)
    |-- docker-compose.blue.yml(앞으로 만들 파일)
      |-- blue-green.sh(앞으로 만들 파일)
    ㄴ-- dockerfile</code></pre><p>먼저 기존에 사용하던 <strong>docker-compose.yml</strong>파일을 삭제하고 <strong>docker-compose.blue.yml</strong>과 <strong>docker-compose.green.yml</strong>파일을 작성한다. 내용은 아래와 같다.</p>
<pre><code>#green
version: &#39;3&#39;
services:
  nextjs: 
      build: ./
      restart: unless-stopped
      ports:
             - 3002:3000</code></pre><pre><code>#blue
version: &#39;3&#39;
services:
  nextjs: 
      build: ./
      restart: unless-stopped
      ports:
             - 3001:3000</code></pre><p>두 파일의 차이점은 port설정이다. 3001포트로 서버가 실행되다가 aws code deploy로 변경사항이 deploy되면 3002포트로 서버를 실행하고 3001포트를 중지하는 방법이다. 이러한 방식은 <strong>Nginx</strong>의 로드밸런싱 기능을 통해 nginx가 3001포트와 3002포트를 로드밸런싱해야 한다.</p>
<p>이전 nginx설정에 아래와 같이 넣어주면 된다.</p>
<pre><code>upstream 원하는 이름 {
                least_conn;
                server 127.0.0.1:3001 weight=5 max_fails=3 fail_timeout=10s;
                server 127.0.0.1:3002 weight=10 max_fails=3 fail_timeout=10s;
}
server {
        server_name 서버의 도메인이나 IP를 넣어주세요;
        location / {
                proxy_set_header HOST $host;
                proxy_pass http://원하는 이름;
       }
}</code></pre><p>upstream에 설정한 이름 그대로 server 블록 내의 location에서 proxy_pass에서 설정해주면 된다.</p>
<p>이제 현재 어떠한 blue, green 컨테이너가 실행중인지 확인하고 이에따라 빌드를 새로하고 컨테이너를 새로 up하고 기존의 컨테이너를 down하는 shell(blue-green.sh)을 루트 폴더에작성하면 된다.</p>
<p><em>DOCKER_APP_NAME은 여러 테스트 결과 해당폴더의 이름과 docker-compose에서 작성한 sevice이름이 underscore( \</em> )로 묶인다._</p>
<pre><code>#!/bin/bash

DOCKER_APP_NAME=본인의 docker service이름

EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml ps | grep Up)

if [ -z &quot;$EXIST_BLUE&quot; ]; then
    echo &quot;blue up&quot;
    docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d

    sleep 10

    docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down
else
    echo &quot;green up&quot;
    docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d

    sleep 10

    docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down
fi</code></pre><p>완료 후 blue-green.sh를 터미널에서 실행하면 된다. 혹시 기존의 도커 이미지나 현재 실행중인 컨테이너를 삭제하려면 docker rm 컨테이너 ID, docker rmi 도커이미지 ID를 하면 된다.</p>
<h3 id="aws-codedeploy-application생성하기">AWS CodeDeploy application생성하기</h3>
<p>먼저 <a href="https://ap-northeast-2.console.aws.amazon.com/codesuite/codedeploy/applications?region=ap-northeast-2">CodeDeploy application탭</a>에서 애플리케이션 생성을 클릭한다.
<img src="https://images.velog.io/images/_woogie/post/30b78ec5-be61-48c0-acf0-5d6de3ff6442/Screen%20Shot%202020-12-27%20at%204.59.59%20PM.png" alt=""></p>
<p>다음 애플리케이션 이름을 설정하고 컴퓨팅 플랫폼을 <strong>EC2/온프레미스</strong>를 설정해준 후 애플리케이션 생성을 누른다.
<img src="https://images.velog.io/images/_woogie/post/a13c0668-a4bf-4204-a315-5837bc6a95a5/Screen%20Shot%202020-12-27%20at%205.02.28%20PM.png" alt=""></p>
<p>생성을 누르면 바로 생성한 해당 애플리케이션 세부사항이 뜬다. 혹시라도 안뜬다면 생성한 애플리케이션을 클릭 후 <strong>배포그룹</strong>탭을 누르면 된다. 이제 배포 그룹생성을 클릭하면 된다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/bee84a4c-db82-49c9-8fad-3e9483bfb99a/Screen%20Shot%202020-12-27%20at%205.02.58%20PM.png" alt=""></p>
<p>먼저 배포그룹 이름을 설정해준다. 이후 서비스역할을 설정해줘야하는데 이전에 AWS 역할생성하기에서 만든 역할을 선택해주면 된다.
<img src="https://images.velog.io/images/_woogie/post/2777ca74-3501-4780-a2e5-c2738406584b/Screen%20Shot%202020-12-27%20at%205.06.00%20PM.png" alt=""></p>
<p>다음으로는 배포유형은 <strong>현재위치</strong>, 환경구성은 <strong>Amazon EC2 인스턴스</strong>를 선택한다. 또한 배포설정은 <strong>CodeDeployDefault.AllAtOnce</strong>이다.
<img src="https://images.velog.io/images/_woogie/post/1e707b11-0212-46a6-9a82-513aa2cb2840/Screen%20Shot%202020-12-27%20at%205.06.06%20PM.png" alt=""><img src="https://images.velog.io/images/_woogie/post/679922ec-d21f-4c0a-9b28-440d033eda21/Screen%20Shot%202020-12-27%20at%205.06.14%20PM.png" alt=""></p>
<h3 id="travis파일에-deploy설정하기">travis파일에 deploy설정하기</h3>
<p>Travis는 스스로 배포할 수 없고 이러한 기능을 지원해준다. 그리고 이러한 배포할 때 필요한 설정은 <strong>.travis.yml</strong>파일에서 가능하다. 설정은 다음과 같다.</p>
<pre><code>language: node_js
node_js:
  - 14 #노드 버전
branches:
  only:
    - master
before_deploy:
  - rm -rf node_modules
  - zip -r futchall-front * #futchall-front이름으로 zip파일 생성
  - mkdir -p deploy #deploy폴더 생성
  - mv futchall-front.zip deploy/futchall-front.zip #만든 zip파일을 deploy폴더로 이동
deploy:
  - provider: s3
    access_key_id: $AWS_ACCESS_KEY #추후에 travis에서 설정
    secret_access_key: $AWS_SECRET_KEY #추후에 travis에서 설정
    bucket: front-build-bucket #s3버킷 이름
    region: ap-northeast-2
    skip_cleanup: true
    local_dir: deploy
    wait-until-deployed: true
    on:
      repo: jaewook-jeong/futchall #본인의 repository
      branch: master
  - provider: codedeploy
    access_key_id: $AWS_ACCESS_KEY
    secret_access_key: $AWS_SECRET_KEY
    bucket: front-build-bucket
    key: futchall-front.zip
    bundle_type: zip
    application: futchall-code-deploy-application
    deployment_group: futchall-front-group #이전단계에서 설정한 deployment배포그룹
    region: ap-northeast-2
    wait-until-deployed: true
    on:
      repo: jaewook-jeong/futchall #본인의 repository
      branch: master
notifications:
  email:
    recipients:
      - dnrlwo11@gmail.com #본인의 이메일</code></pre><p>Travis는 코드를 S3에 업로드하고 CodeDeploy 이벤트를 실행한다. 그리고 이 때 CodeDeploy가 어떻게 해야할지 정의해야 하는데 이는 appspec.yml로 정의한다. 해당 파일을 루트폴더에 작성한다.</p>
<pre><code>version: 0.0
os: linux
files:
  - source:  /
    destination: /home/ubuntu/futchall/ #S3에서 가지고온 파일을 저장할 폴더
hooks:
  AfterInstall: #배포 후 실행할 명령
    - location: execute-deploy.sh
      timeout: 240</code></pre><p>또한 배포 후 실행할 명령인 <strong>execute-deploy.sh</strong>파일을 루트폴더에 만들어 준다. </p>
<pre><code>#!/bin/bash

cd /home/ubuntu/futchall #본인의 EC2 폴더 구조에 따라 변경
sudo ./deploy.sh &gt; /dev/null 2&gt; /dev/null &lt; /dev/null &amp;</code></pre><h3 id="travis-환경변수-설정하기">Travis 환경변수 설정하기</h3>
<p>먼저 환경변수 설정을 위해 해당 repository의 설정에 들어간다.
<img src="https://images.velog.io/images/_woogie/post/9c06f23c-495c-402a-8ea1-c4130163a8f3/Screen%20Shot%202020-12-28%20at%204.43.01%20PM.png" alt=""></p>
<p>설정의 Environment Variables에서 Name에는 AWS_ACCESS_KEY, AWS_SECRET_KEY를 Value에는 이전에 .csv파일로 받은 ACCESS_KEY와 SECRET_KEY를 각각 넣어주면 된다.
<img src="https://images.velog.io/images/_woogie/post/56efc173-c829-4023-83fe-6e8155bc25a7/Screen%20Shot%202020-12-28%20at%204.46.13%20PM.png" alt=""></p>
<h2 id="마무리">마무리</h2>
<p>이제 vscode에서 push를 진행해보자.
travis에서 test가 진행되고 빌드가 완료되면<img src="https://images.velog.io/images/_woogie/post/1320c39c-7513-4ae6-bcf7-9cc3176032bd/Screen%20Shot%202020-12-28%20at%2010.33.15%20PM.png" alt="">
이후 AWS의 <a href="https://ap-northeast-2.console.aws.amazon.com/codesuite/codedeploy/applications?region=ap-northeast-2">CodeDeploy</a>에 접속해 이전에 만든 배포그룹에 들어가면 아래와 같은 화면을 만날 수 있다.
<img src="https://images.velog.io/images/_woogie/post/646e04f2-c47f-4590-92e2-c5eaed85120b/Screen%20Shot%202020-12-28%20at%2010.34.58%20PM.png" alt=""></p>
<br/>

<hr>
<p>이렇게 약 1주간 진행한 배포자동화 및 무중단 배포가 마무리되었다. 도커에 대한 이해와 사용법부터 AWS의 IAM이나 정책에 관한 사용, 더 나아가 Nginx의 이점에 대해 체감했다.</p>
<p>순서나 구조를 이해하면 생각만큼 두려운 과정은 아니었다. 단지 Mac에서 mysql을 사용하기 위해 docker를 얼핏 알았다면 이제는 dockerfile syntax나 docker-compose를 이해하며, 도커의 편리함을 느꼈다. 더불어 AWS 기능의 다양한 사용과 여전히 사용하지 못한 수많은 기능을 보며 나 자신을 다잡았다.</p>
<p>이러한 환경을 설정 후 이전과는 비교할 수 없는 효율을 얻었다. 갈수록 빨라지는 개발, 배포의 상황에서 DevOps에 대한 고민과 기초를 맛 볼 수 있었다. 더 나아가 오픈소스나 프로그램의 사용법 뿐만 아니라 하드웨어나 아키텍쳐에 관한 좀 더 심도있는 영역 또한 웹 개발자로서의 필요충분조건이라 생각했다.</p>
<p>이러한 공부도 게을리 말아야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[EC2시작할 때 스크립트 자동실행하여 서버 배포하기(feat. pm2)]]></title>
            <link>https://velog.io/@_woogie/EC2%EC%8B%9C%EC%9E%91%ED%95%A0-%EB%95%8C-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%9E%90%EB%8F%99%EC%8B%A4%ED%96%89%ED%95%98%EC%97%AC-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0feat.-pm2</link>
            <guid>https://velog.io/@_woogie/EC2%EC%8B%9C%EC%9E%91%ED%95%A0-%EB%95%8C-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%9E%90%EB%8F%99%EC%8B%A4%ED%96%89%ED%95%98%EC%97%AC-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0feat.-pm2</guid>
            <pubDate>Wed, 16 Dec 2020 13:00:09 GMT</pubDate>
            <description><![CDATA[<p>이전글에서 EC2인스턴스를 자동으로 켜고 끄게 만들었다. 그리고 설정해놨던 오전 10시에 접속해보니 502에러가 나를 반겨주고 있었다. 급한 마음에 SSH로 연결해서 확인해보니 서버가 꺼졌다가 켜지느라 pm2를 통해 배포되던 개발물도 없어졌던 것이다.</p>
<h2 id="해결">해결</h2>
<p>기본적으로 AWS는 해결법에 대해 대부분의 내용에 대해 친절히 한글로 알려준다. 해결하기 위해 찾은 <a href="https://aws.amazon.com/ko/premiumsupport/knowledge-center/execute-user-data-ec2/">내용</a> 또한 한글이었다. </p>
<p>그러나 설명과 다르게 여러 시도를 거쳐서 해결했다.</p>
<h3 id="cloud-init">Cloud-init</h3>
<p>먼저 <code>cloud-init</code>이 ec2에 설치되어야 한다. ubuntu를 기준으로 <code>sudo apt-cache search cloud-init</code>명령어를 통해 확인할 수 있다. 설치되어 있지 않다면 <code>sudo apt-get install cloud-init</code>을 통해 설치할 수 있다.</p>
<h3 id="ec2-인스턴스">EC2 인스턴스</h3>
<p>먼저 실행중인 인스턴스를 <strong>중지</strong>해야한다. <del>종료가 아니라 중지다!</del> 다음과 같이 원하는 인스턴스를 클릭 후 인스턴스 상태에서 인스턴스 중지를 클릭하자
<img src="https://images.velog.io/images/_woogie/post/a50d7a20-f72d-404b-b5d5-e0732521b9d3/Screen%20Shot%202020-12-16%20at%209.39.07%20PM.png" alt=""></p>
<p>상태에 빨간색으로 <strong>중지됨</strong>이 뜨지 않으면 아직 중지중이니 기다려주자.</p>
<p>이후 조금 헤맨 부분인데 <code>작업 &gt; 인스턴스 설정 &gt; 사용자 데이터 편집</code> 부분이다. 공식글에도 이렇게 설명되어 있어 너무도 당연하게 인스턴스 상태 버튼 옆의 작업버튼을 클릭하고 인스턴스 설정을 확인했다. 그러나 그곳에는 사용자 데이터 편집 또는 사용자 데이터와 관련한 버튼이 없다. <del>제길</del></p>
<p>버튼은 해당 인스턴스에서 우클릭 후 확인할 수 있다.
<img src="https://images.velog.io/images/_woogie/post/5ed13d63-961a-4e45-bf2e-47286873ce46/Screen%20Shot%202020-12-16%20at%209.44.15%20PM.png" alt=""></p>
<h3 id="사용자-데이터-수정하기">사용자 데이터 수정하기</h3>
<p>이제 다 왔다. 해당 버튼을 클릭하면 중지가 완료되었다는 가정하에 아래와 같은 화면이 나온다. 그리고 원래 선택되어 있는대로 <strong>사용자 데이터를 텍스트로 수정</strong>을 클릭하고 그 아래 textarea에 설정내용을 적어주면 된다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/2659ce81-cbc7-4008-a399-29f9eb602aa8/Screen%20Shot%202020-12-16%20at%208.12.00%20PM.png" alt=""></p>
<p>설정 내용은 다음과 같다. 처리해야할 스크립트는 하단에 <code>#!/bin/bash</code> 아래부터 작성하면 된다. 
작성할 때 주의할 점이 있다면,</p>
<ol>
<li>sudo 기준이다.</li>
<li>root폴더 기준이다. (ec2에 연결하면 home/ubuntu/ 이기 때문에 너무도 당연히 ubuntu폴더가 기준인줄 알았다.)<pre><code>Content-Type: multipart/mixed; boundary=&quot;//&quot;
MIME-Version: 1.0
</code></pre></li>
</ol>
<p>--//
Content-Type: text/cloud-config; charset=&quot;us-ascii&quot;
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename=&quot;cloud-config.txt&quot;</p>
<p>#cloud-config
cloud_final_modules:</p>
<ul>
<li>[scripts-user, always]</li>
</ul>
<p>--//
Content-Type: text/x-shellscript; charset=&quot;us-ascii&quot;
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename=&quot;userdata.txt&quot;</p>
<p>#!/bin/bash
<strong>여기부분에 원하는 스크립트를 작성하시면 됩니다, 아래 2줄은 예시입니다.</strong>
cd home/ubuntu/futchall &lt;---- 폴더 변경
npx pm2 start npm -- start &lt;--- pm2 실행
--//</p>
<p>```</p>
<h2 id="마무리">마무리</h2>
<p>일단 급한불은 껐다. 이렇게 당연한 것마저 고려하지 못하니 역시 초짜이긴 한 것 같다. 시행착오를 겪으며 원리나 흐름의 중요성에 대해 더욱 느낀다.
아직 해당 script의 시점에 대해 의문이 있다. 아무래도 먼저 시작되는 것 같다. 인스턴스를 재실행하고 연결해 pm2 list를 확인하면 npx가 재설치(?)되는 모습을 보인다. 더 찾아보고 추가할 수 있도록 해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[EC2 인스턴스 자동으로 켜고 끄기]]></title>
            <link>https://velog.io/@_woogie/EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EC%BC%9C%EA%B3%A0-%EB%81%84%EA%B8%B0</link>
            <guid>https://velog.io/@_woogie/EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EC%BC%9C%EA%B3%A0-%EB%81%84%EA%B8%B0</guid>
            <pubDate>Tue, 15 Dec 2020 02:43:15 GMT</pubDate>
            <description><![CDATA[<h2 id="왜">왜?</h2>
<p>열심히 개발하던 나에게 어느날 이메일 하나가 찾아왔다.
<img src="https://images.velog.io/images/_woogie/post/ce29fe03-f6df-4739-8fe8-daf49dc31b41/Screen%20Shot%202020-12-15%20at%2010.27.41%20AM.png" alt=""></p>
<p>AWS EC2 프리티어 750시간 중 85%의 사용량을 초과했다고 알려주는 이메일이었다. </p>
<p>당황했다. 750시간이면 하루 24시간으로 나눠도 31일이 넘는데 고작 15일 남짓한 기간에 사용량을 다 썼단다. </p>
<p>다시 잘 읽어보니 인스턴스당 750시간이 아니었다. <code>Front</code>와 <code>Back</code> 두 개의 인스턴스를 유지하던 나에게 딱 맞는 계산이었다. </p>
<p>서둘러 요금제를 확인했다. 물론 750시간을 더 쓴다고 가정했을 때 서울 리전 기준으로 약 11달러정도였다. 이정도 가격이면 괜찮다고 생각했지만 그래도 사람들이 찾아올 시간에, 내가 개발할 시간에만 인스턴스들이 실행중이었으면 좋겠다는 생각을 했다.</p>
<h2 id="자동화-하기">자동화 하기</h2>
<p>먼저 <code>Lambda</code>와 <code>CloudWatch</code>에 대해 이해가 필요하다.
AWS Lambda는 서버를 <a href="https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EB%B9%84%EC%A0%80%EB%8B%9D#:~:text=%ED%94%84%EB%A1%9C%EB%B9%84%EC%A0%80%EB%8B%9D(provisioning)%EC%9D%80%20%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98,%EB%AF%B8%EB%A6%AC%20%EC%A4%80%EB%B9%84%ED%95%B4%20%EB%91%90%EB%8A%94%20%EA%B2%83%EC%9D%84%20%EB%A7%90%ED%95%9C%EB%8B%A4.">프로비저닝</a>하거나 관리하지 않고도 코드를 실행할 수 있게 해주는 컴퓨팅 서비스다. 쉽게말해 코드를 등록해놓고 원하는 상황에 해당 코드가 실행되도록 할 수 있다는 소리이다. 
CloudWatch는 AWS 리소스와 AWS에서 실시간으로 실행 중인 애플리케이션을 모니터링한다. 지표를 감시하고 설정된 값에 도달할 경우 경보를 보내거나 설정한 규칙에 따라 이벤트를 발생시킬 수 있다.</p>
<p>이 두가지를 가지고 작업을 시작한다.</p>
<h3 id="iam정책-역할-생성">IAM정책, 역할 생성</h3>
<p>먼저 <code>AWS</code>의 리소스에 대해 접근하기 위해서 IAM이 필요하다. <code>Lambda</code>를 실행하기 위한 권한이 필요한 것이다. </p>
<p>먼저 정책을 만든다. IAM에 들어가서 왼쪽 메뉴에서 정책을 클릭 후 화면에서 정책 생성을 누르면 된다.
<img src="https://images.velog.io/images/_woogie/post/a1e35ccd-071b-43dc-9868-64b7b4aa4974/Screen%20Shot%202020-12-15%20at%209.52.32%20AM.png" alt=""></p>
<p>이후 <strong>JSON</strong>탭을 선택한 후 아래 코드를 붙여넣는다.</p>
<pre><code>{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;logs:CreateLogGroup&quot;,
        &quot;logs:CreateLogStream&quot;,
        &quot;logs:PutLogEvents&quot;
      ],
      &quot;Resource&quot;: &quot;arn:aws:logs:*:*:*&quot;
    },
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;ec2:Start*&quot;,
        &quot;ec2:Stop*&quot;
      ],
      &quot;Resource&quot;: &quot;*&quot;
    }
  ]
}</code></pre><p><img src="https://images.velog.io/images/_woogie/post/f534b91f-3e8b-4583-9430-b425c34b17b7/Screen%20Shot%202020-12-15%20at%2010.52.39%20AM.png" alt="">
이후 정책검토를 누르고 원하는 이름을 적고 마무리 한다.
<img src="https://images.velog.io/images/_woogie/post/f74d9251-9aa8-4c34-a109-e56c480f0dc0/Screen%20Shot%202020-12-15%20at%209.53.01%20AM.png" alt=""></p>
<p>정책을 생성했으니 이제 <strong>역할</strong>을 생성할 차례이다.
정책과 마찬가지로 IAM 역할 메뉴에서 <code>역할 만들기</code>를 클릭한다. 
이후 사용사례선택에서 <code>Lamda</code>를 선택 후 다음으로 넘어간다.
<img src="https://images.velog.io/images/_woogie/post/0cb46c4d-f976-494e-ad83-902db95c77dc/Screen%20Shot%202020-12-15%20at%2010.58.05%20AM.png" alt=""></p>
<p>다음으로는 이전단계에서 만들어두었던 정책을 연결한다. (이전 정책을 ec2-on-off로 해두었다.)
<img src="https://images.velog.io/images/_woogie/post/cdefa0d8-f629-4c42-9017-2c9ba1b37773/Screen%20Shot%202020-12-15%20at%209.53.25%20AM.png" alt=""></p>
<p>다음 태그는 필요하다면 작성하고 검토에서 <strong>역할 이름</strong>을 잘 설정한 후 역할만들기를 클릭하면 역할이 만들어진다.</p>
<h3 id="lambda생성하기">Lambda생성하기</h3>
<p><a href="https://ap-northeast-2.console.aws.amazon.com/lambda/home?region=ap-northeast-2#/functions">AWS Lambda</a>에 접속 후 함수생성 버튼을 클릭한다.
먼저 인스턴스를 끄는 lambda를 생성한다. 함수 이름을 끄는 함수에 걸맞는 이름을 지정한다. 런타임은 <code>python</code>을 선택한다. 권한은 <strong>기본 실행 역할 변경</strong>을 클릭 후 <strong>기존 역할 사용</strong>을 클릭 후 이전에 만든 역할을 선택 후 함수생성한다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/99b9113e-d063-453f-b48e-b1c88cf17f00/Screen%20Shot%202020-12-15%20at%209.54.12%20AM.png" alt=""></p>
<p>함수 생성을 완료했다면 이제 함수를 수정할 차례이다. 생성한 함수의 구성 탭에는 함수코드라는 항목이 있다. 이 곳에서 코드를 수정할 수 있다. 그전에 자신의 리전과 EC2의 인스턴스 ID가 필요하다. </p>
<p>코드에 아래와 같은 코드에 본인의 리전과 인스턴스 ID를 알맞게 넣으면 된다.</p>
<pre><code>import boto3
region = &#39;us-west-1&#39;
instances = [&#39;i-12345cb6de4f78g9h&#39;, &#39;i-08ce9b2d7eccf6d26&#39;]
ec2 = boto3.client(&#39;ec2&#39;, region_name=region)

def lambda_handler(event, context):
    ec2.stop_instances(InstanceIds=instances)
    print(&#39;stopped your instances: &#39; + str(instances))</code></pre><p>해당코드는 <code>stop</code>에서 볼 수 있듯이 인스턴스를 중단하는 코드이다.
<img src="https://images.velog.io/images/_woogie/post/4095c6f3-d2ba-44ca-89e9-e583d3f09e3a/Screen%20Shot%202020-12-15%20at%209.55.41%20AM.png" alt=""></p>
<p>이후 해당 구성탭의 하단에 있는 기본설정에 제한시간을 10초로 바꿔준다.
<img src="https://images.velog.io/images/_woogie/post/213fcd9d-dc6a-4bdc-b250-34de5a5ee5e6/Screen%20Shot%202020-12-15%20at%209.57.41%20AM.png" alt=""></p>
<p>이후 Lamda를 생성하는 과정을 한 번 더 반복한다. 인스턴스를 끄는 함수를 생성했으니 켜는 함수도 생성해야한다.</p>
<p>생성함수의 python코드는 다음과 같다. 이것또한 마찬가지로 해당 리전과 인스턴스 ID를 본인에 맞게 설정해준다.</p>
<pre><code>import boto3
region = &#39;us-west-1&#39;
instances = [&#39;i-12345cb6de4f78g9h&#39;, &#39;i-08ce9b2d7eccf6d26&#39;]
ec2 = boto3.client(&#39;ec2&#39;, region_name=region)

def lambda_handler(event, context):
    ec2.start_instances(InstanceIds=instances)
    print(&#39;started your instances: &#39; + str(instances))</code></pre><p>Lambda 함수는 테스트 할 수도 있는데 원하는 함수를 클릭 후 뜨는 창에서 테스트를 클릭하면 된다. 
<img src="https://images.velog.io/images/_woogie/post/691b0614-d3dc-4ac6-a322-9b56bdf2ec76/Screen%20Shot%202020-12-15%20at%2011.23.26%20AM.png" alt="">
해당 Lambda는 JSON코드를 사용하지 않기 때문에 테스트클릭 후 뜨는 modal창에서 생성을 클릭하면 된다. 이훟 다시 테스트를 클릭하면 테스트 결과가 나온다.
<img src="https://images.velog.io/images/_woogie/post/3d6f07ac-08d4-4e24-aca8-6b592272015a/Screen%20Shot%202020-12-15%20at%2011.25.59%20AM.png" alt=""></p>
<h3 id="cloudwatch-생성하기">CloudWatch 생성하기</h3>
<p><a href="https://ap-northeast-2.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-2#rules:">CloudWatch 규칙</a>에 접속한다. CloudWatch의 왼쪽메뉴에서 이벤트 하위항목에 규칙이 있다. 해당 페이지에서 규칙생성을 클릭한다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/ca0791f0-c53f-4447-aa2a-df23a431d2f2/Screen%20Shot%202020-12-15%20at%2010.05.22%20AM.png" alt=""></p>
<p>이후 뜨는 창에서 일정을 선택한다. 일정에서는 cron표현식과 고정비율이 있는데 매일 한 번 켜고 끄기 때문에 cron표현식을 사용했다. 설정에 어려움이 있다면 <a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudWatch/latest/events/ScheduledEvents.html">설명</a>을 클릭하면 된다.
새벽 1시, 그러니까 1 AM에 인스턴스를 중단하고 싶기 때문에 아래와 같은 방식을 사용했다. 시간은 UTC를 사용하기 때문에 -9시간을 해주면 된다.
<img src="https://images.velog.io/images/_woogie/post/b91df01b-3621-4c51-8546-666299ac4593/Screen%20Shot%202020-12-15%20at%2010.20.53%20AM.png" alt=""></p>
<p>이후 해당 설정에 맞는 lambda 함수를 설정해준다. 위의 사진은 중단설정이므로 중단하는 Lambda 함수를 설정해줬다.</p>
<p>이후 해당 규칙의 이름과 설명을 설정하면 된다.
<img src="https://images.velog.io/images/_woogie/post/dbd8373a-3626-488b-bb80-08d077c095ce/Screen%20Shot%202020-12-15%20at%2010.24.42%20AM.png" alt=""></p>
<p>이 규칙을 만드는 것 또한 끄는 것과 켜는 것, 각각 만들어주면 된다.
<strong>시간 설정</strong>과 <strong>해당 Lambda</strong>설정에 유의하자.</p>
<h3 id="확인">확인</h3>
<p>Lambda페이지에서 원하는 lambda를 선택 후 <strong>모니터링</strong>탭을 선택하면 아래와 같은 화면이 나온다. 이후 <strong>CloudWatch에서 로그보기</strong>를 클릭한다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/2a124b49-ff44-475b-86a0-6161632b86d7/Screen%20Shot%202020-12-15%20at%2011.36.23%20AM.png" alt=""></p>
<p>클릭하면 아래와 같은 창이 보이고 해당 lambda에 대한 로그 기록을 확인할 수 있다. 성공적으로 실행된 모습이다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/db6f0513-3db3-4a9a-b152-0395ba34bcc8/Screen%20Shot%202020-12-15%20at%2011.35.22%20AM.png" alt=""></p>
<h2 id="마무리">마무리</h2>
<p>그러나 이미 85%를 넘었고 요금이 부과될 예정이다. 인스턴스 작동시간을 하루 12시간으로 설정하면 프리티어로 2개의 인스턴스를 운영할 수 있을 것이다. 그러나 사용자의 접속시간대를 쉽게 예측할 수 없다는 점으로 봤을 때 효율적인 운영인지에 대해 의문이 든다. </p>
<p>어렵게만 느껴진던 AWS였지만 정보가 잘 정리되어있었다. AWS의 다양한 기능을 다 써보기는 쉽지 않겠지만 내가 원하는 기능이 이미 구현되어 있음에 놀라움의 연속이었다. 어려워하지말고 하나씩 해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1주간 나를 힘들게한 nginx설정(feat. CORS)]]></title>
            <link>https://velog.io/@_woogie/1%EC%A3%BC%EA%B0%84-%EB%82%98%EB%A5%BC-%ED%9E%98%EB%93%A4%EA%B2%8C%ED%95%9C-nginx%EC%84%A4%EC%A0%95feat.-CORS</link>
            <guid>https://velog.io/@_woogie/1%EC%A3%BC%EA%B0%84-%EB%82%98%EB%A5%BC-%ED%9E%98%EB%93%A4%EA%B2%8C%ED%95%9C-nginx%EC%84%A4%EC%A0%95feat.-CORS</guid>
            <pubDate>Thu, 10 Dec 2020 10:36:10 GMT</pubDate>
            <description><![CDATA[<h2 id="문제의-발단">문제의 발단</h2>
<h3 id="모바일에서의-cors">모바일에서의 CORS</h3>
<p>모바일에서 테스트중 사진이 등록되지 않는 문제를 발견했다. 콘솔창을 보기위해 <code>chrome://inspect/#devices</code>을 이용해 콘솔을 확인했었다.</p>
<p>콘솔로 얻은 나의 상태는 <strong>CORS.</strong></p>
<p><img src="https://images.velog.io/images/_woogie/post/ef3c3471-994f-4166-a7fa-7c42ca9f1019/Screen%20Shot%202020-12-04%20at%205.56.22%20PM.png" alt=""></p>
<p>너무나도 당황스럽게 CORS문제였다. 웹브라우저에서는 잘 작동하던게 모바일에서 들어간 브라우저에서 작동이 안되다니. 너무나 어처구니가 없었다.
그렇게 multer, s3, lambda를 이잡듯이 뒤졌다.</p>
<h3 id="데이터-타입">데이터 타입?</h3>
<p>그렇게 열심히 찾아보다가 어제인 12월 9일 데이터 타입일지 모른다는 생각을 했다. 이미지 re-sizing을 위해 사용한 <code>sharp</code> 모듈에서 <code>jpg</code> 확장자를 <code>jpeg</code>로 바꾸는 과정에 오류가 있을 수 있다고 생각했다. 그래서 모바일에서 <code>jpeg</code>파일을 업로드하니 정상적으로 되었다.</p>
<blockquote>
<p>아 이거구나!</p>
</blockquote>
<p>그러나 <code>sharp</code>의 <code>jpeg</code>함수를 사용해보며 적용했지만 결과는 같았다. 사실 jpg와 jpeg의 차이는 거의 없었다. 확장자를 단순히 바꿔주는 것만으로도 확장자가 변경되는 사실상 같은 확장자였다. </p>
<p>그러다 문득 용량이 큰 <code>jpeg</code>파일을 웹브라우저에서 업로드를 했고 모바일처럼 똑같은 <strong>CORS에러</strong>가 나를 사로잡았다.</p>
<h3 id="용량">용량!!</h3>
<p>그렇다. 용량문제였다. 그러면 어디서 잘못되었을까. 다시 <code>multer, multer-s3, lambda</code> 모두 확인하는 과정을 거쳤다. multer의 사이즈 오류라고 생각했지만 불현듯 나의 뇌리에 스친건 <strong>Nginx</strong>였다.
<br/></p>
<h2 id="nginx-설정">Nginx 설정</h2>
<p>nginx의 기본설정에 <code>client_max_body_size</code>는 <strong>1mb</strong>로 설정되어있다. 다시말하자면 사진을 아무리 보내도 결국에 1mb이하 사이즈 사진만 업로드 되고 있었던 것이다.</p>
<p><a href="https://stackoverflow.com/questions/28476643/default-nginx-client-max-body-size/28476755">해결</a>
먼저 <code>sudo vim /etc/nginx/nginx.conf</code>로 설정파일에 들어간다.
해당설정은 http, server, location context에 설정할 수 있다.</p>
<blockquote>
<p>예를들어 이렇게!</p>
</blockquote>
<pre><code>http {
    ...
    client_max_body_size 20M;
}    </code></pre><h2 id="마무리">마무리</h2>
<p>이것저것 많이 써보고 싶어서 시도해본 것들이 나를 붙잡았다. 어떻게 이런 오류가 뜰까 싶었지만 이제는 적어도 이해가 간다. 너무 기본적이 내용을 살펴보지 않고 무턱대고 들어간 내 책임이다. 기본적으로 사용법, 기본설정 등은 꼭 읽는 습관을 들여야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[mobile환경 테스트(feat. Android)]]></title>
            <link>https://velog.io/@_woogie/mobile%ED%99%98%EA%B2%BD-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@_woogie/mobile%ED%99%98%EA%B2%BD-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Fri, 04 Dec 2020 09:49:45 GMT</pubDate>
            <description><![CDATA[<br/>

<p><code>모바일에서 프로젝트를 테스트하던 중 모바일에서 어떠한 반응이 없었다.</code></p>
<blockquote>
<p>답답하네?</p>
</blockquote>
<h2 id="어떻게하면-무슨오류가-있는지-알-수-있을까">어떻게하면 무슨오류가 있는지 알 수 있을까?</h2>
<p>크롬 개발자 도구에서 <code>Toggle device toolbar</code>를 통해 모바일을 열심히 테스트 했었다. 작은화면에서 어떠한 비율이나 크기로 화면에 보여지는지 확인했다. 그러다가 문득 핸드폰으로 배포한 프로젝트에 들어가서 테스트 해봤다.</p>
<p>결과는 <strong>먹통</strong></p>
<p><code>An unexpected error has occurred</code>와 같은 View에서의 에러조차 없다. aws서버에서 로그를 찍어봐도 문제가 없다. 이게 무슨일일까?
<br/></p>
<blockquote>
<p>console을 확인하고 network tab도 보고 싶다!</p>
</blockquote>
<h3 id="android장치-디버깅">Android장치 디버깅</h3>
<ol>
<li>먼저 핸드폰을 컴퓨터에 USB연결한다.</li>
<li>핸드폰 개발자 설정에서 USB 디버깅이 설정되어 있는지 확인한다.
2-1. 먼저 환경설정에 들어가서 휴대전화 정보에(LG폰은 시스템) 들어간다.
2-2. 삼성폰은 소프트웨어 정보에 들어간다. (LG폰은 휴대전화 정보)
2-3. <strong>빌드번호</strong> 항목을 찾는다.
2-4. 빌드번호를 여러번 눌러준다.
2-5. 개발자 옵션을 활성화 한다.
2-6. USB 디버깅을 설정해준다.</li>
<li>컴퓨터 크롬창에서 <code>chrome://inspect#devices</code>를 입력해준다.</li>
<li>Inspect with Chrome Developer Tools창에서 <code>Discover USB devices</code>를 활성화해준다.</li>
<li>핸드폰에서 웹브라우저를 열고 컴퓨터에서 해당 브라우저 밑에 inspect를 클릭하여 확인한다.</li>
<li>창에서 확인한다.
<img src="https://images.velog.io/images/_woogie/post/03101cd5-811a-4884-99bf-f79921245f34/Screen%20Shot%202020-12-04%20at%205.56.22%20PM.png" alt=""></li>
</ol>
<h2 id="콘솔창에-에러가-보인다😁">콘솔창에 에러가 보인다😁</h2>
<p>이제 원인을 찾고 해결하면 된다!</p>
<blockquote>
<p>아 CORS구나..</p>
</blockquote>
<p>핸드폰에서만 cors문제가 생긴다. 이유가 뭘까?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[document가 없어요?]]></title>
            <link>https://velog.io/@_woogie/document%EA%B0%80-%EC%97%86%EC%96%B4%EC%9A%94</link>
            <guid>https://velog.io/@_woogie/document%EA%B0%80-%EC%97%86%EC%96%B4%EC%9A%94</guid>
            <pubDate>Tue, 01 Dec 2020 07:45:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>document is not defined.</p>
</blockquote>
<br/>

<h2 id="cookie를-쓰려고-했어요">Cookie를 쓰려고 했어요.</h2>
<p>프로젝트에서 <code>cookie</code>를 이용해 사용자가 찾아본 구장을 저장하고, 최근 본 구장목록을 띄워주는 로직을 작성했다. 중요하지 않은 정보라 생각했고, 단순히 cookie에 구장의 id값을 기록해서 해당 리스트로 목록을 출력하려 했다.</p>
<p>어렵지 않았다. 그저 단순히 <code>document.cookie</code>를 이용해 구장 페이지를 볼 경우와, 지도에서 해당 구장을 클릭해서 상세정보를 볼 때 cookie에 해당 값을 넣어줬다.</p>
<p>이후 사용자 프로필 UI에서 최근 본 구장목록을 <code>document.cookie</code>를 사용해 가져왔고, 이후 <code>SWR</code>을 통해 구장리스트를 가져와서 출력했다.</p>
<br/>

<blockquote>
<p>그런데.<br/>
새로고침을 하는 순간 에러가 떴다.</p>
</blockquote>
<br/>
<br/>

<h2 id="왜">왜?</h2>
<p>서버 pm2 monit에 떠 있던 에러는. </p>
<h3 id="📄documnet-is-not-defined">📄documnet is not defined.</h3>
<p>document가 정의되지 않았다?!
웹페이지 그 자체인 <a href="http://www.tcpschool.com/javascript/js_dom_document">document</a>가 정의되지 않았다? 아니 이게 무슨소리야.</p>
<p>먼저 <strong>Next js</strong>, <strong>SSR, CSR</strong>에 대해 더 잘 알아봐야 한다.</p>
<h3 id="ssr-csr">SSR, CSR</h3>
<p>SSR은 접속한 페이지를 서버에서 렌더링 후 사용자에게 제공된다. 이후 페이지를 동작하게 하거나 구성하는 리소스들을 로드하도록 한다. 이를통해 사용자에게 더 빠른 경험을 전할 수 있고, 검색엔진에 의해 내용을 크롤링하여 SEO 문제도 해결 할 수 있다. 그러나 페이지 이동시에는 해당 페이지를 요청, 렌더링을 해야 하므로 CSR보다 느리다는 단점이 있다.</p>
<p>반면 CSR은 모든 페이지를 사용자가 로드 후에 렌더링을 하게 되므로, 로딩 시간이 SSR보다 느리다. 하지만 다른 페이지로 넘어갈 때, 이미 모든 페이지가 렌더링이 되어 있기 때문에 SSR보다 빠르게 동작한다.</p>
<h3 id="next-js">Next js</h3>
<p>모바일의 발전이 모바일 앱과 같은 웹페이지를 요구했고 이에 CSR이 가능한 SPA인 React, Vue 등 여러 라이브러리, 프레임워크가 모습을 드러냈다. 그러나 SPA는 여러단점들이 존재했고, 이러한 단점을 보완하기위해 Next js와 같은 SSR구현을 쉽게 해주는 프레임워크가 등장했다. React도 SSR이 가능하지만 구현이 어렵다. </p>
<p>Next 프레임워크는 SSR의 장점을 사용하여 첫 접속한 페이지(링크를 통한 접속이나 주소를 통한 접속)를 빠르게 렌더링하여 사용자 경험을 높일 수 있다. 또한 이후 이벤트를 통한 페이지 접속은 CSR 방식으로 작동된다.</p>
<h3 id="document의-정의-시점">document의 정의 시점</h3>
<p>document의 <a href="https://www.w3schools.com/jsref/dom_obj_document.asp">정의 시점</a>은 모든 요소들이 로드되었을 때이다. 즉 document가 정의되기 전에 document객체에 접근했기 때문에 오류가 났던 것이다. </p>
<p>다시 SSR의 상황을 생각해보면 요소들이 load되기전에 document객체를 활용해 데이터를 가져와서 페이지를 조작하려 했기 때문이었다.
<br/>
<br/></p>
<h2 id="해결">해결</h2>
<p>해결을 간단했다. 요소들이 mount된 후. 즉 ComponentDidMount의 시점에 document.cookie를 호출하면 되는 것이었다. 그러나 <code>hooks</code>를 사용중이므로 <code>useEffect</code>를 통해 document 객체에 접근 후  <a href="https://www.npmjs.com/package/swr">SWR</a>을 사용하여 필요한 데이터를 가
져왔다.
<br/>
<br/></p>
<h2 id="마무리">마무리</h2>
<p>이제 프로젝트 마무리 한답시고 진행중인 상황에 나의 부족함이 여실히 드러났다. 단순히 &quot;SEO를 적용하고 싶으니까&quot;, &quot;코드스플리팅 기능도?!&quot;, &quot;라우팅이 쉬우니까&quot;라며 사용했던 나를 반성한다. 기능을 사용할 때 좀 더 구조적인 측면이나 아키텍쳐에 대한 이해가 수반되어야 하겠다. </p>
<p>겉핥기가 아니라 꿰뚫자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사용자의 현재위치를 알고 싶어요]]></title>
            <link>https://velog.io/@_woogie/%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%ED%98%84%EC%9E%AC%EC%9C%84%EC%B9%98%EB%A5%BC-%EC%95%8C%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94</link>
            <guid>https://velog.io/@_woogie/%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%ED%98%84%EC%9E%AC%EC%9C%84%EC%B9%98%EB%A5%BC-%EC%95%8C%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94</guid>
            <pubDate>Mon, 23 Nov 2020 13:25:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/_woogie/post/1a3a6f85-df39-4db7-b339-b95774aea579/0_iJEjjaQn3qLrVAdg.jpg" alt=""></p>
<blockquote>
<p>진행중인 프로젝트인 <a href="https://futchall.com">Futchall</a>에서 발생한 <strong>위치정보 문제</strong>를 해결하기 위한 과정을 적은 글 입니다.</p>
</blockquote>
<br/>
<br/>

<h2 id="기존의-문제점">기존의 문제점</h2>
<br/>

<h3 id="사용자의-📌위치정보를-가져오기">사용자의 📌위치정보를 가져오기</h3>
<p>사용자의 현재위치를 가져오는 API는 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API">Geolocation API</a>가 존재한다. 이는 사용자에게 권한 확인을 받은 후 사용할 수 있다. 사용법은 매우 간단하다.</p>
<p><code>navigator.geolocation</code> 객체를 이용하면 된다. 그러나 해당 객체가 없을 수도 있으므로 다음과 같은 방식으로 처리하여 사용할 수 있다.</p>
<blockquote>
<pre><code>if(&#39;geolocation&#39; in navigator) {
  // 위치정보 사용 가능
} else {
  // 위치정보 사용 불가능
}</code></pre></blockquote>
<pre><code>
&lt;br/&gt;
&lt;br/&gt;


그러나 오직 **https환경**에서만 사용가능했다.![](https://images.velog.io/images/_woogie/post/c78b0db5-d80e-458c-9576-2036055efef8/Screen%20Shot%202020-11-23%20at%206.48.14%20PM.png)

따라서 현재 http인 나의 프로젝트에서는 사용이 불가능했다. 애초에 location설정이 Block되어있는 모습이다.![](https://images.velog.io/images/_woogie/post/4094615b-b1f9-47be-bce9-a9e2fcc67151/Screen%20Shot%202020-11-23%20at%206.13.20%20PM.png)

## 왜?

먼저 HTTPS에 대해 알아볼 필요가 있었다.

### HTTPS🔒

http는 `hypertext transfer protocol`의 약자로 쉽게말해 브라우저와 서버의 통신 규약이다. 다시말해 우리가 데이터를 주고 받는 통신에 있어서의 약속이고 방법이라 할 수 있다.

그러나 이러한 http에는 데이터가 암호화되지 않은 날 것의 상태이기에 정보가 탈취되거나, 전송중 데이터가 변형된다면 치명적이 문제가 발생한다. 그래서 http에 **Secure(SSL)**를 결합한 HTTPS를 고안했다.

SSL은 http와 독립된 프로토콜이며 http이외에도 사용할 수 있는 네트워크보안기술이다. 상세한 내용은 거두절미하고, SSL프로토콜에 의해 데이터가 암호화된다. 그리고 암호화 할 때 사용되는 **Key**가 필요하다. 키는 대칭키와 비대칭키가 있다.

대칭키는 하나의 키로 암호화와 복호화한다. 반면 비대칭키는 2개의 키(공개키, 비공개키)로 암호화와 복호화를 한다. 하나의 키로 암호화를 하면 다른 키로 복호화를 할 수 있다. 그렇다면 우리는 키를 가지고 있어야하는데 이러한 키는 CA로 불리는 인증기관에서 인증서를 발급받아야 한다.

&gt; 돈이 드는거야?

&lt;br/&gt;

### Let`s encrypt

SSL인증서는 **돈**이 든다. 그러나 [let`s encrypt](https://letsencrypt.org/)를 통해 무료로 발급받을 수 있다. **let`s encrypt**는 자동화된 프로세스를 통해 암호화를 위해 무료 인증서를 제공하는 인증 기관이다. 

### HTTPS서버

SSL인증서를 받았다고 끝이 아니다. Front node서버가 https서버로 바꿔줘야 한다. 방법은 크게 [greenlock](https://greenlock.domains/)을 사용하는 방법과 [nginx](https://www.nginx.com/)를 사용하는 방법이 있었다.

greenlock은 lets encrypt를 사용해 SSL을 적용할 수 있는 모듈이다. 반면 nginx는 웹서버로서 더 많은 역할을 담당할 수 있다. 리버스 프록시, 정적파일 처리, 메일 프록시 서버 등 다양한 역할을 할 수 있다. 이러한 이점에 nginx를 사용하기로 결정했다.

### Nginx

Nginx는 웹 서버로, 가볍고 높은 성능을 특징으로 한다. Nginx는 요청에 응답하기 위해 비동기 이벤트 기반 구조를 가진다. 점유율이 가장 높은 아파치와 매우 달리한다. 이러한 Event-driven 구조는 여러 커넥션을 Event Handler에서 비동기 방식으로 처리해서 먼저 처리되는 것부터 로직이 진행되도록 한다.

&lt;br/&gt;
&lt;br/&gt;

## 어떻게?🧐

&lt;br/&gt;

### AWS에 nginx 설치

1. apt-get을 이용해 설치를 한다.
`sudo apt-get install nginx`
2. 이후 vim을 통해 설정파일을 확인할 수 있다.
`sudo vim /etc/nginx/nginx.conf`

### certbot을 통해 let`s crypt 발급받기

1. 먼저 certbot-auto를 다운받는다. 
`wget https://dl.eff.org/certbot-auto`
2. certbot-auto에 실행권한을 추가한다. (a+x, 모든 사용자에게 실행권한을 부여)
`chmod a+x certbot-auto`
3. 기존서버가 있다면 종료해준다.
`sudo lsof -i tcp:80`: 80번 포트에 실행중인지 확인
`kill -9 프로세스아이디` 실행중이라면 해당 프로세스아이디를 입력하여 종료
혹은 pm2를 통해 실행중이라면 `npx pm2 kill`을 통해 종료
4. certbot 실행
`./certbot-auto`
이후 이메일 입력. 해당 이메일은 나중에 3개월이 되면 인증연장 관련 이메일이 오는 계정이다.
5. 설정이 제대로 되어있는지 확인
`sudo vim /etc/nginx/nginx.conf`
아래와 같이 필요한 내용이 자동으로 설정되어 있다.
![](https://images.velog.io/images/_woogie/post/5d1d4866-9e39-43cd-b739-f5258cb6208e/Screen%20Shot%202020-11-23%20at%206.03.04%20PM.png)
6. 리버스 프록시 설정
(5)의 설정에 이어서 http블럭내의 server블럭에서 아래와 같이 본 서버(3060port)를 설정해준다.</code></pre><pre><code>location / {proxy_set_header HOST$host;
    proxy_pass http://127.0.0.1:3060;
    proxy_redirect off;
}</code></pre><p>```
<img src="https://images.velog.io/images/_woogie/post/8f99e15a-7c7f-433e-9018-65117057cc25/Screen%20Shot%202020-11-23%20at%2010.06.02%20PM.png" alt="">
7. nginx를 재실행한다.
<code>sudo systemctl start nginx</code>
8. https설정이 되어있는지 확인한다.<img src="https://images.velog.io/images/_woogie/post/e8e793aa-3c51-447d-ab3c-850895e2a092/Screen%20Shot%202020-11-23%20at%2010.16.01%20PM.png" alt=""></p>
<br/>
<br/>

<h2 id="마무리">마무리</h2>
<p>https의 이해와 더불어 https를 왜 보급하려는지, 왜 let`s encrypt재단을 만들어서까지 노력을 하는지 이해할 수 있었다.
실력을 더 키워, 작게는 npm에 모듈을 올려보며 더 나아가 docker같은 오픈소스를 만들 그날까지 더 노력해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도메인 연결하기 (feat. 🍪 쿠키오류 해결하기)]]></title>
            <link>https://velog.io/@_woogie/%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0-feat.-%EC%BF%A0%ED%82%A4%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_woogie/%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0-feat.-%EC%BF%A0%ED%82%A4%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 18 Nov 2020 10:03:51 GMT</pubDate>
            <description><![CDATA[<p><code>문제는 JWT를 위해 Cookie를 사용하며 발생했다.</code>
<br/>
<br/></p>
<blockquote>
<p>아니, 도메인 없이 쿠키를 못 사용하는거야?</p>
</blockquote>
<p><a href="https://stackoverflow.com/questions/9133696/a-cookie-without-a-domain#:~:text=2%20Answers&amp;text=No%2C%20that%20is%20not%20possible,or%20at%20least%20privacy%2C%20risk.">응.</a></p>
<br/>
<br/>

<h2 id="왜">왜?</h2>
<br/>

<p>AWS로 두개의 인스턴스를 만들었다. 이후 <strong>탄력적 IP 설정</strong>을 통해 IP주소를 할당 받았다. 이후 각종 테스트를 하기 위해 회원가입 후 로그인을 했다. 나를 맞이한 건 <code>This Set-Cookie was blocked</code> 쿠키가 설정되지 않았다.</p>
<br/>

<h3 id="samesite">SameSite</h3>
<p>먼저 나의 첫 오류는 SameSite오류였다. SameSite 속성이 설정이 되어 있지않아 자동으로 <code>SameSite=Lax</code>라는 값으로 설정이 돼 있었다. <a href="https://developers.google.com/web/updates/2020/02/nic80">크롬80</a>버전 부터 기본값이 <strong>Lax</strong>인 탓이었다. </p>
<p>SameSite속성은 <strong>서로다른 도메인에서의 쿠키 전송에 관한 보안</strong>이다. 값으로는 Lax, none, strict가 있다. </p>
<ul>
<li>none : 현재사이트, 현재사이트 이외의 도메인에도 제한없이 전달</li>
<li>strict : 서로 다른 도메인에서는 절대 불가</li>
<li>lax : strict와 같지만 get method등 일부 예외 존재</li>
</ul>
<p>나의 로그인은 <code>lax</code>로 처리가 불가했기에 일단 <code>none</code>으로 처리했다.</p>
<blockquote>
<p>그러나,
Secure에 문제가 생겼다.</p>
</blockquote>
<p><code>SameSite=none</code>일 경우 <code>Secure=true</code>여야 한다는 것이다.
<br/></p>
<h3 id="secure">Secure</h3>
<p><code>secure</code>속성은 https에서만 쿠키 전송이 가능한 속성이었다. </p>
<blockquote>
<p>아 Domain을 사야겠구나!</p>
</blockquote>
<br/>
<br/>

<h2 id="domain-구매-feat-gabia">Domain 구매 (feat. Gabia)</h2>
<br/>

<p>타닥타닥 <code>cheap domain</code> 검색.</p>
<p>해외의 다양한 사이트가 나왔다. 그리고 찾아본 가격은 한국 사이트와 큰 차이가 없었다.</p>
<blockquote>
<p>그러면 한국사이트에서 사자!</p>
</blockquote>
<br/>

<h3 id="가능한-도메인인지-확인하기">가능한 도메인인지 확인하기</h3>
<p>대부분의 도메인 구매 사이트에는 검색창이 있다. 그리고 원하는 도메인명을 검색하면 가능한 도메인이 가격과 함께 제시된다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/70cc94e1-f9af-4eb1-ae1f-98d065790cde/Screen%20Shot%202020-11-18%20at%206.16.57%20PM.png" alt=""><del>이미 futchall.com이라는 사이트는 내가 등록을 마쳐서 이미 등록되었다고 뜬다</del>
<br/></p>
<h3 id="도메인-신청하기">도메인 신청하기</h3>
<p><img src="https://images.velog.io/images/_woogie/post/923c24a0-597d-4693-a2bc-f004ce293883/Screen%20Shot%202020-11-18%20at%204.31.06%20PM.png" alt=""> 대부분의 설정은 기본 내용대로 하면 된다. 그러나 AWS로 배포를 했고 AWS의 <strong>Route 53</strong>을 통해 <strong>네임서버</strong>를 설정할 수 있다.</p>
<blockquote>
<p>AWS의 Route 53으로 이동하자!</p>
</blockquote>
<br/>

<h3 id="route-53">Route 53</h3>
<p><img src="https://images.velog.io/images/_woogie/post/321b2b62-505d-4655-b386-910815253eb9/Screen%20Shot%202020-11-18%20at%204.34.52%20PM.png" alt=""></p>
<p>route 53에 접속해서 DNS관리 호스팅 영역 생성을 클릭한다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/5de1c398-41b8-483f-824c-e526b54ff5a4/Screen%20Shot%202020-11-18%20at%204.35.31%20PM.png" alt=""></p>
<p>하고자 하는 도메인 이름을 입력한다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/dae4cfe6-6f5a-4fc1-a448-ad57905a9220/Screen%20Shot%202020-11-18%20at%204.36.58%20PM.png" alt=""></p>
<p>입력을 완료하면 도메인 이름에 해당하는 호스팅영역이 위와 같이 생긴다. 또한 NS라는 레코드가 생기고 해당하는 값을 복사해둔다.</p>
<p>이전에 <strong>가비아</strong>를 통해 신청중이던 사이트로 돌아가 <strong>네임서버 목록</strong> 버튼을 클릭한다. 클릭하면 아래와 같은 창이 팝업된다.<img src="https://images.velog.io/images/_woogie/post/b31a1142-9a3e-48ba-967f-8ab0b7eed7ef/Screen%20Shot%202020-11-18%20at%204.41.33%20PM.png" alt=""> 해당 창의 각 칸에 <strong>Route 53</strong>에서 복사해온 값을 하나씩 넣어준다.</p>
<p>이후 결제를 하면 된다.
<br/></p>
<h3 id="back과-front-도메인-설정하기">back과 front 도메인 설정하기</h3>
<p>결제가 완료된 후 AWS의 <strong>Route 53</strong>에서 Record를 설정해주어야 한다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/7f2b3126-3633-465c-98b7-0d48b20fb4ef/Screen%20Shot%202020-11-18%20at%206.43.34%20PM.png" alt="">구매한 도메인의 호스팅영역에서 레코드 생성을 누른다. 뜬 화면에서 <strong>단순 라우팅</strong>을 클릭한다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/c713baf5-db06-4cec-af66-a0cb4488679b/Screen%20Shot%202020-11-18%20at%204.51.37%20PM.png" alt=""></p>
<p>front는 앞에 아무것도 붙지 않은 <code>.yoursitename.com</code>이고 ec2에서 실행중인 front인스턴스의 ip주소를 가져와 넣는다. 레코드 유형은 <strong>A</strong>로 한다. back은 <code>api.yoursitename.com</code> 등 front와 다른 이름을 넣어 완료한다.</p>
<br/>

<h3 id="🍪-설정하기">🍪 설정하기</h3>
<p>이제 쿠키에 domain속성에 값을 넣어주면 front와 back 두 도메인 사이에 쿠키가 다닐 수 있게 된다.</p>
<blockquote>
<pre><code>res.cookie(토큰명, 토큰, { 
    httpOnly: true,
    maxAge: 60 * 60 * 24 * 1000,
    domain: &#39;.futchall.com&#39;
});</code></pre></blockquote>
<pre><code>
이후 정상적으로 발급된 쿠키와 저장된 쿠키를 확인할 수 있었다.

![](https://images.velog.io/images/_woogie/post/79dc902b-21af-4f5f-a053-d911299ec8a4/Screen%20Shot%202020-11-18%20at%206.52.10%20PM.png) 크롬 개발자도구 network에서 response-headers의 내용

![](https://images.velog.io/images/_woogie/post/3334b2fe-31aa-4486-aabe-4bebf1383135/Screen%20Shot%202020-11-18%20at%206.52.22%20PM.png) 크롬 개발자도구 application의 쿠키에서 확인한 내용
&lt;br/&gt;
&lt;br/&gt;

## 마무리

덕분에 쿠키에 대해 많은 [공부](https://ko.javascript.info/cookie)를 할 수 있었고 고민하게 되었다. `어떻게 해야 사용자의 경험을 높이면서 보안을 높일 수 있을까?`의 고민이 더욱 필요한 것 같다. 
[사이트](http://www.futchall.com)를 직접 배포하고 운영하면서 여러가지 문제점을 느낀다. 아마 이러한 경험이 앞으로의 개발경험에 중요한 영향을 미칠 것 같다. 

더 매진하자.





</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[AWS에 배포하기]]></title>
            <link>https://velog.io/@_woogie/AWS%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_woogie/AWS%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 14 Nov 2020 09:00:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>완성된 개발물을 배포해보자!</p>
</blockquote>
<h2 id="aws가-뭐야">AWS가 뭐야?</h2>
<h3 id="클라우드-컴퓨팅">클라우드 컴퓨팅</h3>
<p><a href="https://aws.amazon.com/ko/what-is-cloud-computing/?nc1=f_cc">클라우드 컴퓨팅</a>은 IT 리소스를 인터넷을 통해 온디맨드로 제공하고 사용한 만큼만 비용을 지불하는 것을 말한다. 물리적 데이터 센터와 서버를 구입, 소유 및 유지 관리하는 대신 클라우드 공급자로부터 필요에 따라 컴퓨팅 파워, 스토리지, 데이터베이스와 같은 기술 서비스에 접근할 수 있다.</p>
<p><code>AWS는 이러한 클라우드 컴퓨팅을 지원하는 거대 플랫폼이다.</code></p>
<h2 id="왜">왜?</h2>
<h3 id="프리티어를-이용해보자">프리티어를 이용해보자</h3>
<p>신규계정에서 12개월간 무료로 사용할 수 있다. 그러나 사용량이 한도를 초과하면 표준요금이 부과된다. 따라서 비용 발생을 막기위해 <a href="https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/tracking-free-tier-usage.html#free-budget">AWS 예산 프리 티어 사용량 알림</a>을 사용해서 만일의 상황을 대비하면 좋을 것 같다.</p>
<h3 id="기타">기타</h3>
<p>빠르다, 규모의 경제, 이런 말 말고 일단 써보자.</p>
<h2 id="어떻게">어떻게?</h2>
<h3 id="ec2-인스턴스-생성">EC2 인스턴스 생성</h3>
<p>EC2는 독립된 컴퓨터를 임대해주는 시스템이다.</p>
<p>먼저 서울 리전(지역)을 선택해주자</p>
<p><img src="https://images.velog.io/images/_woogie/post/b7c14dd7-c824-4fa0-9b1e-9b92de2b5255/Screen%20Shot%202020-11-14%20at%2011.20.07%20AM.png" alt=""></p>
<p>리전은 서버의 위치를 말하는 것으로 내가 서비스하려는 상품의 주 고객층의 위치와 같을수록 더 빠른 서비스 제공이 가능하다.</p>
<p>이후 EC2를 사용하여를 클릭하자.</p>
<p><img src="https://images.velog.io/images/_woogie/post/0bde61bd-2170-48ef-a5bf-c0ce43e1475d/Screen%20Shot%202020-11-14%20at%2011.22.51%20AM.png" alt=""></p>
<p>이후 화면은 AMI, 운영체제를 선택하는 화면이 나온다. 이후 Ubuntu server를 선택
<img src="https://images.velog.io/images/_woogie/post/887d28fa-4456-4bd3-b71a-de2c1986c37c/Screen%20Shot%202020-11-14%20at%2011.25.30%20AM.png" alt=""></p>
<p>클릭 후 가용한 프리티어 상품은 딱 하나다. 선택해주자.</p>
<p><img src="https://images.velog.io/images/_woogie/post/eb9510e3-36e3-4b3d-9d72-fe02c2a0a19f/Screen%20Shot%202020-11-14%20at%2011.27.14%20AM.png" alt=""></p>
<p>이후 검토 및 시작이 아니라 인스턴스 세부 정보 구성을 누르자.</p>
<p><img src="https://images.velog.io/images/_woogie/post/c8ad576a-0edb-4ac9-a9ef-f946baba277d/Screen%20Shot%202020-11-14%20at%2011.28.52%20AM.png" alt=""></p>
<p>3번탭인 인스턴스 구성 탭이 뜰텐데 프리티어에서는 고려할 사항이 많지 않으니 패스해준다. 그리고 6번 보안 그룹 구성에서 규칙추가를 해준다. </p>
<p><img src="https://images.velog.io/images/_woogie/post/4366b7a5-4288-4a2b-af32-3170aa850758/Screen%20Shot%202020-11-14%20at%2010.01.47%20AM.png" alt=""></p>
<p>이렇게하면 HTTP와 HTTPS의 모든 IP에서 접근이 가능하다. SSH는 Secure Shell Protocol로 원격지의 shell을 안전하게 제어하기 위해 내IP로 설정해두었다. 설정 후 검토 밑 시작을 누른다. 누르면 키페어 관련하여 창이 뜬다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/43dab08c-f918-4bc2-b85d-5aa939e8a3c2/Screen%20Shot%202020-11-14%20at%2011.33.06%20AM.png" alt=""></p>
<p>비밀번호 사용시 해킹위험이 크기 때문에 키페어를 통해 연결하도록 만들어졌다. 또한 해당 키페어는 한 번 발급시 <strong>절대 재발급이 안되니</strong> 주의 해야한다. 발급 받은 키페어는 <code>키페어이름.pem</code>로 이루어져있으며 해당 키페어는 안전한 장소에 보관해야 한다. 키페어는 <a href="https://aws.amazon.com/ko/getting-started/hands-on/launch-a-virtual-machine/">AWS</a>에서 .ssh폴더에 보관하도록 유도하고 있다.</p>
<p>이후 서비스 - 컴퓨팅 - EC2에 들어가면 해당화면을 볼 수 있다.
<img src="https://images.velog.io/images/_woogie/post/8fc73b3c-dbcd-47f1-95fb-369bd291a1c6/Screen%20Shot%202020-11-14%20at%2011.38.42%20AM.png" alt=""></p>
<p>추가적으로 실행중인 인스턴스에 들어가면 아래화면이 나온다. 아무것도 없다고 걱정하지 말자 아직 준비중일 것이다.(조금 기다리면 뜬다, 그것도 급하면 인스턴스 상대: running에 X 버튼을 누르자)</p>
<p><img src="https://images.velog.io/images/_woogie/post/c7e4dfc0-e315-4fae-8078-d33b9e90855e/Screen%20Shot%202020-11-14%20at%2011.39.00%20AM.png" alt=""></p>
<h3 id="인스턴스-연결">인스턴스 연결</h3>
<p>실행중인 인스턴스에서 생성한 인스턴스를 클릭 후 연결을 누르면 해당 창이 나온다.
<img src="https://images.velog.io/images/_woogie/post/13318645-1176-4d7c-b03b-069d14342922/Screen%20Shot%202020-11-14%20at%2011.43.35%20AM.png" alt=""></p>
<p>해당 창에서 <code>예:ssh -i ~~~~~</code>라고 나온 부분을 복사한다.</p>
<p>프로젝트 폴더로 터미널을 이동 후 복사 한 내용을 입력한다. 만약 .ssh 폴더에 pem을 넣어뒀을 경우 복사한 내용에서 <code>&quot;./.ssh/~~.pem&quot;</code>으로 바꿔준다.</p>
<p><code>그런데!</code></p>
<p><img src="https://images.velog.io/images/_woogie/post/544acfd2-5bb6-4d47-acd8-f129cf329724/Screen%20Shot%202020-11-14%20at%202.07.29%20PM.png" alt=""></p>
<p>이와 같은 에러가 뜬다면</p>
<p><code>chmod 400 pem이 있는 경로</code>를 입력하여 권한을 막아주면 된다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/3a14eb4e-5767-4ce5-8c31-873d12e8369e/Screen%20Shot%202020-11-14%20at%201.58.25%20PM.png" alt=""></p>
<p>이후 다시 복사한 내용을 입력하고 <code>yes/no/[fingerprint]</code>를 물어본다면 yes를 입력하면 된다. 생성한 인스턴스 ubuntu에 연결된다.</p>
<h3 id="ubuntu에서-node-사용하기">ubuntu에서 node 사용하기</h3>
<p>먼저 <code>npm i</code>를 하기전에 <code>node</code>를 설치해야한다.</p>
<ol>
<li><code>sudo apt-get update</code></li>
<li><code>sudo apt-get install -y build-essential</code></li>
<li><code>curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash --</code></li>
<li><code>sudo apt-get install -y nodejs</code></li>
</ol>
<p>이후 노드 버전과 npm버전을 확인 후 설치를 확인한다.
<img src="https://images.velog.io/images/_woogie/post/ceabfb18-7dc2-4092-b58d-89098dfc279d/Screen%20Shot%202020-11-14%20at%202.24.55%20PM.png" alt=""></p>
<p>확인 후 <code>npm i</code>를 입력한다.</p>
<p>dependencies가 설정이 완료되고 이제 <code>npm run build</code>를 통해 빌드해 볼 수 있다.</p>
<h3 id="ubuntu에서-mysql-8버전-사용하기">ubuntu에서 mysql 8버전 사용하기</h3>
<ol>
<li><code>wget -c https://dev.mysql.com/get/mysql-apt-config_0.8.11-1_all.deb</code></li>
<li><code>sudo dpkg -i mysql-apt-config_0.8.11-1_all.deb</code></li>
<li><code>sudo apt-get update</code></li>
<li><code>sudo apt-get install mysql-server</code></li>
<li><code>sudo su</code></li>
<li><code>mysql_secure_installation</code></li>
<li>비밀번호 설정 및 <code>y</code>입력</li>
</ol>
<p>이렇게 설치가 끝난다. <a href="https://phoenixnap.com/kb/how-to-install-mysql-on-ubuntu-18-04">출처</a></p>
<p>mysql을 사용하려면</p>
<p><code>service mysql status</code>로 현재 상태를 알 수 있고, <code>service mysql stop</code>로 중단,  <code>service mysql start</code>로 시작할 수 있다.</p>
<p>접속하려면 <code>sudo mysql -u root -p</code>를 입력 후 설정한 비밀번호를 입력하면 된다.</p>
<h3 id="front-back-서버-실행하기">front, back 서버 실행하기</h3>
<p>front는 <code>Next js</code>를 사용중이기 때문에 <code>next build</code>를 했다.
back은 <code>node index</code>를 통해 실행했다.</p>
<blockquote>
<p>그런데!</p>
</blockquote>
<p>back에서 오류가 난다.</p>
<p><img src="https://images.velog.io/images/_woogie/post/01c5849e-7698-450c-9699-e85b41ee792d/Screen%20Shot%202020-11-14%20at%203.12.18%20PM.png" alt=""></p>
<p>이유는 바로 .env로 설정해놓은 값이 gitignore로 등록되면서 AWS에는 파일이 존재하지 않기 때문에 오류가 뜬 것이다.</p>
<h3 id="vim을-사용해-env-만들기">vim을 사용해 .env 만들기</h3>
<p><code>vim .env</code>를 입력해보자</p>
<p><img src="https://images.velog.io/images/_woogie/post/8e6abd5c-0c57-47ce-817a-33030c181d78/Screen%20Shot%202020-11-14%20at%203.14.23%20PM.png" alt=""></p>
<p>위와 같은 에디터가 콘솔창에 뜬다. 이후 a를 입력하면 입력모드로 바뀌게 되고 .env파일을 작성해주면 된다.</p>
<p>입력 후 esc버튼을 누르고 <code>:wq</code>를 누르면 저장(w)하고 나가지게(q) 된다.</p>
<p>이후 다시 콘솔창에 <code>ls -a</code>를 입력하면 .env가 있는 걸 볼 수 있다.</p>
<blockquote>
<p>참고로 리눅스에서 파일이나 폴더 앞에 <code>.</code>이 있는 경우 숨김 폴더이고, <code>-a</code>를 입력해야 <code>ls</code>를 했을 때 확인할 수 있다.</p>
</blockquote>
<h3 id="node는-foreground-process다">node는 foreground process다.</h3>
<p>이게 무슨 말인가?</p>
<p>back에서 연결한 shell이 꺼지면 aws에서 부여한 ip로의 접속이 끊긴다.
왜냐면 node는 foreground process라서 그렇다.</p>
<blockquote>
<p>아니 그러면 shell을 계속 켜놔야 하는거야?</p>
</blockquote>
<h3 id="pm2-이용하기">pm2 이용하기</h3>
<p>pm2를 이용하면 background로 실행이 되며, 에러를 로그에 기록하고 서버가 죽으면 서버를 자동으로 재시작해준다.</p>
<ol>
<li>먼저 pm2를 back 인스턴스에 설치해준다.
<code>npm i pm2</code></li>
<li>설치가 완료되었으면 vim을 이용해 package.json 파일을 연다.
<code>vim package.json</code></li>
<li>package.json에서 scripts에서 &quot;start&quot;: &quot;pm2 start index.js&quot;로 바꿔준다.</li>
<li>저장 후 <code>sudo npm start</code>를 입력하면 pm2로 실행된다.</li>
</ol>
<p>실행하면 background process로 실행되고 다른 명령어 실행이 가능하다.</p>
<p>추가적으로 </p>
<p><code>npx pm2 monit</code>을 통해 로그를 아래와 같이 확인할 수 있다.
<img src="https://images.velog.io/images/_woogie/post/550d1e0f-d74c-401c-979c-8d3e59919fa5/Screen%20Shot%202020-11-14%20at%203.50.53%20PM.png" alt=""></p>
<p>종료하기 위해서는 <code>sudo npx pm2 kill</code>을 하면 된다.
로그를 보려면 <code>sudo npx pm2 logs</code>,
에러로그는 <code>sudo npx pm2 logs --error</code>를 입력하면 된다.
재시작하려면 <code>sudo npx pm2 reload all</code>
실행중인 프로세스를 보려면 <code>sudo npx pm2 list</code></p>
<p>이후 CORS 등 자질구레한 사항을 해결해야 한다.</p>
<h2 id="이제-시작">이제 시작</h2>
<p>잠깐 접해본 AWS는 나에게 <code>이제 진짜 웹 개발을 하고 있구나</code>라는 생각을 주었다. mac에서 개발한 나는 window에서 어떻게 보여질지, 1300해상도에서 작업한 나는 1980 해상도에서 어떻게 작동할지 험난한 여정을 이제 시작한 것이다. 드디어 휴대폰에서도 태블릿에서도 내가 개발한 프로젝트를 만날 수 있었지만 너무 <strong>부족하다.</strong></p>
<p>프로젝트 첫 시작할 때 마음가짐을 다시금 가질 때이다.</p>
<blockquote>
<p>다시 처음으로.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Passport js와 그럴듯한 refresh, access token 만들기]]></title>
            <link>https://velog.io/@_woogie/Passport-js%EC%99%80-%EA%B7%B8%EB%9F%B4%EB%93%AF%ED%95%9C-refresh-access-token-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@_woogie/Passport-js%EC%99%80-%EA%B7%B8%EB%9F%B4%EB%93%AF%ED%95%9C-refresh-access-token-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 10 Nov 2020 11:32:59 GMT</pubDate>
            <description><![CDATA[<p><code>저번에 이어서 어떤 방식으로 구현했는지 좀 더 코드의 중점을 맞췄다.</code></p>
<br/>

<p><a href="https://velog.io/@_woogie/JWT-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EB%B0%A9%EC%8B%9D-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-feat.-session%EC%97%90%EC%84%9C-jwt%EB%A1%9C#%EA%B2%B0%EA%B5%AD-%EB%82%98%EB%8A%94">나의 방식</a>에 대한 코드이다.</p>
<h2 id="로그인">로그인</h2>
<blockquote>
<pre><code>router.post(&#39;/login&#39;, isNotLoggedIn, async (req, res, next) =&gt; {
  passport.authenticate(&#39;local&#39;, { session: false }, (err, user, info) =&gt; {
    if (err) {
      console.error(err);
      return next(err);
    }
    if (info) {
      return res.status(401).send(info.reason);
    }
    return req.login(user, { session: false }, async (loginErr) =&gt; {
      if (loginErr) {
        console.error(loginErr);
        return next(loginErr);
      }
      const fullUserWithoutPwd = // 각 db에 맞춰서..
      const refreshToken = jwt.sign({ /* 원하는 내용 */ }, &quot;JWT_SECRET&quot;, { expiresIn:&#39;14d&#39;});
{
    /*사용중인 DB에 refreshToken 저장*/
}
      const accessToken = jwt.sign({/* 원하는 내용 */}, &quot;JWT_SECRET&quot;, { expiresIn: &#39;30m&#39; });
      res.cookie(&#39;RefreshToken&#39;, refreshToken, { httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 14});
      return res.status(200).json({ me: fullUserWithoutPwd, token: accessToken });
    });
  })(req, res, next);
});</code></pre></blockquote>
<pre><code>
로그인이 진행되면 먼저 passport-local을 통해 수립한 local전략을 처리 후 로그인을 진행한다. 이후 각기 다른 내용으로 RefreshToken과 AccesToken을 만들어 전자는 **쿠키**로 후자는 JSON Payload로 사용자에게 보낸다.

## AccessToken에 필요한 페이지 접속

Next 기반의 SSR을 기반으로 하기에 페이지 전환시 값 유지에 어려움이 있다. ~~Redux Persist를 사용하지 않았다.~~ 따라서 AccessToken이 필요한 지점에 때 맞춰 해당 토큰을 서버에서 보내기로 했다. 

### 사용자 
&gt; ```
export const getServerSideProps = wrapper.getServerSideProps(async (context) =&gt; {
  const cookie = context.req ? context.req.headers.cookie : &#39;&#39;;
    if (cookie) {
      axios.defaults.headers.common.Authorization = `Bearer ${cookie}`;
      context.store.dispatch({ type: LOAD_MY_INFO_REQUEST });
    }
  }
  context.store.dispatch(END);
  await context.store.sagaTask.toPromise();
});</code></pre><p>SSR을 하기 위해 next의 getServerSideProps를 사용해 정보를 가져왔다. 이 때 refreshToken을 사용하여 사용자의 기본적인 정보와 AccessToken을 가져오도록 했다.</p>
<h3 id="서버">서버</h3>
<blockquote>
<pre><code>router.get(&#39;/myinfo&#39;, passport.authenticate(&#39;refresh-jwt&#39;, { session: false }), async (req, res, next) =&gt; {
  try {
    const fullUserWithoutPwd = await db.User.findOne({
    // 원하는 내용
    });
    if (req.headers.authorization !== fullUserWithoutPwd.token) {
      return res.status(403).send(&quot;CSRF Attacked&quot;);
    }
    const accessToken = jwt.sign({ /* 원하는 내용 */}, process.env.JWT_SECRET, { expiresIn: &#39;30m&#39; });
    res.status(200).json({ me: fullUserWithoutPwd, token: accessToken });
  } catch (error) {
    console.error(error);
    next(error);
  }
});</code></pre></blockquote>
<pre><code>
서버에서는 해당 Authorization header를 확인하고 DB의 값과 비교 후 처리하도록 했다.


## passport 전략 여러개 세우기

Refresh Token이 오느냐 Access Token이 오느냐를 확인하기위해 passprot-jwt의 전략을 2가지로 나눴다. [출처](https://stackoverflow.com/questions/39795898/multiple-passport-jwt-strategy-in-the-same-app/51445027)

&gt; ```
    passport.use(&#39;admin-rule&#39;,
        new JwtStrategy(opts, (...........) =&gt; {.........
    }));
    passport.use(&#39;user-rule&#39;,
        new JwtStrategy(opts, (...........) =&gt; {.........
    }));</code></pre><p>단순히 이름을 붙여 전략을 나눌 수 있었다.</p>
<h2 id="마무리">마무리</h2>
<p>여전히 XSS공격이 도사리고 있다. 또한 AccessToken을 재발급 받는 방법이 뚫린다면 여전히 CSRF공격 또한 가능하다. 물론 XSS취약점이 없다는 상황에서, 재발급 받은 AccessToken을 JS에서 다룰 수 없다는 점과 Referer Check를 통해 1차적으로 제한을 뒀다는 점에서 이전과는 비교할 수 없을 정도로 발전했다고 생각한다. </p>
<p>배우지 않았고 너무도 생소한 <strong>보안</strong>의 영역이었지만 빈틈을 계속 메워가며 발전시켜야 할 것 같다.</p>
]]></description>
        </item>
    </channel>
</rss>