<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mono.log.ue</title>
        <link>https://velog.io/</link>
        <description>안녕나는클레오파트라세상에서제일가는포테이토칩</description>
        <lastBuildDate>Sun, 15 Sep 2024 03:41:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>mono.log.ue</title>
            <url>https://images.velog.io/images/cindy-choi/profile/1dcf051d-4820-472b-a3fc-2782449f2430/9D473C99-114F-4D41-A8F3-46B421A49FD1.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. mono.log.ue. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/cindy-choi" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[MS Azure DP-900 따끈따끈 후기]]></title>
            <link>https://velog.io/@cindy-choi/MS-Azure-DP-900-%EB%94%B0%EB%81%88%EB%94%B0%EB%81%88-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@cindy-choi/MS-Azure-DP-900-%EB%94%B0%EB%81%88%EB%94%B0%EB%81%88-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 15 Sep 2024 03:41:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/cindy-choi/post/ac5dd6e1-cc46-4f7b-ada2-a6cd29a1db78/image.png" alt=""></p>
<p>DP 900 ... 어렵지 않다.
개인적인 견해로는 전공자라면 기출문제 두어번 보면 충분히 딸 수 있다. 
⠀
⠀</p>
<h1 id="🖥️-시험-신청">🖥️ 시험 신청</h1>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/ba67a671-b544-47b4-a5e1-03ded05ed1a1/image.png" alt=""></p>
<p><a href="https://learn.microsoft.com/ko-kr/credentials/certifications/azure-data-fundamentals/?practice-assessment-type=certification">https://learn.microsoft.com/ko-kr/credentials/certifications/azure-data-fundamentals/?practice-assessment-type=certification</a></p>
<p>MS사이트에서 &lt;시험 예약&gt; 을 하면 된다. 환율 기준 9만원 넘었다. 
빨리 따는게 이득. </p>
<p>시험 신청 할 때 언어를 *<em>한국어로 해도 상관 없다! *</em></p>
<p>⠀
⠀
⠀</p>
<h1 id="👩🎓-시험-준비">👩‍🎓 시험 준비</h1>
<h2 id="course">Course</h2>
<p>꼭 뭔가를 들어야겠다면 MS 교육 센터에서 제공하는 트레이닝 코스를 추천. </p>
<p><a href="https://learn.microsoft.com/ko-kr/training/courses/dp-900t00">https://learn.microsoft.com/ko-kr/training/courses/dp-900t00</a></p>
<p>⠀</p>
<h2 id="dumps">Dumps</h2>
<p>초급 시험에 돈 안쓰고 싶어서 무료 덤프만 여럿 찾아봤다.</p>
<h4 id="1-examtopics">1) Examtopics</h4>
<p>60문항인가 70문항 정도 Free. Free만 써도 충분!  
<a href="https://www.examtopics.com/exams/microsoft/dp-900/">https://www.examtopics.com/exams/microsoft/dp-900/</a></p>
<h4 id="2-ms-러닝-센터-50문제">2) MS 러닝 센터 50문제</h4>
<p>안내 페이지 하단에 있는 &lt;실습 평가&gt; 로 진입하면 된다. 
<img src="https://velog.velcdn.com/images/cindy-choi/post/a5abeca4-d219-42b2-a713-dbe6a8f51056/image.png" alt=""></p>
<p>들어갈 때 마다 랜덤하게 50문제씩 나온다. 중복도 있고 처음보는 문제도 섞여나오고... 
쭉~ 풀어보고 답변도 체크할 수 있어서 좋다. 
<a href="https://learn.microsoft.com/en-us/credentials/certifications/azure-data-fundamentals/practice/assessment?assessment-type=practice&amp;assessmentId=24&amp;practice-assessment-type=certification">https://learn.microsoft.com/en-us/credentials/certifications/azure-data-fundamentals/practice/assessment?assessment-type=practice&amp;assessmentId=24&amp;practice-assessment-type=certification</a></p>
<h4 id="3-혼밥맨-블로그">3) 혼밥맨 블로그</h4>
<p>여기 답 틀린거 엄청 많으니 유의해서 풀어야 한다.
<a href="https://gogetem.tistory.com/entry/DP-900-%EB%8D%A4%ED%94%84-%E3%85%A3DP900-%EB%8D%A4%ED%94%84%E3%85%A3Microsoft-Azure-Data-Fundamentals-%EB%8D%A4%ED%94%84-">https://gogetem.tistory.com/entry/DP-900-%EB%8D%A4%ED%94%84-%E3%85%A3DP900-%EB%8D%A4%ED%94%84%E3%85%A3Microsoft-Azure-Data-Fundamentals-%EB%8D%A4%ED%94%84-</a></p>
<p>⠀
⠀
⠀</p>
<h1 id="시험-보기">시험 보기</h1>
<p>온라인으로 하면 카메라 켜고 끝없이 의심 받는거 같아서 오프라인으로 시험치기로 했다. 
강남역 12번 출구 HWG Testing Center 가 제일 자주 열리고 접근성이 좋았다.</p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/e88b0c5a-9d3a-4f9f-b775-0925f034f7ba/image.png" alt=""></p>
<p>건물에 안 써져 있어서 헷갈리는데 갓덴스시 옆에 있는 출입구로 들어가면 된다. </p>
<p>10층 내려서 왼쪽으로 꺾으면 테스팅 센터가 있다. 
신분증 검사하고 카메라에 얼굴 스캔하고(?) 락커에 휴대폰 포함 모든 짐을 다 넣어두고나면 시험볼 준비 진짜 끝!</p>
<blockquote>
<p> 🔔 아 맞다
민증 대신 면허증 가져갔는데 이거 보조 증명이라 <strong>체크카드나 신용카드가 추가로 필요</strong>하다고 하니 알아두세요. </p>
</blockquote>
<p>8시 50분쯤 갔는데 바로 시험이 시작됐다. 나보다 먼저 온 사람도 2명이나 있었다.</p>
<p>시험장은 컴퓨터 10대 정도 있는 컴퓨터실이었고 칸막이에 cctv 가 칸마다 달려있다. 
귀마개도 주던데 안 썼다. 숨막히게 조용했다.... </p>
<p>시험 다 치고 그자리에서 합격판정이 나오기 때문에 쫄린다... 
시험치고 나가면 또 뭐 서명하고 그럼 진짜로 끝! 나갈때 요청하면 프린트 해주시는것 같았다. </p>
<p>⠀
⠀
⠀</p>
<h3 id="아-맞다-🐯-시험치는-ui에서-언어-toggle-됨">아 맞다 🐯 시험치는 UI에서 언어 toggle 됨</h3>
<p><em>공부는 영어로 했는데 시험은 한국어로 신청한 나</em>.... 어떻게든 되겠지 했는데 다행히 toggle 기능이 있었다.</p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/d4486cf7-2555-4e9d-8223-5e5c7f0fe3d7/image.png" alt=""></p>
<p>샌드박스 테스트 UI 랑 똑같다
옆에 지구본 누르면 영어 &lt;-&gt; 한국어 바꿀 수 있다. 
시험지에 번역 상태가 좋은건 아니라서 영어로도 읽어보고 한국어로도 읽어보는게 좋을 것 같다. </p>
<p><a href="https://go.microsoft.com/fwlink/?linkid=2226877">https://go.microsoft.com/fwlink/?linkid=2226877</a>
시험 UI를 테스트해볼 수 있게 샌드박스를 제공중! 해당 링크로 들어가면된다. </p>
<h3 id="dump-에서-100-안-나옴">Dump 에서 100% 안 나옴</h3>
<p>체감상 한 8문제 정도는 아예 초면이었다. 그래도 실수만 안 한다면 합격할 수 있는 정도이다.</p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/637146d7-38e7-4031-9d58-2d73b9ab4ba8/image.png" alt=""></p>
<p>합격화면 보고 1시간 정도 지나면 메일로 날아오고 홈페이지에서도 확인할 수 있다. </p>
<p>끝~~ </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Storybook 구 버전 사용하기 (feat. v6.5)]]></title>
            <link>https://velog.io/@cindy-choi/%EC%A7%84%ED%96%89-%EC%A4%91%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-storybook-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@cindy-choi/%EC%A7%84%ED%96%89-%EC%A4%91%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-storybook-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 25 Aug 2023 12:11:32 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에 공통 컴포넌트가 자꾸 추가되는 와중 사용법을 매번 문서로 남기기 귀찮았다.
유닛 테스트도 할 겸 storybook을 도입하기로 했다. </p>
<p>했는데... 설치/실행이 여간 어려웠음. 너무 힘들었어요. 
나같이 포기하는 사람이 없도록... 기록을 남겨본다. </p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/7b6d7d39-ea84-4118-904e-d0f86543a781/image.png" alt=""></p>
<h1 id="1-storybook-설치하기">1. storybook 설치하기</h1>
<p>Storybook init 은 <strong>빈 프로젝트를 위한 명령어가 아니다</strong>. 설치 전 반드시 React 프레임워크 세팅이 되어있어야 한다. CRA 로 프로젝트를 생성한 다음, 프로젝트 경로 안에서 설치를 진행한다.</p>
<p>다행히(?) 나는 진행 중인 프로젝트에 추가하고 싶었던 것이기 때문에... 집도를 시작한다.</p>
<h3 id="최신-버전-설치-시도-실패-🍎">최신 버전 설치 시도: 실패 🍎</h3>
<pre><code># 프로젝트의 root 경로 (package.json 파일이 있는) 에서 실행한다.
npx storybook@latest init</code></pre><p>결과: 
<img src="https://velog.velcdn.com/images/cindy-choi/post/3f0bae1a-335a-4b29-ab7a-a5267f66f344/image.png" alt=""></p>
<p>어 그래.. 한 번에 안 될 줄 알았어. 
아래와 같은 에러가 났다.</p>
<pre><code> Adding Storybook support to your &quot;Create React App&quot; based project
     Error: Storybook 7.0+ doesn&#39;t support react-scripts@&lt;5.0.0.</code></pre><p><img src="https://velog.velcdn.com/images/cindy-choi/post/b502082a-0d04-4153-9e6e-9efd8c61def9/image.png" alt=""></p>
<p>찾아보니 storybook blob에 이런 문장이 있었다.
storybook 7.0 이상은 CRA 4를 지원하지 않으니 업그레이드 하거나 6.x 버전을 사용하라는 것이다. 너무해!!! </p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/4a1cbea3-19d6-4fd5-a925-5130323f0fbb/image.png" alt=""></p>
<p>기존 프로젝트에 갈 영향을 최소화 하려면 CRA 업그레이드보다는 낮은 버전을 설치하는게 좋을 것 같았다. 
그래서 ** CRA 4를 지원하는 버전인 v6.5을 설치하기로 했다.**</p>
<p>storybook repo에 가서 tags 를 확인해보면 7.0 정식 릴리즈 이전 마지막 6.5 버전대가 6.5.15이다. </p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/15032f00-8918-45d7-8969-5f334daa069f/image.png" alt=""></p>
<p>설치합시다</p>
<h3 id="6515-설치-시도-성공-🍏">6.5.15 설치 시도: 성공 🍏</h3>
<pre><code>npx storybook@6.5.15 init
# npx storybook@6.5.15 init -f</code></pre><p>위 명령어를 사용해서 6.5 버전을 설치했다.</p>
<blockquote>
<p>설치 과정에서 <code>There seems to be a Storybook already available in this project.</code> 어쩌고 에러가 난다면 명령어 뒤에 <code>-f</code> 옵션을 붙여서 재시도 하시면 된다. 이전에 설치했던 잔여물이 남은 거다.</p>
</blockquote>
<p>이번엔 설치가 무사히 완료되었다.</p>
<h1 id="2-storybook-실행하기-v6515">2. storybook 실행하기 (v6.5.15)</h1>
<h3 id="1차-시도-실패-🍎">1차 시도: 실패 🍎</h3>
<p>무사히 설치는 했는데 <code>npm run storybook</code> 에서 또 에러가 났다..</p>
<pre><code>Ξ dev/ndc-ui git:(storybook) ▶ npm run storybook

&gt; ndc-ui@1.0.0-rc2 storybook /home/cindy/dev/ndc-ui
&gt; start-storybook -p 6006 -s public

sh: 1: start-storybook: not found
npm ERR! code ELIFECYCLE
npm ERR! syscall spawn
npm ERR! file sh
npm ERR! errno ENOENT
npm ERR! ndc-ui@1.0.0-rc2 storybook: `start-storybook -p 6006 -s public`
npm ERR! spawn ENOENT
npm ERR!
npm ERR! Failed at the ndc-ui@1.0.0-rc2 storybook script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/cindy/.npm/_logs/2023-08-25T11_06_35_438Z-debug.log</code></pre><p>start-storybook 스크립트가 없다는데? 
아오 스토리북.. 너 뭐 돼?</p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/7b612830-635d-4b59-99ca-bcaeceba4184/image.png" alt=""></p>
<p>찾아봤더니 7.0 이상 버전에서 <code>start-storybook</code> 대신 <code>storybook</code> 또는 <code>sb</code> 명령어를 쓰도록 변경되었다고 한다. (참고: <a href="https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#start-storybook--build-storybook-binaries-removed">storybook repo</a>)</p>
<p>근데 이상하잖아.
난 6.5를 깔았다고.</p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/2755c6da-d4eb-4362-9794-dde30ab6d723/image.png" alt=""></p>
<p>혹시나 싶어 package.json 을 열었더니 <code>@storybook</code> 이하 모듈들이 전부 7.3 버전으로 설치되어 있는것이 아닌가.</p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/84c9c111-9658-4758-8aa3-d335a710fcf2/image.png" alt=""></p>
<p>야 이놈들아 내가 6.5.15 버전 설치한다고 했잖아... 
내가 잘못했나 싶어서 다시 시도해봤지만 늘! 언제나! 올웨이즈! 7.3을 설치해주고 있었다. </p>
<p>결국 수동으로 package.json을 열어 <code>@storybook</code> 의존성 모듈 버전을 변경해주고 npm install을 다시 해주기로 한다. </p>
<h3 id="2-1-packagejson--devdependencies-수정">2-1. package.json &gt; devDependencies 수정</h3>
<pre><code>    &quot;@storybook/addon-actions&quot;: &quot;^6.5.15&quot;,
    &quot;@storybook/addon-essentials&quot;: &quot;^6.5.15&quot;,
    &quot;@storybook/addon-interactions&quot;: &quot;^6.5.15&quot;,
    &quot;@storybook/addon-knobs&quot;: &quot;^6.4.0&quot;,
    &quot;@storybook/addon-links&quot;: &quot;^6.5.15&quot;,
    &quot;@storybook/builder-webpack4&quot;: &quot;^6.5.16&quot;,
    &quot;@storybook/manager-webpack4&quot;: &quot;^6.5.16&quot;,
    &quot;@storybook/node-logger&quot;: &quot;^6.5.15&quot;,
    &quot;@storybook/preset-create-react-app&quot;: &quot;^3.2.0&quot;,
    &quot;@storybook/react&quot;: &quot;^6.5.15&quot;,</code></pre><p>@storybook 으로 시작하는 애들 싹 수동으로 버전을 명시했다. (진짜 귀찮네)
testing-library 어쩌고는 중간에 에러 나서 지워버렸는데, 걔는 버전 그대로 둬도 상관 없다.</p>
<h3 id="2-2-node_modules-제거">2-2. node_modules 제거</h3>
<pre><code>rm -rf node_modules

# 다른거 영향 가는게 싫으면 아래 명령어로 storybook 만 지우기
# rm -rf node_modules/@storybook</code></pre><h3 id="2-3-의존성-모듈-재설치">2-3. 의존성 모듈 재설치</h3>
<pre><code>npm install</code></pre><h3 id="2-4-srcstories-삭제">2-4. src/stories 삭제</h3>
<p>샘플로 코드 넣어주는 것 같은데, 이것도 7.3.0 기준으로 작성한 건지 뭔지 실행하면 에러가 난다. 
에러나는 샘플 필요 없어!!! 싹 지워버리자. </p>
<p>그런 다음 마지막 실행. </p>
<pre><code>npm run storybook</code></pre><p><img src="https://velog.velcdn.com/images/cindy-choi/post/7d0c589c-b7e3-4378-9008-55fe71224799/image.png" alt=""></p>
<p>떴다! 
하지만 아직 아무 스토리 파일이 없기 때문에 화면에는 아래와 같은 에러가 뜬다. </p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/09fabe03-c090-43f8-9849-49a4f902f4f3/image.png" alt=""></p>
<p>그래도 서서히 끝이 보인다.. 
셀프로 샘플 스토리를 짜고 긴 여정을 끝내보자. </p>
<h2 id="3-stories-짜기">3. stories 짜기</h2>
<h3 id="3-1-story-파일-만들기">3-1. story 파일 만들기.</h3>
<p>6.5 버전 도큐먼트를 참고해서 내 컴포넌트 중 하나의 테스트 코드를 짜보았다. (참고 링크는 <a href="https://storybook.js.org/docs/6.5/react/get-started/setup">여기</a>)</p>
<p>Emptybox 는 비어있는 화면을 대체하는 공통 컴포넌트 중 하나인데, 별다른 액션 없이 visualization만 하는 컴포넌트니까 간단하게 짤 수 있었다. </p>
<p>stories 디렉토리 안 만들고 그냥 소스파일 옆에 하나 만들었다.
1뎁스 까지는 자동으로 인식하는거 같다. </p>
<pre><code>import { ComponentStory, ComponentMeta } from &#39;@storybook/react&#39;;
import EmptyBox from &#39;./EmptyBox&#39;;
import type { EmptyBoxProps } from &#39;./EmptyBox&#39;;

export default {
  title: &#39;EmptyBox&#39;,
  component: EmptyBox,
} as ComponentMeta&lt;typeof EmptyBox&gt;;

const Template: ComponentStory&lt;typeof EmptyBox&gt; = (args) =&gt; &lt;EmptyBox {...args} /&gt;;
export const FirstStory = Template.bind({});

FirstStory.args = {
  title: &#39;타이틀 예제&#39;,
  message: &#39;메세지 예제&#39;,
  inTable: false,
  hasAction: false,
};</code></pre><h3 id="3-1-alias-적용하기">3-1. alias 적용하기</h3>
<p>난관 끝에 난관 오는구만... 
기존 프로젝트가 상대 경로 alias<code>@</code>를 쓰고 있었는데 이걸 storybook 에서 사용하려면 또 별도로 사용 설정을 해주어야 한다. </p>
<p>우선 tsconfig-paths-webpack-plugin 을 설치한다.</p>
<pre><code>npm install -D tsconfig-paths-webpack-plugin</code></pre><p>프로젝트 루트 경로에 <code>.storybook</code> 폴더가 있는데 이 안에 있는 <code>main.js</code> 파일을 열어 아래 내용을 추가해준다. </p>
<pre><code>// 요거랑
const TsconfigPathsPlugin = require(&#39;tsconfig-paths-webpack-plugin&#39;);

module.exports = {
  &quot;stories&quot;: [
    // 수정하는 김에 따로 폴더 쓸거면 여기 수정해도 된다.
    &quot;../src/**/*.stories.mdx&quot;,
    &quot;../src/**/*.stories.@(js|jsx|ts|tsx)&quot;
  ],
  &quot;addons&quot;: [
    &quot;@storybook/addon-links&quot;,
    &quot;@storybook/addon-essentials&quot;,
    &quot;@storybook/addon-interactions&quot;,
    &quot;@storybook/preset-create-react-app&quot;
  ],
  &quot;framework&quot;: &quot;@storybook/react&quot;,

  // 여기 아래를 추가 
  &quot;webpackFinal&quot;: async (config) =&gt; {
    config.resolve.plugins.push(new TsconfigPathsPlugin({}));
    return config;
  },
}</code></pre><p>다행히 이건 문제 없이 잘 됐다. 
다시 <code>npm run storybook</code> 을 해주면... </p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/0f80ee90-67a7-47d6-8c2e-7c6f4a502603/image.png" alt=""></p>
<p>축하합니다.
먼 길 돌아서 드디어 storybook 6.5 버전을 사용할 준비가 되었다. </p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/46bd1041-f00a-4a58-b932-34a3a7fe7a83/image.png" alt=""></p>
<p>끝 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[REACT] process is not defined 해결하기]]></title>
            <link>https://velog.io/@cindy-choi/REACT-process-is-not-defined-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@cindy-choi/REACT-process-is-not-defined-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 23 Feb 2023 09:25:39 GMT</pubDate>
            <description><![CDATA[<p>오래된 npm 패키지들을 업데이트 받았더니, 핫로딩 후에 자꾸만 UI 가 먹통이 되는 현상이 있었다. 최초 로딩은 잘 되는데 왜...? </p>
<p>콘솔 로그를 뽑아보니 아래와 같은 에러가 발생중이었다. 
<img src="https://velog.velcdn.com/images/cindy-choi/post/c965a115-fae7-4685-a82a-4eae96ac8c39/image.png" alt=""></p>
<pre><code>path.js:25 Uncaught ReferenceError: process is not defined
    at ./node_modules/path/path.js (path.js:25:1)
    at options.factory (react refresh:6:1)
    at __webpack_require__ (bootstrap:24:1)
    at fn (hot module replacement:62:1)
    at ./node_modules/keycloakify/lib/getKcContext/kcContextMocks/urlResourcesPath.js (urlResourcesPath.ts:1:1)
    at options.factory (react refresh:6:1)
    at __webpack_require__ (bootstrap:24:1)
    at fn (hot module replacement:62:1)
    at ./node_modules/keycloakify/lib/getKcContext/kcContextMocks/kcContextMocks.js (kcContextMocks.ts:6:1)
    at options.factory (react refresh:6:1)</code></pre><p>다른 때도 아니고 핫로딩때에만 발생하는 것이니 개발 환경에 관련된 것으로 추정. 서치해보니 <code>react-scripts</code> 와 <code>react-error-overlay</code>의 버전 때문이라고들 한다. </p>
<p>최신 버전 설치하라는 말도 있고 한데, 우선 <code>react-scripts</code> 는 <strong>4.0.3</strong> 버전을, <code>react-error-overlay</code> 는 <strong>6.0.9</strong> 버전을 설치하기로 했다. </p>
<p>중간에 그냥 명령어로 이리저리 재설치 해봤더니 깔끔하게 안되더라. 
아싸리 싹 지우고 다시 하는 것을 추천한다. </p>
<h1 id="1_-packagejson-수정하기">1_ package.json 수정하기</h1>
<pre><code> &quot;react-scripts&quot;: &quot;^4.0.3&quot;,</code></pre><p><code>react-scripts</code> 만 있고 <code>react-error-overlay</code> 는 없었다.
상위 버전 호환 되는 캐럿(^) 문자를 빼고 내가 원하는 버전을 직접 명시해준다. </p>
<pre><code>&quot;react-scripts&quot;: &quot;4.0.3&quot;,
&quot;react-error-overlay&quot;: &quot;6.0.9&quot;,</code></pre><p>명령어로 해도 됨. </p>
<h1 id="2_-node_modules-지우기">2_ node_modules 지우기</h1>
<pre><code>rm -rf node_modules</code></pre><p>기존에 설치된 패키지들을 싹 지워준다. rm -rf 가 두려우면 GUI 로 ... </p>
<h1 id="3_-재설치">3_ 재설치</h1>
<pre><code>npm install</code></pre><p>install 명령어로 새로 정의된 package.json을 활용한 의존성 모듈들을 설치했다. </p>
<p>설치가 끝나면 package-lock.json 파일을 열어서 <code>react-scripts</code>랑 <code>react-error-overlay</code> 버전을 확인해준다. </p>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/6a88645c-c44b-4748-a9ec-c83082e9fd11/image.png" alt=""></p>
<h1 id="4_-로컬-서버-재시작">4_ 로컬 서버 재시작</h1>
<p>다시 웹서버를 띄운 다음 핫로딩 상황을 재현한다. </p>
<p>잘 해결 되었다! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) JS로 clean-code를 짜기 위한 8가지 팁]]></title>
            <link>https://velog.io/@cindy-choi/%EB%B2%88%EC%97%AD-JS%EB%A1%9C-clean-code%EB%A5%BC-%EC%A7%9C%EA%B8%B0-%EC%9C%84%ED%95%9C-8%EA%B0%80%EC%A7%80-%ED%8C%81</link>
            <guid>https://velog.io/@cindy-choi/%EB%B2%88%EC%97%AD-JS%EB%A1%9C-clean-code%EB%A5%BC-%EC%A7%9C%EA%B8%B0-%EC%9C%84%ED%95%9C-8%EA%B0%80%EC%A7%80-%ED%8C%81</guid>
            <pubDate>Sun, 30 Oct 2022 05:03:38 GMT</pubDate>
            <description><![CDATA[<p>요즘 공부를 통 안 한것 같아서... 
본문은 아래 링크의 글을 (제맘대로) 번역한 글입니다. 이해를 위해 일부 이미지와 내용을 추가했습니다.
<a href="https://dev.to/alexomeyer/8-must-know-tips-for-writing-clean-code-with-javascript-i4">https://dev.to/alexomeyer/8-must-know-tips-for-writing-clean-code-with-javascript-i4</a></p>
<p>주워먹기 레쯔고
<img src="https://velog.velcdn.com/images/cindy-choi/post/a67b450c-9623-487b-8bf0-78342030e651/image.png" alt=""></p>
<hr>
<h1 id="8-must-know-tips-for-writing-clean-code-with-javascript">8 MUST-KNOW TIPS FOR WRITING CLEAN CODE WITH JAVASCRIPT</h1>
<p>자바스크립트(이하 JS)가 놀라운 언어이긴 하지만, 클린한 JS 코드를 작성하는 것은 어려운 일입니다. 노련한 개발자에게도 말이죠.</p>
<p>&quot;클린한&quot; JS 코드는 어떤 것일까요? 다음의 조건을 충족해야 합니다.</p>
<ol>
<li>읽기 쉽다</li>
<li>디버깅이 쉽다</li>
<li>효율적이고 고성능이다</li>
</ol>
<p>이에 유용한 툴과 트릭 몇 개를 알려드리겠습니다. 이들이 당신의 JS 코드 품질을 한 단계 발전 시킬거예요.</p>
<h2 id="1_-api를-호출하거나-json-함수를-사용할-때-try-catch를-사용하기">1_ API를 호출하거나 JSON 함수를 사용할 때 try-catch를 사용하기</h2>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/7037a930-9486-4ed9-a91f-528c6b665bd6/image.png" alt=""></p>
<p>데이터를 가져오기 위해 API에 요청을 보낼 때에는 많은 것들이 잘못될 수 있습니다. 그러니 호출 시나리오를 관리하는 건 필수죠. JSON을 다룰 때에도 결과값을 무조건적으로 신뢰하지 말고, 가능한 예외 사항들을 처리하도록 해서 당신의 코드를 좀 더 견고하게 만드세요. </p>
<h2 id="2_-linter-사용하기--eslint--tslint-">2_ Linter 사용하기 ( ESLint / TSLint )</h2>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/9f936ab4-6787-4487-923e-98e58ec80ae1/image.jpeg" alt=""></p>
<p>Linter는 미리 정의해둔 규칙과 설정을 기반으로 프로그래밍 방식 또는 코드의 스타일 상 에러를 체크하는 코드 분석 툴입니다. 짧게 말하자면, 린터는 당신의 JS 또는 TS를 개선하고 통일성을 유지할 수 있도록 도와줄 겁니다. </p>
<h2 id="3_-js-이슈를-에디터편집기-ide로-관리하기">3_ JS 이슈를 에디터(편집기, IDE)로 관리하기</h2>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/25b5cdc1-3e49-4339-83ae-b3daddea96b2/image.webp" alt=""></p>
<p>당신의 JS 코드를 클린하게 관리하는 주된 방법 중 하나는 코드 자체의 이슈를 추적하기 쉽게 만들어두고 코드 내부에서 그 이슈를 보는 것입니다. </p>
<p>코드 베이스 이슈들을 에디터로 관리하면:</p>
<ul>
<li>기술 부채와 같은 큰 이슈에서의 전체적인 시각을 가질 수 있다.</li>
<li>각 코드 베이스 이슈에 대한 맥락을 볼 수 있다.</li>
<li>컨텍스트 스위칭을 줄인다.</li>
<li>지속적으로 기술 부채를 해결할 수 있게 한다.</li>
</ul>
<p>기술 부채를 트래킹 하기 위해 다양한 툴을 사용할 수 있지만, 당장 시작하기에 가장 빠르고 쉬운 방법은 Jira, Linear, Asana 등 프로젝트 관리 툴과 연계되어 있는 VSCode나 JetBrains 의 무료 Stepsize 확장 앱을 사용하는 것입니다. </p>
<h2 id="4_-template-string-사용하기">4_ Template string 사용하기</h2>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/2d3fee27-e7c5-4e21-91f5-dcf723ac0c81/image.jpeg" alt=""></p>
<p>템플릿 스트링은 문자열 사이에 어떤 값을 주입하는 동시에 문자열의 형태를 유지하게 하고, 직접 문자열 연산을 수행하는 것 보다 가독성이 좋습니다. </p>
<h2 id="5_-문자열-검색을-할-때에는-정규식-사용하기">5_ 문자열 검색을 할 때에는 정규식 사용하기</h2>
<p>비록 정규식이 밖에서 보기엔 다소 난해할지 몰라도, 아주 강력한 문자열 처리 툴입니다. 심지어 양도 많고 어려운 문자열 일치 시나리오가 있을 때, 이를 설명하기 위한 복잡한 패턴을 구성할 수 있습니다.</p>
<h2 id="6_-optional-chining-사용하기-물음표-연산">6_ optional chining 사용하기 (물음표 연산)</h2>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/b7eeff34-5aaf-43c9-be8a-603c87d6012d/image.png" alt=""></p>
<p>긴 논리적 접속사를 쓰는 걸 그만두세요. 그리고 optional chining으로 당신의 코드를 간결하게 만드세요. </p>
<h2 id="7_-중첩문-지양하기">7_ 중첩문 지양하기</h2>
<p><img src="https://velog.velcdn.com/images/cindy-choi/post/8d59edfd-0513-4d22-abdb-8c679de33c18/image.jpeg" alt=""></p>
<p>중첩은 코드의 복잡성을 증가시키고, 읽고 이해하기 어렵게 만드는 가장 확실한 방법입니다. 2번 이상 중첩 되는 코드가 있다면 최상위 레벨 반환문, 더 짧은 블록들, 그리고 중텁된 로직을 각각의 기능으로 추상화 하는 방식으로 리팩토링 하는 것을 고려해보세요.</p>
<p>(참고. 가장 많은 추천을 받은 댓글 중에 4-5 개의 중첩 정도는 허용해야 한다는 의견이 있습니다. 글쓴이 역시 프론트엔드의 특성상 중첩을 강박적으로 제거하지 않아도 된다는 생각입니다. 중첩문 내부에 독자적인 로직이 있다면 떼어내고, 그렇지 않다면 그냥 두기.)</p>
<h2 id="8_-모든-비전형적-코드에-주석을-달되-코드-가독성을-이걸로-대체하려고-하지-않을-것">8_ 모든 비전형적 코드에 주석을 달되, 코드 가독성을 이걸로 대체하려고 하지 않을 것</h2>
<p>정해진 규칙 없이, 일반적이지 않은 시나리오를 처리해야 하는 순간이 몇 번 있을 겁니다.  이 때 내 코드가 무슨 일을 하는 것이고 어떤 환경적 상황을 고려하고 있는지 설명하는 주석을 달아 두면 다른 개발자들에게 어마무시한 도움이 될겁니다. 뿐만아니라 나중에 당신 자신이 이 코드를 다시 보게 되었을 때에도 도움이 되겠죠. 하지만 처음 코드를 짤 때 가독성에 대해 제대로 고민하지 않는 것을 무마하기 위한 목발(crutch)로 사용해서는 안 됩니다!</p>
<hr>
<p>뭐라고 쓰다가 날려먹어서 기력없음..
끝</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[serve 명령어가 안 된다: Cannot copy to clipboard: xsel 에러 처리]]></title>
            <link>https://velog.io/@cindy-choi/serve-%EB%AA%85%EB%A0%B9%EC%96%B4%EA%B0%80-%EC%95%88-%EB%90%9C%EB%8B%A4-Cannot-copy-to-clipboard-xsel-%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@cindy-choi/serve-%EB%AA%85%EB%A0%B9%EC%96%B4%EA%B0%80-%EC%95%88-%EB%90%9C%EB%8B%A4-Cannot-copy-to-clipboard-xsel-%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 27 Apr 2022 01:17:46 GMT</pubDate>
            <description><![CDATA[<p>어제 밤까지만 해도 멀쩡히 돌아가던 <code>serve -d {경로}</code> 명령어가 갑자기 안돌아갔다. </p>
<pre><code>$ serve -d dist
WARNING: Checking for updates failed:
TypeError: Cannot read property &#39;code&#39; of undefined
    at getMostRecent (/home/cindy/.npm-packages/lib/node_modules/serve/node_modules/update-check/index.js:125:11)
    at async module.exports (/home/cindy/.npm-packages/lib/node_modules/serve/node_modules/update-check/index.js:185:12)
    at async updateCheck (/home/cindy/.npm-packages/lib/node_modules/serve/bin/serve.js:39:12)
    at async /home/cindy/.npm-packages/lib/node_modules/serve/bin/serve.js:396:3
ERROR: Cannot copy to clipboard: Couldn&#39;t find the required `xsel` binary. On Debian/Ubuntu you can install it with: sudo apt install xsel</code></pre><p>xsel 이 없댄다. </p>
<p>나는 Windows10 환경에서 개발 중이긴 하지만 WSL을 사용중이므로 yum 으로 설치할 수 있다. </p>
<pre><code>sudo yum install xsel</code></pre><p>실행해서 설치해주면 끝
잘 돌아간다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트 개발자를 위한: AWS 에 내 웹 앱 올리기 (feat. Amplify)]]></title>
            <link>https://velog.io/@cindy-choi/%ED%94%84%EB%A1%A0%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-AWS-%EC%97%90-%EB%82%B4-%EC%9B%B9-%EC%95%B1-%EC%98%AC%EB%A6%AC%EA%B8%B0-feat.-Next.js</link>
            <guid>https://velog.io/@cindy-choi/%ED%94%84%EB%A1%A0%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-AWS-%EC%97%90-%EB%82%B4-%EC%9B%B9-%EC%95%B1-%EC%98%AC%EB%A6%AC%EA%B8%B0-feat.-Next.js</guid>
            <pubDate>Sat, 26 Feb 2022 09:42:21 GMT</pubDate>
            <description><![CDATA[<p>와 요즘 일 많나보다 
글 자주 쓰는거 보니</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/6ab6e0be-101b-47c9-8dd5-6ad7b9e15263/image.png" alt=""></p>
<h1 id="오늘의-목표">오늘의 목표</h1>
<p>💬 AWS 에 내 웹 앱 올리기 (백엔드 없음)</p>
<hr>
<br>

<h1 id="aws에-내-웹-앱-올리기-백엔드-없음">AWS에 내 웹 앱 올리기 (백엔드 없음)</h1>
<p>우선 AWS Console 에 계정을 만들어야 한다.
귀찮으니 가입 과정은 적지 않겠음. 
개인 프로젝트 정도는 프리티어(무료 회원)로 충분히 가능하니까 겁먹지 말고 조인.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/a9e44a08-1dbc-4147-8352-f95ecdf59e7b/image.png" alt=""></p>
<p>AWS 콘솔에서 <code>Amplify</code> 를 검색해서 AWS Amplify 서비스로 진입한다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/2f588cf0-80f0-487c-b1b8-e68192eee7dc/image.png" alt=""></p>
<p>시작하기를 누르면 자동으로 스크롤이 스르륵 되면서 </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/0c66bd6d-4b2b-417d-899a-255d13a6b4a6/image.png" alt=""></p>
<p>이런 화면이 나오는데, Amplify Hosting 을 선택한다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/a8dab8eb-c170-4dde-8d60-4bb41a9034d5/image.png" alt=""></p>
<p>github 으로 하겠슴다. 계속.</p>
<p>이미 git hub 로그인 연동 하신 분은 약간의 딜레이가 있고 다음 페이지로 자동으로 이동되므로 여러번 클릭하지 말기. 
(AWS 의 모든 페이지가 이런 식이다... UX 𝙒̲̅𝙝̲̅𝙮̲̅𝙧̲̅𝙖̲̅𝙣̲̅𝙤̲̅ .̲̅.̲̅.̲̅ )</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/3f8d4ffe-e6d5-4784-90b4-a76a8a7532e4/image.png" alt=""></p>
<p>로그인 하고 권한을 나눠 가지면 여기로 온다. 
<code>리포지토리 선택</code> 에서 진행할 프로젝트를 선택하자. </p>
<blockquote>
<p>🐅 여기서 선택 가능하려면 해당 리포에 &lt;관리자 권한&gt;이 있어야 한다.</p>
</blockquote>
<p>권한 변경 후에는 옆에 있는 새로고침 버튼으로 리로드 하면 변경사항이 반영된다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/4d0c9a8c-5621-4836-bfa5-d336751ac98a/image.png" alt=""></p>
<p>repo 선택하면 아래에 브랜치 선택 화면이 뜬다. 
골라주고 다음. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/1daa966f-9bdd-46bb-997f-4b205e4640a7/image.png" alt=""></p>
<p>앱 이름을 써준 다음, 서비스 역할을 생성하라고 나온다. 
하단의 새 역할 생성 버튼을 클릭해서 새 창으로 IAM 관리자 화면을 띄운다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/15a47d06-258d-40c3-b670-6dcd083fb89d/image.png" alt=""></p>
<p>AWS 서비스 -&gt; 사용 사례는 <code>다른 AWS 서비스의 사용 사례</code> 에서 <code>Amplify</code>를 검색해서 선택한다. 
그러면 아래에 Amplify - Backend Deployment 라고 뜨는데 이걸 선택해서 다음 버튼을 눌러 진행한다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/80ed8aaa-9860-470f-ade6-426d2bc8b38f/image.png" alt=""></p>
<p>자동으로 Amplify 권한이 선택되어 있을것이다. 
다른 권한은 필요 없으므로 다음. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/2fdb5be1-015b-451d-8a0d-d4f90e3ce3bc/image.png" alt=""></p>
<p>역할의 이름을 알맞게 지어주고 생성을 완료하자. 
기존의 Amplify 탭으로 돌아와서 새로고침 버튼을 누르면 </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/1c6340f9-5e7e-471d-85c3-6ae36364bf73/image.png" alt=""></p>
<p>방금 만든 역할이 보인다.
선택하고 아래로 내려 (혹은 다음으로 진행) 빌드 설정을 하자. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/0bd4ccf4-f3c9-4029-a039-4d5616099e27/image.png" alt=""></p>
<p>이 화면에서 빌드 스크립트가 자동으로 채워지는 프로젝트도 있고, 비어있는 것들도 있다. 위 캡쳐 화면은 비어있을 때 경고가 뜬 모습이다. </p>
<p>만약 본인 프로젝트의 ampllify.yml 파일이 이상하다면 아래 내용을 복사해서 적용하면 된다. (npm 기준)</p>
<pre><code>version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm install
    build:
      commands: 
        - npm run build
  artifacts:
    baseDirectory: .
    files:
      - &#39;**/*&#39;
  cache:
    paths: 
      - node_modules/**/*</code></pre><p>완료하고 다음 단계로 넘어가자. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/7090ba49-168a-4d86-bb65-1d8ffb0a4e67/image.png" alt=""></p>
<p>지금까지의 설정을 확인하고 배포를 시작하는 페이지이다. 
하단의 <code>저장 및 배포</code> 버튼을 누르면 바로 최초 배포가 시작된다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/20dbb67c-e8c7-4045-8865-f508ed748a53/image.png" alt=""></p>
<p>이런 화면에서 프로비저닝(배포 준비), 빌드(npm run build), 배포(deploy) 를 포함해서 총 4단계가 진행된다. </p>
<h2 id="에러-확인-방법">에러 확인 방법</h2>
<p>보통 빌드 단계에서 에러가 발생한다.
로그만 잘 봐도 뭐가 문제인지 알 수 있다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/5cd1aaa9-4471-4a66-8b57-aa4e75d7e890/image.png" alt=""></p>
<p>빌드 에러난 페이지에서 </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/bb47f650-0499-41d4-915f-a48342350bc2/image.png" alt=""></p>
<p>빌드 탭을 눌러 해당하는 모듈의 로그를 확인할 수 있다. </p>
<br>
<br>



<h1 id="여기요🖐-여러가지-빌드-에러-해결-faq">(여기요🖐) 여러가지 빌드 에러 해결 FAQ</h1>
<p>내가 앱을 배포하면서 겪은 몇가지 빌드 에러와 해결법을 정리한다. </p>
<h2 id="1-nextjs-ssr-lambda-error">1. (Next.js) SSR Lambda error</h2>
<pre><code>2022-02-23T06:43:32.602Z [ERROR]: Error encountered importing modules into your SSR Lambda functions. Check out our FAQ for suggestions on how to resolve the issue: https://github.com/aws-amplify/amplify-console/blob/main/FAQ.md#webpack-modulenotfound-errors
Terminating logging...</code></pre><p>Next.js 로 만든 프로젝트를 올렸는데
SSR Lambda 어쩌구 에러가 나는 경우엔 환경 변수를 추가하면 해결할 수 있다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/236fe249-d060-4ef7-874c-02b39ed9a65e/image.png" alt=""></p>
<p>현재 앱의 환경 변수 메뉴로 진입한다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/c0983b17-34ae-4161-ab30-59cf630036f7/image.png" alt=""></p>
<p>변수 관리 메뉴 클릭</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/09b87e55-f715-4646-85f4-50b55640ff3b/image.png" alt=""></p>
<p>하단의 변수 추가 버튼을 클릭한 다음
변수는 <code>AMPLIFY_NEXTJS_EXPERIMENTAL_TRACE</code>, 값은 <code>true</code> 를 입력한다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/283b53e7-b160-4945-a151-80af3ee92b8c/image.png" alt=""></p>
<p>이렇게영.
그 다음 다시 배포하면 해결된다. </p>
<h2 id="2-npm-err-spawn-enoent">2. npm ERR! spawn ENOENT</h2>
<pre><code>                                 # Starting phase: build
                                 # Executing command: npm run build
                            npm ERR! spawn ENOENT
                                    npm ERR!
                                    npm ERR! Failed at the keycloakify-sample@0.1.0 build script.
                                    npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
2022-02-26T09:03:19.083Z [WARNING]: npm
2022-02-26T09:03:19.083Z [WARNING]: WARN Local package.json exists, but node_modules missing, did you mean to install?
2022-02-26T09:03:19.083Z [WARNING]: npm ERR! A complete log of this run can be found in:
                                    npm ERR!
2022-02-26T09:03:19.083Z [WARNING]: /root/.npm/_logs/2022-02-26T09_03_19_078Z-debug.log
2022-02-26T09:03:19.086Z [ERROR]: !!! Build failed
2022-02-26T09:03:19.088Z [ERROR]: !!! Non-Zero Exit Code detected
2022-02-26T09:03:19.088Z [INFO]: # Starting environment caching...
2022-02-26T09:03:19.088Z [INFO]: # Environment caching completed
Terminating logging...</code></pre><p>중간에 WARN 로그를 보면 <code>Local package.json exists, but node_modules missing, did you mean to install?</code> 라고 묻는다.</p>
<p>이건 amplyfi.yml 에서 당신이 작성한 빌드 명령어가 뭔가 이상하다는 뜻이다. 
이럴 때에는 빌드 명령어를 다시 확인하자. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/9a5f1c8d-90fc-430c-a519-1a668121d685/image.png" alt=""></p>
<p>내 경우는 비슷한 에러를 총 3번 만났다. </p>
<p>1) <code>preBuild</code> 가 <code>prebuild</code> 로 CamelCase 가 잘못 적혀 있었음
2) 남의 거 복사 하다가 <code>npm</code> 대신 <code>yarn</code>을 썼음 🤗
3) <code>buildDirectory</code> 에 애매하게 <code>build</code> 이런거 썼음. (<code>.</code> 이면 된다.)</p>
<hr>
<h1 id="끝">끝</h1>
<p><img src="https://images.velog.io/images/cindy-choi/post/6d36eb33-13b0-43ce-96f2-5e3c3f9c3ca7/image.png" alt=""></p>
<p>이렇게 갖가지 빌드로 고생을 하다가 전부 성공하고 나면
<code>도메인</code>에 보이는 링크로 내 웹 앱에 접근할 수 있다. </p>
<p>PR 미리보기도 쓰고 싶었는데 조금 지쳤다.
내일 써야지</p>
<p>끝</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) Next.js 로 만든 프로젝트에 다국어 설정 끼얹기]]></title>
            <link>https://velog.io/@cindy-choi/React-Next.js-%EB%A1%9C-%EB%A7%8C%EB%93%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%84%A4%EC%A0%95-%EB%81%BC%EC%96%B9%EA%B8%B0</link>
            <guid>https://velog.io/@cindy-choi/React-Next.js-%EB%A1%9C-%EB%A7%8C%EB%93%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%84%A4%EC%A0%95-%EB%81%BC%EC%96%B9%EA%B8%B0</guid>
            <pubDate>Tue, 22 Feb 2022 06:54:33 GMT</pubDate>
            <description><![CDATA[<p>앗! Next.js 다국어 설정 타이어보다 쉽다!</p>
<br>
<br>

<h1 id="알아두기--next-i18next-동작방식">알아두기 : next-i18next 동작방식</h1>
<p>기본적으로 URL 을 통해 적용된다.
기존 Vue나 CRA 프로젝트로는 이렇게 안하고 store에 변수 저장해놓고 어쩌고... 방식에 익숙해서 처음엔 이렇게 하기 싫었다.</p>
<p>근데 일단 하고 나니까 쉬워서 괜찮아졌다 ^.^ 인간은 적응의 동물!</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/5c672fcd-7238-45f2-b34a-2fd4dc301055/image.png" alt=""></p>
<h2 id="🌐-url-방식으로-동작해요">🌐 URL 방식으로 동작해요</h2>
<p>hostname 과 path 사이에 언어가 들어간다. 이런 식.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/11fad988-c6b5-4c2b-a243-92821444c0b5/image.png" alt=""></p>
<p>company 라는 페이지를 영문으로 보고 싶으면 위와 같이 접근하면 되는 식이다. 
물론 한글로 보려면 <code>localhost:3000/ko/company</code> 이렇게 접속하면 된다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/0205303a-9af7-40e2-97a9-c229eb95487d/image.png" alt=""></p>
<p>만약 언어 설정 없이 localhost:3000/company 로 접근한다면?
기본 값으로 설정된 언어가 자동으로 적용 된다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/6e65b1a7-49d8-4354-81f5-9c9683f4f51b/image.png" alt=""></p>
<p>이렇게요 </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/5b313736-fa2a-4530-967e-cf1ce9bdd59c/image.png" alt=""></p>
<br>
<br>

<p>그럼 이렇게 쉬운 언어설정 </p>
<h1 id="레쯔고🛫">레쯔고🛫</h1>
<h2 id="1-i18n-설치">1. i18n 설치</h2>
<p><code>npm install --save next-i18next</code></p>
<h2 id="2-폴더-만들기">2. 폴더 만들기</h2>
<p><code>public</code> 경로 아래에 <code>locales</code> 폴더를 만들고 제공할 언어 별로 또 다시 폴더를 만든다. </p>
<pre><code>.
└── public
    └── locales
        ├── ko
        |   └── common.json
        └── en
            └── common.json</code></pre><p>common.json 은 공통으로 사용하는 문구를 관리하며, 동일 경로에 페이지 별로 문구들을 묶어놓을 수도 있다. </p>
<blockquote>
<p>💌 이전에 프로젝트를 해보니까 이렇게 페이지 별로 관리하는 게 좋다는 생각. 여러명이서 작업하는 프로젝트의 경우 매번 conflict 나는 것도 문제고, 불필요한 문구를 제거할 때도 힘들다. </p>
</blockquote>
<p>그러니까 페이지 별로 분리하자면 이런 식. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/de77376d-79a9-4ec6-929d-0b33c66a2cbb/image.png" alt=""></p>
<p>기본 구조는 이렇고, <strong>물론 변경 가능하다</strong>. 
단, 구조를 변경 했을 경우 다음에 설정할 config 파일에서 <code>localePath</code> 와 <code>localeStructure</code> 를 설정해주어야 함. </p>
<h2 id="3-이제-설정-파일을-만들자">3. 이제 설정 파일을 만들자</h2>
<p>폴더를 만들었으니 이게 읽히도록 하자. 
프로젝트 루트에 <code>next-i18next.config.js</code> 파일을 만들고 다음과 같이 내용을 적는다. </p>
<pre><code>module.exports = {
  i18n: {
    defaultLocale: &#39;ko&#39;,
    locales: [&#39;ko&#39;, &#39;en&#39;],
  },
};</code></pre><p>그리고 위에서 기본 경로가 아닌 다른 경로에 언어 파일을 배치한 경우 아래처럼 추가 설정을 해주면 된다. </p>
<pre><code>const path = require(&#39;path&#39;);

module.exports = {
  i18n: {
    defaultLocale: &#39;ko&#39;,
    locales: [&#39;ko&#39;, &#39;en&#39;],
  },
  localePath: path.resolve(&#39;./my/custom/path&#39;), // 요렇게
};</code></pre><p>더 많은 옵션들이 있다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/763f6d6d-93b0-492e-ba7e-b02c72c3fdf4/image.png" alt=""></p>
<p><a href="https://github.com/isaachinman/next-i18next#options">repo에서 확인하기</a></p>
<h2 id="4-nextjs-가-다국어-설정을-사용하도록-하자">4. next.js 가 다국어 설정을 사용하도록 하자</h2>
<p><code>next.config.js</code> 파일을 열어서 i18n 을 사용하도록 설정한다. </p>
<pre><code>const { i18n } = require(&#39;./next-i18next.config&#39;);

module.exports = {
  i18n,
};</code></pre><p>여기서 잠깐! 🍒
만약 내 프로젝트가 webpack 설정을 쓰느라 <code>next.config.js</code>가 좀 복잡하다?</p>
<p>이렇게 하면 됩니다. (주석참조)</p>
<pre><code>/** @type {import(&#39;next&#39;).NextConfig} */
const path = require(&#39;path&#39;);

// 똑같이 import 해서 
const { i18n } = require(&#39;./next-i18next.config&#39;);

const nextConfig = {
  reactStrictMode: true,
  webpack(config) {
    config.resolve.alias = {
      ...config.resolve.alias,
      components: path.resolve(__dirname, &#39;components/&#39;),
      styled: path.resolve(__dirname, &#39;styled/&#39;),
      utils: path.resolve(__dirname, &#39;utils/&#39;),
    }
    return config;
  },
  i18n, // 요기에 넣어주면 끝! 
};

module.exports = nextConfig;</code></pre><h2 id="5-app-이-번역-기능을-사용하도록-하자">5. App 이 번역 기능을 사용하도록 하자</h2>
<p><code>pages/_app.tsx</code> 파일을 열어 다음과 같이 수정한다. </p>
<pre><code>import { appWithTranslation } from &#39;next-i18next&#39;;

const MyApp = ({ Component, pageProps }) =&gt; &lt;Component {...pageProps} /&gt;;

export default appWithTranslation(MyApp);</code></pre><p>사실상 가운데 과정은 프로젝트 별로 다를 수 있고, </p>
<p>🍋 중요한 것은 <code>export default</code> 로 내보내는 <code>MyApp</code> 이라는 root component 가 <code>appWithTranslation()</code> 으로 감싸져야 한다는 것이다. </p>
<blockquote>
<p>서버 사이드 렌더링은 사용하지 않았다. </p>
</blockquote>
<br>

<h2 id="6-이제-페이지에서-사용하자">6. 이제 페이지에서 사용하자</h2>
<p>사용법은 짱 쉽다. </p>
<ol>
<li>useTranslation 을 import한다.</li>
<li>Component 안에서 useTranslation() hook을 사용해서 <code>t</code> 라는 API 를 받는다. </li>
<li>사용한다. </li>
</ol>
<pre><code>import { useTranslation } from &#39;next-i18next&#39;;

export const Footer = () =&gt; {
  // json 파일 명. common 또는 footer 같은거
  const { t } = useTranslation(&#39;footer&#39;); 

  return (
    &lt;footer&gt;
      {/* json 파일 안에 있는 key 값 */}
      &lt;p&gt;{t(&#39;description&#39;)}&lt;/p&gt;
    &lt;/footer&gt;
  );
};</code></pre><p>여기 <code>useTransaltion(&#39;footer&#39;)</code> 의 <code>footer</code> 는 앞에서 만든 <code>json</code> 파일명이다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/69865d88-e306-492f-b5a2-08ab00979691/image.png" alt=""></p>
<p><code>t(&#39;description&#39;)</code> 의 <code>description</code> 은 json 파일 안에 있는 key값이다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/144f9284-e23b-463f-8ebf-ccb8f31d3b02/image.png" alt=""></p>
<h1 id="🛑-끝일-줄-알았는데-안-될-때">(🛑) 끝일 줄 알았는데 안 될 때</h1>
<p>각 페이지 코드에 getStaticProps 를 만들어주어야 한다. 
이 함수는 Next.js 내용이라 정확히 알지는 못하지만 빌드 할 때 실행되는 코드라고 한다. </p>
<p>props 의 기본 값을 전달하기 위해 페이지에 아래 코드를 넣어주자. </p>
<p><code>@/pages/index.tsx</code></p>
<pre><code>// ... 
// 위에 앱 코드가 막 있으면 그 아래에

export const getStaticProps = async ({ locale }) =&gt; ({
  props: {
    // common 은 위 locales/ko 아래에 만든 json 파일 명이다. 다른 파일을 사용한다면 바꿔주자.
    ...(await serverSideTranslations(locale, [&quot;common&quot;])),
  },
});
</code></pre><p>이렇게 해야 i18n 에 기본 언어 값이 적용되어 다국어 설정이 된다. 
그리고 이건 pages 내의 모든 페이지 <code>index.tsx</code> 파일에 들어가야 하는 듯..</p>
<p>아래 코드를 참조하세요. </p>
<pre><code>// REACT
import { NextPage } from &#39;next&#39;;
import { serverSideTranslations } from &quot;next-i18next/serverSideTranslations&quot;;

const MySample: NextPage = () =&gt; {
  return (
    &lt;div className=&quot;mysample&quot;&gt;
    &lt;/div&gt;
  )
};

export const getStaticProps = async ({ locale }) =&gt; ({
  props: {
    ...(await serverSideTranslations(locale, [&quot;common&quot;])),
  },
});

export default MySample;
</code></pre><h1 id="끝">끝</h1>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) Keycloakify 로 keycloak theme 개발하기 (3)]]></title>
            <link>https://velog.io/@cindy-choi/React-Keycloakify-%EB%A1%9C-keycloak-theme-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-3</link>
            <guid>https://velog.io/@cindy-choi/React-Keycloakify-%EB%A1%9C-keycloak-theme-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-3</guid>
            <pubDate>Wed, 16 Feb 2022 09:35:14 GMT</pubDate>
            <description><![CDATA[<h1 id="📋-오늘의-목표">📋 오늘의 목표</h1>
<p>✔ 로컬 서버에 keycloak 설치하기
✔ 로그인 화면에 신규 테마 적용하기</p>
<blockquote>
<p>이 시리즈에서 개발한 키클락 앱 코드는 깃헙에 올려두었다.
👉 <a href="https://github.com/cindy-choi/keycloak-sample-ts">https://github.com/cindy-choi/keycloak-sample-ts</a></p>
</blockquote>
<br>


<p>오늘은 이전 글에서 빌드한 새로운 테마를 키클락 서버에 적용한다. </p>
<p>이전 글: <a href="https://velog.io/@cindy-choi/React-Keycloakify-%EB%A1%9C-keycloak-theme-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-2">keycloakify 로 새로운 로그인 화면 빌드하기</a></p>
<br>


<h1 id="로컬-서버에-keycloak-설치하기">로컬 서버에 keycloak 설치하기</h1>
<p>keycloak 설치는 전래 쉬움.
가능하면 keycloakify 가 테스트 완료한 버전을 확인하는 것이 좋다. </p>
<p>가장 최신 버전은 아직 테스트가 안되었다. 직접 테스터가 되고 싶은 경우에만 keycloak 17.0을 설치하자...</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/937e279b-9de4-43cc-8411-b76a4af447d7/image.png" alt=""></p>
<h2 id="1-keycloak-다운로드">1. keycloak 다운로드</h2>
<p>키클락 버전 별 다운로드 링크 : <a href="https://www.keycloak.org/downloads-archive.html">https://www.keycloak.org/downloads-archive.html</a></p>
<p>나는 회사에서 15.0.2를 사용해서 그걸로 했고,개인적으로 16 버전에서도 테스트를 해봤다. 
다 잘되니까 아무거나~</p>
<h2 id="2-압축-풀기">2. 압축 풀기</h2>
<p>원하는 경로에 다운받은 zip 파일을 두고 압축을 풀면 된다.</p>
<pre><code> unzip keycloak-15.0.2.zip</code></pre><h2 id="3-실행">3. 실행</h2>
<p>압축 푼 폴더의 bin 경로에 있는 <code>standalone.sh</code>를 실행하면 끝!</p>
<pre><code>sh ./bin/standalone.sh</code></pre><h2 id="4-키클락-admin-페이지-들어가기">4. 키클락 admin 페이지 들어가기</h2>
<p>키클락 서버를 띄우면 admin 페이지에 접근할 수 있다. </p>
<p>보통 8080 포트로 뜨는데 그게 아니면 로그 확인해서 무슨 포트로 떴는지 확인하면 된다. 
근데 로그가 무수히 많으므로.... 눈을 크게 떠야 함</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/1a710a4c-1f8f-4a25-9aff-d49d550691ce/image.png" alt=""></p>
<p>브라우저로 <code>localhost:8080</code> 에 접속한다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/220021e5-e9ef-454a-8722-c41eeb49de84/image.png" alt=""></p>
<p>최초 설치 시에는 admin 용 username 과 비밀번호를 결정해주어야 한다. 
적은 다음 꼭 외우세요... 저는 까먹어서 다시 설치함</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/474083a0-f910-4ca3-ad92-45907529fadd/image.png" alt=""></p>
<p>사용자가 잘 만들어졌다면
바로 아래에 있는 Administration Console 눌러서 이동한다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/32d5bed3-a1cc-4101-a0bc-b7f44deca891/image.png" alt=""></p>
<p>그럼 또 이렇게 로그인 화면이 뜨는데, 방금 만들었던 admin 계정 정보 입력해서 들어가면 된다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/3b4f78d0-5d05-4d39-a7d7-dfdf610de8cb/image.png" alt=""></p>
<p>기본적으로 <code>master</code> 라는 이름의 렐름으로 접속되고, 키클락의 모든 설정이 가능하다. </p>
<h2 id="5-sample-렐름-만들기">5. sample 렐름 만들기</h2>
<p><img src="https://images.velog.io/images/cindy-choi/post/30a515fe-0b52-4d48-b61a-7de34f731e3a/image.png" alt=""></p>
<p>좌측 위에 있는 메뉴에서 Master 위로 포인터를 얹으면 <code>Add realm</code> 메뉴가 나타난다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/94de17a9-bcba-4c5b-accb-a4a2890c13c8/image.png" alt=""></p>
<p>sample 이라는 이름의 렐름을 생성해준다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/4278ecf6-2ad3-4beb-9113-f516c2593d72/image.png" alt=""></p>
<p>생성된 sample 렐름을 선택한 상황에서 
Clients - Create 메뉴를 눌러 우리 UI 가 접근할 클라이언트를 생성하자. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/4e68d178-fcb7-499e-8209-81d498d3b24b/image.png" alt=""></p>
<p>우리 서버 개발자가 처음에 이름을 <code>public-client</code> 라고 해뒀길래 나도 따라함...
클라이언트 ID 만 입력하고 save.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/48ecd588-843c-4c3b-ac0c-9de942751749/image.png" alt=""></p>
<p>생성한 public-client 를 눌러서 설정 화면으로 진입한다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/fd8725b7-42b5-4019-95e3-8d761385c82a/image.png" alt=""></p>
<p>다른건 됐고 위의 URL들을 설정을 해주어야 한다. 
로컬에서 띄운 keycloakify-sample 프로젝트의 URL 을 입력하자. 나는 3000.</p>
<p>(🍊) Valid Redirect URLs 의 path 쪽에 * 을 허용해주면 서비스 UI 의 어느 path 에서든 로그인을 요청하고 해당 페이지로 돌아갈 수 있다. </p>
<p>보안상 * 로 해주기 싫으면 필요한 path 들을 각각 개별 등록하면 된다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/7de1c823-3d83-4303-a184-e60fabb2c4b0/image.png" alt=""></p>
<p>이런식으로. 
만약 여기서 리디렉션 URL 이 허용되지 않는 범위에서 로그인을 시도하면 에러가 발생하면서 로그인 페이지에 진입할 수 없으니(404 난다) 유의. </p>
<br>
<br>

<p>여기까지가 키클락 로컬 세팅이다. </p>
<h1 id="진짜-마지막-테마-적용하기">진짜 마지막. 테마 적용하기</h1>
<p><img src="https://images.velog.io/images/cindy-choi/post/3b4f78d0-5d05-4d39-a7d7-dfdf610de8cb/image.png" alt=""></p>
<p>Realm Settings &gt; Themes 메뉴로 진입한다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/a4a29772-756f-41ce-a2cb-22d323a1fe48/image.png" alt=""></p>
<p>login, account, admin console 등 여러가지 화면에 각기 다른 테마를 적용할 수 있음. 
로그인, 회원가입, 비밀번호 재설정... 등 대부분의 화면은 login theme 로 설정 가능하다. </p>
<p>우선 메뉴를 열어보면 <code>base</code> 와 <code>keycloak</code> 딸랑 두개 있는데 이제부터 지난 글에서 만든 테마를 적용해보자. </p>
<h2 id="1-테마-옮기기">1. 테마 옮기기</h2>
<p>지난 글에서 <code>npm run keycloak</code> 명령어를 때려서 만든 <code>build_keycloak</code> 폴더를 기억하시는지. 
이제 만들어둔 <code>build_keycloak/src/main/resources/theme/keycloakify-sample</code> 을 복사해서 키클락이 설치된 경로의 themes/login 아래에 붙여넣자. </p>
<pre><code> cp -rfp ./src/main/resources/theme/keycloakify-sample ~/dev/keycloak-15.0.2/themes/.</code></pre><p>내 경우 명령어는 위와 같지만 상황에따라 다르므로 잘.. 복사합니다. </p>
<h2 id="2-테마-선택하기">2. 테마 선택하기</h2>
<p>별도로 키클락을 재기동할 필요 없이! 
바로 아까의 테마 메뉴에서 방금 복사한 테마를 확인할 수 있다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/6d6aeb49-cd81-4cd1-8069-a2d213c09bd6/image.png" alt=""></p>
<p>클릭해서 선택하고 적용해주면, 이제 이 키클락 렐름을 통해 로그인 하러 오는 사람들은 새로운 테마의 화면을 볼 수 있게 된다. </p>
<hr>
<br>
<br>

<p>키클락 테마 세팅까지 해주었다. 
여기까지 하면 끝인 줄 알겠지만, 이제 </p>
<p>1) 서비스 UI 에서 private 페이지와 public 페이지를 구분해주고 
2) 로그인이 필요한 페이지에 인증 없이 접근하면 로그인 화면으로 리디렉션
.. 처리를 해주어야 한다. </p>
<p>이것도 만만찮게 길기 때문에 다음 글에 계속</p>
<br>

<p>끝</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) Keycloakify 로 keycloak theme 개발하기 (2)]]></title>
            <link>https://velog.io/@cindy-choi/React-Keycloakify-%EB%A1%9C-keycloak-theme-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-2</link>
            <guid>https://velog.io/@cindy-choi/React-Keycloakify-%EB%A1%9C-keycloak-theme-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-2</guid>
            <pubDate>Mon, 14 Feb 2022 09:10:27 GMT</pubDate>
            <description><![CDATA[<h1 id="📋-오늘의-목표">📋 오늘의 목표</h1>
<p>✔ keycloakify 설치하기
✔ 로그인 화면 커스텀 하기
✔ 회원 가입 화면 커스텀 하기</p>
<p>이 시리즈에서 개발한 키클락 앱 코드는 깃헙에 올려두었다.
👉 <a href="https://github.com/cindy-choi/keycloak-sample-ts">https://github.com/cindy-choi/keycloak-sample-ts</a></p>
<p>완성된 코드 말고 처음부터 따라하고 싶은 사람은 아래에 나오는 보일러플레이트를 사용해도 된다. </p>
<h1 id="프로젝트-구조-설명">프로젝트 구조 설명</h1>
<p>다음은 keycloakify 의 메인 페이지에서 확인 할 수 있는 gif 이다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/5415a580-160f-4dc2-91f8-4a838af63f68/134997335-a28b4a57-0884-47ec-9341-a0e49f835c4d.gif" alt=""></p>
<p>gif를 보면 Onyxia 라는 이름의 검은색 테마의 페이지들이 있고,
쌩뚱맞은 파란색 테마의 로그인 페이지가 보이는데 </p>
<p>검은 페이지들이 일반 서비스 UI 이고, 테마가 맞지 않은 파란 페이지들이 Keycloak UI이다.</p>
<p>잘 보면 keycloak UI 에 진입했을 때 URL이 기존과는 다르게 몹시 긴 것을 볼 수 있다. 완전히 다른 두 개의 UI를 개발해야 하는 것이다. </p>
<br>

<p>이 시리즈에서 진행하는 프로젝트는 하나의 repository에 서비스 UI 와 Keycloak UI 코드를 한 번에 관리&amp;빌드 할 수 있도록 구성했다.</p>
<h1 id="시작하자">시작하자</h1>
<p><img src="https://images.velog.io/images/cindy-choi/post/5c6365f8-9249-4314-acf9-6951b18c44d9/image.png" alt=""></p>
<p>시작하기 전에, 개발 하기 전에 세팅해놓는 것을 좋아해서 미리 만들어둔  boilerplate 를 사용했다.</p>
<p>샘플 코드를 만드는 사람이라면 이 보일러플레이트를 클론 받아서 같이 시작하면 되겠다. </p>
<p>👉 <a href="https://github.com/cindy-choi/cindy-boilerplate-ts">https://github.com/cindy-choi/cindy-boilerplate-ts</a></p>
<p><code>git clone https://github.com/cindy-choi/cindy-boilerplate-ts.git</code></p>
<br>


<h2 id="1-keycloakify-설치하기">1. keycloakify 설치하기</h2>
<p>다음의 명령어로 keycloakify 와 친구들을 설치한다.</p>
<p><code>npm install --save-dev keycloakify @emotion/react tss-react powerhooks</code></p>
<h2 id="2-keyclok-context-사용하기">2. keyclok context 사용하기</h2>
<p>keycloak 관련 전역 변수 등을 관리하기 위해 매니저 파일을 만든다.
파일 명과 경로는 마음대로..</p>
<p><code>@/utils/keycloakManager.ts</code></p>
<pre><code>import { getKcContext } from &#39;keycloakify&#39;;

export const { kcContext } = getKcContext&lt;{
  pageId: &#39;login.ftl&#39;,
}&gt;({ 
  mockData: [
    {
      pageId: &#39;login.ftl&#39;,
    },
  ],
});

const keycloakManager = {
  kcContext,
};

export type KcContextType = NonNullable&lt;typeof kcContext&gt;;
export default keycloakManager;</code></pre><p>keycloakify 로 UI를 빌드할 때 kcContext 값을 사용해서</p>
<ul>
<li>어떤 페이지를 빌드 할 건지</li>
<li>빌드 디버깅 옵션을 사용할 것인지</li>
<li>빌드 시 mockData를 적용할 건지, 적용한다면 어떤 값인지</li>
</ul>
<p>등등을 결정한다. </p>
<h2 id="3-로그인-페이지-개발하기">3. 로그인 페이지 개발하기</h2>
<p><img src="https://images.velog.io/images/cindy-choi/post/8a86ed5c-03cd-4cbe-9f71-f8c974f23745/image.png" alt=""></p>
<p>이렇게 생긴 로그인 페이지를... 만들어보자. 
프론트엔드 개발자 타이틀 달고 퀄리티가 말이 아니다.
아무튼...</p>
<br>

<p>keycloak 관련 페이지는 keycloak 경로에 다 묶어두었다.
서비스 UI 랑 같이 하나의 repo 에서 관리하려면 이 방법이 편하다. </p>
<p><code>@/pages/keycloak/Login.tsx</code></p>
<pre><code>import React, { useState, useRef, memo, useEffect } from &#39;react&#39;;
import { useTranslation } from &#39;react-i18next&#39;;
import styled from &#39;styled-components&#39;;
import { Button, TextField } from &#39;@mui/material&#39;;

import type { KcProps } from &#39;keycloakify/lib/components/KcProps&#39;;
import type { KcContextType } from &#39;@/utils/keycloakManager&#39;;

type KcContext_Login = Extract&lt;KcContextType, { pageId: &#39;login.ftl&#39; }&gt;;

const StyledLogin = styled.div`
  min-width: 100vw;
  min-height: 100vh;
  background-image: url(${bg});
  background-size: cover;
  background-repeat: no-repeat;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
`;

const LoginForm = styled.form`
  width: 25rem;
  height: 15rem;
  background-color: white;
  border-radius: 5px;
  box-shadow: 2px 2px 8px 0px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
`;

const LoginInput = styled(TextField)`
  width: 20rem;
  margin-bottom: 8px !important;
`;

const LoginButton = styled(Button)`
  width: 20rem;
`;

export const Login = memo(({ kcContext, ...props }: { kcContext: KcContext_Login } &amp; KcProps) =&gt; {
    const { t } = useTranslation();
    const form = useRef&lt;HTMLFormElement&gt;(null);
    const { social, url, message, realm, } = kcContext;
    const isSessionOut = message?.summary.includes(&#39;attempt timed out&#39;) || message?.summary.includes(&#39;Timeout&#39;);

    console.log(kcContext);

    const handleSubmit = () =&gt; {
      form?.current?.submit();
    };

    return (
      &lt;StyledLogin&gt;
        &lt;LoginForm ref={form} method=&quot;post&quot; action={url.loginAction}&gt;
          &lt;LoginInput
            id=&quot;username&quot;
            name=&quot;username&quot;
            size=&quot;small&quot;
            label={t(&#39;id&#39;)}
          /&gt;
          &lt;LoginInput
            label={t(&#39;password&#39;)}
            id=&quot;password&quot;
            name=&quot;password&quot;
            type=&quot;password&quot;
            size=&quot;small&quot;
          /&gt;
          &lt;LoginButton variant=&quot;contained&quot; onClick={() =&gt; handleSubmit()}&gt;{ t(&#39;login&#39;) }&lt;/LoginButton&gt;
        &lt;/LoginForm&gt;
      &lt;/StyledLogin&gt;
    );
  },
);

export default Login;</code></pre><p>벨로그에서는 line number 기능이 없군요</p>
<p>하여간 코드를 보면 알 수 있듯, 기존 React로 개발하던 방식 그대로 로그인 페이지를 만들 수 있다. no more ftl! </p>
<p>keycloak 서버가 UI를 렌더링 할 때 전달하는 변수들은 모두 kcContext 객체 안에 들어있다. </p>
<p>예를 들어 위 코드에서 다국어 지원을 i18next 로 하고 있지만, 서버에서 atrribute 로 전달하는 것을 사용해도 무방할 것이다. 난 안해봄. 잘되면 알려주세요.</p>
<h2 id="4-로그인-페이지-사용하기">4. 로그인 페이지 사용하기</h2>
<p>로그인 페이지를 만들었으면, 이제 keycloakify 가 이걸 빌드하도록 설정 해주어야한다. </p>
<p>프로젝트의 root 디렉토리에 KeycloakApp.tsx 파일을 만들자.</p>
<br>

<p><code>@/KeycloakApp.tsx</code></p>
<pre><code>import { memo } from &#39;react&#39;;
import { defaultKcProps } from &#39;keycloakify&#39;;
import { useTranslation } from &#39;react-i18next&#39;;
import type { KcContextType } from &#39;@/utils/keycloakManager&#39;;

import Login from &#39;@/pages/keycloak/Login&#39;;
// import Error404 from &#39;@/pages/common/Error404&#39;;

import &#39;./KeycloakApp.scss&#39;;

export const KeycloakApp = memo(({ kcContext }: { kcContext: KcContextType; }) =&gt; {
  const { t } = useTranslation();

  console.log(kcContext);

  switch (kcContext.pageId) {
    case &#39;login.ftl&#39;:
      return &lt;Login {...{ kcContext, ...defaultKcProps }} /&gt;;

    default:
      return undefined;
  }
});

export default KeycloakApp;</code></pre><p>switch를 보면 서버에서 넘겨주는 kcContext의 pageId 에 따라 어떤 페이지를 렌더링 할지를 결정하는 것을 알 수 있다. </p>
<p>추후 추가로 회원 가입 페이지나, 404 에러 페이지를 개발하게 될 경우 swtich case 에 추가해주면 된다.</p>
<p>그리고 이 KeycloakApp.tsx 를 사용하도록 설정해주자. </p>
<p><code>@/index.tsx</code></p>
<pre><code>import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom&#39;;
import &#39;./index.css&#39;;
import reportWebVitals from &#39;./reportWebVitals&#39;;

// service ui
import App from &#39;./App&#39;;
// keycloak ui
import KeycloakApp from &#39;@/KeycloakApp&#39;;
import { kcContext } from &#39;@/utils/keycloakManager&#39;;


/**
 * keycloak 서버에서 제공할 때에만 kcContext 값이 활성화되며, 기본적으로는 App을 렌더링합니다.
 */
ReactDOM.render(
  &lt;React.StrictMode&gt;
    {
      // kcContext가 존재하면 키클락 App 을 렌더링하고, 그렇지 않으면 App을 렌더링합니다.
      kcContext !== undefined ? (
        &lt;KeycloakApp kcContext={kcContext} /&gt;
      ) : (
        &lt;App /&gt;
      )
    }
  &lt;/React.StrictMode&gt;,
  document.getElementById(&#39;root&#39;),
);

reportWebVitals();</code></pre><p>App.tsx 와 KeycloakApp 을 상황에 따라 다르게 사용하는 것을 볼 수 있다. </p>
<p>구체적으로 말하자면 이 다음에 나오는 <code>npm run keycloak</code> 명령어를 때리면 keycloakify 로 우리가 개발한 웹 페이지를 빌드하는데, 이 때 kcContext가 전달된다고 보면 된다. </p>
<h1 id="개발된-페이지를-keycloak-theme로-내보내기">개발된 페이지를 keycloak theme로 내보내기</h1>
<p>그냥 만들면 짠하고 keycloak UI 가 spa 로 동작하면 얼마나 좋을까.
이제 실제로 keycloakify 를 사용해서 키클락 theme 파일을 만들어보자. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/2812b59a-2a5f-482d-a3a8-d775f624660c/image.png" alt="">
<br> </p>
<p>우선 빌드 명령어를 추가해주어야 한다. </p>
<p><code>package.json</code></p>
<pre><code>  &quot;scripts&quot;: {
    &quot;keycloak&quot;: &quot;craco build &amp;&amp; build-keycloak-theme&quot;,
    &quot;start&quot;: &quot;craco start&quot;,
    &quot;build&quot;: &quot;craco build&quot;,
    &quot;test&quot;: &quot;craco test&quot;,
    &quot;eject&quot;: &quot;react-scripts eject&quot;
  },</code></pre><p>맨 위에 있는 keycloak 을 추가해주면 된다. 
나는 craco 를 쓰는데 npm을 사용중이라면 다음과 같이 변경하면 된다. </p>
<p><code>&quot;keycloak&quot;: &quot;npm build &amp;&amp; build-keycloak-theme&quot;</code></p>
<p>그리고 명령어를 때려보자. 
우선 <code>npm build</code> 때린 것 처럼 빌드를 먼저 하고 진행하기 때문에 제법 오래 걸린다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/3e7659cd-e82d-4736-bd79-f2519c342a6d/image.png" alt=""></p>
<p>이렇게 긴~ 과정을 끝내면 </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/ec4da031-c882-4e4a-88cc-f1da51379344/image.png" alt=""></p>
<p>프로젝트의 root 경로에 build_keycloak 이라는 경로가 생성되고 
그 안에 keycloak 적용에 필요한 것들이 모두 포함되어 나온다. </p>
<p>그 중 우리에게 필요한 건 theme 폴더!
<code>build_keycloak/src/main/resources/theme/</code> 경로 안에 있는 <code>keycloakify-sampe</code> 경로를 확인하자. </p>
<p>참고로 이 폴더의 이름은 package.json 에 명시된 프로젝트의 name 을 따라간다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/44f9ac13-aa18-434f-9e91-07fcc6bafc29/image.png" alt=""></p>
<p>이 <code>keycloakify-sample</code> 폴더를 키클락이 설치된 곳에 복사해두면 끝!
다른 jar 파일들이 많이 생기는데, 내 경우 혹시 몰라서 jar 파일만 서버에 적용 해주었고 나머지는 손대지 않았지만 잘 동작했다. </p>
<p>여기까지 keycloakify 로 UI 빌드까지 끝냈다. 
이제 로컬에 키클락을 띄우고 로그인이 필요한 페이지에서 리디렉션을 걸어서 실제 UI랑 연동을.. 내일 해보자. </p>
<br>

<p>끝</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) Keycloakify 로 keycloak theme 개발하기 (1)]]></title>
            <link>https://velog.io/@cindy-choi/React-Keycloakify-%EB%A1%9C-keycloak-theme-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@cindy-choi/React-Keycloakify-%EB%A1%9C-keycloak-theme-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Mon, 14 Feb 2022 04:30:52 GMT</pubDate>
            <description><![CDATA[<h1 id="📋-오늘의-목표">📋 오늘의 목표</h1>
<p>✔ 키클락이 뭔지 알아보기
✔ 키클락 theme 기능은 어떻게 동작하나
✔ keycloakify 소개</p>
<p>다음글: <a href="https://velog.io/@cindy-choi/React-Keycloakify-%EB%A1%9C-keycloak-theme-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-2">keycloakify 로 react 로 개발한 로그인 페이지 빌드하기!</a></p>
<h1 id="keycloak-은-무엇인가">Keycloak 은 무엇인가</h1>
<p><img src="https://images.velog.io/images/cindy-choi/post/9096f4ce-594a-484f-a9ea-6303005834df/image.png" alt=""></p>
<blockquote>
<p>Keycloak은 오픈소스 인증/접근 관리 솔루션이다. </p>
</blockquote>
<p>로그인 관련 기능을 직접 구현하는 대신 Keycloak을 이용하면 보다 간단히 서비스의 인증 기능을 구축할 수 있다. 쿠버네티스 환경에 서비스를 제공하는 입장에서 <strong>realm 별</strong>로 인증/인가 기능을 대신 처리해주므로 편리하여 여기저기 쓰이는 추세인듯.</p>
<br>


<p>다만 UI 개발자 입장에서는 Keycloak 이 그다지 반가운 선택은 아니었다.</p>
<p>키클락 관련 UI 가 모두 <strong>서버 렌더링 방식</strong>으로 동작하는데다 *<em>화면을 직접 수정 개발할 수 없었기 때문에... *</em> </p>
<p>옛날 바닐라 시절 문법을 끌어내는 수 밖에 없었다. 그리고 난 정말 그것을 하고 싶지 않았음... </p>
<p>바닐라를 쓰지 않고 기존 서비스 화면과 따로 노는 키클락 기본 테마를 어찌저찌 하는 방법은 대체 무엇이란 말인가.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/326e7203-6580-4b04-8cb5-9873e298b41c/image.png" alt=""></p>
<br>
<br>

<h2 id="keycloak-의-theme-기능-이해하기">Keycloak 의 theme 기능 이해하기</h2>
<p>키클락 공식 문서를 보면 theme 기능이라는 항목이 있다. </p>
<p>앞서 말했듯 서버 렌더링 방식으로 동작하는 UI를 내가 직접 수정할 수는 없기 때문에 Keycloak 은 ftl이라고 하는 프리마커를 사용해서 스타일을 조절하도록 해두었다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/c19a115b-0c02-40e4-bda6-07dabf5d322e/image.png" alt=""></p>
<p><em>목차에서 조차 &#39;Server Developer&#39; 챕터에 theme 기능이 들어있는 것을 보면 전적으로 FE 개발자를 위한 솔루션은 아닌 것같지...</em></p>
<br>
<br>

<p>아무튼 Keycloak 의 공식 문서 중 theme 에 관련된 내용을 살펴보면, Keycloak 이 제공하는 화면의 구성은 동일하되 css 외 리소스들로 화면의 스타일을 변경할 수 있다고 한다. </p>
<p>ftl + resources = UI 인 것이다. 그림으로 표현하자면 아래와 같다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/5f5ea0b4-10ee-477b-a158-7b0b71b5112c/image.png" alt=""></p>
<p>여기서 단순히 css를 수정하는 것만으로 요구사항을 만족할 수 있다면 그냥 이렇게 진행하는 것도 괜찮다.
변수명을 변경하거나 추가/삭제 하는 것도 어지간하면 키클락에서 제공해준다.</p>
<p>다만 나는 다음의 상황을 마주쳤고, 당신도 그럴 수 있다...</p>
<p><br><br></p>
<h3 id="문제상황">문제상황</h3>
<p><img src="https://images.velog.io/images/cindy-choi/post/82c04183-5282-45bb-8df1-ee66cbef8ce3/image.png" alt=""></p>
<p>위는 키클락에서 제공하는 회원가입 기본 화면이다.</p>
<ul>
<li>입력 : first name, last name, email, password, confirm password </li>
<li>액션: 로그인 화면으로 돌아가기, 회원가입을 완료하기</li>
</ul>
<p>입력 5개와 액션 2개가 전부로 아주 조촐하다.</p>
<br>

<p>하지만 내가 받은 기획서에는
<img src="https://images.velog.io/images/cindy-choi/post/edd9094b-155d-4d7f-a457-7bad6f365a79/image.png" alt=""></p>
<p>무려 약관 동의에, 인증 메일 주고 받는 기능이 들어가야한다. </p>
<p>앞서 말했다시피 Keycloak 의 theme 기능에 의존하여 UI를 직접 수정하려면 ftl을 포함한 바닐라 방식을 사용하지 않을 수가 없는데.... </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/870b22d0-8f67-48fd-a1d3-9cd5ac0dce65/image.png" alt=""></p>
<p>정말 하기 싫었다. </p>
<br>
<br>

<h1 id="방법은-있어">방법은 있어</h1>
<p>선택지는 두 개다. </p>
<p>1) 기획자를 찾아가 무릎을 꿇거나.. 2) 방법을 찾는다. </p>
<p>눈물의 리서치를 하다가 구세주같은 툴을 발견했다. 바로 keycloakify ! </p>
<p><a href="https://github.com/InseeFrLab/keycloakify">구세주에게 바로가기</a></p>
<p>keycloakify 는 Keycloak theme 때문에 골치가 아픈 먹이사슬 최하위 프론트엔드 개발자를 위한 빌드 툴이다. 할 수만 있다면 star를 열개 쯤 때렸을텐데... </p>
<br>

<p>서툰 ftl과 css를 직접 사용할 필요 없이, 우리가 react.js 나 Vue.js 를 사용하던 방식 그대로 앱을 개발한 다음 keycloakify 로 빌드를 해주면 Keycloak theme 에 사용할 수 있는 ftl 과 resources 를 만들어준다. 그럼 그걸 Keycloak 서버의 theme 폴더에 넣으면 그걸로 끝! </p>
<h4 id="사용방법">사용방법</h4>
<p>1) CRA 프로젝트에 keycloakify 를 설치한다.
2) Reack.js 로 필요한 Keycloak 화면을 개발한다.
3) 개발 완료한 테마를 빌드한다.
4) 키클락에 적용한다. </p>
<p>초 쉽죠? </p>
<p>간단해보이는데 키클락 UI 와 관련한 한글 문서는 거의 없어서 좀 헤멨다. 그래서 간단히 정리를 해두려고 함.</p>
<p>이 글을 서비스에서 Keycloak 을 사용하게 된 힘 없는 프론트엔드 개발자분들께 바칩니다. 그런데 곧 바이올린 연습하러 가야해서요. 다음 글에 계속됩니다. </p>
<p>안녕 ftl🥺</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 18 (RC in now)]]></title>
            <link>https://velog.io/@cindy-choi/React-18-RC-in-now</link>
            <guid>https://velog.io/@cindy-choi/React-18-RC-in-now</guid>
            <pubDate>Wed, 09 Feb 2022 09:19:57 GMT</pubDate>
            <description><![CDATA[<p>와 벨로그 오랜만,,</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/b7eee01d-fa21-4257-8b02-44bc19022bb8/image.png" alt=""></p>
<h1 id="개요">개요</h1>
<p>딱히 할 것도 없는 차에 React 18 버전 업데이트 소식을 뒤늦게 접하였고, 몇 가지 흥미로운 기능들이 보여 공식 discussion의 몇가지 내용을 번역/정리합니다. </p>
<p><a href="https://github.com/reactwg/react-18/discussions/4">링크</a>의 내용을 참조했습니다.</p>
<br>

<p>아직 18 버전은 Alpha, Beta 를 거쳐 현재 RC 상태. 따라서 아래에서 소개하는 기능은 최종 릴리즈 당시 바뀔 수 있음을 알립니다. </p>
<p>많은 블로그 포스트에서 18버전을 아우르는 키워드는 동시성이라고들 해요.</p>
<p>아래에서 다루겠지만 렌더링 순서에 우선순위를 부여(startTransition)한다던가, SSR (Server-Side Rendering) 구조를 개선했다던가 하는 내용이 주를 이룹니다. 싱글 스레드 JS로 개발하는 웹 어플리케이션에 동시성을 부여함으로써, 개발 방식(workflow)과 성능을 개선한다는 것이죠. 
<br></p>
<h4 id="미리-써볼-수-있나요">미리 써볼 수 있나요?</h4>
<p>다음의 방식으로 RC 버전을 설치해 사용할 수 있습니다. </p>
<p><code>npm install next@latest react@rc react-dom@rc</code>
<br></p>
<h4 id="추가변경-되었어요">추가(변경) 되었어요</h4>
<ul>
<li><strong>(out-of-the-box) Automatic batching</strong></li>
<li>(out-of-the-box) SSR support for Suspense</li>
<li>(out-of-the-box) Fixes for Suspense behavior quirks (제외)</li>
<li><strong>startTransition</strong></li>
<li><strong>useDeferredValue</strong></li>
<li><code>&lt;SuspenseList&gt;</code></li>
<li><del>Streaming SSR with selective hydration</del></li>
</ul>
<br>
현재 React 18 Discussion 에서 명시한 변경사항들입니다. 가장 메인은 역시 Automatic batching 이 아닐까..

<p>어쨌든 본 문서에서는 </p>
<ul>
<li>Automatic batching</li>
<li>startTransition</li>
<li>useDeferredValue </li>
</ul>
<p>이 세 가지에 대해 알아보기로 합니다. </p>
<p>SSR (Server-Side-Rendering) 관련 내용은 기회가 된다면 다음에...
<br>
<br></p>
<hr>
<h1 id="1-automatic-batching">1. Automatic batching</h1>
<h3 id="batching-이-뭔데요">Batching 이 뭔데요?</h3>
<blockquote>
<p>Batching is when React groups multiple state updates into a single re-render for better performance.</p>
</blockquote>
<p>다시 말해 여러 개의 state 업데이트 상황을 하나의 re-render로 묶어 더 빨리 처리하자! 하는 것입니다. </p>
<br>

<p>useState 를 사용하신 분들이라면 이 구문이 비동기로 동작한다는 사실을 이미 알고 계실거에요.</p>
<p>한 번의 클릭 이벤트로 2개의 state 가 변경된다고 칩시다. count 와 flag 가 변경되면 <code>&lt;h1&gt;</code> 태그가 변경될테고, 그럼 화면을 다시 렌더링 해야겠죠. <strong>React 는 항상 이 변경 사항(update)들을 하나의 리랜더링(re-render)에 모아서 처리합니다.</strong></p>
 <br>

<p>아래의 코드를 보세요.</p>
<pre><code>function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setCount(c =&gt; c + 1); // 아직 렌더링 안했어요!
    setFlag(f =&gt; !f); // 아직 렌더링 안해요!!
    // React는 이 끝에서 리-렌더링을 한번만 합니다. (that&#39;s batching!)
  }

  return (
    &lt;div&gt;
      &lt;button onClick={handleClick}&gt;Next&lt;/button&gt;
      &lt;h1 style={{ color: flag ? &quot;blue&quot; : &quot;black&quot; }}&gt;{count}&lt;/h1&gt;
    &lt;/div&gt;
  );
}</code></pre><p>그러므로 Batching 은 성능에 어마어마한 영향을 줍니다. 그렇지 않나요?</p>
<p>당장 위의 20줄 짜리 코드에서도 2번 렌더링 할 것을 단 한 번으로 줄여주었으니까요.</p>
 <br>

<p>🤚 또 있습니다. 
batching 을 하지 않으면 count 가 바뀌었을 때 <code>&lt;h1&gt;</code>을 새로 렌더링 될 거고, 동 순간 개발자가 의도한 대로 color는 변경되지 않을 겁니다.</p>
<p>이건 버그에요. Batching 은 이런 <strong>버그를 예방하는 효과</strong>도 있습니다. </p>
<br>
<br>

<p><img src="https://images.velog.io/images/cindy-choi/post/d44ac9de-fbe5-4684-a475-9b1f2942e96d/image.png" alt=""></p>
<h3 id="그런데-말입니다-🤔">그런데 말입니다. 🤔</h3>
 <br>
그동안 React 가 update를 모아서 한 번에 처리하는 Batching에는 일관성이 없었습니다. 

<p>예를 들어 데이터를 fetch한 다음 state를 업데이트 해야하는 상황에서는 Batching 이 적용되지 않아요. 두 번의 독립적인 update 가 실행되는 거죠.</p>
<br>
<br>

<p>왜 그럴까요?</p>
<p>👉 보통 리액트는 브라우저의 이벤트가 일어나는 동안에만 batching을 하기 때문입니다.</p>
 <br>

<p>위의 예시에서는.... 그냥 한 번 더 코드로 다시 보겠습니다. </p>
<pre><code>function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() =&gt; {
      // 그동안의 React 는 여기서 batch 하지 않아요.
      // 이유는.. 위에 적어뒀습니다.
      setCount(c =&gt; c + 1); // re-render 를 유발하는 구문
      setFlag(f =&gt; !f); // 역시 re-render를 유발하는 구문
    });
  }

  return (
    &lt;div&gt;
      &lt;button onClick={handleClick}&gt;Next&lt;/button&gt;
      &lt;h1 style={{ color: flag ? &quot;blue&quot; : &quot;black&quot; }}&gt;{count}&lt;/h1&gt;
    &lt;/div&gt;
  );
}</code></pre><p>요약하자면, 그동안의 React (17) 는 이벤트 핸들러의 밖에서는 Batch 처리를 하지 않았습니다. 이벤트를 핸들링하는 동안에만 일괄 업데이트(batching)를 했어요.</p>
<p>따라서 기본적으로 Promise 구문, setTimeout, native event handlers, 또는 등등의 모든 이벤트 내에서 발생하는 변경사항(update)는 Batch 처리 되지 않았습니다.</p>
<br>

<p>지금까지는요. 
😉</p>
<br>
<br>

<h2 id="진짜-진짜-automatic-batching">진짜 진짜 Automatic batching</h2>
<p>이제 18이 나옵니다! </p>
<p>React 18의 createRoot 를 사용하면, 모든 변경사항(update)은 자동으로 batch 처리 될 거에요! 변경사항이 어디에서 수행되든지 상관 없어요. </p>
<p>즉, promise건 timeout 이건 native 이벤트이건 모든 이벤트가 React 자체 이벤트와 같은 방식으로 변경사항을 일괄 처리(batch)한다는 거죠. 일관성이 생긴 겁니다.</p>
<pre><code>function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() =&gt; {
      // 이제 여기서 batching 이 일어납니다.
      setCount(c =&gt; c + 1);
      setFlag(f =&gt; !f);
      // 이 끝에서 배치 처리 되어 한 번에 업데이트 될거에요!
    });
  }

  return (
    &lt;div&gt;
      &lt;button onClick={handleClick}&gt;Next&lt;/button&gt;
      &lt;h1 style={{ color: flag ? &quot;blue&quot; : &quot;black&quot; }}&gt;{count}&lt;/h1&gt;
    &lt;/div&gt;
  );
}</code></pre><p>이제 React 는 자동으로 모든 변경사항을 Batch 처리 할거에요. 변경(update)가 어디서 일어나든 상관 없이 말이죠.</p>
<p>그래서!</p>
<pre><code>function handleClick() {
  setCount(c =&gt; c + 1);
  setFlag(f =&gt; !f);
  // React will only re-render once at the end (that&#39;s batching!)
}</code></pre><p>이것도   </p>
<pre><code>setTimeout(() =&gt; {
  setCount(c =&gt; c + 1);
  setFlag(f =&gt; !f);
  // React will only re-render once at the end (that&#39;s batching!)
}, 1000);</code></pre><p>이것도</p>
<pre><code>fetch(/*...*/).then(() =&gt; {
  setCount(c =&gt; c + 1);
  setFlag(f =&gt; !f);
  // React will only re-render once at the end (that&#39;s batching!)
})</code></pre><p>이것도    </p>
<pre><code>elm.addEventListener(&#39;click&#39;, () =&gt; {
  setCount(c =&gt; c + 1);
  setFlag(f =&gt; !f);
  // React will only re-render once at the end (that&#39;s batching!)
});</code></pre><p>그리고 이것도!
모두 동일하게 atching 됩니다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/308c03f5-8b0b-46e2-a099-036bce94bd93/image.png" alt=""></p>
  <br>

<p>물론 이런 상황을 의심 많은 사람들을 위한 데모 코드도 있습니다. 아래 샌드박스로 들어가 Console log 를 확인해보세요.</p>
<p>  <a href="https://codesandbox.io/s/jolly-benz-hb1zx?file=/src/index.js">React 17 : 배치 처리 되지 않고 2번 렌더링 되는 예제</a>
  <a href="https://codesandbox.io/s/morning-sun-lgz88?file=/src/index.js">React 18 : event handler 밖에서도 완벽하게 배치 처리되는 예제!</a></p>
<br>
<br>

<h2 id="automatic-batching-적용하는-법--createroot-사용하기">Automatic batching 적용하는 법 : createRoot 사용하기</h2>
<p>일단 &quot;root&quot;가 뭔지부터 짚고 갈까요. </p>
<p>React에서 root는 rendering Tree(다들 아는 그것)를 트래킹 하기 위해 사용하는 데이터 구조의 최상위를 가리킵니다.</p>
  <br>

<p>일반적으로 사용하는 index.js 의 root 코드를 보겠습니다.</p>
<pre><code>import * as ReactDOM from &#39;react-dom&#39;;
import App from &#39;App&#39;;

const container = document.getElementById(&#39;app&#39;);

// 최초의 렌더링
ReactDOM.render(&lt;App tab=&quot;home&quot; /&gt;, container);

// 변경사항이 발생하면, React 는 DOM 객체의 root 에 접근합니다.
ReactDOM.render(&lt;App tab=&quot;profile&quot; /&gt;, container);</code></pre><p>렌더링 할 때 ReactDom.render() 라는 API 를 사용하는 것을 볼 수 있을텐데요, 공식 문서에서는 이것을 Legacy root API 라고 부릅니다.</p>
<p>ReactDom.render() 에서 &quot;root&quot;는 DOM 객체에 이어붙이는(attached) 용도이고 여기에 접근하기 위해서 역시 DOM 노드를 사용하기 때문에 &quot;root&quot;라는게 뭔지 알 수 없습니다. </p>
  <br>
  <br>

<p>지금부터 우리가 사용하려는 New Root API, 즉 createRoot() 에서는 이렇게 바뀔거에요.</p>
<pre><code>import * as ReactDOM from &#39;react-dom&#39;;
import App from &#39;App&#39;;

const container = document.getElementById(&#39;app&#39;);

// root 를 명시적으로 생성합니다.
const root = ReactDOM.createRoot(container);

// 최초의 렌더링: 객체를 root 에 렌더링하기
root.render(&lt;App tab=&quot;home&quot; /&gt;);

// 변경 사항이 발생하면 container 에 root 를 다시 전달 할 필요가 없습니다.
root.render(&lt;App tab=&quot;profile&quot; /&gt;);</code></pre><p>이해가 되시나요?</p>
<p>개발자의 입장에서 대체 뭔 소리야 싶었던 root 가 짜잔🎉, 하고 명시적으로 등장했습니다. 덕분에 렌더링 할 때 마다 container 를 매번 render()함수에 전달 할 필요가 없어졌어요. 
  <br></p>
<h2 id="batch-처리하기-싫어요">Batch 처리하기 싫어요!</h2>
<p><img src="https://images.velog.io/images/cindy-choi/post/7908a6aa-00e5-44c4-bfc4-93bb09ab7198/image.png" alt=""></p>
<p>가끔은 state 가 변경되는 즉시 DOM 에 반영해야 할 상황이 생깁니다. Batching 하기엔 빠른 반영이 필요한 것들.</p>
<p>그런 상황을 위해 Batch에서 제외할 수 있는 API 를 제공합니다. 바로 flushSync 에요.</p>
<pre><code>import { flushSync } from &#39;react-dom&#39;; // Note: react-dom, not react

function handleClick() {
  flushSync(() =&gt; {
    setCounter(c =&gt; c + 1);
  });
  // 여기서 바로 DOM을 업데이트 합니다.
  flushSync(() =&gt; {
    setFlag(f =&gt; !f);
  });
  // 여기서도 마찬가지.
}</code></pre><p>여기까지 batching 관련한 내용이었습니다. </p>
<p>정리하느라 생략된 내용들이 많습니다. <a href="https://github.com/reactwg/react-18/discussions/21">공식 Github 문서</a>에서 나머지 내용을 살펴보세요.</p>
<p>  <br><br></p>
<h1 id="2-starttransition">2. startTransition</h1>
<p>React 18 에서 소개하는 새로운 API 입니다. 이 API는 대량의 update 를 처리하는 와중에도 우리의 어플리케이션이 응답성을 유지할 수 있게 도와줍니다.</p>
<br>

<p>startTransition()을 사용하면 특정 update를 &#39;transitions&#39;로 표시해서 UI를 개선할 수 있습니다. 따라서 이를 이용하명 state 가 변경되는 중에 시각적 피드백을 제공하게 하고 브라우저의 응답성을 유지할 수 있습니다. </p>
<p>무슨 말인지는, 아래의 동영상을 보면 확실히 알 수 있습니다. 
(출처 : <a href="https://github.com/reactwg/react-18/discussions/65">공식 discussion</a>))</p>
<p><a href="https://user-images.githubusercontent.com/2440089/122987896-21fd2180-d36f-11eb-8d82-8f22324b6850.mov">클릭해서 보기</a></p>
<p>코드로도 볼까요.</p>
<pre><code>function FastSlider({defaultValue, onChange}) {
  const [value, setValue] = useState(defaultValue);
  const timeout = useRef(null);

  return (
    &lt;Slider
      value={value}
      onChange={(e, nextValue) =&gt; {
        clearTimeout(timeout.current);

        // Update the slider.
        setValue(nextValue);

        // 슬라이더가 100ms 이상 움직임이 없으면
        // 값이 변경된 걸로 칩니다.
        timeout.current = setTimeout(() =&gt; {
          onChange(nextValue);
        }, 100);
      }}
    /&gt;
  );
}</code></pre><p>상단의 슬라이더로 value를 변경할 수 있으며, 변경된 value를 참조하는 하단의 그래프가 re-rendering되는 UI입니다.</p>
<p>일반적으로는 문제가 없을 수 있지만, 그래프가 대량의 update를 처리하는 경우라면 동영상에서 처럼 화면 자체가 멈춰버리는 현상이 생깁니다. </p>
<pre><code>  ![](https://images.velog.io/images/cindy-choi/post/cdfedd6c-c559-4113-b407-f40f655c6c0a/image.png)</code></pre><br>
최악이야.

<p>그러면 위의 코드를 개선해보겠습니다.
<br></p>
<p>참고로 startTransition을 사용하기 위해서는 섹션 1에 언급 된 New Root API 를 사용해야 하기 때문에, index.js 파일의 root 를 아래와 같이 바꿔주세요.</p>
<pre><code>// 이거를
ReactDOM.render(&lt;App /&gt;, document.getElementById(&#39;root&#39;));

// 이렇게
const root = ReactDOM.createRoot(document.getElementById(&#39;root&#39;));
root.render(&lt;App /&gt;);</code></pre><p>이제 코드에 직접 startTransition()을 사용해보겠습니다. </p>
<pre><code>import { startTransition } from &#39;react&#39;;

// ...그 외 생략

function FastSlider({defaultValue, onChange}) {
  const [value, setValue] = useState(defaultValue);

  return (
    &lt;Slider
      value={value}
      onChange={(e, nextValue) =&gt; {
        {/* 여기에서 slider의 값(value)이 변경됩니다. */}
        setValue(nextValue);
        onChange(nextValue);
      }}
    /&gt;
  );
}

// 버블 차트 부분
function ClassicAssociationsBubbles({associations}) {
  const [minScore, setMinScore] = useState(0.1);

  const data = computeData(minScore);

  return (
    &lt;&gt;
      &lt;FastSlider defaultValue={0.1} onChange={val =&gt; {
        {/* Update the results, in a transition. */}
        startTransition(() =&gt; {
          setMinScore(val);
        });
      }/&gt;
      &lt;ExpensiveChart data={data}/&gt;
    &lt;/&gt;
  );
}</code></pre><p>여기까지만 해도 적용 되지만, 하나만 더 개선해볼게요.</p>
<p>사용자에게 현재 차트가 그려지는 중에는 <strong>차트의 투명도를 낮추어</strong> 펜딩 상태를 가시적으로 표현해보겠습니다.</p>
<pre><code>import { useTransition } from &#39;react&#39;;

// FastSlider 부분은 놔두고 아래의 버블 차트 부분만 변경합니다.

function ClassicAssociationsBubbles({associations}) {
  // New hook in React 18
  const [isPending, startTransition] = useTransition();

  const [minScore, setMinScore] = useState(0.1);
  const data = computeData(minScore);

  return (
    &lt;&gt;
      &lt;FastSlider defaultValue={0.1} onChange={val =&gt; {
        /* transition 내부에서 값을 변경합니다. */}
        startTransition(() =&gt; {
          setMinScore(val);
        });
      }/&gt;
      &lt;ExpensiveChart
        // pending class 적용하기.
        className={isPending ? &#39;pending&#39; : &#39;done&#39;}
        data={data}
      /&gt;
    &lt;/&gt;
  );
}

// index.css
.pending {
  opacity: 0.7;

  // 너무 빨리 렌더링되면
  // 보이지 않을 수 있으므로 0.4s 지연을 줍니다.
  transition: opacity 0.2s 0.4s linear;
}

.done {
  opacity: 1;
  transition: opacity 0s 0s linear;
}</code></pre><p>여기까지!
수정된 코드의 결과 역시 아래의 동영상으로 확인하세요.</p>
<p>그래프가 렌더링 되는 동안에도 슬라이더는 사용자의 움직임에 반응하며, 차트가 렌더링 되는 동안 투명도 값이 조절되므로 사용자는 그래프의 로딩 상황을 알 수 있습니다. 🧐</p>
<p>startTransition에 대해 더 알고 싶다면 <a href="https://github.com/reactwg/react-18/discussions/65">공식 discussion</a>을 참조하세요. </p>
<br>
<br>

<h1 id="3-usedeferredvalue">3. useDeferredValue</h1>
<p>&#39;use&#39;로 시작하는 걸로 봐서는 hook이에요.
이쪽은 정보가 많이 없었습니다. 많이 주목받지 못하는 자그마한 어쩌고,,, </p>
<p>useDeferredValue 는 말 그대로 어떤 변수의 deferred 된 값을 반환하는 hook입니다. </p>
<br>

<p>react 가 deferred 개념을 처음 사용한 것은 아닙니다.</p>
<p>기존에도 HTML 의 <code>&lt;script&gt;</code> 태그에 defer 옵션이 있었고, jQuery의 promise를 공부할 때면 근근히 따라 다니는 것이 deferred 개념이었죠. </p>
<p>가장 쉬운 서치바를 만들 때에도 이 개념은 유효합니다. 저는 직접 구현하기도 했지만 주로 lodash 의 debounce() API 를 사용했었습니다.</p>
<p>아래는 간단한 예시입니다. 
input 에 100ms 동안 변경사항이 없으면 searchQuery에 검색어를 저장하여 MyList에서 필터링합니다.</p>
<pre><code>function FastSlider() {
  const [value, setValue] = useState(&#39;&#39;);
  const [searchQuery, setSearchQuery] = useState(&#39;&#39;);
  const timeout = useRef(null);

  return (
    &lt;input
      value={value}
      onChange={(e, nextValue) =&gt; {
        clearTimeout(timeout.current);
        setValue(nextValue);

        {/* 100ms 이상 값이 변하지 않으면 검색어를 업데이트합니다. */}
        timeout.current = setTimeout(() =&gt; {
          setSearchQuery(nextValue);
        }, 100);
      }}
    /&gt;

    &lt;MyList text={searchQuery} /&gt;
  );
}</code></pre><p>그러나!
이제 React에서 제공하는 hook을 사용해서 아주 짧은 코드로 대체가 가능합니다. </p>
<pre><code>function App() {
  const [text, setText] = useState(&quot;hello&quot;);
  const deferredText = useDeferredValue(text, { timeoutMs: 2000 });

  return (
    &lt;div className=&quot;App&quot;&gt;
      {/* input에 현재 텍스트를 계속 전달합니다. */}
      &lt;input value={text} onChange={handleChange} /&gt;
      ...
      {/* 하지만 이 목록은 필요한 경우 &quot;뒤처질&quot; 수 있습니다. */}
      &lt;MySlowList text={deferredText} /&gt;
    &lt;/div&gt;
  );
 }</code></pre><p>짠.
코드가 매우 심플해졌죠. 또 기존에 lodash 의 debounce()를 사용하시던 분들이라면 외부 라이브러리 의존성을 하나 덜었습니다.</p>
<p>물론 useDeferredValue()의 두번째 매개변수 timeoutMs 는 ms 단위로 사용자가 얼마든지 조절이 가능합니다. </p>
<br>
<br>

<hr>
<p>여기까지 RC 단계에 있는 React 18의 일부 기능에 대해 알아봤습니다.
그럼 이만.</p>
<p>끝</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[git inflate error 해결하기]]></title>
            <link>https://velog.io/@cindy-choi/git-inflate-error-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@cindy-choi/git-inflate-error-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 27 Sep 2021 08:40:05 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/cindy-choi/post/2194896b-e18d-404e-a310-317df4afb0b0/image.png" alt=""></p>
<h2 id="개발-중-다음과-같은-stream-error-가-발생-했을-때">개발 중 다음과 같은 stream error 가 발생 했을 때</h2>
<pre><code>cindy {~/dev/ndc-ui}±(dev); greetings, earthling [8.449Mb]$ ☞ git push
error: inflate: data stream error (unknown compression method)
error: unable to unpack 6f9633f847dcc930d15d7b1e781a4cc4895de56b header
fatal: loose object 6f9633f847dcc930d15d7b1e781a4cc4895de56b (stored in .git/objects/6f/9633f847dcc930d15d7b1e781a4cc4895de56b) is corrupt
fatal: the remote end hung up unexpectedly
fatal: the remote end hung up unexpectedly
fatal: the remote end hung up unexpectedly
error: failed to push some refs to &#39;http://gitlab.~~~~~~.git&#39;</code></pre><p>.git 디렉토리의 hash 파일이 깨진 것이 원인. 
혹시 모르니 백업을 해두길 권장합니다. </p>
<h2 id="해결-방법">해결 방법</h2>
<pre><code>git fsck --full</code></pre><p>해당 명령어를 치면 결과로 깨진 파일들이 쭉 표시됩니다. </p>
<pre><code>error: inflate: data stream error (unknown compression method)
error: unable to unpack header of .git/objects/07/0649ca71bd2ba71f71ee215a731e7b03d287a7
error: 070649ca71bd2ba71f71ee215a731e7b03d287a7: object corrupt or missing: .git/objects/07/0649ca71bd2ba71f71ee215a731e7b03d287a7
error: inflate: data stream error (unknown compression method)
error: unable to unpack header of .git/objects/47/24a71d103cdde9789786e2d0c70f4751531a1d
error: 4724a71d103cdde9789786e2d0c70f4751531a1d: object corrupt or missing: .git/objects/47/24a71d103cdde9789786e2d0c70f4751531a1d
error: inflate: data stream error (unknown compression method)
error: unable to unpack header of .git/objects/6f/9633f847dcc930d15d7b1e781a4cc4895de56b
error: 6f9633f847dcc930d15d7b1e781a4cc4895de56b: object corrupt or missing: .git/objects/6f/9633f847dcc930d15d7b1e781a4cc4895de56b
Checking object directories: 100% (256/256), done.
error: a508fcd0e5c9573604ab8c338380e504b942718d: invalid sha1 pointer in cache-tree
error: inflate: data stream error (unknown compression method)
error: unable to unpack 6f9633f847dcc930d15d7b1e781a4cc4895de56b header
fatal: loose object 6f9633f847dcc930d15d7b1e781a4cc4895de56b (stored in .git/objects/6f/9633f847dcc930d15d7b1e781a4cc4895de56b) is corrupt</code></pre><p>깨졌다고 표시되는 .git/objects/{index} 파일을 모두 제거해준 다음 리셋해주면 끝!  </p>
<pre><code>git reset</code></pre><p>이후에는 같은 문제 없이 git이 잘 동작하는 것을 확인할 수 있습니다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/4b48c8d1-3c56-418c-a5f3-75658650e9f0/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Google API에서 연락처 가져오기]]></title>
            <link>https://velog.io/@cindy-choi/Google-API%EC%97%90%EC%84%9C-%EC%97%B0%EB%9D%BD%EC%B2%98-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</link>
            <guid>https://velog.io/@cindy-choi/Google-API%EC%97%90%EC%84%9C-%EC%97%B0%EB%9D%BD%EC%B2%98-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</guid>
            <pubDate>Wed, 20 Jan 2021 16:25:28 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘의-목표📋">오늘의 목표📋</h1>
<p>☑️ (지난번에 이어) 로그인 후 발급받은 토큰을 로컬 스토리지에 저장하고 관리하기
☑️ Vue에 api 모듈 추가하기
☑️ Google이 제공하는 People API 로 사용자 연락처 받아오기📌</p>
<p>문서가 잘 되어있는듯 안 되어있어서 너무 힘들었다 ㅜㅜ
people API 가 아니라도 Google이 제공하는 모든 open API 는 같은 방식으로 사용할 수 있다. </p>
<h1 id="저번에-로그인-한-거-있잖아요">저번에 로그인 한 거 있잖아요</h1>
<p>그 때 받아온 AuthToken을 AuthManager를 만들어서 관리하기로 했었는데 포스트로 정리 안하고 그냥 했었다.....</p>
<p>벨로그를 오픈한김에 정리 하자면 </p>
<p><code>frontend/src/plugins/AuthManager.js</code> 파일을 만들어서 아래처럼 구현한다. </p>
<p>대충 내용은 로그인 후 발급받은 access token 을 브라우저의 로컬 스토리지에 저장해두고 AuthManager를 통해 관리하겠다는 것이다. </p>
<pre><code>import moment from &#39;moment&#39;;
const AUTH_SECRET_KEY=&#39;secret&#39;;
const AUTH_EXPIRED_KEY=&#39;expired&#39;;

const AuthManager = () =&gt; {
  let state = {
    [AUTH_SECRET_KEY]: null,
    [AUTH_EXPIRED_KEY]: null,
  };

  const initialize = () =&gt; {
    state[AUTH_SECRET_KEY] = null;
    state[AUTH_EXPIRED_KEY] = null;

    localStorage.removeItem(AUTH_SECRET_KEY, null);
    localStorage.removeItem(AUTH_EXPIRED_KEY, null);
  };

  const save = (params) =&gt; {
    console.log(params);
    state[AUTH_SECRET_KEY] = params.token;
    state[AUTH_EXPIRED_KEY] = params.expired;

    localStorage.setItem(AUTH_SECRET_KEY, params.token);
    localStorage.setItem(AUTH_EXPIRED_KEY, params.expired);
  };

  const load = () =&gt; {
    state[AUTH_SECRET_KEY]  = localStorage.getItem(AUTH_SECRET_KEY);
    state[AUTH_EXPIRED_KEY] = localStorage.getItem(AUTH_EXPIRED_KEY);
  };

  const getToken = () =&gt; {
    return state[AUTH_SECRET_KEY];
  };

  const getExpired = () =&gt; {
    return state[AUTH_EXPIRED_KEY];
  };

  return {
    initialize,
    save,
    load,
    getToken,
    getExpired,
    //TODO isValid, 
  };
};

export default AuthManager();</code></pre><p>그리고 로그인 화면에서 로그인 성공 시 토큰과 만료시간을 저장해준다.
<code>frontend/src/views/Login.vue</code></p>
<pre><code>    async handleLogin() {
      try {
        const GoogleUser = await this.$gAuth.signIn(() =&gt; {}, (e) =&gt; {console.log(e);});
        if (!GoogleUser.Bc.access_token) throw new Error(&#39;로그인에 실패했습니다.&#39;);

        // 저장중
        this.googleUser = GoogleUser;

        // AuthManager를 통해 로컬 스토리지에 저장
        AuthManager.save({
          token: GoogleUser.Bc.access_token,
          expired: GoogleUser.Bc.expires_at,
        });

        this.$router.push(&#39;/main&#39;);
      } catch (e) {
        if (e.error === &#39;popup_closed_by_user&#39;) {
          // 로그인 취소, 에러 아님
          return;
        }
        console.error(e);
      } finally {
        this.getLoginInfo();
      }
    },</code></pre><p>내 프로젝트의 경우 직접 구현하는 서버는 폐쇄망에 설치 될 예정이라 별도로 인증 기능은 구현하지 않기로 했다. </p>
<p>단, 저장된 access token의 expired를 체크하여 만료되는 순간 자동으로 <code>AuthManager.initialize()</code>를 호출하여 정보를 클리어해주고 로그인 화면으로 이동 시킬 예정이다. </p>
<h1 id="vue에-api-모듈-세팅">Vue에 api 모듈 세팅</h1>
<p><code>frontend/src/api</code> 경로를 만들고 다음과 같이 구성한다. </p>
<p><code>frontend/src/api/index.js</code></p>
<pre><code>import axios from &#39;axios&#39;;
const client = axios.create({
  headers: {
    [&#39;Content-Type&#39;]: &#39;application/json;charset=UTF-8&#39;,
  },
});

client.interceptors.request.use(
  (request) =&gt; {
    if (!request.data) {
      request.data = {};
    }

    return request;
  },

  (error) =&gt; {
    Promise.reject(error);
  },
);

client.interceptors.response.use(
  (response) =&gt; {
      // 200 이 아니면 에러로 처리
    if (response &amp;&amp; response.status !== 200) {
      return Promise.reject(response);
    }
    return response;
  },
  (error) =&gt; {
    throw error;
  },
);

export default client;</code></pre><p>다음은 google 로 api를 던질때와 내 서버로 api 를 던질때 사용할 url을 정의한다. </p>
<p><code>frontend/src/api/url.js</code></p>
<pre><code>export default {
  server: `${process.env.VUE_APP_SERVER_URL}`,
  google: {
    base: &#39;https://www.googleapis.com&#39;,
  },
};</code></pre><p>이제 나머지에서 가져다 쓰기만 하면 끄읏 😊</p>
<h1 id="✨-이제-진짜로-구글api를-써보자">✨ 이제 진짜로 구글API를 써보자!</h1>
<h2 id="google-api-에서-내가-쓸-라이브러리-골라-권한-얻기">Google API 에서 내가 쓸 라이브러리 골라 권한 얻기</h2>
<p>지난번에 <a href="console.developers.google.com">console.developers.google.com</a> 에서 내 어플리케이션이 사용할 프로젝트 생성하는 방법을 링크로 대체했었다. </p>
<p>그 때 생성했던 프로젝트로 돌아가서 <a href="https://console.developers.google.com/apis/library">API 추가</a> 버튼을 누르자. 
<img src="https://images.velog.io/images/cindy-choi/post/453ac518-fdcd-4e2f-a9e8-923f809b145f/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.34.20.png" alt=""></p>
<p>그럼 멋진 구글 API 고르기 화면이 나온다. 
<img src="https://images.velog.io/images/cindy-choi/post/1f8fba44-c2d9-4437-adaa-3cd10e5d0c3e/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.40.39.png" alt=""></p>
<p>여기서 내 앱에 붙여줄 마음에 드는 API 를 하나하나 둘러볼 수도 있지만
우리는 👥 <strong>연락처</strong> 데이터를 받아올 것이기 때문에 &#39;people&#39; 을 검색한다. </p>
<blockquote>
<p>** 주의!** 
기존에는 연락처 정보를 관리하기 위해 Contract API 를 사용했었지만 이제 그건 사라졌고 People API 를 사용해야 한다.</p>
</blockquote>
<p><img src="https://images.velog.io/images/cindy-choi/post/d9385018-d0ed-4b87-8e13-1577b0bfee67/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.37.17.png" alt=""></p>
<p>찾아 들어가서 </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/89992a16-6953-4a33-b6de-cf6a6ffc6b41/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.37.28.png" alt=""></p>
<p>사용 버튼을 누르면 내 프로젝트(neti)에 로그인한 사람의 연락처 정보를 얻어올 권한이 생긴다. </p>
<h2 id="oauth-에-people-api-scope-추가하기">OAuth 에 People API scope 추가하기</h2>
<p>지난번 포스팅에서 발견한 vue3 전용 oauth2를 아직 안붙였음...
아무튼 각자의 방식으로 oauth 모듈에 people 스콥을 추가해주자. </p>
<p><code>frontend/src/main.js</code></p>
<pre><code>// set auth config
const prompt = &#39;select_account&#39;
const GoogleAuthConfig = Object.assign({ scope: &#39;profile email&#39; }, {
  clientId: process.env.VUE_APP_OAUTH_CLIENT,
  // 여기가 스콥 추가 부분 
  scope: &#39;profile email https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/directory.readonly&#39;,
});</code></pre><p>(잘보면 내가 캘린더 API 도 몰래 추가해놓은 것을 알 수 있다)</p>
<h2 id="api-로-요청을-날려보자">API 로 요청을 날려보자</h2>
<p>내가 사용할 API는 이거!
<a href="https://developers.google.com/people/api/rest/v1/people/listDirectoryPeople">People API 페이지 가기</a></p>
<p><img src="https://images.velog.io/images/cindy-choi/post/533f7a9e-c937-4312-8399-6982d3e7b9cd/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.48.34.png" alt=""></p>
<p>해당 화면 우측에 보면 <strong>Try it</strong> 버튼이 있는데 일단 이걸로 제대로 사용법을 간단히 익혀보자. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/869e098a-5870-4bae-be9b-7f029e24b5f9/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.52.01.png" alt=""></p>
<p>문서가 잘 정리된 듯 잘 정리되어있지 않다... 눈에 안들어와.</p>
<p>Query parameters 를 보면 필수인 애들은 Required 라고 명시 되어있다. 
Try this API 에서 필수인 파라미터를 눈치껏 넣어준 다음 </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/4a99546d-0460-4aa7-b02a-31d1245b9630/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.53.39.png" alt=""></p>
<p>우린 OAuth만 쓸거니까 아래건 체크 해제를 해주고 (사실 뭔 차이인지 모르겠다) EXECUTE 로 실행한다. </p>
<p>권한 설정이 잘못 되어있으면 401이나 403 에러가 난다. 
그런데 Try it 에서 권한 에러가 난다면 당신의 계정 자체가 이 API를 사용할 권한이 없다는 뜻이다.</p>
<h4 id="⚠️-구글-웹으로-서비스를-이용하는-것과-api-권한이-있는건-다르다">⚠️ 구글 웹으로 서비스를 이용하는 것과 API 권한이 있는건 다르다</h4>
<p>처음에 Group Manager API 를 사용하려고 정말 하루 종일 삽질을 엄청 했다. 
내 경우, 구글의 Group 메뉴에서 그룹 추가/삭제/수정 작업을 잘만 했는데 API를 호출하면 권한이 없다고 해서(401) 돌아버릴뻔.... </p>
<p>다른 API 를 써본 결과, 구글 웹페이지를 쓰는 권한과 OAuth로 API를 사용할 권한은 다른것으로 결론이 났다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/4e62d129-95b7-4cec-b1aa-882cc56e23ba/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%201.01.15.png" alt=""></p>
<p>아무튼 200 응답을 받았다면 Try it 상단의 확장 버튼으로 상세한 호출 방법을 확인하자.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/5a11a0fe-bae2-433a-af19-84cd1a17a9c1/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.55.05.png" alt="">
눌러서</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/243552d1-7bda-49e8-bed8-b577f8e83e16/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-21%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%201.03.54.png" alt=""></p>
<p>cURL 부분을 보자.</p>
<p>첫 줄은 우리가 사용할 URL🍒 을, 
두 번째 줄은 HTTPS Request를 날릴 때 Header🧀 를 보여준다. </p>
<h2 id="access-token을-헤더에-싣기">access token을 헤더에 싣기</h2>
<blockquote>
<p>위에서 API 사용설정을 해주었으면 변경된 scope을 적용하기 위해 로그인을 다시 해주어야 한다</p>
</blockquote>
<p>이미 로그인이 되어 로컬 스토리지에 access token이 있다고 가정하겠다. </p>
<p>위에서 확인한 우리가 사용할 URL🍒을 추가해주자. 
<code>frontend/src/api/url.js</code></p>
<pre><code>export default {
  server: `${process.env.VUE_APP_SERVER_URL}`,
  google: {
    base: &#39;https://www.googleapis.com&#39;,
    // people URL 추가
    people: &#39;https://people.googleapis.com/v1&#39;,
  },
};</code></pre><p>이제 Header 🧀 를 설정해주면 된다. 거의 다 왔다!</p>
<p><code>frontend/src/api/google/index.js</code> 파일을 만들어 구글로의 요청을 따로 분리했다.</p>
<pre><code>import client from &#39;@/api&#39;;
import BASE from &#39;@/api/url&#39;;
import AuthManager from &#39;@/plugins/AuthManager&#39;;

const accessToken = () =&gt; {
  AuthManager.load();
  return AuthManager.getToken();
}

export default {
  getMembers: () =&gt; client.get(
    // 위에서 확인한 query param도 붙여넣어주고 
    `${BASE.google.people}/people:listDirectoryPeople?readMask=emailAddresses,names,photos&amp;sources=DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE`,
    { // header 🧀  세팅
      headers : { &#39;Authorization&#39; : `Bearer ${accessToken()}` }
    }
  ),
};</code></pre><p>내 query param은 이메일 주소와 이름, 프로필 이미지를 받아오도록 설정되어있다.</p>
<h1 id="이제-날려볼까">이제 날려볼까?</h1>
<p>테스트용 페이지를 새로 만들어서 버튼을 추가한 다음, 클릭 콜백에서 요 API 를 호출하자. </p>
<p><code>아무 vue 파일</code></p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;button
      @click=&quot;getMembers&quot;
    &gt;
      직원 목록 가져오기
    &lt;/button&gt;
    {{ members }}
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import api from &#39;@/api/google&#39;;

export default {
  name: &#39;GoogleTest&#39;,
  data: () =&gt; ({
    members: null,
  }),

  methods: {
    async getMembers() {
      const response = await api.getMembers();
      console.log(response);
      this.members = response.data.people; 
    },
  },
};
&lt;/script&gt;</code></pre><p>로그인 한 다음 실행해야 한다. 안그럼 권한 없음으로 실패 뜸~! </p>
<p>정상적으로 성공한 <code>response.data</code>를 찍어보면 쬐금 많이 복잡하다. </p>
<pre><code>{
  &quot;people&quot;: [
    {
      &quot;resourceName&quot;: &quot;people/아이디&quot;,
      &quot;etag&quot;: &quot;뭔지 모르지만 숨김&quot;,
      &quot;names&quot;: [
        {
          &quot;metadata&quot;: {
            &quot;primary&quot;: true,
            &quot;source&quot;: {
              &quot;type&quot;: &quot;PROFILE&quot;,
              &quot;id&quot;: &quot;아이디&quot;
            }
          },
          &quot;displayName&quot;: &quot;이름1&quot;,
          &quot;familyName&quot;: &quot;김&quot;,
          &quot;givenName&quot;: &quot;김이름1&quot;,
          &quot;displayNameLastFirst&quot;: &quot;김이름1&quot;,
          &quot;unstructuredName&quot;: &quot;김이름1&quot;
        }
      ],
      &quot;photos&quot;: [
        {
          &quot;metadata&quot;: {
            &quot;primary&quot;: true,
            &quot;source&quot;: {
              &quot;type&quot;: &quot;PROFILE&quot;,
              &quot;id&quot;: &quot;아이디&quot;
            }
          },
          &quot;url&quot;: &quot;https://lh5.googleusercontent.com/어쩌고저쩌고/photo.jpg&quot;,
          &quot;default&quot;: true
        }
      ],
      &quot;emailAddresses&quot;: [
        {
          &quot;metadata&quot;: {
            &quot;primary&quot;: true,
            &quot;verified&quot;: true,
            &quot;source&quot;: {
              &quot;type&quot;: &quot;DOMAIN_PROFILE&quot;,
              &quot;id&quot;: &quot;아이디&quot;
            }
          },
          &quot;value&quot;: &quot;sample@test.com&quot;
        }
      ]
    },
    ...   
  ]
}</code></pre><p>여기까지 된다면 People API 사용 성공!⚡️</p>
<h1 id="너무-복잡한-people을-순화하기">너무 복잡한 People을 순화하기</h1>
<p>마지막에 찍어본 people 구조가 너무 복잡하다.. 
내 프로젝트 내에서 이 구조를 고대로 들고다니자니 getter 하기가 너무 피곤할 것 같아서 나만의 클래스를 쓰기로 한다. </p>
<p><code>frontend/src/entity/people.js</code>생성</p>
<pre><code>class People {
  constructor(_people) {
    if (_people.emailAddresses &amp;&amp; _people.emailAddresses.length) {
      this.email = _people.emailAddresses[0].value;
    }

    if (_people.names &amp;&amp; _people.names.length) {
      this.name = _people.names[0].value;
    } else if (this.email) {
      this.name = this.email.split(&#39;@&#39;)[0];
    }

    if (_people.photos &amp;&amp; _people.photos.length) {
      this.avatar = _people.photos[0].url || &#39;/static/images/default_avatar.png&#39;;
    }
  }

  getEmail() {
    return this.email;
  }

  getName() {
    return this.name;
  }

  getAvatar() {
    return this.avatar;
  }
}

export { People as default };</code></pre><p>만들었으니 써먹자~! </p>
<p><code>아까 그 아무 파일.vue</code> 를 다시 열어서 People로 컨버팅 한 다음 화면에 찍어보자. </p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;button
      @click=&quot;getMembers&quot;
    &gt;
      직원 목록 가져오기
    &lt;/button&gt;
    {{ members }}
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import api from &#39;@/api/google&#39;;
import People from &#39;@/entity/people&#39;;

export default {
  name: &#39;GoogleTest&#39;,
  data: () =&gt; ({
    members: null,
  }),

  methods: {
    async getMembers() {
      const response = await api.getMembers();
      console.log(response);
      this.members = response.data.people.map(people =&gt; new People(people));
    },
  },
};
&lt;/script&gt;</code></pre><p>이럼 대충 간단해져서 관리가 편해진다 ☺️</p>
<p>이거 한다고 삽질한게 꼬박 하루라.. 정리 안하고는 못배기겠다. 
오늘도 끄읏✨</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue3 에 Google OAuth2.0 붙이기!]]></title>
            <link>https://velog.io/@cindy-choi/Vue3-%EC%97%90-Google-OAuth2.0-%EB%B6%99%EC%9D%B4%EA%B8%B0</link>
            <guid>https://velog.io/@cindy-choi/Vue3-%EC%97%90-Google-OAuth2.0-%EB%B6%99%EC%9D%B4%EA%B8%B0</guid>
            <pubDate>Sun, 17 Jan 2021 12:04:50 GMT</pubDate>
            <description><![CDATA[<p>비록 express 로 서버를 만들고는 있지만 가능한 간편하게 하기 위해서 <strong>frontend</strong> 선에서 인증 기능을 구현하기로 했다. </p>
<p>더불어 내가 인증 기능을 만들긴 싫어서 Google OAuth를 쓰기로 한다.</p>
<h1 id="오늘의-목표-📋">오늘의 목표 📋</h1>
<p>☑️ Vue3에 Google OAuth2.0 붙이기
☑️ 로그인 후 사용자 정보 받아오기
☑️ 로그아웃 구현하기</p>
<h1 id="google-api에-내-사이트-등록-하기">Google API에 내 사이트 등록 하기</h1>
<p>사전에 <a href="https://console.developers.google.com">Google API</a>에 들어가서 신규 프로젝트 생성 후 토큰과 비밀키를 발급받아야 한다. </p>
<p>이 부분은 <a href="http://yoonbumtae.com/?p=2631">친절한 블로그</a>에 잘 정리가 되어있으니 따라하자. </p>
<p>마지막에 발급 받은 클라이언트 ID와 비밀키는 <code>.env</code> 파일을 만들어서 저장한다. </p>
<p>Vue는 dotenv를 설치하지 않아도 자동으로 <code>.env</code> 파일을 읽어들인다. 
단! 환경변수 이름이 무조건 <code>VUE_APP_</code> 으로 시작해야 한다. </p>
<p>프로젝트 루트 경로에 <code>.env</code>파일을 만들고 클라이언트 키를 저장해두자.</p>
<pre><code>VUE_APP_OAUTH_CLIENT=비밀이지롱</code></pre><h1 id="내-프론트엔드에-oauth-붙이기">내 프론트엔드에 OAuth 붙이기</h1>
<h3 id="진짜-감사하게도-유명한-라이브러리가-있다">진짜 감사하게도 유명한 라이브러리가 있다</h3>
<p><a href="https://www.npmjs.com/package/vue-google-oauth2">https://www.npmjs.com/package/vue-google-oauth2</a></p>
<p>Vue, google oauth 등으로 검색하면 99%는 위 모듈을 가져다 쓰는 샘플이다. 
검색하면 샘플도 적잖이 있어서 맘편하게 가져다 쓰기로. </p>
<pre><code>npm install vue-google-oauth2</code></pre><p>패키지를 설치해준 다음 <code>frontend/src/main.js</code> 파일을 열어 사용 설정을 해준다. </p>
<pre><code>import GAuth from &#39;vue-google-oauth2&#39;

app.use(GAuth, {
  clientId: process.env.VUE_APP_OAUTH_CLIENT, // 아까 .env 파일에 저장해둔 그것임
  scope: &#39;profile email https://www.googleapis.com/auth/plus.login&#39;
});</code></pre><h3 id="로그인-페이지-만들기">로그인 페이지 만들기</h3>
<p>다음은 테스트용 로그인 페이지이다. </p>
<p><code>frontend/src/views/Login.vue</code>파일을 열어서 테스트 코드를 써주었다. </p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    로그인 테스트 화면입니다
    &lt;button
      @click=&quot;handleLogin&quot;
    &gt;
      Google ID로 로그인
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
  name: &#39;Login&#39;,
  data: () =&gt; ({
  }),
  methods: {
    async handleLogin() {
      try {
        const GoogleUser = await this.$gAuth.signIn();
        console.log(GoogleUser);
      } catch (e) {
        console.error(e);
      }
    },
  },
};
&lt;/script&gt;</code></pre><p>그런 다음 화면을 띄워보면?
<img src="https://images.velog.io/images/cindy-choi/post/d84535da-c5ec-4ed9-a365-0676d53633ac/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-17%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.26.32.png" alt=""></p>
<p>** ☠️ 에러가 난다.** 
시키는 대로 했는데 왜...! </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/9c8fb90b-435c-4ee3-91f3-3a17c8aa3bfd/P20180911_142840000_AD855AC6-7B7B-49B1-8774-96B8E9FDA05E.jpg" alt=""></p>
<p>소스를 까봤더니 아니나 다를까 😊  Vue3로 업그레이드 한 것이 문제가 된 것이다.
Vue3에서 전역모듈을 삽입하는 방법이 바뀐 덕분에 제대로 코드가 돌아가지 않고 있었다. </p>
<p>이제와서 다운그레이드 할 생각은 없고 <code>vue-google-oauth2</code> 코드를 참조해서 새로 모듈을 작성하기로 했다. </p>
<h2 id="다시-시도-하기🔥">다시 시도 하기🔥</h2>
<p>쓰지 못하는 모듈의 전체 소스가 길지 않아 카피해왔다. </p>
<pre><code>npm uninstall vue-google-oauth2</code></pre><p>기존 것은 지워주고</p>
<p><code>frontend/src/authentification.js</code> 파일을 만들어서 
기존 <code>vue-google-oauth2</code>의 코드를 복사해온 다음 Vue3에 맞게 변형한다. </p>
<pre><code>var googleAuth = (function () {
  function installClient() {
    var apiUrl = &#39;https://apis.google.com/js/api.js&#39;
    return new Promise((resolve) =&gt; {
      var script = document.createElement(&#39;script&#39;)
      script.src = apiUrl
      script.onreadystatechange = script.onload = function () {
        if (!script.readyState || /loaded|complete/.test(script.readyState)) {
          setTimeout(function () {
            resolve()
          }, 500)
        }
      }
      document.getElementsByTagName(&#39;head&#39;)[0].appendChild(script)
    })
  }

  function initClient(config) {
    return new Promise((resolve, reject) =&gt; {
      window.gapi.load(&#39;auth2&#39;, () =&gt; {
        window.gapi.auth2.init(config)
          .then(() =&gt; {
            resolve(window.gapi)
          }).catch((error) =&gt; {
            reject(error)
          })
      })
    })

  }

  function Auth() {
    if (!(this instanceof Auth))
      return new Auth()
    this.GoogleAuth = null /* window.gapi.auth2.getAuthInstance() */
    this.isAuthorized = false
    this.isInit = false
    this.prompt = null
    this.isLoaded = function () {
      /* eslint-disable */
      console.warn(&#39;isLoaded() will be deprecated. You can use &quot;this.$gAuth.isInit&quot;&#39;)
      return !!this.GoogleAuth
    };

    this.load = (config, prompt) =&gt; {
      installClient()
        .then(() =&gt; {
          return initClient(config)
        })
        .then((gapi) =&gt; {
          this.GoogleAuth = gapi.auth2.getAuthInstance()
          this.isInit = true
          this.prompt = prompt
          this.isAuthorized = this.GoogleAuth.isSignedIn.get()
        }).catch((error) =&gt; {
          console.error(error)
        })
    };

    this.signIn = (successCallback, errorCallback) =&gt; {
      return new Promise((resolve, reject) =&gt; {
        if (!this.GoogleAuth) {
          if (typeof errorCallback === &#39;function&#39;) errorCallback(false)
          reject(false)
          return
        }
        this.GoogleAuth.signIn()
          .then(googleUser =&gt; {
            if (typeof successCallback === &#39;function&#39;) successCallback(googleUser)
            this.isAuthorized = this.GoogleAuth.isSignedIn.get()
            resolve(googleUser)
          })
          .catch(error =&gt; {
            if (typeof errorCallback === &#39;function&#39;) errorCallback(error)
            reject(error)
          })
      })
    };

    this.getAuthCode = (successCallback, errorCallback) =&gt; {
      return new Promise((resolve, reject) =&gt; {
        if (!this.GoogleAuth) {
          if (typeof errorCallback === &#39;function&#39;) errorCallback(false)
          reject(false)
          return
        }
        this.GoogleAuth.grantOfflineAccess({ prompt: this.prompt })
          .then(function (resp) {
            if (typeof successCallback === &#39;function&#39;) successCallback(resp.code)
            resolve(resp.code)
          })
          .catch(function (error) {
            if (typeof errorCallback === &#39;function&#39;) errorCallback(error)
            reject(error)
          })
      })
    };

    this.signOut = (successCallback, errorCallback) =&gt; {
      return new Promise((resolve, reject) =&gt; {
        if (!this.GoogleAuth) {
          if (typeof errorCallback === &#39;function&#39;) errorCallback(false)
          reject(false)
          return
        }
        this.GoogleAuth.signOut()
          .then(() =&gt; {
            if (typeof successCallback === &#39;function&#39;) successCallback()
            this.isAuthorized = false
            resolve(true)
          })
          .catch(error =&gt; {
            if (typeof errorCallback === &#39;function&#39;) errorCallback(error)
            reject(error)
          })
      })
    };
  }

  return new Auth()
})();

export default googleAuth;</code></pre><p>Vue에 설치하는 부분을 제거했을 뿐이다.</p>
<p>그 다음 <code>frontend/src/main.js</code> 를 재수정한다. 
이전 코드는 전부 지워버려라. 💧</p>
<pre><code>
// import GAuth from &#39;vue-google-oauth2&#39;; // 아래 모듈로 대체된다
import googleAuth from &#39;./authentification&#39;;

import App from &#39;./App.vue&#39;

// Create Vue Instance
const app = createApp(App);

...

app.use(GAuth, {
  clientId: process.env.VUE_APP_OAUTH_CLIENT,
  scope: &#39;profile email https://www.googleapis.com/auth/plus.login&#39;
});

// set auth config
const prompt = &#39;select_account&#39;
const GoogleAuthConfig = Object.assign({ scope: &#39;profile email&#39; }, {
  clientId: process.env.VUE_APP_OAUTH_CLIENT,
  scope: &#39;profile email https://www.googleapis.com/auth/plus.login&#39;,
});

// Install Vue plugin
app.config.globalProperties.$gAuth = googleAuth;
app.config.globalProperties.$gAuth.load(GoogleAuthConfig, prompt)

// ... 생략</code></pre><p>이건 나중에 기존 프로젝트 포크따서 올려둬야겠다. </p>
<p>Vue3 공식 홈페이지를 참조해서 후에 <code>this.$gAuth</code>를 사용할 수 있게 전역 모듈을 등록한 것이다. </p>
<p>여기까지 해주었다면 로그인 페이지를 다시 열어보자. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/e24cbce7-fde3-4027-92d7-9dd083dbd668/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-17%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.49.49.png" alt=""></p>
<p>에러없이 화면이 잘 떴다. 🌟</p>
<p>Google ID로 로그인 버튼을 누르면 새로운 팝업에 구글 로그인 화면이 뜨고 로그인 할 수 있다! </p>
<h2 id="사용자-정보-받아오기">사용자 정보 받아오기</h2>
<p>로그인을 했으면 했다고 떴으면 좋겠다. 
화면을 조금 변경해보자. </p>
<p><code>frontend/src/views/Login.vue</code></p>
<pre><code>&lt;template&gt;
    ...

    &lt;button
      :disabled=&quot;signedIn&quot;
      @click=&quot;handleLogin&quot;
    &gt;
      Google ID로 로그인
    &lt;/button&gt;

    &lt;div&gt;
      &lt;p&gt; 로그인 여부: {{signedIn}}&lt;/p&gt;
      &lt;img :src=&quot;userImage&quot; width=&quot;50&quot; height=&quot;50&quot;/&gt;
      &lt;p&gt; 로그인 사용자 이름: {{ userName }} &lt;/p&gt;
      &lt;p&gt; 로그인 사용자 이메일: {{ userEmail }} &lt;/p&gt;
    &lt;/div&gt;
    ...
&lt;/template&gt;
&lt;script&gt;
export default {
  name: &#39;Login&#39;,
  data: () =&gt; ({
    signedIn: false,
    userName: null,
    userEmail: null,
    userImage: null,
  }),
  methods: {
    clear() {
      this.signedIn = null;
      this.userName = null;
      this.userImage = null;
      this.userEmail = null;
    },

    async handleLogin() {
      try {
        const GoogleUser = await this.$gAuth.signIn();
        if (!GoogleUser.isSignedIn()) throw new Error(&#39;로그인에 실패했습니다.&#39;);

        this.signedIn = GoogleUser.isSignedIn();
        this.userName = GoogleUser.getBasicProfile().getName();
        this.userImage = GoogleUser.getBasicProfile().getImageUrl();
        this.userEmail = GoogleUser.getBasicProfile().getEmail();
      } catch (e) {
        console.error(e);
      }
    },
  },
};
&lt;/script&gt;</code></pre><p>GoogleUser에서 받아올 수 있는 정보는 <a href="https://developers.google.com/identity/sign-in/web/reference#googleuserissignedin">공식 홈페이지</a>를 보고 따왔다.</p>
<p>다시 로그인 페이지로 돌아가서 로그인을 해보자. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/d991c5ae-6e68-4e63-8989-2e18fbb82e71/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-17%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.56.11.png" alt=""></p>
<p>짠! 구글에서 받아온 프로필 이미지, 이름, 이메일 주소 등이 정상적으로 출력된다. </p>
<h2 id="초간단-로그아웃">초간단 로그아웃</h2>
<p><code>frontend/src/views/Login.vue</code>
로그아웃 버튼과 로그아웃 함수, 정보 클리어 함수를 구현한다. </p>
<pre><code>...
    &lt;button
      :disabled=&quot;!token&quot;
      @click=&quot;handleLogout&quot;
    &gt;
      로그아웃
    &lt;/button&gt;
...
methods: {
    clear() {
      this.signedIn = null;
      this.userName = null;
      this.userImage = null;
      this.userEmail = null;
    },

    ...

    async handleLogout() {
      try {
        await this.$gAuth.signOut();
        this.clear();
      } catch (e) {
        console.error(e);
      } finally {
        this.$router.push(&#39;/&#39;);
      }
    }</code></pre><p>로그아웃 버튼을 눌러 동작을 확인한다. 
안될리가 없지 😏 잘 된다. </p>
<p>다만 여기까지 했을 때의 문제점은 이 화면에 들어올 때 마다 ** 로그인을 다시 해야 한다** 는 것이다. </p>
<p>그러니 토큰 매니저를 만들어서 브라우저의 쿠키를 사용하자. 
.. 는 내일하자. </p>
<p>끝</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue3 에 vue-route 추가하기]]></title>
            <link>https://velog.io/@cindy-choi/Vue3-%EC%97%90-vue-route-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@cindy-choi/Vue3-%EC%97%90-vue-route-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 15 Jan 2021 09:32:58 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘의-목표-📋">오늘의 목표 📋</h1>
<p>☑️ Vue3 에 router 기능 추가
☑️ 로그인 전/후 자동 라우팅 기능 추가
☑️ 지원하지 않은 url 은 404 페이지 호출 </p>
<p>제목이 길어서 귀찮다. 다 생략한다. </p>
<h1 id="vue3-에-vue-route-추가하기">Vue3 에 vue-route 추가하기</h1>
<p>멋모르고 <code>npm install vue-router</code> 추가했었는데 잘 안되더라. </p>
<pre><code>npm i vue-router@next</code></pre><p>vuex4 설치할때와 마찬가지로 @next 가 붙는다. </p>
<p><code>frontend/src/router.js</code> 파일작성</p>
<pre><code>// Vuex 때 처럼 create* 함수를 제공한다.
import { createWebHistory, createRouter } from &#39;vue-router&#39;;

const routes = [
  {
    path: &#39;/&#39;,
    name: &#39;Home&#39;,
    component: () =&gt; import(&#39;@/views/Home&#39;), // 동적 import
  },
  {
    path: &#39;/login&#39;,
    name: &#39;Login&#39;,
    component: () =&gt; import(&#39;@/views/Login&#39;),
  },
];

// 이렇게 해도 된다.
// const router = createRouter({
//   history: createWebHistory(),
//   routes,
// });
// export default router;

export const router = createRouter({
  history: createWebHistory(),
  routes,
});</code></pre><p><code>/</code> 로 접근하면 Home 을 보여주고, 
<code>/login</code> 으로 접근하면 로그인 화면을 보여주겠다.</p>
<p><code>frontend/src/main.js</code> 오픈! </p>
<pre><code>import { createApp } from &#39;vue&#39;

import { store } from &#39;./store&#39;;
import { router } from &#39;./router&#39;; // 라우터 추가하고 

import App from &#39;./App.vue&#39;

// Create Vue Instance
const app = createApp(App);

app.use(store);
app.use(router); // 사용 설정 하기

app.mount(&#39;#app&#39;);</code></pre><p>다음으로 App.vue 에 라우터 뷰를 추가한다. 
기존에 사용하던 컴포넌트들을 삭제하고 <code>&lt;router-view /&gt;</code> 를 추가한다.</p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;!-- HelloWorld msg=&quot;Welcome to Your Vue.js App&quot;/--&gt;
    &lt;router-view /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
// import HelloWorld from &#39;@/components/HelloWorld.vue&#39;

export default {
  name: &#39;App&#39;,
  components: {
    // HelloWorld,
  },
}
&lt;/script&gt;

&lt;style&gt;
&lt;/style&gt;</code></pre><p>대신 <code>/</code> 경로에 매핑되는 <code>views/Home.vue</code> 파일을 만들어서 기존 코드를 옮겨주었다. </p>
<p><code>frontend/src/views/Home.vue</code> 파일을 만들어서</p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;HelloWorld msg=&quot;Home&quot;/&gt;
    &lt;!-- 메인 문구에 Home 표시를 해주자 --&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import HelloWorld from &#39;@/components/HelloWorld.vue&#39;;
export default {
  name: &#39;Home&#39;,
  components: {
    HelloWorld,
  },
};
&lt;/script&gt;</code></pre><p>다 옮겨주었다면 이번엔 로그인 파일을 만들어주자. 
제대로 된 화면은 나중에 만들고.. 일단 라우팅만 할 수 있게 파일만 생성하는 느낌으로. </p>
<p><code>frontend/src/views/Login.vue</code></p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    로그인
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
  name: &#39;Login&#39;,
};
&lt;/script&gt;</code></pre><p>다 옮겨주었다면 <code>npm run serve</code> 해주자.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/86d4d215-82ab-44ee-a940-d986bc6a683e/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-15%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%206.26.23.png" alt=""></p>
<p>여전히 못생긴 홈페이지가 잘 보인다! </p>
<p>이번엔 라우팅. 주소창에 <code>http://localhost:5050/login</code> 을 쳐보자</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/3e7d1b69-4054-4b18-b16c-4767e42e53c3/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-15%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%206.27.11.png" alt=""></p>
<p>화면이 잘 바뀌는 것을 확인할 수 있다 😙
끄읏 .</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue + Express 웹 사이트 만들기 (4) 하란대로 했는데 express 코드에 es6가 안 먹힐 때]]></title>
            <link>https://velog.io/@cindy-choi/Vue-Express-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-4-%ED%95%98%EB%9E%80%EB%8C%80%EB%A1%9C-%ED%96%88%EB%8A%94%EB%8D%B0-express-%EC%BD%94%EB%93%9C%EC%97%90-es6%EA%B0%80-%EC%95%88-%EB%A8%B9%ED%9E%90-%EB%95%8C</link>
            <guid>https://velog.io/@cindy-choi/Vue-Express-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-4-%ED%95%98%EB%9E%80%EB%8C%80%EB%A1%9C-%ED%96%88%EB%8A%94%EB%8D%B0-express-%EC%BD%94%EB%93%9C%EC%97%90-es6%EA%B0%80-%EC%95%88-%EB%A8%B9%ED%9E%90-%EB%95%8C</guid>
            <pubDate>Fri, 15 Jan 2021 05:59:36 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘의-목표📋">오늘의 목표📋</h1>
<p>☑️ backend 코드에 es6 적용하기
☑️ nodemon 으로 백엔드 코드 띄우기</p>
<p>이거 하고 객체 설계 해야해서 목표를 짧게 잡았다. 
express es6 로 검색하면 많은 가이드가 나오고 명료하다. </p>
<h3 id="1-필요한-패키지를-설치한다">1. 필요한 패키지를 설치한다.</h3>
<pre><code>npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node</code></pre><p>위 목록은 es6 를 사용하는 데에 필요한 모듈들이고 
<code>nodemon</code>은 소스 코드 변경을 감지해서 핫로딩을 해주는 툴이다. </p>
<p>전역으로 깔았다.(로컬로 해도 무관)</p>
<pre><code>npm install --save-dev nodemon

# 또는

npm install -g nodemon</code></pre><h3 id="2-코드-내용을-es6-스타일로-바꾼다">2. 코드 내용을 ES6 스타일로 바꾼다.</h3>
<p>제일 거슬렸던 require() 구문을 import로 바꾸어준다. </p>
<p><code>backend/app.js</code> 를 바꾸자.</p>
<pre><code>import express from &#39;express&#39;;
import path from &#39;path&#39;;
import cookieParser from &#39;cookie-parser&#39;;
import logger from &#39;morgan&#39;;

// router
import indexRouter from &#39;./routes/index&#39;;
import usersRouter from &#39;./routes/users&#39;;
import meetingroomsRouter from &#39;./routes/meetingrooms&#39;;

// db
import connection from &#39;./database&#39;;

const app = express();

app.use(logger(&#39;dev&#39;));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

// app.use(express.static(path.join(__dirname, &#39;public&#39;)));

// samples
app.use(&#39;/&#39;, indexRouter);
app.use(&#39;/users&#39;, usersRouter);

// meetingroom
app.use(&#39;/meetingrooms&#39;, meetingroomsRouter);

export default app;</code></pre><p><code>database.js</code> 도 바꾸자.</p>
<pre><code>import mysql from &#39;mysql&#39;;

const connection = mysql.createConnection({
  host     : &#39;localhost&#39;,
  user     : &#39;root&#39;,
  password : &#39;alfzmxlfkEp!&#39;,
  database : &#39;neti&#39;
});

connection.connect();

export default connection;</code></pre><p><code>routes/</code> 안에 있는 파일들은 <code>meetingrooms.js</code> 만 바꿔두겠다.
<code>backend/routes/meetingrooms.js</code> </p>
<pre><code>import express from &#39;express&#39;;
const router = express.Router();

import connection from &#39;../database&#39;;

// ... 중간 생략

export default router;</code></pre><p>마지막으로 <code>backend/bin/www</code>까지.</p>
<pre><code>import app from &#39;../app&#39;;
import debugLib from &#39;debug&#39;;
import http from &#39;http&#39;;

const debug = debugLib(&#39;backend:server&#39;); // backend 는 백엔드 코드 최상위 경로 이름이다. 우리는 backend.

// ... 이하 생략</code></pre><p>babel 설정은 보통 루트 디렉토리에 <code>.babelrc</code> 를 만들어서 처리하는데, 우리는 babel 설정파일이 이미 있다. </p>
<p><code>babel.config.js</code> 요것! 
바벨 공식 홈페이지에 따르면 파일명이 꼭 <code>.babelrc</code> 일 필요는 없기 때문에 여기에 프리셋 설정을 추가하면 된다.</p>
<pre><code>module.exports = {
  presets: [
    &#39;@vue/cli-plugin-babel/preset&#39;,
    &#39;@babel/preset-env&#39; 
  ]
}</code></pre><p>다음으로 npm run start 명령어 스크립트를 바꿔준다. </p>
<p>node 대신 node-babel 을 쓰면 되는데 여기에 nodemon 을 쓰도록 수정하는 것도 같이 적용하자. </p>
<p><code>package.json</code> 파일 내 script.start 를 다음과 같이 변경한다.</p>
<pre><code>  &quot;scripts&quot;: {
    ...
    &quot;start&quot;: &quot;nodemon ./backend/bin/www --exec babel-node&quot;
  },</code></pre><p>다음은 <code>npm run start</code> !</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/a04c3f34-88d9-41a4-9b96-6e19829450e7/image.png" alt=""></p>
<p>?? import 구문을 못알아먹는다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/46d64ea1-9eab-4cae-8c2f-70e55f4f0ebb/image.png" alt=""></p>
<h2 id="하라는-대로-했는데-왜-안돼">하라는 대로 했는데 왜 안돼?</h2>
<p>왜 안되냐면 <code>backend/bin/www</code> 가 js 파일로 인식되지 않기 때문이다.
<code>www</code> 파일에 <code>.js</code> 확장자를 추가해주고 시작 명령어를 바꿔보자. </p>
<pre><code>mv backend/bin/www backend/bin/www.js</code></pre><p><code>package.json</code> 파일을 변경.</p>
<pre><code>  &quot;scripts&quot;: {
    ... 
    &quot;start&quot;: &quot;nodemon ./backend/bin/www.js --exec babel-node&quot;
  },</code></pre><p>다시 띄워보자. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/a236480a-dc4e-4c85-8848-4033566f5c73/image.png" alt=""></p>
<p>굿!✨</p>
<p>이제 백엔드 코드도 ES6 스타일로 작성할 수 있다.
끄읏 ☺️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue + Express 웹 사이트 만들기 (3) Express와 MySQL 연동]]></title>
            <link>https://velog.io/@cindy-choi/Vue-Express-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-Express%EC%99%80-MySQL-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@cindy-choi/Vue-Express-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-Express%EC%99%80-MySQL-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Wed, 13 Jan 2021 15:19:04 GMT</pubDate>
            <description><![CDATA[<p>드디어! 백엔드 작업을 한다! </p>
<h3 id="오늘의-할-일📋">오늘의 할 일📋</h3>
<p>☑️ mysql에 neti 데이터베이스 생성하기
☑️ 회의실 테이블 생성하고 데이터 삽입하기
☑️ express로 회의실 테이블 CRUD API 만들기</p>
<h1 id="mysql에-neti-데이터베이스-생성하기">mysql에 neti 데이터베이스 생성하기</h1>
<p>mysql 설치는 brew 로 하거나
<a href="https://dev.mysql.com/downloads/mysql/">공식 홈페이지</a>에서 Community server 를 받아 설치하면 된다. </p>
<p>mysql에 들어가서 </p>
<pre><code>mysql -uroot -p</code></pre><p>neti 라는 데이터베이스를 만들자.</p>
<pre><code>create database neti;</code></pre><h1 id="회의실-테이블-생성하고-데이터-넣기">회의실 테이블 생성하고 데이터 넣기</h1>
<p>만든 neti로 들어가서 </p>
<pre><code>use neti;</code></pre><p>예전 기억을 되살려 회의실 테이블을 만들었다. 
이름, 설명, 온라인 URL, 사용 여부 정도가 사용 될거고 나머지는 그냥 나만의 기본 셋이다. 😎</p>
<pre><code>CREATE TABLE IF NOT EXISTS `TBL_MEETING_ROOM`
(
  ID         BIGINT      NOT NULL AUTO_INCREMENT,
  NAME  VARCHAR(50) NOT NULL,
  DESCRIPTION  VARCHAR(50) NOT NULL,
  ONLINE_URL VARCHAR(125),
  USE_YN     Boolean,
  CREATED    DATETIME(6) NOT NULL,
  CREATOR    VARCHAR(20),
  UPDATED    DATETIME(6) NOT NULL,
  UPDATER    VARCHAR(20),
  PRIMARY KEY (ID)
)
DEFAULT CHARACTER SET utf8
COLLATE utf8_general_ci
ENGINE = INNODB;</code></pre><p>회의실은 자주 수정되는 항목이 아니라서 테스트겸 데이터도 미리 넣어주자.
4개만 넣어주자.</p>
<pre><code>INSERT INTO TBL_MEETING_ROOM (NAME, DESCRIPTION, ONLINE_URL, USE_YN, CREATED, CREATOR, UPDATED, UPDATER) VALUES (&#39;1번&#39;, &#39;1번 회의실&#39;, null, true, now(), &#39;admin&#39;, now(), &#39;admin&#39;);

INSERT INTO TBL_MEETING_ROOM (NAME, DESCRIPTION, ONLINE_URL, USE_YN, CREATED, CREATOR, UPDATED, UPDATER) VALUES (&#39;2번&#39;, &#39;2번 회의실&#39;, null, true, now(), &#39;admin&#39;, now(), &#39;admin&#39;);

INSERT INTO TBL_MEETING_ROOM (NAME, DESCRIPTION, ONLINE_URL, USE_YN, CREATED, CREATOR, UPDATED, UPDATER) VALUES (&#39;3번&#39;, &#39;3번 회의실&#39;, null, true, now(), &#39;admin&#39;, now(), &#39;admin&#39;);

INSERT INTO TBL_MEETING_ROOM (NAME, DESCRIPTION, ONLINE_URL, USE_YN, CREATED, CREATOR, UPDATED, UPDATER) VALUES (&#39;4번&#39;, &#39;4번 회의실&#39;, null, true, now(), &#39;admin&#39;, now(), &#39;admin&#39;);</code></pre><p>잘 들어갔나 볼까?</p>
<pre><code>SELECT * FROM TBL_MEETING_ROOM;</code></pre><p><img src="https://images.velog.io/images/cindy-choi/post/d30c30dc-f2be-4c6e-9285-10d1f449674d/image.png" alt=""></p>
<p>음~ 퍼펙 ✨</p>
<h1 id="express-에-데이터베이스-연동하기">express 에 데이터베이스 연동하기</h1>
<p>이제 본격적으로 express 에 이어붙여보자.
우선 npm의 mysql 패키지를 설치해주어야 한다. </p>
<pre><code>npm install --save mysql</code></pre><p>설치 뒤<code>backend/database.js</code> 파일 생성!</p>
<pre><code>const mysql = require(&#39;mysql&#39;);

const connection = mysql.createConnection({
  host     : &#39;localhost&#39;,
  user     : &#39;아이디&#39;,
  password : &#39;비밀번호&#39;,
  database : &#39;neti&#39;
});

connection.connect();

module.exports = connection;</code></pre><p>백엔드 구조 짜는데 익숙하지 않아서 일단 다 때려넣었다.</p>
<p>이렇게 만든 mysql 커넥션을 app.js 에서 사용하게 해주어야겠지?
<code>backend/app.js</code> 오픈</p>
<pre><code>const express = require(&#39;express&#39;);
const path = require(&#39;path&#39;);
const cookieParser = require(&#39;cookie-parser&#39;);
const logger = require(&#39;morgan&#39;);

// router
const indexRouter = require(&#39;./routes/index&#39;);
const usersRouter = require(&#39;./routes/users&#39;);
const meetingroomsRouter = require(&#39;./routes/meetingrooms&#39;);

// db
// import connection from &#39;./database&#39;;
const connection = require(&#39;./database&#39;);

const app = express();

app.use(logger(&#39;dev&#39;));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
// ui 안쓰니까 생략한다
// app.use(express.static(path.join(__dirname, &#39;public&#39;)));

// samples
app.use(&#39;/&#39;, indexRouter);
app.use(&#39;/users&#39;, usersRouter);

// meetingroom
app.use(&#39;/meetingrooms&#39;, meetingroomsRouter);

module.exports = app;</code></pre><p>이제 새로 추가하게 될 회의실 라우터(meetingroomsRouter)도 미리 추가해주었다.
기본으로 있던 샘플들도 일단 두자. </p>
<p><code>backend/routes/meetingrooms.js</code> 파일을 새로 만들자.
연결 테스트 단계이므로 READ(=GET) 만 만들어보겠다.</p>
<pre><code>const express = require(&#39;express&#39;);
const router = express.Router();

const connection = require(&#39;../database&#39;);
// import connection from &#39;../database&#39;;

// connection.connect();

/* GET meetingrooms listing. */
router.get(&#39;/&#39;, function(req, res, next) {

  // 쿼리 날려서 가져오기
  connection.query(&#39;SELECT * from TBL_MEETING_ROOM&#39;, (error, rows, fields) =&gt; {
    if (error) {
      console.error(error);
      res.status(500).send(&#39;Internal Server Error&#39;);
    }

    console.log(&#39;: &#39;, rows);
    res.send(rows);
  });
});

module.exports = router;</code></pre><p>쓰다보니 es6 문법이 안먹히더라... 내일은 이걸 해야겠다 마음먹으면서 <code>npm run start</code> 명령어로 서버를 띄우자. </p>
<p>테스트용이니까 거창하게 포스트맨을 띄우지 않고 curl 로 테스트하자.</p>
<pre><code>curl -v http://localhost:5051/meetingrooms | json_pp</code></pre><p><img src="https://images.velog.io/images/cindy-choi/post/4420a112-8c49-43c7-b446-b9e2faa3f8eb/image.png" alt=""></p>
<p>굿! 잘 나온당🍻</p>
<h1 id="이제-crud-테스트">이제 CRUD 테스트</h1>
<p>사실 의미가 없긴 하지만 이왕 R을 만든김에 CUD도 같이 만들어두자.</p>
<h2 id="create-post">Create (POST)</h2>
<pre><code>/* POST meetingrooms listing. */
router.post(&#39;/&#39;, function(req, res, next) {
  const params = req.body;
  const query = `INSERT INTO TBL_MEETING_ROOM (NAME, DESCRIPTION, ONLINE_URL, USE_YN, CREATED, CREATOR, UPDATED, UPDATER) VALUES
  (&#39;${params.name}&#39;, &#39;${params.description}&#39;, &#39;${params.onlineUrl}&#39;, 1, now(), &#39;admin&#39;, now(), &#39;admin&#39;);`;

  console.log(&#39;[POST] meetingrooms query: &#39;, query);

  connection.query(query, (error, result) =&gt; {
    if (error) {
      console.error(error);
      res.status(500).send(&#39;Internal Server Error&#39;);
    }

    console.log(&#39;[POST] meetingrooms result: &#39;, result);

    if (Number.isNaN(result.insertId) || result.insertId &lt; 0) {
      res.status(500).send(&#39;Item create failed.&#39;);
    }

    res.send({
      id: result.insertId,
    });
  });
});</code></pre><h2 id="update-put">Update (PUT)</h2>
<pre><code>/* PUT meetingrooms listing. */
router.put(&#39;/:id&#39;, function(req, res, next) {
  if (!req.params.id) {
    res.status(500).send(&#39;Id is not exist.&#39;);
    return;
  }

  const params = req.body;
  const query = `UPDATE TBL_MEETING_ROOM SET ${Object.keys(params).map(key =&gt; `${key} = &#39;${params[key]}&#39;`).join(&#39;,&#39;)}, updated = now() WHERE id = ${req.params.id};`;

  console.log(&#39;[PUT] meetingrooms query: &#39;, query);

  connection.query(query, (error, result) =&gt; {
    if (error) {
      console.error(error);
      res.status(500).send(&#39;Internal Server Error&#39;);
    }

    console.log(&#39;[PUT] meetingrooms result: &#39;, result);

    res.send({});
  });
});</code></pre><h2 id="delete-delete">Delete (DELETE)</h2>
<pre><code>/* DELETE meetingrooms listing. */
router.delete(&#39;/:id&#39;, function(req, res, next) {
  if (!req.params.id) {
    res.status(500).send(&#39;Id is not exist.&#39;);
    return;
  }

  const query = `DELETE FROM TBL_MEETING_ROOM WHERE id = ${req.params.id};`;
  console.log(&#39;[DELETE] meetingrooms query: &#39;, query);

  connection.query(query, (error, result) =&gt; {
    if (error) {
      console.error(error);
      res.status(500).send(&#39;Internal Server Error&#39;);
    }

    console.log(&#39;[DELETE] meetingrooms result: &#39;, result);

    res.send({});
  });
});</code></pre><p>테스트는 포스트맨으로 대충 완료함. </p>
<hr>
<p>이로써 백이랑 프론트의 기초 공사는 다 했다. </p>
<p>내일은 backend 소스도 es6를 사용할 수 있도록 babel을 추가하고, API 호출 시 사용되는 response /request 에 일정한 규칙을 부여해서 객체화 할 예정이다.😊</p>
<p>좀 더 우아한 소스를 쓸 수 있게 공부도 더 해야겠다 💪✨</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue + Express 웹 사이트 만들기 (2) Vuex 4.0 Migration]]></title>
            <link>https://velog.io/@cindy-choi/Vue-Express-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-Vuex-4.0-Migration</link>
            <guid>https://velog.io/@cindy-choi/Vue-Express-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-Vuex-4.0-Migration</guid>
            <pubDate>Wed, 13 Jan 2021 03:45:29 GMT</pubDate>
            <description><![CDATA[<p>vuex 신규 버전이 있다는 소식을 접했고 새 거 처돌이는 마이그레이션을 하기로 했다! </p>
<h2 id="마이그레이션-들어가기-전에">마이그레이션 들어가기 전에</h2>
<p>상대 경로 대신 <code>@</code> 로 기본 경로를 설정해서 소스 코드를 간결하게 하는 방법이 있다. </p>
<p><code>vue.config.js</code> 파일을 열어서 명시해주면 된다.</p>
<pre><code>
const path = require(&#39;path&#39;);

module.exports = {
  pages: {
    index: {
      // entry for the page
      entry: &#39;frontend/src/main.js&#39;,
      title: &#39;Index Page&#39;,
    },
  },

  configureWebpack: {
    resolve: {
      alias: {
        &#39;@&#39;: path.join(__dirname, &#39;frontend/src/&#39;)
      },
    },
  },
}</code></pre><p>이제부터는 아래쪽 resolve.alias.@ 가 frontend/src/ 경로와 매칭된다.</p>
<p>설정 변경 후 잘 되는지 테스트를 해보자. 
<code>frontend/src/App.vue</code> 파일을 열어 기본으로 import 하고 있던 Helloworld 경로를 수정한다.</p>
<pre><code>import HelloWorld from &#39;./components/HelloWorld.vue&#39;;
import HelloWorld from &#39;@/components/HelloWorld.vue&#39;;</code></pre><p>npm run serve 해보면 에러 없이 잘 된당😋</p>
<p>그럼 이제</p>
<h1 id="본격적으로-마이그레이션">본격적으로 마이그레이션!</h1>
<h2 id="vue3-설치하기">Vue3 설치하기</h2>
<p>Vuex 4.0을 사용하려면 Vue 역시 2.x에서 3.x 로 버전업해주어야 한다.
이전 작업에서 frontend 소스를 건드린게 없으니 과감하게 지워버리고 vue 3를 새로 설치한다. </p>
<pre><code># 혹시 모르니 백업
mv frontend frontend_v2</code></pre><pre><code># 새로 만들기
vue create frontend</code></pre><p>이 때 Vue3를 선택하자.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/86dcf840-95b1-46ae-ac31-7acc2c7bdd6f/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2012.00.12.png" alt=""></p>
<p>이전 글을 참조하여 사전 설정을 마친 다음, 다시 페이지를 띄워본다.</p>
<p>⚠️ Vue를 3.x로 업그레이드 하면 vue dev-tool도 새로 설치해주어야한다.</p>
<p>크롬 확장프로그램 공식 버전을 쓰고 있었는데 Vue 탭이 아무리 해도 안나오는 것이다. 
알아보니 Beta 버전을 받아서 사용해야 하는  거였음. </p>
<p><a href="https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd">이게 공식</a>이고 베타 버전 <a href="https://chrome.google.com/webstore/detail/vuejs-devtools/ljjemllljcmogpfapbkkighbhhppjdbg">다운로드 링크</a>는 여기.</p>
<p>베타라 그런지 기능이 별로 없다. 이 때 조금 마이그레이션을 후회했다😊
베타라니.. 이런 말 없었잖아요 </p>
<h2 id="vuex-4x-사용하기">Vuex 4.x 사용하기</h2>
<p>Vue를 3.x로 업그레이드 했다면 자연스럽게 vuex 4.x를 사용해야 한다. </p>
<h4 id="일단-설치하고">일단 설치하고</h4>
<pre><code>npm install vuex@next --save</code></pre><h4 id="store-생성">store 생성</h4>
<p><code>frontend/src/store/index.js</code> 파일을 만들고 내용을 채운다.</p>
<pre><code>import { createStore } from &#39;vuex&#39;;
import reservation from &#39;./modules/reservation&#39;;

// ⚠️ store 변수를 만들어서 export 한다는 점에 주의! 
export const store = createStore ({
    modules: { reservation },
});</code></pre><p>vuex4 는 vuex 모듈에서 <code>createStore</code>를 제공한다. 말 그대로 스토어 만들기!
<code>reservation</code> 모듈은 이제부터 만들거임. 원하는 이름으로 생성하자.</p>
<h4 id="reservation-모듈-만들기">reservation 모듈 만들기</h4>
<p><code>frontend/src/store/modules/reservation/index.js</code> 파일을 만들고 아래처럼 state, getters, actions, mutaions를 정의한다.</p>
<pre><code>import MUTATIONS from &#39;./mutation&#39;;

const state = {
  reservations: [],
};

const getters = {
  reservations: (_state) =&gt; {
    return _state.reservations;
  }
};

const actions = {
};

const mutations = {
  [MUTATIONS.SET_RESERVATIONS]: (_state, list) =&gt; {
      _state.reservations = list;
  }
};

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations,
};</code></pre><p><code>frontend/src/store/modules/reservation/mutation.js</code> 도 만들어서 아래처럼 테스트용으로 추가했다.</p>
<pre><code>const MUTATIONS = {
  SET_RESERVATIONS: &#39;setReservations&#39;,
};

export default MUTATIONS;</code></pre><h4 id="만든-store-를-mainjs-에서-가져다-쓰자">만든 store 를 main.js 에서 가져다 쓰자</h4>
<p><code>frontend/src/main.js</code> 를 다음처럼 수정해서 방금 만든 store를 사용하게 한다.</p>
<pre><code>import { createApp } from &#39;vue&#39;
import { store } from &#39;./store&#39;;

import App from &#39;./App.vue&#39;

const app = createApp(App);

app.use(store);
app.mount(&#39;#app&#39;);</code></pre><h4 id="이제-컴포넌트화면에서-써보자">이제 컴포넌트(화면)에서 써보자.</h4>
<p>기본으로 생성되어있던 HelloWorld.vue 컴포넌트에 테스트를 해보겠다.</p>
<p><code>frontend/src/components/HelloWorld.vue</code> 오픈!</p>
<pre><code>&lt;script&gt;
import { createNamespacedHelpers } from &#39;vuex&#39;;
import MUTATIONS from &#39;@/frontend/src/store/modules/reservation/mutation&#39;;

const { mapGetters, mapMutations } = createNamespacedHelpers(&#39;reservation&#39;);

export default {
  name: &#39;HelloWorld&#39;,
  props: {
    msg: String,
  },
  computed: {
    ...mapGetters({
      list: &#39;reservations&#39;,
    }),
  },

  methods: {
    ...mapMutations({
      setList: MUTATIONS.SET_RESERVATIONS,
    }),
  },
}
&lt;/script&gt;</code></pre><p>아까 만든 reservation 모듈을 이어붙였다.</p>
<p>이번엔 변경도 되는지 확인해볼것이다. template 영역에 list를 찍어주고, 버튼 하나를 만들어서 수정해보자.</p>
<pre><code>&lt;template&gt;
  &lt;div class=&quot;hello&quot;&gt;
    &lt;h1&gt;{{ msg }}&lt;/h1&gt;
    &lt;div&gt;
       &lt;!-- 찍어보기용 --&gt;
       {{ list }}

       &lt;!-- 바꿔보기용 --&gt;
       &lt;input type=button @click=&quot;handleTest&quot; /&gt;
    &lt;/div&gt;
    ...</code></pre><p>handleTest 함수도 추가해주었다.</p>
<pre><code>  methods: {
    ...mapMutations({
      setList: &#39;setReservations&#39;,
    }),
    handleTest() {
      // mutation 함수를 호출해서 list 배열에 아무 데이터나 넣어주었다.
      this.setList([&#39;hi&#39;, &#39;i\&#39;m&#39;, &#39;sena&#39;,]);
    },
  },</code></pre><h4 id="확인의-시간">확인의 시간!</h4>
<p>  자 이제 npm run serve 를 때려서 확인해보자! 👻</p>
<p>  <img src="https://images.velog.io/images/cindy-choi/post/977f0eef-5f67-4594-a41a-693946dff5ae/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2012.37.45.png" alt=""></p>
<p>  zㅋㅋㅋ 정말 못생겼네... reservation store 의 state 초기 값인 <code>[]</code> 빈 배열이 잘 찍히고 있다. </p>
<p>  혹시나 싶으니 vue dev-tool 로도 찍어본다. </p>
<p>  <img src="https://images.velog.io/images/cindy-choi/post/155b53a2-8d02-47b7-aab1-234431e6c013/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2012.38.03.png" alt=""></p>
<p>  굿굿! </p>
<p>  이젠 버튼을 눌러서 handleTest() 함수를 호출해보자. </p>
<p>  <img src="https://images.velog.io/images/cindy-choi/post/738daf70-2dbb-4a4d-9a0d-4e4a8f026b32/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2012.38.13.png" alt=""></p>
<p>  <img src="https://images.velog.io/images/cindy-choi/post/494efbfb-2f14-423b-afbd-3fb542ffe57f/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2012.38.26.png" alt=""></p>
<p>  음~ 잘 바뀌었다.</p>
<p>  오늘은 이서 끝! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue + Express 웹 사이트 만들기 (1) Initial setting]]></title>
            <link>https://velog.io/@cindy-choi/Vue-Express-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-Initial-setting</link>
            <guid>https://velog.io/@cindy-choi/Vue-Express-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-Initial-setting</guid>
            <pubDate>Tue, 12 Jan 2021 17:51:09 GMT</pubDate>
            <description><![CDATA[<p>친구랑 같이 4주짜리 미니 해커톤을 진행하기로 했다.
오랜만에 백엔드 작업을 하려니 왕 설레~</p>
<p>우선 작은 규모의 프로젝트이니만큼 하나의 repo 에 client와 server 소스를 함께 두어 관리 포인트를 줄이기로 했다. </p>
<h4 id="뭐든-처음-시작하는게-어렵지어떻게-시작할까">뭐든 처음 시작하는게 어렵지...어떻게 시작할까?</h4>
<ol>
<li><p>github 에서 boilerplate 찾기
👉 원하는 조합으로 원하는 구성을 가진 템플릿이 없었다.</p>
</li>
<li><p>vue + express 로 검색해서 튜토리얼 따라하기
👉 공통적으로 사용하는 <a href="https://mrw0119.tistory.com/136">이것</a>.
많이 찾아봤지만 한글로 된 튜토리얼은 대부분 이 내용이었다.
그런데 보다 보니 서버를 띄울 때 webpack의 dev server로 띄우는 것 같았다. 음, 내가 원하는 구조는 아니라서 패스.</p>
</li>
<li><p>더 찾기 귀찮아서 걍 때려 만들기로.</p>
</li>
</ol>
<h1 id="오늘의-목표-📝">오늘의 목표 📝</h1>
<p>☑️ git repo 만들기
☑️ neti 디렉토리 안에 frontend (vue.js) 구성하기
☑️ neti 디렉토리 안에 backend (express) 구성하기
☑️ npm run serve 명령어로 frontend 띄우기
☑️ npm run start 명령어로 backend 띄우기
☑️ git repo 연동하기</p>
<hr>
<h2 id="git-repository-만들기">Git Repository 만들기</h2>
<p><img src="https://images.velog.io/images/cindy-choi/post/356da232-53ec-41af-8dca-d36d8c5d4671/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.44.51.png" alt=""></p>
<p>뉴 버튼 누르고</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/1c4a9fc1-526c-483c-a8f2-b84fedcd5122/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.43.59.png" alt=""></p>
<p>하나 팠다. </p>
<p><code>.gitignore</code> template 를 선택할 때 대체 뭘 선택해야 하는지 헷갈렸는데... 결론은 <strong>아무거나 해도 상관 없다</strong>.
처음 생성 했을 때 &quot;<code>.gitignore</code> 파일 안에 기본값을 뭘로 넣어줄까?&quot; 하는 것이므로, <a href="https://ko.wikipedia.org/wiki/%EA%B8%80%EB%A1%9C%EB%B8%8C_(%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D)">Glob 패턴</a>을 사용하는 것은 마찬가지이다. </p>
<h2 id="frontend-vuejs-구성하기">frontend (vue.js) 구성하기</h2>
<p>우선 vue-cli 명령어로 neti 폴더를 만듦과 동시에 프론트엔드도 구성하자.</p>
<pre><code>vue create neti</code></pre><p>프리셋은 그냥 많이 써본걸로 선택했다. vue2랑 eslint. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/13e20eed-cbf0-48d4-b9d8-5ccf347cfec8/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-12%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.28.15.png" alt=""></p>
<p>neti 경로가 생겼다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/9cc99877-a74b-47ba-9c50-6d0e9a733c27/image.png" alt=""></p>
<p>여기서 끝이 아니다.
코드를 깔끔하게 관리하려면 frontend / backend 디렉토리를 나누어야지! </p>
<p>frontend 폴더를 만들고 클라이언트 코드를 옮겨주자. </p>
<pre><code>mkdir frontend

mv src frontend/.
mv public frontend/.</code></pre><p>나머지를 neti/ 하위에 남겨두는 이유는 그냥.. 관리 편할것 같아서.
<img src="https://images.velog.io/images/cindy-choi/post/daba2cea-d737-4d42-a34a-e858577a5f2a/image.png" alt=""></p>
<p>옮겨주고 나면 이런 형상이 된다. </p>
<p>📌  <strong>포트 번호 바꾸기</strong>
내 경우 이미 8080 포트를 사용 중인 프로젝트가 있어서 5050, 5051 포트를 사용하도록 변경해줄 필요가 있었다. </p>
<p><code>package.json</code> 파일을 열어서 serve 명령어에 <code>--port</code> 옵션을 추가해주자.</p>
<pre><code>&quot;serve&quot;: &quot;vue-cli-service serve --port 5050&quot;,</code></pre><p>여기까지 한 다음 <code>npm run serve</code> 명령어로 웹 페이지를 띄우면!</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/1a309b9d-9154-4f76-bcb6-bfb03db896ce/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.04.23.png" alt=""></p>
<p>에러가 난다. 💣</p>
<p>말하자면 소스코드들이 단체로 frontend 안으로 이사를 가버렸는데 vue-cli 한테 알려주지 않은 꼴이라 진입점을 찾지 못한 것이다.</p>
<p>이 문제를 해결하려면 <code>vue.config.js</code> 파일로 소스코드가 어디있는지 vue-cli 에게 알려주어야 한다.</p>
<p><code>vi vue.config.js</code> 파일을 만들고 다음과 같이 파일 내용을 작성하자.
<a href="https://cli.vuejs.org/config/#pages">공식 홈페이지</a>를 참조했다.</p>
<pre><code>module.exports = {
  pages: {
    index: {
      // entry for the page
      entry: &#39;frontend/src/main.js&#39;,
      title: &#39;Index Page&#39;,
    },
  }
}</code></pre><p>그런 다음 <code>npm run serve</code>  명령어를 다시 사용하면,</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/d0962d27-dd4f-42da-b8bf-585224569285/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.07.43.png" alt=""></p>
<p><img src="https://images.velog.io/images/cindy-choi/post/c744e392-bb2b-4e2b-8957-6b2a525553a4/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.07.58.png" alt=""></p>
<p>굿. 👍✨</p>
<h1 id="backend-설정하기">backend 설정하기</h1>
<p>프론트 개발자는 익스프레스가 없어... express를 전역으로 설치하자.</p>
<pre><code>sudo npm install express-generator -g</code></pre><p>express 로 서버 만들기를 검색하면 다들 <code>express (폴더명) --view=pub</code> 이렇게 쓰던데 난 view를 안쓸거라서 생략했다. (우린 vue로 만들거니까)</p>
<pre><code>express (폴더명) --no-view</code></pre><p>그러면 아래처럼 backend 경로가 생기고 안에 새로운 프로젝트가 구성된다. 
<img src="https://images.velog.io/images/cindy-choi/post/f40de2b5-0d7c-45e0-8cf2-45c63d418914/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/cindy-choi/post/a7fbac0d-f790-4e64-8337-bba03dd26b11/image.png" alt=""></p>
<p>여기에도 package.json 이 생겼는데 난 package.json 을 한군데서만 관리하고 싶단 말이야..
<code>neti/backend/package.json</code> 파일을 열어서 필요한 내용만 복사하여 <code>neti/package.json</code> 에 붙여넣은 다음 <code>neti/backend/package.json</code> 을 지워버리자.</p>
<p>scripts의 start, dependencies 의 debug, express, morgan 이것만 옮겨주면 된다. 
옮겨주고 난 뒤의 package.json 최종 형상👇</p>
<pre><code>{
  &quot;name&quot;: &quot;neti&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;serve&quot;: &quot;vue-cli-service serve&quot;,
    &quot;build&quot;: &quot;vue-cli-service build&quot;,
    &quot;lint&quot;: &quot;vue-cli-service lint&quot;,
    &quot;start&quot;: &quot;node ./backend/bin/www&quot;
  },
  &quot;dependencies&quot;: {
    &quot;core-js&quot;: &quot;^3.6.5&quot;,
    &quot;vue&quot;: &quot;^2.6.11&quot;,
    &quot;cookie-parser&quot;: &quot;~1.4.4&quot;,
    &quot;debug&quot;: &quot;~2.6.9&quot;,
    &quot;express&quot;: &quot;~4.16.1&quot;,
    &quot;morgan&quot;: &quot;~1.9.1&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@vue/cli-plugin-babel&quot;: &quot;~4.5.0&quot;,
    &quot;@vue/cli-plugin-eslint&quot;: &quot;~4.5.0&quot;,
    &quot;@vue/cli-service&quot;: &quot;~4.5.0&quot;,
    &quot;babel-eslint&quot;: &quot;^10.1.0&quot;,
    &quot;eslint&quot;: &quot;^6.7.2&quot;,
    &quot;eslint-plugin-vue&quot;: &quot;^6.2.2&quot;,
    &quot;vue-template-compiler&quot;: &quot;^2.6.11&quot;
  },
  &quot;eslintConfig&quot;: {
    &quot;root&quot;: true,
    &quot;env&quot;: {
      &quot;node&quot;: true
    },
    &quot;extends&quot;: [
      &quot;plugin:vue/essential&quot;,
      &quot;eslint:recommended&quot;
    ],
    &quot;parserOptions&quot;: {
      &quot;parser&quot;: &quot;babel-eslint&quot;
    },
    &quot;rules&quot;: {}
  },
  &quot;browserslist&quot;: [
    &quot;&gt; 1%&quot;,
    &quot;last 2 versions&quot;,
    &quot;not dead&quot;
  ]
}</code></pre><p>⚠️ script의 start를 복사할 때 경로를 <code>node ./bin/www</code> 에서 <code>node ./backend/bin/www</code> 로 변경해야 한다는 점을 주의하자.</p>
<p>다 옮긴 다음에는 포트 번호를 바꿔주자.
<code>./backend/bin/www</code> 파일을 열어 15 line 의 포트 정보를 변경해주면 된다. </p>
<pre><code>vi ./backend/bin/www</code></pre><p>5050은 frontend, 5051은 backend 로 정했다.</p>
<pre><code> 15 var port = normalizePort(process.env.PORT || &#39;5051&#39;);
 16 app.set(&#39;port&#39;, port);</code></pre><p>설정이 끝난 것 같으니 서버도 띄워보자.</p>
<pre><code>npm run start</code></pre><p><img src="https://images.velog.io/images/cindy-choi/post/31dc3dad-069e-45db-aa7e-b96f0c0f518b/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.12.45.png" alt=""></p>
<p>이렇게 에러 없이 뜨면 잘 뜬 것이다. 
아까 바꾼 포트로 express 페이지도 접근해보면. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/cbe1bc78-08d3-4117-969f-1781700376d1/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-01-13%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.16.24.png" alt=""></p>
<p>잘 나오네. 굿✨</p>
<hr>
<h1 id="git-repo-연동하기👻">Git repo 연동하기👻</h1>
<p>오늘은 이까지 하고 작업한 것들을 처음에 만들어둔 git repo에 올려보자.</p>
<h3 id="1-remote-연결하기">1. remote 연결하기</h3>
<p>vue create 명령어가 자동으로 .git 과 .gitignore 파일을 생성해둔 것 같았다.
혹시나 연결된 정보가 있는지 확인해본다.</p>
<pre><code>git remote -v</code></pre><p>위 명령어를 쳐서 아무것도 나오지 않으면 연결 정보가 아직 없는 것이다. </p>
<pre><code>git init</code></pre><p>한번 해주고</p>
<pre><code># git remote add (remote 이름) (repo 주소)
git remote add origin https://github.com/cindy-choi/neti.git</code></pre><p>git remote 명령어로 원격 저장소를 연결해준다. 보통 이름은 origin을 씀.</p>
<h4 id="2-수정사항-커밋하기">2. 수정사항 커밋하기</h4>
<pre><code>git add --all</code></pre><p>처음이니까 놓치는 것이 없도록 걍 전체 저장을 한다.
이후 작업 사항을 저장할 때에는 --all(-A) 옵션은 지양하자. 나도 모르는 파일이 추가 되어버리면 지우기 귀찮으니까.</p>
<pre><code>git commit -m &#39;Initial setting.&#39;</code></pre><p>메세지와 함께 커밋을 하고</p>
<pre><code>git push origin master</code></pre><pre><code>git push --set-upstream origin dev</code></pre><p>master 또는 dev 브랜치를 생성하여 푸시한다.</p>
<hr>
<p>오늘은 여기까지!
내일은 테스트용 REST API 를 만들고 웹에서 이를 호출해볼것이다.😊</p>
<p>진짜진짜 끝!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[VUE] 🌱우아한 프로젝트 구조 짜기]]></title>
            <link>https://velog.io/@cindy-choi/VUE-%EC%9A%B0%EC%95%84%ED%95%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0-%EC%A7%9C%EA%B8%B0</link>
            <guid>https://velog.io/@cindy-choi/VUE-%EC%9A%B0%EC%95%84%ED%95%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0-%EC%A7%9C%EA%B8%B0</guid>
            <pubDate>Mon, 11 Jan 2021 14:59:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>엔터프라이즈라고 부르기엔 구조가 좀 협소하지 않나?</p>
</blockquote>
<p>누군가 그렇게 물었고 반년이 지났다.
너무너무 신경쓰인다. &gt;협소한 구조&lt; 란 대체 무엇이며 우아한 프로젝트의 구조란 무엇인가?</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/9cc4d249-0a70-4ecc-9163-ffc2375aca14/image.png" alt="우아한?"></p>
<h1 id="vue-공식-프로젝트-구조부터-뜯어보자">vue 공식 프로젝트 구조부터 뜯어보자.</h1>
<p>vue cli 로 자동 생성한 프로젝트의 구조를 보면 vue 가 어떤 방식의 구조를 선호하는지 알 수 있지 않을까?</p>
<pre><code>vue create helloworld</code></pre><p>생성해보았다.
<img src="https://images.velog.io/images/cindy-choi/post/d9b096ed-229e-4d9d-886f-25341dee7fe7/image.png" alt=""></p>
<p>놀라울 정도로 아무 것도 없었다. </p>
<p>조금 더 공식 사이트를 둘러보았고 VUEX 에서 살짝 언급된 프로젝트 구조는 다음과 같았다. 
<img src="https://images.velog.io/images/cindy-choi/post/6e66eb39-83be-46ce-86bb-cc6a2e6b76d8/image.png" alt=""></p>
<p>역시 별 건 없었다. </p>
<h1 id="boilerplate를-분해해보자">Boilerplate를 분해해보자.</h1>
<p> vue 공식이 주지 않는 &#39;우아한 구조&#39;에 대한 단서는 Github 에서 찾기로 한다. vue, boilerplate 키워드로 검색해서 Star 수가 2k 이상인 repo를 기준으로 찾아보았다. </p>
<h2 id="vue-enterprise-boilerplate">vue-enterprise-boilerplate</h2>
<p>⭐️ Star 6.6k
🤖 Link <a href="https://github.com/chrisvfritz/vue-enterprise-boilerplate">github 가기</a></p>
<p> <img src="https://images.velog.io/images/cindy-choi/post/adb963eb-c0f0-4ca2-875a-2b11aecec373/image.png" alt=""></p>
<p>그동안 내가 참여했던 프로젝트 A와 비교해보니 몇 가지를 제외하고는 대부분 일치했다.</p>
<ul>
<li><p>views/ 디렉토리가 src/의 하위가 아닌 router/의 하위에 있음
→   views 는 router에 밀접히 연관되어 해당하는 화면 소스코드를 내포한다는 점을 좀 더 명확히 할 수 있다.</p>
</li>
<li><p>layout/ 디렉토리가 별도로 존재   →   레이아웃의 중요도가 높아지는 효과!</p>
</li>
</ul>
<p>소스코드 파일에 <a href="https://vuejs.org/v2/style-guide/#Component-files-strongly-recommended">vue의 컴포넌트 작명 규칙</a>이 강하게 적용되어있는 것도 참고하면 좋을 것 같다.</p>
<h2 id="vue-express-mongo-boilerplate">vue-express-mongo-boilerplate</h2>
<p>⭐️ Star 2.7k
🤖 Link <a href="https://github.com/icebob/vue-express-mongo-boilerplate">github 가기</a></p>
<p>Server side 소스도 포함하고 있어서 Client 쪽만 열어보았다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/48eb289b-594e-462e-81f8-b6a0413891c2/image.png" alt=""></p>
<p>vue의 기본 구조와는 꽤 많이 다른 것 처럼 보이지만 자세히 보면 결국 비슷한 양상을 띈다. </p>
<p>최상단의 app/ 디렉토리가 client의 메인 진입점이고, 그 하위에는 core/가 있는데 이게 우리가 보통 사용하는 src/ 디렉토리와 동일하다.</p>
<p>views/ 디렉토리가 별도로 없고 화면 파일(DefaultAdminPage)이 메인 경로에 덜렁 놓여있는 것은 마음에 안들지만 이를 개선한다면 심플하면서도 명확한 구조 분리라고 생각된다.</p>
<h2 id="vuesion">vuesion</h2>
<p>⭐️ Star 2.2k
🤖 Link <a href="https://github.com/vuesion/vuesion">github 가기</a></p>
<p>Storybook과 Mocha가 기본적으로 적용 된, (뒤에 나오겠지만) 큰 대규모 프로젝트에 걸맞는 구조이다. 유행하는 기술을 잘 집약해놓은 느낌? TS를 쓰는 것 중에는 가장 Star 수가 높았다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/6dea7ab6-6735-4f68-91ea-7e6f2078f838/image.png" alt=""></p>
<p>개인적으로는 찾아본 구조 중에 가장 복잡했다. (app안의 app안의 App이라니!)</p>
<p>App.vue 쪽이 진입점이고 example, home 디렉토리가 하나의 모듈이다.</p>
<p>각 모듈마다 vuex 소스가 분리되어 있고, 한 페이지는 무조건 동일한 이름의 디렉토리 하위에 파일로서 생성된다. Mocha의 spec, Storybook의 stories 파일과 함께 놓기 위함인듯하다.</p>
<h3 id="boilerplate-종합-결과">Boilerplate 종합 결과</h3>
<p>가장 Vue의 오리지널 규칙을 잘 따른 쪽은 <strong>vue-enterprise-boilerplate</strong> 이고 최신 기술이 잘 적용 된, 보다 더 큰 규모에 어울리는 쪽은 <strong>vuesion</strong> 이었다. </p>
<p>개인적으로 새로운 프로젝트를 하게 되면 vuesion을 기반으로 진행해보고 싶다.</p>
<h1 id="규칙-찾아보기">규칙 찾아보기</h1>
<p>vue 공식이 제안한 프로젝트 구조와 유명한 오픈소스들의 구조를 뜯어보았다. 생각보다 다양한 스타일이 있었는데 잘 보면 공통된 규칙들이 있다. 조금 더 리서치를 해보았다. </p>
<h2 id="반복적으로-사용되는-규칙들">반복적으로 사용되는 규칙들</h2>
<h3 id="규칙1-컴포넌트의-이름은-multi-word를-사용한다">규칙1. 컴포넌트의 이름은 Multi-word를 사용한다.</h3>
<p>단일 단어(Card, Modal, Button)로 이름지으면 이름이 중복될 가능성이 있으며 명료하지 않다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/299ab0f3-d6da-47b4-b44d-120bf4812fe6/image.png" alt=""></p>
<h3 id="규칙2-부모-컴포넌트와-강하게-엮여있는-컴포넌트는-부모-컴포넌트의-이름을-prefix로-사용한다">규칙2. 부모 컴포넌트와 강하게 엮여있는 컴포넌트는 부모 컴포넌트의 이름을 prefix로 사용한다.</h3>
<p>일반적으로 강하게 엮인 부모-자식 컴포넌트는 아래처럼 nested 형태를 취하여 처리하지 않는가?</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/ebdca604-6f08-4ae8-a90e-b02d6ad6284b/image.png" alt=""></p>
<p>이 방식은 Vue 공식 사이트에서 권장하지 않는다. 코드 편집기의 성능(또는 기능)에 영향을 미치기 때문이란다.</p>
<ul>
<li>비슷한 이름을 가진 파일이 너무 많으면 단축키로 파일간의 이동이 어려워진다. </li>
<li>하위 경로로 엮인 구조가 늘어나면 IDE의 사이드바에서 컴포넌트들을 브라우징하는데 더 많은 시간이 걸릴 수 있다. </li>
</ul>
<p>그러므로 아래와 같이 부모 컴포넌트의 이름을 prefix로 가진 네이밍을 권장한다.
<img src="https://images.velog.io/images/cindy-choi/post/16dc6064-129e-4d0e-907c-4349b3098ef9/image.png" alt=""></p>
<h3 id="규칙3-전체-페이지에서-유일하게-사용하는-컴포넌트에는-the-를-붙인다">규칙3. 전체 페이지에서 유일하게 사용하는 컴포넌트에는 The 를 붙인다.</h3>
<p><img src="https://images.velog.io/images/cindy-choi/post/ff263f81-ec96-49d2-b6f3-67676e1f9d12/image.png" alt=""></p>
<p>Header, Footer 의 경우는 보통 모든 페이지를 통틀어 단 하나의 인스턴스만 생성된다. 이 때에는 <em>The</em> Prefix를 붙여 명시해주는 것이 좋다. </p>
<p>이렇게 사용되는 컴포넌트는 보통 props를 사용하지 않는 것이 좋다.</p>
<h1 id="bonus-💰">Bonus 💰</h1>
<h2 id="프로젝트-규모-별-소스-구조-추천">프로젝트 규모 별 소스 구조 추천</h2>
<p>리서치를 하다보니 vue 공식 문서에는 언급되지 않은 노하우들이 많이 보였는데 그 중 앞단에서 찾아보았던 boilerplate와 연관된 내용이 있어 가져와보았다.</p>
<h4 id="여기-당신의-작고-소중한-프로젝트가-있었다">여기 당신의 작고 소중한 프로젝트가 있었다.</h4>
<p>기능이 추가되고 유지보수 되면서 프로젝트 규모는 점차 커진다. 공통 모듈이라 생각해서 components에 빼놓은 컴포넌트들이 쌓이기 시작한다. 이를 정리하지 않으면 끝내는 다음과 같은 상태가 된다. </p>
<p>** 사방에서 쓰는 (또는 쓰이는, 아니면 어디서 쓰는지 모르는!) 모든 컴포넌트들이 components/ 디렉토리에 널려있음. **</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/c379f486-4a64-4b61-88f3-00726f5dbc85/image.png" alt=""></p>
<p>예시에는 파일 개수가 얼마 되지 않지만 components/ 경로 안에 한.. 100개의 컴포넌트가 널려있다고 생각해보자. 상상만으로도 끔찍하다. 그걸 다 언제 파악하고 재사용하며 유지보수를 할 것인지! </p>
<p>규모가 작은 프로젝트라면 지금 상태를 유지해도 괜찮다. 
단, 파일명 네이밍 규칙을 잘 지켰을 경우에만. (부모-자식간의 prefix 규칙)</p>
<h4 id="조금-커진-프로젝트가-있다">조금 커진 프로젝트가 있다.</h4>
<p>한 눈에 용도를 알아볼 수 없을 정도의 규모까지 도달한 프로젝트라면 다음과 같이 개선해볼 수 있겠다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/036c024c-8f54-4f6c-b074-8481850ea049/image.png" alt=""></p>
<p>하위 컴포넌트를 엮어 디렉토리 구조를 만들었다. 
이제 용도 별로 components를 한 눈에 알아볼 수 있게 되었다. </p>
<blockquote>
<p>위에서 언급했던 규칙2에 어긋나는 개선 방향이기 때문에 components를 한 눈에 알아보기 힘들 정도로 규모가 불어난 프로젝트에 적용하도록 하자.</p>
</blockquote>
<h4 id="여기서-더-커지면요">여기서 더 커지면요?</h4>
<p>위에서는 AvatarCard, Notification 을 예로 들었지만 상위 모듈이 컴포넌트 단위가 아닌 기능 단위로 커진 상황을 가정해보자. </p>
<p>내가 실제로 진행한 프로젝트는 Enterprise 급으로 분리된 화면 하나가 App 하나의 규모였었다. 이렇게 엄청나게 커다란 규모의 프로젝트에서는 어떤 구조를 차용해야 할까?</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/e0779e56-5662-41ba-93d6-7abfe4a02746/image.png" alt=""></p>
<p>바로 모듈 별로 경로를 쪼갠 다음, 하나의 모듈이 작은 프로젝트 단위라고 생각하고 하위 경로를 구성하는 것이다. 
위 예시를 보면 browser, dashboard, tableManager 가 하나의 모듈이자 작은 프로젝트 단위가 된다. </p>
<p>여기서 조금 더 모듈에게 독립성을 부여해보자. components뿐 아니라 모듈 내에서 반복되는 유틸리티나 helper, 서비스 코드를 분리하는 것이다.</p>
<p><img src="https://images.velog.io/images/cindy-choi/post/bb7c5710-1b32-481b-8f72-569add02bba0/image.png" alt=""></p>
<p>잠깐. 어디서 많이 본 구조가 아닌가?😗</p>
<p>위에서 살펴본 Boilerplates 중 <strong>vuesion</strong> 을 떠올려보자. 각 모듈마다 vuex를 가진 구조가 복잡하게 느껴졌지만, 여기까지 와서 보니 꽤 그럴싸한 구조였던 것 같다. </p>
<p>vuesion 을 떠올려보며 아예 api, router, store(vuex) 까지도 별도로 분리해준다면 다음과 같이 완성할 수 있을 것이다. </p>
<p><img src="https://images.velog.io/images/cindy-choi/post/19246a64-d190-41c5-9d51-0f296db01a58/image.png" alt=""></p>
<p>browser/ 모듈이 아주 작은 단위의 프로젝트와 똑같은 구성을 갖추게 되었다. 
이렇게 분리가 된다면 대규모 프로젝트에서 전체 모듈이 공용으로 사용하곤 하는 components/ 와 같은 경로를 더욱 깔끔하게 관리할 수 있을 것이다. </p>
<p>끝!😤</p>
]]></description>
        </item>
    </channel>
</rss>