<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>soshin_dev.log</title>
        <link>https://velog.io/</link>
        <description>누군가의 선택지가 될 수 있는 사람이 되자</description>
        <lastBuildDate>Mon, 17 Apr 2023 10:29:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>soshin_dev.log</title>
            <url>https://velog.velcdn.com/images/soshin_dev/profile/de9217c6-419c-431c-a1cf-61f659a5266a/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. soshin_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/soshin_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[ts-Mockito 도입]]></title>
            <link>https://velog.io/@soshin_dev/ts-Mockito-%EB%8F%84%EC%9E%85</link>
            <guid>https://velog.io/@soshin_dev/ts-Mockito-%EB%8F%84%EC%9E%85</guid>
            <pubDate>Mon, 17 Apr 2023 10:29:51 GMT</pubDate>
            <description><![CDATA[<h1 id="ts-mockito-라이브러리를-도입하려고-생각한-이유">ts-Mockito 라이브러리를 도입하려고 생각한 이유</h1>
<p>현재 테스트 프레임워크로 Jest를 선택하고, 도입했지만 Jest의 Mock/Stub 기능의 불편함과 부족함이 느껴져, 이를 보완하기 위한 방법을 찾다가 <strong>ts-mockito</strong> 를 찾게 되었다.</p>
<p>기존 Jest를 사용하면서 느꼈던 불편함은 아래와 같다.</p>
<pre><code class="language-tsx">find_by_pk: jest.fn().mockImplementation(
              async (menu_master_idx: number): Promise&lt;MenuMasterSchema&gt; =&gt; {
                if (menu_master_idx === -99) {
                  return null;
                }
                return menu_master_data;
              }
            )
</code></pre>
<ul>
<li>별도의 <code>when</code> 을 지원하지 않아, Jest에서 위와 같이 Stub 함수 내부에서 분기 로직을 처리해야한다는점.</li>
<li>IDE의 지원을 받을 수 없는 문자열 베이스<ul>
<li>Stubbing 메소드 지정을 문자열로 하기 때문에 IDE의 리팩토링, 자동완성 등을 지원받을 수 없다.</li>
<li>특히 직접 메소드를 입력해야하니 오타 문제 등이 존재한다</li>
</ul>
</li>
<li>단순한 클래스 Stub에도 장황한 코드가 필요하다.</li>
<li>ts-Mockito는 Mock 라이브러리 이기 떄문에, 다른 테스트 프레임워크를 사용해도 mock 하는 부분은 동일하게 사용할 수 있다.</li>
</ul>
<p>위와 같은 이유 떄문에 ts-Mockito를 도입하기로 했다.</p>
<hr>
<h1 id="적용-과정">적용 과정</h1>
<p>기존 Service에서 함수를 Mocking 할 때는 아래와 같이 진행했다.</p>
<pre><code class="language-tsx">useValue: {
            search_seller_menu_list_v3: jest.fn().mockResolvedValue([menu_master_data]),
            update_menu_name: jest.fn().mockResolvedValue([1, [menu_master_data]]),
            menu_img_url: jest.fn().mockResolvedValue([1, [menu_master_data]]),
            update_menu_origin_price: jest.fn().mockResolvedValue([1, [menu_master_data]]),
            find_by_pk: jest.fn().mockImplementation(
              async (menu_master_idx: number): Promise&lt;MenuMasterSchema&gt; =&gt; {
                if (menu_master_idx === -99) {
                  return null;
                }
                return menu_master_data;
              }
            )
          }</code></pre>
<p>그리고 ts-mockito로 작성하면 아래와 같이 된다.</p>
<pre><code class="language-tsx">const mock_service = mock(MenuMasterService);
  when(mock_service.find_by_pk(anyNumber())).thenResolve(menu_master_data);
  when(mock_service.find_by_pk(-99)).thenResolve(null);
  when(mock_service.update_menu_name(anything())).thenResolve([1, [menu_master_data]]);
  when(mock_service.menu_img_url(anything())).thenResolve([1, [menu_master_data]]);
  when(mock_service.update_menu_origin_price(anything())).thenResolve([1, [menu_master_data]]);
  when(mock_service.search_seller_menu_list_v3(anything())).thenResolve([menu_master_data]);

useValue: instance(mock_service)
</code></pre>
<p>기존 Jest로 작성할 때 find_by_pk 는 특정 상황을 만들려면, Stub 함수 내부에서 분기 로직을 처리해야했지만, ts-mockito를 사용하면 위처럼 when으로 어떤 상황이 주어졌을 때 어떤 값을 반환할 것인지 직관적으로 파악할 수 있기 때문에 실제 코드 작성할 때 기존 로직에 대해 몰라도 모킹할 수 있도록 도와준다.</p>
<hr>
<h1 id="ts-mockito-사용법">ts-mockito 사용법</h1>
<p>기본 사용법은 <a href="https://github.com/NagRock/ts-mockito">공식 github</a>에 정리되어 있으니, 자주 사용되는 <code>when</code>, <code>verify</code>, <code>capture</code> 에 대해서만 작성하려고 한다.</p>
<h2 id="when">when</h2>
<p>when은 <strong>특정 상황에서 어떤 반환값 / 행위를 할지</strong> 지정할 수 있다.즉, 아래와 같은 상황을 지정할 수 있다.</p>
<ul>
<li>A 라는 값이</li>
<li>B 라는 메소드로 전달되면</li>
<li>C 라는 값이 반환되어야 한다</li>
</ul>
<p>여기서 A라고 불리는 <strong>특정 메소드 인자</strong>의 범위는 다음과 같다</p>
<ul>
<li><code>1</code>, <code>&quot;a&quot;</code>, <code>{&quot;menu_name&quot;:&quot;치킨&quot;}</code> 등의 고정된 값</li>
<li><code>anyString()</code>, <code>anyNumber()</code> 등 <strong>문자열, 숫자</strong>등의 타입</li>
<li><code>anyOfClass()</code>, <code>anyFunction()</code> 등의 클래스, 함수 타입</li>
<li><code>between()</code>, <code>objectContaining</code> 등 범위 조건</li>
</ul>
<pre><code class="language-tsx">when(mock_service.find_by_pk(anyNumber())).thenResolve(menu_master_data);
when(mock_service.find_by_pk(-99)).thenResolve(null);
when(mock_service.update_menu_name(anything())).thenResolve([1, [menu_master_data]]);
when(mock_service.menu_img_url(anything())).thenResolve([1, [menu_master_data]]);
when(mock_service.update_menu_origin_price(anything())).thenResolve([1, [menu_master_data]]);
when(mock_service.search_seller_menu_list_v3(anything())).thenResolve([menu_master_data]);</code></pre>
<p>이때, 결과값은 아래와 같이 지정해줄 수 있다.</p>
<ul>
<li><code>thenThrow</code>: throw Error</li>
<li><code>thenCall</code>: 별도의 커스텀 메소드(함수)를 호출</li>
<li><code>thenReturn</code>: return</li>
<li><code>thenResolve</code> : resolve promise</li>
<li><code>thenReject</code>: rejects promise</li>
</ul>
<h2 id="verify">verify</h2>
<p>verify 는 지정된 인자가 <strong>특정 조건</strong> (파라미터 값, 타입, 총 호출된 횟수, 호출 순서 등) 에 맞춰 <strong>몇번, 몇번째 순서로 호출되었음</strong>을 검증할 수 있다.</p>
<pre><code class="language-tsx">const myServiceMock = mock(MyService);
const myService = instance(myServiceMock);

myService.someMethod(42);

// 검증: someMethod가 인자 42와 함께 호출되었는지 확인합니다.
verify(myServiceMock.someMethod(42)).called();</code></pre>
<p>verify 를 쓸때 주의할 점은 다음과 같다</p>
<ul>
<li><code>verify</code> 의 인자는 <code>instance()</code> 의 결과가 아닌 <code>mock(MyService)</code> 의 결과가 사용되어야 한다</li>
</ul>
<p>when 절처럼 <code>instance(mockService)</code> 를 통해 검증해버리면 오류가 발생한다.</p>
<p>verify 자체가 검증문이고, 이때는 <code>mock(MyService)</code> 의 결과를 사용해야함을 주의해야한다.</p>
<h1 id="capture"><strong>capture</strong></h1>
<p><strong><code>capture</code></strong> 함수는 모의 객체(mock)의 특정 메서드가 호출될 때 사용된 인자를 캡쳐하여 검사하는 데 사용된다.</p>
<p>다음은 <strong><code>capture</code></strong>의 사용 예제 이다.</p>
<pre><code class="language-tsx">const myServiceMock = mock(MyService);
when(myServiceMock.someMethod(anything())).thenReturn(&#39;Hello World!&#39;);

const myService = instance(myServiceMock);
myService.someMethod(42);

// 검증: someMethod가 호출되었는지 확인합니다.
verify(myServiceMock.someMethod(anything())).called();

// 캡쳐: someMethod의 호출 시 사용된 인자를 캡쳐합니다.
const [capturedArg] = capture(myServiceMock.someMethod).first();
console.log(capturedArg); // 출력: 42
</code></pre>
<p>위 예제에서 <strong><code>capture</code></strong>함수를 사용하여 <strong><code>someMethod</code></strong> 호출 시 사용된 인자를 캡쳐하고, <strong><code>first()</code></strong>를 사용하여 첫 번째 호출에서 캡쳐된 인자를 가져왔습니다.</p>
<hr>
<p><a href="https://github.com/NagRock/ts-mockito">ts-mockito 라이브러리 </a></p>
<h1 id="적용-후기">적용 후기</h1>
<p>기존 Jest만 사용해서 mock 할 때 보다 훨씬 직관적이고, 특정 로직에서 반환 값이 달라지는 경우도 쉽게 작성할 수 있어서 좋았다.</p>
<p>다만 Jest mock이 아닌 ts-mockito에 대한 학습도 해야하다보니 기존 mock에 익숙한 사람이라면 금방 배우겠지만, 처음 하는 사람이라면 헷갈릴 수도 있다는 생각이 들었다.</p>
<p>아직 테스트코드를 작성한지 오래 되지 않아 공식문서와 구글링을 통해 찾아가면서 작성하고 있지만, 나중에 손에 익는다면 훨씬 보기 편하고 직관적으로 로직을 파악할 수 있는 테스트코드를 작성하는데 큰 도움이 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TDD 도입기]]></title>
            <link>https://velog.io/@soshin_dev/TDD-%EB%8F%84%EC%9E%85%EA%B8%B0</link>
            <guid>https://velog.io/@soshin_dev/TDD-%EB%8F%84%EC%9E%85%EA%B8%B0</guid>
            <pubDate>Wed, 12 Apr 2023 14:42:56 GMT</pubDate>
            <description><![CDATA[<h1 id="테스트-코드를-도입하기로-한-이유">테스트 코드를 도입하기로 한 이유</h1>
<h3 id="1-기존-서비스-코드의-리팩토링-시-기존-서비스에-영향을-주지-않게-하기-위해">1. 기존 서비스 코드의 리팩토링 시 기존 서비스에 영향을 주지 않게 하기 위해</h3>
<p>기존 서비스 코드에서 작동하던 로직들을 수정할 일이 있거나, 리팩토링 해야할 일이 있을 때, 해당 코드를 수정했을 때 어디 부분까지 이 코드가 영향을 미치는지, 수정해도 이상이 없는지 여부 등을 체크하기 어렵고, 놓치게 되어 에러가 발생하는 문제가 있었기 때문에, 테스트 코드를 도입하여 수정 시 어디 부분에 영향을 미치고, 어떤 부분에서 에러가 일어나는지 확실하게 체크하여 유지보수 및 신기술 도입 등에 도움을 주기 위해 필요했습니다.</p>
<h3 id="2-코드의-구조-및-품질-향상을-위해">2. 코드의 구조 및 품질 향상을 위해</h3>
<p>테스트 코드를 작성하기 위해서는 테스트 코드를 작성하기 쉽게 코드를 작성해야합니다.
테스트 코드를 작성하기 쉬운 코드란 , 한 함수에서는 하나의 로직만 수행해야하며, 많은 파라미터를 받아선 안되고, 제어할 수 없는 코드가 있으면 안된다 등이 있는데 이 것들은 모두 테스트코드 에서만이 아닌 개발 영역에서도 똑같은 이야기입니다.
즉 테스트하기 쉬운 코드라는건 서비스를 유지하기 좋은 품질 높은 코드라는 걸 의미하므로, 테스트 코드를 작성하면서 동시에 서비스를 이루고 있는 코드의 품질 또한 함께 향상시킬 수 있습니다.</p>
<hr>
<h1 id="jest를-선택한-이유">JEST를 선택한 이유</h1>
<h2 id="중요하게-생각하는-점">중요하게 생각하는 점</h2>
<ol>
<li><strong>현재 테스트코드를 작성해본 시니어 혹은 사수가 없는 환경이기 때문에 많은 사람들이 사용하고, 레포가 많은 라이브러리 일 것.</strong></li>
<li>공식문서가 잘 만들어져있어, 다른 곳을 보지 않고 공식문서만 참고하더라도 테스트코드를 작성할 수 있을 것.</li>
<li>Angular와 NestJs 두 곳 모두에서 환경을 구성할 때 문제가 없을 것.</li>
</ol>
<p>위 3개를 중요 시 하게 보았으며, 그 중 1번을 가장 높은 우선 순위로 두고 고르기로 했습니다.</p>
<p>위 사항들을 토대로 비교군을 3개 [ Jest, Mocha, Jasmine ]를 지정했으며, 그 비교군들 중에 왜 JEST를 선택했는지 작성하겠습니다.</p>
<h3 id="npm-다운로드-수-비교">NPM 다운로드 수 비교</h3>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/c008129e-e3a8-435d-920f-7f5b3104e380/image.png" alt="">
<strong>Jest &gt; Mocha &gt; Jasmine 순서대로 다운로드 수 가 많다.</strong></p>
<hr>
<h2 id="결론적으로-jest를-선택한-이유">결론적으로 JEST를 선택한 이유</h2>
<ul>
<li><p>장단점</p>
<h2 id="1-jest">1. Jest</h2>
<h3 id="장점">장점</h3>
<ul>
<li>테스트 속도: Jest는 빠른 테스트 실행 속도를 제공합니다. 이는 병렬 테스트 실행과 효율적인 파일 변경 감지 덕분입니다.</li>
<li>설정 용이성: Jest는 설정이 간단하며, 설정 파일을 통해 많은 옵션을 쉽게 구성할 수 있습니다.</li>
<li>간편한 mocking: Jest는 기본 제공 mock 함수를 통해 간편하게 모듈, 함수 또는 타이머를 모의할 수 있습니다.</li>
<li>코드 커버리지: Jest는 내장된 코드 커버리지 도구를 제공합니다. 이를 통해 테스트가 얼마나 많은 코드를 커버하는지 쉽게 파악할 수 있습니다.</li>
<li>NestJS와 Angular 호환성: Jest는 NestJS 및 Angular 와 잘 호환되며, NestJS에서 기본으로 제공하는 테스트 도구입니다. 
또한 Jasmine 을 기반으로 만들었기 때문에 Angular와의 호환성도 매우 높습니다.</li>
<li>커뮤니티 및 지원: Jest는 큰 커뮤니티와 광범위한 지원 문서를 가지고 있어, 도움이 필요한 경우 쉽게 찾을 수 있습니다.</li>
<li><strong>Immersive Watch Mode 를 사용하면 변경사항에 영향을 받는 파일만 테스트가 가능합니다.</strong>
<img src="https://velog.velcdn.com/images/soshin_dev/post/31335b51-15d1-4781-8f57-09d54d342b37/image.png" alt=""></li>
</ul>
</li>
</ul>
<pre><code>### 단점

- Jasmine에 비해 Angular 프로젝트에서의 사용 경험이 상대적으로 적습니다.

## 2. Jasmine

### 장점

- Angular와의 호환성: Angular는 기본적으로 Jasmine을 테스트 프레임워크로 사용하므로, Angular 프로젝트에서 사용하기에 아주 적합합니다.
- 통합 테스트 환경: Jasmine은 단위 테스트와 통합 테스트를 모두 지원하며, 프론트엔드와 백엔드 모두에서 사용할 수 있는 통합 테스트 환경을 제공합니다.

### 단점

- 테스트 속도: Jest에 비해 Jasmine의 테스트 실행 속도가 다소 느릴 수 있습니다. 이는 Jest가 병렬 테스트 실행과 효율적인 파일 변경 감지를 제공하기 때문입니다.
- 추가 도구 필요: Jasmine 자체에는 목(mock) 생성이나 스파이(spy) 기능이 내장되어 있지만, 추가 도구(예: Sinon)를 사용해야 할 수도 있습니다.
- 코드 커버리지: Jasmine에는 내장된 코드 커버리지 도구가 없으므로, 별도의 도구(예: Istanbul)를 사용해야 합니다.
![](https://velog.velcdn.com/images/soshin_dev/post/7bfae5dc-b6e2-4411-8358-5d368137222a/image.png)

## 3. Mocha

### 장점

- 유연성: Mocha는 매우 유연한 테스트 프레임워크로, 다양한 라이브러리와 플러그인과 함께 사용할 수 있습니다. 이는 원하는 기능과 도구를 추가하고 구성할 수 있는 큰 장점입니다.
- 커뮤니티와 지원: Mocha는 오랫동안 존재해온 테스트 프레임워크로 인해 커뮤니티와 지원이 강력합니다. 따라서 문제가 발생했을 때 솔루션을 찾기가 더 쉬울 수 있습니다.

### 단점

- 복잡한 설정: Mocha를 사용하면 다양한 라이브러리와 플러그인을 함께 사용해야 하는데, 이는 설정이 복잡해질 수 있습니다. 예를 들어, Angular와 NestJS에서 Mocha를 사용하려면, 추가로 assertion 라이브러리(예: Chai), mocking 라이브러리(예: Sinon), 코드 커버리지 도구(예: Istanbul) 등을 설치하고 설정해야 합니다.
- 기본적인 기능 제한: Mocha는 기본적인 기능만 제공하므로 필요한 도구를 직접 찾아서 추가해야 합니다. 이는 시간과 노력이 필요할 수 있습니다.
- NestJS와 Angular 호환성:: NestJS와 Angular 둘 다 기본 테스트 프레임워크로 Mocha를 사용하지 않고 있기 때문에 Mocha를 사용하려면 추가적인 구성 작업이 필요할 수 있습니다.
![](https://velog.velcdn.com/images/soshin_dev/post/05167cfe-771a-4856-acd1-1c60bc2d7e51/image.png)</code></pre><p>위 처럼 각자의 장단점을 찾아 분석해본 결과, 현재 우리 개발 팀의 상황에서는 참고할 레포가 많고, 문제 상황이 생겼을 때 알아볼 수 있는 자료가 많은 JEST가 좋다고 생각하여 결정하게 되었습니다.</p>
<ul>
<li>위 중요하게 생각하는 점을 모두 충족하는 Jest.
다운로드수의 경우 가장 최근 날짜 기준 [ 2023.03.31 ] 으로 Jest [21,544,753], Mocha [7,395,784], Jasmine [1,786,472 ] 로 압도적인 수로 가장 많습니다.</li>
<li>Angular 와 NestJs 두 환경에서 모두 사용이 가능하며, 호환성도 좋습니다.</li>
<li>공식문서가 가장 보기 편하게 되어 있으며, Angular와 NestJs에 적용한 베스트 사례 블로그 글 또한 공식문서에 올라와 있습니다.</li>
</ul>
<hr>
<h1 id="jest-자주-쓰는-matcher">JEST 자주 쓰는 Matcher</h1>
<p>Jest를 현 프로젝트에 적용해야하는데 어디부터 시작해야할지 고민이였지만,</p>
<p>가장 자주 보고, 수정하여 익숙한 Menu_master 부터 시작하기로 했습니다.</p>
<p>적용한 과정을 보여드리기 전 JEST에 대해 알고 보면 훨씬 이해가 빠를 것 같아 자주쓰는 matcher 등을 정리해놓았습니다.</p>
<h3 id="tobe"><strong>toBe</strong></h3>
<p><strong><code>toBe</code></strong>는 값이 일치하는지 확인합니다. 객체의 경우 참조가 같아야 일치로 간주됩니다.</p>
<pre><code class="language-tsx">test(&#39;2 + 2 는 4&#39;, () =&gt; {
  expect(2 + 2).toBe(4);
});</code></pre>
<h3 id="toequal"><strong>toEqual</strong></h3>
<p><strong><code>toEqual</code></strong>은 객체 또는 배열이 동일한 구조와 값을 가지고 있는지 확인합니다.</p>
<pre><code class="language-tsx">test(&#39;객체 동일성 확인&#39;, () =&gt; {
  const data = { one: 1, two: 2 };
  expect(data).toEqual({ one: 1, two: 2 });
});</code></pre>
<h3 id="tobenull-tobeundefined-tobedefined-tobetruthy-tobefalsy"><strong>toBeNull, toBeUndefined, toBeDefined, toBeTruthy, toBeFalsy</strong></h3>
<p>이 Matcher들은 각각 값이 <strong><code>null</code></strong>, <strong><code>undefined</code></strong>, 정의되어 있는지, 참으로 간주되는지, 거짓으로 간주되는지를 확인합니다.</p>
<pre><code class="language-tsx">test(&#39;null 확인&#39;, () =&gt; {
  const value = null;
  expect(value).toBeNull();
});

test(&#39;undefined 확인&#39;, () =&gt; {
  const value = undefined;
  expect(value).toBeUndefined();
});

test(&#39;정의 확인&#39;, () =&gt; {
  const value = &#39;defined&#39;;
  expect(value).toBeDefined();
});

test(&#39;참 확인&#39;, () =&gt; {
  const value = true;
  expect(value).toBeTruthy();
});

test(&#39;거짓 확인&#39;, () =&gt; {
  const value = false;
  expect(value).toBeFalsy();
});</code></pre>
<h3 id="tobegreaterthan-tobegreater-thanorequal-tobelessthan-tobelessthanorequal"><strong><strong>toBeGreaterThan, toBeGreater, ThanOrEqual, toBeLessThan, toBeLessThanOrEqual</strong></strong></h3>
<p>이 Matcher들은 숫자 값의 대소 관계를 확인합니다.</p>
<pre><code class="language-tsx">test(&#39;숫자 비교&#39;, () =&gt; {
  const value = 10;
  expect(value).toBeGreaterThan(5);
  expect(value).toBeGreaterThanOrEqual(10);
  expect(value).toBeLessThan(15);
  expect(value).toBeLessThanOrEqual(10);
});</code></pre>
<h3 id="tocontain-tocontainequal"><strong>toContain, toContainEqual</strong></h3>
<p><strong><code>toContain</code></strong>은 배열이 특정 항목을 포함하는지 확인합니다. 객체의 경우, <strong><code>toContainEqual</code></strong>을 사용하여 동일한 구조와 값을 가지는 객체가 포함되어 있는지 확인할 수 있습니다.</p>
<pre><code class="language-tsx">test(&#39;배열 포함 확인&#39;, () =&gt; {
  const array = [&#39;apple&#39;, &#39;banana&#39;, &#39;orange&#39;];
  expect(array).toContain(&#39;banana&#39;);
  expect(array).not.toContain(&#39;grape&#39;);
});

test(&#39;객체 배열 포함 확인&#39;, () =&gt; {
  const array = [
    { id: 1, name: &#39;John&#39; },
    { id: 2, name: &#39;Jane&#39; },
  ];
  expect(array).toContainEqual({ id: 1, name: &#39;John&#39; });
  expect(array).not.toContainEqual({ id: 3, name: &#39;Jake&#39; });
});</code></pre>
<h3 id="tohavelength"><strong>toHaveLength</strong></h3>
<p><strong><code>toHaveLength</code></strong>는 문자열 또는 배열의 길이를 확인합니다.</p>
<pre><code class="language-tsx">test(&#39;길이 확인&#39;, () =&gt; {
  const string = &#39;hello&#39;;
  const array = [1, 2, 3];

  expect(string).toHaveLength(5);
  expect(array).toHaveLength(3);
});</code></pre>
<h3 id="tohaveproperty"><strong>toHaveProperty</strong></h3>
<p><strong><code>toHaveProperty</code></strong>는 객체가 특정 속성을 가지고 있는지 확인합니다.</p>
<pre><code class="language-tsx">test(&#39;객체 속성 확인&#39;, () =&gt; {
  const object = { name: &#39;John&#39;, age: 30 };

  expect(object).toHaveProperty(&#39;name&#39;);
  expect(object).toHaveProperty(&#39;name&#39;, &#39;John&#39;);
  expect(object).not.toHaveProperty(&#39;address&#39;);
});</code></pre>
<h3 id="tobeinstanceof"><strong>toBeInstanceOf</strong></h3>
<p>테스트에서 객체가 특정 클래스의 인스턴스인지 확인하는 데 사용합니다.</p>
<pre><code class="language-tsx">test(&#39;Object should be an instance of the specified class&#39;, () =&gt; {
  class MyClass {
    constructor() {}
  }

  const myObject = new MyClass();
  expect(myObject).toBeInstanceOf(MyClass);
});</code></pre>
<h3 id="jestfn"><strong>jest.fn()</strong></h3>
<p>Jest에서 가장 기본적인 mocking 기능을 제공하는 함수입니다. </p>
<p>이 함수는 새로운 mock 함수를 생성하며, 호출 횟수, 전달된 인수 등과 같은 정보를 추적합니다. 
이를 사용하여 함수를 대체하거나 테스트 동안 해당 함수의 동작을 제어할 수 있습니다.</p>
<pre><code class="language-tsx">const mockFn = jest.fn();
mockFn(&#39;Hello&#39;);
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(&#39;Hello&#39;);</code></pre>
<h3 id="jestspyon"><strong>jest.spyOn()</strong></h3>
<p>이 함수는 객체의 메소드에 대한 spy를 생성합니다. 
spyOn을 사용하면 원본 함수의 동작은 유지하면서 호출 횟수, 전달된 인수 등과 같은 정보를 추적할 수 있습니다. 선택적으로 원본 함수를 mock 구현으로 대체할 수도 있습니다.</p>
<pre><code class="language-tsx">const myObject = {
  sayHello: (name) =&gt; `Hello, ${name}!`
};

const spy = jest.spyOn(myObject, &#39;sayHello&#39;);
const result = myObject.sayHello(&#39;John&#39;);

expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(&#39;John&#39;);
expect(result).toBe(&#39;Hello, John!&#39;);</code></pre>
<h3 id="mockreturnvaluevalue"><strong>mockReturnValue(value)</strong></h3>
<p>이 함수는 mock 함수가 호출될 때 반환할 값을 설정합니다. 이를 사용하여 테스트 동안 함수의 반환값을 제어할 수 있습니다.</p>
<pre><code class="language-tsx">const mockFn = jest.fn().mockReturnValue(&#39;Hello, World!&#39;);
const result = mockFn();
expect(result).toBe(&#39;Hello, World!&#39;);</code></pre>
<h3 id="mockresolvedvaluevalue"><strong>mockResolvedValue(value)</strong></h3>
<p>이 함수는 mock 함수가 호출될 때 반환할 Promise를 설정합니다. 반환값이 resolve된 Promise로 감싸집니다. 이를 사용하여 테스트 동안 비동기 함수의 반환값을 제어할 수 있습니다.</p>
<pre><code class="language-tsx">const mockFn = jest.fn().mockResolvedValue(&#39;Hello, World!&#39;);
const result = await mockFn();
expect(result).toBe(&#39;Hello, World!&#39;);</code></pre>
<h3 id="mockrejectedvaluevalue"><strong>mockRejectedValue(value)</strong></h3>
<p>이 함수는 mock 함수가 호출될 때 반환할 rejected Promise를 설정합니다. 반환값이 reject된 Promise로 감싸집니다. 이를 사용하여 테스트 동안 비동기 함수의 반환값을 제어할 수 있습니다.</p>
<pre><code class="language-tsx">const mockFn = jest.fn().mockRejectedValue(new Error(&#39;An error occurred&#39;));
try {
  await mockFn();
} catch (error) {
  expect(error).toBeInstanceOf(Error);
  expect(error.message).toBe(&#39;An error occurred&#39;);
}</code></pre>
<h3 id="mockimplementationfn"><strong>mockImplementation(fn)</strong></h3>
<p>이 함수는 mock 함수의 구현을 대체합니다. 이를 사용하여 원래 함수와 다른 동작을 하는 함수로 대체할 수 있습니다.</p>
<pre><code class="language-tsx">const mockFn = jest.fn().mockImplementation((name) =&gt; `Hello, ${name}!`);
const result = mockFn(&#39;John&#39;);
expect(result).toBe(&#39;Hello, John!&#39;);</code></pre>
<h3 id="tohavebeencalled"><strong>toHaveBeenCalled()</strong></h3>
<p>이 Matcher는 함수가 호출되었는지 확인하는 데 사용됩니다. 
테스트에서 Mock이나 Spy가 호출되었는지 확인할 수 있습니다. 
호출되지 않았다면 테스트는 실패합니다.</p>
<pre><code class="language-tsx">const mockFunction = jest.fn();
mockFunction();
expect(mockFunction).toHaveBeenCalled();</code></pre>
<h3 id="tohavebeencalledtimescount"><strong>toHaveBeenCalledTimes(count)</strong></h3>
<p>이 Matcher는 함수가 정확한 횟수만큼 호출되었는지 확인하는 데 사용됩니다. 
함수 호출 횟수가 다르면 테스트가 실패합니다.</p>
<pre><code class="language-tsx">const mockFunction = jest.fn();
mockFunction();
mockFunction();
expect(mockFunction).toHaveBeenCalledTimes(2);</code></pre>
<h3 id="tohavebeencalledwitharg1-arg2-"><strong>toHaveBeenCalledWith(arg1, arg2, ...)</strong></h3>
<p>이 Matcher는 함수가 주어진 인수로 호출되었는지 확인하는 데 사용됩니다. 
인수가 다르면 테스트가 실패합니다.</p>
<pre><code class="language-tsx">const mockFunction = jest.fn();
mockFunction(1, &#39;hello&#39;);
expect(mockFunction).toHaveBeenCalledWith(1, &#39;hello&#39;);</code></pre>
<h3 id="tohavebeenlastcalledwitharg1-arg2-"><strong>toHaveBeenLastCalledWith(arg1, arg2, ...)</strong></h3>
<p>이 Matcher는 함수의 마지막 호출이 주어진 인수로 이루어졌는지 확인하는 데 사용됩니다. 마지막 호출에 사용된 인수가 다르면 테스트가 실패합니다.</p>
<pre><code class="language-tsx">const mockFunction = jest.fn();
mockFunction(1, &#39;hello&#39;);
mockFunction(2, &#39;world&#39;);
expect(mockFunction).toHaveBeenLastCalledWith(2, &#39;world&#39;);</code></pre>
<h3 id="tohavebeennthcalledwithnthcall-arg1-arg2-"><strong>toHaveBeenNthCalledWith(nthCall, arg1, arg2, ...)</strong></h3>
<p>이 Matcher는 함수의 n번째 호출이 주어진 인수로 이루어졌는지 확인하는 데 사용됩니다. 
n번째 호출에 사용된 인수가 다르면 테스트가 실패합니다.</p>
<pre><code class="language-tsx">const mockFunction = jest.fn();
mockFunction(1, &#39;hello&#39;);
mockFunction(2, &#39;world&#39;);
expect(mockFunction).toHaveBeenNthCalledWith(1, 1, &#39;hello&#39;);</code></pre>
<h3 id="objectcontaining"><strong>objectContaining</strong></h3>
<p>사용자가 지정한 속성들과 일치하는 속성을 가진 객체와 일치하는지 확인합니다. 
매칭되는 객체에 다른 속성이 있어도 상관없습니다.</p>
<p>예를 들어, 아래와 같이 <strong><code>objectContaining</code></strong> 매처를 사용하여 객체에 특정 속성이 있는지 확인할 수 있습니다.</p>
<pre><code class="language-tsx">const receivedObject = {
  name: &#39;John&#39;,
  age: 25,
  city: &#39;New York&#39;,
};

expect(receivedObject).toEqual(
  expect.objectContaining({
    name: &#39;John&#39;,
    age: 25,
  })
);</code></pre>
<h3 id="resolves"><strong>resolves</strong></h3>
<p><strong><code>resolves</code></strong>는 Promise가 성공적으로 resolve되었을 때, 예상되는 값을 검증하는 데 사용됩니다. 
예를 들어, 다음과 같이 사용할 수 있습니다.</p>
<pre><code class="language-tsx">test(&#39;async test with resolves&#39;, async () =&gt; {
  const promise = Promise.resolve(&#39;Success&#39;);
  await expect(promise).resolves.toBe(&#39;Success&#39;);
});</code></pre>
<h3 id="rejects"><strong>rejects</strong></h3>
<p><strong><code>rejects</code></strong>는 Promise가 거부되었을 때, 예상되는 에러를 검증하는 데 사용됩니다. </p>
<p>예를 들어, 다음과 같이 사용할 수 있습니다.</p>
<pre><code class="language-tsx">test(&#39;async test with rejects&#39;, async () =&gt; {
  const promise = Promise.reject(new Error(&#39;Error&#39;));
  await expect(promise).rejects.toThrow(&#39;Error&#39;);
});</code></pre>
<hr>
<h3 id="스파이와-모의mock의-차이"><strong>스파이와 모의(Mock)의 차이</strong></h3>
<p>스파이와 모의의 주요 차이점은 대체 여부입니다. 스파이는 기존 함수를 대체하지 않고 호출을 추적하며, 모의는 기존 함수나 모듈을 완전히 대체합니다. 스파이는 기존 구현을 사용하고 호출을 추적하는 반면, 모의는 기존 구현을 사용하지 않고 테스트에서 지정한 동작을 수행합니다.</p>
<ul>
<li>스파이를 사용하는 경우:<ul>
<li>함수가 호출되었는지 확인하고 싶을 때</li>
<li>함수가 특정 인자로 호출되었는지 확인하고 싶을 때</li>
<li>기존 구현을 유지하면서 함수 호출을 추적하고 싶을 때</li>
</ul>
</li>
<li>모의를 사용하는 경우:<ul>
<li>기존 구현을 완전히 대체하고 싶을 때</li>
<li>특정 동작이 발생할 때 특정 값을 반환하도록 설정하고 싶을 때</li>
<li>외부 서비스, API 호출, 파일 시스템 작업 등의 부작용을 가진 함수를 테스트하고 싶을 때</li>
</ul>
</li>
</ul>
<hr>
<p><strong><code>test</code></strong>와 <strong><code>describe</code></strong>와 <strong><code>it</code></strong>는 Jest 테스트 프레임워크에서 사용되는 함수들입니다. </p>
<ol>
<li><strong><code>test</code></strong>: 이 함수는 개별 테스트 케이스를 정의하는 데 사용됩니다. <strong><code>test</code></strong> 함수는 첫 번째 인수로 테스트 케이스의 설명을 문자열로 받고, 두 번째 인수로 실행할 테스트 코드를 함수로 받습니다.</li>
</ol>
<p>예시:</p>
<pre><code class="language-tsx">test(&#39;숫자 2와 3을 더하면 5가 나온다&#39;, () =&gt; {
  expect(2 + 3).toBe(5);
});</code></pre>
<ol>
<li><strong><code>describe</code></strong>: 이 함수는 관련된 테스트 케이스들을 그룹화하는 데 사용됩니다. </li>
</ol>
<p><strong><code>describe</code></strong>는 첫 번째 인수로 그룹의 설명을 문자열로 받고, 두 번째 인수로 그룹 내부의 테스트 케이스들을 정의하는 함수로 받습니다. 
<strong><code>describe</code></strong> 블록 내부에서는 <strong><code>test</code></strong> 또는 <strong><code>it</code></strong> 함수를 사용하여 테스트 케이스를 정의할 수 있습니다.</p>
<p>예시:</p>
<pre><code class="language-tsx">describe(&#39;덧셈 테스트&#39;, () =&gt; {
  test(&#39;숫자 2와 3을 더하면 5가 나온다&#39;, () =&gt; {
    expect(2 + 3).toBe(5);
  });

  test(&#39;숫자 4와 6을 더하면 10이 나온다&#39;, () =&gt; {
    expect(4 + 6).toBe(10);
  });
});</code></pre>
<ol>
<li><strong><code>it</code></strong>: 이 함수는 <strong><code>test</code></strong>와 동일한 역할을 합니다. <strong><code>it</code></strong>은 BDD(Behavior-Driven Development) 스타일의 테스트 프레임워크에서 사용되는 함수이며, Jest에서도 지원됩니다. </li>
</ol>
<p><strong><code>it</code></strong> 함수는 테스트 케이스의 설명을 자연스럽게 작성할 수 있도록 돕습니다.</p>
<p>예시:</p>
<pre><code class="language-tsx">describe(&#39;덧셈 테스트&#39;, () =&gt; {
  it(&#39;should return 5 when adding 2 and 3&#39;, () =&gt; {
    expect(2 + 3).toBe(5);
  });

  it(&#39;should return 10 when adding 4 and 6&#39;, () =&gt; {
    expect(4 + 6).toBe(10);
  });
});</code></pre>
<p>결론적으로, <strong><code>test</code></strong>와 <strong><code>it</code></strong>는 개별 테스트 케이스를 정의하는 데 사용되며, 기능적으로 동일합니다. 
두 함수 중 선호하는 것을 선택하여 사용할 수 있습니다. 
<strong><code>describe</code></strong>는 테스트 케이스를 그룹화하는 데 사용되며, 테스트 코드의 구조를 명확하게 표현할 수 있습니다.</p>
<hr>
<h2 id="jest-hook">JEST Hook</h2>
<h3 id="beforeall"><strong>beforeAll</strong></h3>
<p>이 훅은 테스트 스위트의 모든 테스트 케이스가 실행되기 전에 한 번만 호출됩니다. 
<strong><code>beforeAll</code></strong>에서는 일반적으로 테스트 전에 필요한 리소스를 설정하거나 초기화하는 작업을 수행합니다.
예를 들어, 데이터베이스 연결을 설정하거나 테스트에 필요한 데이터를 생성할 수 있습니다.</p>
<pre><code class="language-tsx">beforeAll(() =&gt; {
  // 이 부분은 테스트 스위트가 실행되기 전에 한 번만 실행됩니다.
});</code></pre>
<h3 id="afterall"><strong>afterAll</strong></h3>
<p>이 훅은 테스트 스위트의 모든 테스트 케이스가 완료된 후에 한 번만 호출됩니다. 
<strong><code>afterAll</code></strong>에서는 일반적으로 테스트 후에 정리해야 하는 작업을 수행합니다. 
예를 들어, 데이터베이스 연결을 종료하거나 생성된 데이터를 삭제할 수 있습니다.</p>
<pre><code class="language-tsx">
afterAll(() =&gt; {
  // 이 부분은 테스트 스위트가 완료된 후에 한 번만 실행됩니다.
});</code></pre>
<h3 id="beforeeach"><strong>beforeEach</strong></h3>
<p>이 훅은 각 테스트 케이스가 실행되기 전에 호출됩니다. 
<strong><code>beforeEach</code></strong>에서는 일반적으로 각 테스트 케이스 전에 필요한 설정 작업을 수행합니다. 
예를 들어, 테스트에 필요한 초기 상태를 설정하거나 목(mock) 객체를 생성할 수 있습니다.</p>
<pre><code class="language-tsx">beforeEach(() =&gt; {
  // 이 부분은 각 테스트 케이스가 실행되기 전에 호출됩니다.
});</code></pre>
<h3 id="aftereach"><strong>afterEach</strong></h3>
<p>이 훅은 각 테스트 케이스가 완료된 후에 호출됩니다. 
<strong><code>afterEach</code></strong>에서는 일반적으로 각 테스트 케이스 후에 정리해야 하는 작업을 수행합니다. 
예를 들어, 테스트에서 변경된 상태를 복원하거나 목 객체를 초기화할 수 있습니다.</p>
<pre><code class="language-tsx">afterEach(() =&gt; {
  // 이 부분은 각 테스트 케이스가 완료된 후에 호출됩니다.
});
</code></pre>
<hr>
<h2 id="적용-과정--menu_master_service">적용 과정 [ Menu_master_service]</h2>
<h3 id="1-describe-를-통해-테스트-케이스-그룹화-및-testingmodule-을-통한-테스트-환경-격리">1. <strong><code>describe</code> 를 통해 테스트 케이스 그룹화 및 TestingModule 을 통한 테스트 환경 격리</strong></h3>
<pre><code class="language-tsx">describe(&#39;MenuMasterService&#39;, () =&gt; {
  let service: MenuMasterService;

  beforeEach(async () =&gt; {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        MenuMasterService,
        MenuMasterSchema
      ]
    }).compile();

    service = module.get&lt;MenuMasterService&gt;(MenuMasterService);
    jest.clearAllMocks();
  });</code></pre>
<p>위 코드에서 TestingModule은 아래와 같은 이유로 설정해주어야 합니다.</p>
<p>NestJS 애플리케이션은 여러 모듈과 서비스로 구성되어 있기 때문에 전체 애플리케이션을 실행하면, 모든 모듈과 서비스가 함께 로드되어 실행됩니다. 
그러나 테스트를 진행할 때는 전체 애플리케이션을 실행할 필요가 없습니다. 대신 특정 모듈이나 서비스에 집중하여 해당 부분만 테스트하는 것이 바람직합니다.</p>
<p>NestJS에서 제공하는 <strong><code>TestingModule</code></strong>을 사용하면, 전체 애플리케이션 대신 테스트 대상 모듈이나 서비스와 관련된 부분만 로드할 수 있습니다. 
즉, 애플리케이션의 일부분만 테스트하기 위한 격리된 환경을 생성하는 것입니다. 
이렇게 함으로써 테스트의 실행 속도를 높이고, 다른 모듈이나 서비스로 인한 부작용을 최소화할 수 있습니다.</p>
<p>예를 들어 위와 같이, <strong><code>MenuMasterService</code></strong>를 테스트할 때 전체 애플리케이션을 로드하는 대신 <strong><code>MenuMasterService</code></strong>와 관련된 의존성만 로드하여 서비스의 동작을 테스트할 수 있습니다. 
이는 테스트의 목적과 범위를 명확히 하고, 필요한 리소스만 사용하여 효율적으로 테스트를 수행할 수 있도록 합니다.</p>
<h3 id="2-provider에-의존성-주입하기">2. Provider에 의존성 주입하기</h3>
<ol>
<li><strong><code>MenuMasterService</code></strong>와 관련된 의존성을 <strong><code>providers</code></strong> 배열에 추가합니다. 이를 통해 <strong><code>MenuMasterService</code></strong>가 필요로 하는 의존성을 제공하고 테스트 환경에서 사용할 수 있습니다.
``테스트에 사용할 서비스인<code>MenuMasterService</code> 를 주입합니다.</li>
<li><strong><code>MenuMasterSchema</code></strong> 의존성 주입</li>
</ol>
<h3 id="3-테스트-모듈을-컴파일">3. 테스트 모듈을 컴파일</h3>
<p><strong><code>compile</code></strong>함수를 호출하여 테스트 모듈을 컴파일합니다.</p>
<h3 id="4-인스턴스-가져오기">4. 인스턴스 가져오기</h3>
<p> <strong><code>get</code></strong> 함수를 사용하여 테스트 모듈에서 <strong><code>MenuMasterService</code></strong> 인스턴스를 가져옵니다. 이 인스턴스는 각 테스트 케이스에서 사용됩니다.</p>
<p>[ beforeEach에서 선언했기 때문 ]</p>
<h3 id="5-mock-함수-초기화">5. Mock 함수 초기화</h3>
<p><strong><code>jest.clearAllMocks</code></strong> 함수를 호출하여 모든 Jest mock 함수의 호출 정보를 초기화합니다. 
이렇게 하면 각 테스트 케이스가 독립적으로 실행되어 서로 영향을 주지 않도록 할 수 있습니다.</p>
<hr>
<h2 id="menu_master_service-테스트-코드의-hook-살펴보기">Menu_master_service 테스트 코드의 Hook 살펴보기</h2>
<pre><code class="language-tsx">// src/modules/menu_master/menu_master.service.spec.ts
beforeAll(async () =&gt; {
  before_data = await MenuMasterSchema.findOne({ where: { menu_master_idx: 1 }, logging: false });
});

beforeEach(async () =&gt; {
  const module: TestingModule = await Test.createTestingModule({
        providers: [MenuMasterService, MenuMasterSchema]    
        }).compile();

    service = module.get&lt;MenuMasterService&gt;(MenuMasterService);
    jest.clearAllMocks();
  });

 afterAll(async () =&gt; {
    const { menu_name, menu_img_url, menu_origin_price } = before_data;
  await MenuMasterSchema.update(
    { menu_name, menu_img_url, menu_origin_price },
    { where: { menu_master_idx: before_data.menu_master_idx }, logging: false }
  );
  await MenuMasterSchema.destroy({ where: { menu_master_idx: delete_idx }, logging: false });
  jest.restoreAllMocks();
  await sequelize.close();
});</code></pre>
<h3 id="beforeall-1"><strong>beforeAll</strong></h3>
<p><strong><code>beforeAll</code></strong>은 테스트 파일 내의 모든 테스트 케이스가 실행되기 전에 한 번만 실행되는 함수입니다. 
여기서는 테스트를 시작하기 전에 <strong><code>MenuMasterSchema</code></strong>에서 <strong><code>menu_master_idx</code></strong>가 1인 데이터를 찾아 <strong><code>before_data</code></strong> 변수에 저장하고 있습니다. 
이 데이터는 테스트가 끝난 후 원래 상태로 복구할 때 사용됩니다.</p>
<h3 id="beforeeach-1"><strong>beforeEach</strong></h3>
<p><strong><code>beforeEach</code></strong>는 각 테스트 케이스가 실행되기 전에 매번 호출되는 함수입니다. 
여기서는 테스트 모듈을 생성하고 컴파일하며, <strong><code>MenuMasterService</code></strong>와 <strong><code>MenuMasterSchema</code></strong>를 프로바이더로 등록합니다. 
그리고 <strong><code>MenuMasterService</code></strong> 인스턴스를 가져와 <strong><code>service</code></strong> 변수에 저장하고, 모든 Jest 목(mock) 함수의 호출 정보를 초기화합니다. 
이렇게 하면 각 테스트 케이스가 독립적으로 실행되도록 보장할 수 있습니다.</p>
<h3 id="afterall-1"><strong>afterAll</strong></h3>
<p><strong><code>afterAll</code></strong>은 테스트 파일 내의 모든 테스트 케이스가 실행된 후 한 번만 실행되는 함수입니다. 
여기서는<strong><code>beforeAll</code></strong>에서 저장한 <strong><code>before_data</code></strong>를 사용하여 원래의 <strong><code>MenuMasterSchema</code></strong> 데이터를 복구하고, 테스트에서 생성된 데이터를 삭제합니다. 
그리고 모든 Jest 목(mock) 함수의 복원을 시도하고, 마지막으로 Sequelize 연결을 닫습니다. 
이렇게 하면 테스트 실행 이후 환경을 정리할 수 있습니다.</p>
<hr>
<h3 id="clearmocks"><strong>clearMocks</strong></h3>
<p><strong><code>clearMocks</code></strong> 메서드는 Jest에서 생성한 모든 목(mock) 함수의 호출 정보를 초기화합니다. 
즉, 이 메서드를 호출한 이후로 목 함수가 얼마나 호출되었는지, 어떤 인자와 함께 호출되었는지 등의 정보가 초기화됩니다. 
이를 통해 각 테스트 케이스가 서로 영향을 주지 않고 독립적으로 실행될 수 있도록 합니다. 
일반적으로 <strong><code>beforeEach</code></strong> 훅에서 호출되어, 각 테스트 케이스 시작 전에 목 함수의 상태를 초기화합니다.</p>
<h3 id="restoreallmocks"><strong>restoreAllMocks</strong></h3>
<p><strong><code>restoreAllMocks</code></strong> 메서드는 Jest에서 생성한 모든 목(mock) 함수를 원래의 구현으로 복원합니다. 
이 메서드는 테스트 실행 이후에 호출되어, 테스트 도중 변경된 목 함수를 원래 상태로 되돌립니다. 
이를 통해 다른 테스트 파일이나 테스트 외부의 코드에서 목 함수를 사용할 때, 테스트로 인해 변경된 상태가 영향을 주지 않도록 합니다. 
일반적으로 <strong><code>afterAll</code></strong> 훅에서 호출되어, 모든 테스트 케이스가 실행된 후에 목 함수의 상태를 복원합니다.</p>
<hr>
<h2 id="실제-적용한-테스트-코드-살펴보기">실제 적용한 테스트 코드 살펴보기</h2>
<h2 id="-menu_masterservicespects-">[ Menu_master.service.spec.ts ]</h2>
<pre><code class="language-tsx">it(&#39;MenuMasterService 가 정상적으로 등록되어있는지 확인&#39;, () =&gt; {
    expect(service).toBeDefined();
  });</code></pre>
<p>위 코드는 연결 한 menu_master.service가 정상적으로 등록이 되었는지 toBeDefined() 로 확인하는 테스트입니다.
이때 인스턴스가 생성되지 않아 service에 할당되지 않았다면 undefined 이기 때문에 에러가 발생합니다.</p>
<pre><code class="language-tsx">describe(&#39;create 테스트&#39;, () =&gt; {
    it(&#39;함수 등록 확인&#39;, () =&gt; {
      expect(service.create).toBeDefined();
    });

    it(&#39;원하는 데이터로 create 됐는지 확인&#39;, async () =&gt; {
      const data: InputMenuMasterSchema = {
        menu_name: &#39;Create Test Menu Name&#39;,
        menu_origin_price: 10000,
        menu_img_url: &#39;https://~~~&#39;
      };
      const result = await service.create(data);
      expect(result).toBeInstanceOf(MenuMasterSchema);
      delete_idx = result.menu_master_idx;
    });
  });</code></pre>
<p>위 코드는 MenuMasterService에서 create 함수를 테스트 하는 코드입니다.
위에서 아까와 같이 현재 service에 create 라는 함수가 있는지 확인하기 위해 toBeDefined Matcher로 확인합니다.
그 후 함수의 결과 값이 MenuMasterSchema와 일치 하는지 확인 후, 나중에 DB에서 생성한 값을 삭제하기 위해 생성한 idx 값을 저장해둡니다.</p>
<pre><code class="language-tsx">describe(&#39;search_seller_menu_list_v3 테스트&#39;, () =&gt; {
    it(&#39;함수 등록 확인&#39;, () =&gt; {
      expect(service.search_seller_menu_list_v3).toBeDefined();
    });
    it(&#39;함수 반환 값이 MenuMasterSchema 배열을 반환하는지 확인&#39;, async () =&gt; {
      const result = await service.search_seller_menu_list_v3({ take: 1, skip: 0 });
      expect(result).toBeInstanceOf(Array);
      if (result.length &gt; 0) {
        expect(result[0]).toBeInstanceOf(MenuMasterSchema);
      }
    });
  });</code></pre>
<p>위 코드는 search_seller_menu_list_v3 함수를 테스트하는 코드입니다.
search_seller_menu_list_v3 함수의 반환 값은 MenuMasterSchema 배열인데 이때, toBeInstanceOf 를 통해 반환 값이 배열의 형태인지 확인하고, mock 데이터로 하는게 아닌 실제 DB 값을 가져오는 것 이기 때문에 데이터가 없는 경우도 있어, 현재 가져온 배열의 길이를 체크 한 후, 값이 있는 경우에 그 값이 MenuMasterSchema 형태인지 확인합니다.</p>
<pre><code class="language-tsx">describe(&#39;find_include_brand_by_idx 테스트&#39;, () =&gt; {
    let brand: BrandSchema;
    it(&#39;함수 등록 확인&#39;, () =&gt; {
      expect(service.find_include_brand_by_idx).toBeDefined();
    });

    it(&#39;함수 반환 값이 MenuMasterSchema 타입인지 확인&#39;, async () =&gt; {
      const result = await service.find_include_brand_by_idx(before_data.menu_master_idx);
      brand = result?.brand;
      expect(result).toBeInstanceOf(MenuMasterSchema);
    });

    it(&#39;반환 값에 BrandSchema 가 포함되어 있는지 확인&#39;, () =&gt; {
      expect(brand).toBeInstanceOf(BrandSchema);
    });
  });</code></pre>
<p>위 코드는 menu_master_idx를 통해 MenuMaster테이블에서 원하는 row를 받아오는 함수입니다.
이때, include 를 통해 BrandSchema도 같이 가져오는데, 이를 확인하기 위해 받아온 값에서 brand 값을 한번 더 체크해주도록 하였습니다.</p>
<hr>
<h2 id="적용-후기">적용 후기</h2>
<p>Jest로 MenuMasterService와 MenuMasterController에 테스트코드를 작성하며 Covrage를 100%를 달성하였다.</p>
<p>테스트코드를 작성하면서 기존에 있던 함수들도 리팩토링을 조금 더 테스트하기 쉽게, 서로간의 역활을 명확하게 하고, 한 로직 당 한가지 행위를 수행하게 하기 위해 수정을 진행하며, 기존에는 수정하고, 직접 그걸 graphql로 불러서 확인하는 과정이 있었지만 이젠 수정한걸 편하게 테스트 코드를 돌려서 확인할 수 있다는게 엄청 편하게 느껴졌다.</p>
<p>특히 테스트코드를 작성하고 나서 리팩토링을 진행하니 어떤 부분들이 나뉘어져야하는지, 어떤 함수가 많은 양의 책임을 가지고 있었는지가 눈에 보여 더 쉽게 진행했었던거 같다.</p>
<p>지금은 이미 만들어져있는 코드를 가지고 테스트코드를 작성했지만, 앞으로 진행할 내용에서는 테스트 코드를 먼저 작성한 후, 그 테스트 코드에 맞춰서 로직을 작성하게 될 것이니, 더 유지보수하기 편한 코드를 작성할 수 있을 것 같아 기대가 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인프런 퇴근길 밋업 후기]]></title>
            <link>https://velog.io/@soshin_dev/%EC%9D%B8%ED%94%84%EB%9F%B0-%ED%87%B4%EA%B7%BC%EA%B8%B8-%EB%B0%8B%EC%97%85-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@soshin_dev/%EC%9D%B8%ED%94%84%EB%9F%B0-%ED%87%B4%EA%B7%BC%EA%B8%B8-%EB%B0%8B%EC%97%85-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Fri, 02 Dec 2022 03:19:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/soshin_dev/post/596eb502-3d44-42b8-b659-0ed46df08044/image.jpg" alt=""></p>
<h2 id="판교를-찾아가며">판교를 찾아가며</h2>
<p>판교는 태어나서 처음 가보지만 인프런 쪽에서 문자로 상세한 주소와 시간을 알려주셔서 퇴근 후 후딱 뛰어서 갔다.</p>
<p>건물을 올라간 후 어느쪽으로 가야할지 몰라서 뒤를 딱 도는데 인프런측에서 친절하게 어느쪽으로 가야하는지 표시를 해두셨고, 그 후 샌드위치와 물을 받아 입장하였다.</p>
<h2 id="발표-내용">발표 내용</h2>
<h3 id="세션-1--주니어-개발자의-nodejs-코드-변천사-">세션 1 [ 주니어 개발자의 Node.js 코드 변천사 ]</h3>
<p>첫 세션에서는 현재 인프랩에 재직 중 이시고, 랠릿 이라는 인프랩에서 준비하는 채용 사이트를 담당하고 계시는 이소연 주니어 개발자분의 발표였다.</p>
<p>내용은 멀티레포에서 모노레포로 리팩토링했던 과정들, 코드 파일을 분리했던 경험들, 등등 주니어 개발자로서 여러가지 느끼고, 경험했던 내용들을 말씀해주셔서 정말 공감하면서 들었던 내용이였다.</p>
<p>내가 만약 나가서 그렇게 발표하라고 하면 정말 벌벌 떨었을 것 같은데, 떨지 않고 자신이 준비해놓으신 내용을 차근차근 말씀 잘 해주셔서 집중해서 들었고, 주니어 개발자라고 모르는게 당연하다 생각하지말고 아직 여러가지 공부할게 많다는 걸 또 한번 느꼈던 세션이였다.</p>
<h3 id="세션-2-shell-we-nestjs">세션 2 [Shell We NestJS?]</h3>
<p>두 번째 세션은 팀 스파르타의 CTO 이신 남병관 CTO 님의 발표셨다.
내용은 NestJS 의 provider, controller, pipe 등 기본적인 내용을 설명해주셨으며, Nest가 왜 의존성 주입을 사용하여 서비스를 이용하게 하는지 등 기초적인 내용을 중심적으로 이야기 해주셨다.</p>
<h3 id="세션-3-nodejs에서-cpu-intensive한-코드를-찾아내는-방법">세션 3 [Node.js에서 CPU-intensive한 코드를 찾아내는 방법]</h3>
<p>세 번째 세션은 당근마켓의 김경덕 백엔드 개발자 분 이셨다.
이 세션이 제일 궁금하고 관심이 많이 갔었는데 역시 발표 내용을 듣고 들으러 오길 잘했다 라는 생각이 들었다.
개발을 진행하면서 부하테스트도 진행해보고싶었고, 어디서 병목이 발생하는지, 어디서 성능이 떨어지는지 이런 부분을 찾기 어려웠는데 그 부분에 대해 찾을 수 있게 도와주는 툴에 대해 말씀해주시고, 그 툴을 사용하여 어떤 식으로 수정하셨는지, 어떤 코드가 이런 병목을 발생시키는지에 대한 원인 등에 대해 정말 말씀을 잘 해주셔서 많은 도움이 됐던 세션이였다.</p>
<h2 id="밋업-후기">밋업 후기</h2>
<p>발표가 끝난 후 여러 QnA가 있었고, 이 또한 인프랩에서 시간이 부족하지 않게 여러가지 질문을 미리 받을 수 있게 셋팅해두었고, 그로인해 많은 사람이 궁금해하는 질문들을 위주로 답변을 잘 받을 수 있었다.</p>
<p>아쉽게도 네트워킹 시간은 막차 시간 때문에 참여하지 못했지만 인프랩에서 밋업에 오시는 분들이 불편하지 않게 준비했다는건 충분히 느껴져서 정말 좋은 경험이였다.</p>
<p>다음에 또 이런 자리가 마련된다면, 멀지만 또 한번 가보고 싶은 그런 밋업이였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[gRPC 란 무엇일까?]]></title>
            <link>https://velog.io/@soshin_dev/gRPC-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@soshin_dev/gRPC-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Fri, 25 Nov 2022 05:10:02 GMT</pubDate>
            <description><![CDATA[<h2 id="grpc-정리">gRPC 정리</h2>
<ul>
<li>gRPC는 구글에서 개발한 RPC(RPC, Remote Procedure Call) 시스템이며 모든 환경에서 실행할 수 있는 오픈 소스 <strong>고성능 RPC 프레임워크</strong>이다.</li>
<li>HTTP/2기반,  다양한 언어 Java, C ++, Python, Java Lite, Ruby, JavaScript, Objective-C 및 C#에서 사용이 가능하며 스텁, 프로토콜버퍼 등의 특징이 있으며 <strong>서비스들을 효율적으로 연결할 수 있는 강점</strong>을 가진다.</li>
</ul>
<h3 id="rpc란-무엇이인가">RPC란 무엇이인가?</h3>
<ul>
<li>RPC(Remote Procedure Call)는 프로세스 간 통신 (IPC)의 한 형태로 떨어져 있거나 분산되어 있는 컴퓨터간의 통신을 위한 기술이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/662ba6ba-bd3e-4f75-8718-9eaf8ebbc8e0/image.png" alt=""></p>
<p>클라이언트가 스텁을 거처 RPC의 런타임을 거처 다시 런타임, 스텁, 서버로 전달되는 과정이 들어가있는 것이 RPC의 매커니즘이다.</p>
<h3 id="스텁--stub--이란">스텁 [ Stub ] 이란?</h3>
<ul>
<li>스텁은 RPC의 핵심 개념으로 매개변수 [ parameter ] 객체를 메세지 [ Message ] 로 변환 [ Marshaling ] , 역변환 [ Unmarshaling ] 하는 레이어로, 클라이언트 스텁, 서버 스텁으로 나뉜다.<ul>
<li>클라이언트 스텁 [ Main ] : 함수 호출에 사용된 파라미터의 변환 및 함수 실행 후 서버에서 전달된 결과의 변환을 담당한다.</li>
<li>서버 스텁 [ feature-api ]: 클라이언트가 전달한 매개 변수의 역변환 및 함수 실행 결과 반환을 담당한다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/bb090e06-e2ed-4c23-a899-32c8150045e8/image.png" alt=""></p>
<p>위 그림처럼 스텁을 이용하여 서버와 클라이언트간의 언어가 달라도 요청과 응답을 구현할 수 있다.</p>
<h3 id="프로토콜-버퍼--protocol-buffer-">프로토콜 버퍼 [ protocol buffer ]</h3>
<ul>
<li>gRPC는 IDL [ interface Definition Language ] 로 프로토콜 버퍼를 사용한다.</li>
<li>프로토콜 버퍼는 구조화된 데이터를 직렬화하기 위한 구글의 언어 중립적, 플랫폼 중립적, 확장 가능한 메커니즘을 말한다.</li>
<li>보통 XML 이나 JSON을 생각할 수 있지만 더 작고 빠르다.</li>
<li>프로토콜 버퍼를 통해 데이터를 주고 받을 수 있기 때문에 어떤 언어나 플랫폼에서도 통신 프로토콜 등에 쉽게 적용할 수 있다.</li>
</ul>
<h3 id="장점">장점</h3>
<ul>
<li>gRPC는 대부분의 아키텍쳐에 사용할 수 있지만, MSA에 가장 적합한 기술로, 많은 서비스 간의 API 호출로 인한 성능 저하를 개선하며 보안, API 게이트웨이, 로깅등을 개발하기 쉽다.</li>
<li>높은 생산성을 가지고, 프로토콜 버퍼의 IDL 만 정의하면 서비스와 메세지에 대한 소스코드가 자동으로 생성되고 데이터를 주고 받을 수 있다.</li>
<li>gRPC는 HTTP/2 기반으로 통신하며 이 때문에 양방향 스트리밍이 가능하다.</li>
<li>gRPC는 HTTP/2 레이어 위에서 프로토콜 버퍼를 사용하여 ‘직렬화된 바이트 스트림’으로 통신하여 JSON 기반의 통신보다 더 가볍고 통신속도가 빠르다.</li>
<li>런타임 환경과 개발환경 구축이 쉽다.</li>
<li>다양한 언어를 기반으로 만들 수 있다.</li>
<li>유지보수가 쉽다.</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>rest api 와 다르게 메세지가 바이너리로 전달되기 때문에 테스트가 어렵다.</li>
<li>메세지 구조가 많이 변할 경우 안정성 확보가 어렵다.</li>
<li>개발 프로세스의 복잡도가 증가할 수 있다.</li>
<li>브라우저와 서버간의 gRPC 통신이 지원되지 않는다.<ul>
<li>브라우저에서 JSON으로 서버에 요청하면 서버는 grpc-gateway를 통해 protobuf 형식으로 데이터 변환 후 사용</li>
</ul>
</li>
<li>사람이 읽을 수 없는 데이터 형식이기 때문에 네트워크 단에서 데이터를 보고 싶으면 추가적인 작업이 필요하다.</li>
</ul>
<h2 id="프로토파일-작성법">프로토파일 작성법</h2>
<ol>
<li>Proto 파일  생성 [ 파일명.proto ] 만들기</li>
</ol>
<pre><code class="language-protobuf">// 프로토 버전 명시 [ 명시하지 않을 시 컴파일러는 2를 사용하는걸로 가정합니다. ]
syntax = &quot;proto3&quot;;

// 패키지 이름 작성하기
package tutorial;

// 서비스 이름을 작성 후 서비스 내에 존재하는 함수 선언 후 파라미터와 Return 타입 지정해주기
service TutorialService {
  rpc Accumulate (NumberArray) returns (SumOfNumberArray);
  rpc NameArrayReturn (NameArray) returns (NameArrayResponse);
}

// 파라미터가 필요없는 함수를 위한 빈 타입
message Empty {}

// 배열은 앞에 repeated 를 붙인 후 배열 안 타입을 지정해준다.
message NumberArray {
  repeated double data = 1;
}
message SumOfNumberArray {
  double sum = 1;
}

message NameArray {
  repeated string name = 1;
}

message NameArrayResponse {
  repeated string name = 1;
}</code></pre>
<ul>
<li>변수 선언은 아래 링크를 참고!
<a href="https://developers.google.com/protocol-buffers/docs/proto3">Language Guide (proto3)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Pre-Signed URL 이란?]]></title>
            <link>https://velog.io/@soshin_dev/Pre-Signed-URL-%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@soshin_dev/Pre-Signed-URL-%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Wed, 06 Jul 2022 12:31:43 GMT</pubDate>
            <description><![CDATA[<h2 id="사용하게-된-계기">사용하게 된 계기</h2>
<ul>
<li>이번에 프로젝트를 진행하면서 프론트에서 이미지를 보내오면 S3 버킷에 저장해주는 로직이 있었다.
이때 프론트에서 이미지를 보내주면, <code>Node</code>에서 <code>Multer</code> 라이브러리를 통해 이미지를 받아 그걸 다시 S3 버킷에 저장해주는 식으로 진행을 하였다.
하지만 이 과정이 뭔가 복잡스럽고 <code>Node</code>에서 이미지 처리를 담당한다는거 자체가 서버에무리가 많이 갈 것 같았다.
그래서 방법을 찾던 중 <code>Pre-Signed url</code> 이란걸 사용하면 보안적으로도 안전하게 프론트에서 바로 S3 버킷에 이미지를 저장시킬 수 있다는 걸 듣고 적용해보았다.</li>
</ul>
<h2 id="pre-sign-url에-대해">Pre-Sign URL에 대해</h2>
<ul>
<li><p>AWS 공식 문서에서는 아래와 같이 말해준다.</p>
<blockquote>
<p>미리 서명된 URL의 생성자가 해당 객체에 대한 액세스 권한을 보유할 경우, 미리 서명된 URL은 URL에서 식별된 객체에 대한 액세스를 부여합니다. 
즉, 객체를 업로드하기 위해 미리 서명된 URL을 수신하는 경우, 미리 서명된 URL의 생성자가 해당 객체를 업로드하는 데 필요한 권한을 보유하는 경우에만 객체를 업로드할 수 있습니다.</p>
</blockquote>
</li>
<li><p>즉 사용자에게 미리 서명된 URL 을 제공하여 이를 이용하여 <code>S3 버킷</code>에 접근 권한을 설정해줄 수 있다.</p>
</li>
<li><p>이때 미리 서명된 URL은 URL을 아는 모든 사람에게 <code>Amazon S3</code> 버킷에 대한 액세스 권한을 부여하므로 적절하게 접근 권한 및 만료시간을 설정해주어야 한다.</p>
</li>
</ul>
<h2 id="사용해보기--node-express-">사용해보기! [ Node, Express ]</h2>
<ul>
<li>Pre-Sign URL 만들기<pre><code class="language-js">import AWS from &quot;aws-sdk&quot;;
</code></pre>
</li>
</ul>
<p>AWS.config.update({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION,
  signatureVersion: &quot;v4&quot;,
});</p>
<p>const s3 = new AWS.S3();
const myBucket = process.env.BUCKET_NAME;
const signedUrlExpireSeconds = 60 * 5;</p>
<p>export default function createUrl(fileName) {
  const url = s3.getSignedUrl(&quot;putObject&quot;, {
    Bucket: myBucket,
    Key: fileName,
    Expires: signedUrlExpireSeconds,
    ContentType: &quot;image/*&quot;,
  });
  return url;
}</p>
<pre><code>위 코드는 ``Pre-Sign URL`` 을 생성해주는 코드이다.
위 ``AWS.config.update``는 AWS 설정에 사용자 인증을 받는 것 이고, 아래 ``AWS s3`` 부분은 그 사용자의 S3에 대해 접근한다는걸 알려준다.
이때 ``createUrl`` 함수안에 ``getSignedUrl``을 통해 ``Pre-Sign URL`` 만들어주고 이 안에 권한 설정 및 필요한 인자 값들을 넣어준다.
``putObject``는 이미지를 전송하는 권한
``Bucket``은 버킷의 이름, ``Key``는 파일의 이름 [ 유니크한 값 ], ``Expires`` 는 URL의 유효시간이다.

* Router 설정
```js
uploadRouter.get(&#39;/:file&#39;, (req, res, next) =&gt; {
  const { file } = req.params;
  const fileName = Date.now() + file;
  try {
    const url = createUrl(`diary/${fileName}`);
  } catch (error) {
    next(error);
  }
  const imageUrl = `https://ai-project-last.s3.ap-northeast-2.amazonaws.com/diary/${fileName}`;
  const body = {
    success: true,
    url,
    imageUrl,
  };
  res.status(201).json(body);
});</code></pre><p>path로 파일의 이름을 받은 후, 유니크한 값으로 만들기 위해 현재 시간을 더한 문자열로 만들어준다.
그 후 위에서 만든 <code>createUrl</code> 을 통해 만든 파일 이름을 보낸 후, 그것에 해당하는 <code>Pre-Sign URL</code>을 발급 받아 Front에 그 값을 전달해준다.</p>
<hr>
<h2 id="사용-후">사용 후</h2>
<p>그 전에 Multer와 Node를 사용해 이미지를 처리할 때 보다 훨씬 빠른 속도로 처리가 가능해졌다.</p>
<p>또한, 서버에 이미지가 오지 않기 때문에 서버에 대한 부담도 줄일 수 있었다.</p>
<p>백엔드 개발자는 기능 구현에만 신경쓰는 것이 아닌 서버에 대한 최적화, 보안 등을 모두 다 생각해야 한다고 배웠을 땐 <code>기능만 잘 구현하면 되는거 아닌가..?</code> 라고 생각했다.</p>
<p>하지만 위 방법을 통해 같은 기능이지만 완전히 다른 방향으로 구현하고, 사용하는 방법 또한 완전히 다르지만 그로인한 성능차이 등을 느끼며 정말 백엔드 개발자는 지식에 따라 엄청난 차이가 생기는구나 생각을 했다.</p>
<p>다음번 프로젝트 또한 기능을 구현할 때 더 좋은 방법, 서버에 대한 무리를 더 줄이는 방법 등등을 더 학습하고 구현해봐야겠다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Sentry로 Error Log 관리하기]]></title>
            <link>https://velog.io/@soshin_dev/Sentry%EB%A1%9C-Error-Log-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@soshin_dev/Sentry%EB%A1%9C-Error-Log-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 30 May 2022 14:41:40 GMT</pubDate>
            <description><![CDATA[<h2 id="sentry란">Sentry란?</h2>
<blockquote>
<p>Sentry는 어플리케이션에서 오류가 발생하면 알려주는 에러 트래킹 서비스로 Error가 발생하면 메일 또는 웹훅으로 연결한 어플리케이션으로 알림을 주고, Sentry 대쉬보드를 통해 에러 위치, 에러 로그 등을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/9184de36-c220-4274-82a0-85283107ac7f/image.png" alt=""></p>
</blockquote>
<hr>
<h2 id="sentry-를-사용하게-된-이유">Sentry 를 사용하게 된 이유</h2>
<blockquote>
<p>이전 프로젝트에서는 Morgan과 Winston으로 Log를 관리해줬는데 에러가 발생했을 때 알림이 오지 않는 점, 어떤 에러가 어디서 발생했는지 한 눈에 파악하기 힘든 점 등을 통해 더 편하고 보기 좋은 Error Logging 관리 방법이 있을까 하다가 Sentry를 보고 한눈에 반해서 사용해보았다.</p>
</blockquote>
<hr>
<h2 id="sentry-사용법--node-express-환경-">Sentry 사용법 [ Node, Express 환경 ]</h2>
<blockquote>
<p><a href="https://docs.sentry.io/platforms/node/guides/express/">Sentry 공식 문서</a>
위 <code>Sentry</code> 공식 문서에서 정말 친절하게 하는 방법을 정리해 두었다.</p>
</blockquote>
<pre><code class="language-js">import express from &quot;express&quot;;
import * as Sentry from &quot;@sentry/node&quot;;
const app = express();
Sentry.init({ dsn: &quot;yourDsn&quot; }); 
app.use(Sentry.Handlers.requestHandler());
app.get(&quot;/&quot;, function rootHandler(req, res) {
  res.end(&quot;Hello world!&quot;);
});
app.use(Sentry.Handlers.errorHandler());
app.use(function onError(err, req, res, next) {
  res.statusCode = 500;
  res.end(res.sentry + &quot;\n&quot;);
});
app.listen(3000);</code></pre>
<p>위 코드가 <code>Sentry</code> 공식문서에서 제공하는 코드이다.
신경써야 할 부분을 하나하나 살펴보면</p>
<pre><code class="language-js">Sentry.init({ dsn: &quot;yourDsn&quot; }); </code></pre>
<p><code>Sentry</code> 는 app 에서 가능한 한 빨리 <code>init</code> 되어야 하기 때문에 <code>app</code>을 선언해준 후 바로 다음에 <code>init</code>해준다.</p>
<pre><code class="language-js">app.use(Sentry.Handlers.requestHandler());</code></pre>
<p>그 후 <code>requestHandler</code>가 <code>app</code>의 첫번째 미들웨어가 되어야하기 때문에 <code>init</code> 다음에 선언해줍니다.</p>
<pre><code class="language-js">app.use(Sentry.Handlers.errorHandler());</code></pre>
<p>그 후 오류 핸들러는 다른 오류 미들웨어 이전과 모든 컨트롤러의 뒤 [마지막] 에 위치해야합니다.</p>
<hr>
<h2 id="sentry-적용-후">Sentry 적용 후</h2>
<blockquote>
<p><code>Sentry</code>를 적용한 후 일부러 <code>Error</code>를 일으켜 어떤식으로 나오는지에 대해 알아보겠습니다!</p>
</blockquote>
<pre><code class="language-js">basicRouter.get(&quot;/test&quot;, (req, res) =&gt; {
  res.send(test);
});</code></pre>
<p>위 코드처럼 존재하지 않는 변수를 참조하여 일부러 오류를 일으켜보겠습니다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/d013a649-a08e-4eed-906b-2cc214270cf6/image.png" alt="">
위  router로 요청을 보내면 위 사진 처럼 Sentry 에서 ReferenceError 라고 알려주고
아래 더 상세한 내용을 알려줍니다.
[ test 가 선언되지 않았다. ]
<img src="https://velog.velcdn.com/images/soshin_dev/post/cdc5a4db-2f73-4adb-8536-041fb03f76e8/image.png" alt="">
그 후 클릭하면 더 상세한 내용들과 코드의 어느 부분에서 에러가 발생한 것인지 위치 까지 알려주게 된다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/daf8e964-5bf8-4194-ae02-6abcfbb6164b/image.png" alt="">
또한 위 처럼 등록한 주소로 해당 에러에 대한 정보를 메일로 보내준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Project ] "되어봐요 숲잘알! 프로젝트 정리]]></title>
            <link>https://velog.io/@soshin_dev/Project-%EB%90%98%EC%96%B4%EB%B4%90%EC%9A%94-%EC%88%B2%EC%9E%98%EC%95%8C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@soshin_dev/Project-%EB%90%98%EC%96%B4%EB%B4%90%EC%9A%94-%EC%88%B2%EC%9E%98%EC%95%8C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 25 May 2022 03:46:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/soshin_dev/post/731a2d88-7526-4339-bb02-bb9edc5c4cb2/image.png" alt=""></p>
<blockquote>
<h3 id="👨🎓-프로젝트-진행-과정">👨‍🎓 프로젝트 진행 과정</h3>
<p>프로젝트는 시리즈에 정리한 것 처럼 매일 아침 스크럼을 통해 개발 일정을 조율해가고, 추가할 기능에 대해 토론을 하며 진행했다.
나는 Express와 MongoDB를 활용하여 백엔드 파트를 맡았다.
Gitlab을 통해 협업을 진행하였고, 자신의 개발 정도와 목표 등에 대해 이슈와 마일스톤으로 관리를 했었다.
또한 매주 현직자 분에게 코드리뷰를 신청하여 코드 상태와 사용한 라이브러리들을 확인한 후 리팩토링하였다.</p>
</blockquote>
<hr>
<h2 id="✨-구현-기능">✨ 구현 기능</h2>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/ceda49e2-c0f8-4564-8a8e-4f8455af4512/image.png" alt=""></p>
<h3 id="🎉-오늘의-주인공-기능">🎉 오늘의 주인공 기능</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/908f4dbb-a471-40a4-90f6-fcbb52726644/image.png" alt="">
<code>Front</code>에서 오늘 생일자에 대한 <code>Get</code> 요청이 오면 오늘 날짜에 생일인 주민을 반환해줍니다.
이때 생일인 주민을 개별적으로 클릭하여 한명 한명에게 생일 축하 메세지를 남길 수 있도록 했습니다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/8f00e57b-3218-404f-b400-be691d5045f4/image.png" alt="">
생일 축하 메세지는 <code>Commnet DB</code>에 주민 이름과 <code>location</code>을 입력받아 생일인 곳이면 <code>Birthday</code> 라는 <code>location</code> 값과 주민 이름을 포함하여 <code>Front</code>에서 <code>Post</code> 요청을 보내주면 <code>MongoDB</code>에 보관해두었다가 <code>Query</code>를 통해 주민의 이름과 location 값을 보내주면 해당하는 주민의 댓글 위치에 해당하는 댓글을 반환해주게 하여 구현하였습니다.</p>
</blockquote>
<h3 id="📙-주민-도감-기능">📙 주민 도감 기능</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/c1ca541d-d6fa-4e2b-9b05-a44e7277f488/image.png" alt="">
주민 도감 기능은 처음 들어왔을 땐 현재 존재하는 모든 주민을 보여주고, 여러 검색조건에 따라 필터링이 가능하게 만들었습니다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/d97e10fc-a15f-4e9b-b7e8-8f6432ca38fc/image.png" alt="">
각 조건을 누르고 검색을 하게 되면 <code>Query</code>를 통해 <code>Front</code>에서 반환받고 싶은 Field 값, 필터링 조건, 검색한 값을 넣어서 보내주게 되고, <code>Back</code>에서 위의 필터링을 처리하여 값을 반환해줍니다.</p>
</blockquote>
<h3 id="📱-주민-통계-기능">📱 주민 통계 기능</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/00d21ffe-bd54-485e-bc56-e1b5d353888f/image.png" alt="">
각 주민의 데이터를 <code>Pandas</code>를 통해 데이터를 분석하여 통계를 낸 후, 그 데이터를 사용하여 <code>Front</code>에서 <code>recharts</code> 라이브러리를 통해 시각화를 해주었습니다.</p>
</blockquote>
<h3 id="🏹-주민-매칭-기능">🏹 주민 매칭 기능</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/5d2c9e7a-0a1f-4dc8-bdaf-5892c6e9a47d/image.png" alt="">
주민 매칭 기능은 위 같은 질문을 5개를 만들어두어, 각 질문에 대한 대답을 유저가 선택하면 데이터분석을 통해 주민들과의 연관성을 비교하여 가장 비슷한 값을 가진 주민과 가장 비슷하지 않은 주민을 반환해주어 나와 잘 맞는 주민, 잘 맞지 않는 주민에 대해 값을 반환하도록 만들었다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/0ba05378-73f3-445c-a6d3-1071f9ce0a85/image.png" alt="">
<img src="https://velog.velcdn.com/images/soshin_dev/post/2b3a9118-b832-4be7-bd62-e06493d09e58/image.png" alt="">
<img src="https://velog.velcdn.com/images/soshin_dev/post/7dab50eb-4690-4a81-80e0-8a87e822dc00/image.png" alt="">
이때 가장 많은 유형은 주민 매칭에 참여해주신 유저분들의 총 수와 선택받은 주민들의 수를 가지고 MongoDB Aggregation을 통해 [여러 document들을 grouping하여 연산을 수행한 후 하나의 result를 반환하는 연산] 구현하였습니다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/85163e2c-790e-4336-8045-1251bbba73cb/image.png" alt="">
또한 매칭된 각 주민에 대해서도 메세지를 남길 수 있도록 만들었습니다.
<code>Comment</code>부분은 위에 생일 주민 댓글 부분과 동일하게 작동합니다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/1853f8e3-10e9-40be-9081-efdc39f4e49f/image.png" alt="">
또한 공유하기 기능을 통해 링크를 복사할 수 있고, 복사한 링크로 들어가게 되면 해당하는 유저가 나온 주민, 그 주민과 잘 어울리는 주민에 대한 정보를 알려주게 됩니다.</p>
</blockquote>
<h3 id="🎮-미니게임-기능">🎮 미니게임 기능</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/ae29064b-c522-44ee-91db-af07a2ee1df9/image.png" alt="">
미니 게임 같은 경우엔 카드 뒤집기 게임을 구현하였습니다.
주민의 이름과 사진이 랜덤으로 뒤집혀 있으며, 주민의 이미지와 맞는 이름을 선택하면 뒤집히는 게임입니다.
총 5개의 라운드로 구성되어 있으며 한 라운드당 1분의 제한시간을 두었습니다.
이때 라운드를 클리어한 시간과 클리어한 라운드 수를 통해 점수를 계산하게 됩니다.
라운드가 높아질수록 동물의 숲 마을 주민 랭킹이 낮은 [ 유명하지 않은 ] 주민이 나오도록 셋팅을 해두었습니다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/6348fa64-8542-4afd-a595-e1e3f08f6652/image.png" alt="">
<img src="https://velog.velcdn.com/images/soshin_dev/post/3dfde4a4-6f06-4970-a5bb-861328cb866a/image.png" alt="">
<img src="https://velog.velcdn.com/images/soshin_dev/post/c3d711dc-337a-4414-86fd-f370ad9a5868/image.png" alt=""></p>
</blockquote>
<h3 id="📑방명록과-게시판">📑방명록과 게시판!</h3>
<blockquote>
<p><a href="https://velog.velcdn.com/images/soshin_dev/post/274d736d-6baf-45c9-b939-180b74b7398e/image.png"></a>
<img src="https://velog.velcdn.com/images/soshin_dev/post/1e6a9199-c48f-4cc6-a1f8-8a3d6227eee2/image.png" alt="">
방명록은 위 처럼 닉네임을 입력받지 않고 익명으로 글을 남길 수 있는 기능으로, 글을 남기면 동물의 숲 아이콘으로 보이도록 구현하였습니다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/96068b90-c260-42d3-afb5-5f6a59734eb5/image.png" alt="">
게시판 기능은 위 처럼 제목, 닉네임, 글을 작성하고 아래 업로드 버튼을 통해 사진을 업로드 할 수 있도록 만들었습니다.
사진은 <code>Multer</code> 라이브러리를 통해 미들웨어로 구현하였으며 <code>limits</code>를 통해 파일사이즈를 <code>10mb</code>로 제한을 두었습니다.
<img src="https://velog.velcdn.com/images/soshin_dev/post/89fa62dd-f37c-4a5c-a787-6489e7e8f411/image.png" alt="">
<img src="https://velog.velcdn.com/images/soshin_dev/post/c9b6c15a-22c1-4bc0-8aab-6e564dc6e913/image.png" alt=""></p>
</blockquote>
<hr>
<h2 id="코드-확인하러-가기">코드 확인하러 가기!</h2>
<p><a href="https://github.com/Shin-GC/AnimalCrossingSite.git">Github 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node.js] Map 사용법]]></title>
            <link>https://velog.io/@soshin_dev/Node.js-Map-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@soshin_dev/Node.js-Map-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Wed, 18 May 2022 03:32:17 GMT</pubDate>
            <description><![CDATA[<h3 id="맵과-obejct의-각각-특징">맵과 Obejct의 각각 특징</h3>
<h4 id="object-특징">object 특징</h4>
<ul>
<li>object는 key값으로 String만 사용가능하다.</li>
<li>object는 순서를 보장하지 않는다.</li>
<li>수동으로 크기를 구해야한다.
ex) <code>Object.keys(obj).length</code></li>
<li>object는 프로토타입을 가지기 때문에 유의하여 사용해야한다.</li>
</ul>
<h4 id="map-특징">Map 특징</h4>
<ul>
<li>Map객체는 ECMAScript 6에서 값들을 매핑하기 위해 나온 새로운 데이터 구조이다.</li>
<li>Map은 키값으로 모든 값을 가질 수 있다.</li>
<li>Map은 삽입된 순서대로 반복된다 =&gt; 안정성 보장</li>
<li>size 속성을 통하여 쉽게 크기를 구할 수 있다.</li>
</ul>
<hr>
<h3 id="object와-map-사용-시점-판단">Object와 Map 사용 시점 판단</h3>
<p>MDN 문서에서는 다음과 같이 설명한다.</p>
<p>실행 시까지 키를 알수 없고, 모든 키가 동일한 type이며 모든 값들이 동일한 type일 경우에는 objects를 대신해서 map을 사용해라.
각 개별 요소에 대해 적용해야 하는 로직이 있을 경우에는 objects를 사용해라.</p>
<h4 id="object-사용-시점">Object 사용 시점</h4>
<ul>
<li>각 요소에 대해서 별도의 로직이 필요한 경우</li>
</ul>
<h4 id="map-사용-시점">Map 사용 시점</h4>
<ul>
<li>key와 value가 각각 같은 타입인 경우</li>
<li>런타임까지 key가 정해지지 않은 경우</li>
<li>데이터의 크기가 큰경우 (일반적으로 더 좋은 성능을 보인다고 한다)</li>
<li>안정성을 유지해야 할때 (순서의 보장)</li>
</ul>
<hr>
<h3 id="맵">맵</h3>
<p><code>맵(Map)</code>은 키가 있는 데이터를 저장한다는 점에서 객체와 유사하지만 맵은 키에 다양한 자료형을 허용한다는 점에서 차이가 있습니다.</p>
<p>맵에는 다음과 같은 주요 메서드와 프로퍼티가 있습니다.</p>
<ul>
<li><code>new Map()</code> – 맵을 만듭니다.</li>
<li><code>map.set(key, value)</code> – key를 이용해 value를 저장합니다.</li>
<li><code>map.get(key)</code> – key에 해당하는 값을 반환합니다. key가 존재하지 않으면 <code>undefined</code>를 반환합니다.</li>
<li><code>map.has(key)</code> – key가 존재하면 true, 존재하지 않으면 false를 반환합니다.</li>
<li><code>map.delete(key)</code> – key에 해당하는 값을 삭제합니다.</li>
<li><code>map.clear()</code> – 맵 안의 모든 요소를 제거합니다.</li>
<li><code>map.size</code> – 요소의 개수를 반환합니다.</li>
</ul>
<pre><code class="language-js">let map = new Map();

map.set(&quot;1&quot;, &quot;1 String&quot;); // 문자형 키
map.set(1, &quot;1 Number&quot;); // 숫자형 키
map.set(true, &quot;1 Boolean&quot;); // 불린형 키

// 객체는 키를 문자형으로 변환하지만
// 맵은 키의 타입을 변환시키지 않고 그대로 유지합니다. 따라서 아래의 코드는 출력되는 값이 다릅니다.
console.log(map.get(1)); // &#39;1 Number&#39;
console.log(map.get(&quot;1&quot;)); // &#39;1 String&#39;
console.log(map.get(true)); // &#39;1 Boolean&#39;
console.log(map.size); // 3

/*출력
1 Number
1 String
1 Boolean
3
*/</code></pre>
<p>맵은 객체와 달리 키를 문자형으로 변환하지 않고, 키엔 자료형 제약이 없습니다.</p>
<blockquote>
<h4 id="🧨주의-사항">🧨주의 사항</h4>
<p><code>map[key]</code>는 <code>Map</code>을 쓰는 바른 방법이 아닙니다.
<code>map[key] = 2</code>로 값을 설정하는 것 같이 <code>map[key]</code>를 사용할 수 있긴 합니다. 
하지만 이 방법은 <code>map</code>을 일반 객체처럼 취급하게 됩니다. 
따라서 여러 제약이 생기기 때문에 map을 사용할 땐 map전용 메서드 <code>set</code>, <code>get</code> 등을 사용해야만 합니다.</p>
</blockquote>
<hr>
<h3 id="map-set-체이닝">Map set 체이닝</h3>
<p><code>map.set</code>을 호출할 때마다 맵 자신이 반환됩니다. 
이를 이용하여 <code>map.set</code>을 &#39;체이닝(chaining)&#39;할 수 있습니다.</p>
<pre><code class="language-js">map.set(&quot;1&quot;, &quot;1 String&quot;).set(1, &quot;1 Number&quot;).set(true, &quot;1 Boolean&quot;);

console.log(map.get(1)); // &#39;1 Number&#39;
console.log(map.get(&quot;1&quot;)); // &#39;1 String&#39;
console.log(map.get(true)); // &#39;1 Boolean&#39;
console.log(map.size); // 3

/*출력
1 Number
1 String
1 Boolean
3
*/</code></pre>
<hr>
<h3 id="map-메서드-사용">Map 메서드 사용</h3>
<pre><code class="language-js">let map = new Map();
map.set(&quot;1&quot;, &quot;1 String&quot;).set(1, &quot;1 Number&quot;).set(true, &quot;1 Boolean&quot;);

console.log(map.size); // 3
console.log(map.has(1)); // true

console.log(map); // Map(3) { &#39;1&#39; =&gt; &#39;1 String&#39;, 1 =&gt; &#39;1 Number&#39;, true =&gt; &#39;1 Boolean&#39; }

console.log(map.delete(1)); // true
console.log(map.has(1)); // false
console.log(map); // Map(2) { &#39;1&#39; =&gt; &#39;1 String&#39;, true =&gt; &#39;1 Boolean&#39; }

map.clear();
console.log(map); // Map(0) {}

/*
출력
3
true
Map(3) { &#39;1&#39; =&gt; &#39;1 String&#39;, 1 =&gt; &#39;1 Number&#39;, true =&gt; &#39;1 Boolean&#39; }
true
false
Map(2) { &#39;1&#39; =&gt; &#39;1 String&#39;, true =&gt; &#39;1 Boolean&#39; }
Map(0) {}
*/</code></pre>
<h3 id="맵의-요소에-반복-작업하기">맵의 요소에 반복 작업하기</h3>
<ul>
<li><code>map.keys()</code> – 각 요소의 키를 모은 반복 가능한(iterable, 이터러블) 객체를 반환합니다.</li>
<li><code>map.values()</code> – 각 요소의 값을 모은 이터러블 객체를 반환합니다.</li>
<li><code>map.entries()</code> – 요소의 [키, 값]을 한 쌍으로 하는 이터러블 객체를 반환합니다. 이 이터러블 객체는 <code>for..of</code>반복문의 기초로 쓰입니다.</li>
</ul>
<pre><code class="language-js">let map = new Map();
map.set(&quot;1&quot;, &quot;1 String&quot;).set(1, &quot;1 Number&quot;).set(true, &quot;1 Boolean&quot;);

console.log(map.keys());
console.log(map.values());
console.log(map.entries());

for (let i of map.keys()) {
  console.log(i);
}

for (let i of map.values()) {
  console.log(i);
}

for (let i of map.entries()) {
  console.log(i);
}

/* 출력
[Map Iterator] { &#39;1&#39;, 1, true }
[Map Iterator] { &#39;1 String&#39;, &#39;1 Number&#39;, &#39;1 Boolean&#39; }
[Map Entries] {
  [ &#39;1&#39;, &#39;1 String&#39; ],
  [ 1, &#39;1 Number&#39; ],
  [ true, &#39;1 Boolean&#39; ]
}
1
1
true
1 String
1 Number
1 Boolean
[ &#39;1&#39;, &#39;1 String&#39; ]
[ 1, &#39;1 Number&#39; ]
[ true, &#39;1 Boolean&#39; ]
*/</code></pre>
<pre><code class="language-js">let map = new Map();
map.set(&quot;1&quot;, &quot;1 String&quot;).set(1, &quot;1 Number&quot;).set(true, &quot;1 Boolean&quot;);

map.forEach((item, key) =&gt; console.log(`${key} : ${item}`));

/*
출력
1 : 1 String
1 : 1 Number
true : 1 Boolean
*/</code></pre>
<hr>
<h3 id="객체-object-를--map으로-바꾸기">객체[ Object ]를  Map으로 바꾸기</h3>
<p>평범한 객체를 가지고 맵을 만들고 싶다면 내장 메서드 <code>Object.entries(obj)</code>를 활용해서 객체의 키-값 쌍을 요소<code>([key, value])</code>로 가지는 배열로 만들어줍니다.</p>
<pre><code class="language-js">let obj = {
  name: &quot;John&quot;,
  age: 30,
};

let map = new Map(Object.entries(obj));

console.log(map);
console.log(map.get(&quot;name&quot;));

/*
출력
Map(2) { &#39;name&#39; =&gt; &#39;John&#39;, &#39;age&#39; =&gt; 30 }
John
*/</code></pre>
<hr>
<h3 id="objectfromentries-맵을-객체로-바꾸기">Object.fromEntries: 맵을 객체로 바꾸기</h3>
<pre><code class="language-js">let map = new Map();
map.set(&quot;name&quot;, &quot;Shin&quot;).set(&quot;age&quot;, &quot;26&quot;).set(true, &quot;1 Boolean&quot;);

//let obj = Object.fromEntries(map.entries()); // 아래와 같은 동작
let obj = Object.fromEntries(map); 
console.log(obj);

/*
출력
{ name: &#39;Shin&#39;, age: &#39;26&#39;, true: &#39;1 Boolean&#39; }
*/</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 타입 스크립트를 사용하게 된 이유와 기본 타입 정리]]></title>
            <link>https://velog.io/@soshin_dev/TypeScript-%ED%83%80%EC%9E%85-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@soshin_dev/TypeScript-%ED%83%80%EC%9E%85-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 30 Apr 2022 08:48:40 GMT</pubDate>
            <description><![CDATA[<h3 id="👨🎓-타입-스크립트를-공부하게-된-이유">👨‍🎓 타입 스크립트를 공부하게 된 이유</h3>
<blockquote>
<p>: <code>express</code> 를 사용하여 프로젝트를 진행하며 <code>Request</code>로 들어오는 값들의 유효성 검사를 만들고, 에러 처리 하고, <code>JSDoc</code>을 통해 어떤 인자에 어떤 <code>Type</code>이 들어가야 하는지에 대해 적다보니 애초에 타입이 정해져 있는 정적 타입 언어면 이런걸 안해도 괜찮았을텐데.. 라는 생각을 하다가 <code>TypeScript</code>가 떠올라 바로 공부를 시작했다!</p>
</blockquote>
<hr>
<h3 id="📝타입스크립트란-">📝타입스크립트란 ?</h3>
<blockquote>
<ul>
<li>타입스크립트는 MS에서 개발하고 관리하는 오픈소스 프로그래밍 언어로 어떤 브라우저나 호스트, 운영체제에서도 동작한다. </li>
</ul>
</blockquote>
<ul>
<li>타입스크립트는 자바스크립트의 상위 집합으로서 ECMA의 최신 표준을 충분히 지원한다. </li>
<li>타입스크립트는 ES5를 포함하는 집합이기 때문에 기존의 ES5 자바스크립트 문법을 그대로 사용할 수 있다. </li>
<li>ES6의 새로운 기능들을 사용하기 위해 Babel과 같은 별도 트랜스파일러를 사용하지 않아도 ES6의 새로운 기능을 기존의 자바스크립트 엔진에서 사용할 수 있다.</li>
</ul>
<hr>
<h3 id="typescript-장단점">TypeScript 장단점</h3>
<p><code>TypeScript</code>의 장점</p>
<ul>
<li>정적 타입 언어(<code>static type language</code>)이기 때문에 컴파일 시 시간이 조금 걸리더라도 안정성을 보장한다는 점이다.</li>
<li><code>IDE(통합개발환경)</code>를 포함한 다양한 도구의 지원을 받을 수 있다는 것이다. <code>IDE</code>와 같은 도구에 타입 정보를 제공함으로써 높은 수준의 인텔리센스(<code>IntelliSense</code>), 코드 어시스트, 타입 체크, 리팩토링 등을 지원받을 수 있다. </li>
<li>인터페이스, 제네릭 등과 같은 강력한 객체지향 프로그래밍 지원은 크고 복잡한 프로젝트의 코드 기반을 쉽게 구성할 수 있도록 도와준다.</li>
</ul>
<p><code>TypeScript</code>의 단점</p>
<ul>
<li>타입을 지정해주고, 미리 만들어놓기 때문에 코드가 길어진다.
[ 아직까지 단점을 못 찾았습니다! 찾을 때 마다 최신화 예정! ]</li>
</ul>
<hr>
<h3 id="타입-지정해보기">타입 지정해보기</h3>
<p> : 타입스크립트의 기본 타입 [ 12 종류 ]</p>
<ul>
<li>Boolean</li>
<li>Number</li>
<li>String</li>
<li>Object</li>
<li>Array</li>
<li>Tuple</li>
<li>Enum</li>
<li>Any</li>
<li>Void</li>
<li>Null</li>
<li>Undefined</li>
<li>Never<h3 id="string">String</h3>
</li>
</ul>
<hr>
<pre><code class="language-js">let str: string = &quot;문자열 입니다.&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/e45076a4-9206-4fec-ae17-05f3ea14198f/image.png" alt=""></p>
<p>만약 위 처럼 타입을 지정 해준 후, 지정해 준 것과 다른 타입을 사용하면 위와 같이 <code>TypeScript</code>에서 바로 문제점을 알려준다.</p>
<blockquote>
<p>📌 이때 위와 같이 <code>:</code>를 이용하여 자바스크립트 코드에 타입을 정의하는 방식을 타입 표기(<code>Type Annotation</code>)라고 합니다. </p>
</blockquote>
<hr>
<h3 id="number">Number</h3>
<pre><code class="language-js">let num: number = 5</code></pre>
<hr>
<h3 id="boolean">Boolean</h3>
<pre><code class="language-js">let bool: boolean = false</code></pre>
<hr>
<h3 id="object">Object</h3>
<pre><code class="language-js">let obj: object = {
  name: &quot;Shin&quot;,
};</code></pre>
<hr>
<h3 id="array">Array</h3>
<pre><code class="language-js">let numArr: number[] = [1, 2, 3, 4, 5];
let strArr: string[] = [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;];
let boolArr: boolean[] = [true, false, true, false]</code></pre>
<hr>
<h3 id="tuple">Tuple</h3>
<p>: 튜플은 배열의 길이가 고정되고 각 요소의 타입이 지정되어 있는 배열 형식을 의미한다.</p>
<pre><code class="language-js">let tuple: [string, number] = [&quot;test&quot;, 123]</code></pre>
<hr>
<h3 id="enum">Enum</h3>
<p>: 특정 값(상수)들의 집합을 의미한다.</p>
<pre><code class="language-js">enum Avengers {
  Capt = &quot;캡틴&quot;,
  IronMan = &quot;아이언맨&quot;,
  Thor = &quot;토르&quot;,
}
let hero: Avengers = Avengers.Capt;
  // 캡틴</code></pre>
<hr>
<h3 id="any">Any</h3>
<p>: <code>Any</code>는 모든 타입을 허용한다는 뜻으로 쉽게 타입 스크립트의 타입 방어를 해제하고 기존 자바스크립트 처럼 사용한다는 뜻이다.
<code>Any</code>를 사용하게 되면 타입 스크립트를 사용하는 의미가 사라지므로 되도록 사용하지 않도록 하자.</p>
<pre><code class="language-js">let all: any = 2
all = &quot;테스트&quot;
all = true</code></pre>
<hr>
<h3 id="void">Void</h3>
<p>: <code>Void</code> 타입은 <code>undefined</code> 값을 가진 변수를 선언하거나 반환 값을 가지지 않는 함수에 사용하는 타입 이다.</p>
<pre><code class="language-js">let unuseful: void = undefined;

function notuse(): void {
  console.log(&#39;void 타입은 반환 값이 없습니다.&#39;);
}</code></pre>
<h3 id="never">Never</h3>
<p>: <code>Never</code> 타입은 종료하지 않는 함수 즉 함수의 끝에 도달할 수 없는 함수에 사용하는 타입 이다.
항상 오류를 출력하거나 리턴 값을 절대로 내보내지 않음을 의미한다.</p>
<pre><code class="language-js">function neverEnd(): never {
  while (true) {
  }
}
</code></pre>
<p><a href="https://joshua1988.github.io/ts/guide/basic-types.html">참고 사이트</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Node ] Express 에서 Morgan과 Winston 으로 Logging 관리하기]]></title>
            <link>https://velog.io/@soshin_dev/Node-Express-%EC%97%90%EC%84%9C-Morgan%EA%B3%BC-Winston-%EC%9C%BC%EB%A1%9C-Logging-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@soshin_dev/Node-Express-%EC%97%90%EC%84%9C-Morgan%EA%B3%BC-Winston-%EC%9C%BC%EB%A1%9C-Logging-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 25 Apr 2022 18:55:23 GMT</pubDate>
            <description><![CDATA[<h3 id="로그-정리에-사용하는-모듈은-총-3가지-입니다">로그 정리에 사용하는 모듈은 총 3가지 입니다.</h3>
<ul>
<li><code>winston</code> : 로그 파일 및 로그 레벨 관리 모듈</li>
<li><code>winston-dayily-rotate-file</code> : 매일 날짜 별로 로그 파일 생성 및 관리 모듈 ( 시간이 지나면 자동으로 삭제 &amp; 압축 관리 )</li>
<li><code>morgan</code> : <code>request</code> 요청 로깅 미들웨어</li>
</ul>
<blockquote>
<p><code>winston</code>으로 로그 레벨, 타임라인을 관리하고 <code>morgan</code>으로 요청/응답 정보들을 로깅을 할 수 있게 해서 <code>Express</code> 로그를 관리하고 위에서 로깅한 정보들을 날짜 별로 로그 파일을 생성해주고 일정 시간이 지나면 삭제 및 압축을 해주는 기능을 사용하기 위해 <code>winston-dayily-rotate-file</code> 을 사용했습니다!</p>
</blockquote>
<hr>
<h3 id="morgan-사용법">Morgan 사용법</h3>
<p><a href="https://www.npmjs.com/package/morgan">모건 주소</a></p>
<p>: <code>Morgan</code> 사용법은 정말 간단합니다 기본적으로 사용할 때는</p>
<pre><code class="language-js">margan(&quot;출력타입&quot;);</code></pre>
<p>위 처럼 작성하면 끝 입니다! 이때 출력 타입들의 종류는 커스터마이징 하거나 기본적으로 제공해주는 <code>Format</code> 중에 선택하면 됩니다.</p>
<p>여러가지 <code>Format</code> 중 주로 사용하는 부분만 정리하면 아래와 같습니다.</p>
<ul>
<li><code>:http-version</code> : 요청한 <code>HTTP</code> 버전</li>
<li><code>:method</code> : 요청한 <code>HTTP method</code></li>
<li><code>:status</code> : 응답의 상태 코드 입니다.<blockquote>
<h3 id="📌-status-주의-사항">📌 <code>status</code> 주의 사항</h3>
<p>클라이언트에 응답을 보내기 전에 요청/응답 주기가 완료되면(예: 클라이언트가 요청을 중단하여 TCP 소켓이 조기에 닫히는 상황) 상태가 비어 있게 됩니다.<br>( <code>&quot;-&quot;</code>가 로그에 표시됨).</p>
</blockquote>
</li>
<li><code>:url</code> : 요청한 <code>URL</code></li>
<li><code>:response-time</code> : <code>morgan</code>으로 들어오는 요청과 응답 헤더가 작성된 시간 사이의 시간(응답시간)</li>
<li><code>:res[content-length]</code> : 응답 길이</li>
</ul>
<p>저는 아래와 같은 형식을 주로 사용합니다.
<code>:method :status :url :response-time ms</code></p>
<p>코드에서는</p>
<pre><code class="language-js">// app.js 파일
app.use(
  morgan(&quot;:method :status :url :response-time ms&quot;, { stream: logger.stream })
);</code></pre>
<p>위처럼 <code>morgan</code>을 통해 <code>dev format</code>으로 생성 된 <code>log</code>를 <code>winston</code>에 보내주는 코드 입니다.</p>
<hr>
<h3 id="winston-사용법">Winston 사용법</h3>
<p><a href="https://www.npmjs.com/package/winston#streams-objectmode-and-info-objects">윈스턴 주소</a></p>
<p><code>winston</code>은 <code>createLogger</code>를 통해 자신만의 고유한 <code>logger</code>를 만들 수 있습니다.
<code>winston</code>은 기본 로깅 수준 설정은 <code>RFC5424</code>에서 지정한 심각도 순서를 따릅니다.</p>
<pre><code class="language-js">const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  verbose: 4,
  debug: 5,
  silly: 6,
};</code></pre>
<p>즉 따로 설정하지 않으면 위의 수준을 사용하는건데 따로 설정할 필요가 없어 저대로 사용하였습니다.</p>
<h4 id="로그-format-설정">로그 format 설정</h4>
<pre><code class="language-js">const logFormat = printf((info) =&gt; {
  return `${info.timestamp} ${info.level}: ${info.message}`;
});

const logger = winston.createLogger({
  format: combine(
    timestamp({
      format: &quot;YYYY-MM-DD HH:mm:ss&quot;,
    }),
    logFormat
  ),

logger.stream = {
  // morgan wiston 설정
  write: (message) =&gt; {
    logger.info(message);
  },
};</code></pre>
<p><code>winston createLogger</code>의 설정은 위와 같게 되어있습니다.<br>하나씩 풀면 <code>logFormat</code>을 통해 로그 형식을 맞춘 후,</p>
<p><code>createLogger</code>의 <code>format: combine</code>을 통해 여러 형식을 하나의 <code>format</code>으로 합쳐줍니다.<br>여기서 <code>timestamp</code> 즉 수신한 시간과 <code>logFormat</code>으로 만든 사용자 지정 로그 <code>format</code>을 <code>combine</code>으로 하나의 <code>format</code>으로 합쳐줍니다.</p>
<p>이때 <code>logger.stream</code>을 통해 <code>logger</code>의 <code>info</code>에 받은 <code>message</code>값을 넣어주게 됩니다.</p>
<p>그 후, <code>app.js</code>파일의 <code>{ stream: logger.stream }</code> 을 통해 <code>morgan</code>을 통해 나온 로그를 넣어줍니다.</p>
<pre><code class="language-js">app.use(
  morgan(&quot;:method :status :url :response-time ms&quot;, { stream: logger.stream })
);</code></pre>
<hr>
<h3 id="winston-dayily-rotate-file-설명">winston-dayily-rotate-file 설명</h3>
<p><img src="https://www.npmjs.com/package/winston-daily-rotate-file" alt="winston-dayily-rotate-file 주소"></p>
<p><code>winston-dayily-rotate-file</code>은 코드 중 <code>transport</code>부분을 담당하고 있습니다.<br><code>new winstonDaily</code> 를 통해 하나의 로그 파일의 포맷과 폴더 형식들을 정해줄 수 있다.</p>
<pre><code class="language-js">const logDir = &quot;logs&quot;;

 new winstonDaily({
      level: &quot;info&quot;,
      datePattern: &quot;YYYY-MM-DD&quot;,
      dirname: logDir,
      filename: `%DATE%.log`, // file 이름 날짜로 저장
      maxFiles: 30, // 30일치 로그 파일 저장
      zippedArchive: true,
    }),</code></pre>
<p>위 코드의 값들에 대해 하나씩 알아보자면</p>
<ul>
<li><code>level</code> : 저장될 로그의 레벨</li>
<li><code>datePattern</code> : 날짜형식</li>
<li><code>dirname</code>: 로그가 저장 될 폴더의 이름</li>
<li><code>filename</code>: 로그 파일 이름 형식</li>
<li><code>maxFiles</code> : 로그 파일이 저장되는 기간</li>
<li><code>zippedArchive</code> : 압축여부</li>
</ul>
<p>입니다.</p>
<hr>
<h3 id="winston-로그-사용법">Winston 로그 사용법</h3>
<p>오류가 나타나야 하는 부분에 직접 로그를 작성하는 방법은 <code>logger</code>를 <code>import</code> 해온 후 오류를 작성할 부분에</p>
<pre><code class="language-js">import { logger } from &quot;../utils/winstonLogger.js&quot;;

logger.info(&quot;정보를 나타내는 로그&quot;)
logger.warn(&quot;위험성을 나타내는 로그&quot;)
logger.error(&quot;에러를 나타내는 로그&quot;)</code></pre>
<p>위 처럼 3단계로 나눠져있는 부분을 필요에 맞게 작성하시면 됩니다.
<code>logger.warn</code> 과 <code>logger.error</code>로 작성된 것은 <code>error</code>폴더와 <code>warn</code> 폴더에 따로 저장이 됩니다.  </p>
<p>이때 <code>error</code>는 <code>warn</code>에도 같이 저장되고 <code>info</code>에도 저장되며
<code>warn</code>은 <code>info</code>에도 같이 저장이 되고
<code>info</code>는 혼자만 저장 됩니다.</p>
<hr>
<h3 id="production-환경-일때의-콘솔창-로그">production 환경 일때의 콘솔창 로그</h3>
<pre><code class="language-js">// Production 환경이 아닌 경우(dev 등) 배포 환경에서는 
// 최대한 자원을 안잡아 먹는 로그를 출력해야함
if (process.env.NODE_ENV !== &quot;production&quot;) {
  logger.add(
    new winston.transports.Console({
      format: combine(
        colorize({ all: true }), // console 에 출력할 로그 컬러 설정 적용함
        logFormat // log format 적용
      ),
    })
  );
}</code></pre>
<p>위 코드는 배포 환경에서는 console에 로그를 출력하지 않고, 그 외의 환경에서만 console에 로그를 출력하도록 설정한 것 입니다!</p>
<hr>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-js">import winston from &quot;winston&quot;;
import winstonDaily from &quot;winston-daily-rotate-file&quot;;
const { combine, timestamp, printf, colorize } = winston.format;

const logDir = &quot;logs&quot;; // logs 디렉토리 하위에 로그 파일 저장

const logFormat = printf((info) =&gt; {
  return `${info.timestamp} ${info.level}: ${info.message}`;
});
/*
 * Log Level
 * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
 */
const logger = winston.createLogger({
  format: combine(
    timestamp({
      format: &quot;YYYY-MM-DD HH:mm:ss&quot;,
    }),
    logFormat
  ),
  transports: [
    // info 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: &quot;info&quot;,
      datePattern: &quot;YYYY-MM-DD&quot;,
      dirname: logDir,
      filename: `%DATE%.log`, // file 이름 날짜로 저장
      maxFiles: 30, // 30일치 로그 파일 저장
      zippedArchive: true,
    }),
    // warn 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: &quot;warn&quot;,
      datePattern: &quot;YYYY-MM-DD&quot;,
      dirname: logDir + &quot;/warn&quot;,
      filename: `%DATE%.warn.log`, // file 이름 날짜로 저장
      maxFiles: 30, // 30일치 로그 파일 저장
      zippedArchive: true,
    }),
    // error 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: &quot;error&quot;,
      datePattern: &quot;YYYY-MM-DD&quot;,
      dirname: logDir + &quot;/error&quot;, // error.log 파일은 /logs/error 하위에 저장
      filename: `%DATE%.error.log`,
      maxFiles: 30,
      zippedArchive: true,
    }),
  ],
});

logger.stream = {
  // morgan wiston 설정
  write: (message) =&gt; {
    logger.info(message);
  },
};

// Production 환경이 아닌 경우(dev 등) 배포 환경에서는 
// 최대한 자원을 안잡아 먹는 로그를 출력해야함
if (process.env.NODE_ENV !== &quot;production&quot;) {
  logger.add(
    new winston.transports.Console({
      format: combine(
        colorize({ all: true }), // console 에 출력할 로그 컬러 설정 적용함
        logFormat // log format 적용
      ),
    })
  );
}

export { logger };
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] Docker , Dockerfile Tutorial]]></title>
            <link>https://velog.io/@soshin_dev/Docker-Docker%EB%A1%9C-express-%EC%8B%A4%ED%96%89%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@soshin_dev/Docker-Docker%EB%A1%9C-express-%EC%8B%A4%ED%96%89%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 12 Apr 2022 08:11:44 GMT</pubDate>
            <description><![CDATA[<h3 id="🎇docker를-이용하여-express를-background-에서-실행해보기">🎇Docker를 이용하여 express를 background 에서 실행해보기</h3>
<blockquote>
<p>목표 : 기존 터미널에서 실행하던 <code>express</code> 서버를 <code>docker image</code>를 만들어 컨테이너를 생성 한 후 <code>background</code>에서 실행시키기!</p>
</blockquote>
<h4 id="1-🚲-docker-image-만들기">1. 🚲 Docker image 만들기</h4>
<blockquote>
<p><code>docker image</code>를 만들기 위한 <code>Dockerfile</code> 을 프로젝트의 <code>root</code> 폴더에 생성해줍니다.
<code>Dockerfile</code> 포멧
하나의 <code>Dockerfile</code>은 기본적으로 다음과 같은 구조를 가진 여러 개의 명령문으로 구성되어 있습니다.</p>
<blockquote>
</blockquote>
</blockquote>
<pre><code class="language-shell">#주석(Comment)
명령어(INSTRUCTION) 인자(arguments)</code></pre>
<p>각 명령문은 명령어로 시작하고 여러 개의 인자가 따라올 수 있으며, 해당 명령문에 대한 주석도 달 수 있습니다. 
인자와 구분이 쉽도록 명령어는 모두 영문 대문자로 써주는 것이 관례입니다.</p>
<blockquote>
<pre><code class="language-js">FROM node:16
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --silent
COPY . .
ARG port
EXPOSE ${port}
ENTRYPOINT [&quot;node&quot;]
CMD [&quot;index.js&quot; ]</code></pre>
</blockquote>
<pre><code>생성한 ``Dockerfile`` 안에 위의 내용을 작성해줍니다
&gt;&gt;✔ 이때 파일 이름을 ``Dockerfile``이 아니라 다른 이름으로 해도 괜찮지만, 그렇게 할 시 설정할 때 추가적으로 바꾼 이름으로 설정을 추가해줘야 하므로 편하게 ``default`` 이름인 ``Dockerfile``로 생성하였습니다.
&gt;

#### 이때 위의 작성한 내용들이 의미하는 바를 하나씩 정리해보겠습니다.
---

#### FROM 명령어
``FROM`` :  어떤 이미지를 사용해서 빌드할 것인지를 정의하는 것  \[ 코드를 작성할 때 node의 최신 버전인 ``16``을 사용했기 때문에 ``node:16``으로 설정해주었습니다.

&gt; ``FROM`` 명령문
```js
FROM &lt;이미지&gt;
FROM &lt;이미지&gt;:&lt;태그&gt;</code></pre><p>하나의 <code>Docker Image</code>는 <code>base image</code>부터 시작해서 기존 이미지위에 새로운 이미지를 중첩해서 여러 단계의 이미지 층(layer)을 쌓아가며 만들어집니다.
<code>FROM</code> 명령문은 이 <code>base image</code>를 지정해주기 위해서 사용되는데, 보통 <code>Dockerfile</code> 내에서 최상단에 위치합니다. <code>base image</code>는 일반적으로 <code>Docker Hub</code>와 같은 <code>Docker repository</code>에 올려놓은 잘 알려진 공개 이미지인 경우가 많습니다.</p>
<blockquote>
<blockquote>
<p>예시 :  <code>NodeJS 16</code>를 <code>base</code> 이미지로 사용</p>
</blockquote>
</blockquote>
<pre><code class="language-js">FROM node:16</code></pre>
<hr>
<h3 id="workdir-명령문">WORKDIR 명령문</h3>
<ul>
<li><code>WORKDIR</code> : 이미지 안에 애플리케이션 코드를 넣기 위한 디렉터리를 생성하는 명령어로 이 디렉터리가 애플리케이션의 작업 디렉터리가 됩니다.<blockquote>
<p><code>WORKDIR</code> 명령문</p>
<pre><code class="language-js">WORKDIR &lt;이동할 경로&gt;</code></pre>
</blockquote>
</li>
</ul>
<blockquote>
<p><code>WORKDIR</code> 명령문은 <code>shell</code>의 <code>cd</code> 명령문처럼 컨테이너 상에서 작업 디텍토리로 전환을 위해서 사용됩니다. 
<code>WORKDIR</code> 명령문으로 작업 디렉터리를 전환하면 그 이후에 등장하는 모든 <code>RUN, CMD, ENTRYPOINT, COPY, ADD</code> 명령문은 해당 디렉터리를 기준으로 실행됩니다.</p>
<blockquote>
<p><code>/usr/app</code>으로 작업 디렉터리 전환</p>
</blockquote>
</blockquote>
<pre><code class="language-js">WORKDIR /usr/app</code></pre>
<hr>
<h3 id="copy-add-명령문">COPY, ADD 명령문</h3>
<ul>
<li><code>COPY or ADD</code> : <code>COPY</code> 명령문은 호스트 컴퓨터에 있는 디렉터리나 파일을 <code>Docker image</code>의 파일 시스템으로 복사하기 위해서 사용됩니다. 
절대 경로와 상대 경로를 모두 지원하며, 상대 경로를 사용할 때는 이 전에 등장하는 <code>WORKDIR</code> 명령문으로 작업 디렉터리를 어디로 전환을 해놨는지 고려해야 합니다.<pre><code class="language-js"># COPY 명령문
COPY &lt;src&gt;... &lt;dest&gt;
COPY [&quot;&lt;src&gt;&quot;,... &quot;&lt;dest&gt;&quot;]</code></pre>
<blockquote>
<p><code>ADD</code> 명령문은 좀 더 파워풀한 <code>COPY</code> 명령문이라고 생각할 수 있습니다. 
<code>ADD</code> 명령문은 일반 파일 뿐만 아니라 압축 파일이나 네트워크 상의 파일도 사용할 수 있습니다. 
이렇게 특수한 파일을 다루는 게 아니라면 <code>COPY</code> 명령문을 사용하는 것이 권장됩니다.</p>
</blockquote>
</li>
<li><code>package.json</code> 파일만 복사<pre><code class="language-js">COPY package.json ./</code></pre>
</li>
<li>이미지를 빌드한 디렉터리의 모든 파일을 컨테이너의 <code>app/ directory</code>로 복사<pre><code class="language-js">WORKDIR app/
COPY . .</code></pre>
</li>
<li>package.json과 package-lock.json을 모두 복사하기 위해 와일드카드 사용<pre><code class="language-js">COPY package*.json ./</code></pre>
</li>
</ul>
<hr>
<h3 id="run-명령문">RUN 명령문</h3>
<ul>
<li><code>RUN</code>
<code>RUN</code> 명령문은 마치 <code>shell</code>에서 커맨드를 실행하는 것 처럼 이미지 빌드 과정에서 필요한 커맨드를 실행하기 위해서 사용됩니다. <code>shell</code>을 통해 거의 못하는 작업이 없는 것 처럼 <code>RUN</code> 명령문으로 할 수 있는 작업은 무궁무진하지만 보통 이미지 안에 특정 소트트웨어를 설치하기 위해서 많이 사용됩니다. <pre><code class="language-js">RUN [&quot;&lt;커맨드&gt;&quot;, &quot;&lt;파라미터1&gt;&quot;, &quot;&lt;파라미터2&gt;&quot;]
RUN &lt;전체 커맨드&gt;</code></pre>
</li>
</ul>
<blockquote>
<ul>
<li>npm 패키지 설치</li>
</ul>
</blockquote>
<pre><code class="language-js">RUN npm install --silent</code></pre>
<ul>
<li>curl 도구 설치<pre><code class="language-js">RUN apk add curl</code></pre>
</li>
<li>pip 패키지 설치<pre><code class="language-js">RUN pip install -r requirements.txt</code></pre>
</li>
</ul>
<hr>
<h3 id="expose-명령문">EXPOSE 명령문</h3>
<ul>
<li><code>EXPOSE</code>
<code>EXPOSE</code> 명령문은 네트워크 상에서 컨테이너로 들어오는 <code>트래픽(traffic)</code>을 
<code>리스닝(listening)</code>하는 <code>포트</code>와 <code>프로토콜</code>를 지정하기 위해서 사용됩니다. <code>프로토콜</code>은 <code>TCP</code>와 <code>UDP</code> 중 선택할 수 있는데 지정하지 않으면 <code>TCP</code>가 기본값으로 사용됩니다.</li>
</ul>
<pre><code class="language-js">EXPOSE &lt;포트&gt;
EXPOSE &lt;포트&gt;/&lt;프로토콜&gt;</code></pre>
<blockquote>
<blockquote>
<p>❗ 여기서 주의할 점은 <code>EXPOSE</code> 명령문으로 지정된 <code>포트</code>는 
<code>해당 컨테이너의 내부</code>에서만 유효하며, <code>호스트(host)</code> 컴퓨터에서는 이 포트를 바로 접근을 할 수 있는 것은 아니라는 겁니다. 
호스트 컴퓨터로부터 해당 포트로의 접근을 허용하려면, <code>docker run</code> 커맨드를 
<code>-p</code> 옵션을 통해 호스트 컴퓨터의 <code>특정 포트</code>를 <code>포워딩(forwarding)</code>시켜줘야 합니다.</p>
</blockquote>
</blockquote>
<hr>
<h3 id="entrypoint-명령문">ENTRYPOINT 명령문</h3>
<ul>
<li><code>ENTRYPOINT</code>
: <code>ENTRYPOINT</code> 명령문은 이미지를 컨테이너로 띄울 때 항상 실행되야 하는 커맨드를 지정할 때 사용합니다. 
<code>ENTRYPOINT</code> 명령문은 <code>Docker image</code>를 마치 하나의 실행 파일처럼 사용할 때 유용합니다. 
왜냐하면 컨테이너가 뜰 때 <code>ENTRYPOINT</code> 명령문으로 지정된 커맨드가 실행되고, 이 커맨드로 실행된 프로세스가 죽을 때, 컨테이너로 따라서 종료되기 때문입니다.</li>
</ul>
<pre><code class="language-js">ENTRYPOINT [&quot;&lt;커맨드&gt;&quot;, &quot;&lt;파라미터1&gt;&quot;, &quot;&lt;파라미터2&gt;&quot;]
ENTRYPOINT &lt;전체 커맨드&gt;</code></pre>
<ul>
<li><p><code>npm start</code> 스크립트 실행</p>
<pre><code class="language-js">ENTRYPOINT [&quot;npm&quot;, &quot;start&quot;]</code></pre>
</li>
<li><p><code>Django</code> 서버 실행</p>
<pre><code class="language-js">ENTRYPOINT [&quot;python&quot;, &quot;manage.py&quot;, &quot;runserver&quot;]</code></pre>
</li>
</ul>
<hr>
<h3 id="cmd-명령문">CMD 명령문</h3>
<ul>
<li><code>CMD</code>
: <code>CMD</code> 명령문은 해당 이미지를 컨테이너로 띄울 때 디폴트로 실행할 커맨드나, <code>ENTRYPOINT</code> 명령문으로 지정된 커맨드에 디폴트로 넘길 파라미터를 지정할 때 사용합니다.
<code>CMD</code> 명령문은 많은 경우, <code>ENTRYPOINT</code> 명령문과 함께 사용하게 되는데, ENTRYPOINT 명령문으로는 커맨드를 지정하고, <code>CMD</code> 명령문으로 디폴트 파리미터를 지정해주면 매우 유연하게 이미지를 실행할 수 있게 됩니다.</li>
</ul>
<pre><code class="language-js">CMD [&quot;&lt;커맨드&gt;&quot;,&quot;&lt;파라미터1&gt;&quot;,&quot;&lt;파라미터2&gt;&quot;]
CMD [&quot;&lt;파라미터1&gt;&quot;,&quot;&lt;파라미터2&gt;&quot;]
CMD &lt;전체 커맨드&gt;</code></pre>
<p>예를 들어, <code>node</code> 커맨드로 디폴트로는 <code>index.js</code>를 실행하고, <code>docker run</code>커맨드에 인자가 있는 경우, 해당 인자를 실행하고 싶은 경우, 다음과 같이 <code>Dockerfile</code>을 작성합니다.</p>
<pre><code class="language-js">ENTRYPOINT [&quot;node&quot;]
CMD [&quot;index.js&quot;]</code></pre>
<p>그러면 다음과 같이 <code>docker run</code> 커맨드의 인자 유무에 따라 <code>node</code> 커맨드로 다른 파일이 실행되게 할 수 있습니다.</p>
<ul>
<li><code>node index.js</code> 실행<pre><code class="language-shell">$ docker run test</code></pre>
<ul>
<li><code>node main.js</code> 실행<pre><code class="language-shell">$ docker run test main.js</code></pre>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="env-명령문">ENV 명령문</h3>
<ul>
<li><p><code>ENV</code></p>
<pre><code class="language-js">ENV &lt;키&gt; &lt;값&gt;
ENV &lt;키&gt;=&lt;값&gt;</code></pre>
<p><code>ENV</code> 명령문은 환경 변수를 설정하기 위해서 사용합니다. 
<code>ENV</code> 명령문으로 설정된 환경 변수는 이미지 빌드 시에도 사용됨은 물론이고, 해당 컨테이너에서 돌아가는 애플리케이션도 접근할 수 있습니다.</p>
</li>
<li><p><code>NODE_ENV</code> 환경 변수를 <code>production</code>으로 설정</p>
<pre><code class="language-js">ENV NODE_ENV production</code></pre>
</li>
</ul>
<hr>
<h3 id="arg-명령문">ARG 명령문</h3>
<ul>
<li><code>ARG</code><pre><code class="language-js">ARG &lt;이름&gt;
ARG &lt;이름&gt;=&lt;기본 값&gt;</code></pre>
: <code>ARG</code> 명령문은 <code>docker build</code> 커맨드로 이미지를 빌드 시, <code>--build-arg</code> 옵션을 통해 넘길 수 있는 인자를 정의하기 위해 사용합니다.</li>
</ul>
<p>예를 들어, <code>Dockerfile</code>에 다음과 같이 <code>ARG</code> 명령문으로 <code>port</code>를 인자로 선언해주면,</p>
<pre><code class="language-js">ARG port</code></pre>
<p>다음과 같이 <code>docker build</code> 커맨드에 <code>--build-arg</code> 옵션에 <code>port</code> 값을 넘길 수가 있습니다.</p>
<pre><code class="language-shell">$ docker build --build-arg port=8080 .</code></pre>
<p>설정된 인자 값은 다음과 같이 <code>${인자명}</code> 형태로 읽어서 사용할 수 있습니다.</p>
<pre><code class="language-js">CMD start.sh -h 127.0.0.1 -p ${port}</code></pre>
<hr>
<h3 id="2-🚓-dockerignore-파일-만들기">2. 🚓 .dockerignore 파일 만들기</h3>
<p><code>Docker image</code>를 빌드할 때 제외 시키고 싶은 파일이 있다면, <code>.dockerignore</code> 파일에 추가해주면 됩니다.</p>
<ul>
<li><code>.dockerignore</code>
```js
.git</li>
<li>.md<pre><code>&gt;위처럼 설정을 해주면 ``Docker``는 프로젝트 최상위 디렉터리에 위치하고 있는 ``markdown`` 파일들을 무사하게 되므로, ``RUN``과 ``CMD, COPY``와 같은 명령문이 해당 파일을 사용할 수 없게 됩니다.
</code></pre></li>
</ul>
<blockquote>
<p>실사용 : <code>Docker</code> 이미지에 로컬 모듈과 디버깅 로그를 복사하는 것을 막아서 이미지 내에서 설치한 모듈을 덮어쓰지 않게 하기 위해 <code>.dockerignore</code>파일을 만들어 줍니다.</p>
</blockquote>
<pre><code class="language-js">node_modules
npm-debug.log</code></pre>
<p>만든 후 위의 내용을 추가해줍니다.</p>
<hr>
<h3 id="3-🛵-docker-실행해보기">3. 🛵 Docker 실행해보기</h3>
<p><img src="https://velog.velcdn.com/images/soshin_dev/post/75d50e99-b06e-456f-9b45-24ade6fdf467/image.png" alt=""></p>
<blockquote>
<p>위 처럼 docker 명령어를 통해 실행해 줄 시 아래처럼 로그에 작성했던 명령문들이 실행되며 진행되는 것을 볼 수 있다!
<img src="https://velog.velcdn.com/images/soshin_dev/post/ca0c0b96-d782-4a43-9462-7a7e28f0d7cc/image.png" alt="">
<img src="https://velog.velcdn.com/images/soshin_dev/post/2d58eeba-ebbf-449e-abc8-4f7bc147a868/image.png" alt=""></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node.js] express-validator]]></title>
            <link>https://velog.io/@soshin_dev/Node.js-express-validator</link>
            <guid>https://velog.io/@soshin_dev/Node.js-express-validator</guid>
            <pubDate>Mon, 04 Apr 2022 12:27:40 GMT</pubDate>
            <description><![CDATA[<h3 id="👨🎓-express-validator-란">👨‍🎓 Express-validator 란?</h3>
<blockquote>
<p>요청이나 데이터들이 유효한지 확인하는 유효성 검사 모듈
[ express-validator is a set of express.js middlewares that wraps validator.js validator and sanitizer functions. ]</p>
</blockquote>
<h3 id="❓-사용하게-된-이유">❓ 사용하게 된 이유</h3>
<blockquote>
<p>원래는 함수로 직접 validator를 생성하여 middleware로 사용했었는데, 코드리뷰를 해주던 멘토님이 express-validator 라는 것이 있으니, 쓰면 편할 것 이라는 말씀을 해주셔서 찾아보고 사용해봤는데 코드만 봐도 직관적이고, 어떤 부분의 유효성을 검사하는지 알 수 있는게 좋아서 사용하게 되었다.</p>
</blockquote>
<h3 id="🍳-사용-전-코드">🍳 사용 전 코드</h3>
<blockquote>
<pre><code class="language-js">function checkProjectCreated(req, res, next) {
  const fields = [&quot;title&quot;, &quot;description&quot;, &quot;from&quot;, &quot;to&quot;];
  const body = Object.keys(req.body);
  const check = fields.filter((field) =&gt; !body.includes(field));
  if (check.length) {
    return res.status(400).json({
      success: false,
      error: {
        code: 400,
        message: `${check.join(&quot;, &quot;)} 은(는) 필수로 입력해줘야 합니다.`,
      },
    });
  }
  next();
}</code></pre>
</blockquote>
<pre><code>위 코드는 express-validator를 사용하지 않고, Project를 생성할 때, 필수로 들어가야할 요소들이 요청 ``body``에 들어가있는지 확인하는 미들웨어 함수의 모습이다.

### 🔑 사용 후 코드
&gt;```js
const validate = (req, res, next) =&gt; {
  const errors = validationResult(req);
  if (errors.isEmpty()) {
    return next();
  }
  return res.status(400).json({
    success: false,
    error: {
      code: 400,
      message: errors.array()[0].msg,
      detail: errors.errors,
    },
  });
};
const checkUserCreated = [
  body(&quot;name&quot;)
    .exists()
    .withMessage(&quot;이름을 입력해주세요.&quot;)
    .bail()
    .isLength({ min: 2 })
    .withMessage(&quot;이름은 필수로 2 글자 이상 입력해야 합니다!&quot;)
    .bail(),
  body(&quot;email&quot;)
    .exists()
    .withMessage(&quot;이메일을 입력해주세요.&quot;)
    .bail()
    .isEmail()
    .withMessage(&quot;올바른 이메일을 입력해주세요.&quot;)
    .bail(),
  body(&quot;password&quot;)
    .exists()
    .withMessage(&quot;비밀번호를 입력해주세요.&quot;)
    .bail()
    .isLength({ min: 4 })
    .withMessage(&quot;비밀번호는 4글자 이상이어야 합니다.&quot;),
  validate,
];</code></pre><p>위 코드는 User를 생성할 때 필수로 들어가야할 요소들과, 그 요소에 해당하는 유효성을 검증하는 코드 입니다.
아까 사용하지 않았던 코드와 다르게 보기만 해도 어떤 부분이 필요한지, 어떤 유효성 검사를 통과해야하는지 한 눈에 직관적으로 알 수 있고, 통일성 있게 구성할 수 있어 협엽할 때도 확실히 도움이 많이 되었습니다.</p>
<h3 id="🎁-자주-사용하는-express-validator-함수들">🎁 자주 사용하는 express-validator 함수들</h3>
<blockquote>
<ul>
<li><code>trim()</code> : 공백을 제거해줍니다.</li>
</ul>
</blockquote>
<ul>
<li><code>isLength(num)</code> : 길이가 <code>num</code>인지 확인해줍니다. <code>{ min: num, max: num }</code> 과 같이 최소, 최대 값도 지정할 수 있습니다.</li>
<li><code>bail()</code> : 해당 부분에서 에러가 발생하면 다음으로 넘어가지 않습니다.</li>
<li><code>isNumeric()</code> : 숫자 형태이지 확인한다. string이어도 해당 string이 숫자인지 확인해줍니다.</li>
<li><code>isEmail()</code> : string이 이메일 형태인지 확인해줍니다.</li>
<li><code>isJSON()</code> : string이 유효한 JSON인지 확인해줍니다. JSON.parse를 사용한다고 합니다.</li>
<li><code>isMobilePhone()</code> : string이 모바일 휴대폰 번호인지 확인해줍니다.</li>
<li><code>withMessage</code>: 유효성 검사가 실패 했을시 <code>validationResult</code>로 에러 메세지를 전달해주기 위해 사용합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Project - 완성 ] 포트폴리오 공유 사이트 제작기]]></title>
            <link>https://velog.io/@soshin_dev/Project-%EC%99%84%EC%84%B1-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EA%B3%B5%EC%9C%A0-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%A0%9C%EC%9E%91%EA%B8%B0</link>
            <guid>https://velog.io/@soshin_dev/Project-%EC%99%84%EC%84%B1-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EA%B3%B5%EC%9C%A0-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%A0%9C%EC%9E%91%EA%B8%B0</guid>
            <pubDate>Thu, 31 Mar 2022 12:58:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/soshin_dev/post/68374cd9-ff5a-4ad2-80dc-b8f0984f538d/image.png" alt=""></p>
<h3 id="👨🎓-프로젝트-진행-과정">👨‍🎓 프로젝트 진행 과정</h3>
<blockquote>
<p>프로젝트는 시리즈에 정리한 것 처럼 매일 아침 스크럼을 통해 개발 일정을 조율해가고, 추가할 기능에 대해 토론을 하며 진행했다.
Gitlab을 통해 협업을 진행하였고, 자신의 개발 정도와 목표 등에 대해 이슈와 마일스톤으로 관리를 했었다.</p>
</blockquote>
<hr>
<h3 id="🔎-검색-기능">🔎 검색 기능</h3>
<blockquote>
<p><img src="https://images.velog.io/images/soshin_dev/post/5e30782e-a615-4331-8449-1c580c9d3ba7/image.png" alt="">
검색 기능은 Mongoose ODM을 사용하여 사용자의 이름, 기술 스택에서 검색어가 존재하는지 정규표현식을 통해 대소문자 구분 없이 검색이 가능하게 하였다.
이때 검색을 진행할 때 쿼리가 여러개 가는것을 막고 한번에 검색 가능하게 만들기 위해 $or operator 또한 사용해주었다.
<img src="https://images.velog.io/images/soshin_dev/post/1c217617-a250-465d-8781-e7a7a51a4dc5/image.png" alt=""></p>
</blockquote>
<hr>
<h3 id="🎨-이미지-업로드-기능">🎨 이미지 업로드 기능</h3>
<blockquote>
<p><img src="https://images.velog.io/images/soshin_dev/post/b8db9872-feae-4b43-bbfe-27ed413b6393/image.png" alt="">
이미지 업로드 기능은 Multer를 사용하여 구현하였다.
DB에는 Multer의 diskStroage로 현재 시간과 파일 이름을 합쳐 커스터마이징한 사진 이름을 저장한 후, images 폴더에 실제 사진 데이터를 저장하게 만들었다.
이때 배포 환경과 개발환경의 환경 변수 [이미지 폴더 경로 ]를 다르게 설정하기 위해 env 파일에서 경로를 불러온다.
<img src="https://images.velog.io/images/soshin_dev/post/845106cf-7181-4d98-8ee0-6533dec38844/image.png" alt=""></p>
</blockquote>
<hr>
<h3 id="📬-비밀번호-찾기-기능-구현--nodemailer-">📬 비밀번호 찾기 기능 구현 [ nodemailer ]</h3>
<blockquote>
<p><img src="https://images.velog.io/images/soshin_dev/post/e1fee906-cc98-415d-bb7a-da1372248a34/image.png" alt="">
<img src="https://images.velog.io/images/soshin_dev/post/16767768-1b47-447f-ac8f-31753d2c0dbe/image.png" alt="">
비밀번호 찾기 기능은 <code>nodemailer</code> 모듈을 사용하여 구현하였다.
<img src="https://images.velog.io/images/soshin_dev/post/9617dce4-2d37-4265-aa7c-89f5692780f4/image.png" alt="">
<img src="https://images.velog.io/images/soshin_dev/post/2b9210c1-9923-4f27-91f8-3d4a52230027/image.png" alt=""></p>
</blockquote>
<hr>
<h3 id="🚳-미들웨어-구성">🚳 미들웨어 구성</h3>
<blockquote>
<p>요청이 들어올 때, 올바른 값을 가지고 있는지에 대한 검증을 위한 검증 미들웨어를 만들었다.</p>
</blockquote>
<pre><code class="language-js">function checkProjectCreated(req, res, next) {
  const fields = [&quot;title&quot;, &quot;description&quot;, &quot;from&quot;, &quot;to&quot;];
  const body = Object.keys(req.body);
  const check = fields.filter((field) =&gt; !body.includes(field));
  if (check.length) {
    return res.status(400).json({
      success: false,
      error: {
        code: 400,
        message: `${check.join(&quot;, &quot;)} 은(는) 필수로 입력해줘야 합니다.`,
      },
    });
  }
  next();
}</code></pre>
<p>초기엔 위와 같이 직접 request안의 body나 query, parameter 등을 검증하는 validator를 만들었지만, 이후 더 쉽고, 직관성있게 검증 미들웨어를 만들 수 있도록 해주는
<code>express-validator</code> 를 알게되어 코드를 변경하였다.</p>
<pre><code class="language-js">const validate = (req, res, next) =&gt; {
  const errors = validationResult(req);
  if (errors.isEmpty()) {
    return next();
  }
  return res.status(400).json({
    success: false,
    error: {
      code: 400,
      message: errors.array()[0].msg,
      detail: errors.errors,
    },
  });
};
const checkUserCreated = [
  body(&quot;name&quot;)
    .exists()
    .withMessage(&quot;이름을 입력해주세요.&quot;)
    .bail()
    .isLength({ min: 2 })
    .withMessage(&quot;이름은 필수로 2 글자 이상 입력해야 합니다!&quot;)
    .bail(),
  body(&quot;email&quot;)
    .exists()
    .withMessage(&quot;이메일을 입력해주세요.&quot;)
    .bail()
    .isEmail()
    .withMessage(&quot;올바른 이메일을 입력해주세요.&quot;)
    .bail(),
  body(&quot;password&quot;)
    .exists()
    .withMessage(&quot;비밀번호를 입력해주세요.&quot;)
    .bail()
    .isLength({ min: 4 })
    .withMessage(&quot;비밀번호는 4글자 이상이어야 합니다.&quot;),
  validate,
];</code></pre>
<p>변경 후 코드는 위 코드와 같다.
딱 봤을 때 코드 길이가 줄어들지 않아서 어디가 간편하고 좋은지 모를 수 있지만 딱 코드만 보고도 어느 값이 필수 값인지, 어느 값에는 어떤 조건을 통과해야하는지에 대한 것들이 한 눈에 보이기 때문에 직관적으로 검증 로직에 대해 알 수 있다는 장점이 있어 유용하게 사용해야겠다고 생각했던 경험이였다!</p>
<hr>
<h3 id="💬-경력-부분-구현">💬 경력 부분 구현</h3>
<blockquote>
<p><img src="https://images.velog.io/images/soshin_dev/post/fcb9334a-5a87-4ae3-8425-d4bd80f5d044/image.png" alt="">
경력 부분 구현에 있어서는 다른곳에서 구현한 것과 다른것이 없었다.
다만 <code>Mongoose ODM</code>의 <code>populate</code> 와 <code>sort</code>를 가장 적절하게 사용한 부분이라 소개에 넣었다.</p>
</blockquote>
<pre><code class="language-js">  static async findAll({ userId }) {
    const user = await UserModel.findOne({ id: userId });
    const careers = await CareerModel.find(
      { author: user },
      { _id: false, __v: false }
    ).populate(&#39;author&#39;, &#39;id -_id&#39;).sort({ fromDate: 1, toDate: 1 });
    return careers;
  }</code></pre>
<p><code>Mongoose</code>에서 <code>sort</code>를 구현하기 위해서 위 처럼 <code>sort</code>에  <code>1</code> 또는 <code>0</code> 의 값을 넣어주면 된다.
위 코드처럼 <code>1</code>을 넣어주게 될 경우에는 오름차 순으로 정렬하게 된다.</p>
<h3 id="🎇-이것저것">🎇 이것저것</h3>
<blockquote>
<p>위의 기능들 말고도 여러가지 기능을 열심히 구현했지만 소개할 정도의 기능은 아니라 여기까지만 넣어놨다!
만약 다른 부분의 코드가 궁금하거나 완성했을 때 어떻게 작동하는지가 궁금하다면
<a href="https://github.com/Shin-GC/OurPortfolioServiceProject.git">https://github.com/Shin-GC/OurPortfolioServiceProject.git</a>
위 주소의 <code>github</code> 레포지토리를 참고하시면 됩니다!</p>
</blockquote>
<hr>
<h3 id="🖥💻-팀원들과-함께-프로젝트를-진행-후-소감-모음">🖥💻 팀원들과 함께 프로젝트를 진행 후 소감 모음</h3>
<blockquote>
<blockquote>
<p>신광천 <code>Back</code>
소통과 구조가 중요하다고 생각하게된 프로젝트였어요!
확실히 일을 시작하기전 아침마다 스크럼을 통해 각자 진행정도에 맞춰서 진행하고, 기능 추가 여부를 확인해서 추가를 하자고 한건 다 성공할 수 있었던 것 같고, 데이터 구조 부분은 몇번이나 수정을 했었는데 딱 구조를 정해뒀으면 프론트 팀원분들이 수정하는 고생을 덜 할 수 있고, 저희 코드 수정도 최소화 할 수 있어 기능을 하나라도 더 추가할 수 있지 않을까 라는 아쉬움이 있어서 이 두가지가 이 프로젝트를 진행하면서 가장 중요하게 느꼈던 것 같아요 🙂</p>
</blockquote>
</blockquote>
<blockquote>
<blockquote>
<p>백** <code>Front</code>
제가 생각하는 제일 중요한 것은 애정이라고 생각합니다! 프로젝트에 대한 애정이 있어야 이것저것 할 욕구도 생기고 각자 맡은 부분에 대한 자신감도 생기기 때문에 더 좋은 아이디어들이 추가된 프로젝트가 만들어질 수 있다고 생각합니다. 그래서 이번 저희 프로젝트 진행이 잘 이루어지지 않았나 싶습니다! 다들 이번 프로젝트에 대한 애정이 많으셔서 공통된 목표에 각자 아이디어들을 덧붙이니까 너무 좋은 결과물이 나왔어요!!</p>
</blockquote>
</blockquote>
<blockquote>
<blockquote>
<p>박** <code>Front</code>
협업에서 중요한것은 소통인것 같습니다</p>
</blockquote>
</blockquote>
<ul>
<li>혼자 문제를 끌어 안기보단 팀원들과 함께 문제를 풀어나가는게 중요하다 생각합니다 (효율성, 다양한 시각의 해결법을 배움)</li>
<li>하나의 작품을 만들기 위해, 개발 스타일을 통일 시킬 때에도 충분한 소통이 중요하다 생각합니다</li>
</ul>
<blockquote>
<blockquote>
<p>김** <code>back</code>
제가 하고 싶은 말이 다 나와버려서 쓸 게 없긴 하지만, 저는 믿음이 중요하다고 생각했습니다. 내가 만든 기능을 프론트쪽에서 잘 구현해주실거라는 믿음, 내가 맡은 일을 잘 해낼 수 있을 거라는 자신감, 다섯 명이 모이면 뭐든지 할 수 있을거라는 믿음이 모여서 프로젝트를 진행하면서 이것저것 과감하게 도전할 수 있는 원동력이 되었다고 생각해요</p>
</blockquote>
</blockquote>
<p><img src="https://images.velog.io/images/soshin_dev/post/90e1fcc7-4129-4b88-bc63-7f585c0ca359/Animation.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Project - OfficeHours ] 오피스 아워 - 3]]></title>
            <link>https://velog.io/@soshin_dev/Project-OfficeHours-%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%95%84%EC%9B%8C-3</link>
            <guid>https://velog.io/@soshin_dev/Project-OfficeHours-%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%95%84%EC%9B%8C-3</guid>
            <pubDate>Wed, 30 Mar 2022 15:16:49 GMT</pubDate>
            <description><![CDATA[<h4 id="2022-03-23-office-hour">2022-03-23 Office Hour</h4>
<h2 id="이미지-파일-관련">이미지 파일 관련</h2>
<p>파일 제공 서버를 node로 쓰면 안 좋다 =&gt; 느림!  
nginx 사용하면 좋음<br>reverse proxy, cors  </p>
<h2 id="코드리뷰">코드리뷰</h2>
<h4 id="middleware-사용하기">middleware 사용하기</h4>
<p>body, params, query 체크 -&gt; 나머지 로직 실행<br>각자 미들웨어 하나씩 만들어 주는 것도 괜찮다.  </p>
<h4 id="istruthy">isTruthy</h4>
<p>함수가 애매하다.<br><code>if (data) {}</code> 쓰면 되지 않을까?  </p>
<p>목적이 있다면 함수 이름을 좀 더 명확하게<br><a href="https://developer.mozilla.org/ko/docs/Glossary/Truthy">https://developer.mozilla.org/ko/docs/Glossary/Truthy</a><br><a href="https://developer.mozilla.org/ko/docs/Glossary/Falsy">https://developer.mozilla.org/ko/docs/Glossary/Falsy</a>  </p>
<h4 id="date">Date</h4>
<p>Date는 Date 형식으로 넣어주는 것이 좋다.<br><a href="https://food4ithought.com/2021/05/24/unix-time-%EC%9D%B4%EB%9E%80-epoch-posix-%EC%BF%BC%EB%A6%AC-%EC%8B%9C-%EC%A3%BC%EC%9D%98-%EC%82%AC%ED%95%AD/">https://food4ithought.com/2021/05/24/unix-time-%EC%9D%B4%EB%9E%80-epoch-posix-%EC%BF%BC%EB%A6%AC-%EC%8B%9C-%EC%A3%BC%EC%9D%98-%EC%82%AC%ED%95%AD/</a><br><a href="https://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%89%EC%8A%A4_%EC%8B%9C%EA%B0%84">https://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%89%EC%8A%A4_%EC%8B%9C%EA%B0%84</a><br><a href="https://currentmillis.com/">https://currentmillis.com/</a>  </p>
<p>+) Temporal<br><a href="https://tc39.es/proposal-temporal/docs/">https://tc39.es/proposal-temporal/docs/</a>  </p>
<p>+) Moment<br><a href="https://momentjs.com/">https://momentjs.com/</a><br>이거 좀 무거움  </p>
<p>+) Day.js<br><a href="https://day.js.org/docs/en/installation/installation">https://day.js.org/docs/en/installation/installation</a><br><a href="https://jsikim1.tistory.com/196">https://jsikim1.tistory.com/196</a><br><a href="https://www.npmjs.com/package/dayjs">https://www.npmjs.com/package/dayjs</a>  </p>
<h2 id="error-관련">error 관련</h2>
<p>custom error를 만드는 것도 좋다.<br>Error를 상속받아서 넓혀주기  </p>
<p>지금은 그냥 <code>error.statusCode=500</code> 이런 식으로 하기<br><code>res.status(error.statusCode)</code>  </p>
<h2 id="class-prototype">class, prototype</h2>
<h4 id="prototype">prototype</h4>
<pre><code>function UserAuthService() {
}

UserAuthService.searchUser = function() {}

UserAuthService.prototype.foo = function() {} // static이 아닌 경우</code></pre><h4 id="class">class</h4>
<pre><code>const userService = new UserAuthService();</code></pre><h2 id="-operator">?? operator</h2>
<p>null, undefined 체크<br><a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator</a>  </p>
<h2 id="webassembly">WebAssembly</h2>
<p>성능은 좋다.<br><a href="https://arghya.xyz/articles/webassembly-wasm-wasi/">https://arghya.xyz/articles/webassembly-wasm-wasi/</a><br><a href="https://d2.naver.com/helloworld/8257914">https://d2.naver.com/helloworld/8257914</a>  </p>
<h2 id="deno">Deno</h2>
<p>이건 제가 들으려고 스크랩해둔 건데 참고하세요!  
<a href="https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%94%94%EB%85%B8-%EA%B8%B0%EB%B3%B8#curriculum">https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%94%94%EB%85%B8-%EA%B8%B0%EB%B3%B8#curriculum</a>  </p>
<h2 id="morgan">morgan</h2>
<p><a href="https://www.npmjs.com/package/morgan">https://www.npmjs.com/package/morgan</a><br><a href="https://chan180.tistory.com/164">https://chan180.tistory.com/164</a>  </p>
<h2 id="postman-share">postman share</h2>
<p>export해서 JSON 파일로 만들고 git에다 올리기<br>또는 invite 해서 같이 공유</p>
<blockquote>
<h3 id="📒-오피스-아워-진행-후">📒 오피스 아워 진행 후</h3>
<p>: 역시 이번 오피스아워 시간에도 여러가지 지식들을 알 수 있어서 너무 좋았다.
이미지 파일을 express static을 통해 정적으로 제공하고 있었는데, 이런 방식은 작은 프로젝트나 토이 프로젝트에서는 괜찮지만 실제 큰 서비스를 진행할 때는 속도가 매우 느리다고 했다.
그때 Nginx를 사용해서 정적파일을 제공하면 훨씬 빠른 속도로 진행할 수 있다는 것을 알게되어 Nginx에 대해 공부하는 시간을 가졌다.
또한 원래 만들던 parameter 체크 middleware를 express validator를 통해 만들면 조금 더 간결하고 직관적으로 만들 수 있다 하여 express validator 또한 공부하여 적용하였다.
그리고 Morgan이라는 패키지를 알려주었는데, Morgan은 요청을 보내거나 요청이 올때, 그걸 console창에 보여주는 역활을 하는데 설치하고 적용하자 어디서 요청이 오고, 오디다 요청을 보내는지에 대한 정보가 나와 프로그래밍을 할 때 더욱 편함을 느꼈다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Project - OfficeHours ] 오피스 아워 - 2]]></title>
            <link>https://velog.io/@soshin_dev/Project-OfficeHours-%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%95%84%EC%9B%8C-2</link>
            <guid>https://velog.io/@soshin_dev/Project-OfficeHours-%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%95%84%EC%9B%8C-2</guid>
            <pubDate>Wed, 30 Mar 2022 15:01:49 GMT</pubDate>
            <description><![CDATA[<h3 id="2022-03-19-office-hour">2022-03-19 Office Hour</h3>
<h2 id="스키마-변경-시-기존-db-변경">스키마 변경 시 기존 DB 변경</h2>
<p>Atlas에서 column 추가, updateMany 사용  </p>
<h2 id="기존-정보를-바꿔야-하는-경우">기존 정보를 바꿔야 하는 경우</h2>
<p>프로시저 실행, query 왕창 날리기<br>db에 변경사항 먼저 업뎃하고 배포  </p>
<h2 id="코드리뷰">코드리뷰</h2>
<ul>
<li>Model<ul>
<li>명명법 통일 필요  </li>
</ul>
</li>
<li>Router<ul>
<li>parameter 유무 파악하는 로직 필요</li>
<li>res 전달하는 부분도 통일하는 게 좋음(aws 참고)  <ul>
<li>errorMiddleware</li>
</ul>
</li>
<li>status code를 상황에 따라 나눠주는 것이 좋음  </li>
</ul>
</li>
</ul>
<p>공통되는 부분 <code>utils.js</code>로 뽑아두는 것 추천  </p>
<h2 id="좋아요-기능">좋아요 기능</h2>
<p>유저 스키마에 like field를 추가하고자 함<br>누가 누구에게 눌렀는지<br>유저 id 배열로 넣기  </p>
<pre><code class="language-js">userAuthRouter.post(&#39;users/:id/likes&#39;), async (req, res, next) =&gt; {
    const {likedUserId} = req.body;
    // …
}</code></pre>
<p>populate하면 너무 커져버리지 않을까? &gt; 유저 ID만 배열로 주자!
아니면 populate를 필터링해서 가져오기</p>
<h2 id="gitignore-쉽게하기">gitignore 쉽게하기</h2>
<p><a href="https://www.toptal.com/developers/gitignore">https://www.toptal.com/developers/gitignore</a></p>
<h2 id="eslint-참조">ESLint 참조</h2>
<p><a href="https://eslint.org/docs/user-guide/getting-started">https://eslint.org/docs/user-guide/getting-started</a>  </p>
<h2 id="node_modules">node_modules</h2>
<p>각 패키지마다 또 의존하는 애들이 있기 때문에 node_modules 안의 내용이 많아짐<br>이들의 정보는 package-lock.json에 존재함  </p>
<h2 id="acid">ACID</h2>
<p><a href="https://ko.wikipedia.org/wiki/ACID">https://ko.wikipedia.org/wiki/ACID</a></p>
<h2 id="cap">CAP</h2>
<p><a href="https://ko.wikipedia.org/wiki/CAP_%EC%A0%95%EB%A6%AC">https://ko.wikipedia.org/wiki/CAP_%EC%A0%95%EB%A6%AC</a><br><a href="http://eincs.com/2013/07/misleading-and-truth-of-cap-theorem/">http://eincs.com/2013/07/misleading-and-truth-of-cap-theorem/</a>  </p>
<h2 id="react-virtualized">react-virtualized</h2>
<p><a href="https://velog.io/@kimjh96/react-virtualized-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94">https://velog.io/@kimjh96/react-virtualized-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</a>  </p>
<h2 id="유저-정보-공개-범위-지정">유저 정보 공개 범위 지정</h2>
<p>middleware에서 처리하는가<br>스키마에서 공개 여부를 저장하는가  </p>
<p>유지가 되어야 하니 스키마에서 저장되는 것이 맞음<br>전체에 대해 true/false를 지정한 다음 이 값을 FE에서 보고 결정 =&gt; 개발자도구로 까보는 사람도 있음<br>따라서 보여줄 부분만 백엔드에서 주는 것을 추천  </p>
<h2 id="지원-여부-확인">지원 여부 확인</h2>
<p><a href="https://node.green/">https://node.green/</a><br><a href="https://caniuse.com/">https://caniuse.com/</a></p>
<blockquote>
<h3 id="📒-오피스아워-진행-후">📒 오피스아워 진행 후</h3>
<p>: 확실히 현업자 코치님과 함께 이야기를 진행하고, 코드리뷰를 받으며 여러가지를 배울 수 있어서 오피스 아워 시간이 너무 좋았다.
체크해주신 사항으로는 들어오는 parameter를 체크해주는 middleware를 두기, res 데이터 구조 통일하기, errorMiddleware 만들기, Model 이름 통일화 하기가 있었다.
이때 Model명과 res 데이터 구조는 직접 프로그래밍 하면서도 느꼈던 부분이여서 확실히 빠르게 수정해야겠다고 느꼈다.
그리고 middleware로 체크하는 부분도 만들기 전에는 필요성을 높게 느끼지 않았지만 만들고 사용해보자 확실히 없을 때 보다 에러 체크 하기도 편하고 보낼 요청이 확 줄어들어 최적화에도 큰 도움이 된 것 같았다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Project - OfficeHours ] 오피스 아워 - 1]]></title>
            <link>https://velog.io/@soshin_dev/Project-OfficeHours-%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%95%84%EC%9B%8C-1</link>
            <guid>https://velog.io/@soshin_dev/Project-OfficeHours-%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%95%84%EC%9B%8C-1</guid>
            <pubDate>Mon, 21 Mar 2022 06:40:57 GMT</pubDate>
            <description><![CDATA[<h2 id="2022-03-17-office-hour">2022-03-17 Office Hour</h2>
<hr>
<h3 id="delete-관련">delete 관련</h3>
<p>바로 db에서 삭제하는 것이 아니라 일정 유효 기간을 두고 한꺼번에 쌓인 것들 삭제하는 식으로 작성하는 것이 어떤지<br>=&gt; cron 참조</p>
<p>주기적으로 db 확인해주고 일정 시간이 지나면 지우도록 하는 것도 괜찮을 거 같음.</p>
<hr>
<h3 id="get이랑-post만-사용하는-것-관련">get이랑 post만 사용하는 것 관련</h3>
<p>get, post는 기존에 존재하던 것과 달리 put, delete는 나중에 나옴.<br>앞의 두 개보다 뒤의 두개가 문제가 많다고 볼 수는 있긴 함.<br>그러나 get과 post만 써야한다는 건 아니고 put과 delete가 100% 안전하다고 할 수는 없지만 많이 걱정을 할 필요는 없을 것.</p>
<hr>
<h3 id="cors">Cors</h3>
<p>불필요한 접속 방지.<br>여기서 어떤 메서드의 요청을 받을지 정할 수 있음.<br>cors documentation 참조.  </p>
<p><a href="https://evan-moon.github.io/2020/05/21/about-cors/">https://evan-moon.github.io/2020/05/21/about-cors/</a></p>
<hr>
<h3 id="db에서-검색할-때-id-ref">DB에서 검색할 때 id? ref?</h3>
<p>둘이 큰 차이가 없음.
하지만 관리적인 측면에서는 ref를 이용하는 게 좋음</p>
<hr>
<h3 id="검색-기능">검색 기능</h3>
<p>프론트에 이미 데이터들이 전달되어있다고 하지만 데이터가 얼마나 신선한지를 봐야 함.<br>텀이 짧다면 프론트에서 하는 게 시간이 적게 걸림.  </p>
<p>만약 백엔드에서 검색어를 받고 검색 결과를 프론트에 전달한다면<br>자동완성 기능같은 경우 onChange 이벤트 일어날 때마다 요청이 발생하는가?<br>=&gt; 회사마다 다르다.<br>글자 하나 당 요청을 보낼 수도, 자음, 모음 하나 당  요청을 보낼 수도 있다.<br>threshold 생각을 해야 함.  </p>
<p>input을 받을 때 몇 초간에만 입력을 받고 그것들을 모아서 쿼리 하나를 보내자<br>=&gt; throttle은 일정 시간마다, debounce는 입력이 끝난 후 요청  </p>
<p><a href="https://pks2974.medium.com/throttle-%EC%99%80-debounce-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0-2335a9c426ff">참조 사이트</a></p>
<p>아니면 처음 입력 몇 개만 보고 결과들을 다 프론트에 전달한 뒤 프론트에서 알맞은 걸 골라서 처리하는 방법도 있음.</p>
<p>사용자에게 안 보이게 일정 시간 지나면 데이터 다시 신선하게 돌리는 방법도 좋음<br>이 경우 무한히 돌아가는 코드이기 때문에 페이지를 이동한다던가 하면 죽여줘야 함  </p>
<hr>
<h3 id="redis">Redis</h3>
<p>이거 회사들 많이 사용.
사용자가 많으면 필요.  </p>
<p>참조<br><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Expires">https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Expires</a></p>
<hr>
<h3 id="vscode의-githistory-extension">VScode의 gitHistory extension</h3>
<p>git GUI 툴 못 쓸 경우 gitHistory 사용  </p>
<hr>
<h3 id="코드-리뷰">코드 리뷰</h3>
<ul>
<li>Service<ul>
<li>클래스 시작은 대문자  </li>
</ul>
</li>
<li>db&gt;Schema<ul>
<li>이름은 되도록이면 camelCase 사용</li>
<li>이름은 직관적으로  </li>
</ul>
</li>
<li>education.js<ul>
<li>enum에 값을 바로 넣기 보다는 값들을 대표하는 id를 넣어주는 것이 좋음<br>값을 바로 넣는다고 할 때 값이 바뀔 경우 바꾸어주어야 하는 코드들이 많기 때문</li>
</ul>
</li>
<li>routes<ul>
<li>api path는 복수를 씀<br>certificateRouter.post(‘certificate/create’) 대신 certificateRouter.post(‘/certificates’)<br>certificateRouter.post(‘/certificates’) 라고 하면 certificate create를 하겠다는 뜻</li>
</ul>
</li>
<li>educationService<ul>
<li>리팩토링 필요(숙제!)</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="🩹-오피스아워-후">🩹 오피스아워 후</h3>
<p>: 여러가지 궁금했던 부분들을 현직자 분에게 여쭤볼 수 있는 기회가 생겨서 너무 좋았다.
특히 개발 외적으로 보안쪽으로 신경써야할 부분이나, 잘못하면 그냥 지나가는 부분들도 집어서 알려주시는게 너무 좋았다.
코드 리뷰 부분에서 쿼리를 한번만 해도 되는걸 여러번 하거나, 이름의 통일성이 없는 부분 등이 있어 수정사항을 받았고, <code>API path</code>를 <code>RESTful</code> 하게 만드는 법도 배울 수 있어 아주 유용했다!
여러가지 수정사항이 나왔지만 수정사항이 나왔다기보다 더 발전할 수 있는 방법들이 나온 것 같아 기분이 너무 좋았던 코드 리뷰 였다!
다음번엔 코드 리뷰를 완벽하게 통과하는걸 목표로 잡아야겠다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Project ] 개발 시작 구조]]></title>
            <link>https://velog.io/@soshin_dev/Project-%EA%B0%9C%EB%B0%9C-%EC%A7%84%ED%96%89-1</link>
            <guid>https://velog.io/@soshin_dev/Project-%EA%B0%9C%EB%B0%9C-%EC%A7%84%ED%96%89-1</guid>
            <pubDate>Sun, 20 Mar 2022 05:28:15 GMT</pubDate>
            <description><![CDATA[<h3 id="📁-express-3계층-설계-3-layer-architecture--구조-만들기">📁 Express 3계층 설계[ 3-Layer architecture ] 구조 만들기</h3>
<blockquote>
<p>설계란? </p>
</blockquote>
<ul>
<li>프로그래밍에서의 코드 설계는 코드와 파일, 그리고 폴더 구조를 설계하는 것을 말합 니다.</li>
</ul>
<p>만약 적절한 설계를 하지 않고 코드를 작성하게 될 경우 여러가지 문제가 발생할 수 있습니다.</p>
<ul>
<li><p>코드가 몇백 줄(혹은 몇천 줄)이 되기 때문에, 수정할 코드가 있을 때 해당 부분을 찾기 힘듭니다. 따라서 유지보수가 어려워집니다.</p>
</li>
<li><p>코드와 코드 사이 (함수와 함수 사이 등) 어떤 관계가 있는지 한 눈에 알아보기 힘듭니다.</p>
</li>
<li><p>각 코드(함수, 객체 등)의 역할과 기능이 명확하게 구분되어 있지 않으므로, 기능별로 테스트(유닛 테스트)를 진행하는 것이 어려워집니다.</p>
</li>
<li><p>하나의 파일을 동시에 여러 사람이 수정하는 것은 힘들기 때문에, 분업이 어려워 집니다.</p>
</li>
<li><p>새로운 기능을 추가하고자 할 때, 기존에 존재하는 코드의 어느 부분을 수정하고 혹은 새로 작성해야 하는지 알아내기 힘듭니다. 즉, 확장성이 부족한 코드가 됩니다. </p>
</li>
</ul>
<p>위 문제들을 해결하기 위해 다양한 코드 구조 설계가 만들어졌는데, 저는 그 중 3계층 구조 설계를 사용하기로 했습니다!</p>
<p>3계층 구조 설계란 3개 구조로 나누어 설계하는 것을 말합니다.</p>
<p><img src="https://images.velog.io/images/soshin_dev/post/c9647f0f-4c72-4469-8cb5-2610cd1418d1/image.png" alt=""></p>
<blockquote>
<p><strong>Control layer (컨트롤러)</strong>: 사용자의 요청(request)을 분석한 후, 알맞은 서비스로 해당 요청을 전달해 준 다음, 서비스의 결과를 다시 응답(response)하는 층입니다.
즉, 라우팅(Routing)이 이루어지는 층입니다.
express의 경우,req.params(), req.body(), res.status(), res.send()와 같은 코드가 작성됩니다.</p>
</blockquote>
<blockquote>
<p><strong>Service layer(서비스)</strong>: 컨트롤러로부터 전달된 요청에 로직을 적용하는 층입니다.
이 때 로직이라는 것은, 예를 들어 로그인 서비스의 경우 다음과 같습니다.
아래와 같은 로직을 if-else 등의 코드로 구현할 수 있습니다.</p>
</blockquote>
<blockquote>
<p><strong>Model layer(데이터)</strong>: 서비스 층에서 데이터베이스 접근이 필요한 경우가 있는데, 이 때 데이터 관련 코드가 작성되는 층입니다. Mongoose의 경우 Model.find({})와 같은 코드가 작성됩니다.
로그인 서비스를 예시로 보면, 요청으로 온 ID가 데이터베이스에 존재하는지, 만약 존재한다면 그 때 데이터베이스 상에 저장된 비밀번호는 요청으로 온 (사용자가 입력한) 비밀번호와 일치하는지 확인해야 합니다.
이 때 서비스 층은 데이터 층에 데이터베이스 확인 요청을 하게 되는 것입니다.</p>
</blockquote>
<p>위와 같이 코드를 역할별로 분리하여 각 층에 구현하는 경우, 다음과 같은 장점이 있습니다.</p>
<ul>
<li><p>각 역할별로 개발 업무를 분담할 수 있으므로 분업이 용이해집니다.</p>
</li>
<li><p>역할 별로가 아닌 MVP 별로 개발 업무를 분담하는 경우에도, 각 MVP가 어떤 흐름으로 구현되는지 (<code>Controller</code>에서 사용자로부터 요청을 받고, 요청에 대해 <code>Service</code> 층에서 로직을 구현하고, 필요 시 데이터베이스에 접근한 후, 결과를 <code>Controller</code>가 응답합니다) 코드 구조를 보고 이해하기 쉬워집니다.</p>
</li>
<li><p>라우팅 관련 코드는 <code>Controller</code> 폴더에서, 서비스 로직 관련 코드는 <code>Service</code> 폴더에서, 데이터 관련 코드는 <code>Model</code> 폴더에서 구분하여 확인할 수 있으므로, 필요한 코드를 빠르게 찾을 수 있으며, 따라서 유지보수가 용이해집니다.</p>
</li>
<li><p>웹 서비스의 구현 방식을 변경하고자 할 때, 예를 들어 데이터베이스를 <code>Mongodb</code>에서 <code>Mysql</code>로 변경하고자 할 때, 각 폴더는 역할이 분리되어 있으므로, <code>Model</code> 폴더 코드만 변경하면 되고 나머지 <code>Controller</code>, <code>Service</code> 폴더는 변경하지 않아도 됩니다. 유지보수가 용이해집니다.</p>
</li>
<li><p>코드가 기능별로 구분되어 있으므로, 기능별로 테스트(유닛 테스트)를 진행하기 용이해집니다.</p>
</li>
</ul>
<p>그래서 이런 3계층 구조 설계를 사용하기 위해 현재 폴더구조를 아래와 같이 설정했습니다.</p>
<pre><code class="language-shell">└───src
    ├───db           # Model layer
    │   ├───models
    │   └───schemas
    ├───middlewares
    ├───routers     # Control layer
    └───services    # Service layer</code></pre>
<hr>
<h3 id="💻-수상-내역-award--부분-db-설계-진행">💻 수상 내역 [Award ] 부분 DB 설계 진행</h3>
<blockquote>
<p>처음 설계할 때 User DB에 배열로 수상 내역을 넣는 식으로 진행 했으나 Award 부분 관리를 하는데 User DB 부분을 같이 봐야하는 점이 불편해서 User Model 과 Award Model을 따로 나눈 후 Award 모델에서 User모델을 참조하게 하여 진행하였습니다.</p>
</blockquote>
<pre><code class="language-js">import mongoose from &quot;mongoose&quot;;
const Schema = mongoose.Schema;
const model = mongoose.model;
const AwardSchema = new Schema({
  id: {
    type: String,
    unique: true,
    required: true,
  },
  title: {
    type: String,
    required: true,
  },
  description: String,
  author: {
    type: Schema.Types.ObjectId,
    ref: &quot;User&quot;,
    required: true,
  },
});
const AwardModel = model(&quot;Award&quot;, AwardSchema);
export { AwardSchema, AwardModel };</code></pre>
<p>그리고 담당한 다른 파트인 Education [학력 부분 API ] 의 모델도 Award Model 처럼 설정해주었습니다.</p>
<hr>
<h3 id="💾-gitlab-이슈를-commit-메세지로-닫기">💾 GitLab 이슈를 Commit 메세지로 닫기</h3>
<p>Gitlab 이슈를 생성 하게 되면 이슈 번호가 지정되는데, Commit 을 할 때 특정 명령어와 이 이슈번호를 사용하면 Commit을 할 때 이슈를 자동으로 닫게 할 수 있습니다.</p>
<p>만약 이슈 번호가 #79번 일때, <code>&quot;close #79&quot;</code> 또는 <code>&quot;fix #79&quot;</code> 처럼 입력하게 된다면 commit 을 하면 이슈도 자동으로 닫히게 됩니다.</p>
<p>만약 다른 방식으로 닫게 하고 싶거나 추가하고 싶다면 <code>config/gitlab.yml</code> 의 <code>issue_closing_pattern</code> 에 지정하면 됩니다.</p>
<hr>
<h3 id="📒-느낀점">📒 느낀점</h3>
<p>DB 설계를 그냥 머리에서 떠오르는 대로 시작했더니 결국 DB를 다시 뜯어고치는 일이 생겼다.</p>
<p>처음부터 DB 설계를 짜고, 진행할 일 들을 미리 구상했으면 뜯어고치는 일을 없애거나 최소화 할 수 있을 것 같아 다음번엔 꼭 진행할 예정에 맞춰 설계를 한 후, 스키마를 작성해야겠다.</p>
<p>그리고 이슈를 작성하고, commit 하고, merge 한 후, 이슈를 직접 닫아 줬었는데 GitLab에서 작성한 이슈를 commit 으로 자동으로 닫게 할 수 있다는걸 알고 역시 아는 만큼 편하고 효율성 높게 작업할 수 있다는 걸 다시 한번 느껴 내가 사용하는 툴 들의 기능을 한번 더 공부해봐야겠다.</p>
<p>처음 3계층 구조 설계로 폴더를 나눈 후 코드를 작성하는데 여기서 작성하고 저기서 작성하고 처음에는 헷갈리고 힘들었지만, 적응하고 나니 내가 원하는 부분을 편집할 때 딱 그 부분만 가서 수정하면 되는 것을 몸으로 느끼자 평소에 한 곳에 짜는 것 보다 훨씬 편하게 느껴졌다.</p>
<p>앞으로 다른 프로젝트를 진행할 때는 다른 구조도 사용해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Error] Node 에서 환경 변수를 읽어오지 못하는 오류]]></title>
            <link>https://velog.io/@soshin_dev/Error-Node-%EC%97%90%EC%84%9C-%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98%EB%A5%BC-%EC%9D%BD%EC%96%B4%EC%98%A4%EC%A7%80-%EB%AA%BB%ED%95%98%EB%8A%94-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@soshin_dev/Error-Node-%EC%97%90%EC%84%9C-%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98%EB%A5%BC-%EC%9D%BD%EC%96%B4%EC%98%A4%EC%A7%80-%EB%AA%BB%ED%95%98%EB%8A%94-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Wed, 16 Mar 2022 03:24:18 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-발생">문제 발생</h3>
<blockquote>
<p><code>Node</code> 에서 환경 변수를 사용하기 위해 파일을 생성 후 <code>process</code> 를 통해 사용하려 했으나 값이 <code>undefined</code> 으로 나오는 문제</p>
</blockquote>
<h3 id="환경">환경</h3>
<pre><code class="language-jsx">SERVER_PORT=5001
MONGODB_URL=&quot;mongodb+srv://&lt;Username&gt;:&lt;Password&gt;@portfolio.zk0k1.mongodb.net/myFirstDatabase?retryWrites=true&amp;w=majority&quot;</code></pre>
<h3 id="문제-발생-원인">문제 발생 원인</h3>
<blockquote>
<p>환경 변수를 사용해주기 위해 필요한 라이브러리 import 및 명령어를 사용하지 않아서 환경 변수를 읽어오지 못하는 문제</p>
</blockquote>
<hr>
<h3 id="문제-해결">문제 해결</h3>
<pre><code class="language-jsx">import dotenv from &quot;dotenv&quot;;
dotenv.config();

console.log(process.env.SERVER_PORT);

// 출력 : 5001</code></pre>
<p><code>dotenv</code>를 <code>import</code>해준 후, 환경변수를 사용한다는 명령어인 <code>config</code>를 사용해주어 해결하였다.</p>
<h3 id="기억할-점">기억할 점</h3>
<blockquote>
<p><strong>위의 <code>.env</code>를 불러오는 코드는 가능한 코드의 최상단의 위치시켜주는 것을 권장합니다. 그렇지 않고 <code>.env</code>파일이 <code>process.env</code>에 로드 되기 전에 접근하게 되면 <code>undefined</code>가 됩니다.</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client 오류]]></title>
            <link>https://velog.io/@soshin_dev/ERRHTTPHEADERSSENT-Cannot-set-headers-after-they-are-sent-to-the-client-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@soshin_dev/ERRHTTPHEADERSSENT-Cannot-set-headers-after-they-are-sent-to-the-client-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Mon, 14 Mar 2022 06:43:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/soshin_dev/post/90bdc76d-557a-48d7-8026-875ec86e6fd0/abstract-fatal-error.gif" alt=""></p>
<h3 id="err_http_headers_sent">[ERR_HTTP_HEADERS_SENT]</h3>
<p>node 로 express를 통해 작업 중 아래와 같은 오류가 발생했습니다.
<code>[ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client</code>
오류 <code>[ERR_HTTP_HEADERS_SENT]</code>는 서버가 클라이언트에 둘 이상의 응답을 보내려고 할 때 발생하는 오류 입니다.
즉 하나의 응답을 보낸 후 또 다른 응답을 동시에 보내려고 할 때 생기는 오류 입니다.</p>
<hr>
<h4 id="예시">예시</h4>
<pre><code class="language-js">router.get(&quot;/&quot;, verifyToken, (req, res) =&gt; {
  if (req.user) {
    res.send(`${req.user.username}님 환영합니다!`);
  }
  res.send(&quot;로그인 후 이용해주세요.&quot;);
});</code></pre>
<p>위 코드를 보면 <code>req.user</code>에 값이 있을 경우에 그 안에 담긴 <code>username</code> 값을 포함해서 내보내고, 만약 없다면 <code>&quot;로그인 후 이용해주세요.&quot;</code> 라는 문구를 내보내게 만들었습니다.</p>
<p>이때 <code>req.user</code>값이 있어서 if 문을 들어가게 되면 앞서 말했던 <code>username</code>을 포함한 값을 내보낸 후 나와서 <code>&quot;로그인 후 이용해주세요.&quot;</code> 라는 문구까지 내보내려고 응답을 중복해서 두번 내려하기 때문에 오류가 발생하는 것 입니다.</p>
<hr>
<h4 id="해결-방법">해결 방법</h4>
<pre><code class="language-js">router.get(&quot;/&quot;, verifyToken, (req, res) =&gt; {
  if (req.user) {
    return res.send(`${req.user.username}님 환영합니다!`);
  }
  return res.send(&quot;로그인 후 이용해주세요.&quot;);
});</code></pre>
<p>위 코드 처럼 내보낼 때 return 문을 통해 응답을 하나만 하게 만들면 쉽게 오류를 해결 할 수 있습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Node.js ] Mongoose 를 사용해보자!]]></title>
            <link>https://velog.io/@soshin_dev/Node.js-Mongoose-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@soshin_dev/Node.js-Mongoose-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 28 Feb 2022 11:34:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="🤷♂️mongoose란-무엇일까">🤷‍♂️Mongoose란 무엇일까?</h3>
</blockquote>
<ul>
<li>Mongoose 모듈은 MongoDB 라는 NoSQL 데이터베이스를 Node.js로 사용할 수 있도록 하는 확장 모듈 중 하나 입니다.</li>
<li>Mongoose는 데이터를 만들고 관리하기 위해 스키마 [ Schema ]를 만들고, 그 스키마로 모델을 만들어 데이터를 관리 합니다.</li>
<li>스키마와 모델을 통하여 data를 불러온 후 객체화 시켜 빠르게 수정함으로써 데이터에 접근 가능하게 만들어줍니다.</li>
<li>모델링 된 문서 [ Document ]가 모여있는 Collection을 관리하는 것을 수월하게 만들어 줍니다.</li>
</ul>
<hr>
<blockquote>
<h3 id="🍀-주요-메소드-사용해보기">🍀 주요 메소드 사용해보기</h3>
</blockquote>
<blockquote>
<h4 id="💻데이터-베이스-연결하기">💻데이터 베이스 연결하기</h4>
</blockquote>
<pre><code class="language-js">const mongoose = require(&#39;mongoose&#39;)
mongoose.connect(&quot;mongodb://127.0.0.1:27017/UserAPI&quot;)
  .then(() =&gt; {
    console.log(&quot;Connected to MongoDB =&gt; UserAPI&quot;);
  })
  .catch((err) =&gt; {
    console.log(err);
  });
</code></pre>
<ol>
<li><code>const mongoose = require(&#39;mongoose`)</code> 를 통해 mongoose 모듈을 불러옵니다.</li>
<li>그 후 <code>mongoose.connetc(&quot;데이터 베이스 주소&quot;)</code> 를 통해 MongoDB와 연결할 수 있습니다.</li>
</ol>
<blockquote>
<h4 id="💡-이때-mongoose-query는-then-함수를-가진-thenable-이기-때문에-프로미스를-이용하듯이-이용할수-있습니다">💡 이때 Mongoose query는 then() 함수를 가진 thenable 이기 때문에 프로미스를 이용하듯이 이용할수 있습니다.</h4>
</blockquote>
<ol start="3">
<li>then 을 통해 연결에 성공 했을 때와 실패 했을때를 구분해서 메세지를 출력할 수 있습니다.</li>
</ol>
<hr>
<blockquote>
<h4 id="🎫-모델-정의하기--schema-만들기-">🎫 모델 정의하기 [ Schema 만들기 ]</h4>
</blockquote>
<pre><code class="language-js">const mongoose = require(&#39;mongoose&#39;)
const {Schema} = require(&#39;mongoose&#39;)

const UserSchema = new Schema({
  user_id: {
    required: true,
    unique: true,
    type: String,
  },
  password: {
    required: true,
    type: String,
  },
  salt: {
    required: true,
    type: String,
  },
  email: {
    required: true,
    type: String,
    unique: true,
  }
})

mongoose.model(&#39;users&#39;, UserSchema)
</code></pre>
<ol>
<li>mongoose를 불러온 후, mongoose 안의 Schema를 가져온다.</li>
<li>그 후 <code>new Schema</code>를 통해 Schema를 구성해 준다.</li>
<li>그 후 생성해준 Schema를 사용해서 <code>mongoose.model(&#39;모델명&#39;, 사용할 스키마)</code>를 통해 모델을 생성해줍니다.</li>
</ol>
<hr>
<blockquote>
<h4 id="🔍-검색하기">🔍 검색하기!</h4>
</blockquote>
<blockquote>
<h4 id="조건에-맞는-모든-값을-찾기">조건에 맞는 모든 값을 찾기</h4>
</blockquote>
<pre><code class="language-js">userModel.find({user_id: &#39;admin&#39;}).then((docs) =&gt; {
    console.log(docs)
  })</code></pre>
<p>또는</p>
<blockquote>
<pre><code class="language-js">userModel.find({user_id: &#39;admin&#39;}, (err,docs) =&gt; {
    console.log(docs)
  })</code></pre>
</blockquote>
<pre><code>
&gt; #### 조건에 맞는 값 하나만 찾기
```js
userModel.findOne({user_id: &#39;admin&#39;}).then((docs) =&gt; {
    console.log(docs)
  })</code></pre><blockquote>
<h4 id="id-값으로-검색하기">id 값으로 검색하기</h4>
</blockquote>
<pre><code class="language-js">userModel.findById(&quot;621b574ff0bfe6d32b330505&quot;).then((docs) =&gt; {
    console.log(docs)
  })</code></pre>
<hr>
<blockquote>
<h4 id="💾-도큐먼트-추가--값-저장-">💾 도큐먼트 추가 ( 값 저장 )</h4>
</blockquote>
<blockquote>
<h4 id="모델-인스턴스-생성-후-추가하기">모델 인스턴스 생성 후 추가하기</h4>
</blockquote>
<pre><code class="language-js">  const userInfo = {
    user_id: req.body.user_id,
    salt: salt,
    password: hash.cryptoPassword(req.body.password, salt),
    email: req.body.email,
  }
  const newUser = await new userModel(userInfo)
  newUser.save((err) =&gt; {
    if (err) {
      console.log(&quot;POST : 유저 생성 실패&quot;)
      return res.status(500).json({
        message: `create failed: ${err}`
      })
    }
    console.log(newUser)
    res.status(200).json({
      message: &quot;create succeed&quot;
    })
  })</code></pre>
<blockquote>
<h4 id="모델-인스턴스를-생성하지-않고-모델을-이용하여-추가하기">모델 인스턴스를 생성하지 않고 모델을 이용하여 추가하기</h4>
</blockquote>
<pre><code class="language-js">  const userInfo = {
    user_id: req.body.user_id,
    salt: salt,
    password: hash.cryptoPassword(req.body.password, salt),
    email: req.body.email,
  }
userModel.create(userInfo, (err) =&gt; {
    if (err) {
      console.log(&quot;POST : 유저 생성 실패&quot;)
      return res.status(500).json({
        message: `create failed: ${err}`
      })
    }
    console.log(userInfo)
    res.status(200).json({
      message: &quot;create succeed&quot;
    })
  })</code></pre>
<hr>
<blockquote>
<h4 id="🗑-도큐먼트-제거--삭제-">🗑 도큐먼트 제거 ( 삭제 )</h4>
</blockquote>
<blockquote>
<pre><code class="language-js">  const post = await postModel.findOne({title: req.params.title})
  if (!post) {
    console.log(&quot;삭제할 게시글이 없습니다. &quot;)
    return res.status(500).json({message: &quot;None Data&quot;})
  }
  postModel.deleteOne(post, (err) =&gt; {
    if (err) {
      return console.log(err);
    }
    console.log(&quot;삭제 성공&quot;)
  })</code></pre>
</blockquote>
<pre><code>
---

&gt; #### 🛠 도큐먼트 수정 ( Update)

&gt;```js
postModel.updateOne(post, updatePost, (err) =&gt; {
    if (err) {
      console.log(`포스트 수정 실패 =&gt; ${err}`)
      return res.status(500).json({message: &quot;Update Failed&quot;})
    }
    console.log(&quot;포스트 수정 성공&quot;)
    res.status(200).json({
      message: &quot;Update Success&quot;,
      data: {updatePost}
    })
    }</code></pre><hr>
<blockquote>
<h4 id="🧵-mongoose-populate--innerjoin-">🧵 Mongoose Populate [ INNERJOIN ]</h4>
</blockquote>
<blockquote>
<h5 id="schema-설정">Schema 설정</h5>
</blockquote>
<pre><code class="language-js">const postSchema = new Schema({
  title: {
    type: String,
    required: true,
  },
  body: {
    type: String,
    required: true,
  },
  author: {
    type: Schema.Types.ObjectId,
    ref: &quot;users&quot;,
    index: true,
    required: true,
  },
  time: {
    type: Date,
    default:getDate()
  }
  ,
})</code></pre>
<p>위 코드의 <code>author</code> 부분에 <code>type</code>이 <code>Schema.Types.ObjectId</code> 인 것을 볼 수 있습니다.
이건 <code>users</code>모델의 <code>id</code>를 가져오기 위한 타입 설정이며, 이를 위해 <code>ref</code>에 연결한 모델의 이름을 작성해줍니다.</p>
<blockquote>
<h5 id="populate-전-데이터">populate 전 데이터</h5>
</blockquote>
<pre><code class="language-js">{
_id: &quot;621b827c92b350447277a06e&quot;,
title: &quot;수정 테스트&quot;,
body: &quot;성공해람&quot;,
author: &quot;621b574ff0bfe6d32b330505&quot;,
time: &quot;2022-02-28T20:14:57.865Z&quot;,
__v: 0
},</code></pre>
<p> populate 하기 전 데이터를 확인해보면 <code>author</code> 안에 <code>ObjectId</code>타입으로 데이터가 들어가 있는 것을 확인할 수 있습니다.
이 데이터를 이제 사용할 수 있도록 펼치기 위해 populate를 사용하게 됩니다.</p>
<blockquote>
<h5 id="populate-하는-법">populate 하는 법</h5>
</blockquote>
<pre><code class="language-js">  try {
    const user = await userModel.find({user_id: req.params.author})
    postModel.find({author: user}).populate(&#39;author&#39;).then((post) =&gt; {
      res.status(200).json(post)
    })
  } catch (e) {
    console.log(`검색 실패 =&gt; ${e}`)
    res.status(500).json({message: &quot;search Failed&quot;})
  }</code></pre>
<blockquote>
<h5 id="populate-사용-후-데이터-모습">populate 사용 후 데이터 모습</h5>
</blockquote>
<pre><code class="language-js">{
_id: &quot;621b827c92b350447277a06e&quot;,
title: &quot;수정 테스트&quot;,
body: &quot;성공해람&quot;,
author: {
_id: &quot;621b574ff0bfe6d32b330505&quot;,
user_id: &quot;admin&quot;,
password: &quot;30b55922e2cd2752a859b4e2294b10cd47192c5c8f9174b39c4aaf28837d16de&quot;,
salt: &quot;b5676ff2b24934a33005986e88ce838fdba6197cdef40dfbf23fa40ad36ddafc&quot;,
email: &quot;test@example.ocm&quot;,
__v: 0
},
time: &quot;2022-02-28T20:14:57.865Z&quot;,
__v: 0
}</code></pre>
<hr>
]]></description>
        </item>
    </channel>
</rss>