<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>seo__namu.log</title>
        <link>https://velog.io/</link>
        <description>주니어 프론트엔드 개발자</description>
        <lastBuildDate>Fri, 12 May 2023 10:51:28 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>seo__namu.log</title>
            <url>https://velog.velcdn.com/images/seo__namu/profile/3264fa86-761f-45b0-a6c7-2b28a63458a4/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. seo__namu.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/seo__namu" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Token Transformer 사용하기]]></title>
            <link>https://velog.io/@seo__namu/Token-Transformer-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-u6uwunqe</link>
            <guid>https://velog.io/@seo__namu/Token-Transformer-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-u6uwunqe</guid>
            <pubDate>Fri, 12 May 2023 10:51:28 GMT</pubDate>
            <description><![CDATA[<p>Token Transformer는 Figma Tokens Studio plugin에서 추출된 <code>json</code> 파일을 변환하는 도구이다.</p>
<p>그런데 <a href="https://www.npmjs.com/package/token-transformer">공식문서</a>가 그닥 친절하지 않다. <a href="https://github.com/tokens-studio/figma-plugin/tree/main/token-transformer">GitHub 저장소</a>에 옵션별 결과 파일들이 있긴 하지만 어떤 명령어에 대한 결과 파일인지 직접 확인해봐야 한다. <del>솔직히 말하면 공식문서라고 하기도 애매함</del></p>
<p>그래서 직접 옵션들을 바꿔가면서 테스트를 해봤고, 각 옵션에 대해 설명하고자 한다.</p>
<h2 id="명령어로-실행하기">명령어로 실행하기</h2>
<p>패키지를 설치해서 실행해도 되고, npx로 바로 실행시킬 수 있다.</p>
<blockquote>
<p>npx token-transformer input.json output.json sets excludes</p>
</blockquote>
<p>실행 명령어는 간단하며, 파라미터에 대해 알아보자.</p>
<h3 id="파라미터">파라미터</h3>
<p>기본적으로 변환 작업에 대한 정의를 하는데 사용되는 파라미터들이다.</p>
<ul>
<li>input.json : 변환할 대상 파일</li>
<li>output.json : 추출할 대상 파일 (존재하지 않으면 생성함)</li>
<li>sets : 변환시 참조할 대상 key값</li>
<li>excludes : 추출 제외 대상 key값</li>
</ul>
<p><code>sets</code>와 <code>excludes</code>에 대해서는 조금 더 자세한 설명이 필요하다.</p>
<pre><code class="language-json">{
  &quot;global&quot;: {...},
  &quot;light&quot;: {
    &quot;bg&quot;: {
      &quot;default&quot;: {
        &quot;value&quot;: &quot;{colors.white}&quot;,
        &quot;type&quot;: &quot;color&quot;
      }
    }
  },
  &quot;dark&quot;: {
    &quot;bg&quot;: {
      &quot;default&quot;: {
        &quot;value&quot;: &quot;{colors.gray.900}&quot;,
        &quot;type&quot;: &quot;color&quot;
      }
    }
  }
}</code></pre>
<p>여기에 input에 사용될 tokens.json이 있다. light, dark는 global의 colors의 값을 참조해서 값을 설정하고 있다.</p>
<p>만약 특정 테마만 변환해서 추출할 때, global을 참조하도록 설정하는 것이 필요하다.</p>
<blockquote>
<p><strong>light 테마만 추출하기</strong>
npx token-transformer input.json light.json global,light global</p>
</blockquote>
<p>global과 light을 참조해서 추출하되, global은 추출하지 말라는 명령어다.</p>
<blockquote>
<p><strong>dark 테마만 추출하기</strong>
npx token-transformer input.json dark.json global,dark global</p>
</blockquote>
<p>dark 테마도 light와 동일하게, global을 참조하되 global은 추출하지 않는다.</p>
<h3 id="옵션">옵션</h3>
<p>변환시 사용할 수 있는 옵션들이다. <code>--</code> prefix로 시작하며 <code>=</code>로 값을 대입해준다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>기본 값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>expandTypography</td>
<td>false</td>
<td>Typography 유형의 자동 확장을 활성화한다.</td>
</tr>
<tr>
<td>expandShadow</td>
<td>false</td>
<td>boxShadow 유형의 자동 확장을 활성화한다.</td>
</tr>
<tr>
<td>expandBorder</td>
<td>false</td>
<td>stroke 유형의 자동 확장을 활성화한다.</td>
</tr>
<tr>
<td>preserveRawValue</td>
<td>false</td>
<td>원시 값을 가지는 rawValue를 추가한다.</td>
</tr>
<tr>
<td>throwErrorWhenNotResolved</td>
<td>false</td>
<td>참조 해결되지 않을 시 오류를 사용하지 않는다.</td>
</tr>
<tr>
<td>resolveReferences</td>
<td>true</td>
<td>참조를 해결하고, 별칭이나 수학 표현식을 제거해서 토큰을 생성한다.</td>
</tr>
<tr>
<td>expandComposition</td>
<td>false</td>
<td>레이어 관련 설정이라는데, 설명이 없다...</td>
</tr>
</tbody></table>
<p>옵션들에 대해 더 자세히 알아보자.</p>
<h4 id="expand로-시작하는-옵션들">expand로 시작하는 옵션들</h4>
<p>expand로 시작하는 옵션들은 자동 확장 여부를 결정한다. (expandTypography, expandShadow, expandBorder)</p>
<p>자동 확장 여부란, 추출된 토큰이 참조한 대상 토큰의 type도 함께 포함하는 것을 의미한다.</p>
<p>대표로 <code>expandTypography</code> 옵션을 예로 들어 확인해보자.</p>
<pre><code class="language-json">{
  &quot;H1&quot;: {
    &quot;Bold&quot;: {
      &quot;type&quot;: &quot;typography&quot;,
      &quot;value&quot;: {
        &quot;fontFamily&quot;: &quot;Inter&quot;,
        &quot;fontWeight&quot;: &quot;Bold&quot;,
        &quot;lineHeight&quot;: &quot;110%&quot;,
        &quot;fontSize&quot;: 48.829,
        &quot;paragraphSpacing&quot;: 32,
        &quot;letterSpacing&quot;: &quot;-5%&quot;
      }
    }
  }
}</code></pre>
<p>expandTypography=false, 즉 기본값으로 추출했을 경우다.</p>
<pre><code class="language-json">{
  &quot;typography&quot;: {
    &quot;H1&quot;: {
      &quot;Bold&quot;: {
        &quot;fontFamily&quot;: { &quot;value&quot;: &quot;Inter&quot;, &quot;type&quot;: &quot;fontFamilies&quot; },
        &quot;fontWeight&quot;: { &quot;value&quot;: &quot;Bold&quot;, &quot;type&quot;: &quot;fontWeights&quot; },
        &quot;lineHeight&quot;: { &quot;value&quot;: &quot;110%&quot;, &quot;type&quot;: &quot;lineHeight&quot; },
        &quot;fontSize&quot;: { &quot;value&quot;: 48.829, &quot;type&quot;: &quot;fontSizes&quot; },
        &quot;paragraphSpacing&quot;: { &quot;value&quot;: 32, &quot;type&quot;: &quot;paragraphSpacing&quot; },
        &quot;letterSpacing&quot;: { &quot;value&quot;: &quot;-5%&quot;, &quot;type&quot;: &quot;letterSpacing&quot; }
      }
    }
  }
}</code></pre>
<p>반면에, true로 설정하면 fontFamily, fontSize 등의 토큰들이 어떤 토큰을 참조했는지 함께 추출된다.</p>
<h4 id="preserverawvalue-옵션">preserveRawValue 옵션</h4>
<p><code>preserveRawValue</code> 옵션도 expand로 시작하는 옵션들과 비슷한데, 이 경우에는 기존에 참조하던 형태를 그대로 가져온다.</p>
<blockquote>
<p><strong>light 테마 preserveRawValue 옵션 사용해서 추출하기</strong>
npx token-transformer input.json light.json global,light global --preserveRawValue=true</p>
</blockquote>
<pre><code class="language-json">{
  &quot;bg&quot;: {
    &quot;default&quot;: {
      &quot;value&quot;: &quot;#ffffff&quot;,
      &quot;type&quot;: &quot;color&quot;,
      &quot;rawValue&quot;: &quot;{colors.white}&quot;
    }
  }
}</code></pre>
<p>false인 경우에 <code>rawValue</code> 필드가 없다.</p>
<h2 id="config-파일로-실행하기">config 파일로 실행하기</h2>
<p>사실상 config를 읽어서 실행하는게 아니라, 직접 실행 방식을 정의해야 한다.</p>
<p><strong><code>tt.config.js</code></strong></p>
<pre><code class="language-js">const fs = require(&#39;fs&#39;);
const path = require(&#39;path&#39;);
const { transformTokens } = require(&#39;token-transformer&#39;);

// 추출 대상 파일 경로
const filePath = &#39;src/tokens&#39;;
const dir = path.join(__dirname, filePath);
// 만약 없다면 생성
if (!fs.existsSync(dir)) {
  fs.mkdirSync(dir, { recursive: true });
}

// 변환 옵션
const transformerOptions = {};

// 파일 읽기
fs.readFile(&#39;tokens.json&#39;, &#39;utf8&#39;, (err, data) =&gt; {
  if (err) throw err;
  const tokens = JSON.parse(data);

  // $metadata에 token key가 있음
  const tokenKeys = [...tokens.$metadata.tokenSetOrder];

  tokenKeys.forEach((key) =&gt; {
    // 변환 작업
    const resolved = transformTokens(
      tokens, // 변환할 파일
      key === &#39;light&#39; || key === &#39;dark&#39; ? [&#39;global&#39;, key] : tokenKeys, // 참조 대상
      [...tokenKeys].filter((k) =&gt; k !== key), // 추출 제외 대상
      transformerOptions // 변환 옵션
    );

    // 파일 생성
    fs.writeFileSync(
      `${filePath}/${key}.json`,
      JSON.stringify(resolved),
      (err) =&gt; {
        if (err) throw err;
      }
    );
  });
});</code></pre>
<p>이후 node로 config 파일을 실행하면 된다.</p>
<blockquote>
<p>node tt.config.js</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드에 디자인 시스템 적용하기]]></title>
            <link>https://velog.io/@seo__namu/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 09 May 2023 13:24:43 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>내가 참여하고 있는 사이드 프로젝트에서 디자인 시스템을 구축하고 있다. 디자인의 일관성 유지와 프론트엔드 개발자의 개입없이 <strong>Figma에서 변경된 디자인이 프론트엔드에 바로 적용</strong>되는 것을 목표로 한다.</p>
<p>변경된 디자인이 프론트엔드에 바로 반영되는 것은 <strong>커뮤니케이션 비용의 절약</strong>으로 이어지며, 디자이너와 개발자는 <strong>각자의 역할에 집중</strong>할 수 있게 된다.</p>
<h3 id="디자인-시스템이란">디자인 시스템이란?</h3>
<blockquote>
<p>디자인 시스템이란 일관성 있는 디자인을 유지하기 위해 재사용할 수 있는 디자인 요소들을 구성하는 것이다.</p>
</blockquote>
<p>프론트엔드에서 재사용되는 코드 블럭을 컴포넌트로 만들어서 사용하는 것처럼, Figma에서도 컴포넌트를 만들어 블럭을 쌓아가는 형태로 페이지를 만들 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/6c7de4df-cd91-4448-bbc9-618b2f3d8819/image.png" alt=""></p>
<p>여기에 파란색 확인 버튼이 있다. </p>
<p>이 버튼이 <strong>약 100개</strong>의 페이지에서 사용되었다고 가정하고, 만약 누군가 확인 버튼을 초록색으로 변경해달라고 한다면 어떻게 될까?</p>
<ol>
<li>컴포넌트 사용 X : 등록 버튼이 포함된 100개의 페이지를 찾아서 버튼 색상 변경</li>
<li>컴포넌트 사용 O : 원본 컴포넌트의 색상을 초록색으로 변경하면 확인 버튼을 사용한 <strong>모든 페이지에 적용</strong></li>
</ol>
<p>조금 극단적인 예시였으나, 디자인 요소들을 컴포넌트로 생성해서 사용하는 것이 훨씬 생산적이고 효율적이라는 것을 어필하고 싶었다.</p>
<h3 id="그래서-프론트엔드는-뭘-해야하는데">그래서 프론트엔드는 뭘 해야하는데?</h3>
<p>Figma에서는 컴포넌트를 제작하는데 기본이 되는 설정 값들 spacing(간격), color(색상), typography(문자) 등을 <a href="https://tokens.studio/">Tokens Studio 플러그인</a>을 이용해 값을 설정한다. </p>
<blockquote>
<p>디자인 요소 값들은 <code>json</code> 파일로 추출할 수 있으며, 이 파일을 프론트엔드에서 사용한다. <code>json</code> 파일은 직접 추출할 수도 있으나 GitHub 저장소와 연동이 가능하다!</p>
</blockquote>
<p>따라서 프론트엔드 개발자가 해야할 일은 아래와 같다.</p>
<ol>
<li>Figma와 GitHub 연동</li>
<li><code>json</code> 파일 변환 작업</li>
<li>GitHub Actions 구성해서 자동 PR 생성하기</li>
</ol>
<p>3번 GitHub Actions 구성하는 작업은 필수는 아니지만, 프론트엔드 개발자의 <strong>편의</strong>를 위해 구성하는 것이 좋다. <strong>신규 커밋을 감지</strong>해서 변환 파일을 생성하고 <code>main</code> 브랜치에 merge 하는 과정을 <strong>자동화</strong>하면 개발자의 일이 줄어든다!</p>
<h3 id="json-파일을-왜-변환해야-돼"><code>json</code> 파일을 왜 변환해야 돼?</h3>
<p>디자인 관련 요소들이 정리되어 있는 <code>json</code> 파일을 왜 변환해야 하는가에 대한 의문이 생길 수 있다.</p>
<pre><code class="language-plain">Figma
├── global/
│   └── colors/
│       ├── black
│       ├── white
│       └── gray/
│            ├── 100
│            ├── 200
│            └── ...
├── light/
│   └── bg/
│       ├── primary 
│       ├── secondary
│       └── ...
└── dark/
    └── bg/
        ├── primary 
        ├── secondary
        └── ...</code></pre>
<p>Figma에서 추출된 <code>json</code> 파일 예시이다. <em>(각 프로젝트마다 구조가 다를 수 있다.)</em></p>
<p>보통의 경우 디자인을 할 때, <code>global</code> (혹은 core) 같은 베이스가 되는 녀석을 두고 <code>light</code>와 <code>dark</code>는 <code>global</code>을 참조해서 디자인 작업을 한다.</p>
<pre><code class="language-json">{
  &quot;light&quot;: {
    &quot;bg&quot;: {
      &quot;primary&quot;: {
        &quot;value&quot;: &quot;{colors.white}&quot;,
        &quot;type&quot;: &quot;color&quot;
      },
      &quot;secondary&quot;: {
        &quot;value&quot;: &quot;{colors.gray.100}&quot;,
        &quot;type&quot;: &quot;color&quot;
      }
    }
}</code></pre>
<p>추출되는 <code>json</code> 파일이 이런 형태인데, 이대로는 참조하는 <code>global</code> 의 값을 읽어올 수 없어서 변환 작업을 거쳐야한다.</p>
<p>이제 Figma와 GitHub 연동부터 시작해보자!</p>
<h2 id="figma와-github-연동">Figma와 GitHub 연동</h2>
<p>GitHub와 연동하기에 앞서, GitHub 계정이 하나 필요하다. 디자이너 혹은 디자인 팀에서 사용할 <strong>계정을 별도로 생성</strong>하는 것을 추천한다. 디자인 관련된 commit, push가 발생하는데, 개인적으로는 이를 관리하려면 별도의 계정이 있는게 좋다고 생각한다.</p>
<p>Figma와 GitHub 연동하는 순서는 아래와 같다.</p>
<ol>
<li>저장소 생성</li>
<li>GitHub Token 생성</li>
<li>Figma에 정보 입력</li>
</ol>
<h3 id="저장소-생성">저장소 생성</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/f32f9b38-0d1c-4edd-911b-b076d0cc3a2d/image.png" alt=""></p>
<p>나는 <code>design</code> 저장소를 하나 생성했다.</p>
<h3 id="github-token-생성">GitHub Token 생성</h3>
<p>아래의 순서대로하면 토큰을 생성하는 화면이 나온다.</p>
<blockquote>
<p>settings / Developer settings / Personal access tokens / Tokens (classic)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/66502da6-8a2f-4e37-bc7c-391c027e39f9/image.png" alt=""></p>
<p>Generate new token (classic)을 선택하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/0ef57974-1de4-4b6f-9100-a87c303af6e6/image.png" alt=""></p>
<p>나는 토큰 이름을 <code>design</code> 으로 했고, 저장소에 대한 읽기 쓰기 정도의 권한을 설정하고 토큰을 생성했다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/01450722-b7bb-409b-9f39-5af1f01d4a81/image.png" alt=""></p>
<p>생성된 토큰을 Figma에 입력해야하기 때문에 복사해둔다.</p>
<h3 id="figma에-정보-입력">Figma에 정보 입력</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/09b51af1-bbed-4b01-97bf-253c897598f5/image.png" alt=""></p>
<p>Tokens Studio를 키고, <code>Settings</code> 에서 <code>Add new</code> 버튼을 누르고, <code>GitHub</code>을 선택한다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/a1d7bbbb-7a52-4e7d-aac7-46b3fa7ffe35/image.png" alt=""></p>
<ul>
<li>Name : GitHub 계정이름</li>
<li>Personal Access Token : GitHub 토큰</li>
<li>Repository : GitHub 저장소 주인 / 저장소 이름</li>
<li>Branch : <code>json</code> 파일을 push할 브랜치</li>
<li>File Path : <code>json</code> 파일명 (확장자 꼭 붙이기)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/3acd3591-0e2d-41ec-8682-ac5024adf630/image.png" alt=""></p>
<p>연동이 완료되었고, 임시 값을 추가한 후 GitHub에 push를 해보자!</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/842f8750-3aa0-4bcb-bf75-1288786032bc/image.png" alt=""></p>
<p><code>Tokens</code>에서 <code>Tools</code>를 누르면 파일을 불러오거나 추출할 수 있는데, <code>Load from file/folder or preset</code> 을 누른다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/db99cbd4-6880-47af-858d-270f12f88bff/image.png" alt=""></p>
<p><code>Preset</code>을 로드해주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/5dd2aeb4-62bb-4cd0-988d-b5a915c2824c/image.png" alt=""></p>
<p>그러면 이런 형태의 임시 값들이 불러와지고, 하단에 파란색으로 표시되어있는 <code>업로드 버튼</code>을 누르면 GitHub에 push 할 수 있다.</p>
<h2 id="json-파일-변환-작업">json 파일 변환 작업</h2>
<p><a href="https://docs.tokens.studio/sync/github#7-how-to-use-tokens-stored-in-github-in-development">Tokens Studio 공식문서</a>에 따르면 아래의 순서대로 변환 작업을 진행하면 된다.</p>
<ol>
<li><a href="https://www.npmjs.com/package/token-transformer">Token Transformer</a></li>
<li><a href="https://amzn.github.io/style-dictionary/#/">Style Dictionary</a></li>
</ol>
<blockquote>
<p><strong>CSS-in-JS</strong>를 사용한다면 Token Transformer만 사용해서 변환하면 되고, <strong>CSS</strong>를 사용한다면 Style Dictionary를 사용해야 한다.</p>
</blockquote>
<p>Style Dictionary는 CSS, SCSS, JavaScript, TypeScript 등 <strong>다양한 언어</strong>를 지원하기 때문에 옵션 값들이 다양하다.</p>
<p>하지만 나는 CSS-in-JS를 사용하기 때문에 간단하게 Token Transformer만 사용해서 변환 작업을 했다.</p>
<p><em>Style Dictionary는 공식문서가 잘 되어있으니 사용하실 경우에 참고하세요!</em></p>
<h3 id="token-transformer로-변환하기">Token Transformer로 변환하기</h3>
<!-- Token Transformer를 사용하는 방법은 매우 간단하다.

> npx token-transformer **input-file-name** **output-file-name **

npx로 실행하면 되고, token-transformer 입력 후 `입력 파일명.json` `출력 파일명.json`을 순서대로 적어주면 된다.

> npx token-transformer tokens.json ./src/designTokens.json

입력 파일은 `tokens.json`이고, 출력은 src 폴더 하위에 `designTokens.json`으로 생성하도록 했다. -->

<blockquote>
<p>npx token-transformer tokens.json ./src/global.json global</p>
</blockquote>
<p>기본이 되는 global을 변환한다고 했을 때, 실행하는 명령어이다.</p>
<p><em>Token Transformer 사용 방법에 대한 자세한 내용은 <a href="https://velog.io/@seo__namu/Token-Transformer-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-u6uwunqe">Token Transformer 사용하기</a>에서 확인할 수 있습니다.</em></p>
<pre><code class="language-json">{
  &quot;button&quot;: {
    &quot;primary&quot;: {
      &quot;background&quot;: {
        &quot;value&quot;: &quot;#5a67d8&quot;,
        &quot;type&quot;: &quot;color&quot;
      },
      &quot;text&quot;: {
        &quot;value&quot;: &quot;#ffffff&quot;,
        &quot;type&quot;: &quot;color&quot;
      }
    },
    &quot;borderRadius&quot;: {
      &quot;value&quot;: 8,
      &quot;type&quot;: &quot;borderRadius&quot;
    },
    &quot;borderWidth&quot;: {
      &quot;value&quot;: 8,
      &quot;type&quot;: &quot;borderWidth&quot;
    }
  },
}</code></pre>
<p>명령어를 실행하면, 이런 형태로 참조가 아닌 값 자체를 넣어준다.</p>
<h2 id="github-actions-구성해서-자동-pr-생성하기">GitHub Actions 구성해서 자동 PR 생성하기</h2>
<p>GitHub Actions의 실행 순서는 아래와 같다.</p>
<ol>
<li>design 브랜치의 <code>tokens.json</code> push 감지</li>
<li>token transformer로 변환 작업 후 새로운 commit을 생성, push</li>
<li>main에 PR을 생성</li>
</ol>
<pre><code class="language-yml">name: Create PR from design to main

# design 브랜치의 tokens.json 파일에 대한 push 감지
on:
  push:
    branches:
      - design
    paths:
      - &#39;tokens.json&#39;

jobs:
  createPullRequest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      # 디자인 파일 변환 후 생성된 파일도 push해서 main 브랜치로 병합하는 PR을 생성
      - name: Run Token Transformer
        run: |
            npx token-transformer tokens.json ./src/global.json global
            git config --global user.name &quot;GitHub 디자인 계정 이름&quot; 
            git config --global user.email &quot;GitHub 디자인 계정 이메일&quot;
            git add .
            git commit -m &#39;피그마 디자인 파일 변환&#39;
            git push
        env: 
            GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
      - name: Create Pull Request
        run: gh pr create -B main -H design --title &#39;💄 디자인 토큰 업데이트&#39; --body &#39;디자인 토큰이 업데이트 후 변환작업을 수행했습니다.&#39;
        env:
            GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}</code></pre>
<h3 id="저장소-설정-작업하기">저장소 설정 작업하기</h3>
<p>위 파일을 보면 GITHUB_TOKEN을 <code>secrets</code> 환경 변수에서 읽어오는데, 이 부분도 저장소에 설정을 해줘야한다.</p>
<blockquote>
<p>Settings / Secrets and variables / Actions</p>
</blockquote>
<p>저장소의 설정으로 들어가서, Actions의 새로운 변수를 추가해준다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/6a35e72d-62a4-46c3-86f1-70ece9c46a94/image.png" alt=""></p>
<p>Name에 <strong>ACCESS_TOKEN</strong>을 입력하고, Secret에는 초기에 만들었던 <strong>GitHub 토큰 값</strong>을 넣어주면 된다.</p>
<p>그리고 actions runner가 저장소에 신규 commit을 생성하고 push하는 권한도 설정해줘야한다.</p>
<blockquote>
<p>Settings / Actions / General</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/08caf8c9-35c1-4c20-9eeb-d51b6619405e/image.png" alt=""></p>
<p><code>Read and write permissions</code>에 체크하고 저장하면 된다.</p>
<blockquote>
<p>마지막으로는 기존의 design 브랜치에는 yml 파일이 없기 때문에, rebase를 하거나 브랜치를 삭제했다가 Figma에서 다시 push를 해야 actions가 실행된다.</p>
</blockquote>
<h3 id="figma에서-push를-하면">Figma에서 Push를 하면?</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/98ceb61e-a1f9-4e70-aacc-564bb86dfe8c/image.png" alt=""></p>
<p>Github Actions가 잘 수행되어 PR이 생성된다! 🥳</p>
<h2 id="마무리">마무리</h2>
<p>Figma 토큰을 프론트엔드에서 사용할 수 있게 GitHub과 연동하고 변환하는 작업에 대한 자료가 많이 없어서 글을 쓰게 됐다.</p>
<p>사실 GitHub Actions를 사용하지 않는다면, <code>tokens.json</code>을 프론트엔드에서 쓸  수 있도록 변환 작업을 하는 것 자체는 매우 간단하다. 하지만 자동화를 해두면 생산성이 올라가니까!</p>
<p>GitHub Actions를 처음 사용하다보니 많은 실패를 겪었다. </p>
<ul>
<li>runner는 정상적으로 수행했으나 PR을 생성하지 않고 main에 바로 merge 해버리기도 했으며,</li>
<li>runner가 commit을 수행하기 위해서 권한 설정하는 것도 몰랐고,</li>
<li>특히 PR을 생성할 때 <code>body</code>가 없으면 오류 발생하는 것도 몰랐다.</li>
</ul>
<p>이번 경험 덕분에 GitHub Actions와 친해질 수 있어서 좋았다.</p>
<p>혹시라도 Figma와 GitHub을 연동하는 과정에서 자동화를 하고자 하는 분이 계시다면, 나와 같은 실수로 시간을 허비하지 않길 바라며,,🙏</p>
<!-- 
P.S) Token-Transformer에 대한 자료가 없어서 직접 여러번 테스트를 거친 후 글을 작성했는데, 업로드를 한 다음날인 2023년 5월 11일,, 내가 Token-Transformer를 잘못 사용하고 있다는 것을 발견했다. 그래서 Token-Transformer에 대해서는 따로 글을 작성할 예정이다.

우선 [GitHub 저장소](https://github.com/ixio0330/design-token2)에 올바른 사용 방법으로 구성을 해놓았으니, 참고하실분 있다면 확인해보세요!
-->]]></description>
        </item>
        <item>
            <title><![CDATA[React 비동기 처리 상태 관리하기]]></title>
            <link>https://velog.io/@seo__namu/React-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/React-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 04 May 2023 05:32:48 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드에서는 서버로부터 데이터를 받아올 때, <strong>로딩 에러 성공</strong> 등의 <code>상태</code>를 관리해야 합니다.</p>
<p>각 상태에 따라 사용자에게 보여줄 화면이 다르기 때문입니다.</p>
<pre><code class="language-js">const getUserInfo = () =&gt;
  new Promise((resolve) =&gt;
    setTimeout(
      () =&gt; resolve({ name: &#39;서나무&#39;, job: &#39;프론트엔드 개발자&#39; }),
      1000,
    ),
  );</code></pre>
<p>여기에 1초 후에 사용자 정보를 반환하는 <code>getUserInfo</code> 함수가 있습니다.</p>
<p>더 정확히 표현하면 위 함수는 Promise를 반환하는데, 반환된 Promise가 1초 후에 resolve 처리를 합니다.</p>
<p><code>getUserInfo</code> 함수를 통해 받은 데이터를 화면에 보여주는 코드를 작성해볼까요?</p>
<pre><code class="language-jsx">import React, { useState, useEffect } from &#39;react&#39;;

const UserProfile = () =&gt; {
  const [user, setUser] = useState({});
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() =&gt; {
    const fetchUserInfo = async () =&gt; {
      setLoading(true); // 로딩 시작
      try {
        setUser(await getUserInfo()); // 상태 업데이트
      } catch (error) {
        setError(error); // 에러 처리
      } finally {
        setLoading(false); // 로딩 종료
      }
    };
    fetchUserInfo();
  }, []);

  if (loading) return &lt;div&gt;로딩중...&lt;/div&gt;;
  if (error) return &lt;div&gt;오류가 발생했습니다.&lt;/div&gt;;
  return (
    &lt;div&gt;
      &lt;p&gt;이름: {user?.name}&lt;/p&gt;
      &lt;p&gt;직업: {user?.job}&lt;/p&gt;
    &lt;/div&gt;
  );
};</code></pre>
<p>1초 동안은 <code>로딩중...</code>이라는 문구가 보여지고, 1초 후에 사용자 정보를 화면에 출력합니다.</p>
<p>만약 비동기 함수 호출에 따른 상태 관리를 한 번만 한다면 위의 코드를 사용해도 괜찮다고 생각합니다.</p>
<p>하지만 서버로부터 데이터를 받아와야 하는 경우가 많아진다면, 비동기 처리 상태를 관리하는 로직을 추상화하는 것이 필요해집니다.</p>
<h2 id="1-hook으로-분리하기">1. Hook으로 분리하기</h2>
<p>비동기 처리를 하는 함수를 파라미터로 받아옵니다.</p>
<p>그리고 기존에 <code>getUserInfo</code>를 대체하기만 하면 끝입니다!</p>
<pre><code class="language-jsx">const useFetch = (fetchCallback) =&gt; {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);

  const fetchData = async () =&gt; {
    setLoading(true);
    try {
      setData(await fetchCallback());
    } catch (error) {
      setError(error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() =&gt; {
    fetchData();
  }, []);

  return {
    loading,
    error,
    data,
    refetch: fetchData,
  };
};</code></pre>
<p>사용하는 방법은 매우 간단하며,</p>
<pre><code class="language-jsx">const UserProfile = () =&gt; {
  const { data, error, loading } = useFetch(getUserInfo);
  if (loading) return &lt;div&gt;로딩중...&lt;/div&gt;;
  if (error) return &lt;div&gt;오류가 발생했습니다.&lt;/div&gt;;
  return (
    &lt;div&gt;
      &lt;p&gt;이름: {data?.name}&lt;/p&gt;
      &lt;p&gt;직업: {data?.job}&lt;/p&gt;
    &lt;/div&gt;
  );
};</code></pre>
<p>사용자 정보가 아닌, 게시글 정보, 댓글 정보 등 다양한 비동기 처리 시 상태를 관리하기 쉬워졌습니다.</p>
<h2 id="2-usereducer로-상태-업데이트-로직-분리하기">2. useReducer로 상태 업데이트 로직 분리하기</h2>
<p>현재 useFetch hook에서 개선해야 할 점들이 보입니다.</p>
<ul>
<li>여러 개의 <code>useState</code>를 사용</li>
<li>상태를 업데이트하는 로직이 종속되어 있음</li>
</ul>
<p>useReducer는 여러 개의 상태를 관리할 수 있으며, action이라는 객체로 상태를 업데이트 하는 로직을 분리합니다.</p>
<pre><code class="language-jsx">const useFetchReducer = (state, action) =&gt; {
  switch (action.type) {
    case &#39;LOADING&#39;: // loading 중
      return {
        loading: true,
        error: null,
        data: null,
      };
    case &#39;ERROR&#39;: // 비동기 처리 reject(실패)
      return {
        loading: false,
        error: action.error,
        data: null,
      };
    case &#39;SUCCESS&#39;: // 비동기 처리 resolve(성공)
      return {
        loading: false,
        error: null,
        data: action.data,
      };
    default:
      throw Error(`[useFetch] ${action.type} is not valid`);
  }
};</code></pre>
<p>상태 변화를 조작하는 reducer 함수입니다.</p>
<p>action의 type을 받아서, type에 따라 상태를 반환합니다.</p>
<pre><code class="language-jsx">const useFetch = (fetchCallback) =&gt; {
  const [state, dispatch] = useReducer(
    useFetchReducer, // 상태 조작 함수
    { // 상태 초기화
      loading: false,
      error: null,
      data: null,
    }
  );

  const fetchData = async () =&gt; {
    dispatch({ type: &#39;LOADING&#39; });
    try {
      dispatch({ type: &#39;SUCCESS&#39;, data: await fetchCallback() });
    } catch (error) {
      dispatch({ type: &#39;ERROR&#39;, error });
    }
  };

  useEffect(() =&gt; {
    fetchData();
  }, []);

  return {
    ...state,
    refetch: fetchData,
  };
};</code></pre>
<p><code>setState</code>를 사용하지 않고, <code>dispatch</code>를 사용해 상태를 조작합니다.</p>
<p>이 외에는 기존의 useFetch와 크게 다른점이 없으며, 사용법도 동일합니다.</p>
<p>만약 hook으로 분리하지 않았다면 비동기 처리를 하는 모든 로직을 찾아 수정했을텐데, 하나의 hook으로 관리하니 유지보수도 용이해졌습니다.</p>
<h2 id="3-typescript로-마이그레이션">3. TypeScript로 마이그레이션</h2>
<p>JavaScript는 타입이 없는 언어입니다.</p>
<p>타입이 없다는 것은 유연하다고 볼 수도 있고, 안정성이 보장되지 않는다고 볼 수 있다고 생각합니다.</p>
<p>TypeScript를 사용해 타입을 설정하면, 런타임 오류를 최소화하고 코드의 안정성을 향상시킬 수 있습니다.</p>
<pre><code class="language-tsx">import React, { useReducer, useEffect } from &#39;react&#39;;

// State와 Action에서 공통으로 사용
interface BaseState&lt;D, E&gt; {
  error?: E | null;
  data?: D | null;
}

// State 타입
interface UseFetchState&lt;D, E&gt; extends BaseState&lt;D, E&gt; {
  loading: boolean;
}

// Action 타입
interface UseFetchAction&lt;D, E&gt; extends BaseState&lt;D, E&gt; {
  type: &#39;LOADING&#39; | &#39;ERROR&#39; | &#39;SUCCESS&#39;;
}

const useFetchReducer = &lt;D, E&gt;(
  state: UseFetchState&lt;D, E&gt;,
  action: UseFetchAction&lt;D, E&gt;,
): UseFetchState&lt;D, E&gt; =&gt; {
  switch (action.type) {
    case &#39;LOADING&#39;:
      return {
        loading: true,
        error: null,
        data: null,
      };
    case &#39;ERROR&#39;:
      return {
        loading: false,
        error: action.error,
        data: null,
      };
    case &#39;SUCCESS&#39;:
      return {
        loading: false,
        error: null,
        data: action.data,
      };
    default:
      throw Error(`[useFetch] ${action.type} is not valid`);
  }
};

// 제네릭으로 반환 데이터와 오류의 타입 지정
const useFetch = &lt;D, E&gt;(fetchCallback: () =&gt; Promise&lt;D&gt;) =&gt; {
  const [state, dispatch] = useReducer(useFetchReducer&lt;D, E&gt;, {
    loading: false,
    error: null,
    data: null,
  } as UseFetchState&lt;D, E&gt;);

  const fetchData = async () =&gt; {
    dispatch({ type: &#39;LOADING&#39; });
    try {
      dispatch({ type: &#39;SUCCESS&#39;, data: await fetchCallback() });
    } catch (error) {
      dispatch({ type: &#39;ERROR&#39;, error });
    }
  };

  useEffect(() =&gt; {
    fetchData();
  }, []);

  return {
    ...state,
    refetch: fetchData,
  };
};</code></pre>
<p>reducer 함수의 state, action의 타입을 지정해주는 것과 제네릭으로 데이터와 오류 타입을 추상화했습니다.</p>
<pre><code class="language-tsx">interface UserInfo {
  name: string;
  job: string;
}

const getUserInfo = () =&gt;
  //❗Promise 반환 타입 정의
  new Promise&lt;UserInfo&gt;((resolve, reject) =&gt;
    setTimeout(
      () =&gt; resolve({ name: &#39;서나무&#39;, job: &#39;프론트엔드 개발자&#39; }),
      1000,
    ),
  );</code></pre>
<p>Promise가 반환하는 데이터 타입을 <code>UserInfo</code>로 설정해줍니다. 타입을 지정해주지 않으면 <code>unknown</code>으로 추론되기 때문입니다.</p>
<p>기존에는 함수의 반환 타입을 지정해줬는데, 댓글로 <code>new Promise&lt;T&gt;</code>으로 타입 지정해주는 방법을 알려주셔서 수정했습니다. 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React useInput hook으로 유효성 검사하기]]></title>
            <link>https://velog.io/@seo__namu/React-useInput-hook%EC%9C%BC%EB%A1%9C-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/React-useInput-hook%EC%9C%BC%EB%A1%9C-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 16 Dec 2022 22:56:57 GMT</pubDate>
            <description><![CDATA[<p>사용자의 입력을 받고, 유효성 검사를 해야된다면 작성해야하는 코드량이 엄청 늘어난다.</p>
<pre><code class="language-jsx">import { useState, useEffect } from &#39;react&#39;;

export default function App() {
  // 사용자가 입력한 값을 담을 state
  const [email, setEmail] = useState(&#39;&#39;);

  // input이 변경되면 값을 email에 저장
  const onChangeEmail = (e) =&gt; {
    setEmail(() =&gt; e.target.value);
  };

  // email 유효성 결과를 담을 state
  const [emailValid, setEmailValid] = useState(&#39;&#39;);

  // email 유효성 검사 함수
  const emailRule = (v = &#39;&#39;) =&gt; {
    if (v === &#39;&#39;) return;
    if (!v.includes(&#39;@&#39;)) {
      return &#39;형식이 올바르지 않습니다.&#39;;
    }
  };

  // email이 변경되면 유효성 검사
  useEffect(() =&gt; {
    if (!email) return;
    setEmailValid(() =&gt; emailRule(email));
  }, [email]);

  return (
    &lt;div&gt;
      &lt;div&gt;
        &lt;label htmlFor=&#39;email&#39;&gt;이메일&lt;/label&gt;
        &lt;input
          id=&#39;email&#39;
          type=&#39;text&#39;
          value={email}
          onChange={onChangeEmail}
          placeholder=&#39;email@example.com&#39;
        /&gt;
        &lt;p style={{ color: &#39;red&#39; }}&gt;{emailValid}&lt;/p&gt;
      &lt;/div&gt;
      &lt;button&gt;Sing up&lt;/button&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>만약 여기서 비밀번호도 입력받으면 어떻게 될까? <strong>말해 뭐해? 코드가 2배가 된다.</strong></p>
<h2 id="input-component">Input Component</h2>
<p>일단 iuput 태그를 컴포넌트로 만들자!</p>
<pre><code class="language-jsx">import React from &quot;react&quot;;

const Input = ({
  id = Math.floor(Math.random() * 10000 + 1), // random 아이디 부여
  type = &#39;text&#39;,
  value = &#39;&#39;,
  onChange,
  label = &#39;&#39;,
  placeholder = &#39;&#39;,
  disabled = false,
  valid = &#39;&#39;, // 유효성 검사 결과
}) =&gt; {
  return (
    &lt;div&gt;
      &lt;div&gt;
        &lt;label htmlFor={id}&gt;{label}&lt;/label&gt;
      &lt;/div&gt;
      &lt;input
        id={id}
        type={type}
        value={value}
        onChange={onChange}
        placeholder={placeholder}
        disabled={disabled}
      /&gt;
      &lt;p style={{color: &#39;red&#39;}}&gt;{valid}&lt;/p&gt;
    &lt;/div&gt;
  );
};

export default React.memo(Input);</code></pre>
<p><code>React.memo</code>를 사용했는데, props가 변경되지 않으면 컴포넌트를 재렌더링 하지 않도록 최적화를 한 것이다.</p>
<h3 id="useinput-hook">useInput hook</h3>
<p>이제 state와 유효성 검사 관련 로직을 hook으로 분리해보자.</p>
<pre><code class="language-js">import { useState, useEffect, useCallback } from &#39;react&#39;;

// 초기 값, 유효성 검사 함수를 받음
const useInput = (initValue = &#39;&#39;, rule) =&gt; {
  const [value, setValue] = useState(initValue);
  const [valid, setValid] = useState(&#39;&#39;);
  const onChange = useCallback((e) =&gt; {
    setValue(() =&gt; e.target.value);
  }, []);
  // value가 변경되면 유효성 검사하기
  useEffect(() =&gt; {
    if (rule) {
      setValid(() =&gt; rule(value));
    }
  }, [value]);
  return {
    value,
    setValue,
    onChange,
    valid,
  };
};

export default useInput;</code></pre>
<h3 id="input--useinput">Input + useInput</h3>
<p>이제 <code>Input</code> 컴포넌트와 <code>useInput</code> hook을 사용해서 코드를 간결하게 작성해보자.</p>
<pre><code class="language-jsx">// Component
import Input from &#39;../components/form/Input&#39;;

// hook
import useInput from &#39;../hooks/useInput&#39;;

export default function App() {
  const {
    value: email,
    valid: validEmail,
    onChange: onChangeEmail,
  } = useInput(&#39;&#39;, emailRule);
  return (
    &lt;div&gt;
      &lt;Input
        label=&#39;이메일&#39;
        value={email}
        onChange={onChangeEmail}
        valid={validEmail}
        placeholder=&#39;email@example.com&#39;
      /&gt;
      &lt;button&gt;Sing up&lt;/button&gt;
    &lt;/div&gt;
  );
}

const emailRule = (v = &#39;&#39;) =&gt; {
  if (v === &#39;&#39;) return;
  if (!v.includes(&#39;@&#39;)) {
    return &#39;형식이 올바르지 않습니다.&#39;;
  }
};</code></pre>
<p>input이 여러 개가 생겨도 코드가 2배로 늘어날 일은 없어졌다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript Intl API 사용하기]]></title>
            <link>https://velog.io/@seo__namu/JavaScript-Intl-API-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/JavaScript-Intl-API-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 16 Dec 2022 22:53:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Intl 개체는 ECMAScript 국제화 API의 네임스페이스로, 언어 구분 문자열 비교, 숫자 형식, 날짜 및 시간 형식을 제공합니다.
출처: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl">[MDN] Intl</a></p>
</blockquote>
<p>Intl API를 사용하면 각 나라의 언어와 표현에 맞게 날짜 혹은 시간 등을 표시해줄 수 있다.</p>
<h2 id="relativetimeformat">RelativeTimeFormat</h2>
<p><code>RelativeTimeFormat</code>를 이용하면 각 나라의 언어에 맞게 <code>N일 전</code> <code>N days age</code> 이렇게 시간을 나타낼 수 있다. </p>
<pre><code class="language-js">// 인스턴스 생성할 때 언어 설정
const formatter = new Intl.RelativeTimeFormat(&#39;ko&#39;);

// 오늘과 1년 전 날짜의 차이를 구한다.
// month는 index로 설정해야해서 12월 달의 index인 11을 넣어줘야한다.
const passed = new Date() - new Date(2021, 11, 16); 

// passed를 하루 단위로 나눔
const passedForDay = Math.floor(passed / (1000 * 60 * 60 * 24));

const diff = formatter.format(
  -passedForDay, // 음수는 지난 날짜, 양수는 남은 날짜로 표현한다.
  &#39;day&#39;, // 포맷을 &#39;day&#39;로 설정한다.
);

console.log(diff); // 365일 전</code></pre>
<p>지금과 1년 전의 날짜 차이를 콘솔에 한국어로 출력하도록 했다.</p>
<pre><code class="language-js">// 언어를 영어로 설정
const formatter = new Intl.RelativeTimeFormat(&#39;en-US&#39;); // 365 days ago

// 날짜를 양수로 설정
const diff = formatter.format(passedForDay, &#39;day&#39;); // 365일 후</code></pre>
<p>이렇게 간단하게 옵션을 변경해보면서 출력되는 결과를 확인해볼 수 있다.</p>
<h2 id="generatetimestring">generateTimeString</h2>
<p>라이브러리를 사용하지 않고 <code>RelativeTimeFormat</code>를 사용해서 <strong>지나간 날짜</strong>를 똑똑하게 나타내는 함수를 만들어보자!</p>
<p>참고로 날짜 관련된 라이브러리는 <code>Moment.js</code> <code>Day.js</code> 등이 있으며 <code>Moment.js</code>는 더 이상 업데이트가 이루어지지 않을 것이라고 한다.</p>
<pre><code class="language-js">const generateTimeString = (time, lang = &#39;ko&#39;) =&gt; {
  const formatter = new Intl.RelativeTimeFormat(lang, {
    numeric: &#39;always&#39;,
  });
  const passed = new Date() - new Date(time);
}</code></pre>
<p><code>generateTimeString</code> 함수는 시간과 언어를 파라미터로 받는다. 언어는 한국어를 기본 값으로 설정했다.</p>
<p><code>nemeric</code> 옵션은 날짜 형식을 어떻게 표현할지 설정할 수 있다. </p>
<p><strong>nemeric 옵션 설정에 따른 결과</strong></p>
<ul>
<li><code>auto</code> : <code>어제</code>, <code>그저께</code></li>
<li><code>always</code> : <code>1일 전</code>, <code>2일 전</code></li>
</ul>
<h3 id="시간-표현-형식">시간 표현 형식</h3>
<p>시간을 어떻게 표현할지 형식을 정해서 <strong>해당 구간</strong>에 맞는 결과를 줘야한다.</p>
<table>
<thead>
<tr>
<th>구간</th>
<th>표현</th>
</tr>
</thead>
<tbody><tr>
<td>1분 미만</td>
<td><code>s</code>초 전</td>
</tr>
<tr>
<td>1분 이상 ~ 1시간 미만</td>
<td><code>m</code>분 전</td>
</tr>
<tr>
<td>1시간 이상 ~ 1일 미만</td>
<td><code>h</code>시간 전</td>
</tr>
<tr>
<td>1일 이상 ~ 1달 미만</td>
<td><code>d</code>일 전</td>
</tr>
<tr>
<td>1달 이상</td>
<td><code>yyyy</code>년 <code>mm</code>월 <code>dd</code>일</td>
</tr>
</tbody></table>
<p>한 달의 기준이 29일, 30일, 31일 이렇게 다양하니, 한 달이 지나가면 <strong>연월일</strong>로 표시하도록 했다.</p>
<h3 id="시간-단위-체크하기">시간 단위 체크하기</h3>
<p><strong>시간 표현 형식</strong>에 맞게 날짜를 표현하려면 각 시간의 단위를 체크해야 한다.</p>
<p>JavaScript는 시간의 단위가 <code>millisecond</code>인 점을 유의해야 한다.</p>
<table>
<thead>
<tr>
<th>시간</th>
<th>millisecond로 계산</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>1초</td>
<td><code>1000</code></td>
<td>1000</td>
</tr>
<tr>
<td>1분</td>
<td><code>1000 * 60</code></td>
<td>60000</td>
</tr>
<tr>
<td>1시간</td>
<td><code>1000 * 60 * 60</code></td>
<td>3600000</td>
</tr>
<tr>
<td>1일</td>
<td><code>1000 * 60 * 60 * 24</code></td>
<td>86400000</td>
</tr>
</tbody></table>
<p>우선 1달은 30이라고 가정했으며, 구간의 조건을 파악해보자.</p>
<table>
<thead>
<tr>
<th>구간</th>
<th>표현</th>
<th>조건1</th>
<th>조건2</th>
</tr>
</thead>
<tbody><tr>
<td>1분 미만</td>
<td><code>s</code>초 전</td>
<td>60000(1분) 미만</td>
<td>-</td>
</tr>
<tr>
<td>1분 이상 ~ 1시간 미만</td>
<td><code>m</code>분 전</td>
<td>60000(1분) 이상</td>
<td>3600000(1시간) 미만</td>
</tr>
<tr>
<td>1시간 이상 ~ 1일 미만</td>
<td><code>h</code>시간 전</td>
<td>3600000(1시간) 이상</td>
<td>86400000(1일) 미만</td>
</tr>
<tr>
<td>1일 이상 ~ 1달 미만</td>
<td><code>d</code>일 전</td>
<td>86400000(1일) 이상</td>
<td>2592000000(1달) 미만</td>
</tr>
<tr>
<td>1달 이상</td>
<td><code>yyyy</code>년 <code>mm</code>월 <code>dd</code>일</td>
<td>2592000000(1달) 이상</td>
<td>-</td>
</tr>
</tbody></table>
<p>시간 단위를 체크하기 위해 객체를 만들고, 객체 안에 함수를 선언했다.</p>
<pre><code class="language-js">const checkTime = {
  isSecond: (time) =&gt; time &lt; 60000,
  isMinute: (time) =&gt; 60000 &lt;= time &amp;&amp; time &lt; 3600000,
  isHour: (time) =&gt; 3600000 &lt;= time &amp;&amp; time &lt; 86400000,
  isDay: (time) =&gt; 86400000 &lt;= time &amp;&amp; time &lt; 2592000000,
  isOverOneMonth: (time) =&gt; 2592000000 &lt;= time,
};</code></pre>
<p>그리고 각 조건을 체크해서 각 시간대에 맞는 문자열을 return 해도록 했다.</p>
<pre><code class="language-js">const generateTimeString = (time, lang = &#39;ko&#39;) =&gt; {
  const formatter = new Intl.RelativeTimeFormat(lang, {
    numeric: &#39;always&#39;,
  });
  const passed = new Date() - new Date(time);
  // 조건 추가
  if (checkTime.isSecond(passed)) {
    return &#39;1분 미만&#39;;
  }
  if (checkTime.isMinute(passed)) {
    return &#39;1시간 미만&#39;;
  }
  if (checkTime.isHour(passed)) {
    return &#39;1일 미만&#39;;
  }
  if (checkTime.isDay(passed)) {
    return &#39;1달 미만&#39;;
  }
  if (checkTime.isOverOneMonth(passed)) {
    return &#39;1달 이상&#39;;
  }
};</code></pre>
<h4 id="테스트-해보기">테스트 해보기</h4>
<pre><code class="language-js">const second = new Date();
second.setSeconds(second.getSeconds() - 1);
console.log(generateTimeString(second)); // 1분 미만

const minute = new Date();
minute.setMinutes(minute.getMinutes() - 1);
console.log(generateTimeString(minute)); // 1시간 미만

const hour = new Date();
hour.setHours(hour.getHours() - 1);
console.log(generateTimeString(hour)); // 1일 미만

const yesterday = new Date();
yesterday.setHours(yesterday.getHours() - 24);
console.log(generateTimeString(yesterday)); // 1달 미만

const dayBeforYesterday = new Date();
dayBeforYesterday.setHours(dayBeforYesterday.getHours() - 48);
console.log(generateTimeString(dayBeforYesterday)); // 1달 미만

const month = new Date();
month.setMonth(month.getMonth() - 1);
console.log(generateTimeString(month)); // 1달 이상</code></pre>
<h3 id="계산하기">계산하기</h3>
<p>이제는 시간대에 맞게 <strong>계산</strong>을 해줘야한다.</p>
<p>시간 차이가 1초대인데, 1시간 단위로 시간을 계산해서 출력하면 안되기 때문이다.</p>
<p>이번에도 객체를 선언하고, 객체 안에 시간단위에 맞게 계산해주는 함수들을 선언했다.</p>
<pre><code class="language-js">const calcTime = {
  second: (time) =&gt; Math.floor(time / 1000),
  minute: (time) =&gt; Math.floor(time / 60000),
  hour: (time) =&gt; Math.floor(time / 3600000),
  day: (time) =&gt; Math.floor(time / 86400000),
};</code></pre>
<p>이제 각 단위에 맞게 시간을 계산한 결과를 return 해주면 끝이다!</p>
<pre><code class="language-js">const generateTimeString = (time, lang = &#39;ko&#39;) =&gt; {
  const formatter = new Intl.RelativeTimeFormat(lang, {
    numeric: &#39;always&#39;,
  });
  const passed = new Date() - new Date(time);
  if (checkTime.isSecond(passed)) {
    // 초 단위
    return formatter.format(-calcTime.second(passed), &#39;second&#39;);
  }
  if (checkTime.isMinute(passed)) {
    // 분 단위
    return formatter.format(-calcTime.minute(passed), &#39;minute&#39;);
  }
  if (checkTime.isHour(passed)) {
    // 시간 단위
    return formatter.format(-calcTime.hour(passed), &#39;hour&#39;);
  }
  if (checkTime.isDay(passed)) {
    // 일 단위
    return formatter.format(-calcTime.day(passed), &#39;day&#39;);
  }
  if (checkTime.isOverOneMonth(passed)) {
    // yyyy년 mm월 dd일
    return new Intl.DateTimeFormat(lang, {
      year: &#39;numeric&#39;,
      month: &#39;short&#39;,
      day: &#39;numeric&#39;,
    }).format(new Date(time));
  }
};</code></pre>
<p><a href="#%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%B4%EB%B3%B4%EA%B8%B0">테스트 해보기</a>에 있는 코드를 실행시켜보자!</p>
<pre><code>1초 전
1분 전
1시간 전
1일 전
2일 전
2022년 11월 16일</code></pre><p>콘솔 출력 결과다. 의도한 대로 시간대에 맞게 잘 출력해주고 있다.</p>
<pre><code class="language-js">const formatter = new Intl.RelativeTimeFormat(lang, {
  numeric: &#39;auto&#39;,
});</code></pre>
<p><code>numeric</code>을 <code>auto</code>로 바꿔보고 다시 실행해보자.</p>
<pre><code>1초 전
1분 전
1시간 전
어제
그저께
2022년 11월 16일</code></pre><p>1일 전을 <code>어제</code>, 2일 전을 <code>그저께</code>로 표현해주고 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React useModal hook 만들기]]></title>
            <link>https://velog.io/@seo__namu/React-useModal-hook-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/React-useModal-hook-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 15 Dec 2022 07:48:02 GMT</pubDate>
            <description><![CDATA[<p>모달 컴포넌트를 만들어서 사용하는데, 모달의 렌더링 여부가 결정되는 <code>state</code>를 모달 컴포넌트에게 위임할 수 없을까하는 생각이 들었다.</p>
<p><strong>기존 코드 예시</strong></p>
<pre><code class="language-jsx">import { useState } from &quot;react&quot;;
import Modal from &quot;../components/modal/modal&quot;;

export default function App() {
  const [isOpen, setIsOpen] = useState(false);
  const open = () =&gt; {
    setIsOpen(() =&gt; true);
  };
  const close = () =&gt; {
    setIsOpen(() =&gt; false);
  };
  return (
    &lt;div&gt;
      &lt;button onClick={open}&gt;open modal&lt;/button&gt;
      &lt;Modal open={isOpen}&gt;
        &lt;p&gt;삭제하시겠습니까?&lt;/p&gt;
        &lt;div&gt;
          &lt;button onClick={close}&gt;OK&lt;/button&gt;
          &lt;button onClick={close}&gt;Cancle&lt;/button&gt;
        &lt;/div&gt;
      &lt;/Modal&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p><code>Modal</code> 컴포넌트는 props로 받은 <code>isOpen</code>이 true일 경우에만 모달을 보여주고, false인 경우 null을 반환해서 아무것도 렌더링 되지 않도록 했다.</p>
<p>이렇게하면 모달을 사용하는 모든 화면 혹은 컴포넌트에서 모달의 상태 값을 가지고 있어야 하는데, 이 부분이 너무 효율적이지 않았다.</p>
<blockquote>
<p>이럴 때는 <code>hook</code>을 만들어서 사용하자!</p>
</blockquote>
<h2 id="modal-component">Modal Component</h2>
<p>먼저 모달 컴포넌트를 수정했다.</p>
<p>기존에는 props로 <code>isOpen</code>을 받아서 렌더링 여부를 모달 컴포넌트 내부에서 결정했지만, 이 일을 <code>hook</code>에게 위임할 예정이다.</p>
<pre><code class="language-jsx">// css
const modalWrapStyle = {
  width: &#39;100%&#39;, 
  height: &#39;100vh&#39;, 
  overflow: &#39;hidden&#39;, 
  position: &#39;fixed&#39;, 
  top: 0, left: 0, 
  backgroundColor: &#39;rgba(0,0,0,0.3)&#39;,
  display: &#39;flex&#39;,
  justifyContent: &#39;center&#39;,
  alignItems: &#39;center&#39;,
  textAlign: &#39;center&#39;
}

const modalStyle = {
  width: 380, 
  padding: 20, 
  backgroundColor: &#39;#fff&#39;
}

// 어떠한 작업도 하지 않고 모달을 보여주는 역할만 함
export default function Modal({ onClose, children }) {
  return (
    &lt;div 
      className=&quot;modal&quot;
      style={{...modalWrapStyle}}
      onClick={onClose}
    &gt;
      &lt;div 
        className=&quot;modal_container&quot;
        style={{...modalStyle}}
        onClick={(e) =&gt; e.stopPropagation()}
      &gt;
        {children}
      &lt;/div&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>class명이 <code>modal</code>인 div는 모달의 외부 배경이다. </p>
<p>배경을 클릭하면 모달이 닫히도록 할 것이기 때문에 <code>onClick</code> 이벤트 발생시 hook에서 받아온 <code>onClose</code> 함수를 호출해서 모달을 닫는다.</p>
<p>class명이 <code>modal_container</code>인 div가 모달이다.</p>
<p><code>modal_container</code>를 클릭하면 부모에게 이벤트가 전달되기 때문에 <code>onClick={(e) =&gt; e.stopPropagation()}</code>로 이벤트가 전달되는 것을 막았다.</p>
<h2 id="usemodal-hook">useModal hook</h2>
<p>모달의 상태 값을 가지며 제어할 수 있는 <code>useModal</code> hook이다.</p>
<pre><code class="language-js">import React, { useCallback, useState } from &#39;react&#39;;
import Modal from &#39;../components/modal/modal&#39;;

// `useBlur` props로 모달 외부를 클릭하면 모달을 닫을지 선택하도록 했다.
const useModal = ({ useBlur = true } = {}) =&gt; {
  // 모달의 렌더링 여부를 설정할 상태 값
  const [isOpen, setIsOpen] = useState(false);

  // 모달 열기
  const open = useCallback(() =&gt; {
    setIsOpen(() =&gt; true);
  }, []);

  // 모달 닫기
  const close = useCallback(() =&gt; {
    setIsOpen(() =&gt; false);
  }, []);

  // isOpen이 true라면 Modal 컴포넌트를 반환, false라면 null을 반환
  return {
    Modal: isOpen
      ? ({ children }) =&gt; (
          &lt;Modal onClose={useBlur ? close : null}&gt;{children}&lt;/Modal&gt;
        )
      : () =&gt; null,
    open,
    close,
    isOpen,
  };
};

export default useModal;</code></pre>
<p><strong>return</strong> 할 때 자체적으로 <code>isOpen</code>을 체크해서 <code>true</code>일 경우에만 Modal 컴포넌트를 반환해주고, <code>false</code>인 경우에는 null을 반환하도록 한다.</p>
<h2 id="usemodal-사용하기">useModal 사용하기</h2>
<p>useModal hook을 사용하면 이렇게 간단하게 모달 컴포넌트를 사용할 수 있게 된다.</p>
<pre><code class="language-jsx">import useModal from &#39;../hooks/useModal&#39;;

export default function App() {
  const { Modal, open, close } = useModal();

  return (
    &lt;div&gt;
      &lt;button onClick={open}&gt;open modal&lt;/button&gt;
      &lt;Modal&gt;
        &lt;p&gt;삭제하시겠습니까?&lt;/p&gt;
        &lt;div&gt;
          &lt;button onClick={close}&gt;OK&lt;/button&gt;
          &lt;button onClick={close}&gt;Cancle&lt;/button&gt;
        &lt;/div&gt;
      &lt;/Modal&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p><code>isOpen</code>으로 모달이 보여지는지 체크할 필요가 없어졌으며, <code>Modal</code> 컴포넌트는 <code>isOpen</code>이 true일 때만 보여지는 것을 <strong>보장</strong>받는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Error Boundary로 비동기 에러 잡아내기]]></title>
            <link>https://velog.io/@seo__namu/React-Error-Boundary%EB%A1%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%97%90%EB%9F%AC-%EC%9E%A1%EC%95%84%EB%82%B4%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/React-Error-Boundary%EB%A1%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%97%90%EB%9F%AC-%EC%9E%A1%EC%95%84%EB%82%B4%EA%B8%B0</guid>
            <pubDate>Fri, 09 Dec 2022 08:12:29 GMT</pubDate>
            <description><![CDATA[<p><span style="color: red">이 글은 공부하면서 작성한 글입니다. 이 글에 나와있는 방법이 절대적인 정답은 아니기 때문에 스스로의 판단하에 적용여부를 결정하시기 바랍니다.</span></p>
<p>React를 사용해서 개발을 하면서 항상 드는 의문이 있었다. </p>
<p>&quot;비동기 에러를 처리하는 더 좋은 방법이 없을까? 정말 이게 최선일까?&quot;</p>
<h2 id="내가-비동기-에러를-처리하던-방법">내가 비동기 에러를 처리하던 방법</h2>
<p>내가 비동기 에러를 처리하던 방법을 설명하기 위해 간단한 코드를 작성했다.</p>
<p><strong>api &gt; <code>users.js</code></strong></p>
<pre><code class="language-js">// 사용자 목록
const users = [
  {
    id: &#39;A&#39;,
    name: &#39;UserA&#39;,
    email: &#39;A@example.com&#39;
  },
  {
    id: &#39;B&#39;,
    name: &#39;UserB&#39;,
    email: &#39;B@example.com&#39;
  },
  {
    id: &#39;C&#39;,
    name: &#39;UserC&#39;,
    email: &#39;C@example.com&#39;
  }
];

// 사용자 목록을 가져옴
export const getAllUsers = (rejected = false) =&gt; new Promise((resolve, reject) =&gt; {
  setTimeout(() =&gt; {
    // reject 여부 옵션이 true면
    if (rejected) {
      // Promise reject 처리 (실패)
      reject(new Error(&#39;Error!&#39;));
      return;
    }
    // reject 여부 옵션이 false면
    // Promise resolve 처리 (성공)
    resolve(users);
  }, 2000);
});</code></pre>
<p>실제로 서버와 통신하는 코드를 작성할 수 없어서, <code>rejected</code> 여부를 <strong>옵션</strong>으로 받아서 이에 따라 <strong>성공, 실패 처리</strong>를 하는 함수를 만들었다.</p>
<p>성공시 사용자 목록을 반환하고, 실패시 <code>Error</code> 클래스의 인스턴스를 생성해서 에러를 발생시킨다.</p>
<p><strong>views &gt; <code>example.jsx</code></strong></p>
<pre><code class="language-jsx">import { useState } from &quot;react&quot;;
import { getAllUsers } from &quot;../api/users&quot;;

export default function Example() {
  const [users, setUsers] = useState([]);
  // fetch 버튼 클릭 이벤트
  const onClickFetch = async () =&gt; {
    try {
      // 사용자 정보를 가져옴
      const response = await getAllUsers(true);
      setUsers(response); // 실행 X
    } catch (error) {
      // rejected 옵션이 true이므로 catch로 에러를 잡음
      window.alert(error); // 실행 O
    }
  };
  return (
    &lt;div&gt;
      &lt;button onClick={onClickFetch}&gt;Fetch&lt;/button&gt;
      &lt;ul&gt;
        {
          users.map((user) =&gt; (
            &lt;li key={user.id}&gt;
              &lt;p&gt;Name: {user.name}&lt;/p&gt;
              &lt;p&gt;Email: {user.email}&lt;/p&gt;
            &lt;/li&gt;
          ))
        }
      &lt;/ul&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p><code>getAllUsers</code> 함수를 사용하는 <code>Example</code> 컴포넌트다.</p>
<p>서버와 통신하는 로직 자체는 파일을 따로 분리했지만, 통신 후 에러가 발생하면 <code>catch</code>로 에러를 잡아서 처리해주는 코드는 화면 컴포넌트 내부에 작성할 수 밖에 없다.</p>
<p>항상 catch로 에러를 잡아서 처리하는 코드를 작성하면, 나중에 에러를 처리하던 방식이 <code>alert</code>에서 <code>confirm</code>으로 바뀌게 되었을 때 <strong>모든 코드</strong>를 찾아가서 수정해야 하는 불상사가 날 수 있다. <del>실제로 경험도 해봤다.</del></p>
<p>그래서 <code>getAllUsers</code> 함수를 호출한 후에 작성하는 코드들은 전부 서버 통신이 <strong>성공적</strong>으로 처리 되었을 때만 실행되는 코드임을 <strong>보장</strong>하고, 에러 발생시 다른 모듈에게 에러 처리를 <strong>위임</strong>하면 좋겠다는 생각이 들었다.</p>
<pre><code class="language-jsx">const onClickFetch = async () =&gt; {
  const response = await getAllUsers(false);
  // 비동기 처리 이후에 작성한 코드는
  // 반드시 비동기 처리가 성공했을 경우 실행되는 것을 보장받음
  setUsers(response);
};</code></pre>
<p>이렇게 하면 Example 컴포넌트에서는 에러가 발생했을 때를 고려하지 않고, <strong>명확히</strong> 해당 컴포넌트가 처리해야 할 부분에 집중할 수 있다.</p>
<p>이런 생각을 하게된 것은 <code>Nestjs</code>를 경험해봤었기 때문이다. Nestjs는 에러를 던지면, 에러 유형에 맞게 <code>response</code>를 보내준다.</p>
<p>그래서 프론트엔드에서도 에러가 발생하면 <strong>에러 담당 컴포넌트</strong>가 에러 <strong>유형</strong>에 맞게 처리를 해주도록 코드를 작성해보고 싶었다.</p>
<h2 id="react-공식문서에-나와있는-error-boundary">React 공식문서에 나와있는 Error Boundary</h2>
<p>먼저 공식문서에서 제공해주는 방법을 찾아봤다.</p>
<blockquote>
<p>에러 경계는 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여주는 React 컴포넌트입니다. 에러 경계는 렌더링 도중 생명주기 메서드 및 그 아래에 있는 전체 트리에서 에러를 잡아냅니다.
출처: <a href="https://ko.reactjs.org/docs/error-boundaries.html">[React] 에러 경계(Error Boundaries)</a></p>
</blockquote>
<p>공식문서에 나와있는 Error Boundary 컴포넌트는 <strong>렌더링 도중</strong>에 발생하는 에러를 잡아낸다. 그리고 <code>비동기적 코드</code>에서 발생한 오류를 잡아내지 못한다고 나와있다.</p>
<p>비동기적 코드에서 발생한 오류를 잡지 못한다면 아무런 소용이 없었다. </p>
<p>다른 방법이 있을거라는 생각으로 검색을 해본 결과 stackoverflow에서 <code>unhandledrejection</code> 이벤트를 사용하면 된다는 글을 보게되었다.</p>
<h2 id="unhandledrejection-이벤트">unhandledrejection 이벤트</h2>
<p>unhandledrejection 이벤트는 웹 브라우저 환경에서 사용할 수 있는 이벤트다.</p>
<p>처리되지 못한 <strong>Promise 거부</strong>, 즉 <strong>reject</strong>가 발생하면 이 이벤트가 발생한다.</p>
<p><strong>views &gt; <code>example.jsx</code></strong></p>
<pre><code class="language-jsx">export default function Example() {
  // 생략

  // Promise 거부가 발생하면 실행
  const captureReject = (e) =&gt; {
    // 이벤트 버블링 방지
    e.preventDefault();
    console.log(e.reason);
    console.log(e.reason instanceof Error);
  };

  useEffect(() =&gt; {
    // 이벤트 등록
    window.addEventListener(&#39;unhandledrejection&#39;, captureReject);
    return () =&gt; {
      // 컴포넌트가 사라질 때 이벤트 삭제
      window.removeEventListener(&#39;unhandledrejection&#39;, captureReject);
    }
  }, []);
  return (
    &lt;div&gt;
      {/* 생략 */}
    &lt;/div&gt;
  )
}</code></pre>
<p>catch로 에러를 처리하지 사용하지 않고, unhandledrejection 이벤트를 사용했다.</p>
<p><code>reason</code>에 에러 인스턴스가 담기는데, <code>instanceof</code>로 <code>Error</code> 클래스의 인스턴스인지 확인해보면 <code>true</code>로 나온다.</p>
<p>확인 결과 unhandledrejection 이벤트를 등록하면 하위 컴포넌트에서 발생한 비동기 오류를 상위 컴포넌트에서 잡아낼 수 있다는 것을 알게되었다.</p>
<h2 id="커스텀-error-boundary">커스텀 Error Boundary</h2>
<p><strong>boundary &gt; <code>errorBoundary.jsx</code></strong></p>
<pre><code class="language-jsx">import { useEffect } from &quot;react&quot;;

export default function ErrorBoundary({ children }) {
  const captureReject = (e) =&gt; {
    e.preventDefault();

    // e.reason이 Error의 인스턴스일 경우
    if (e.reason instanceof Error) {
      window.alert(e.reason.message);
      return;
    }

    console.log(&#39;처리하지 못한 비동기 오류입니다.&#39;);
  };

  useEffect(() =&gt; {
    window.addEventListener(&#39;unhandledrejection&#39;, captureReject);
    return () =&gt; {
      window.removeEventListener(&#39;unhandledrejection&#39;, captureReject);
    }
  }, []);

  return children;
}</code></pre>
<p>위에서 Example 컴포넌트에서 사용한 방법과 같으며, Example 컴포넌트에서 작성했던 unhandledrejection 이벤트 관련 코드는 지우면 된다.</p>
<p>이제 <code>ErrorBoundary</code> 컴포넌트로 <code>Example</code> 컴포넌트를 감싸주면 된다.</p>
<p><strong><code>App.js</code></strong></p>
<pre><code class="language-jsx">function App() {
  return (
    &lt;ErrorBoundary&gt;
      &lt;Example /&gt;
    &lt;/ErrorBoundary&gt;
  )
}

export default App;</code></pre>
<p>이제 Example 컴포넌트에서 발생한 비동기 에러를 ErrorBoundary 컴포넌트에서 잡아내 처리를 해줄 수 있게 되었다! 👏👏👏</p>
<h2 id="마지막으로">마지막으로</h2>
<p>사실 아직까지도 &quot;이 방법이 최선일까?&quot;하는 의문이 남아있다.</p>
<p>unhandledrejection 이벤트로 잡아낸 에러에 대해 <code>회복할 수 없는 에러</code>라고 표현한 글이 있었기 때문이다.</p>
<blockquote>
<p>대개 이런 에러는 회복할 수 없기 때문에 개발자로서 할 수 있는 최선의 방법은 사용자에게 문제 상황을 알리고 가능하다면 서버에 에러 정보를 보내는 것입니다.
출처: <a href="https://ko.javascript.info/promise-error-handling#ref-377">[모던 JavaScript 튜토리얼] 프라미스와 에러 핸들링</a></p>
</blockquote>
<p>그래도 내가 원하는 방식으로 비동기 에러를 처리하는 방법을 생각해보고 적용해보는 과정이 재미있었다.</p>
<p>더 좋은 방법이 있다면 댓글로 알려주세요! 🤗</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript 주석 정복하기]]></title>
            <link>https://velog.io/@seo__namu/JavaScript-%EC%A3%BC%EC%84%9D-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/JavaScript-%EC%A3%BC%EC%84%9D-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 08 Dec 2022 05:47:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>주석 또는 코멘트(comment)는 프로그래밍에 있어 내용을 메모하는 목적으로 쓰인다.
소스 코드를 더 쉽게 이해할 수 있게 만드는 것이 주 목적이며, 협업할 때 유용히 쓰인다. 컴파일러와 인터프리터에 의해 일반적으로 무시되어 프로그램에 영향을 주지 않는다.
출처: <a href="https://ko.wikipedia.org/wiki/%EC%A3%BC%EC%84%9D_(%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D)">[위키백과] 주석 (프로그래밍)</a></p>
</blockquote>
<p>쉽게 말하면 <code>주석</code>은 작성한 코드에 대한 <strong>설명</strong>을 적고싶을 때 사용한다.</p>
<h2 id="주석의-중요성">주석의 중요성</h2>
<p>어렴풋이,, 주석없이 함수의 이름만 봐도 어떤 일을 수행하는 함수인지 알 수 있도록 코드를 작성하는게 <strong>진정한 개발자</strong>라는 말을 들은 기억이 있다.</p>
<p>이 말이 틀린건 아니겠지만, 나는 개인적으로 협업을 해야하는 프로젝트에서 주석은 필수라고 생각한다.</p>
<p>소프트웨어는 한번 개발을 하고 뒤돌아서 끝내는게 아니다. 계속해서 수정하고 다듬어가면서 사용자의 요구사항에 맞춰가야 한다. </p>
<p>처음 코드는 내가 작성했어도 수정할 때는 <strong>다른 개발자</strong>가 코드를 작성할 수 있기 때문에 코드를 <strong>이해하는 시간</strong>을 줄이기 위해서는 주석을 꼼꼼하게 작성하는 습관이 필요하다. <del>나는 내가 짠 코드도 일주일만 지나면 까먹는다..</del></p>
<p>솔직히 나는 그동안 주석을 꼼꼼히 작성하지 않았었는데, 최근 <strong>주석의 중요성</strong>을 느끼게 되면서 자바스크립트에서 사용 가능한 다양한 주석에 대해서 소개해보려고 한다.</p>
<p>내가 주석의 중요성을 느끼게 된 것에 대해 먼저 작성했는데, 궁금하지 않다면 <a href="#%EB%8B%A4%EC%96%91%ED%95%9C-%EC%A3%BC%EC%84%9D-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">다양한 주석 알아보기</a>부터 보면 된다.</p>
<h3 id="내가-다-아는-코드야">내가 다 아는 코드야</h3>
<p>회사에서 혼자 <code>Vue</code>를 사용해 프론트엔드 개발을 맡아서 진행하던 <code>프로젝트 B</code>가 있다.</p>
<p>정규 프로젝트는 아니었지만 총 22개의 페이지를 만들었으며, 각 페이지에서 필요한 기능들도 많이 개발을 해왔다.</p>
<p>페이지가 22개라고 하니까 정말 많아 보이지만,, 약 15개는 간단한 데이터 CRUD를 하는 단순한 페이지들이다. </p>
<p>처음에는 작게 시작한 프로젝트였는데 규모가 점점 커지면서 코드량이 많아졌지만 <strong>&quot;어차피 정규 프로젝트도 아니니까&quot;</strong>, <strong>&quot;내가 다 아는 코드야&quot;</strong> 등의 핑계를 대면서 주석을 작성하지 않았다.</p>
<p>그러던 중 <code>프로젝트 B</code>가 중단되고,, 회사에서 기존에 <code>C#</code>으로 진행하던 <code>프로젝트 A</code>에 투입되면서 <strong>작은 버그</strong>들을 고치거나 <strong>기능을 수정</strong>하는 일을 맡게 되었다.</p>
<h3 id="천-줄은-기본이지-변수명은-item1">천 줄은 기본이지! 변수명은 item1</h3>
<p><code>프로젝트 A</code>는 3년 동안 진행되면서 수 많은 개발자들이 거쳐간 규모가 큰 프로젝트다.</p>
<p>화면 하나당 코드량이 <strong>기본 천 줄</strong>이었고, 많은건 <strong>삼천 줄</strong>도 넘어갔다. </p>
<p>그런데 <code>item1</code>, <code>item2</code> 이런 이름을 가진 변수들도 많았고, 주석이 없는 경우도 꽤 있었다. 대충 지은 변수명과 주석 없는 코드들을 보고 막막한 마음부터 들었다.</p>
<p>난이도 높은 버그는 아니였지만 기존 코드들을 <strong>분석</strong>하면서 <strong>원인</strong>을 파악하고 수정하다보니 꽤 오랜시간이 걸렸다.</p>
<p>이렇게 몇 개의 버그와 기능들을 수정하면서 <code>주석이 정말 중요하구나.</code>라는 것을 느끼게 되었다. </p>
<h2 id="다양한-주석-알아보기">다양한 주석 알아보기</h2>
<h3 id="1-기본-주석">1. 기본 주석</h3>
<p>먼저 기본 주석이다. 보통 <code>자바스크립트 주석</code>이라고 검색하면 나오는 주석이다.</p>
<pre><code class="language-js">// 한 줄 주석

/**
 * 두 줄 이상
 * 주석
 */</code></pre>
<h3 id="2-jsdoc">2. JSDoc</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/4f4068a6-b116-4aec-9647-53634e04a6fd/image.png" alt=""></p>
<blockquote>
<p><a href="https://jsdoc.app/">JSDoc</a>은 자바스크립트 API 문서 생성기다. 자바스크립트 소스코드에 JSDoc 형식의 주석을 추가하면 API를 설명하는 HTML 문서를 생성할 수 있다.
출처: <a href="https://poiemaweb.com/jsdoc-type-hint">15.9 JSDoc을 사용하여 자바스크립트에 타입 힌트 제공하기</a></p>
</blockquote>
<p>JSDoc를 사용하면 함수의 <code>기능</code>, <code>파라미터</code>, <code>return 값</code>에 대한 설명을 작성할 수 있고 해당 함수를 사용할 때 작성한 정보를 표시해줄 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/6eb4b64e-83d9-4c03-a8d2-455fb9f4ab67/image.png" alt=""></p>
<p>자바스크립트로 코드를 작성하면 모든 타입이 <code>any</code>로 추론된다. 그래서 정확히 어떤 타입의 데이터를 파라미터로 넘겨야 하는지 알려면 <strong>함수 선언부</strong>에 가서 작성된 주석을 보거나 코드를 직접 해석해야 한다.</p>
<p>그런데 JSDoc로 주석을 작성해두면 함수를 선언한 곳에 찾아가서 코드를 해석하지 않아도, 함수의 기능과 파라미터 타입에 대해 알 수 있어 매우 유용하다.</p>
<p><strong>함수</strong></p>
<pre><code class="language-js">/**
 * a와 b를 더한 결과를 반환
 * @param {number} a 첫번째 숫자
 * @param {number} b 두번째 숫자
 * @returns {number} a와 b를 더한 결과
 */
function plus(a, b) {
  return a + b;
}</code></pre>
<p>plus함수에 대한 주석이다. 맨 첫번째 줄은 함수에 대한 설명이고, <code>@param</code>은 파라미터, <code>@returns</code>은 리턴 데이터에 대한 설명을 적을 수 있는 <code>태그</code>다.</p>
<p>파라미터에 대한 주석은 <code>태그유형</code> <code>{타입}</code> <code>변수명</code> <code>설명</code> 순으로 작성하면 된다.</p>
<p><strong>변수</strong></p>
<pre><code class="language-js">/** @type {number} 이름 */
let name = &#39;&#39;;</code></pre>
<p>변수에 대한 주석은 <code>태그유형</code> <code>{타입}</code> <code>설명</code> 순으로 작성하면 된다.</p>
<p><a href="https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html">JSDoc 참조</a>에서 다양한 종류의 태그들을 더 볼 수 있다.</p>
<h3 id="3-region">3. region</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/c620843d-c317-4824-af00-0cd3760140d8/image.png" alt=""></p>
<p>region 주석은 코드를 <strong>묶어주는 기능</strong>을 한다.</p>
<pre><code class="language-js">//#region 숫자 계산 함수
/**
 * a와 b를 더한 결과를 반환
 * @param {number} a 첫번째 숫자
 * @param {number} b 두번째 숫자
 * @returns {number} a와 b를 더한 결과
 */
 function plus(a, b) {
  return a + b;
}

/**
 * a에서 b를 뺀 결과를 반환
 * @param {number} a 첫번째 숫자
 * @param {number} b 두번째 숫자
 * @returns {number} a에서 b를 뺀 결과
 */
 function minus(a, b) {
  return a - b;
}
//#endregion</code></pre>
<p>코드를 묶을 시작점에서 <code>//#region 설명</code>을 작성해주고, 끝나는 지점에는 <code>//#endregion</code>을 작성하면 된다. </p>
<h2 id="주석을-어떻게-작성하면-잘-작성했다고-소문이-날까">주석을 어떻게 작성하면 잘 작성했다고 소문이 날까?</h2>
<p>주석을 작성하는 방법을 설명하기 위해 회원가입을 처리하는 코드를 간단하게 작성해봤다.</p>
<pre><code class="language-js">// 닉네임
let nickname = &#39;&#39;;
// 이메일
let email = &#39;&#39;;
// 비밀번호
let password = &#39;&#39;;

// 회원가입 클릭 이벤트. 회원가입 결과 콘솔에 출력
async function onClickRegister(nickname, email, password) {
  if (!isValidNickname(nickname)) {
    console.log(&#39;Invalid nickname&#39;);
    return;
  }
  if (!isValidEmail(email)) {
    console.log(&#39;Invalid email&#39;);
    return;
  }
  if (!isValidPassword(password)) {
    console.log(&#39;Invalid password&#39;);
    return;
  }
  const response = await register(nickname, email, password);
  console.log(response);
}

// 회원가입 처리
async function register(nickname, email, password) {
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      resolve(&#39;Register Successful!&#39;);
    }, 2000);
  });
}

// 닉네임 유효성 검사
function isValidNickname(nickname) {
  return /^[가-힣]+$/.test(nickname);
}

// 이메일 유효성 검사
function isValidEmail(email) {
  return /[a-z0-9]+@[a-z]+\.[a-z]{2,3}/.test(email);
}

// 비밀번호 유효성 검사
function isValidPassword(password) {
  return /^[a-z0-9_-]{6,18}$/.test(password);
}

// 코드 실행 결과를 보기 위한 임시 코드
nickname = &#39;서나무&#39;;
email = &#39;seo.namu@example.com&#39;;
password = &#39;1234qwer&#39;;

onClickRegister(nickname, email, password);</code></pre>
<p>코드를 보면 <strong>회원가입 관련</strong>된 함수가 <strong>두 개</strong>나 있다. </p>
<ul>
<li><code>register</code> : 서버와 통신해서 회원가입을 처리하는 함수</li>
<li><code>onClickRegister</code> : 사용자가 회원가입 버튼을 클릭했을 때 실행시킬 함수<ul>
<li>닉네임, 이메일, 비밀번호 유효성 검사</li>
<li>register 함수를 호출한 결과를 콘솔에 출력</li>
</ul>
</li>
</ul>
<p>이렇게 코드를 작성하고 나면 여러 문제들이 발생할 수 있다.</p>
<ol>
<li>변수에 다른 자료형의 데이터를 넣는 <a href="https://ko.wikipedia.org/wiki/%EC%9D%B8%EC%A0%81%EC%98%A4%EB%A5%98">휴먼에러</a>가 발생할 수 있다. </li>
<li>회원가입 처리시 <code>onClickRegister</code>와 <code>register</code> 중 어떤 함수를 사용해야 하는지 알기 어렵다.</li>
<li>각 함수가 어떤 일을 하는지 알기 위해서는 함수 선언부로 가서 주석을 확인해야 한다.</li>
<li>나중에 혹은 다른 개발자가 파일을 열었을 때 어떤 코드들이 작성되어 있는지 한눈에 파악하기 어렵다.</li>
</ol>
<p>그러면 주석을 사용해서 위 문제들을 해결해보자!</p>
<h3 id="변수-주석-작성하기">변수 주석 작성하기</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/533e4901-d650-4645-be0c-87e2def5c524/image.png" alt=""></p>
<blockquote>
<ol>
<li><code>let</code>으로 선언한 변수에 다른 자료형의 데이터를 넣는 <a href="https://ko.wikipedia.org/wiki/%EC%9D%B8%EC%A0%81%EC%98%A4%EB%A5%98">휴먼에러</a>가 발생할 수 있다. </li>
</ol>
</blockquote>
<p>사실 문자열로 초기화를 해주면 타입 추론이 <code>string</code>으로 되지만, 변수에 대한 설명을 통해 어떤 데이터를 넣어야할지 알 수 있게 된다.</p>
<pre><code class="language-js">/** @type {string} 닉네임 */
let nickname = &#39;&#39;;
/** @type {string} 이메일 */
let email = &#39;&#39;;
/** @type {string} 비밀번호 */
let password = &#39;&#39;;</code></pre>
<h3 id="함수-주석-작성하기">함수 주석 작성하기</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/338e8a84-3f76-4db2-9d1f-c39791ecff24/image.png" alt=""></p>
<blockquote>
<ol start="2">
<li>회원가입 처리시 <code>onClickRegister</code>와 <code>register</code> 중 어떤 함수를 사용해야 하는지 알기 어렵다.</li>
<li>각 함수가 어떤 일을 하는지 알기 위해서는 함수 선언부로 가서 주석을 확인해야 한다.</li>
</ol>
</blockquote>
<p>함수를 사용할 때 함수의 기능에 대해 이해하기 쉽고, 파라미터도 어떤 자료형을 넣어줘야 할지 바로 알 수 있다.</p>
<pre><code class="language-js">/**
 * 회원가입 클릭 이벤트. 회원가입 결과 콘솔에 출력
 * @param {string} nickname 닉네임
 * @param {string} email 이메일
 * @param {string} password 비밀번호
 */
async function onClickRegister(nickname, email, password) {
  if (!isValidNickname(nickname)) {
    console.log(&#39;Invalid nickname&#39;);
    return;
  }
  if (!isValidEmail(email)) {
    console.log(&#39;Invalid email&#39;);
    return;
  }
  if (!isValidPassword(password)) {
    console.log(&#39;Invalid password&#39;);
    return;
  }
  const response = await register(nickname, email, password);
  console.log(response);
}

/**
 * 회원가입 처리
 * @param {string} nickname 닉네임
 * @param {string} email 이메일
 * @param {string} password 비밀번호
 * @returns {Promise&lt;string&gt;} 회원가입 결과 메시지
 */
async function register(nickname, email, password) {
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      resolve(&#39;Register Successful!&#39;);
    }, 2000);
  });
}

/**
 * 닉네임 유효성 검사
 * @param {string} nickname 닉네임
 * @returns {boolean} 유효성 검사 결과
 */
function isValidNickname(nickname) {
  return /^[가-힣]+$/.test(nickname);
}

/**
 * 이메일 유효성 검사
 * @param {string} email 이메일
 * @returns {boolean} 유효성 검사 결과
 */
function isValidEmail(email) {
  return /[a-z0-9]+@[a-z]+\.[a-z]{2,3}/.test(email);
}

/**
 * 비밀번호 유효성 검사
 * @param {string} password 비밀번호
 * @returns {boolean} 유효성 검사 결과
 */
function isValidPassword(password) {
  return /^[a-z0-9_-]{6,18}$/.test(password);
}</code></pre>
<h3 id="region-주석으로-그룹화하기">region 주석으로 그룹화하기</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/204d23d3-6a4f-4baf-b834-76ac227390f4/image.png" alt=""></p>
<blockquote>
<ol start="4">
<li>나중에 혹은 다른 개발자가 파일을 열었을 때 어떤 코드들이 작성되어 있는지 한눈에 파악하기 어렵다.</li>
</ol>
</blockquote>
<p>깔끔하게 정리되어 있어서 어떤 코드들이 있을지 파악이 가능하다.</p>
<pre><code class="language-js">
//#region 변수
/** @type {string} 닉네임 */
let nickname = &#39;&#39;;
/** @type {string} 이메일 */
let email = &#39;&#39;;
/** @type {string} 비밀번호 */
let password = &#39;&#39;;
//#endregion

//#region 이벤트
/**
 * 회원가입 클릭 이벤트. 회원가입 결과 콘솔에 출력
 * @param {string} nickname 닉네임
 * @param {string} email 이메일
 * @param {string} password 비밀번호
 */
async function onClickRegister(nickname, email, password) {
  if (!isValidNickname(nickname)) {
    console.log(&#39;Invalid nickname&#39;);
    return;
  }
  if (!isValidEmail(email)) {
    console.log(&#39;Invalid email&#39;);
    return;
  }
  if (!isValidPassword(password)) {
    console.log(&#39;Invalid password&#39;);
    return;
  }
  const response = await register(nickname, email, password);
  console.log(response);
}
//#endregion

//#region 함수
/**
 * 회원가입 처리
 * @param {string} nickname 닉네임
 * @param {string} email 이메일
 * @param {string} password 비밀번호
 * @returns {Promise&lt;string&gt;} 회원가입 결과 메시지
 */
async function register(nickname, email, password) {
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      resolve(&#39;Register Successful!&#39;);
    }, 2000);
  });
}

/**
 * 닉네임 유효성 검사
 * @param {string} nickname 닉네임
 * @returns {boolean} 유효성 검사 결과
 */
function isValidNickname(nickname) {
  return /^[가-힣]+$/.test(nickname);
}

/**
 * 이메일 유효성 검사
 * @param {string} email 이메일
 * @returns {boolean} 유효성 검사 결과
 */
function isValidEmail(email) {
  return /[a-z0-9]+@[a-z]+\.[a-z]{2,3}/.test(email);
}

/**
 * 비밀번호 유효성 검사
 * @param {string} password 비밀번호
 * @returns {boolean} 유효성 검사 결과
 */
function isValidPassword(password) {
  return /^[a-z0-9_-]{6,18}$/.test(password);
}
//#endregion</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript 얕은 복사 깊은 복사]]></title>
            <link>https://velog.io/@seo__namu/JavaScript-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC</link>
            <guid>https://velog.io/@seo__namu/JavaScript-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC</guid>
            <pubDate>Fri, 02 Dec 2022 01:28:48 GMT</pubDate>
            <description><![CDATA[<!-- 원시타입 참조타입에 대한 설명을 먼저 읽고 오는 것 권유 -->

<h2 id="얕은-복사">얕은 복사</h2>
<p>얕은 복사는 참조타입의 변수를 직접 대입해서, 같은 메모리 주소를 참조하게 되는 것을 의미한다.</p>
<h3 id="객체의-얕은-복사">객체의 얕은 복사</h3>
<pre><code class="language-js">// 초기 값을 가지는 init 객체
const init = {
  name: &#39;&#39;,
  job: &#39;&#39;,
};

// clone에 init 대입
const clone = init;
clone.name = &#39;서나무&#39;;
clone.job = &#39;Frontend developer&#39;;

console.log(init);
console.log(clone);</code></pre>
<p>코드를 실행시켜보면 <code>clone</code> 객체뿐 아니라 <code>init</code> 객체의 <code>name</code> <code>job</code> 도 함께 변경되었다.</p>
<h3 id="배열의-얕은-복사">배열의 얕은 복사</h3>
<pre><code class="language-js">// 초기 값을 가지는 init 배열
const init = [&#39;name&#39;, &#39;job&#39;];

// clone에 init 대입
const clone = init;
clone[1] = &#39;game&#39;;

console.log(init);
console.log(clone);</code></pre>
<p>코드를 실행시켜보면 <code>clone</code> 배열뿐 아니라 <code>init</code> 배열의 <code>index1</code>에 해당하는 요소도 함께 변경되었다.</p>
<h2 id="깊은-복사">깊은 복사</h2>
<p>깊은 복사란 <code>복사 대상 객체</code>에게 <strong>새로운 메모리</strong>에 할당해서 <code>복사 소스 객체</code>와 <strong>다른 메모리</strong> 주소를 참조하도록 하는 것을 의미한다.</p>
<h3 id="spread-연산자-전개-연산자">Spread 연산자 (전개 연산자)</h3>
<p>JavaScript에서 객체와 배열을 복사할 때, spread 연산자를 사용할 수 있다.</p>
<p><strong>객체</strong></p>
<pre><code class="language-js">const init = {
  name: &#39;&#39;,
  job: &#39;&#39;,
};

const clone = { ...init };
clone.name = &#39;서나무&#39;;
clone.job = &#39;Frontend developer&#39;;

console.log(init);
console.log(clone);</code></pre>
<p>코드를 실행시켜보면 <code>clone</code> 객체의 <code>name</code> <code>job</code>만 변경된 것을 볼 수 있다.</p>
<p><strong>배열</strong></p>
<pre><code class="language-js">const init = [&#39;name&#39;, &#39;job&#39;];

const clone = [...init];
clone[1] = &#39;game&#39;;

console.log(init);
console.log(clone);</code></pre>
<p>코드를 실행시켜보면 <code>clone</code> 배열의 <code>index1</code>에 해당하는 요소만 변경된 것을 볼 수 있다.</p>
<h3 id="객체-objectassign-함수">객체 Object.assign() 함수</h3>
<p>assign 함수는 첫번째 인자로 복사 <code>대상 객체</code>를 넣어주고, 두번째 인자로 복사할 <code>소스 객체</code>를 넣어주면 된다.</p>
<pre><code class="language-js">const init = {
  name: &#39;&#39;,
  job: &#39;&#39;,
};

const clone = Object.assign({}, init);
clone.name = &#39;서나무&#39;;
clone.job = &#39;Frontend developer&#39;;

console.log(init);
console.log(clone);</code></pre>
<p>코드를 실행시켜보면 <code>clone</code> 객체의 <code>name</code> <code>job</code>만 변경된 것을 볼 수 있다.</p>
<h3 id="배열-concat-함수">배열 concat() 함수</h3>
<p>concat 함수는 빈 배열을 선언하고 복사 <code>소스 배열</code>을 넣어주면 된다.</p>
<pre><code class="language-js">const init = [&#39;name&#39;, &#39;job&#39;];

const clone = [].concat(init);
clone[1] = &#39;game&#39;;

console.log(init);
console.log(clone);</code></pre>
<p>코드를 실행시켜보면 <code>clone</code> 배열의 <code>index1</code>에 해당하는 요소만 변경된 것을 볼 수 있다.</p>
<h2 id="javascript-깊은-복사-함수의-한계">JavaScript 깊은 복사 함수의 한계</h2>
<p>JavaScript에서 제공하는 전개 연산자, assign함수, concat함수는 치명적인 한계가 있다.</p>
<p>깊이가 1인 경우에만 깊은 복사가 되고, 깊이가 깊어지는 다차원 객체, 배열의 경우에는 얕은 복사가 된다는 것이다.</p>
<pre><code class="language-js">const init = {
  name: &#39;&#39;,
  job: &#39;&#39;,
  use: {
    lang: &#39;&#39;,
  },
};

const clone = { ...init };
clone.name = &#39;서나무&#39;;
clone.job = &#39;Frontend developer&#39;;
clone.use.lang = &#39;JavaScript&#39;;

console.log(init);
console.log(clone);</code></pre>
<p><strong>코드 실행 결과</strong></p>
<pre><code class="language-js">{ name: &#39;&#39;, job: &#39;&#39;, use: { lang: &#39;JavaScript&#39; } } // ! 얕은 복사
{ name: &#39;서나무&#39;, job: &#39;Frontend developer&#39;, use: { lang: &#39;JavaScript&#39; } }</code></pre>
<p><code>name</code> <code>job</code>은 깊은 복사가 됐지만, <code>use</code> 객체의 <code>lang</code>은 얕은 복사가 됐다.</p>
<h2 id="다차원-객체-배열-깊은-복사">다차원 객체, 배열 깊은 복사</h2>
<p>외부 라이브러리를 사용하지 않고 다차원 객체, 배열을 깊은 복사를 해보자.</p>
<h3 id="재귀-함수-구현하기">재귀 함수 구현하기</h3>
<p>검색을 해보니 객체와 배열이 혼합된 형태의 깊은 복사를 할 수 있는 재귀 함수 예제가 없었다.</p>
<p>그래서 직접 재귀 함수를 구현해봤다.</p>
<pre><code class="language-js">const deepCopy = (target) =&gt; {
  /**
   * 객체를 받아서 깊은 복사 후 반환하는 재귀함수
   * @param {Object} object 복사할 객체
   * @returns {Object} 복사된 객체
   */
  const deepCopyObject = (object) =&gt; {
    if (object === null || typeof object !== &#39;object&#39;) return object;
    const clone = {};
    for (const key in object) {
      if (Array.isArray(object[key])) {
        clone[key] = deepCopyArray(object[key]);
      } else {
        clone[key] = deepCopyObject(object[key]);
      }
    }
    return clone;
  };

  /**
   * 배열을 받아서 깊은 복사 후 반환하는 재귀함수
   * @param {Array} array 복사할 배열
   * @returns {Array} 복사된 배열
   */
  const deepCopyArray = (array) =&gt; {
    if (array === null || !Array.isArray(array)) return array;
    const clone = [];
    for (let key = 0; key &lt; array.length; key++) {
      if (Array.isArray(array[key])) {
        clone[key] = deepCopyArray(array[key]);
      } else {
        clone[key] = deepCopyObject(array[key]);
      }
    }
    return clone;
  };

  if (typeof target === &#39;object&#39; || Array.isArray(target)) {
    return Array.isArray(target)
      ? deepCopyArray(target)
      : deepCopyObject(target);
  }
  return target;
};</code></pre>
<p>재귀 함수를 이용해서 다차원 객체를 깊은 복사 해보자.</p>
<pre><code class="language-js">const init = {
  name: &#39;&#39;,
  job: &#39;&#39;,
  use: {
    lang: &#39;&#39;,
  },
  hobby: [
    {
      game: &#39;&#39;,
    },
  ],
};

const clone = deepCopy(init);
clone.use.lang = &#39;JavaScript&#39;;
clone.hobby[0].game = &#39;Crazy Arcade&#39;;

console.log(init);
console.log(clone);</code></pre>
<p><strong>코드 실행 결과</strong></p>
<pre><code class="language-js">{ name: &#39;&#39;, job: &#39;&#39;, use: { lang: &#39;&#39; }, hobby: [ { game: &#39;&#39; } ] }
{
  name: &#39;&#39;,
  job: &#39;&#39;,
  use: { lang: &#39;JavaScript&#39; },
  hobby: [ { game: &#39;Crazy Arcade&#39; } ]
}</code></pre>
<p>깊은 복사가 잘 되었고, 이는 객체 뿐 아니라 다차원 배열도 깊은 복사가 가능하다.</p>
<h3 id="json-객체-사용하기">JSON 객체 사용하기</h3>
<p>JSON 객체는 두 가지 메서드를 가지고 있다.</p>
<ul>
<li><code>stringify</code> : javascript 값을 JSON으로 변환 후 반환</li>
<li><code>parse</code> : JSON을 javascript 값으로 변환 후 반환</li>
</ul>
<p>복사할 소스 객체를 <code>stringify</code>메서드로 JSON으로 만든 후 다시 <code>parse</code>메서드로 javascript 값으로 변환하면 된다.</p>
<pre><code class="language-js">const init = {
  name: &#39;&#39;,
  job: &#39;&#39;,
  use: {
    lang: &#39;&#39;,
  },
};

const clone = JSON.parse(JSON.stringify(init));
clone.name = &#39;서나무&#39;;
clone.job = &#39;Frontend developer&#39;;
clone.use.lang = &#39;JavaScript&#39;;

console.log(init);
console.log(clone);</code></pre>
<h3 id="json-메서드는-정말-느릴까">JSON 메서드는 정말 느릴까?</h3>
<p>JSON 객체의 메서드를 사용하는 것은 매우 느리다는 의견이 있어서 정말 그런지 궁금했다. </p>
<p>그래서 콘솔로 JSON 메서드와 내가 구현한 재귀 함수의 실행 시간을 출력해봤다.</p>
<pre><code class="language-js">const init = {
  &#39;depth1-1&#39;: {
    dethp2: [
      &#39;depth3&#39;,
      {
        depth4: &#39;&#39;,
      },
      [
        {
          &#39;depth5-1&#39;: &#39;&#39;,
          &#39;depth5-2&#39;: &#39;&#39;,
          &#39;depth5-3&#39;: {
            depth6: &#39;&#39;,
          },
        },
      ],
    ],
  },
  &#39;depth1-2&#39;: {
    dethp2: [
      &#39;depth3&#39;,
      {
        depth4: &#39;&#39;,
      },
      [
        {
          &#39;depth5-1&#39;: &#39;&#39;,
          &#39;depth5-2&#39;: &#39;&#39;,
          &#39;depth5-3&#39;: {
            depth6: &#39;&#39;,
          },
        },
      ],
    ],
  },
};

console.time(&#39;Custom recursion function&#39;);
const clone1 = deepCopy(init);
console.timeEnd(&#39;Custom recursion function&#39;);

console.time(&#39;JSON object&#39;);
const clone2 = JSON.parse(JSON.stringify(init));
console.timeEnd(&#39;JSON object&#39;);</code></pre>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/3b0d52fe-59d3-4b2e-924a-acf92bf03dfb/image.JPG" alt=""></p>
<p>내가 만든 재귀 함수가 훨씬 더 느리다. 🤣🤣</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Data Table 구현하기]]></title>
            <link>https://velog.io/@seo__namu/React-Data-Table-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/React-Data-Table-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 25 Nov 2022 11:19:39 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@lky5697/react-junior-code-review-and-refactoring">(번역) 데이터 구조를 개선하여 코드 43% 줄이기</a>를 보고, 나도 <code>Set</code> 자료 구조를 사용해서 데이터 테이블을 직접 구현해볼까? 하는 생각이 들어서 후다닥 만들어봤습니다. </p>
<p>React로 데이터 테이블을 구현하는 과정을 기록했으며, 잘못된 정보가 있거나 설명이 부족한 부분이 있다면 댓글로 남겨주시면 감사하겠습니다! 🤗</p>
<p>동작하는 코드는 <a href="https://stackblitz.com/edit/react-cfer5c?file=src%2FApp.js">stackblitz</a>에서 바로 확인해볼 수 있습니다. </p>
<h2 id="컴포넌트-설계">컴포넌트 설계</h2>
<p>가장 먼저 할 일은 컴포넌트를 설계하는 것입니다.</p>
<p>컴포넌트 설계 단계에서는 머리속으로 생각하고 있는 <strong>추상적인 기능</strong>들을 <strong>구체화</strong>합니다. 컴포넌트 설계가 미흡하면 추후 컴포넌트를 <strong>수정</strong>하는 일이 발생하고, <strong>코드가 복잡</strong>해질 가능성이 생기기 때문에 매우 중요한 단계입니다.</p>
<p>저는 컴포넌트를 설계할 때 <code>커스텀 가능성</code>을 중점적으로 고려하는 편이며, 커스텀 가능성에 따라 장단점이 나뉘지기 때문에 컴포넌트의 목적에 따라 설정해야 합니다. </p>
<table>
<thead>
<tr>
<th>커스텀 가능성</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>높다</td>
<td>다양한 옵션으로 사용 가능</td>
<td>러닝커브가 높아짐, 컴포넌트가 무거워질 수 있음</td>
</tr>
<tr>
<td>낮다</td>
<td>러닝커브가 낮음</td>
<td>기능이 적고, 유연하지 못한 컴포넌트가 될 수 있음</td>
</tr>
</tbody></table>
<p><strong>러닝커브</strong>가 높고 낮다는 의미는 내가 만든 컴포넌트를 사용하는 <strong>다른 개발자</strong>의 입장에서 적은 것 입니다. 커스텀 가능성이 높을수록 컴포넌트에 대한 깊은 이해가 있어야 의도에 맞게 컴포넌트를 사용할 수 있기 때문이죠.</p>
<h2 id="기능-목록">기능 목록</h2>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/6828fccf-2a11-4565-949d-a0377960f7ef/image.png" alt=""></p>
<ol>
<li>헤더 순서에 맞게 데이터 객체에서 값 넣어주기</li>
<li>행 선택 기능 사용여부 
행 선택 기능을 사용하면 <code>checkbox</code>가 표시되며, 사용하지 않으면 표시되지 않습니다.</li>
<li>행 비활성화 가능
데이터 객체에 <code>disabled: true</code> 요소를 추가하면 행을 비활성화 시킬 수 있습니다.</li>
</ol>
<p>이해를 돕기 위해 사진을 첨부했습니다!</p>
<h2 id="props">Props</h2>
<table>
<thead>
<tr>
<th>props</th>
<th>데이터 타입</th>
<th>설명</th>
<th>필수여부</th>
</tr>
</thead>
<tbody><tr>
<td>headers</td>
<td>array</td>
<td>테이블 헤더 행을 정의</td>
<td>√</td>
</tr>
<tr>
<td>items</td>
<td>array</td>
<td>테이블 바디에 보여지는 데이터 목록</td>
<td></td>
</tr>
<tr>
<td>itemKey</td>
<td>string</td>
<td>선택 행의 키값</td>
<td></td>
</tr>
<tr>
<td>selectable</td>
<td>boolean</td>
<td>행 선택 기능 사용여부</td>
<td></td>
</tr>
<tr>
<td>updateSelection</td>
<td>function</td>
<td>선택된 행이 변경되면 실행할 함수</td>
<td></td>
</tr>
</tbody></table>
<p>데이터 타입이 배열인 header와 items는 객체를 담는 배열로 아래에 자세한 설명을 작성했습니다.</p>
<h3 id="headers">headers</h3>
<pre><code class="language-js">[
  {
    text: &#39;컬럼명&#39;,
    value: &#39;데이터명&#39;
  }
]</code></pre>
<p>각 컬럼을 객체로 정의하며, 컬럼의 수 만큼 배열에 넣어줘야 합니다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/15e6a2ce-a631-4528-a88e-c14ccd6c11dd/image.JPG" alt=""></p>
<p>위 사진을 보면 테이블 헤더에 컬럼이 3개가 있습니다.</p>
<pre><code class="language-js">const headers = [
  {
    text: &#39;Name&#39;,
    value: &#39;name&#39;
  },
  {
    text: &#39;Version&#39;,
    value: &#39;version&#39;
  },
  {
    text: &#39;Launch Date&#39;,
    value: &#39;launch&#39;
  }
];</code></pre>
<p>테이블 헤더는 이렇게 정의해줄 수 있습니다.</p>
<h3 id="items">items</h3>
<p>headers에서 지정한 컬럼의 수 만큼 데이터를 입력합니다. 객체 하나가 열 하나에 해당합니다.</p>
<pre><code class="language-js">const items = [
  {
    name: &#39;React&#39;,
    version: &#39;18.2.0&#39;,
    launch: &#39;2013-05-29&#39;
  },
  {
    name: &#39;Vue&#39;,
    version: &#39;3.2.45&#39;,
    launch: &#39;2014-02&#39;
  },
  {
    name: &#39;jQuery&#39;,
    version: &#39;3.3&#39;,
    disabled: true,
    launch: &#39;2006-08-26&#39;
  },
  {
    name: &#39;Svelte&#39;,
    version: &#39;3.53.1&#39;,
    launch: &#39;2016-11-26&#39;
  }
 ];</code></pre>
<p><a href="#headers">headers</a>에서 name, version, launch 세 개의 컬럼을 지정했기 때문에, 데이터 객체에서 지정한 데이터명에 데이터를 넣어줍니다.</p>
<h2 id="1-컴포넌트-초기-세팅">1. 컴포넌트 초기 세팅</h2>
<pre><code class="language-jsx">export default function DataTable({}) {
  return (
    &lt;table&gt;
      &lt;thead&gt;
        {/* TODO 테이블 헤드 바인딩 */}
      &lt;/thead&gt;
      &lt;tbody&gt;
        {/* TODO 테이블 데이터 바인딩 */}
      &lt;/tbody&gt;
    &lt;/table&gt;
  )
}</code></pre>
<p>컴포넌트 테이블의 기본 구성 요소들을 배치합니다. </p>
<h2 id="2-headers-바인딩">2. headers 바인딩</h2>
<p><a href="#props">Props</a>를 보면 <code>headers</code>는 필수 props입니다. 따라서 headers가 없다면 에러를 발생시키고, headers가 있을 경우에만 <code>map</code> 함수로 순환하며 테이블에 추가해줍니다.</p>
<pre><code class="language-jsx">export default function DataTable({ headers }) {
  // headers가 있는지 체크하고, 없다면 에러를 던짐
  if (!headers || !headers.length) {
    throw new Error(&#39;&lt;DataTable /&gt; headers is required.&#39;)
  }
  return (
    &lt;table&gt;
      &lt;thead&gt;
        &lt;tr&gt;
          {
            headers.map((header) =&gt; 
              &lt;th key={header.text}&gt;
                {header.text} {/* 컬럼명 바인딩 */}
              &lt;/th&gt; 
            )
          }
        &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
        {/* TODO 테이블 데이터 바인딩 */}
      &lt;/tbody&gt;
    &lt;/table&gt;
  )
}</code></pre>
<p>이제 <code>App.js</code>에서 컴포넌트를 사용해볼까요? props로 넘겨주는 headers 배열은 <a href="#headers">headers</a>에 있습니다.</p>
<pre><code class="language-jsx">import DataTable from &quot;./components/dataTable&quot;;
function App() {
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;DataTable 
        headers={headers} {/* headers props 보내기 */} 
      /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/127c4515-5257-4599-bb20-906b6fadb076/image.JPG" alt=""></p>
<p>테이블의 헤더 부분이 잘 나오네요! 😄</p>
<h2 id="3-items-바인딩">3. items 바인딩</h2>
<p>데이터를 표시할 때 신경써야 하는 부분은 데이터가 헤더에 설정한 순서대로 정의되어서 오지 않을 수 있다는 점입니다.</p>
<p>따라서 데이터 객체에 들어있는 순서에 상관 없이 헤더에 맞는 데이터를 보여줘야 합니다.</p>
<p>저는 header의 value들이 담겨있는 <code>headerKey</code> 배열을 추가로 만들었고, 행을 표시할 때 <code>headerKey</code> 배열을 순회하면서 행의 요소들에 접근해서 데이터를 바인딩해줬습니다.</p>
<pre><code class="language-jsx">export default function DataTable(
  { 
    headers, 
    items = [], // items props 받기, default parameter 빈 배열로 설정
  }) {
  if (!headers || !headers.length) {
    throw new Error(&#39;&lt;DataTable /&gt; headers is required.&#39;)
  }
  // value 순서에 맞게 테이블 데이터를 출력하기 위한 배열
  const headerKey = headers.map((header) =&gt; header.value);
  return (
    &lt;table&gt;
      &lt;thead&gt;
        {/* ... */}
      &lt;/thead&gt;
      &lt;tbody&gt;
        {
          items.map((item, index) =&gt; (
            &lt;tr key={index}&gt;
              {/* headerKey를 순회하면서 key를 가져옴 */}
              { 
                headerKey.map((key) =&gt; 
                  &lt;td key={key + index}&gt;
                    {item[key]} {/* key로 객체의 값을 출력 */}
                  &lt;/td&gt;
                )
              }
            &lt;/tr&gt;
          ))
        }
      &lt;/tbody&gt;
    &lt;/table&gt;
  )
}</code></pre>
<p>데이터가 잘 출력되는지 <code>App.js</code>에서 데이터를 넘겨볼까요? props로 넘겨주는 items 배열은 <a href="#items">items</a>에 있습니다.</p>
<pre><code class="language-jsx">function App() {
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;DataTable 
        headers={headers} 
        items={items} {/* items props 보내기 */} 
      /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/bc872015-af37-4a81-95f8-eb7ac203420a/image.JPG" alt=""></p>
<p>아직 비활성 행에 대한 코드를 작성하지 않아서 모두 활성화된 행으로 보여주고 있지만, 잘 출력되고 있네요! 👍</p>
<h2 id="4-selectable-체크박스-바인딩">4. selectable 체크박스 바인딩</h2>
<p>이제 행 선택 기능 사용여부에 따라서 체크박스를 보여줘야 합니다. </p>
<p><code>selectable</code> props를 받아서 <code>true</code>일 경우에만 체크박스를 보여주면 되겠죠?</p>
<pre><code class="language-jsx">export default function DataTable(
  { 
    headers, 
    items = [],
    selectable = false, // selectable props 받기
  }) {
  if (!headers || !headers.length) {
    throw new Error(&#39;&lt;DataTable /&gt; headers is required.&#39;)
  }
  const headerKey = headers.map((header) =&gt; header.value);

  return (
    &lt;table&gt;
      &lt;thead&gt;
        &lt;tr&gt;
          {/* 선택 기능을 사용할 때만 바인딩 */}
          {
            selectable &amp;&amp; &lt;th&gt;&lt;input type=&quot;checkbox&quot; /&gt;&lt;/th&gt;
          }
          {
            headers.map(
              (header) =&gt; &lt;th key={header.text}&gt;{ header.text }&lt;/th&gt;
            )
          }
        &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
        {
          items.map((item, index) =&gt; (
            &lt;tr key={index}&gt;
              {/* 선택 기능을 사용할 때만 바인딩 */}
              {
                selectable &amp;&amp; &lt;td&gt;&lt;input type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
              }
              {
                headerKey.map((key) =&gt; 
                  &lt;td key={key + index}&gt;{item[key]}&lt;/td&gt;
                )
              }
            &lt;/tr&gt;
          ))
        }
      &lt;/tbody&gt;
    &lt;/table&gt;
  )
}</code></pre>
<p><code>App.js</code>에서 props로 <code>selectable={true}</code>를 보내서 체크박스가 잘 출력되는지 확인해볼까요?</p>
<pre><code class="language-jsx">function App() {
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;DataTable 
        headers={headers} 
        items={items} 
        selectable={true} {/* selectable props 보내기 */} 
      /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/34da9a20-a598-4ce6-997f-8ba0070f78ea/image.JPG" alt=""></p>
<p>체크박스가 잘 보이네요! 이제 데이터 테이블 컴포넌트를 사용하는 개발자가 원하는 경우에 따라 행 선택 여부를 제어할 수 있게 되었습니다.</p>
<h2 id="5-selectable-기능-구현">5. selectable 기능 구현</h2>
<p>드디어 <code>Set</code>을 사용해서 행 선택 기능을 구현할 차례입니다.</p>
<p>코드가 길어지는 것을 방지하기 위해 DataTable의 기존 코드들은 생략했습니다.</p>
<pre><code class="language-jsx">export default function DataTable(
  { 
    headers, 
    items = [], 
    selectable = false,
    itemKey // itemKey props 받기
  }
) {
  // itemKey가 없다면 headers의 첫번째 요소를 선택 
  if (!itemKey) {
    itemKey = headerKey[0];
  }
  // 선택한 row의 itemKey를 담은 배열
  const [selection, setSelection] = useState(new Set());
}</code></pre>
<p>먼저 선택한 행의 데이터를 담을 배열을 선언해줍니다.</p>
<p>행 전체의 데이터를 담지 않고, <code>itemKey</code>에 해당하는 데이터만 배열에 담으려고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/b8fe19fb-db55-4306-b129-dc8751bb1346/image.JPG" alt=""></p>
<p><code>itemKey</code>는 props가 없을 경우 <code>headerKey</code>의 <strong>첫번째 요소</strong>를 선택하는데, 위의 데이터 테이블의 itemKey는 <code>name</code>이므로 행을 선택하면 <code>[&#39;React&#39;, &#39;Vue&#39;, &#39;jQuery&#39;, &#39;Svelte&#39;]</code> 와 같은 데이터가 담기게 됩니다.</p>
<h3 id="단일-행-선택">단일 행 선택</h3>
<p>단일 행을 선택했을 경우 데이터 객체에서 itemKey로 값을 받아와 넣어주게 됩니다.</p>
<pre><code class="language-jsx">export default function DataTable({ /* ... */ }) {
  const [selection, setSelection] = useState(new Set());
  const onChangeSelect = (value) =&gt; {
    // 기존의 selection으로 새로운 Set 생성
    const newSelection = new Set(selection);
    if (newSelection.has(value)) {
      // value가 있으면 삭제 (checked가 false이기 때문)
      newSelection.delete(value);
    } else {
      // value가 없으면 추가 (checked가 true이기 때문)
      newSelection.add(value);
    }
    // 새로운 Set으로 state 변경
    setSelection(newSelection);
  };
}</code></pre>
<p>특정 행 체크박스 클릭시 <code>selection</code>에 <code>value</code>가 없다면 value를 추가해줍니다. 이 상태에서 또 같은 행 체크박스를 클릭하면 <code>selection</code>에 이미 <code>value</code>가 있기 때문에 기존에 있던 value를 제거하게 되는거죠.</p>
<p><strong>HTML 코드</strong></p>
<pre><code class="language-jsx">return (
    &lt;table&gt;
      &lt;thead&gt;{/* ... */}&lt;/thead&gt;
      &lt;tbody&gt;
        {
          items.map((item, index) =&gt; (
            &lt;tr 
              key={index} 
              className={
              `
                ${selection.has(item[itemKey]) ? &#39;select_row&#39;: &#39;&#39;} 
                ${item.disabled ? &#39;disabled_row&#39; : &#39;&#39;}
              `
            }&gt;
              {/* 속성 넣어주기 */}
              {
                selectable &amp;&amp; 
                  &lt;td&gt;
                    &lt;input 
                      type=&quot;checkbox&quot;
                      disabled={item.disabled}
                      checked={selection.has(item[itemKey])}
                      onChange={() =&gt; onChangeSelect(item[itemKey])}   
                    /&gt;
                  &lt;/td&gt;
              }
              { 
                headerKey.map((key) =&gt; 
                  &lt;td key={key + index}&gt;
                    {item[key]} {/* key로 객체의 값을 출력 */}
                  &lt;/td&gt;
                )
              }
            &lt;/tr&gt;
          ))
        }
      &lt;/tbody&gt;
    &lt;/table&gt;
  )</code></pre>
<p><code>onChange</code>이벤트와 <code>checked</code>, <code>disabled</code> 등의 속성 값도 넣어줍니다. 참고로 <code>tr</code>의 <code>className</code>은 css를 위해 클래스를 바인딩하는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/5564ad5c-f312-4e85-9fae-3d12416c320e/image.JPG" alt=""></p>
<p>단일 행 선택 기능이 완성되었습니다!</p>
<h3 id="전체-선택">전체 선택</h3>
<p>전체 선택의 경우 이벤트 객체로 <code>target</code>의 <code>checked</code> 상태로 전체 선택 상태를 파악합니다.</p>
<pre><code class="language-jsx">export default function DataTable({ /* ... */}) {
  // disabled가 true인 item만 반환하는 함수
  const getAbledItems = (items) =&gt; {
    return items.filter(({ disabled }) =&gt; !disabled );
  };
  const onChangeSelectAll = (e) =&gt; {
    if (e.target.checked) {
      // checked가 true인 경우 전체 선택
      const allCheckedSelection = new Set(
        // 활성화된 행의 배열을 순회하며 itemKey로 요소에 접근해 데이터를 저장
        getAbledItems(items).map((item) =&gt; item[itemKey])
      );
      setSelection(allCheckedSelection);
    } else {
      // checked가 false인 경우 전체 선택 해제
      setSelection(new Set());
    }
  };
  // 전체 선택 상태 여부
  const isSelectedAll = () =&gt; {
    return selection.size === getAbledItems(items).length;
  };
}</code></pre>
<p><code>selection</code>의 <code>size</code>와 활성화 행들의 <code>length</code>가 같다면 전체 선택이 되어있다는 것을 알 수 있습니다. 전체 선택 체크박스의 <code>checked</code> 속성을 사용하기 위해 함수로 만들었습니다.</p>
<p><strong>HTML 코드</strong></p>
<pre><code class="language-jsx">  return (
    &lt;table&gt;
      &lt;thead&gt;
        &lt;tr&gt;
          {/* 속성 넣어주기 */}
          {
            selectable &amp;&amp; 
            &lt;th&gt;
              &lt;input 
                type=&quot;checkbox&quot;
                checked={isSelectedAll()}
                onChange={onChangeSelectAll}
              /&gt;
            &lt;/th&gt;
          }
          {
            headers.map((header) =&gt; 
              &lt;th key={header.text}&gt;
                {header.text} {/* 컬럼명 바인딩 */}
              &lt;/th&gt; 
            )
          }
        &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;{/* ... */}&lt;/tbody&gt;
    &lt;/table&gt;
  )</code></pre>
<p><code>onChange</code>이벤트와 <code>checked</code> 등의 속성 값도 넣어줍니다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/9c479fd7-8372-4298-9c5f-32cc40fea862/image.JPG" alt=""></p>
<p>이제 모든 기능을 완성했습니다. 🎉🎉</p>
<h2 id="6-updateselection">6. updateSelection</h2>
<p>하지만 아직까지는 외부에서 선택된 행의 <code>value</code> 값을 담은 <code>selection</code>을 받을 수 없습니다.</p>
<p><code>updateSelection</code> 함수를 props로 받아서 <code>selection</code>을 함수에 인자로 넘겨줘서 외부에서 확인할 수 있도록 해볼까요?</p>
<pre><code class="language-jsx">const onChangeSelect = (value) =&gt; {
  const newSelection = new Set(selection);
  if (newSelection.has(value)) {
    newSelection.delete(value);
  } else {
    newSelection.add(value);
  }
  setSelection(newSelection);
  // updateSelection 함수 호출
  updateSelection([...newSelection]);
};

const onChangeSelectAll = (e) =&gt; {
  if (e.target.checked) {
    const allCheckedSelection = new Set(
      getAbledItems(items).map((item) =&gt; item[itemKey])
    );
    setSelection(allCheckedSelection);
    // updateSelection 함수 호출
    updateSelection([...allCheckedSelection]);
  } else {
    setSelection(new Set());
    // updateSelection 함수 호출
    updateSelection([]);
  }
};</code></pre>
<p><code>onChangeSelect</code>함수와 <code>onChangeSelectAll</code>함수에서 호출해서 상태를 업데이트 해줍니다.</p>
<p>마지막으로 <code>App.js</code>에서 selection을 담을 상태를 선언하고, <code>useEffect</code>로 selection이 변경하는 것을 감지해서 콘솔에 출력하도록 해보겠습니다.</p>
<pre><code class="language-jsx">function App() {
  const [selection, setSelection] = useState([]);
  useEffect(() =&gt; {
    console.log(selection);
  }, [selection]);

  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;DataTable 
        headers={headers} 
        items={items} 
        selectable={true} 
        updateSelection={setSelection}
      /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>단일 행 선택과 전체 선택을 해보면 콘솔에 <code>name</code> 값이 담긴 배열이 출력되는 것을 볼 수 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/b4f0ebf8-84ef-49ed-aa2c-02a64dbcfa8f/image.JPG" alt=""></p>
<p>이제 정말 완성입니다! 👏👏👏</p>
<h2 id="🔗-github-링크">🔗 Github 링크</h2>
<p><a href="https://github.com/ixio0330/react-data-table">React Data Table</a>에 소스코드가 공개되어 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[클래스와 인스턴스]]></title>
            <link>https://velog.io/@seo__namu/%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4</link>
            <guid>https://velog.io/@seo__namu/%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4</guid>
            <pubDate>Thu, 10 Nov 2022 02:21:56 GMT</pubDate>
            <description><![CDATA[<h2 id="🌳-클래스란">🌳 클래스란?</h2>
<blockquote>
<p>클래스는 특정 객체를 생성하기 위해 변수와 메소드를 정의하는 일종의 틀(template)이다. 객체를 정의하기 위한 메소드와 변수로 구성된다.</p>
</blockquote>
<p>클래스를 일종의 <strong>틀</strong>이라고 하는데, 클래스를 어떻게 이해해야할까?</p>
<p>예를 들어, <strong>차</strong>라는 단어를 보면 각자 사람마다 떠오르는 차의 형태가 있을 것이다.</p>
<table>
<thead>
<tr>
<th>사람</th>
<th>차</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>🚗</td>
</tr>
<tr>
<td>B</td>
<td>🚙</td>
</tr>
<tr>
<td>C</td>
<td>🚚</td>
</tr>
<tr>
<td>D</td>
<td>🚌</td>
</tr>
</tbody></table>
<p>사람들이 떠올린 네 가지의 차는 다 다른 차이다.</p>
<p>하지만 네 종류의 차는 모두 <strong>공통점</strong>을 가지고 있다.</p>
<ul>
<li>바퀴가 있고 네 개다.</li>
<li>사람이 탈 수 있다.</li>
<li>이동 수단이다.</li>
<li>기름을 넣어야 움직인다.</li>
</ul>
<p>이 외에도 정말 많은 공통점이 있는데, 이러한 공통점을 가진 대상이 <strong>차</strong>이다.</p>
<p>따라서 차라는 <strong>추상적인 개념</strong>은 다양한 종류의 차의 근본이 되는 틀이라고 할 수 있으며, 이렇게 틀이 되는 것을 클래스라고 한다.</p>
<h2 id="🌱-인스턴스란">🌱 인스턴스란?</h2>
<blockquote>
<p>인스턴스란 클래스를 통해 생성된 객체를 의미한다.</p>
</blockquote>
<p>쉽게 말하면 <strong>차</strong>라는 <strong>틀</strong>을 기반으로 만들어진 🚗 🚙 🚚 🚌 이 차들을 인스턴스라고 할 수 있다.</p>
<p>인스턴스는 독립적인 존재이며, 클래스에 있는 기능들을 사용할 수 있다.</p>
<p>예를 들어, 차라는 클래스가 있다고 가정해보자.</p>
<pre><code class="language-kotlin">차 클래스 // 차가 공통적으로 가지는 구성요소와 기능을 정의한다.
{ 
  // 차 구성요소 = 속성
  // 변경 가능함
  기종 = 미정
  색상 = 미정
  // 변경 불가능함
  바퀴 = [앞 오른쪽, 앞 왼쪽, 뒤 오른쪽, 뒤 왼쪽]
  사이드미러 = [오른쪽, 왼쪽]

  // 차 기능 = 메소드
  시동키기() {}
  전진() {}
  후진() {}
  주차() {}
  오른쪽깜빡이키기() {}
  왼쪽깜빡이키기() {}

  // 차를 인스턴스로 생성할 때 실행되는 특수한 함수
  차_출고시_선택사항(기종, 색상) {
    기종 = 기종
    색상 = 색상
  }
}</code></pre>
<p>클래스 <strong>내부</strong>에 선언된 <strong>변수</strong>를 <strong>프로퍼티</strong>, <strong>함수</strong>는 <strong>메소드</strong>라고 한다.</p>
<p>차 클래스로 여러 종류의 차들을 만들어보자.</p>
<pre><code class="language-kotlin">// 아래 차들은 차 클래스의 인스턴스들이다. 
모닝 = 차(모닝, 노란색)
아반떼 = 차(아반떼, 파란색)
쏘렌토 = 차(쏘렌토, 흰색)</code></pre>
<p>모닝, 아반떼, 쏘렌토 모두 차의 기능들을 사용할 수 있다.</p>
<pre><code class="language-kotlin">모닝.시동키기()
아반떼.시동키기()
아반떼.전진()
아반떼.주차()
쏘렌토.시동키기()
쏘렌토.오른쪽깜빡이키기()</code></pre>
<p>클래스의 장점은 하나의 클래스에서 프로퍼티, 메소드를 선언하면 해당 클래스의 인스턴스에서 해당 프로퍼티, 메소드를 사용할 수 있다는 점이다.</p>
<p>다시보면 차 클래스에 시동을 끄는 기능이 없다. </p>
<pre><code class="language-kotlin">차 클래스 
{
  // ...생략
  시동끄기() {}
}

// ...생략
모닝.시동끄기()
아반떼.시동끄기()
쏘렌토.시동끄기()</code></pre>
<p>클래스에서 시동끄기 메소드를 선언하면 모닝, 아반떼, 쏘렌토에서 모두 다 시동을 끌 수 있게 된다.</p>
<p>클래스와 인스턴스는 직접 생각해볼 수 있는 것들을 떠올리면서 이해해나가면 된다.</p>
<p>공 클래스에서 ⚽️ 🏀 🥎 🏐 🎱 여러 종류의 공 인스턴스를 생성할 수 있다.</p>
<p>음식 클래스에서 🍛 🍣 🍔 🍳 🍝 여러 종류의 음식 인스턴스를 생성할 수 있다.</p>
<p>좋은 예시가 있다면 댓글로 달아주세요! 🤗</p>
<h4 id="참고자료">참고자료</h4>
<p><a href="https://ko.wikipedia.org/wiki/%ED%81%B4%EB%9E%98%EC%8A%A4_(%EC%BB%B4%ED%93%A8%ED%84%B0_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D)">[위키피디아] 클래스 (컴퓨터 프로그래밍)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express vscode에서 디버깅하기]]></title>
            <link>https://velog.io/@seo__namu/Express-vscode%EC%97%90%EC%84%9C-%EB%94%94%EB%B2%84%EA%B9%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/Express-vscode%EC%97%90%EC%84%9C-%EB%94%94%EB%B2%84%EA%B9%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 11 Oct 2022 13:30:05 GMT</pubDate>
            <description><![CDATA[<p>개발을 하다보면 파라미터로 어떤 데이터가 들어왔는지 확인해야하는 경우가 많다.</p>
<p>그런데 매번 console.log 함수로 로그를 출력하는건 꽤 번거롭다.</p>
<p>개발을 할때는 vscode에서 제공하는 디버깅 기능을 사용하면 간편하게 내가 원하는 부분의 데이터를 확인할 수 있다.</p>
<p>왼쪽에 4번째 디버깅 아이콘을 눌러보면 디버깅 관련 설정을 안한 경우 아래와 같이 나온다. </p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/55cf203e-feca-4908-935a-70986e7d5fe7/image.JPG" alt=""></p>
<p>create a launch.json file을 눌러서 디버깅 관련 설정을 위한 launch.json 파일을 생성했다.</p>
<p><strong>.vscode/launch.json</strong></p>
<pre><code class="language-json">{
  &quot;version&quot;: &quot;0.2.0&quot;,
  &quot;configurations&quot;: [
    {
      &quot;type&quot;: &quot;node&quot;,
      &quot;request&quot;: &quot;launch&quot;,
      &quot;name&quot;: &quot;Launch Program&quot;,
      &quot;skipFiles&quot;: [
        &quot;&lt;node_internals&gt;/**&quot;
      ],
      &quot;program&quot;: &quot;${file}&quot;
    }
  ]
}</code></pre>
<p>type은 디버깅을 실행시킬 환경을 의미한다. 나는 express로 개발한 서버를 디버깅 할거니까 node로 선택했다.</p>
<p>name은 디버깅의 이름인데, 디버깅 모드에 진입하면 내가 설정한 이름이 뜬다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/f44fe77e-6cd8-45cd-8498-cca8610abd21/image.png" alt=""></p>
<p>program에는 디버깅을 실행시킬 파일의 경로를 설정해주면 된다.</p>
<p>내가 만든 서버의 진입점은 server 폴더안에 src 폴더의 app.js이기 때문에 아래와 같이 설정했다.</p>
<pre><code class="language-json">&quot;program&quot;: &quot;${workspaceFolder}\\server\\src\\app.js&quot;</code></pre>
<p>디버깅 모드는 서버를 끄고 &#39;F5&#39; 혹은 RUN AND DEBUG 옆에 초록색 화살표를 누르면 실행된다.</p>
<p>작성한 코드가 몇 번째 줄인지 알려주는 숫자 옆에 마우스를 올려보면 빨간색 점이 보인다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/4496e367-bc38-4060-b686-48bff155aa48/image.png" alt=""></p>
<p>그 부분을 클릭하면 해당 코드가 실행될 때, 어떤 데이터가 있는지 확인해볼 수 있도록 중단점이 찍힌다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/ba1824d3-ea81-4a67-9959-0b4b46b5c9d7/image.png" alt=""></p>
<p>나는 / 경로로 들어오는 부분에 중단점을 찍었고, <a href="http://localhost:3001">http://localhost:3001</a>로 접속해보면 중단점을 찍은 부분이 노란색으로 표시되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/7ab53b3b-2b86-470b-be9f-9cff2de1f300/image.png" alt=""></p>
<p>위에 화살표 버튼으로 코드를 실행시키기 전까지는, &#39;Hello Express!&#39;가 출력되지 않는다. 그리고 옆에 디버깅 창에서 request 객체에 어떤 값들이 있는지 확인해볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express 오류 처리 미들웨어 활용하기]]></title>
            <link>https://velog.io/@seo__namu/Express-%EC%98%A4%EB%A5%98-%EC%B2%98%EB%A6%AC-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/Express-%EC%98%A4%EB%A5%98-%EC%B2%98%EB%A6%AC-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 11 Oct 2022 13:06:01 GMT</pubDate>
            <description><![CDATA[<p>이전 포스팅에서 데이터베이스에 연결해서 로그인, 회원가입, 글 CRUD 기능 개발을 마쳤다.</p>
<p>그런데 나는 쿼리 결과 값을 true인지 null인지 판단해서 사용자에게 응답을 해주는 것이 조금은 비효율적이라고 생각이 들었다.</p>
<p>따라서 서버가 정상적인 처리를 하지 못했을 경우에는 오류를 던지고, 오류 처리 미들웨어가 그 오류를 받아서 사용자에게 응답해주도록 코드를 수정해보려고 한다.</p>
<h2 id="🚨-class-myerror-extends-error">🚨 Class MyError Extends Error</h2>
<p>나는 오류를 처리하기 위해 세 개의 오류 클래스를 만들었다.</p>
<ol>
<li><code>BadRequest</code> : 사용자가 <strong>잘못된 데이터</strong>를 보냈을 경우 </li>
<li><code>ExpiredToken</code> : <strong>토큰</strong>이 유효하지 않을 경우</li>
<li><code>MethodNotAllowed</code> : 사용할 수 없는 <strong>메소드</strong>에 접근했을 경우</li>
</ol>
<p>오류 클래스는 Error 클래스를 상속받아서 만들었다.</p>
<p>error 폴더에 각자 파일을 생성했다.</p>
<p><strong>error/badRequest.js</strong></p>
<pre><code class="language-javascript">class BadRequest extends Error {
  status = 400;
  constructor(message = &#39;잘못된 요청입니다.&#39;) {
    super(message);
    this.name = &#39;Bad Request&#39;;
  }
}
module.exports = BadRequest;</code></pre>
<p><strong>error/expiredToken.js</strong></p>
<pre><code class="language-javascript">class ExpiredToken extends Error {
  status = 419;
  constructor(message = &#39;유효하지 않은 토큰입니다.&#39;) {
    super(message);
    this.name = &#39;Expired Token&#39;;
  }
}
module.exports = ExpiredToken;</code></pre>
<p><strong>error/methodNotAllowed.js</strong></p>
<pre><code class="language-javascript">class MethodNotAllowed extends Error {
  status = 405;
  constructor(message = &#39;사용할 수 없는 메소드입니다.&#39;) {
    super(message);
    this.name = &#39;Method Not Allowed&#39;;
  }
}
module.exports = MethodNotAllowed;</code></pre>
<p>먼저, 존재하지 않는 경로로 요청이 들어왔을 때 사용할 MethodNotAllowed 부터 적용했다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">const MethodNotAllowed = require(&#39;./error/methodNotAllowed&#39;);
app.all(&#39;*&#39;, (res, req) =&gt; {
  throw new MethodNotAllowed();
});</code></pre>
<p>토큰 유효성을 체크하는 미들웨어에도 적용했다.</p>
<p><strong>middleware/tokenVerify.js</strong></p>
<pre><code class="language-javascript">const tokenService = require(&#39;../jwt&#39;);
const BadRequest = require(&#39;../error/badrequest&#39;);
const ExpiredToken = require(&#39;../error/expiredToken&#39;);

const tokenVerify = (req, res, next) =&gt; {
  // 토큰이 없을 때
  if (!req.headers.authorization) {
    throw new BadRequest(&#39;토큰이 존재하지 않습니다.&#39;);
  }

  try {
    const payload = tokenService.getPayload(req.headers.authorization);
    req.user_id = payload.user_id;
  } catch (error) {
    // 토큰 만료됐을 때
    throw new ExpiredToken();
  }

  next();
};</code></pre>
<p>그리고 로그인, 회원가입 기능에서도 id가 없을 경우마다 체크해주지 않고 오류를 발생시키도록 했다.</p>
<p><strong>user/index.js</strong></p>
<pre><code class="language-javascript">const BadRequest = require(&#39;../error/badrequest&#39;);

// 사용자 등록
async function create({ id, password, name}) {
  try {
    await database.query(`
        insert into users (id, password, name) 
        values (&#39;${id}&#39;, &#39;${password}&#39;, &#39;${name}&#39;)
    `);
  } catch (error) {
    throw new Error(&#39;사용자 등록중 오류가 발생했습니다.&#39;);
  }
}

// 회원가입
app.post(&#39;/user/register&#39;, async (req, res) =&gt; {
  const { id, password, name } = req.body;
  // id, password, name이 있는지 체크한다.
  if (!id || !password || !name) {
    throw new BadRequest(&#39;id, password, name은 필수입력 사항입니다.&#39;);       
  }

  // id는 중복되지 않도록한다.
  const user = await getById(id);
  if (user) {
    throw new BadRequest(&#39;이미 존재하는 아이디입니다.&#39;);
  }

  // 사용자를 추가한다.
  const result = await create(req.body);
  res.send({ message: &#39;사용자를 등록했습니다.&#39; });
});


// 로그인
app.post(&#39;/user/login&#39;, (req, res) =&gt; {
  const { id, password } = req.body;
  // id, password가 있는지 체크한다.
  if (!id || !password) {
    throw new BadRequest(&#39;id, password는 필수입력 사항입니다.&#39;); 
  }

  // 입력받은 id의 사용자를 찾는다.
  const user = await getById(id);
  if (!user) {
    throw new BadRequest(&#39;존재하지 않는 사용자입니다.&#39;); 
  }

  // 입력받은 password와 찾은 사용자의 password가 일치하는지 체크한다.
  if (user.password !== password) {
    throw new BadRequest(&#39;비밀번호가 일치하지 않습니다.&#39; );
  }

  // 토큰을 발급한다.
  res.status(200).send({ token: &#39;token&#39; });
});</code></pre>
<p>post 라우터에도 적용해줬다.</p>
<p><strong>post/index.js</strong></p>
<pre><code class="language-javascript">const BadRequest = require(&#39;../error/badrequest&#39;);

// id로 글 조회 함수
async function getById(_id) {
  try {
    const { rows } = await database.query(`
        select _posts.id, name, title, content, created_on 
        from _posts
          inner join _users 
          on _posts.user_id = _users.id
        where _posts.id = &#39;${_id}&#39;
    `);
    if (!rows[0]) {
      throw new BadRequest(&#39;존재하지 않는 글입니다.&#39;);
    }
    return rows[0];
  } catch (error) {
    throw new Error(&#39;글 조회중 오류가 발생했습니다.&#39;);
  }
}

// 글 생성 함수
async function create({ user_id, title, content }) {
  try {
    await database.query(`
        insert into _posts (id, user_id, title, content, created_on) 
        values (&#39;${uuid.v1()}&#39;, &#39;${user_id}&#39;, &#39;${title}&#39;, &#39;${content}&#39;, 
                &#39;${new Date().toISOString()}&#39;)`);
  } catch (error) {
    throw new Error(&#39;글 생성 중 오류가 발생했습니다.&#39;);
  }
}

// 글 수정 함수
async function update({ user_id, id, title, content }) {
  const post = await getById(id);
  try {
    await database.query(`
        update _posts 
        set title=&#39;${title}&#39;, content=&#39;${content}&#39; 
        where id=&#39;${id}&#39;`);
  } catch (error) {
    throw new Error(&#39;글 수정 중 오류가 발생했습니다.&#39;);
  }
}

// 글 삭제 함수
async function remove({ user_id, _id }) {
  const post = await getById(_id);
  try {
    await database.query(`delete from _posts where id=&#39;${_id}&#39;`);
  } catch (error) {
    throw new Error(&#39;글 삭제 중 오류가 발생했습니다.&#39;);
  }
}

// 글 전체 조회
router.get(&#39;/&#39;, async (req, res) =&gt; {
  try { 
    const { rows } = await database.query(`
      select _posts.id, name, title, created_on 
      from _posts inner join _users on _posts.user_id = _users.id
    `); 
    res.send(rows);
  } catch (error) {
    res.status(400).send({ message: &#39;글 조회중 오류가 발생했습니다.&#39; });
  }
});

// 글 단일 조회
router.get(&#39;/:id&#39;, async (req, res) =&gt; {
  const post = await getById(req.params.id);
  res.send(post);
});

// 글 등록
router.post(&#39;/&#39;, async (req, res) =&gt; {
  const { title, content } = req.body;

  if (!title || !content) {
    throw new BadRequest(&#39;title, content는 필수 입력 사항입니다.&#39;);
  }

  const result = await create({ title, content });
  res.send({ message: &#39;글을 등록했습니다.&#39; });
});

// 글 수정
router.put(&#39;/:id&#39;, async (req, res) =&gt; {
  const { title, content } = req.body;

  if (!req.params.id || !title || !content) {
    throw new BadRequest(&#39;id, title, content는 필수 입력 사항입니다.&#39;);
  }
  const result = await update({ id: req.params.id, user_id: req.user_id, title, content });
  res.send({ message: &#39;글을 수정했습니다.&#39; });
});

// 글 삭제
router.delete(&#39;/:id&#39;, async (req, res) =&gt; {

  if (!req.params.id) {
    throw new BadRequest(&#39;id는 필수 입력 사항입니다.&#39;);
  }
  const result = await remove({ id: req.params.id, user_id, req.user_id });
  res.send({ message: &#39;글을 삭제했습니다.&#39; });
});</code></pre>
<p>if 조건문으로 판단하는 로직이 사라져서 훨씬 깔끔해졌다.</p>
<p>마지막으로 오류 처리 미들웨어에서 오류를 받아서 사용자에게 상태 코드와 메시지를 반환해주도록 수정했다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">app.use((error, req, res, next) =&gt; {
  res
    .status(error.status || 500)
    .send({ 
      name: error.name || &#39;Internal Server Error&#39;,
      message: error.message || &#39;서버 내부에서 오류가 발생했습니다.&#39;
    });
});</code></pre>
<p>만약 error의 status, 메시지 등이 없다면 서버 내부의 오류로 생각하고 대응하도록 했다.</p>
<p>API 호출을 하는 테스트를 했는데, 예상치 못한 오류가 발생했다. 검색을 해보니, express에서 비동기 오류를 지원하지 않아서 발생하는 오류였고 express-async-errors 라이브러리를 사용해서 해결했다.</p>
<pre><code>$ npm i express-async-errors</code></pre><p>서버가 실행되는 app.js 상단에서 라이브러리를 불러왔다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">require(&#39;express-async-errors&#39;);</code></pre>
<p>이제는 비동기 에러 처리가 잘 된다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express 데이터베이스 연결하기]]></title>
            <link>https://velog.io/@seo__namu/Express-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/Express-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 11 Oct 2022 12:34:46 GMT</pubDate>
            <description><![CDATA[<h2 id="🐘-postgresql-연결하기">🐘 Postgresql 연결하기</h2>
<p>Express에서 postgresql 데이터베이스와 연결하기 위해서는 pg 라이브러리를 사용해야 한다.</p>
<pre><code>$ npm i pg</code></pre><p>db 폴더에 index.js 파일을 생성해서 Database class를 선언하고 Database 인스턴스를 생성해서 외부에서는 생성된 인스턴스를 사용했다.</p>
<p>new 키워드로 인스턴스가 생성되면, 생성자에서 connect 함수를 호출해서 데이터베이스와 연결하도록 했다.</p>
<p><strong>db/index.js</strong></p>
<pre><code class="language-javascript">const { Pool } = require(&#39;pg&#39;);

class Database {
  #database;

  constructor() {
    this.#connect();
  }

  async #connect() {
    this.#database = new Pool({
      user: &#39;tester&#39;, // Database 소유자
      host: &#39;localhost&#39;, // Host
      database: &#39;test_db&#39;, // Database 이름
      password: &#39;qwer&#39;, // Database 비밀번호
      port: 5432, // Database 포트
    });
    try {
      await this.#database.connect();
    } catch (error) {
      console.log(&#39;Database connect failed&#39;);
    }
  }

  async query(_query) {
    return await this.#database.query(_query);
  }
}

const database = new Database();
module.exports = database;</code></pre>
<p>database 객체를 private으로 선언했기 때문에, query 함수를 선언해서 사용했다.</p>
<h2 id="💾-database에-데이터-저장하기">💾 Database에 데이터 저장하기</h2>
<p>이제 로컬로 개발되어 있던 부분을 database에 저장하도록 수정해야 한다.</p>
<h3 id="📌-로그인-회원가입">📌 로그인, 회원가입</h3>
<p>로그인, 회원가입을 할 때 id로 사용자를 찾는 로직이 반복되어서 하나의 함수로 만들어서 가독성을 높였다.</p>
<p><strong>user/index.js</strong></p>
<pre><code class="language-javascript">const databaes = require(&#39;../db&#39;);
async function getById(_id) {
  try {
    const { rows } = await database.query(`select * from users where id=&#39;${_id}&#39;`);
    return rows[0];
  } catch (error) {
    return null;
  }
}</code></pre>
<p>query 함수는 rows에 쿼리 결과를 담아서 준다. query 함수의 반환 값이 객체이기 때문에, 구조분해할당으로 rows를 받아와서 0번째 요소를 반환해주도록 코드를 작성했다.</p>
<p>그리고 사용자를 생성하는 함수도 따로 선언했다.</p>
<pre><code class="language-javascript">async function create({ id, password, name}) {
  try {
    await database.query(`
        insert into users (id, password, name) 
        values (&#39;${id}&#39;, &#39;${password}&#39;, &#39;${name}&#39;)
    `);
    return true;
  } catch (error) {
    return null;
  }
}</code></pre>
<p>이제 getById, create 함수를 사용해서 회원가입, 로그인 기능을 수행하는 코드를 수정해보자.</p>
<pre><code class="language-javascript">// 회원가입
app.post(&#39;/user/register&#39;, async (req, res) =&gt; {
  const { id, password, name } = req.body;
  // id, password, name이 있는지 체크한다.
  if (!id || !password || !name) {
    res.status(400).send({ message: &#39;id, password, name은 필수입력 사항입니다.&#39; });
    return;
  }

  // id는 중복되지 않도록한다.
  const user = await getById(id);
  if (user) {
    res.status(400).send({ message: &#39;이미 존재하는 아이디입니다.&#39; });
    return;
  }

  // 사용자를 추가한다.
  const result = await create(req.body);
  if (result) {
    res.send({ message: &#39;사용자를 등록했습니다.&#39; });
  } else {
    res.send({ message: &#39;사용자 등록에 실패했습니다. &#39;});
  }
});


// 로그인
app.post(&#39;/user/login&#39;, (req, res) =&gt; {
  const { id, password } = req.body;
  // id, password가 있는지 체크한다.
  if (!id || !password) {
    res.status(400).send({ message: &#39;id, password는 필수입력 사항입니다.&#39; });
    return;
  }

  // 입력받은 id의 사용자를 찾는다.
  const user = await getById(id);
  if (!user) {
    res.status(400).send({ message: &#39;존재하지 않는 사용자입니다.&#39; });
    return;
  }

  // 입력받은 password와 찾은 사용자의 password가 일치하는지 체크한다.
  if (user.password !== password) {
    res.status(400).send({ message: &#39;비밀번호가 일치하지 않습니다.&#39; });
    return;
  }

  // 토큰을 발급한다.
  res.status(200).send({ token: &#39;token&#39; });
});</code></pre>
<h3 id="📌-글-crud">📌 글 CRUD</h3>
<p>먼저 글 id로 조회하기, 생성, 수정, 삭제 기능을 수행하는 함수를 만들었다.</p>
<p>회원가입 기능과 마찬가지로, 쿼리를 잘 수행했으면 true를 반환하고 오류가 발생하면 null을 반환하도록 했다.</p>
<p>그리고 글 마다 고유한 id를 부여하기 위해 uuid 라이브러리를 사용했다.</p>
<pre><code>$ npm i uuid</code></pre><p><strong>post/index.js</strong></p>
<pre><code class="language-javascript">// id로 글 조회
async function getById(_id) {
  try {
    const { rows } = await database.query(`
        select _posts.id, name, title, content, created_on 
        from _posts
          inner join _users 
          on _posts.user_id = _users.id
        where _posts.id = &#39;${_id}&#39;
    `);
    return rows[0];
  } catch (error) {
    return null;
  }
}

// 글 생성
async function create({ user_id, title, content }) {
  try {
    await database.query(`
        insert into _posts (id, user_id, title, content, created_on) 
        values (&#39;${uuid.v1()}&#39;, &#39;${user_id}&#39;, &#39;${title}&#39;, &#39;${content}&#39;, 
                &#39;${new Date().toISOString()}&#39;)`);
    return true;
  } catch (error) {
    return null;
  }
}

// 글 수정
async function update({ user_id, id, title, content }) {
  const post = await getById(id);
  if (!post) {
    return null;
  }
  try {
    await database.query(`
        update _posts 
        set title=&#39;${title}&#39;, content=&#39;${content}&#39; 
        where id=&#39;${id}&#39;`);
    return true;
  } catch (error) {
    return null;
  }
}

// 글 삭제
async function remove({ user_id, _id }) {
  const post = await getById(_id);
  if (!post) {
    return null;
  }
  try {
    await database.query(`delete from _posts where id=&#39;${_id}&#39;`);
    return true;
  } catch (error) {
    return null;
  }
}</code></pre>
<p>쿼리를 작성하면서 콤마나 따옴표 등 오타를 많이 내서 많은 오류가 났었다..</p>
<p>글 생성, 수정, 삭제 기능의 경우 user_id가 필요하다. 그래서 하지만 매번 token이 유효한지 검사하는건 비효율적이기 때문에, 미들웨어를 사용해서 user_id를 request 객체에 저장하려고  한다.</p>
<p><strong>middleware/tokenVerify.js</strong></p>
<pre><code class="language-javascript">const tokenService = require(&#39;../jwt&#39;);

const tokenVerify = (req, res, next) =&gt; {
  // Request headers에 토큰이 없으면 오류를 반환한다. 
  if (!req.headers.authorization) {
    res.status(400).send({ message: &#39;토큰이 존재하지 않습니다.&#39; });
    return;
  }

  try {
    const payload = tokenService.getPayload(req.headers.authorization);
    // request 객체에 사용자 아이디를 저장한다.
    req.user_id = payload.user_id;
  } catch (error) {
    // 토큰 payload를 얻는데 실패하면 유효한 토큰이므로 client에 알려준다.
    res.status(400).send({ message: &#39;유효하지 않은 토큰입니다.&#39; });
    return;
  }

  next();
};

module.exports = tokenVerify;</code></pre>
<p>미들웨어는 라우터에서 미들웨어를 실행하고 싶은 위치에 넣어주면 된다.</p>
<p>나는 글 생성, 수정, 삭제 기능을 수행하기 전에 토큰이 유효한지 체크하는 미들웨어를 실행시키도록 했다.</p>
<p><strong>post/index.js</strong></p>
<pre><code>router.post(&#39;/&#39;, tokenVerify, async (req, res) =&gt; { //... }
router.put(&#39;/&#39;, tokenVerify, async (req, res) =&gt; { //... }
router.delete(&#39;/&#39;, tokenVerify, async (req, res) =&gt; { //... }</code></pre><p>이제 글 CRUD 기능을 마무리 해보자.</p>
<pre><code class="language-javascript">// 글 전체 조회
router.get(&#39;/&#39;, async (req, res) =&gt; {
  try { 
    const { rows } = await database.query(`
      select _posts.id, name, title, created_on 
      from _posts inner join _users on _posts.user_id = _users.id
    `); 
    res.send(rows);
  } catch (error) {
    res.status(400).send({ message: &#39;글 조회중 오류가 발생했습니다.&#39; });
  }
});

// 글 단일 조회
router.get(&#39;/:id&#39;, async (req, res) =&gt; {
  const post = await getById(req.params.id);
  // 글이 존재하는지 체크
  if (!post) {
    res.status(400).send({ message: &#39;존재하지 않는 글입니다.&#39; });
    return;
  }
  res.send(post);
});

// 글 등록
router.post(&#39;/&#39;, async (req, res) =&gt; {
  const { title, content } = req.body;
  // title, content가 있는지 체크
  if (!title || !content) {
    res.status(400).send({ message: &#39;Title, content는 필수 입력 사항입니다.&#39; });
    return;
  }
  // 글 추가
  const result = await create({ title, content });
  if (!result) {
    res.status(400).send({ message: &#39;글 등록에 실패했습니다.&#39; });
    return;
  }

  res.send({ message: &#39;글을 등록했습니다.&#39; });
});

// 글 수정
router.put(&#39;/:id&#39;, async (req, res) =&gt; {
  const { title, content } = req.body;
  // id, title, content가 있는지 체크
  if (!req.params.id || !title || !content) {
    res.status(400).send({ message: &#39;id, title, content는 필수 입력 사항입니다.&#39; });
    return;
  }
  // 글 수정
  const result = await update({ id: req.params.id, user_id: req.user_id, title, content });
  if (!result) {
    res.status(400).send({ message: &#39;글 수정에 실패했습니다.&#39; });
    return;
  }
  res.send({ message: &#39;글을 수정했습니다.&#39; });
});

// 글 삭제
router.delete(&#39;/:id&#39;, async (req, res) =&gt; {
  // id 있는지 체크
  if (!req.params.id) {
    res.status(400).send({ message: &#39;id는 필수 입력 사항입니다.&#39; });
    return;
  }
  // 글 삭제
  const result = await remove({ id: req.params.id, user_id, req.user_id });
  if (!result) {
    res.status(400).send({ message: &#39;글 삭제에 실패했습니다.&#39; });
    return;
  }

  res.send({ message: &#39;글을 삭제했습니다.&#39; });
});</code></pre>
<p>이제 기능 개발은 끝이다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express 미들웨어 사용하기]]></title>
            <link>https://velog.io/@seo__namu/Express-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/Express-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 10 Oct 2022 13:33:27 GMT</pubDate>
            <description><![CDATA[<h2 id="🔍-미들웨어middleware란">🔍 미들웨어(Middleware)란?</h2>
<p>나는 미들웨어를 내가 원하는 기능을 수행하기 전에, 서버에서 필요로 하는 정보가 클라이언트에서 잘 넘어왔는지 확인하는 함수라고 이해했다.</p>
<p>예를 들어, 로그인을 하고 글을 등록, 수정, 삭제 할 때에 사용자에게 발급한 토큰으로 사용자의 접근 권한이 유효한지 체크해야한다. 그런데 토큰이 유효한지 체크하는 기능을 모든 라우터의 함수에서 작성하는 것은 매우 비효율적이다.</p>
<p>만약 토큰이 유효한지 체크한 후, 어떤 작업을 추가로 진행해야 된다면 현재는 등록, 수정, 삭제 총 세 군데 이지만 나중에는 수백여곳의 코드를 찾아서 고쳐야할 수도 있다.</p>
<p>따라서 토큰이 유효한지 체크하는 기능을 가진 미들웨어 함수를 생성하고, 토큰 유효성 검사가 필요한 라우터에 적용해주는 것이다.</p>
<h3 id="📌-express의-미들웨어-종류">📌 Express의 미들웨어 종류</h3>
<ul>
<li>애플리케이션 레벨 미들웨어</li>
<li>라우터 레벨 미들웨어</li>
<li>오류 처리 미들웨어</li>
<li>기본 제공 미들웨어</li>
<li>써드파티 미들웨어</li>
</ul>
<h3 id="📌-express의-미들웨어가-수행하는-기능">📌 Express의 미들웨어가 수행하는 기능</h3>
<ul>
<li>모든 코드를 실행</li>
<li>요청 및 응답 개체를 변경</li>
<li>요청, 응답 주기를 종료</li>
<li>스택에서 다음 미들웨어 함수를 호출</li>
</ul>
<p>자세한 내용은 <a href="https://expressjs.com/en/guide/using-middleware.html">Express 미들웨어 사용</a>에서 확인해볼 수 있다.</p>
<h2 id="🚧-미들웨어-사용하기">🚧 미들웨어 사용하기</h2>
<p>미들웨어는 requset와 response 객체 그리고 next 함수를 인자로 받는다. </p>
<p>next 함수는 다음 함수를 실행시키는 함수인데, 만약 next를 호출해주지 않으면 내가 라우터마다 작성한 코드들이 실행되지 않는다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">// 라우터 요청을 처리하기 전에 실행시킬 미들웨어
app.use((req, res, next) =&gt; {
  console.log(`[${req.method}] ${req.path} | ${new Date().toLocaleString()}`);
  next();
});

// ...

// 오류 처리 미들웨어
app.use((error, req, res, next) =&gt; {
  console.log(error.message);
  res.status(500).send({ message: &#39;서버 내부에서 오류가 발생했습니다.&#39; });
});</code></pre>
<p>간단하게 라우터 요청을 처리하기 전에 요청 내용 로그를 찍는 미들웨어와 오류를 처리해주는 미들웨어를 작성했다.</p>
<p>오류 처리 미들웨어는 라우터 중 맨 아래에 작성해야되며, 오류 처리 미들웨어보다 위에 있는 라우터는 오류 처리 미들웨어가 실행되지 않는다.</p>
<p>서버에서 로직을 실행하다가 throw로 오류가 발생되면 모두 오류 처리 미들웨어로 가게 된다.</p>
<p>나중에는 라우터에서 매번 res.send()로 각 상황에 따라서 결과값을 반환해주지 않고, 오류를 던져서 예외 처리 미들웨어에서 오류 결과를 반환하도록 하려고 한다.</p>
<h2 id="🧺-사용하지-않는-라우터-처리">🧺 사용하지 않는 라우터 처리</h2>
<p>API 테스트를 하다보면 잘못된 주소로 요청을 보내는 경우가 있다. </p>
<p>게시판 프로젝트를 진행하면서 여러번 잘못된 주소를 보냈었는데, 사용하지 않는 라우터라는 것을 명시해주기 위해서 코드를 추가했다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">app.all(&#39;*&#39;, (res, req) =&gt; {
  req.status(405).send({ message: &#39;사용할 수 없는 라우터입니다.&#39; });
});</code></pre>
<p>위에 있는 모든 라우터에 해당하지 않을 경우 마지막으로 이 라우터를 통과하게 되기 때문에, 존재하지 않는 경로로 http 요청을 보냈을 경우 응답을 보내줄 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express CRUD 기능 구현]]></title>
            <link>https://velog.io/@seo__namu/Express-CRUD-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@seo__namu/Express-CRUD-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 10 Oct 2022 12:52:19 GMT</pubDate>
            <description><![CDATA[<h2 id="⌨️-crud-기능-구현하기">⌨️ CRUD 기능 구현하기</h2>
<p>이제 게시글 등록, 조회, 수정, 삭제 기능을 구현할 차례다.</p>
<p>로그인, 회원가입과 마찬가지로 먼저 로컬로 개발한 후 나중에 데이터베이스 연동을 할 예정이다. 데이터 베이스를 연동하고 </p>
<p>로그인, 회원가입 기능은 app.js에서 작성한 후 따로 파일을 분리했지만, 글 CRUD 기능은 처음부터 파일을 분리해서 개발했다.</p>
<p><strong>post/index.js</strong></p>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const router = express.Router();

// 글 정보를 담을 배열과 id
const posts = {
  data: [],
  id: 1,
}

// 글 전체 조회
router.get(&#39;/&#39;, (req, res) =&gt; {
  // TODO 글 전체 반환해주기
});

// 글 단일 조회
router.get(&#39;/:id&#39;, (req, res) =&gt; {
  // TODO 글이 존재하는지 체크
  // TODO 글이 존재하면 응답 객체에 넣어서 보내기
});

// 글 등록
router.post(&#39;/&#39;, (req, res) =&gt; {
  // TODO title, content가 있는지 체크
  // TODO 글 추가
});

// 글 수정
router.put(&#39;/:id&#39;, (req, res) =&gt; {
  // TODO id, title, content가 있는지 체크
  // TODO 글이 존재하는지 체크
  // TODO 글 수정
});

// 글 삭제
router.delete(&#39;/:id&#39;, (req, res) =&gt; {
  // id가 있는지 체크
  // TODO 글이 존재하는지 체크
  // TODO 글 삭제
});

module.exports = router;</code></pre>
<p>이렇게 주석으로 개발할 내용을 정리해보고 하나하나 개발했다.</p>
<p><strong>post/index.js</strong></p>
<pre><code class="language-javascript">// 글 전체 조회
router.get(&#39;/&#39;, (req, res) =&gt; {
  res.send(posts.data);
});

// 글 단일 조회
router.get(&#39;/:id&#39;, (req, res) =&gt; {
  const post = posts.data.find((post) =&gt; post.id === parseInt(req.params.id));
  // TODO 글이 존재하는지 체크
  if (!post) {
    res.status(400).send({ message: &#39;존재하지 않는 글입니다.&#39; });
    return;
  }
  res.send(post);
});

// 글 등록
router.post(&#39;/&#39;, (req, res) =&gt; {
  const { title, content } = req.body;
  // TODO title, content가 있는지 체크
  if (!title || !content) {
    res.status(400).send({ message: &#39;Title, content는 필수 입력 사항입니다.&#39; });
    return;
  }
  // TODO 글 추가
  posts.data.push({
    ...req.body,
    id: posts.id,
  });
  posts.id++;
  res.send({ message: &#39;글을 등록했습니다.&#39; });
});

// 글 수정
router.put(&#39;/:id&#39;, (req, res) =&gt; {
  const { title, content } = req.body;
  // TODO id, title, content가 있는지 체크
  if (!req.params.id || !title || !content) {
    res.status(400).send({ message: &#39;id, title, content는 필수 입력 사항입니다.&#39; });
    return;
  }
  const post = posts.data.find((post) =&gt; post.id === parseInt(req.params.id));
  // TODO 글이 존재하는지 체크
  if (!post) {
    res.status(400).send({ message: &#39;존재하지 않는 글입니다.&#39; });
    return;
  }

  // TODO 글 수정
  post.title = title;
  post.content = content;
  res.send({ message: &#39;글을 수정했습니다.&#39; });
});

// 글 삭제
router.delete(&#39;/:id&#39;, (req, res) =&gt; {
  if (!req.params.id) {
    res.status(400).send({ message: &#39;id는 필수 입력 사항입니다.&#39; });
    return;
  }
  const post = posts.data.find((post) =&gt; post.id === parseInt(req.params.id));
  // TODO 글이 존재하는지 체크
  if (!post) {
    res.status(400).send({ message: &#39;존재하지 않는 글입니다.&#39; });
    return;
  }

  posts.data = posts.data.filter((_post) =&gt; _post.id !== post.id);
  res.send({ message: &#39;글을 삭제했습니다.&#39; });
});

module.exports = router;</code></pre>
<p>기능 구현을 다 했다면, app.js에 라우터를 등록해줘야 한다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">const postRouter = require(&#39;./post&#39;);
app.use(&#39;/post&#39;, postRouter);</code></pre>
<h2 id="📑-api-테스트">📑 API 테스트</h2>
<p>REST Client를 사용해서 api 테스트를 해보자.</p>
<p><strong>.http</strong></p>
<pre><code>POST http://localhost:3001/post
Content-Type: application/json

{
  &quot;title&quot;: &quot;title&quot;,
  &quot;content&quot;: &quot;content&quot;
}</code></pre><p><img src="https://velog.velcdn.com/images/seo__namu/post/de9b95bb-33a0-4a80-8f24-5c0a3ed10758/image.JPG" alt=""></p>
<p>API 요청을 보내면 글이 잘 등록된다. 3번 정도 똑같은 내용으로 글을 등록하고 전체 글 조회를 한 결과도 잘 받았다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/5c0c186b-3226-4dab-a05b-0500ef3ccab3/image.JPG" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express 로그인 회원가입 기능 구현]]></title>
            <link>https://velog.io/@seo__namu/Express-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@seo__namu/Express-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 10 Oct 2022 05:45:47 GMT</pubDate>
            <description><![CDATA[<p>나는 먼저 기능을 구현한 후에 데이터베이스와 연동해서 데이터를 저장했다. 데이터베이스에 연동하기 전까지는 우선 메모리에 저장하도록 했다.</p>
<h2 id="📝-회원가입-기능">📝 회원가입 기능</h2>
<p>회원가입을 하는데 2가지 조건이 있다.</p>
<ol>
<li>id, password, name는 필수로 입력해야 한다.</li>
<li>id는 중복되지 않도록한다.</li>
</ol>
<p>만약 두가지 조건에 부합하지 않는다면 회원가입에 실패한다. </p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">// 사용자 정보를 담을 배열
const users = [];

// 회원가입
app.post(&#39;/user/register&#39;, (req, res) =&gt; {
  const { id, password, name } = req.body;
  // TODO id, password, name이 있는지 체크한다.
  if (!id || !password || !name) {
    res.status(400).send({ message: &#39;id, password, name은 필수입력 사항입니다.&#39; });
    return;
  }

  // TODO id는 중복되지 않도록한다.
  const user = users.find((user) =&gt; user.id === id);
  if (user) {
    res.status(400).send({ message: &#39;이미 존재하는 아이디입니다.&#39; });
    return;
  }

  // TODO 사용자를 추가한다.
  users.push(req.body);
  res.send({ message: &#39;사용자를 등록했습니다.&#39; });
});</code></pre>
<h2 id="📑-rest-client를-사용해서-api-테스트하기">📑 REST Client를 사용해서 API 테스트하기</h2>
<p>회원가입 기능을 구현했으니, 잘 되는지 확인해보기 위해 API 테스트를 해보자.</p>
<p>Postman, Talend API 등 좋은 툴들이 많지만 나는 vscode에서 바로 사용할 수 있는 확장앱인 REST Client를 자주 사용하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/d6a5e392-3d07-45d4-a270-528ff9dd66ce/image.JPG" alt=""></p>
<p>확장앱을 설치하고 나서, .http 확장자를 사용해서 파일을 생성하고 아래처럼 http request를 작성해보자.</p>
<pre><code># Method, Path
POST http://localhost:3001/user/register 
# Headers
Content-Type: application/json
# Body
{
  &quot;id&quot;: &quot;test1&quot;,
  &quot;name&quot;: &quot;테스트1&quot;,
  &quot;password&quot;: &quot;qwer&quot;
}</code></pre><p>메소드 위에 &#39;Send Request&#39; 버튼을 클릭해서 http 요청을 보낼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/43f66c14-67cf-4c5a-aca5-4a1b1b548734/image.JPG" alt=""></p>
<p>처음 요청을 보내면, test1이라는 id를 사용하는 사용자가 없기때문에 무사히 회원가입에 성공한다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/a54834ac-5a18-4c38-9cc6-94b97c653454/image.JPG" alt=""></p>
<p>요청을 한번 더 보내면 이미 존재하는 아이디이기 때문에 회원가입에 실패한다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/2014f9f2-3e15-4a18-9aae-fa67ba1606e4/image.JPG" alt=""></p>
<h2 id="📝-로그인-기능">📝 로그인 기능</h2>
<p>로그인을 할 때는 id와 password를 받아서 두 가지를 체크한다.</p>
<ol>
<li>존재하는 아이디인지 확인한다.</li>
<li>존재하는 아이디일 경우, 비밀번호가 일치하는지 확인한다.</li>
</ol>
<p>만약 아이디가 존재하지 않거나, 비밀번호가 일치하지 않으면 로그인에 실패한다. 로그인 성공 시, 토큰을 반환해서 프론트엔드에서 로그인을 오래동안 유지할 수 있도록 할 예정이다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">app.post(&#39;/user/login&#39;, (req, res) =&gt; {
  const { id, password } = req.body;
  // TODO id, password가 있는지 체크한다.
  if (!id || !password) {
    res.status(400).send({ message: &#39;id, password는 필수입력 사항입니다.&#39; });
    return;
  }

  // TODO 입력받은 id의 사용자를 찾는다.
  const user = users.find((user) =&gt; user.id === id);
  if (!user) {
    res.status(400).send({ message: &#39;존재하지 않는 사용자입니다.&#39; });
    return;
  }

  // TODO 입력받은 password와 찾은 사용자의 password가 일치하는지 체크한다.
  if (user.password !== password) {
    res.status(400).send({ message: &#39;비밀번호가 일치하지 않습니다.&#39; });
    return;
  }

  // TODO 토큰을 발급한다.
  res.status(200).send({ token: &#39;token&#39; });
});</code></pre>
<h3 id="🚀-jwt">🚀 JWT</h3>
<p>나는 토큰 발급을 위해 jsonwebtoken 라이브러리를 사용했다.</p>
<pre><code>$ npm i jsonwebtoken</code></pre><p>서버는 토큰이 유효할 경우에만 로그인 상태라고 생각하고, 토큰이 유효하지 않으면 글 등록, 수정, 삭제 등의 기능을 제한할거다.</p>
<p>토큰 기능은 따로 jwt 폴더에 파일을 생성해서 관리했다.</p>
<p><strong>jwt/index.js</strong></p>
<pre><code class="language-javascript">const jwt = require(&#39;jsonwebtoken&#39;);

const tokenService = {
  // TODO 토큰 발급
  getToken(user_id) {
  // 토큰에 담을 정보, 사용할 키 (아무 값이나 가능), 토큰 옵션
    return jwt.sign({ user_id }, &#39;SECRET_KEY&#39;, {
      expiresIn: &#39;1d&#39; // 만료시간
    });
  },
  // TODO 토큰이 유효하다면 토큰에 담긴 정보를 반환
  getPayload(token) {
    return jwt.verify(token, &#39;SECRET_KEY&#39;);
  }
}

module.exports = tokenService;</code></pre>
<p>이제 로그인 기능에서 토큰 발급을 해서 반환하도록 하면 된다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">const tokenService = require(&#39;./jwt&#39;);
app.post(&#39;/user/login&#39;, (req, res) =&gt; {
  const { id, password } = req.body;
  // ...
  // TODO 토큰을 발급한다.
  res.status(200).send({ token: tokenService.getToken(id) });
});</code></pre>
<p>로그인 API를 테스트 하려고 하는데, 서버를 계속해서 재시작해서 기존 데이터가 사라져서 불편하다. 기본 사용자 정보를 배열에 저장한 후에 테스트를 진행했다.</p>
<pre><code class="language-javascript">const users = [
  {
    id: &#39;test&#39;,
    name: &#39;tester&#39;,
    password: &#39;test&#39;
  }
];</code></pre>
<p><strong>.http</strong></p>
<pre><code>POST http://localhost:3001/user/login 
Content-Type: application/json

{
  &quot;id&quot;: &quot;test&quot;,
  &quot;password&quot;: &quot;test&quot;
}</code></pre><p><img src="https://velog.velcdn.com/images/seo__namu/post/0138f5bc-31e1-4703-a2a7-81755f57a014/image.JPG" alt=""></p>
<p>응답으로 토큰을 잘 발급해주고 있다. </p>
<p>그런데 서버를 실행시키는 app.js 파일이 꽤 복잡해졌다. app.js는 서버를 실행시키는 코드만 남기고, 라우터를 관리하는 파일을 따로 생성해서 관심사를 분리해보자!</p>
<h2 id="🔌-라우터-분리하기">🔌 라우터 분리하기</h2>
<p>user 폴더를 생성하고 index.js 파일을 만들었다. </p>
<p>라우터를 분리하기 위해서는 express의 Router 함수로 라우터를 생성해서 사용하면 된다.</p>
<p><strong>user/index.js</strong></p>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const router = express.Router();

// app이 아닌 router!
router.post(&#39;/login&#39;, (req, res) =&gt; {
  // ...
});
router.post(&#39;/register&#39;, (req, res) =&gt; {
  // ...
});

module.exports = router;</code></pre>
<p>그리고 기존에 app.js에서는 userRouter를 불러와서 넣어주기만 하면 된다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">const userRouter = require(&#39;./user&#39;);
app.use(&#39;/user&#39;, userRouter);</code></pre>
<p>app.js가 매우 깔끔해졌다. ^0^</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express 개발 환경 구성하기]]></title>
            <link>https://velog.io/@seo__namu/Express-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seo__namu/Express-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 10 Oct 2022 03:40:42 GMT</pubDate>
            <description><![CDATA[<p>Express로 개발 하기에 앞서서, 먼저 개발 환경을 구성해야 한다.</p>
<h2 id="📁-npm-프로젝트-생성">📁 npm 프로젝트 생성</h2>
<p>Nodejs를 설치했다면 npm이 자동으로 설치되는데, npm을 사용해서 package들을 관리하기 위해 원하는 폴더에서 터미널을 열어서 아래 명령어로 npm 프로젝트를 생성한다. </p>
<pre><code>$ npm init -y</code></pre><p>-y옵션으로 프로젝트를 만들면 초기 세팅 없이 빈 프로젝트를 생성해준다.</p>
<h2 id="📂-express-설치">📂 Express 설치</h2>
<p>이제 Express를 설치해보자.</p>
<pre><code>$ npm i express</code></pre><h3 id="🔍-express-설치하는-동안-읽어보기">🔍 Express 설치하는 동안 읽어보기</h3>
<p>여러 자료나 강의를 보면 npm 모듈을 다운받을 때, <strong>--save(-S)</strong> 옵션을 주는 경우가 많은데 이 옵션은 package.json의 dependency 항목에 모듈을 추가한다는 의미다.</p>
<p>나는 -S를 안해도 잘만 dependency 항목에 들어가길래 무슨 차이인가 싶었는데, 예전에는 package.json dependency 항목에 추가되지 않는게 기본 옵션이었다고 한다. 다른 환경에서 작업할 경우 -S 옵션을 사용해 다운받지 않은 라이브러리는 npm i 를 했을 경우 node_modules에 추가되지 않았었다고,,</p>
<p>하지만 npm5 부터는 --save가 <strong>기본 옵션</strong>이라서 이제는 --save를 사용하지 않아도 dependency에 자동으로 항목이 추가된다.</p>
<h2 id="⌨️-express-서버-코드-작성">⌨️ Express 서버 코드 작성</h2>
<p>src 폴더 안에 app.js 파일을 생성한다. </p>
<p>Epxress 앱을 가져와서 localhost 3001번 포트에서 실행시키고, / 경로로 요청이 오면 &#39;Hello Express!&#39;를 보내주도록 했다.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const app = express();
app.use(express.json());
app.listen(3001, () =&gt; {
  console.log(&#39;http://localhost:3001&#39;);
});
app.get(&#39;/&#39;, (req, res) =&gt; {
  res.send(&#39;Hello Express!&#39;);
});</code></pre>
<p>나는 commonjs 방식으로 모듈들을 require해서 사용했는데, moudle 방식으로 import 해서 사용하고 싶다면 package.json에 아래 설정을 추가해주면 된다.</p>
<pre><code>&quot;type&quot;: &quot;module&quot;</code></pre><h2 id="💡-express-서버-실행">💡 Express 서버 실행</h2>
<p>src 경로에서 터미널을 열고 아래 명령어로 서버를 실행시킨다.</p>
<pre><code>node app.js</code></pre><p>그런데 서버를 실행시킬 때마다 매번 node app.js를 치는건 번거로운 일이다. 그래서 package.json에 scripts 객체에 start 명령어로 스크립트를 작성해보자.</p>
<p><strong>package.json</strong></p>
<pre><code class="language-node">&quot;scripts&quot;: {
  &quot;start&quot;: &quot;node ./src/app.js&quot;
}</code></pre>
<p>명령어가 실행되는건 package.json 파일이 있는 경로이기 때문에, 상대경로로 경로를 설정해줬다. </p>
<p>이제는 package.json이 있는 경로의 터미널에서 npm start라는 명령어로 간편하게 서버를 실행시킬 수 있다.</p>
<p>서버를 실행시키고, <a href="http://localhost:3001">http://localhost:3001</a>에 접속해보면 브라우저에 &#39;Hello Express!&#39;가 출력된 것을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/a2f5c4a4-f4f3-4ba3-b1c0-a51107649027/image.JPG" alt=""></p>
<p>&#39;Hello Express!&#39;가 아니라 &#39;Hello World.&#39;를 출력해보자.</p>
<p><strong>app.js</strong></p>
<pre><code class="language-javascript">app.get(&#39;/&#39;, (req, res) =&gt; {
  res.send(&#39;Hello World.&#39;);
});</code></pre>
<p>코드를 수정하고 저장을 한 뒤에 브라우저를 새로고침 해서 확인해봤는데, 여전히 &#39;Hello Express!&#39;가 출력된다.</p>
<p>이유는 node는 app.js가 최종적으로 저장된 결과물을 실행시켜줄 뿐, 서버가 실행되는 동안 소스가 변경된다 해도 반영되지 않는다. 그래서 내용이 변경되면 서버를 끄고, 다시 실행시켜야한다.</p>
<p>이런 불편함을 덜기위해 nodemon이라는 모듈을 사용해서, 자동으로 서버를 재시작해줄 수 있다.</p>
<h2 id="🔭-nodemon-사용해서-변경사항-감지하기">🔭 nodemon 사용해서 변경사항 감지하기</h2>
<p>아래 명령어로 nodemon을 설치한다.</p>
<pre><code>$ npm i nodemon</code></pre><p>그리고 package.json에서 서버를 실행시키는 명령어를 node가 아닌 nodemon으로 변경하면 된다.</p>
<p><strong>package.json</strong></p>
<pre><code>&quot;scripts&quot;: {
  &quot;start&quot;: &quot;nodemon --watch ./src ./src/app.js&quot;
}</code></pre><p>--watch 명령어로 src 폴더 경로에 있는 소스 코드의 변경을 감지하고 서버를 재시작해준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express로 게시판 개발하기]]></title>
            <link>https://velog.io/@seo__namu/Express-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@seo__namu/Express-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Mon, 10 Oct 2022 01:21:44 GMT</pubDate>
            <description><![CDATA[<h2 id="🐣-express-첫-걸음">🐣 Express 첫 걸음</h2>
<p>Express는 웹 어플리케이션 프레임워크다. Nodejs 기반으로 만들어진 프레임워크로 자바스크립트로 서버 개발이 가능해서 프론트엔드 개발자인 나도 어렵지 않게 서버 개발 경험을 해볼 수 있었다.</p>
<p>Express를 익히기 위해 혼자서 게시판 프로젝트를 진행했는데, 서버 개발을 하면서 DB연동도 하고 sql로 간단한 쿼리도 작성하면서 정말 즐거웠다!</p>
<h3 id="📚-게시판-프로젝트-목표">📚 게시판 프로젝트 목표</h3>
<!--![](https://velog.velcdn.com/images/seo__namu/post/4839494b-1ff7-487f-85b0-a5562b9b2cad/image.png)-->

<ul>
<li>CRUD 기능 개발</li>
<li>Middleware 사용</li>
<li>REST Client로 API 테스트</li>
<li>Database 연동</li>
<li>JWT로 토큰 발급</li>
<li>crypto로 비밀번호 암호화</li>
<li>CORS 라이브러리를 사용해 SOP(same origin policy) 처리</li>
<li>vscode로 디버깅</li>
</ul>
<p>간단해보이는 게시판 프로젝트여도 알아야 할 것들이 정말 많았다. 나는 기본적으로 서버를 구현할 때 알아야 한다고 생각되는 것들을 목표로 하고 구현해 나가면서 공부했다.</p>
<p>웹 프론트엔드 개발을 하면서는 디버깅을 많이 안했었는데, 사실 디버깅을 하지 않고 매번 console.log로 들어오는 파라미터 값들을 확인하는건 꽤 번거롭다. 그래서 express로 서버 개발을 하면서는 vscode의 디버깅 기능을 꼭 사용해보겠다고 마음 먹었는데, 생각보다 디버깅 방법이 간단했다.</p>
<h3 id="📃-어플리케이션-요구-사항">📃 어플리케이션 요구 사항</h3>
<p>요구 사항을 직접 설정하고 개발을 시작했다. 복잡한 어플리케이션이 아니여서, 간단하게 설정했다.</p>
<ul>
<li>로그인, 회원가입 기능</li>
<li>로그인 여부에 상관없이 글 조회 가능</li>
<li>로그인 한 사용자만 글 생성 가능</li>
<li>본인 글만 수정, 삭제 가능</li>
</ul>
<h3 id="📁-database-table">📁 Database Table</h3>
<p>테이블은 사용자 정보를 저장할 users와 글 정보를 저장할 posts 두 개가 있다.</p>
<p><strong>users Table</strong></p>
<table>
<thead>
<tr>
<th align="left">컬럼명</th>
<th align="left">Data Type</th>
<th align="center">Not Null</th>
</tr>
</thead>
<tbody><tr>
<td align="left">id</td>
<td align="left">varchar</td>
<td align="center">✔</td>
</tr>
<tr>
<td align="left">name</td>
<td align="left">varchar</td>
<td align="center">✔</td>
</tr>
<tr>
<td align="left">password</td>
<td align="left">varchar</td>
<td align="center">✔</td>
</tr>
<tr>
<td align="left">salt</td>
<td align="left">varchar</td>
<td align="center">✔</td>
</tr>
</tbody></table>
<p><strong>posts Table</strong></p>
<table>
<thead>
<tr>
<th align="left">컬럼명</th>
<th align="left">Data Type</th>
<th align="center">Not Null</th>
</tr>
</thead>
<tbody><tr>
<td align="left">id</td>
<td align="left">varchar</td>
<td align="center">✔</td>
</tr>
<tr>
<td align="left">user_id</td>
<td align="left">varchar</td>
<td align="center">✔</td>
</tr>
<tr>
<td align="left">title</td>
<td align="left">varchar</td>
<td align="center">✔</td>
</tr>
<tr>
<td align="left">content</td>
<td align="left">varchar</td>
<td align="center">✔</td>
</tr>
<tr>
<td align="left">created_on</td>
<td align="left">varchar</td>
<td align="center">✔</td>
</tr>
</tbody></table>
<h2 id="👋-게시판-앱-소개">👋 게시판 앱 소개</h2>
<h3 id="📌-게시판-페이지">📌 게시판 페이지</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/6b9b0fde-ca32-44bd-9647-0ea8da0347b5/image.JPG" alt=""></p>
<p>디자인은 간단하게 했다. 글 조회는 로그인 없이 가능하지만, 글 작성 및 수정 삭제 기능은 로그인을 해야 가능하다.</p>
<p>Token에 사용자 정보를 담아서, 글 작성자와 글 수정 삭제를 요청하는 사용자가 같을 경우에만 요청을 처리해준다.</p>
<h3 id="📌-글-상세-페이지">📌 글 상세 페이지</h3>
<p><img src="https://velog.velcdn.com/images/seo__namu/post/a3f24adf-1fe1-4562-8e2f-42823101f3b9/image.JPG" alt=""></p>
<p>제목, 작성자, 작성일시, 내용을 조회해서 보여주는데 글 조회를 요청한 사용자와 글을 작성한 사용자가 같을 경우에만 수정, 삭제 버튼이 보이도록 했다.</p>
<p>이미 개발을 마친 상태이지만, 개발 과정을 기록해두고 싶어서 velog에 시리즈로 포스팅 할 예정이다.</p>
]]></description>
        </item>
    </channel>
</rss>