<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Quartz.log</title>
        <link>https://velog.io/</link>
        <description>Code what we love. 좋아하는 것들을 구현하고 있는 프론트엔드 개발자입니다. 사용자도 함께 만족하는 서비스를 만들고 싶습니다.</description>
        <lastBuildDate>Sun, 29 Jan 2023 09:26:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Quartz.log</title>
            <url>https://velog.velcdn.com/images/skyu_dev/profile/a2e8be91-8fc0-44f4-8e70-18248a0ec765/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Quartz.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/skyu_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Cache API] 프론트엔드의 자체적인 HTTP caching 구현 및 만료 일자 지정하기 (feat. 서버의 response headers를 변경할 수 없을 때)]]></title>
            <link>https://velog.io/@skyu_dev/Cache-API-%EC%84%9C%EB%B2%84-%EC%9D%91%EB%8B%B5response%EC%9D%98-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%BA%90%EC%8B%B1%ED%95%98%EC%97%AC-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%A4%84%EC%97%AC%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@skyu_dev/Cache-API-%EC%84%9C%EB%B2%84-%EC%9D%91%EB%8B%B5response%EC%9D%98-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%BA%90%EC%8B%B1%ED%95%98%EC%97%AC-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%A4%84%EC%97%AC%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 29 Jan 2023 09:26:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 개인적인 <a href="https://seokyoungyou.github.io/DDobok-frontend/">주짓수 도복 큐레이션 웹 앱</a>의 개발 과정을 담았으며, 정보의 오류가 있다면 댓글로 알려주시면 감사하겠습니다.</p>
</blockquote>
<h1 id="0-introduction">0. Introduction</h1>
<p>백엔드로부터 리소스를 받기 위해 API 통신을 하다보면 이미 존재하는 데이터를 불필요하게 재요청하는 경우가 있다. 이 경우 서버의 부하를 높일 뿐만 아니라 프론트 단에서도 서버의 응답을 매번 기다려야 하므로 웹 성능이 저하되는 문제점이 발생한다. 필자는 아래의 웹 페이지에서 도복의 정보와 이미지를 응답받는 경우 2 가지의 개선점이 필요하다고 느꼈고 캐싱 기술을 도입하기로 하였다.</p>
<blockquote>
<ol>
<li>사용자가 색상/가격 필터를 변경할 때마다 <code>GET</code> 요청이 매번 발생한다.</li>
<li>용량이 비교적 큰 이미지 데이터를 매번 요청하면 웹 로딩 성능이 좋지 않을 것이다.</li>
</ol>
</blockquote>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/27dd5afa-42c6-46fe-8c29-bf5a6e712b8e/image.png" alt=""></p>
<h2 id="01-http-caching-이란">0.1 HTTP caching 이란?</h2>
<p><strong>캐싱(Caching)</strong>은 주어진 <strong>리소스의 복사본을 저장</strong>하고 있다가 요청 시에 서버로부터 리소스를 다시 다운받지 않고 해당 복사본을 반환하는 기술이다. 이를 활용하면 <strong>서버의 부하를 완화</strong>하고, <strong>리소스가 클라이언트에 더 가깝게 존재</strong>하므로 회신에 더 적은 시간이 소요되어 성능이 향상될 수 있다. <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Caching">HTTP 캐싱</a>에서는 일반적으로 <code>GET</code>에 대한 응답을 캐싱한다.</p>
<h2 id="02-본-프로젝트의-캐싱-목표">0.2 본 프로젝트의 캐싱 목표</h2>
<p>본 프로젝트의 리소스는 개발자가 <code>admin</code> 페이지에서 변경하고 있으므로 정보의 실시간 업데이트가 비교적 중요하지 않다. 또한 리소스 변경 빈도도 낮으므로 사용자가 하루 단위로 새로운 데이터를 전달받는 것도 적절하다고 판단하였다.</p>
<ul>
<li>리소스 복사본이 1 일이 지나면 만료(<code>stale</code>)되었다고 판단하여 서버에 요청을 보낸다.</li>
<li>도복 정보 필터링에 의한 잦은 HTTP 요청을 줄이자.</li>
</ul>
<h1 id="1-http-cahcing-구현하기">1. HTTP cahcing 구현하기</h1>
<h2 id="11-누가-캐싱-유효-기간을-결정할-것인가">1.1 누가 캐싱 유효 기간을 결정할 것인가?</h2>
<p>일반적인 HTTP caching은 프론트와 백엔드 간 사전에 캐싱 규약을 정하고, 서버에서 응답을 보내줄 때 <code>Cache-Control</code> 헤더에 따라 리소스의 유효 기간을 전달한다. 리소스가 아직 신선하다면 브라우저는 <strong>서버에 요청을 보내는 것이 아니라 디스크/메모리</strong>에서만 캐시를 읽어와 사용한다. 본 프로젝트의 서버 응답은 아래와 같이 2 가지로 나눌 수 있다.</p>
<h3 id="예시-1-response-header를-통한-http-caching">예시 1: Response Header를 통한 HTTP caching</h3>
<p>첫 번째 예시는 서버로부터 받아오는 이미지 리소스의 응답에 <code>Cache-Control</code>이 이미 되어 있는 경우이다. 백엔드에서 이미지 저장소로 AWS S3 버킷을 사용하고 있는데 자동으로 <code>Cache-Control</code> 헤더를 아래와 같이 1 일로 붙여주고 있다고 한다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/f01f9cc3-dc1c-4085-bf2f-65931a8408d2/image.png" alt=""></p>
<p>따라서 좌측의 최초 페이지 로딩 시에는 서버의 응답을 일일히 받아오지만, 우측에서는 이미지가 메모리에 캐싱되어 리소스 로딩 시간이 단축된 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/bde32ddd-e1a1-4902-ae57-1fc816d4d4df/image.gif" alt=""></p>
<h3 id="예시-2-프론트엔드에서-http-caching-작업이-필요">예시 2: 프론트엔드에서 HTTP caching 작업이 필요</h3>
<p>두 번째 예시에서는 응답 헤더에 캐싱과 관련된 정보가 들어있지 않다. 따라서 좌측의 Name에서 확인할 수 있듯이 요청이 중복으로 발생되고 있어 캐싱이 필요하다. 이 경우 &lt;예시 1&gt;처럼 서버 측에 헤더를 추가해달라고 이야기할 수 있으나, 본 프로젝트에서는 <strong>프론트엔드에서 자체적으로 캐싱을 구현</strong>해보기로 하였다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/2914abbe-656b-4a0e-a87d-246e6a59f356/image.png" alt=""></p>
<h2 id="12-어떤-저장소를-사용할-것인가">1.2 어떤 저장소를 사용할 것인가?</h2>
<p><a href="https://han41858.tistory.com/54">웹 브라우저는 많은 형태의 저장소</a>를 지원한다. 프론트엔드 개발자라면 local storage, session storage가 친근하겠지만 이들은 5 MB 크기의 문자열만 저장 가능하므로 HTTP 캐싱에 사용하기에는 제약이 많다. 필자는 <a href="https://github.com/wanted-pre-onboarding-team-7/pre-onboarding-8th-3-7/wiki/Assignment-3-1-%7C-API-%EB%A1%9C%EC%BB%AC-%EC%BA%90%EC%8B%B1-%EA%B5%AC%ED%98%84#-localcacheservice-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%97%90%EC%84%9C-api-%EB%A1%9C%EC%BB%AC-%EC%BA%90%EC%8B%B1-%EA%B5%AC%ED%98%84">타 팀프로젝트에서 유사한 문제를 해결하기 위해 Map 객체를 활용하여 로컬 캐싱을 구현한 경험</a>이 있다. 그러나 이 예시에서 캐시의 유효 기간은 세션이 종료되기 전까지가 되므로 본 프로젝트의 유효 기간 1 일 조건을 만족할 수 없다.</p>
<p>공식 문서에 따르면 네트워크 요청(request url)에 따라 리소스를 캐싱할 경우 <a href="https://developer.mozilla.org/ko/docs/Web/API/Cache">Cache Stroage API</a>가 적합하다고 한다. </p>
<blockquote>
<p>다음 장부터는 캐시 저장소를 활용하여 서버측 headers의 영향을 받지 않고 리소스를 캐싱하는 과정을 코드와 함께 설명해보려 한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/3120f1b2-5b33-4eac-96cd-44b3c7617008/image.png" alt=""></p>
<h1 id="2-cache-api를-사용하여-자체적으로-http-캐싱하기">2. Cache API를 사용하여 자체적으로 HTTP 캐싱하기</h1>
<h2 id="20-필터링-요청을-최소화하는-쿼리-고민하기">2.0 필터링 요청을 최소화하는 쿼리 고민하기</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/627248d6-0681-4aaa-997c-08e28c38aae1/image.png" alt=""></p>
<p>본 프로젝트에서는 도복 데이터를 색상과 가격으로 필터링하는 로직을 서버에 쿼리를 요청해 구현하고 있다. 모든 필터링 경우의 수<code>(ex. color=Black&amp;color=Blue&amp;color=White&amp;max_price=150000)</code>를 각각의 쿼리로 보낸다면 캐싱의 장점이 저하되므로, 필자는 <strong>색상 1 개 이하 * 가격 1 개 이하 조합</strong>의 쿼리<code>(ex. color=Black&amp;max_price=150000)</code>를 사용하기로 하였다. </p>
<blockquote>
<p>따라서 캐싱을 사용한다면 <strong>필터링에 대한 요청은 1 일 최대 30 개</strong>(색상 6 가지 * 가격 5 가지)로 제한할 수 있다.</p>
</blockquote>
<p>사용자가 필터를 클릭하면 color와 price 두 개의 <code>atom</code>에 해당 정보가 저장되고, 쿼리 생성 로직은 <code>querySelector</code>에서 담당하도록 구현하였다(Recoil 사용). 해당 셀렉터는 <strong>서버에 요청할 문자열인 쿼리들을 배열</strong>에 담아 반환하게 된다.</p>
<pre><code class="language-js">export const querySelector = selector({
  key: &quot;querySelector&quot;,
  get: ({ get }) =&gt; {
    const colorQueries = get(colorQueryState);
    const priceQueries = get(priceQueryState);
    return combineQueries(colorQueries, priceQueries);
  },
});</code></pre>
<h2 id="21-요청-리소스-캐싱하기">2.1 요청 리소스 캐싱하기</h2>
<p>이제 30 개 요청을 Cache API 를 사용하여 캐싱해보자. 필자는 <code>CacheApiServer</code> 클래스 내에 정적 메소드인 <code>getGisByQuery</code>를 구현하였다. <code>caches.match()</code> 메소드를 사용하여 해당 쿼리에 대한 캐시를 찾아 <strong>캐시가 존재하면 리턴하고 없으면 서버로부터 데이터를 요청</strong>받아 사용하는 간단한 로직이다.</p>
<pre><code class="language-js">export class CacheApiServer {
  private static giCacheStorage = CACHE_STORAGE.GIS;

  static async getGisByQuery(query: string) {
    const url = `${BASE_URL}/gis/${query}`; // 쿼리 url 생성
    const cache = await caches.open(this.giCacheStorage); // 캐시 저장소 열기

    return await this.getValidResponse(cache, url);
  }

  private static async getValidResponse(cache: Cache, url: string) {
    const cacheResponse = await caches.match(url); // 요청 쿼리와 일치하는 캐시 응답 가져오기

    return cacheResponse 
      ? await cacheResponse.json() // 응답이 존재하면 반환
      : await this.getFetchResponse(cache, url); // 존재하지 않으면 서버에 요청
  }

  private static async getFetchResponse(cache: Cache, url: string) {
    const fetchResponse = await fetch(url); 
    cache.put(url, fetchResponse.clone()); // 캐시 저장

    return fetchResponse.json();
  }

  }
}</code></pre>
<p>크롬의 개발자 도구에서 캐시 저장소에 쿼리에 따라 응답이 저장되어 있는 모습을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/12558c7d-794f-4dc3-b8bd-6193aeb761ed/image.png" alt=""></p>
<h2 id="22-유효-기간-지정하기">2.2 유효 기간 지정하기</h2>
<p>그 다음으로 저장한 캐시의 유효 기간을 지정해보자. 캐시 저장소와 함께 활용할 수 있든 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Worker API</a>를 사용하면 <a href="https://developer.chrome.com/docs/workbox/modules/workbox-expiration/">ExpirationPlugin</a>을 활용하여 캐시의 생명 주기를 지정할 수 있다고 한다. 그러나 본 프로젝트는 규모가 크지 않으므로 플러그인을 사용하지 않고 <strong><code>fetchResponse</code>에 응답 시간 정보를 담은 <code>fetch-date</code> <a href="https://gomakethings.com/how-to-set-an-expiration-date-for-items-in-a-service-worker-cache/">헤더를 직접 추가하여 캐시를 저장하는 방법</a>을 사용</strong>하였다.</p>
<pre><code class="language-js">  const HEADER_FETCH_DATE = &quot;fetch-date&quot;;

  private static async getResponseWithFetchDate(fetchResponse: Response) {
    const cloneResponse = fetchResponse.clone();
    const newBody = await cloneResponse.blob();
    let newHeaders = new Headers(cloneResponse.headers);
    newHeaders.append(HEADER_FETCH_DATE, new Date().toISOString());

    return new Response(newBody, {
      status: cloneResponse.status,
      statusText: cloneResponse.statusText,
      headers: newHeaders,
    });
  }
</code></pre>
<p>캐싱된 데이터의 만료 여부를 판단하는 <code>isCacheExpired</code> 로직은 아래와 같다.</p>
<pre><code class="language-js">  const ONE_DAY_MILISECOND = 60 * 60 * 24 * 1000;

  private static isCacheExpired(cacheResponse: Response) {
    const fetchDate = new Date(
      cacheResponse.headers.get(HEADER_FETCH_DATE)!
    ).getTime();
    const today = new Date().getTime();

    return today - fetchDate &gt; ONE_DAY_MILISECOND;
  }
</code></pre>
<h2 id="23-캐싱-서비스-사용하기">2.3 캐싱 서비스 사용하기</h2>
<p>최종적으로 응답된 도복 정보를 받는 <code>GiItems</code> 컴포넌트는 아래와 같이 구현되어 있다. 사용자가 선택한 필터값이 바뀔 때마다 <code>querySelector</code>는 <strong>서버에 요청할 문자열인 쿼리들을 배열</strong>에 담아 반환하고, <code>useEffect</code>의 의존성에 의해 <code>loadData</code>가 실행된다. 이 때 <code>CacheApiServer.getGisByQuery</code> 메소드를 사용하여<code>queries</code> 배열의 각각의 응답을 순차적으로 가져오게 되며, 모든 응답을 기다리기 위해 해당 로직을 <code>Promise</code>로 감싸는 비동기처리를 진행했다. <code>loadData</code> 내의 로직이 마무리되면 마지막으로 <code>gis</code> 상태가 업데이트된다.</p>
<pre><code class="language-js">  const queries = useRecoilValue(querySelector); // 사용자가 필터에서 선택한 쿼리
  const [gis, setGis] = useState&lt;Gis&gt;([] as Gis);

  const loadData = useCallback(async () =&gt; {
    let results: Gis = [];

    const promises = queries.map(async (query) =&gt; {
      const result = await CacheApiServer.getGisByQuery(query); // query에 대한 응답 가져오기(cach / api)
      results.push(...result);
    });

    await Promise.all(promises);

    setGis(results);
  }, [queries]);

  useEffect(() =&gt; {
    loadData();
  }, [queries, loadData]); //쿼리가 바뀔 때마다 새로운 도복 데이터 로딩
</code></pre>
<h1 id="3-결과물-및-마무리">3. 결과물 및 마무리</h1>
<p>최종 데모는 아래와 같다. 좌측의 페이지로 처음 진입한 경우 서버로 각각의 요청을 발생시키지만, 우측의 경우 <strong>사용자가 필터링을 변경하여도 서버로 요청을 보내지 않고 브라우저의 Cache Storage에서 해당 응답을 가져오게 된다.</strong></p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/2e05a369-8109-4953-8757-60afe889a0f4/image.gif" alt=""></p>
<p>캐싱이라는 개념이 단순히 서버 요청을 줄이기 위한 기술이라고 생각하였는데, 리소스를 클라이언트단에 더 가깝게 두어 웹 성능을 높일 수 있다는 점을 새롭게 배울 수 있었다. 실무에서는 <code>Cache-Control</code> 헤더를 사용하는 경우가 많겠지만, 서버 단의 코드를 변경할 수 없다는 특수한 상황을 상정하고 나니 브라우저 (저장소)에 대해 더 깊은 이해를 해볼 수 있어 뜻깊었다. 잘 구축되어있는 라이브러리와 패키지를 활용하는 것도 좋지만 기초 뼈대 직접 만들어보는 것도 좋은 경험이었다고 생각한다.</p>
<p>전체 코드는 <a href="https://github.com/SeokyoungYou/DDobok-frontend">깃허브 저장소</a>에서 확인하실 수 있습니다.</p>
<h4 id="참고-자료">참고 자료</h4>
<ul>
<li><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Caching">MDN: HTTP 캐싱</a></li>
<li><a href="https://han41858.tistory.com/54">웹 스토리지 : 무엇을 써야할까요?</a></li>
<li><a href="https://tech.osci.kr/2022/07/13/react-query/">React-Query 도입을 위한 고민 (feat. Recoil)</a></li>
<li><a href="https://toss.tech/article/smart-web-service-cache">웹 서비스 캐시 똑똑하게 다루기</a></li>
<li><a href="https://www.youtube.com/watch?v=HiBDZgTNpXY">영상: Everything you need to know about HTTP Caching</a></li>
<li><a href="https://springfall.cc/post/7">[Javascript] 비동기, Promise, async, await 확실하게 이해하기</a></li>
<li><a href="https://gomakethings.com/how-to-set-an-expiration-date-for-items-in-a-service-worker-cache/">How to set an expiration date for items in a service worker cache</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Refactoring] 프론트엔드의 AuthService에 Class, Context API, Custom hook을 첨가하여 객체 지향적으로 만들기]]></title>
            <link>https://velog.io/@skyu_dev/Refactoring-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%9D%98-AuthService%EB%A5%BC-Class-Context-API-Custom-hook%EC%9D%84-%EC%B2%A8%EA%B0%80%ED%95%98%EC%97%AC-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/Refactoring-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%9D%98-AuthService%EB%A5%BC-Class-Context-API-Custom-hook%EC%9D%84-%EC%B2%A8%EA%B0%80%ED%95%98%EC%97%AC-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 09 Jan 2023 10:45:47 GMT</pubDate>
            <description><![CDATA[<h1 id="introduction">Introduction</h1>
<p>최근 <a href="http://www.yes24.com/Product/Goods/89649360">리팩터링</a> 교재와 <a href="https://academy.dream-coding.com/courses/refactoring">강의</a>를 공부하면서 <strong>데이터와 로직을 함께 두는 class(in JS)를 사용하면 코드 응집도가 높아지고 유지 보수가 쉬워진다</strong>는 것을 배웠다. 필자는 대부분을 로직만 함수 추상화를 해두고 절차 지향적으로 코드를 짜왔기에 해당 부분을 사이드 프로젝트에 적용해야겠다고 생각하였다. 혼자 리액트 프로젝트에 클래스 적용을 도전하였을 때는 클래스 인스턴스를 어떻게 관리해야 하는지 감이 오지 않았는데, 운이 좋게도 원티드 프리온보딩 인턴십에서 이 부분을 다뤄주셔서 해당 강의를 참고하여 글을 작성하였다.</p>
<p>본 글에서는 아래와 같은 순서로 리팩토링이 이뤄진다.</p>
<p><strong>1. Auth 관련 데이터와 로직을 Class로 추출
2. Class의 method를 Context API를 통해 리액트 컴포넌트에서 공유
3. 리액트 컴포넌트단의 복잡하고 반복되는 로직들을 커스텀 훅으로 추출</strong></p>
<blockquote>
<p>이 글은 개인 기록 용도로 작성되었으며 잘못된 부분이 존재할 수도 있습니다.</p>
</blockquote>
<h1 id="auth-service-리팩토링하기">Auth Service 리팩토링하기</h1>
<p>본 프로젝트는 React Native 라이브러리를 사용하며 일부 패키지를 제외하고 리팩토링 로직은 React와 동일하다. 구현해야할 AuthService는 다음과 같다.</p>
<ul>
<li>사용자의 이메일/비밀번호 입력: firebase signUp/login 로직에 사용 ➡️ AsyncStorage(웹의 LocalStorage)에 이메일 저장</li>
<li>AsyncStorage에 저장되어있는 이메일: 앱 시작시 자동 로컬 로그인 &amp; firebase logout 로직에 사용</li>
</ul>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/2c90aa29-5570-4b8c-b245-6c6b646eb455/image.gif" alt=""></p>
<h2 id="1-class로-데이터와-로직-함께-두기">1. Class로 데이터와 로직 함께 두기</h2>
<p>먼저 <strong>Firebase 로그인에 필요한 데이터와 함수들을 모아 클래스로 추출</strong>한다. 클래스를 사용하면 함수를 필요할 때마다 import할 필요없이 <strong>데이터와 로직을 함께 가지고 다닐 수 있어 용이</strong>하다.</p>
<ul>
<li><strong>private field</strong>: <code>email</code><ul>
<li>클래스 외부에서는 필드값 변경이 불가능하게 <strong>캡슐화</strong>하였다.</li>
</ul>
</li>
<li><strong>public method</strong>: <code>email</code> <code>localLogin</code> <code>signUp</code> <code>login</code> <code>logout</code> <code>resetPassword</code><ul>
<li>로그인/회원가입/비밀번호 찾기 등의 <strong>firebase auth</strong> 함수와 관련된 로직</li>
</ul>
</li>
<li><strong>private method</strong>: <code>handleFetchUser</code> <code>saveAsyncStorageUser</code> <code>removeAsyncStorageUser</code><ul>
<li>response 데이터를 바탕으로 <strong>asyncStorage</strong>와 관련된 로직을 내부에서만 처리하게 하였다. </li>
<li>firebase와 asyncStorage 관련 함수를 따로 관리하고 싶어 추출했으나 지나친 면이 있어 <code>saveAsyncStorageUser</code> <code>removeAsyncStorageUser</code>는 다시 인라인하는 것이 좋아보인다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">export default class AuthService {
  #email;

  constructor() {
    this.#email = &quot;&quot;;
  }

  email() {
    return this.#email;
  }

  localLogin(email) {
    this.#email = email;
  }

  async signUp(input) {
    const signUpData = await createUserWithEmailAndPassword(
      authService,
      input.email,
      input.password
    );
    this.#handleFetchUser(signUpData);
  }

  async login(input) {
    const loginData = await signInWithEmailAndPassword(
      authService,
      input.email,
      input.password
    );
    await this.#handleFetchUser(loginData);
  }

  async logout() {
    authService.signOut();
    this.#email = &quot;&quot;;
    this.#removeAsyncStorageUser();
  }

  async resetPassword(email) {
    await sendPasswordResetEmail(authService, email);
  }

  async #handleFetchUser(fetchUser) {
    const { email, localId } = fetchUser._tokenResponse;
    const asyncUser = new AsyncFirebaseUser(email, localId);
    this.#email = email;
    this.#saveAsyncStorageUser(asyncUser.emailAndIdObj);
  }

  async #saveAsyncStorageUser(userData) {
    saveStorageFirebaseUser(userData);
  }

  async #removeAsyncStorageUser() {
    removeStorageFirebaseUser();
  }
}</code></pre>
<h2 id="2-context-api로-class-instance--method를-전달할-통로-만들기">2. Context API로 Class instance / method를 전달할 통로 만들기</h2>
<p>추출한 클래스의 메소드를 리액트 컴포넌트에서 사용하려면 이를 전달할 통로가 필요하다. Context API를 사용하여 <strong>Provider는 authService 인스턴스를 props로 주입받고, 해당 인스턴스의 method를 context로 보내준다.</strong> 이 때 context로 보내주지 않은 메소드는 접근이 불가하다. 사용 편의성을 위해 <code>useAuth</code> hook을 별도로 생성하였다.</p>
<pre><code class="language-jsx">import { createContext, useContext } from &quot;react&quot;;

const AuthContext = createContext(null);

export const useAuth = () =&gt; useContext(AuthContext);

export function AuthProvider({ authService, children }) {
  const email = authService.email.bind(authService);
  const localLogin = authService.localLogin.bind(authService);
  const login = authService.login.bind(authService);
  const signUp = authService.signUp.bind(authService);
  const logout = authService.logout.bind(authService);
  const resetPassword = authService.resetPassword.bind(authService);

  return (
    &lt;AuthContext.Provider
      value={{
        email,
        localLogin,
        login,
        signUp,
        logout,
        resetPassword,
      }}
    &gt;
      {children}
    &lt;/AuthContext.Provider&gt;
  );
}</code></pre>
<p>생성된 <strong>Provider 컴포넌트로 AuthService가 필요한 컴포넌트들을 감싼다.</strong> 해당 프로젝트에서는 Redux로 추가 전역 상태관리를 진행 중인데 Context API가 아닌 Redux로 해당 코드를 변환할 수 있을지 추후 알아볼 예정이다.</p>
<pre><code class="language-jsx">
import { AuthProvider } from &quot;./context/AuthProvider&quot;;
import AuthService from &quot;./class/AuthService-firebase&quot;;

const authService = new AuthService();

export default function App() {
  return (
    &lt;AuthProvider authService={authService}&gt;
      &lt;Provider store={reduxStore}&gt;
        &lt;NavigationContainer&gt;
          &lt;StackNavigation /&gt;
        &lt;/NavigationContainer&gt;
      &lt;/Provider&gt;
    &lt;/AuthProvider&gt;
  );
}</code></pre>
<p>이제 Provider로 감싸진 컴포넌트들에서는 <code>useAuth</code>를 사용하여 간편하게 클래스 메소드를 활용할 수 있다.</p>
<pre><code class="language-jsx">import { useAuth } from &quot;../context/AuthProvider&quot;;

export default function Login() {

  const auth = useAuth();
  auth.login(input);

  ...
}</code></pre>
<h2 id="3-custom-hook으로-분리하기">3. Custom hook으로 분리하기</h2>
<p>필자의 경우 <strong>Form을 검증하고 제출하는 로직</strong>이 생각보다 길어져 해당 부분을 custom hook으로 분리하였다. 예를 들어 회원가입 로직은 다음과 같다.</p>
<ol>
<li>사용자로부터 받은 이메일/비밀번호를 검증한다.<ul>
<li>실패 시 오류 메시지를 사용자에게 출력한다.</li>
</ul>
</li>
<li>Firebase 회원가입을 시도한다.<ul>
<li>성공/실패에 대한 메시지를 사용자에게 출력한다.</li>
</ul>
</li>
<li>로그인 화면에서 설정 화면으로 이동한다.</li>
</ol>
<pre><code class="language-jsx">const useAuthForm = (navigation) =&gt; {
  const auth = useAuth();
  const [formMsg, setFormMsg] = useState(&quot;&quot;);

  const validateEmail = (email) =&gt; {
    if (!email || !email.includes(&quot;@&quot;)) {
      setFormMsg(LOGIN_MSG.EMAIL_ERR); 
      return false;
    }
    return true;
  };

  const useSignUpForm = async (input) =&gt; {
    if (!validateEmail(input.email) || !validatePassword(input.password)) { //1.
      return;
    }

    try {
      await auth.signUp(input); //2.
      setFormMsg(LOGIN_MSG.SUCCESS);
      navigation.navigate(SCREEN_NAME.SETTING); //3.
    } catch (error) {
      setFormMsg(`${LOGIN_MSG.FAIL}\n에러 코드:${JSON.stringify(error.code)}`);
    }
  };

 ...

  const resetFormMsg = () =&gt; {
    setFormMsg(&quot;&quot;);
  };

  return {
    formMsg,
    resetFormMsg,
    useSignUpForm,
    useLoginForm,
    useResetPassword,
  };
};

export default useAuthForm;</code></pre>
<p>해당 커스텀 훅을 사용하면 리액트 컴포넌트 내부를 아래와 같이 간결하게 유지할 수 있다. <code>handleSubmit</code> 함수를 객체로 만들어 <code>formState</code>에 따라 다른 커스텀 훅의 함수를 호출하게 하였다.</p>
<pre><code class="language-jsx">
export default function AuthSubmit({ formState, navigation, input }) {
  const { formMsg, resetFormMsg, useSignUpForm, useLoginForm, useResetPassword } = useAuthForm(navigation);

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

  const handleSubmit = {
    SIGN_UP() {
      useSignUpForm(input);
    },
    LOGIN() {
      useLoginForm(input);
    },
    REST_PASSWORD() {
      useResetPassword(input.email);
    },
  };

  return (
    &lt;&gt;
      &lt;TouchableOpacity
        onPress={handleSubmit[formState]}
        style={styles.submitBtn}
      &gt;
        &lt;Text&gt;{BTN_TEXT[formState]}&lt;/Text&gt;
      &lt;/TouchableOpacity&gt;
      &lt;Text style={styles.msg}&gt;{formMsg}&lt;/Text&gt;
    &lt;/&gt;
  );
}</code></pre>
<h1 id="후기--work-to-do">후기 &amp; Work to do</h1>
<p>본 글에서 모두 다루지는 않았지만 <strong>authService 인스턴스를 통해 앱 내에서 method 뿐만 아니라 <code>email</code> 데이터도 공유할 수 있어 코드 응집도가 상당히 높아졌다.</strong> 글로 정리하고 나니 리액트에 객체지향적 관점을 도입하는 것이 많이 복잡하지 않게 느껴진다. 리팩토링을 공부하고 난 뒤 코드를 보는 관점이 많이 좋아졌다. 함수를 작성할 때 목적과 의도를 한 번 더 고민하고, TypeScript가 도입되지 않은 JavaScript 리팩토링 시 interface를 구축하고 불변성을 유지하기 위해 객체 대신 class를 사용하는 것을 생각해보게 되었다.</p>
<p>앞으로는 여러 인스턴스 공유가 필요할 때 Provider를 여러 개 사용하는 것 외의 대안이 있는지에 대해 공부해볼 예정이다. 또한 커스텀 훅으로 내부 구현을 추상화하였으나 내부 구현에 대해 여전히 반복되는 로직이 남아있어 리팩토링을 더 진행할 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[주니어 웹 개발자의 4 개월만의 첫 앱 출시기 (feat. 구글의 전화)]]></title>
            <link>https://velog.io/@skyu_dev/%EC%A3%BC%EB%8B%88%EC%96%B4-%EC%9B%B9-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-4-%EA%B0%9C%EC%9B%94%EB%A7%8C%EC%9D%98-%EC%B2%AB-%EC%95%B1-%EC%B6%9C%EC%8B%9C%EA%B8%B0-feat.-%EA%B5%AC%EA%B8%80%EC%9D%98-%EC%A0%84%ED%99%94</link>
            <guid>https://velog.io/@skyu_dev/%EC%A3%BC%EB%8B%88%EC%96%B4-%EC%9B%B9-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-4-%EA%B0%9C%EC%9B%94%EB%A7%8C%EC%9D%98-%EC%B2%AB-%EC%95%B1-%EC%B6%9C%EC%8B%9C%EA%B8%B0-feat.-%EA%B5%AC%EA%B8%80%EC%9D%98-%EC%A0%84%ED%99%94</guid>
            <pubDate>Sun, 08 Jan 2023 14:05:11 GMT</pubDate>
            <description><![CDATA[<p><strong>들어가기 전에</strong></p>
<p>저의 앱 출시기에 많은 관심 가져주셔서 감사합니다. 지난 주말부터 갑자기 조회수가 늘더니 벨로그 이번 주 트렌딩이라니 감개무량하네요. 부족한 글이지만 읽어주셔서 감사합니다. 스크린샷은 제가 간직하기 위해 첨부합니다 ㅎㅎ
<img src="https://velog.velcdn.com/images/skyu_dev/post/1ae24ad5-42a4-43c5-a9ec-1604eceeef54/image.png" alt=""></p>
<hr>
<h1 id="0-introduction">0. Introduction</h1>
<p>2022 년 하반기 필자에게 가장 큰 이벤트였던 <strong>Post Black Belt</strong> 앱이 드디어 출시되었다. 본 글에서는 주니어 프론트엔드 개발자가 앱을 개발하게된 계기와 개발 과정 중에 느낀 점들을 종합한 1 인 개발기를 적어보려 한다. 힘든 과정이었지만 프론트엔드에 국한하여 나 자신을 가두었다면 경험하지 못했을 값진 경험들과 구글 플레이스토어로부터의 전화(..^^)를 받을 수 있어서 개발자 인생에서도 좋은 터닝 포인트였다. </p>
<ul>
<li>구글 플레이스토어 출시 링크: <a href="https://play.google.com/store/apps/details?id=com.quartz.postblackbelt">https://play.google.com/store/apps/details?id=com.quartz.postblackbelt</a></li>
<li>앱스토어 출시 링크(update: 2023.02.25) : <a href="https://apps.apple.com/us/app/post-black-belt-%EC%A3%BC%EC%A7%93%EC%88%98-%EB%8B%A4%EC%9D%B4%EC%96%B4%EB%A6%AC-%EC%9D%BC%EC%A7%80/id1673061463">https://apps.apple.com/us/app/post-black-belt-주짓수-다이어리-일지/id1673061463</a></li>
</ul>
<h2 id="01-self-introduction">0.1 Self introduction</h2>
<p>먼저 필자에 대해 간단한 소개를 하자면 아래와 같다.</p>
<blockquote>
<ul>
<li>2021 년 7 월부터 웹 개발(JavaScript) 입문</li>
</ul>
</blockquote>
<ul>
<li>기존 학업을 마무리짓고 2022 년 8 월부터 본격적으로 프론트엔드 개발 중</li>
</ul>
<p>본 앱을 처음 기획했을때 1 년<del>(심지어 학업과 병행하였기에 반쪽짜리..)</del>이라는 짧은 웹 개발 경력을 가지고 있어 커리어에 고민이 많았지만, 지금은 번듯한 앱 하나를 만들어 낸 것에 큰 자긍심을 가지고 있다. 이 글이 앱 개발을 고민해보고 있는 다른 웹 개발자 분들에게도 도움이 되길 바라며 본격적인 개발기를 적어보겠다. 구글의 전화가 궁금하신 분들은 글의 가장 하단부를 확인하러 가시면 된다.</p>
<h1 id="1-프로젝트-기획하기">1. 프로젝트 기획하기</h1>
<h2 id="11-ideation-주제-정하기">1.1 Ideation: 주제 정하기</h2>
<p>본래 이 프로젝트는 <strong>백엔드 개발자 1 명과 함께 기획한 웹 프로젝트</strong>였다. 요즘 필자는 주짓수라는 운동에 빠져있는데, 도장에 백엔드를 공부하고 있는 개발자 친구를 발견하여 함께 주짓수 기술을 분류하는 웹사이트를 만들어보기로 하였다.</p>
<p><a href="https://ko.wikipedia.org/wiki/%EB%B8%8C%EB%9D%BC%EC%A7%88_%EC%A3%BC%EC%A7%93%EC%88%98">주짓수(Jiu-jitsu)</a>는 일본의 유도를 브라질 격투술에 접목하여 관절 꺾기나 조르기 등을 이용하여 상대방을 제압하는 무술이다. 아래 영상은 상대방의 경동맥을 차단해 기절시키는 초크를 필자가 가장 좋아하는 선수가 구사하는 모습이다(<a href="https://gfycat.com/ko/gifs/search/rafa+mendes">출처 링크</a>). <del>이 영상을 보고 가슴이 두근거린 분들은 어서 도장에 등록하시고 앱을 다운받으시길 바란다.</del> </p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/a811e5e4-6c7d-4d04-9d1b-bae94e821ae9/image.gif" alt=""></p>
<p>주짓수에는 상대를 제압하는 다양한 기술들이 존재하며 <strong>인간 체스</strong>라는 별칭이 있을 정도로 나와 상대의 움직임에 따라 수많은 기술을 구사할 수 있다. 우리 팀은 통상적으로 주짓수의 가장 높은 단계로 여겨지는 블랙 벨트로 가기 위해 10 년이라는 긴 기간이 소요되는 요인 중 하나가 이 때문이라 생각하였다. 따라서 <strong>주짓수 수련자들의 운동을 돕는 서비스</strong>의 목적을 달성하기 위해 <strong>다양한 기술들을 일러스트레이트와 설명으로 분류하고 연관 영상들을 첨부</strong>하는 웹 사이트를 기획하게 되었다.</p>
<blockquote>
<p>참고 개발기: <a href="https://velog.io/@skyu_dev/%EA%B8%B0%ED%9A%8D1-%EC%B7%A8%EB%AF%B8%EB%A5%BC-%EC%BD%94%EB%93%9C%EB%A1%9C-%EC%A3%BC%EC%A7%93%EC%88%98-%EB%8F%84%EC%9E%A5-%EA%B4%80%EC%9B%90%EA%B3%BC-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%A0%9C%EC%9E%91%ED%95%98%EA%B8%B0-FE-%EC%B4%88%EC%95%88-%EC%A0%9C%EC%9E%91%EA%B8%B0">[PBB(1)] 취미를 코드로, 주짓수 도장 관원과 웹사이트 제작하기 &amp; FE 초안 제작기</a></p>
</blockquote>
<h2 id="12-pivoting-idea">1.2 Pivoting Idea</h2>
<p>그러나 첫 아이디어에는 <strong>두 가지 큰 문제점</strong>이 있었다.</p>
<blockquote>
<ol>
<li>두 개발자 모두 주짓수를 수련한지 1 년이 채 되지 않아 <strong>기술에 대한 이해도가 높지 않다.</strong></li>
<li>기술 별 일러스트와 설명에 대한 <strong>콘텐츠가 필요하다.</strong></li>
</ol>
</blockquote>
<p>첫 번째는 관장님의 도움을 받아 일정 부분 해결이 가능하지만 지속적으로 아이디어를 발전시키기에는 한계가 존재했다. 두 번째의 경우 지인의 소개를 받아 관련 일러스트를 그리시는 분을 찾았지만 주짓수 특성상 동일한 기술에도 주관적인 견해가 많이 주입되는 것에 어려움을 겪었다.</p>
<p>결국 우리는 기술 콘텐츠를 서비스에서 제공하는 것이 아니라 사용자가 직접 기록하는 방향으로 설정하여 <strong>주짓수 다이어리</strong>를 만드는 것으로 아이디어를 변경하였다. 본 서비스의 특별화된 점은 다음과 같다. 이해를 돕기위해 실제 서비스 스크린샷을 첨부하였다. <del>필자는 아직 화이트 벨트지만 예쁜 스크린 샷을 위해 퍼플 벨트를 잠시 꿈꾸어보았다.</del>
<img src="https://velog.velcdn.com/images/skyu_dev/post/e31dda7c-db4e-447d-871a-c75450e3ebf5/image.png" alt=""></p>
<blockquote>
<ol>
<li>사용자는 일기를 <strong>주짓수 카테고리(ex. 기술 연습, 대회 등)와 기술 카테고리(ex. 가드, 가드 패스 등)와 함께 작성하</strong>고 <strong>월간 달력 형식</strong>으로 확인한다.</li>
<li>작성된 일기를 <strong>기술 별로 분류</strong>하여 수련 기록을 돕고 <strong>기술 분포도</strong> 및 개인 정보를 통해 사용자의 수련 현황을 체크할 수 있다. </li>
</ol>
</blockquote>
<h2 id="13-pivoting-platform-and-team-1-인-개발-앱으로-전환">1.3 Pivoting Platform and Team: 1 인 개발 앱으로 전환</h2>
<p>아이디어를 변경하고 나니 일기장을 웹 페이지에서 작성하는 것에 대한 새로운 논점이 발생하였다. 일기는 사용자들이 자주 가지고 다니는 스마트폰에 앱으로 설치하는 것이 적합하는 의견이 모아졌고 여러 서치와 고민 끝에 <strong>React에서 React Native</strong>로 프론트엔드 라이브러리를 변경하기로 하였다. 참고 개발기를 보면 아직 React도 잘 다루지 못하는데 새로운 라이브러리를 학습하는 것에 두려움을 가진 필자의 모습을 확인할 수 있다. 결과적으로는 <strong>React와 유사하게 앱 개발</strong>을 할 수 있다는 큰 장점(<del>비슷하다고 했지 똑같다고는 안했다..</del>)을 직접 느껴볼 수 있었고, React Native만의 API를 사용하면서 비교를 통해 <strong>React를 더 깊게 이해하게 되는 좋은 계기</strong>가 되었다.</p>
<p>위와 같이 2022 년 9 월부터 아이디어를 확정하여 개발하기 시작하였으나 기쁘고 안타깝게도 함께 프로젝트를 하던 백엔드 개발자 친구가 교육 프로그램에 합격하여 함께하지 못하게 되었다. 따라서 필자는 따로 서버를 띄우지 않고 <strong>사용자의 기기에만 데이터를 저장</strong>하여 프론트엔드 개발에만 더 집중하기로 하였다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/08f8d024-9da8-485c-9bb2-9885fc1e66af/image.png" alt="">
 <del>그러나 개발하다보면 어쩔 수 없이 풀스택을 건드리게 되는 모습을 #2 장의 개발기에서 확인할 수 있다... To be continued</del></p>
<blockquote>
<p>참고 개발기: <a href="https://velog.io/@skyu_dev/Post-Black-Belt2-%EC%95%B1-%EB%A7%8C%EB%93%9C%EB%8A%94%EB%8D%B0-%EC%99%9C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EB%BD%91%EC%9D%84%EA%B9%8C-Web-App-%EA%B0%9C%EB%B0%9C%EA%B8%B0">[PBB(2)] 앱📱 만드는데 왜 프론트엔드 개발자를 뽑을까? | Web ➡️ Mobile App 전환하기</a></p>
</blockquote>
<h1 id="2-exporeact-navtive-앱-개발기">2. Expo(React Navtive) 앱 개발기</h1>
<h2 id="21-expo를-덧붙여-당장의-편리성을-취하자">2.1 Expo를 덧붙여 당장의 편리성을 취하자</h2>
<p>필자는 규모가 큰 기술을 새로 접할 때 학습 시간을 단축하기 위해 유튜브나 무료 강의들을 빠르게 수강한 후 프로젝트에 적용하는 편이다. React Native를 처음 배우기 위해 <a href="https://nomadcoders.co/react-native-for-beginners">노마드 코더의 무료 강의</a>를 2 일정도 수강하였고, React Native는 JavaScript 언어를 사용하여 앱 개발을 할 수 있지만 앱 개발자와 동일한 셋업을 모두 갖춰야 한다는 단점을 알게 되었다. 따라서 이를 보완하기 위해 인프라 구조가 갖춰진 <strong>Expo</strong>를 추가로 붙이기로 결정하였고, 빠르게 앱을 내 기기에 띄워볼 수 있었다.(고마워요 니꼬쌤!!)</p>
<h3 id="211-react-native는-html을-사용하지-않습니다">2.1.1 React Native는 HTML을 사용하지 않습니다</h3>
<p>당연한 이야기이지만 React Native는 React와 유사한 개발 경험을 가지게 만든 앱 개발 라이브러리이므로 우리(웹 개발자)가 사용하는 HTML 태그와 다른 이름들을 활용하게 된다. 아래의 코드는 <strong>간단한 버튼을 구현한 예시</strong>인데 컴포넌트에 글자를 넣기 위해서는 꼭 <code>Text</code> 태그 내에 넣어줘야 한다는 차이점이 있다. 또한 CSS도 웹과 동일하게 적용되지 않는 경우가 간혹 발생한다.</p>
<pre><code class="language-jsx">import { Text, TouchableOpacity, StyleSheet } from &quot;react-native&quot;;

export default function WideBtn({ children, backgroundColor, onPress }) {
  return (
    &lt;TouchableOpacity
      style={{ ...styles.btn, backgroundColor }}
      onPress={onPress}
    &gt;
      &lt;Text&gt;{children}&lt;/Text&gt;
    &lt;/TouchableOpacity&gt;
  );
}

const styles = StyleSheet.create({
  btn: {
    paddingHorizontal: 70,
    paddingVertical: 15,
    borderRadius: 10,
    alignItems: &quot;center&quot;,
  },
});
</code></pre>
<p>웹의 경우 <code>onClick</code>이라는 이벤트를 통해 대부분의 태그에서 발생하는 사용자의 클릭 이벤트를 받아올 수 있지만, React Native에서는 <code>TouchableOpacity</code> 등의 터치를 가능하게 해주는 컴포넌트로 감싸주어야 사용자의 터치 이벤트를 받아올 수 있다. 이 외에도 웹에서는 <code>&lt;a&gt;</code> 태그로 간편하게 다른 페이지로의 이동이 가능했지만, React Native에서는 <a href="https://reactnative.dev/docs/linking">Linking API</a>를 사용해야 앱 내에서 다른 URL로 이동이 가능하다.<del>React 쓸 때가 좋았다.</del></p>
<h3 id="212-stack이-뭐죠">2.1.2 Stack이 뭐죠?</h3>
<p>#2.1.1은 공식 문서에서 API 사용법을 익히면 해결되는 사소한 차이점들이었다. 필자가 앱 개발을 하며 가장 혼돈을 겪었던 부분은 <strong>웹의 Routing과 앱의 Stack Navigation의 차이</strong>다. 화면이 변경될 때 React에서 사용하던 <code>useEffect</code> hook이 예상처럼 적용되지 않는다는 것을 깨달았고, 화면을 이동하는 방식이 아래 그림과 같이 차이점을 갖는다는 사실을 알게되었다. 이 부분을 공부하면서 <strong>React의 라우팅 방식에 대해서도 더 면밀히</strong> 살펴볼 수 있게 되었고, <strong>패키지를 사용할 때 작동 방식을 이해하고 프로젝트에 적용</strong>해야 한다는 말을 개발기를 작성하며 몸소 실천하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/a83569e8-cec1-4966-be88-9233c8987c41/image.png" alt=""></p>
<blockquote>
<p>참고 개발기: <a href="https://velog.io/@skyu_dev/React-Navigation-Bottom-Tab%EC%9D%98-Require-cycle-warning-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0">[React Navigation] useEffect가 아닌 useFocusEffect 사용하여 stack 구조 화면 초기화하기</a></p>
</blockquote>
<h2 id="22-expo를-덧붙이면-패키지-사용이-쉬울수도-어려울수도-있습니다">2.2 Expo를 덧붙이면 패키지 사용이 쉬울수도 어려울수도 있습니다...</h2>
<p>노마드 코더 니꼬쌤의 강의에 따르면 React Native팀은 처음에 많은 컴포넌트와 API를 제공했다고 한다. 그러나 해당 기능들에서 버그가 많아지면서 서비스 규모를 줄이고 해당 부분들을 커뮤니티의 패키지에서 사용하는 것으로 변경하였다. 다행히도 <strong>Expo를 사용하게 되면 React Native에서 지원이 종료된 패키지들을 손쉽게 사용</strong>할 수 있다.</p>
<p>위의 장점도 있지만 나의 경험에서는 <strong>Expo SDK를 사용하는 경우 제한된 접근</strong>으로 인해 몇몇의 패키지를 사용하지 못하는 경우가 있었다. 따라서 관련 <strong>패키지를 검색할 때는 반드시 Expo를 함께 검색어에 입력</strong>하여 찾아야 한다. <em>그렇지 않으면 패키지를 다운받고 예시 코드를 열심히 읽어 구현한 후, 에러 메시지가 떠서 구글링하면 &quot;Expo는 지원하지 않는다&quot;는 친절한 후기들을 확인할 수 있다....</em></p>
<h3 id="221-expo의-좋은-예시-디바이스-로컬-저장소asyncstorage-sqlite">2.2.1 Expo의 좋은 예시: 디바이스 로컬 저장소(AsyncStorage, SQLite)</h3>
<p>필자는 사용자 정보를 로컬 디바이스에 저장하기 위해 Expo에서 지원하는 <a href="https://docs.expo.dev/versions/latest/sdk/async-storage/">AsyncStorage</a>를 사용하였다. 해당 API는 <strong>key-value 데이터를 웹의 Local Storage와 같이 저장</strong>할 수 있게 해주며, 본래 <a href="https://reactnative.dev/docs/asyncstorage">React Native에서도 지원</a>하였으나 현재는 지원이 종료되어 커뮤니티 패키지를 사용해야 하나 Expo에서는 공식으로 지원해주고 있다. 사용하는 방법도 아래와 같이 Local Storage와 유사하다.</p>
<pre><code class="language-js">import AsyncStorage from &quot;@react-native-async-storage/async-storage&quot;;

export const saveStorageData = async (key, value) =&gt; {
  try {
    const stringValue = JSON.stringify(value);
    await AsyncStorage.setItem(key, stringValue);
  } catch (e) {
    console.error(e.message);
  }
};

export const getStorageData = async (key) =&gt; {
  try {
    const value = await AsyncStorage.getItem(key);
    if (value !== null) {
      const data = JSON.parse(value);
      return data;
    }
  } catch (e) {
    console.log(e.message);
  }
};

export const removeStorageData = async (key) =&gt; {
  try {
    await AsyncStorage.removeItem(key);
  } catch (e) {
    console.error(e.message);
  }
};
</code></pre>
<p>본 프로젝트에서 웬만하면 백엔드 개발을 하지 않으려 했지만 일기 데이터를 저장하고 날짜 및 기술 별로 가져오기 위해서는 <strong>데이터를 표 형태로 저장하는 SQL DB</strong>가 필요했다. 따라서 Expo에서 제공하는 <a href="https://docs.expo.dev/versions/latest/sdk/sqlite/">expo-sqlite</a> 패키지를 사용하여 이를 비교적 간단하게(<del>필자에게는 간단하지 않았다. DB를 직접 구현하다니... 장족의 발전이다</del>) 구현할 수 있었다.</p>
<pre><code class="language-js">import * as SQLite from &quot;expo-sqlite&quot;;
export const db = SQLite.openDatabase(&quot;MainDB&quot;); // returns Database object

export const getDiaryById = (id, handleSuccess = printResult) =&gt; {
  try {
    db.transaction((tx) =&gt; {
      tx.executeSql(
        `SELECT ${TB.ALL} FROM ${TB_NAME} WHERE ${TB.ID} = ?`,
        [id],
        handleSuccess,
        handleError
      );
    });
  } catch (e) {
    console.log(e);
  }
};

export const saveNewDiary = (data, handleSuccess = printResult) =&gt; {
  try {
    db.transaction((tx) =&gt; {
      tx.executeSql(
        `INSERT INTO ${TB_NAME} (${TB.DATE}, ${TB.DIARY_CAT}, ${TB.TECH_CAT}, ${TB.TITLE}, ${TB.CONTENT}) values (?, ?, ?, ?, ?)`,
        [
          data.date,
          data.diaryCategory,
          data.techCategory,
          data.title,
          data.content,
        ],
        handleSuccess,
        handleError
      );
    });
  } catch (e) {
    console.log(e);
  }
};
</code></pre>
<h3 id="222-expo의-슬픈-예시-firebase-google-auth">2.2.2 Expo의 슬픈 예시: Firebase Google Auth</h3>
<p>사실 이 출시기에는 아주 슬픈 이야기가 포함되어 있다. 바로 앱을 처음 개발해본 필자가 <a href="https://firebase.google.com/docs/auth/web/google-signin">Firebase 구글 소셜 로그인</a>을 구현하려다가 앱의 key를 변경하여 잃어버렸기 때문이다. <strong>로그인이 필요했던 이유는 1. 앱을 삭제하면 일기 데이터가 삭제된다는  문제점을 해결하고 2. 추후 친구 기능을 추가해 사용자 유입 및 사용 시간을 증대</strong>하기 위해서이다.</p>
<p>해당 기능이 구현되지 않아 key store의 키를 마구 생성하다 key가 아예 변경되어 버리는 불상사가 일어났고, 사실 이 문제는 구글에 문의해 해결 가능하지만 설치 사용자수가 미미하고 앱의 패키지명 또한 변경해야 했기 때문에 <strong>앱을 재출시</strong> 하는 것으로 결정하였다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/6b2993c9-190e-42a6-b692-9b72b5b7702e/image.png" alt=""></p>
<p><a href="https://medium.com/nerd-for-tech/apple-google-authentication-in-expo-apps-using-firebase-997125440032">이 문제에 대해 설명한 글에 따르면</a> Firebase의 소셜 로그인은 팝업창이 뜨는 <code>signInWithPopup</code> 메소드를 사용하는데 <strong>해당 로직이 브라우저에서 실행되는 것으로 구현되어 React Native를 사용하면 문제가 발생</strong>한다고 한다. 따라서 Expo에서 지원하는 구글 로그인 패키지를 사용하여 계정 정보를 받아오고 해당 정보를 Firebase로 보내주는 방식을 채택하게 되는데, 많은 개발자들이 이 과정에서 iOS에서는 작동하지만 <strong>Android에서는 버그</strong>가 발생한다고 이야기하고 있다. 많은 검색을 통해 비교적 최근 <a href="https://velog.io/@g_c0916/react-native-Expo%EB%A1%9C-OAuth-%EA%B5%AC%ED%98%84%EA%B5%AC%EA%B8%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8-with-firebase">해당 부분을 구현한 다른 개발자 분의 글</a>을 발견하였고 앱의 slug, pacakge name, scheme을 일치시켜야 한다는 것을 알게되었다. </p>
<p>Expo에서는 OS에 따라 패키지 명에 <code>_</code> 나<code>-</code>를 지원하지 않는 경우가 있었는데 필자는 Expo 사용자명에 <code>_</code> 가 포함되어 있어 자동생성된 패키지명들이 불일치하고 있었다.  따라서 이 부분을 해결하기 위해 패키지명을 고쳐 앱을 재출시하게 되었다. 그러나 필자의 경우 development 단계에서는 구현이 되지만 production 모드에서는 계속 작동이 되지 않았고 커뮤니티에도 유사한 경험들이 많이 존재했다. </p>
<p>React Native 특히 Expo의 패키지를 사용할 때 이러한 자잘한 오류들이 많이 존재하지만 <strong>커뮤니티가 작아 해결되지 않고 남아있는 경우</strong>가 종종 있었다. 필자는 결국 소셜 로그인 대신 <strong>아이디-비밀번호 로그인 방식으로 선회하여 서버 데이터 동기화 기능을 추가</strong>할 수 있었다. <del>Expo 계정을 만들 때 언더바를 사용하지 말라는 경고는 없었으며... 소셜 로그인 기능을 구현하다 성격을 버릴 것 같아 차선책을 선택하였다</del></p>
<h1 id="3-google-play-store-출시">3. Google Play Store 출시</h1>
<h2 id="31-앱스토어는-나중에-출시하기로-하자">3.1 앱스토어는 나중에 출시하기로 하자</h2>
<p>React Native의 가장 큰 장점은 JavaScript 언어만으로 Android, iOS, Web 등 <strong>여러 플랫폼으로 앱을 빌드</strong>할 수 있다는 것이다. 필자는 MacBook을 사용하여 개발하고 있으므로 시뮬레이터 활용이 쉬운 iOS로 개발을 진행하였고, 실기기 검증은 안드로이드 스마트폰을 사용하였다. 따라서 두 운영체제 모두 앱 출시가 가능했으나 <strong>2022 년에는  Google Play Store에만 앱을 출시</strong>하기로 결정하였다.</p>
<p>첫 번째 이유는 개발 모드와 프로덕션 모드에서의 기능 작동 차이(#2.2.2)를 겪은 후 <strong>아이폰에서 검증 후 출시가 필요</strong>하다고 느꼈기 때문이다. 필자는 테스트용 iOS 실기기가 존재하지 않았으므로 2023 년에 아이폰으로 기기를 변경하면 앱스토어 출시를 도전해볼 계획이다. <del>절대 새로운 스마트폰을 구매하려는 수작이 아니다.</del> 두 번째 이유는 앱스토어는 앱 출시가 까다롭고 매년 99 달러를 지불해야 하기 때문이다. Google Play Store의 경우 최초 25 달러를 지불하면 개발자 등록이 완료되지만 앱스토어는 매년 개발자 등록비를 지불해야 앱을 운영할 수 있다. 필자는 아직 웹 개발을 주로 하기 때문에 앞으로 앱을 출시할 일이 많아지거나 안드로이드에서 Post Black Belt 앱의 이용자 수가 많아진다면 iOS 개발자 등록을 하는 것으로 선택하였다.</p>
<h2 id="32-첫-앱-출시-완료">3.2 첫 앱 출시 완료</h2>
<p>많은 고난과 역경을 겪은 뒤 드디어 Google Play Store에 앱을 출시할 수 있었다. 앱을 올리고 구글에서 검토하는 기간이 일주일 가량 소요되었는데 매일 Google Play Console을 드나들며 <code>검토중</code>이 바뀌기를 기다리고 있었던 것 같다. 앱을 재출시하면서 2022 년 내에 프로젝트를 마무리짓지 못하겠다는 생각도 있었는데 다행히 구글에서 크리스마스 연휴가 끝난 뒤 열일해주어 <strong>2022 년 12 월 30 일</strong>에 웹 개발자의 앱 출시기를 마무리할 수 있었다. </p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/d63a330a-d243-4a7e-ad9f-aba73804a4f8/image.png" alt=""></p>
<h1 id="4-후기">4. 후기</h1>
<h2 id="41-앱-개발-후기">4.1 앱 개발 후기</h2>
<p>필자가 만들고 싶은 프로젝트를 직접 구현해보며 정말 많은 경험을 했다. 그동안의 프로젝트는 클론 코딩이나 단발성으로만 사용하는 웹 사이트를 제작했었는데, 이번 프로젝트를 통해 <strong>실사용자에게 필요한 기능들을 생각해보고 구현해보는 것에 큰 성취감</strong>을 느꼈다. 첫 개인 프로젝트라 현 버전 1.1 에서는 의욕이 앞서 나열한 기능의 1/4 정도만 최종 구현된 것 같다. 앞으로는 <strong>테스트 코드를 적용하고 코드 리팩토링</strong> 후 기능을 더 붙여갈 예정이다.</p>
<p>또한 새로운 라이브러리인 React Native를 배우면서도 많은 것을 깨달았다. API와 패키지를 동영상 강의 없이 공식 문서를 한 줄 한 줄 읽어가며 직접 적용해보며 <strong>공식 문서의 중요성</strong>을 몸소 깨닫게 되었다. 또한 Expo를 사용하면서 개발 편의성을 위해 라이브러리/프레임워크를 도입하는 것의 장단점도 느꼈다. 그 중에서도 가장 크게 와닿은 점은 <strong>커뮤니티가 큰 라이브러리는 개발자에게 축복</strong>이라는 것이다. <del>따봉 React야 고맙다..</del></p>
<h2 id="42-실제-앱-사용기">4.2 실제 앱 사용기</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/ac97f219-a1bd-43f2-814c-3f5aa8011d25/image.png" alt=""></p>
<p>아직까지 앱의 실사용자는 필자뿐이다. 관원들과 지인들에게 앱을 소개해주고 있지만 아직 보완할 점이 많다고 생각해 홍보를 본격적으로 하지 않고 있었다. 이제 새해가 되었으니 새로운 마음으로 앱을 추천해주고 <strong>실사용자들에게 보완점</strong>을 직접 받아보려 한다. 실제로 필자는 주짓수 수련을 기록하면서 기술 실력이 많이 늘었으며 이번 달 한 그랄을 승급하였다! 🎉</p>
<h2 id="43-구글의-전화-후기">4.3 구글의 전화 후기</h2>
<p>이제 글의 제목과 썸네일에 적은 <strong>구글에서 온 국제 전화 후기</strong>를 이야기해보려 한다. 두 번째 앱이 출시되고 며칠이 되지 않아 구글에서 아래와 같은 메일을 보냈다. 메일의 내용은 <strong>2 영업일 이후에 전화</strong>를 할테니 준비를 해두라는 것이었다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/649eee32-ad3d-4c09-b5b0-7b70feb69636/image.png" alt=""></p>
<p>상세히 알아보니 2022 년 6 월부터 구글은 선별된 개발자들에게 앱에 관해 질문을 한다고 하며, Google Play 사용자의 안전을 위한 목적이라고 한다. 필자의 경우 동일한 앱을 2 번 출시하였으므로 해당 부분을 문제 삼을 것으로 예상하였고 당연히(..?) 구글 코리아에서 전화가 올 것으로 생각하여 크게 개의치 않았다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/f7083587-ca35-4d6b-b4ca-e0e15222fb6d/image.png" alt=""></p>
<p>그리고 메일이 온지 3 일 후 오전 10 시에 국제전화를 받게 된다.... <del>Hello...?</del> 핑계로 들릴 수는 있으나 필자는 정확히 오전 10 시부터 팀 미팅이 있었기에 빠르게 일을 해결하려는 생각으로 전화를 받았고 갑자기 쏟아지는 영어에 굉장히 당황하였다. <del>이름이 무엇이냐는 물음도 3 번 되묻고 알아들었다. 질문자분도 내 발음을 잘 못알아듣는 것 같았다..</del> 질문 내용은 아래와 같다.</p>
<blockquote>
<ul>
<li><strong>이름</strong> &gt; 본명을 대답</li>
</ul>
</blockquote>
<ul>
<li><strong>출시한 앱 이름</strong> &gt; Post Black Belt</li>
<li><strong>앱의 목적</strong> &gt; Diray for jiu-jitsu<ul>
<li>사실 주짓수 수련자라고 말하고 싶었는데 athlete밖에 떠오르지 않았다..</li>
</ul>
</li>
<li><strong>앱이 수익을 어떻게 창출하고 있는지?</strong> &gt; 현재는 수익 창출 계획이 없으며 나와 친구들을 위해 제작한 앱이다.<ul>
<li>이때부터 조금 정신을 차려 문장을 구사하였다.</li>
</ul>
</li>
</ul>
<p>필자의 경우 잘 들리지 않는 리스닝과 짧은 스피킹으로 대화를 하였으나 이 글을 읽고 구글의 전화를 받는 개발자 분들은 미리 준비하여 잘 대답하시기를 바란다. 동일 앱 재출시에 관한 질문은 없었으며 나의 앱에 대한 정보만 간단히 묻고 종료되었다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/223d8723-5677-4abb-ad2e-2cd48152d301/image.png" alt=""></p>
<p>전화를 받고 나니 캠블리 구독을 취소했던 지난날에 대한 후회가 극심하게 밀려왔다. 사실 한국어로도 앱을 소개해본 적이 없어 더 말이 안나왔던 것 같다. 돌이켜 생각해보면 필자가 웹 프론트엔드 개발에만 국한되어 있었다면 구글 플레이 스토어의 전화를 받을 기회도 없었을 것이라 생각한다. 구글의 전화는 <strong>2023 년을 시작하는 필자에게 좋은 개발 원동력</strong>이 되었다. 또한, 최근 취업을 준비하며 고민이 많은 상태인 필자에게 미래의 개발자 커리어에 대해 생각해보는 큰 자극제가 되었다.</p>
<p>앞으로 더 좋은 웹, 앱 개발을 통해 더 나은 회고록을 작성할 예정이다. 2023 년도 해피 코딩을 할 수 있기를!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] 팀프로젝트 특성에 맞는 브랜치 전략/보호 규칙/병합(Merge) 설계하기]]></title>
            <link>https://velog.io/@skyu_dev/Git-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%8A%B9%EC%84%B1%EC%97%90-%EB%A7%9E%EB%8A%94-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EB%B0%8F-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EB%B3%B4%ED%98%B8-%EC%A0%84%EB%9E%B5-%EC%84%B8%EC%9A%B0%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/Git-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%8A%B9%EC%84%B1%EC%97%90-%EB%A7%9E%EB%8A%94-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EB%B0%8F-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EB%B3%B4%ED%98%B8-%EC%A0%84%EB%9E%B5-%EC%84%B8%EC%9A%B0%EA%B8%B0</guid>
            <pubDate>Mon, 02 Jan 2023 15:49:11 GMT</pubDate>
            <description><![CDATA[<h1 id="0-introduction">0. Introduction</h1>
<p>처음으로 <a href="https://github.com/orgs/wanted-pre-onboarding-team-7/repositories">팀 프로젝트</a>에 참여하면서 코드 공유, Pull Request를 통한 협업, 브랜치 병합 등 Git을 제대로 활용하는 경험을 할 수 있었다. 첫 주차 미션이 종료되고 아래와 같이 2 가지의 문제점을 보완해야겠다는 생각이 들었고, 이를 정리한 해결 방안을 공유해보려 한다.</p>
<blockquote>
<p>첫 주차 미션에서 느낀 팀 코드 저장소의 문제점</p>
</blockquote>
<ol>
<li>프로젝트 성향을 고려한 브랜치 전략 부재</li>
<li>main, develop 등 공유 브랜치의 보호 규칙 부재</li>
</ol>
<h1 id="1-git-브랜치-전략-세우기">1. Git 브랜치 전략 세우기</h1>
<p>여러 개발자가 하나의 저장소를 사용하는 팀 프로젝트 환경에서 브랜치 관리를 효과적으로 하기 위하여 다수의 브랜치 전략들이 알려져있다. 본 글에서는 <a href="https://inpa.tistory.com/entry/GIT-%E2%9A%A1%EF%B8%8F-github-flow-git-flow-%F0%9F%93%88-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5">잘 알려져있는 브랜치 전략들</a>과 <a href="https://youtu.be/etnFe2tBD5I?t=1222">NHN 컨퍼런스에서 소개된 깃 워크플로 사례</a>를 참고하여 필자의 팀에 적합한 브랜치 전략을 세워나가는 과정을 이야기할 예정이다.</p>
<h2 id="11-팀-프로젝트-특성-분석하기">1.1 팀 프로젝트 특성 분석하기</h2>
<ul>
<li>구성원: 프론트엔드 개발자 7 명</li>
<li>코드 구현 기간: 미션당 4 일</li>
<li>프로젝트 진행 과정: 개인 개발 ➡️ 토론 ➡️ 베스트 코드 찾기 ➡️ 코드 통합하기 ➡️ 배포</li>
<li>특징: 팀원 각각 미션을 구현 후 토론을 통해 하나의 베스트 프로젝트를 만들어야 함</li>
</ul>
<p>본 팀 프로젝트는 4 일동안 주어진 과제를 구현하고 배포까지 해야 하는 단기간 프로젝트이다. 특이 사항은 기능을 나누어 개발하는 것이 아니라, 팀원 각각 완성된 프로젝트(practice)를 개발한 후 <strong>가장 좋은 코드(best practice)만 선별</strong>하여 하나의 프로젝트로 완성시켜야 하는 것이다. 첫 주차에서 브랜치 전략 부재의 문제점을 느낀 시점은 팀원 개별의 practice 코드를 관리할 때였다. 기존에는 팀 원격 저장소를 팀원들이 <code>fork</code>하여 practice 코드를 <code>develop</code> 브랜치로 Pull Request를 통해 공유하였다. 이 경우 팀 원격 저장소에서 각 코드를 보관하지 않아 PR이 close되면 확인하기 어렵다는 단점이 있었다. 또한 practice 코드와 best practice 코드 모두 <code>develop</code> 브랜치에 PR되어 이를 구분하기 어려웠다.</p>
<h2 id="12-적용하기-설계한-브랜치-전략-소개">1.2 적용하기) 설계한 브랜치 전략 소개</h2>
<p>따라서 새로운 브랜치 전략은 아래 두 가지의 큰 기준을 바탕으로 4 개의 브랜치와 서브 브랜치들로 구성하였다.</p>
<blockquote>
<ul>
<li>팀 원격 저장소를 <code>clone</code>하여 모든 코드를 팀 저장소에 공유</li>
</ul>
</blockquote>
<ul>
<li>main: 배포를 담당, develop: best practice 코드 담당, practice: 팀원 개별 개발 코드 담당</li>
</ul>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/a272fa4d-544f-439f-867e-8a20dc073f8e/image.png" alt=""></p>
<p>프로젝트 초기 설정부터 배포까지의 과정을 본 프로젝트의 브랜치 전략을 활용하여 설명하면 다음과 같다.</p>
<ol>
<li>프로젝트 초기 구조 &amp; CI 설정을 <code>main</code> 브랜치에서 완료한다.</li>
<li><code>practice</code> 브랜치를 분기하고, 팀원들은 <code>practice</code> 브랜치에서 각자의 이름/깃헙 아이디로 된<code>practice sub-branch</code>를 분기하여 practice 코드를 작성한다.</li>
<li><code>practice sub-branch</code>에서 개발이 완료되면 <code>practice</code> 브랜치로 PR을 올려 토론 후 best practice를 선정한다.</li>
<li>CD 설정을 마치고 <code>develop</code> 브랜치를 분기한다.</li>
<li><code>develop</code> 브랜치에서 기능/issue 별로 <code>develop sub-branch</code>를 분기하여 선정된 best practice를 바탕으로 코드를 작성한다.</li>
<li><code>develop sub-branch</code>에서 개발이 완료되면 PR을 통해 코드 리뷰를 받고, <code>develop</code> 브랜치에 코드를 merge하고 해당 브랜치를 삭제한다.</li>
<li>모든 best practice가 구현 완료되면 <code>main</code> 브랜치에서 <code>develop</code> 브랜치의 코드를 merge하여 자동 배포가 이루어지도록 한다.</li>
<li><code>main</code>의 배포된 코드에 문제가 생긴 경우 <code>hotfix</code> 브랜치를 생성하여 버그를 고친다.</li>
<li>버그가 고쳐지면 해당 코드를 <code>main</code> <code>develop</code> 브랜치로 merge하고 해당 브랜치를 삭제한다.</li>
</ol>
<h3 id="장점">장점</h3>
<ol>
<li>팀 저장소에서 모든 코드를 확인할 수 있다.</li>
<li>코드의 목적에 맞게 브랜치를 나눌 수 있다.<h3 id="단점">단점</h3>
</li>
<li>배포 후에도 여러 브랜치가 남아있어 혼란스러울 수 있다.<ul>
<li>팀원들의 개별 코드가 저장되어 있는 <code>practice sub-branch</code>들을 모두 살려두는 방식을 사용하여 나타난 단점이다.</li>
<li>개인의 코드만 따로 확인할 수 있으므로 포트폴리오로 활용 시 필요할 수도 있다.</li>
</ul>
</li>
<li>단기간 배포이므로 <code>hotfix</code> 브랜치 사용은 과도할 수 있다.<ul>
<li>팀원들과 상의 후에 전략을 수정해 볼 예정이다.</li>
</ul>
</li>
</ol>
<h1 id="2-git-브랜치-보호-규칙-세우기">2. Git 브랜치 보호 규칙 세우기</h1>
<h2 id="21-브랜치-보호-규칙의-필요성">2.1 브랜치 보호 규칙의 필요성</h2>
<p>팀 저장소를 운영하다보면 브랜치가 실수로 삭제되거나 PR이 아닌 다른 방식으로 커밋이 추가되는 등 예상치 못한 문제가 발생할 수 있다. 필자의 경우 팀원들의 브랜치 생성을 가능하게 하기 위해 팀 멤버 권한을 Read(clone, pull 가능)에서 Write(clone, pull, push 가능)로 변경하였다가 PR되지 않은 코드가 머지되어 보호 규칙의 필요성을 느끼게 되었다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/a00e6ecc-68b1-45af-9394-2bcb564aedf9/image.png" alt=""></p>
<p> Branch Protection Rule는 팀의 규칙으로 설정하거나 저장소별로 구분하여 설정할 수도 있다. <a href="https://docs.github.com/ko/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule">공식 문서</a>와 <a href="https://kotlinworld.com/292">다음 블로그 글</a>을 참고하여 본 프로젝트에서는 3 개의 브랜치(<code>main</code> <code>develop</code> <code>practice</code>)에 규칙을 설정해보려 한다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/d9e0cffe-f1f3-4656-b595-5f38af1e5545/image.png" alt=""></p>
<h2 id="22-적용하기-설정한-브랜치-보호-규칙-소개">2.2 적용하기) 설정한 브랜치 보호 규칙 소개</h2>
<h4 id="main">main</h4>
<ul>
<li>Require a pull request before merging: PR을 통해서만 코드 병합</li>
<li>Require status checks to pass before merging: 테스트 코드를 통과한 코드만 병합<h4 id="develop">develop</h4>
</li>
<li>Require a pull request before merging<ul>
<li>Require approvals(2): 2 명 이상의 승인이 있어야 병합 가능</li>
</ul>
</li>
<li>Require status checks to pass before merging</li>
<li>Require conversation resolution before merging: PR 코드 리뷰를 통해 생성된 conversation이 모두 solve되어야 병합 가능<h4 id="practice">practice</h4>
</li>
<li>Require a pull request before merging</li>
<li>Lock branch: 코드를 push하지 못하는 read-only 브랜치</li>
</ul>
<p><code>develop</code>은 팀의 주요 개발이 이루어지는 브랜치이므로 다른 브랜치보다 더 빡빡한 규칙을 두었다. 코드 병합을 위해서는 PR을 생성하여 2 명 이상의 코드 승인이 필요하고, 테스트 코드를 모두 통과해야 하며, PR 리뷰가 모두 해결되어야 한다. 조건이 달성되지 않으면 아래와 같이 merge 버튼이 비활성화되어 있는 것을 확인할 수 있다. 참고 사항으로 테스트 코드에 대한 CI는 <a href="https://velog.io/@skyu_dev/CI-ESLint-Prettier-Jest-test-%EC%9E%90%EB%8F%99%ED%99%94%EB%A1%9C-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%A7%80%EC%86%8D%EC%A0%81%EC%9C%BC%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-Husky-GitHub-Actions">GitHub Actions를 사용해 다음 글</a>에서 구현해두었다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/df3ec7d3-5d58-461f-bdc3-5ef941c4076c/image.png" alt=""></p>
<h1 id="3-merge-option-선정">3. Merge option 선정</h1>
<p>마지막으로 PR된 코드를 merge할 때 사용할 수 있는 세 가지 옵션을 알아보자. PR 페이지에서 아래와 같이 3 가지의 병합 방법을 선택할 수 있고, 자세한 개념은 <a href="https://docs.github.com/en/github-ae@latest/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges">공식 문서</a>와 <a href="https://im-developer.tistory.com/182">다음 블로그 글</a>에서 잘 설명해주고 있다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/0f700736-4f28-4843-9c00-7288e1ace00f/image.png" alt=""></p>
<p>각 팀원이 개발한 best practice의 서브 브랜치로부터 <code>develop</code> 브랜치에 출시할 코드를 병합하는 과정을 예를 들어보자. 일반 <code>merge</code> 옵션을 사용할 경우 <strong>서브 브랜치의 커밋들 참조 + 머지 커밋</strong>이 추가된다. <code>squash</code>옵션을 사용하는 경우 <strong>스쿼시 머지 커밋</strong>만 추가되어 <code>develop</code> 브랜치를 깔끔하게 관리할 수 있다. <code>rebase</code> 옵션을 사용하는 경우 <strong>서브 브랜치 커밋들</strong>만이 추가되어 마치 하나의 브랜치에서 작업한 것과 동일한 결과물을 얻을 수 있다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/b29ebc98-9928-451f-8ced-992bb71591a3/image.png" alt=""></p>
<p>다수의 개발자가 참여하는 팀 프로젝트의 경우 <code>squash</code>를 사용하여 주축이 되는 브랜치를 깔끔하게 관리한다고 한다. 지난 1 주차 미션의 경우에도 <code>main</code> 브랜치에 무려 128 개의 커밋이 쌓여 이력 확인이 어려운 문제가 있었다. <code>squash</code>를 사용하면 기능에 따라 커밋 개수를 줄일 수 있는 장점이 있으나 <a href="http://story.haezoom.com/?p=936">무한 merge commit 지옥</a>에 빠질 우려가 있다고 한다. 개인 개발단에서 <code>rebase</code> 등 명령어를 통해 커밋을 합치는 방법도 있다고 하니 이 옵션에 대해서는 팀프로젝트에 직접 적용해보며 결정하는 편이 낫다고 판단하였다.</p>
<h4 id="참고자료">참고자료</h4>
<ul>
<li><a href="https://inpa.tistory.com/entry/GIT-%E2%9A%A1%EF%B8%8F-github-flow-git-flow-%F0%9F%93%88-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5">(블로그) 깃 브랜치 전략 정리 - Github Flow / Git Flow</a></li>
<li><a href="https://youtu.be/etnFe2tBD5I?t=1222">(영상) NHN FORWARD 2019: &#39;깃&#39;깔나는 Git 워크플로 알아보기</a><ul>
<li>NHN에서 실제 사용하고 있는 깃 워크플로 사례 소개</li>
</ul>
</li>
<li><a href="https://nvie.com/posts/a-successful-git-branching-model/">(블로그) A successful Git branching model</a><ul>
<li>Git Flow 창시자의 문서</li>
</ul>
</li>
<li><a href="https://kotlinworld.com/292">(블로그) [GitHub] Branch Protection Rule 적용해 브랜치 보호하기</a></li>
<li><a href="https://docs.github.com/ko/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule">(공식 문서) 브랜치 보호 전략</a></li>
<li><a href="https://im-developer.tistory.com/182">(블로그) [Git] Merge 이해하기 (Merge / Squash and Merge / Rebase and Merge)</a></li>
<li><a href="https://velog.io/@code-bebop/Github-merge-squash-merge-rebase-merge">(블로그) Github - merge / squash merge / rebase merge</a></li>
<li><a href="https://docs.github.com/en/github-ae@latest/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges">(공식 문서) Pull Request Merge Options</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Jest] React Testing Library를 사용하여 DOM  테스팅하기]]></title>
            <link>https://velog.io/@skyu_dev/Jest-React-Testing-Library%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-DOM-%ED%85%8C%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/Jest-React-Testing-Library%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-DOM-%ED%85%8C%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 28 Dec 2022 04:13:52 GMT</pubDate>
            <description><![CDATA[<h1 id="0-introduction">0. Introduction</h1>
<p>Jest를 사용하면서 대부분 함수 단위의 유닛 테스트나 통합 테스트만 사용했었다. <a href="https://velog.io/@skyu_dev/CI-ESLint-Prettier-Jest-test-%EC%9E%90%EB%8F%99%ED%99%94%EB%A1%9C-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%A7%80%EC%86%8D%EC%A0%81%EC%9C%BC%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-Husky-GitHub-Actions">팀 단위 프로젝트의 CI/CD를 구상</a>하면서 리액트 컴포넌트가 잘 렌더링되고 있는지 확인이 필요했고, CRA에서 기본적으로 지원하는 <strong>React Testing Library</strong>를 사용해보기로 하였다.</p>
<h1 id="1-기본-개념">1. 기본 개념</h1>
<p>본 라이브러리를 학습하기 위해 <a href="https://youtu.be/K1w6WN7q6k8">코딩 앙마님의 React Testing Library 강의</a> 영상을 수강하였고, 기본 개념과 코드들은 해당 영상을 참고하여 작성하였다.</p>
<h2 id="11-컴포넌트-렌더링-테스트">1.1 컴포넌트 렌더링 테스트</h2>
<p>가장 기본적인 App 컴포넌트가 렌더링되고 있는지에 대한 테스트를 작성해보자. 아래의 테스트는 App 컴포넌트를 렌더링하여 <code>heading</code>역할을 하는 요소를 받아온다. 그리고 해당 엘리먼트가 화면에 존재하는지 검증하면 테스트가 종료된다.</p>
<ul>
<li><code>render()</code> 특정 컴포넌트를 렌더링</li>
<li><code>screen</code> 객체의 쿼리 메소드로 HTML element에 접근<pre><code class="language-js">// App.test.js
import { render, screen } from &#39;@testing-library/react&#39;;
</code></pre>
</li>
</ul>
<p>test(&#39;<App /> 렌더링시 / 경로로 렌더링 되나요?&#39;, async () =&gt; {
    render(<App />);</p>
<pre><code>const headingEl = screen.getByRole(&#39;heading&#39;);
expect(headingEl).toBeInTheDocument();</code></pre><p>  });</p>
<pre><code>## 1.2 jest-dom의 custom matchers

`toBeInTheDocument`와 같이 matcher를 통해 특정 DOM 요소의 상태를 테스트할 수 있으며, [공식 문서](https://github.com/testing-library/jest-dom#tobeinthedocument)에서는 다양한 matcher를 제공하고 있다. 회원이 아닌 경우 버튼을 비활성화하고 안내 문구를 빨간색으로 나타내는 JoinBtn 컴포넌트를 테스트해보자.

```js
test(&quot;회원이 아닌 경우 버튼을 비활성화합니다. 안내 문구는 빨간색입니다.&quot;, () =&gt;{
    render(&lt;JoinBtn isMember={false}/&gt;);

    const btnEl = screen.getByRole(&quot;button&quot;);
    const textEl = screen.getByRole(&quot;heading&quot;);

    expect(btnEl).toBeInTheDocument(); // 해당 버튼이 화면에 존재하나요?
    expect(textEl).toBeInTheDocument(); // 해당 문구가 화면에 존재하나요?
    expect(btnEl).toBeDisbaled(); // 버튼 비활성화 되어있나요?
    expect(btnEl).toHaveStyle({ // 문구가 빨간색인가요?
        color: &quot;red&quot;,
    });
})</code></pre><blockquote>
<p>테스트에 용이한 컴포넌트를 위해서 가변적인 데이터를 props로 외부에서 주입하자!</p>
</blockquote>
<h2 id="13-html-element를-찾는-쿼리">1.3 HTML element를 찾는 쿼리</h2>
<p>렌더링된 컴포넌트에서 테스트를 위한 요소를 찾기 위해 <a href="https://testing-library.com/docs/queries/about/">공식 문서</a>에서는 다양한 쿼리들을 제공한다. 이 쿼리들을 단일/여러개의 요소를 찾을 것인지, 요소를 어떤 방식으로 찾을 것인지에 따라 분류할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/ee6795de-f24c-4e3b-84d5-c63610ee91e7/image.png" alt="">
[표 1. Summary Table: Type of Query(출처: 공식 문서)] </p>
<h3 id="131-단일--여러-개의-요소-찾기">1.3.1 단일 / 여러 개의 요소 찾기</h3>
<p>하나의 요소를 찾는 쿼리들은 1 개의 요소가 매칭되었을 때 해당 요소를 반환하고, 여러 개가 매칭되면 [표 1]과 같이 에러를 발생시킨다. 요소를 찾는 방법은 역할, 텍스트, 지정한 테스트 id 등으로 다양하며 요소를 특정하기 위해 옵션을 추가하기도 한다. Wrapper로만 사용하는 <code>div</code>는 테스트 id를 붙여 찾을 수 있으나 테스트만을 위한 코드가 프로젝트에 추가되므로 최후의 방법으로 사용하는 것이 좋다.</p>
<pre><code class="language-html">&lt;div data-testid=&quot;my-div&quot;&gt;
  &lt;h1&gt;마이페이지&lt;/h1&gt;
  &lt;label htmlFor=&quot;username&quot;&gt;이름&lt;/label&gt;
  &lt;input type=&quot;text&quot; id=&quot;username&quot; /&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-js">const textEl = screen.getByRole(&quot;heading&quot;,{
    level: 1, // h1 tag 찾기
});

const inputEl = screen.getByRole(&quot;textbox&quot;, {
    name: &quot;이름&quot;, //label&#39;s children text 이름으로 찾기 - htmlFor id가 연결되어 있어야 함
});

const inputEl = screen.getByLabelText(&quot;이름&quot;);
// label이 아닌 연결된 textbox를 찾음

const inputEl = screen.getByLabelText(&quot;이름&quot;, {
    selector: input,
});

const inputElements = screen.getAllByRole(&quot;textbox&quot;); // 배열을 요소로 반환

const divEl = screen.getByTextId(&quot;my-div&quot;);
</code></pre>
<h3 id="132-여러-방식으로-요소-찾기">1.3.2 여러 방식으로 요소 찾기</h3>
<p>먼저 요소를 찾는 방식에 따라 아래와 같이 3 가지 타입으로 먼저 분류해보자.</p>
<ul>
<li>get... 일치하는 요소가 없으면 에러 발생</li>
<li>query... 일치하는 요소가 없으면 null, 빈 배열 반환 ➡️ 없는 요소 테스트에 활용!</li>
<li>find... 프로미스를 반환(default = 1 초)<pre><code class="language-js">const liElements = screen.getAllByRole(&quot;listitem&quot;); // 요소 없으면 아예 에러뜸
const liElements = screen.queryByRole(&quot;listitem&quot;); // null 반환
const liElements = screen.queryAllByRole(&quot;listitem&quot;); // 빈 배열 반환
</code></pre>
</li>
</ul>
<p>test(&quot;잠시 후 제목이 나타납니다.&quot; async() =&gt;{
    render(<UserList users={users}/>);
    screen.debug(); // 렌더링된 DOM 트리 확인할 수 있는 디버깅 모드
    const titleEl = await screen.findByRole(&quot;heading&quot;, {
        name: &quot;사용자 목록&quot;<br>    },
    {
        timeout: 2000, // default = 1 초
    });
    screen.debug();
});</p>
<pre><code># 2. 팀 프로젝트에 적용해보기
## 2.1 테스트하기 좋은 코드로 리팩토링하기
해당 내용을 바탕으로 [원티드 프리 온보딩 1 주차 과제](https://github.com/wanted-pre-onboarding-team-7/week1-auth-todo)의 `Route` 컴포넌트에 테스트를 간단하게 추가해보았다. **Assignment3**의 요구 조건에서는 로그인 여부(토큰 유무)에 따라 리다이렉트 처리를 한다. 우리 팀은 `Router` 컴포넌트 내에서 `PublicRoute`와 `PrivateRoute` 컴포넌트를 두어 토큰을 확인하고 리다이렉트 처리를 위임하였다.

![](https://velog.velcdn.com/images/skyu_dev/post/4d14254b-96cf-4b07-80d7-d61ca3816c48/image.png)

`PublicRoute`의 좌측 코드도 잘 작동하지만 테스트를 적용하려면 #1.2와 같이 `isLogin` 값을 외부로부터 주입하는 것이 좋다.

![](https://velog.velcdn.com/images/skyu_dev/post/c8827de2-bd2c-4f96-962a-2cb1b5041a40/image.png)
따라서 `Router` 컴포넌트에서 토큰을 확인하는 역할을 갖고, `PublicRoute`와 `PrivateRoute` 컴포넌트는 리다이렉트 처리만 위임받게 리팩토링하였다.

## 2.2 테스트 코드를 잘 작성하기
라우팅에 대한 테스트 코드는 아래와 같이 작성하였다. `Router` 컴포넌트 내에 서 렌더링 되는 `Route`의 element를 테스트하였으며, 주의할 점은 이 컴포넌트들은 `Navigate`라는 react-router-dom 컴포넌트를 사용하므로`BrowserRouter`로 감싸주어야 한다.

테스트 코드도 코드이므로 재사용성이 좋게 잘 작성해야 한다는 말이 있다. `getByRole` 쿼리를 사용해 타이틀 요소를 가져올 때 option을 만드는 로직이 반복되므로 `makeOptions` 함수를 추출하여 상단에 작성하였다.
```js
import { BrowserRouter } from &#39;react-router-dom&#39;;

const makeOptions = (level, name) =&gt; {
  return { level, name };
};

describe(&#39;Routing 테스트&#39;, () =&gt; {
  test(&#39;토큰이 없는 상태로 / 경로로 접근하면 &lt;Home /&gt;이 렌더링 되나요?&#39;, () =&gt; {
    render(
      &lt;PublicRoute isLogin={null}&gt;
        &lt;Home /&gt;
      &lt;/PublicRoute&gt;,
      { wrapper: BrowserRouter },
    );
    const headingEl = screen.getByRole(&#39;heading&#39;, makeOptions(1, &#39;회원 가입&#39;));
    expect(headingEl).toBeInTheDocument();
  });

  test(&#39;토큰이 있는 상태로 /todo 경로로 접근하면 &lt;Todos /&gt;가 렌더링 되나요?&#39;, () =&gt; {
    render(
      &lt;PrivateRoute isLogin=&quot;someToken&quot;&gt;
        &lt;Todos /&gt;
      &lt;/PrivateRoute&gt;,
      { wrapper: BrowserRouter },
    );

    const headingEl = screen.getByRole(&#39;heading&#39;, makeOptions(1, &#39;Todo list&#39;));
    expect(headingEl).toBeInTheDocument();
  });
});</code></pre><p>아주 기초적인 수준이지만 본 테스트로 브라우저 상에서 로그인하여 토큰을 만들고 다시 지워서 리다이렉션을 테스트하는 번거로움을 줄일 수 있다고 생각한다. 모든 코드를 테스트하면 좋겠지만 확인하기 번거로운 로직들을 우선적으로 테스트 코드를 작성하는 이런 방식의 차선책도 존재한다. </p>
<h4 id="참고자료">참고자료</h4>
<ul>
<li><a href="https://youtu.be/K1w6WN7q6k8">React Testing Library 강의</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD] Husky, GitHub Actions 로 팀 프로젝트 코드를 지속적으로 통합/배포하기  (ESLint, Prettier, Jest, gh-pages)]]></title>
            <link>https://velog.io/@skyu_dev/CI-ESLint-Prettier-Jest-test-%EC%9E%90%EB%8F%99%ED%99%94%EB%A1%9C-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%A7%80%EC%86%8D%EC%A0%81%EC%9C%BC%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-Husky-GitHub-Actions</link>
            <guid>https://velog.io/@skyu_dev/CI-ESLint-Prettier-Jest-test-%EC%9E%90%EB%8F%99%ED%99%94%EB%A1%9C-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%A7%80%EC%86%8D%EC%A0%81%EC%9C%BC%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-Husky-GitHub-Actions</guid>
            <pubDate>Tue, 27 Dec 2022 16:09:07 GMT</pubDate>
            <description><![CDATA[<p>최근 리액트 네이티브 앱을 빌드하고 구글 플레이스토어에 지속적으로 배포하는 과정을 겪으면서 CI/CD의 필요성을 느끼게 되었다. 그러나 개인 프로젝트 특성상 기능 구현만으로도 일정에 치이다보니 따로 공부할 겨를이 없었다. 그러던 중 원티드 프리온보딩 인턴십에서 CI/CD 관련 세션을 수강하게 되었고 이를 팀 단위 프로젝트에 직접 적용하기 위해 공부한 내용을 적어보려 한다.</p>
<blockquote>
<p>본 글은 CI/CD를 처음 겪어본 개발자의 셋업이며 개인 정리용도의 글입니다.</p>
</blockquote>
<h1 id="팀-프로젝트-ci-셋업하기">팀 프로젝트 CI 셋업하기</h1>
<p>프로젝트를 셋업하는 과정은 아래와 같이 구분하여 정리하였고, 리액트 라이브러리를 사용하는 프론트엔드 개발 셋업을 기준으로 설명할 예정이다.</p>
<blockquote>
<p>1)  CRA로 리액트 프로젝트를 만들고 gitHub에 올리기
2-3) Test, linter, code formatter를 셋업하기
4-5) Husky, GitHub Actions를 사용하여 2-3)의 과정 자동화하기</p>
</blockquote>
<h2 id="1-cra--git-원격-저장소에-올리기">1. CRA &amp; git 원격 저장소에 올리기</h2>
<p>먼저, 로컬에서 리액트 프로젝트를 만들고 GitHub 페이지에서 원격 저장소를 만들어 연결한다.</p>
<ul>
<li><code>npx create-react-app project-name</code></li>
<li>GitHub에서 원격 저장소 만들기</li>
<li><code>git init</code> <code>git remote add origin repo-url</code></li>
</ul>
<h2 id="2-jest-test-설정하기">2. Jest test 설정하기</h2>
<p>본 프로젝트에서 테스팅 툴은 CRA에 내장되어 있는 <strong>Jest</strong>를 사용하였다. Jest의 <a href="https://velog.io/@skyu_dev/Jest-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-JS%EC%9D%98-%EA%B8%B0%EB%8A%A5-%EC%A0%90%EA%B2%80%ED%95%98%EA%B8%B0">기본 사용 방법</a>과 <a href="https://velog.io/@skyu_dev/Jest-React-Testing-Library%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-DOM-%ED%85%8C%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0">컴포넌트 렌더링 테스트</a> 방법은 다른 글에 정리해놓았다.</p>
<h3 id="21-app-컴포넌트-렌더링-테스트-생성하기">2.1 App 컴포넌트 렌더링 테스트 생성하기</h3>
<p>사실 CI/CD 테스트 코드를 다루는 여러 글과 컨퍼런스를 참고하였지만 현재 팀 상황에 마땅한 코드를 찾지 못하였다. 우리 팀은 앞으로 주어지는 3 개의 서로 다른 과제에 대해 미리 CI/CD 셋업을 하는 것을 목표로 하며, <strong>개인 개발 ➡️ 토론 ➡️ 베스트 코드 찾기 ➡️ 코드 통합하기 ➡️ 배포</strong>의 과정을 4 일 안에 완료해야 하는 빡빡한 스케줄에 놓여있다. 어떤 과제가 주어질지 아예 모르는 상황에서 테스트 코드를 짜기는 불가능하므로 아래와 같은 사고 과정으로 기준을 세워 보았다.</p>
<ul>
<li>Unit / Integration test<ul>
<li>1 차 구현 완료 후 코드를 구현한 개발자가 테스트 코드 작성</li>
</ul>
</li>
<li>E2E test<ul>
<li>컴포넌트 렌더링 테스트로 대체</li>
<li>짧은 기간 내에 click, hover 등 사용자 이벤트를 고려한 테스트 코드 작성은 어려워보임</li>
</ul>
</li>
</ul>
<p>그렇다면 어떠한 컴포넌트 렌더링 테스트를 임의의 프로젝트에 적용할 수 있을까? 필자는 팀프로젝트를 처음 겪으면서 <strong>여러 팀원들의 코드가 merge될 때 <code>npm run start</code> 커맨드를 입력하면 에러나 흰 화면이 뜨는 것이 가장 두려웠다.</strong> 따라서 <strong>앱 컴포넌트가 제대로 렌더링되고 있는지를 확인하는 테스트</strong>를 셋업 코드로 결정하게 되었다.</p>
<pre><code class="language-jsx">// App.js
function App() {
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;h1&gt;APP title&lt;/h1&gt;
    &lt;/div&gt;
  );
}
export default App;</code></pre>
<pre><code class="language-js">// src/__test__/DOM.test.js
import &quot;@testing-library/jest-dom&quot;; // CRA의 setupTest.js 파일 삭제 시 추가해야 함
import { render, screen } from &quot;@testing-library/react&quot;;
import App from &quot;../App&quot;;

describe(&quot;App 컴포넌트 렌더링 테스트&quot;, () =&gt; {
  test(&quot;&lt;App /&gt; 렌더링이 되나요?&quot;, async () =&gt; {
    render(&lt;App /&gt;);

    // App 컴포넌트의 `h1` 렌더링 여부 확인하는 테스트 코드
    const headingEl = screen.getByRole(&quot;heading&quot;, makeOptions(1, &quot;APP title&quot;));
    expect(headingEl).toBeInTheDocument();
  });
});

const makeOptions = (level, name) =&gt; {
  return { level, name };
};</code></pre>
<h3 id="22-test-script-수정하기">2.2 <code>test</code> script 수정하기</h3>
<ul>
<li>Jest Unexpected Token Error 방지를 위해</li>
</ul>
<pre><code class="language-json">//package.json
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;react-scripts test --transformIgnorePatterns \&quot;node_modules/(?!@toolz/allow-react)/\&quot; --env=jsdom --watchAll&quot;,
  },</code></pre>
<h2 id="3-eslint--prettier-설정하기">3. ESLint &amp; Prettier 설정하기</h2>
<p>여러 개발자가 관리하는 프로젝트 내의 문법, 코드 포맷팅을 정하기 위해 JavaScript에서는 <strong>Linter로 ESLint</strong>를 <strong>Code formatter로 Prettier</strong>를 사용한다. 팀원들과 상의를 통해 컨벤션을 정하여 아래와 같이 셋팅을 진행하였다.</p>
<h3 id="31-패키지-설치하기">3.1 패키지 설치하기</h3>
<p><code>npm install eslint --save-dev</code>
<code>npm install prettier --save-dev</code>
<code>npm install eslint-config-prettier --save-dev</code> // ESLint의 포맷팅 셋팅을 꺼주는 역할</p>
<pre><code>// .eslintrc
{
  &quot;extends&quot;: [&quot;react-app&quot;, &quot;eslint:recommended&quot;],
  &quot;rules&quot;: {
    &quot;no-var&quot;: &quot;error&quot;,
    &quot;no-multiple-empty-lines&quot;: &quot;error&quot;,
    &quot;no-console&quot;: [&quot;warn&quot;, { &quot;allow&quot;: [&quot;warn&quot;, &quot;error&quot;, &quot;info&quot;] }],
    &quot;eqeqeq&quot;: &quot;error&quot;,
    &quot;no-unused-vars&quot;: &quot;warn&quot;,
    &quot;no-undef&quot;: &quot;warn&quot;
  }
}</code></pre><pre><code class="language-js">// .prettierrc.js
module.exports = {
  singleQuote: true,
  semi: true,
  useTabs: false,
  tabWidth: 2,
  trailingComma: &quot;all&quot;,
  printWidth: 80,
};</code></pre>
<h3 id="32-script-추가하기">3.2 script 추가하기</h3>
<ul>
<li><code>.gitignore</code> 파일에 .eslintcache 추가</li>
</ul>
<pre><code class="language-json">//package.json
  &quot;scripts&quot;: {
    &quot;format&quot;: &quot;prettier --write --cache .&quot;,
    &quot;lint&quot;: &quot;eslint --cache .&quot;
  },</code></pre>
<p>터미널로 명령을 실행한다는 것은 <strong>자동화</strong>가 가능하는 뜻이다. #2-3에서 <code>package.json</code> 파일에 추가한 scripts를 통해 해당 커맨드들을 자동화해보자.</p>
<h2 id="4-husky를-사용한-2-3-자동화">4. Husky를 사용한 #2-3 자동화</h2>
<p><a href="https://typicode.github.io/husky/#/">Husky</a>는 git hook 설정을 간단하게 도와주는 npm 패키지이다. Git hook이란 commit, push와 같은 git의 특정 이벤트 전후로 특정 동작을 실행하도록 하는 것이다. Husky는<code>npm i</code> 과정에서 최초 프로젝트 셋업시 사전에 설정해둔 git hook이 적용되므로 모든 팀원이 사용도록 하기가 편하다. </p>
<h3 id="41-husky-설치">4.1 Husky 설치</h3>
<ul>
<li><code>npm install husky --save-dev</code> (<code>git init</code>이 되어있어야 함)</li>
<li><code>npx husky install</code><ul>
<li>husky에 등록된 hook을 .git에 적용시키기 위한 스크립트. 이후 저장소를 클론 받는 사람들은 <code>npm i</code> 이후 자동으로 <code>postinstall</code> 스크립트가 실행됨</li>
</ul>
</li>
</ul>
<pre><code class="language-json">// package.json
  &quot;scripts&quot;: {
    &quot;postinstall&quot;: &quot;husky install&quot;
  },
</code></pre>
<h3 id="42-git-hooks-추가하기">4.2 Git hooks 추가하기</h3>
<ul>
<li>커밋 이전에 코드 포맷팅하기
<code>npx husky add .husky/pre-commit &quot;npm run format&quot;</code></li>
<li>푸시 이전에 린터&amp;테스트 실행하기<ul>
<li><code>npx husky add .husky/pre-push &quot;npm run lint&quot;</code></li>
<li><code>npx husky add .husky/pre-push &quot;npm run test&quot;</code><ul>
<li><code>./husky/pre-push</code>의 커맨드 수정: <code>npm run test -- --watchAll=false</code></li>
<li>테스트가 한 번만 실행되고 종료되도록 한다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="5-github-actions를-사용한-3-자동화">5. GitHub Actions를 사용한 #3 자동화</h2>
<p>여기까지 왔다면 프로젝트의 셋업이 거의 마무리되었다고 생각하면 된다. 필자는 추가로 <code>pull request</code> 이벤트에 대한 훅을 설정하기 위해 GitHub에서 제공하는 클라우드형 CI/CD 툴 <a href="https://docs.github.com/en/actions">GitHub Actions</a>를 추가하였다.</p>
<ul>
<li>목적:<code>develop</code> 브랜치의 코드가 검증되었는가?</li>
<li>테스트 코드: 현재 develop 브랜치의 코드를 테스트<ul>
<li>주의: 테스트 코드 또한 현재 브랜치의 테스트 코드를 사용함</li>
</ul>
</li>
</ul>
<h3 id="51-pr-test의-trigger-결정하기">5.1 PR test의 trigger 결정하기</h3>
<p>개발이 이루어지고 있는 <code>develop</code> 브랜치 코드의 검증 시기와 테스트 결과 확인 사용성을 고려하여 두 가지 방안을 고안하였다.</p>
<p><strong>A안) PR이 merge되었을 떄</strong>
<a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#running-your-workflow-when-a-pull-request-merges">공식문서에 의하면 아래와 같은 설정</a>을 사용하여 PR이 병합되어 닫힌 경우 테스트 코드를 실행 할 수 있다. 이 경우 코드의 충돌이 없는 상태에서 머지가 완료되면 테스트 성공 여부를 확인할 수 있으며, 저장소의 Actions 탭에서 테스트 결과를 확인하거나 fail할 경우 메일(알림 설정이 되어있으면)을 받을 수 있다.</p>
<pre><code class="language-yml">on:
  pull_request:
    types:
      - closed

jobs:
  if_merged:
    if: github.event.pull_request.merged == true</code></pre>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/a51641e4-846e-4424-b501-8f1bad7be5f5/image.png" alt="">
<strong>B안) PR 생성되었을 때</strong>
이 경우 PR을 처음 생성했을 뿐만 아니라 추가 커밋으로 PR이 갱신되거나 다른 PR이 먼저 병합되어 현재 develop 브랜치의 코드가 업데이트 되었을 때에도 트리거된다. A안과 달리 PR 창에서 테스트를 확인할 수 있어 해당 PR을 올린 팀원은 현재 <code>develop</code> 브랜치의 코드가 검증되어 있는지를 쉽게 확인할 수 있다. 그러나 B안의 단점은 여러 PR이 열려있는 경우 훅이 너무 많이 트리거된다는 단점이 있다. <a href="https://docs.github.com/ko/billing/managing-billing-for-github-actions/about-billing-for-github-actions#about-spending-limits">About billing for GitHub Actions</a>에 따르면 공개 저장소와 self-hosted runner를 사용하면 무료로 사용할 수 있지만, 필자는 <code>GitHub-hosted runners</code>를 사용하므로 1 달에 2000 분 제한이 존재한다.</p>
<pre><code class="language-yml">on:
  pull_request:
    branches:
      - develop</code></pre>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/3caedf31-5199-4556-87c4-6e5cc75d8200/image.png" alt=""></p>
<h3 id="52-저장소에-yml-파일-추가하기">5.2 저장소에 .yml 파일 추가하기</h3>
<pre><code class="language-yml">// .github/workflows/testPR.yml
name: test PR

on:
  pull_request:
    branches:
      - develop

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          ref: &quot;develop&quot;
      - run: npm ci
      - run: npm run test -- --watchAll=false</code></pre>
<h1 id="github-actions를-사용한-cd-구축하기">GitHub Actions를 사용한 CD 구축하기</h1>
<p><code>gh-pages</code>를 사용한 자동 정적 배포를 자동화할 수 있는 GitHub Actions를 아래와 같이 설정해보았다. </p>
<p>먼저 <code>npm i gh-pages</code>로 패키지를 설치해주고, package.json에 <code>predeploy</code> <code>deploy</code> 관련 스크립트를 아래와 같이 작성한다.</p>
<pre><code class="language-js">package.json
{
  &quot;scripts&quot;: {
    &quot;predeploy&quot;: &quot;npm run build&quot;,
    &quot;deploy&quot;: &quot;gh-pages -d build&quot;
  },
   &quot;homepage&quot;: &quot;https://[githubID].github.io/[repo-name]&quot; 
}</code></pre>
<p>ESLint 를 사용하고 있다면 빌드 파일을 검사하지 않도록 <code>.eslintignore</code> 파일을 만들어야 한다.</p>
<pre><code>.eslintignore
/build
</code></pre><p>GitHub Actions는 아래와 같이 두 가지 경우에 대해 자동 배포가 트리거되게 만들 수 있다.</p>
<h3 id="1-main-브랜치에-코드가-push되거나-actions-버튼을-눌러-자동-배포">1) Main 브랜치에 코드가 push되거나 actions 버튼을 눌러 자동 배포</h3>
<p>GitHub Actions 관련 코드는 <a href="https://github.com/marketplace/actions/deploy-to-github-pages">Marketplace</a>에서 필요한 작업을 찾아 사용하였다.</p>
<pre><code class="language-yml">name: gh-pages deploy

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: write

jobs:
  build-and-deploy:
    concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession.
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v3

      - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the &#39;build&#39; folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
        run: |
          npm ci
          npm run build
      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          folder: build # The folder the action should deploy.


</code></pre>
<h3 id="2-main-브랜치에서-pr이-merge되었을-때-자동-배포">2) Main 브랜치에서 PR이 merge되었을 때 자동 배포</h3>
<pre><code class="language-yml">name: gh-pages deploy

on:
  pull_request:
    branches: [&#39;main&#39;]
    types:
      - closed

permissions:
  contents: write

jobs:
  if_merged:
    if: github.event.pull_request.merged == true
    concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession.
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v3

      - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the &#39;build&#39; folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
        run: |
          npm ci
          npm run build

      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          folder: build # The folder the action should deploy.

</code></pre>
<p>아래 이미지에서 main 브랜치에 코드가 merge되고 자동 배포 코드가 실행되어 gh-pages로 배포가 완료된 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/8c62252d-6c16-4193-90bf-59b4bae42a70/image.png" alt=""></p>
<h4 id="참고자료">참고자료</h4>
<ul>
<li>원티드 프론트엔드 프리온보딩 인턴십 [Week 1-2. 프로젝트를 지속적으로 통합하고 배포하는 법] 세션 강의</li>
<li><a href="https://typicode.github.io/husky/#/">Husky</a></li>
<li><a href="https://docs.github.com/en/actions">GitHub Actions</a></li>
<li><a href="https://youtu.be/VuGi6vCxu-0">[Track 1-6] 김동우 - 진정한 CI CD를 위한 E2E Test 시작하기</a></li>
<li><a href="https://dev.to/bytebodger/how-i-fixed-the-unexpected-token-error-in-jest-4o1j">How I Fixed The Unexpected Token Error In Jest</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Post Black Belt 개인정보처리방침]]></title>
            <link>https://velog.io/@skyu_dev/Post-Black-Belt-%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</link>
            <guid>https://velog.io/@skyu_dev/Post-Black-Belt-%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</guid>
            <pubDate>Sun, 18 Dec 2022 16:32:17 GMT</pubDate>
            <description><![CDATA[<p>&lt; Quartz &gt;(&#39;<a href="https://velog.io/@skyu_dev&#39;%EC%9D%B4%ED%95%98">https://velog.io/@skyu_dev&#39;이하</a> &#39;Post Black Belt&#39;)은(는) 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.</p>
<p>○ 이 개인정보처리방침은 2022년 12월 15부터 적용됩니다.</p>
<p>제1조(개인정보의 처리 목적)</p>
<p>&lt; Quartz &gt;(&#39;<a href="https://velog.io/@skyu_dev&#39;%EC%9D%B4%ED%95%98">https://velog.io/@skyu_dev&#39;이하</a> &#39;Post Black Belt&#39;)은(는) 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.</p>
<ol>
<li>홈페이지 회원가입 및 관리</li>
</ol>
<p>회원 가입의사 확인, 회원제 서비스 제공에 따른 본인 식별·인증 목적으로 개인정보를 처리합니다.</p>
<p>제2조(개인정보의 처리 및 보유 기간)</p>
<p>① &lt; Quartz &gt;은(는) 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.</p>
<p>② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다.</p>
<p>1.&lt;홈페이지 회원가입 및 관리&gt;
&lt;홈페이지 회원가입 및 관리&gt;와 관련한 개인정보는 수집.이용에 관한 동의일로부터&lt;준영구&gt;까지 위 이용목적을 위하여 보유.이용됩니다.
보유근거 : 회원 식별
관련법령 :
예외사유 :</p>
<p>제3조(처리하는 개인정보의 항목)</p>
<p>① &lt; Quartz &gt;은(는) 다음의 개인정보 항목을 처리하고 있습니다.</p>
<p>1&lt; 홈페이지 회원가입 및 관리 &gt;
필수항목 : 이메일
선택항목 : 사용자 정보, 일기, 친구 목록</p>
<p>제4조(개인정보의 제3자 제공에 관한 사항)</p>
<p>① &lt; Quartz &gt;은(는) 개인정보를 제1조(개인정보의 처리 목적)에서 명시한 범위 내에서만 처리하며, 정보주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.</p>
<p>② &lt; Quartz &gt;은(는) 다음과 같이 개인정보를 제3자에게 제공하고 있습니다.</p>
<ol>
<li>&lt; &gt;
개인정보를 제공받는 자 :
제공받는 자의 개인정보 이용목적 :
제공받는 자의 보유.이용기간:</li>
</ol>
<p>제5조(개인정보의 파기절차 및 파기방법)</p>
<p>① &lt; Quartz &gt; 은(는) 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체없이 해당 개인정보를 파기합니다.</p>
<p>② 정보주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.</p>
<ol>
<li>법령 근거 :</li>
<li>보존하는 개인정보 항목 : 계좌정보, 거래날짜</li>
</ol>
<p>③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.</p>
<ol>
<li><p>파기절차
&lt; Quartz &gt; 은(는) 파기 사유가 발생한 개인정보를 선정하고, &lt; Quartz &gt; 의 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.</p>
</li>
<li><p>파기방법</p>
</li>
</ol>
<p>전자적 파일 형태의 정보는 기록을 재생할 수 없는 기술적 방법을 사용합니다</p>
<p>제6조(정보주체와 법정대리인의 권리·의무 및 그 행사방법에 관한 사항)</p>
<p>① 정보주체는 Quartz에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다.</p>
<p>② 제1항에 따른 권리 행사는Quartz에 대해 「개인정보 보호법」 시행령 제41조제1항에 따라 서면, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 Quartz은(는) 이에 대해 지체 없이 조치하겠습니다.</p>
<p>③ 제1항에 따른 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다.이 경우 “개인정보 처리 방법에 관한 고시(제2020-7호)” 별지 제11호 서식에 따른 위임장을 제출하셔야 합니다.</p>
<p>④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 의하여 정보주체의 권리가 제한 될 수 있습니다.</p>
<p>⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.</p>
<p>⑥ Quartz은(는) 정보주체 권리에 따른 열람의 요구, 정정·삭제의 요구, 처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.</p>
<p>제7조(개인정보의 안전성 확보조치에 관한 사항)</p>
<p>&lt; Quartz &gt;은(는) 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.</p>
<ol>
<li><p>정기적인 자체 감사 실시
개인정보 취급 관련 안정성 확보를 위해 정기적(분기 1회)으로 자체 감사를 실시하고 있습니다.</p>
</li>
<li><p>개인정보에 대한 접근 제한
개인정보를 처리하는 데이터베이스시스템에 대한 접근권한의 부여,변경,말소를 통하여 개인정보에 대한 접근통제를 위하여 필요한 조치를 하고 있으며 침입차단시스템을 이용하여 외부로부터의 무단 접근을 통제하고 있습니다.</p>
</li>
</ol>
<p>제8조(개인정보를 자동으로 수집하는 장치의 설치·운영 및 그 거부에 관한 사항)</p>
<p>① Quartz 은(는) 이용자에게 개별적인 맞춤서비스를 제공하기 위해 이용정보를 저장하고 수시로 불러오는 ‘쿠키(cookie)’를 사용합니다.
② 쿠키는 웹사이트를 운영하는데 이용되는 서버(http)가 이용자의 컴퓨터 브라우저에게 보내는 소량의 정보이며 이용자들의 PC 컴퓨터내의 하드디스크에 저장되기도 합니다.
가. 쿠키의 사용 목적 : 이용자가 방문한 각 서비스와 웹 사이트들에 대한 방문 및 이용형태, 인기 검색어, 보안접속 여부, 등을 파악하여 이용자에게 최적화된 정보 제공을 위해 사용됩니다.
나. 쿠키의 설치•운영 및 거부 : 웹브라우저 상단의 도구&gt;인터넷 옵션&gt;개인정보 메뉴의 옵션 설정을 통해 쿠키 저장을 거부 할 수 있습니다.
다. 쿠키 저장을 거부할 경우 맞춤형 서비스 이용에 어려움이 발생할 수 있습니다.</p>
<p>제9조 (개인정보 보호책임자에 관한 사항)</p>
<p>① Quartz 은(는) 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.</p>
<p>▶ 개인정보 보호책임자
성명 :유서경
직책 :대표
직급 :대표
연락처 :01089928527, <a href="mailto:skyu.dev@gmail.com">skyu.dev@gmail.com</a>,
※ 개인정보 보호 담당부서로 연결됩니다.</p>
<p>▶ 개인정보 보호 담당부서
부서명 :
담당자 :
연락처 :, ,
② 정보주체께서는 Quartz 의 서비스(또는 사업)을 이용하시면서 발생한 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다. Quartz 은(는) 정보주체의 문의에 대해 지체 없이 답변 및 처리해드릴 것입니다.</p>
<p>제10조(개인정보의 열람청구를 접수·처리하는 부서)
정보주체는 ｢개인정보 보호법｣ 제35조에 따른 개인정보의 열람 청구를 아래의 부서에 할 수 있습니다.
&lt; Quartz &gt;은(는) 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.</p>
<p>▶ 개인정보 열람청구 접수·처리 부서
부서명 :
담당자 :
연락처 : , ,</p>
<p>제11조(정보주체의 권익침해에 대한 구제방법)</p>
<p>정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고, 상담에 대하여는 아래의 기관에 문의하시기 바랍니다.</p>
<ol>
<li>개인정보분쟁조정위원회 : (국번없이) 1833-6972 (<a href="http://www.kopico.go.kr">www.kopico.go.kr</a>)</li>
<li>개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)</li>
<li>대검찰청 : (국번없이) 1301 (<a href="http://www.spo.go.kr">www.spo.go.kr</a>)</li>
<li>경찰청 : (국번없이) 182 (ecrm.cyber.go.kr)</li>
</ol>
<p>「개인정보보호법」제35조(개인정보의 열람), 제36조(개인정보의 정정·삭제), 제37조(개인정보의 처리정지 등)의 규정에 의한 요구에 대 하여 공공기관의 장이 행한 처분 또는 부작위로 인하여 권리 또는 이익의 침해를 받은 자는 행정심판법이 정하는 바에 따라 행정심판을 청구할 수 있습니다.</p>
<p>※ 행정심판에 대해 자세한 사항은 중앙행정심판위원회(<a href="http://www.simpan.go.kr">www.simpan.go.kr</a>) 홈페이지를 참고하시기 바랍니다.</p>
<p>제12조(개인정보 처리방침 변경)</p>
<p>① 이 개인정보처리방침은 2022년 12월 15부터 적용됩니다.</p>
<p>② 이전의 개인정보 처리방침은 아래에서 확인하실 수 있습니다.</p>
<p>예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)</p>
<p>예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)</p>
<p>예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Design Pattern] 변경에 유연한 Picker Component 만들기]]></title>
            <link>https://velog.io/@skyu_dev/React-Design-Pattern-%EB%B3%80%EA%B2%BD%EC%97%90-%EC%9C%A0%EC%97%B0%ED%95%9C-Picker-Component-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/React-Design-Pattern-%EB%B3%80%EA%B2%BD%EC%97%90-%EC%9C%A0%EC%97%B0%ED%95%9C-Picker-Component-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 12 Dec 2022 10:14:45 GMT</pubDate>
            <description><![CDATA[<h1 id="0-introduction">#0 Introduction</h1>
<p>리액트를 사용하여 프론트엔드 개발을 하다보면 유사한 로직을 가지는 코드를 반복적으로 작성하는 경우가 자주 발생한다. 첫 번째는 같은 역할을 하는 컴포넌트를 다시 개발하는 경우이고, 두 번째는 디자인이나 서비스의 변경이 생겨 코드를 변경하는 경우이다. </p>
<blockquote>
<p>즉, 서비스가 확장되고 제품이 변경됨에 따라 우리는 코드를 반복적으로 작성하게 된다.</p>
</blockquote>
<p><code>&lt;Post Black Belt&gt;</code> 프로젝트에서 다양한 UI를 개발하면서 이러한 문제점을 개선하기 위해 변경에 유연한 컴포넌트를 만드는 디자인 패턴을 공부하게 되었다. 이 글에서는 나에게 좋은 인사이트를 주었던 <a href="https://youtu.be/fR8tsJ2r7Eg">토스의 한재엽 개발자님의 컨퍼런스 강연(Effective Component 지속 가능한 성장과 컴포넌트)</a>을 소개하고 해당 내용을 적용한 나의 사례를 이야기할 예정이다. 우리가 만드는 제품(ex. 웹 서비스)은 사용자를 만족시키기 위해 변화하며 성장한다. User Interface를 담당하는 프론트엔드 개발은 이러한 변화에 맞닿아있으며 개발자들은 각자의 기준에 맞춰 컴포넌트를 분리하여 <strong>변경에 유연하게 개발</strong>하는 환경을 만든다.</p>
<h1 id="1-effective-component-지속-가능한-성장과-컴포넌트-강연">#1 Effective Component 지속 가능한 성장과 컴포넌트 (강연)</h1>
<h2 id="11-컴포넌트를-분리하는-기준-세우기">#1.1 컴포넌트를 분리하는 기준 세우기</h2>
<p>해당 강연에서 제시한 컴포넌트를 분리하는 기준은 다음과 같다.</p>
<blockquote>
<ol>
<li>Headless 기반의 추상화
 디자인에 의존하는 <strong>UI</strong>와 <strong>데이터</strong>를 관리하는 로직을 각각 추상화하기</li>
<li>Composition: 한 가지 역할만 하는 컴포턴트 혹은 컴포넌트들의 조합으로 구성
 독립적인 각 컴포넌트를 합성하여 재사용하기</li>
<li>도메인(비즈니스 로직)을 분리
 컴포넌트는 기존에 알고있는 <strong>표준</strong>(도메인을 포함하지 않는) 내용일수록 이해하기 쉬움</li>
</ol>
</blockquote>
<p>Headless UI component란 기능은 있지만 스타일이 없는 컴포넌트를 말한다. 디자인과 데이터를 각각 추상화(ex custom hooks 생성)하여 컴포넌트가 한 가지 역할에만 집중하도록 만든다. 이는 우테코 프리코스를 수강하며 공부하였던 <a href="https://velog.io/@skyu_dev/Design-Pattern-UI-Architecture%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%B6%84%EB%A6%AC%ED%95%98%EC%9E%90-MVC-MVP">UI 아키텍쳐의 기본인 MVC, MVP 패턴</a>의 해결 방법과 유사하다.</p>
<p>사용자에게 시간과 음식 입력을 받는 서비스를 만든다고 하자. 우리는 각 컴포넌트를 <code>TimePicker</code>와 <code>FoodPicker</code>로 나누어 개발할 수도 있지만, <code>Picker</code>라는 표준 컴포넌트를 기반으로 컴포넌트를 합성하여 반복되는 로직을 개선할 수 있다. 이러한 방식으로 개발할 때 비즈니스(데이터) 로직과 UI 로직 중 어떤 것을 스스로 처리하고 어떤 것을 위임할지 잘 고민해보아야 한다. 이를 바탕으로 내가 느낀 컴포넌트 분리의 핵심을 아래의 한 문장으로 정리해보았다.</p>
<blockquote>
<p>컴포넌트를 잘게 쪼개서 모든 사람이 알고 있는 기능을 하도록 만들자!</p>
</blockquote>
<h2 id="12-코드를-짜는-방법">#1.2 코드를 짜는 방법</h2>
<p>위 내용을 바탕으로 더 수월하게 개발하기 위해 해당 강연에서 제시한 개발 방법론은 아래와 같다.</p>
<blockquote>
<ol>
<li>Component Interface를 먼저 고민하기
 도메인을 포함하지 않는 표준 컴포넌트가 이미 존재한다고 가정하고 Component Interface를 작성한다. 어떤 데이터를 props로 공유해야 할지 컴포넌트의 <strong>의도, 기능, 표현</strong>을 먼저 파악한다.</li>
<li>Component를 나누는 이유 생각하기
 현재 컴포넌트를 분리하는 이유가 복잡도를 낮추는 방향인지 혹은 재사용 가능한 컴포넌트를 만들기 위함인지 파악하여야 한다.</li>
</ol>
</blockquote>
<h1 id="2-지속가능한-list-picker-component-만들기-사례">#2 지속가능한 List Picker Component 만들기 (사례)</h1>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/625bf8d7-7fcb-4122-b7a8-60b85f07d39b/image.gif" alt=""></p>
<p>위 내용을 바탕으로 실제 프로젝트 코드를 개선한 사례를 소개한다. 현재 <a href="https://github.com/SeokyoungYou/post-blackbelt">내가 개발중인 서비스</a>는 주짓수 수련자를 위한 일기 기록 앱이다. React Native를 사용하고 있으므로 React와 상이한 JSX 코드가 등장하는 것을 참고하길 바란다. 아래와 같이 <code>DiaryCategoryPicker</code>와 <code>TechCategoryPicker</code>라는 두 개의 컴포넌트를 개발하고 보니 디자인 외에는 유사한 동작을 하고 있음을 깨달았다. 다음과 같이 반복되는 사항들을 개선하여 <strong>컴포넌트의 재사용성</strong>을 높인 경험에 대해 공유해보려 한다.</p>
<ul>
<li>디자인의 반복: 카테고리를 리스트 형태로 나열 &amp; 사용자의 선택 외 항목들은 <code>opacity</code> 낮춤</li>
<li>데이터 처리의 반복: 사용자가 선택한 항목을 전역 상태 관리</li>
</ul>
<h2 id="21-component-interface-고민하기">#2.1 Component Interface 고민하기</h2>
<p>따로 개발된 두 개의 컴포넌트를 하나의 <code>ListPicker</code>로부터 상속시키기 위하여 아래와 같은 Component Interface를 먼저 구상하였다. <code>ListPicker</code> 에서 다뤄야할 <strong>데이터</strong>와 관련된 로직들은 <code>item</code> <code>dispatch</code> <code>getCurrIndex</code>이며 <strong>UI</strong>와 관련된 로직은 <code>jsx</code> props로 전달할 예정이다.</p>
<pre><code class="language-jsx">// DiaryCatListPicker
&lt;ListPicker
  items={DIARY_CAT}
  dispatch={dispatchPressedCategory}
  getCurrIndex={getCurrIndex}
  jsx={listComponent}
/&gt;

// TechCatListPicker
&lt;ListPicker
  items={TECH_CAT}
  dispatch={dispatchPressedCategory}
  getCurrIndex={getCurrIndex}
  jsx={listComponent}
  /&gt;</code></pre>
<h2 id="22-도메인-분리하기">#2.2 도메인 분리하기</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/ba3cbfed-0674-4acc-800c-dde931f5f104/image.gif" alt="">
 그 전에 특정 비즈니스가 아닌 표준 컴포넌트 <code>ListPicker</code>에 대해 먼저 코드를 작성해보자. 임의의 데이터와 UI로 아래와 같은 반복성을 <code>ListPicker</code> 컴포넌트에 위임하였다.</p>
<blockquote>
<ul>
<li>디자인의 반복: 카테고리를 리스트 형태로 나열 &amp; 사용자의 선택 외 항목들은 <code>opacity</code> 낮춤</li>
</ul>
</blockquote>
<ul>
<li>데이터 처리의 반복: 사용자가 선택한 항목을 처리</li>
</ul>
<pre><code class="language-jsx">const ICON_OPACITY = {
  INACTIVE: 0.3,
  ACTIVE: 1,
};

const items = [0, 0, 0, 0, 0, 0]; // 임의의 데이터

export default function ListPicker() {
  const opacityArr = Array(items.length).fill(ICON_OPACITY.INACTIVE);
  const [iconsOpacity, setIconOpacity] = useState(opacityArr);
  const storeDiary = useSelector((state) =&gt; state.editDiary);

  const updateActiveIconOpacity = (i) =&gt; {
    setIconOpacity(() =&gt; {
      const result = opacityArr;
      result[i] = ICON_OPACITY.ACTIVE;
      return result;
    });
  };
  const handleOnPress = (CAT, i) =&gt; {
    updateActiveIconOpacity(i);
    // 데이터 처리 로직 작성
  };

  return (
    &lt;&gt;
      {items.map((CAT, i) =&gt; {
        return (
          &lt;Pressable
            key={CAT.ID}
            style={{ opacity: iconsOpacity[i] }}
            onPress={handleOnPress.bind(this, CAT, i)}
          &gt;
            &lt;Text&gt;{CAT}&lt;/Text&gt; // 임의의 UI
          &lt;/Pressable&gt;
        );
      })}
    &lt;/&gt;
  );
}</code></pre>
<h2 id="23-데이터-로직-주입하기">#2.3 데이터 로직 주입하기</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/3ef0e462-a60f-4f45-a9b7-3d3839813622/image.gif" alt=""></p>
<p>이제 우리는 도메인이 분리된 <code>ListPicker</code>라는 표준 컴포넌트에 데이터와 UI 로직을 각각 주입하여 <code>DiaryCategoryPicker</code>와 <code>TechCategoryPicker</code> 컴포넌트의 비즈니스 로직을 구현할 수 있다. 먼저 데이터 로직을 주입해보자.</p>
<pre><code class="language-jsx">// DiaryCatListPicker
&lt;ListPicker
  items={DIARY_CAT}
  dispatch={dispatchPressedCategory}
/&gt;

// TechCatListPicker
&lt;ListPicker
  items={TECH_CAT}
  dispatch={dispatchPressedCategory}
 /&gt;</code></pre>
<p><code>items</code> prop은 각 카테고리별 항목들을 전달하며, 사용자가 클릭한 항목을 전역으로 관리하기 위한 redux 관련 함수를 <code>dispatch</code> prop으로 전송한다. #2.2의 <code>ListPicker</code> 에 아래와 같은 데이터 로직을 추가하면 디자인은 존재하지 않고 데이터만 처리하는 Headless UI 컴포넌트를 작성할 수 있다.</p>
<pre><code class="language-jsx">export default function ListPicker({ items, dispatch }) {

  const handleOnPress = (CAT, i) =&gt; {
    updateActiveIconOpacity(i);
    dispatch(CAT.ID); // 데이터를 전역으로 저장
  };

  return (
    &lt;&gt;
      {items.map((CAT, i) =&gt; {
        return (
          &lt;Pressable&gt;
            &lt;Text&gt;{CAT}&lt;/Text&gt; // 임의의 UI
          &lt;/Pressable&gt;
        );
      })}
    &lt;/&gt;
  );
}
</code></pre>
<h2 id="24-ui-로직-주입하기컴포넌트-합성">#2.4 UI 로직 주입하기(컴포넌트 합성)</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/e5a48890-db23-40f6-957c-3a59e3c00c83/image.gif" alt="">
이제 각 카테고리 Picker가 가진 UI만 주입하면 컴포넌트 구현이 완료된다. UI 로직은 <code>jsx</code> prop으로 <code>ListComponent</code>를 전달하여 <code>ListPicker</code>에서 컴포넌트를 합성하는 방식을 사용하였다.</p>
<pre><code class="language-jsx">// DiaryCatListPicker
const DiaryListComponent = (CAT) =&gt; {
  return (
    &lt;View style={styles.diaryCategory}&gt;
      &lt;Image style={styles.diaryCategoryImg} source={CAT.IMG_SRC} /&gt;
      &lt;Text style={styles.diaryCategoryTitle}&gt;{CAT.KOR}&lt;/Text&gt;
    &lt;/View&gt;
  );
};
...
&lt;ListPicker
   jsx={DiaryListComponent}
/&gt;


// TechCatListPicker
const TechListComponent = (CAT) =&gt; {
  return (
    &lt;View style={styles.diaryCategory}&gt;
      &lt;Text style={styles.diaryCategoryEng}&gt;{CAT.ENG}&lt;/Text&gt;
      &lt;Text style={styles.diaryCategoryKor}&gt;{CAT.KOR}&lt;/Text&gt;
    &lt;/View&gt;
  );
};
...
&lt;ListPicker
   jsx={TechListComponent}
 /&gt;</code></pre>
<p>위와 같이 각 카테고리 Picker의 UI를 작성하고 #2.3에서 완성된 Headless UI component에 <code>jsx</code>를 아래와 같이 합성하면 UI 로직을 간단하게 주입할 수 있다.</p>
<pre><code class="language-jsx">export default function ListPicker({ jsx }) {

  return (
    &lt;&gt;
      {items.map((CAT, i) =&gt; {
        return (
          &lt;Pressable&gt;
            {jsx(CAT)} // UI 로직 합성
          &lt;/Pressable&gt;
        );
      })}
    &lt;/&gt;
  );
}
</code></pre>
<h1 id="마치며">마치며</h1>
<p>강연을 듣고 큰 감명을 받았지만 실제 나의 프로젝트에 적용하기까지는 기간이 꽤 걸린 것 같다. 규모가 작은 프로젝트를 개발하고 있기도 했고, 토스의 개발자분이 올려놓은 코드 구조를 그대로 따라가려 해서 어디에 사용할지 감이 오지 않은 점도 있었다. 시간이 지나면서 우테코 프리코스를 통해 간단한 예시로 데이터 처리 로직과 UI 로직 분리의 필요성을 직접 느끼고, 리팩토링을 배우면서 코드의 재사용성을 고민하다보니 강연을 실제 코드로 접목시킬 수 있었다. 당장은 적용하기 힘들더라도 다른 개발자 분들의 고민과 사유가 담겨있는 강연 영상을 자주 봐야겠다는 교훈을 얻게 되었다. </p>
<blockquote>
<p><a href="https://github.com/SeokyoungYou/post-blackbelt/commit/02b0904a21065316c0bd14d368c0e383b565903c">자세한 프로젝트 코드는 깃허브</a>에서 확인할 수 있습니다.</p>
</blockquote>
<h4 id="참고자료">참고자료</h4>
<p><a href="https://youtu.be/fR8tsJ2r7Eg">토스ㅣSLASH 22 - Effective Component 지속 가능한 성장과 컴포넌트</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[원티드 프리온보딩 프론트엔드 인턴십] 참가 에세이 - 유서경]]></title>
            <link>https://velog.io/@skyu_dev/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%9D%B8%ED%84%B4%EC%8B%AD-%EC%B0%B8%EA%B0%80-%EC%97%90%EC%84%B8%EC%9D%B4-%EC%9C%A0%EC%84%9C%EA%B2%BD</link>
            <guid>https://velog.io/@skyu_dev/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%9D%B8%ED%84%B4%EC%8B%AD-%EC%B0%B8%EA%B0%80-%EC%97%90%EC%84%B8%EC%9D%B4-%EC%9C%A0%EC%84%9C%EA%B2%BD</guid>
            <pubDate>Mon, 12 Dec 2022 05:48:56 GMT</pubDate>
            <description><![CDATA[<h1 id="숏에세이">숏에세이</h1>
<h2 id="1-최종합격까지-몇-개-이상의-이력서-작성이-필요할까요">1. 최종합격까지 몇 개 이상의 이력서 작성이 필요할까요?</h2>
<p>최종 합격까지 20 개 이상의 기업에 이력서를 지원해야 합니다.
프리온보딩 인턴십 과정을 통해 멘토님들의 도움을 받아 부족했던 점들을 보완하고 실무적 개발 역량을 키워 더 나은 이력서를 만들고 싶습니다.</p>
<h2 id="2-프리온보딩-인턴십에서-숏에세이-작성제도를-시행하는-목적">2. 프리온보딩 인턴십에서 숏에세이 작성제도를 시행하는 목적</h2>
<p>숏에세이를 작성하면서 지원자가 스스로 프리온보딩 인턴십 과정을 명확히 이해하고 책임감을 가지게 하기 위해서입니다. 본 과정에서는 기존 인턴십과 유사하게 기업의 서비스를 경험하고 채용과 연계되지만, 관계 지향이 아닌 과제와 기술 중심의 프로젝트라는 차이점이 있습니다. 저는 내년 초 채용 지원을 목표로 하고 있으며 본 인턴십 과정에서 제시하는 커리큘럼과 채용 설명회의 필요성을 느껴 지원하였습니다. 특히 다수의 기업 과제와 기술 스택을 경험해보고 직접 그 기업에 지원할 수 있다는 점이 취업 준비생으로서 가장 필요했던 부분입니다. 인턴십에 참가하게 된다면 책임감을 가지고 성실하게 참여하겠습니다.</p>
<h2 id="3-지원하고-싶은-참가기업은-어디인지">3. 지원하고 싶은 참가기업은 어디인지</h2>
<h3 id="이노야드">이노야드</h3>
<p>서비스 출시를 앞두고 있는 기업에서 함께 도전해보고 싶습니다. 3D 모델링 툴로 프린팅하여 기기를 만들어본 경험이 있어 3D 스캐닝을 통해 맞춤형 기기를 제작하는 서비스가 궁금하여 지원해보고 싶습니다. 이노야드에서 프론트엔드 개발자는 어떤 기술 스택으로 어떤 개발을 할 수 있을지 궁금합니다.</p>
<h3 id="아티슨앤오션">아티슨앤오션</h3>
<p>개인적으로 스포츠 시장에 관심이 있어 관련 앱을 만들어본 경험이 있습니다. 워터스포츠라는 특수한 시장에서 어떻게 이익을 창출하고 있고 고객들에게 어떤 서비스를 제공하는지 궁금하여 지원해보고 싶습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개인정보처리방침]]></title>
            <link>https://velog.io/@skyu_dev/%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</link>
            <guid>https://velog.io/@skyu_dev/%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</guid>
            <pubDate>Mon, 05 Dec 2022 09:46:24 GMT</pubDate>
            <description><![CDATA[<p>&lt; Quartz &gt;(&#39;<a href="https://velog.io/@skyu_dev&#39;%EC%9D%B4%ED%95%98">https://velog.io/@skyu_dev&#39;이하</a> &#39;Post Black Belt&#39;)은(는) 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.</p>
<p>○ 이 개인정보처리방침은 2022년 12월 1부터 적용됩니다.</p>
<p>제1조(개인정보의 처리 목적)</p>
<p>&lt; Quartz &gt;(&#39;<a href="https://velog.io/@skyu_dev&#39;%EC%9D%B4%ED%95%98">https://velog.io/@skyu_dev&#39;이하</a> &#39;Post Black Belt&#39;)은(는) 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.</p>
<ol>
<li>홈페이지 회원가입 및 관리</li>
</ol>
<p>회원 가입의사 확인 목적으로 개인정보를 처리합니다.</p>
<p>제2조(개인정보의 처리 및 보유 기간)</p>
<p>① &lt; Quartz &gt;은(는) 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.</p>
<p>② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다.</p>
<p>1.&lt;홈페이지 회원가입 및 관리&gt;
&lt;홈페이지 회원가입 및 관리&gt;와 관련한 개인정보는 수집.이용에 관한 동의일로부터&lt;지체없이 파기&gt;까지 위 이용목적을 위하여 보유.이용됩니다.
보유근거 : 회원 정보 기반으로 서비스 제공
관련법령 :
예외사유 :</p>
<p>제3조(처리하는 개인정보의 항목)</p>
<p>① &lt; Quartz &gt;은(는) 다음의 개인정보 항목을 처리하고 있습니다.</p>
<p>1&lt; 홈페이지 회원가입 및 관리 &gt;
필수항목 : 이름
선택항목 :</p>
<p>제4조(개인정보의 제3자 제공에 관한 사항)</p>
<p>① &lt; Quartz &gt;은(는) 개인정보를 제1조(개인정보의 처리 목적)에서 명시한 범위 내에서만 처리하며, 정보주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.</p>
<p>② &lt; Quartz &gt;은(는) 다음과 같이 개인정보를 제3자에게 제공하고 있습니다.</p>
<ol>
<li>&lt; &gt;
개인정보를 제공받는 자 :
제공받는 자의 개인정보 이용목적 :
제공받는 자의 보유.이용기간:</li>
</ol>
<p>제5조(개인정보처리의 위탁에 관한 사항)</p>
<p>① &lt; Quartz &gt;은(는) 원활한 개인정보 업무처리를 위하여 다음과 같이 개인정보 처리업무를 위탁하고 있습니다.</p>
<ol>
<li>&lt; &gt;
위탁받는 자 (수탁자) :
위탁하는 업무의 내용 :
위탁기간 :
② &lt; Quartz &gt;은(는) 위탁계약 체결시 「개인정보 보호법」 제26조에 따라 위탁업무 수행목적 외 개인정보 처리금지, 기술적․관리적 보호조치, 재위탁 제한, 수탁자에 대한 관리․감독, 손해배상 등 책임에 관한 사항을 계약서 등 문서에 명시하고, 수탁자가 개인정보를 안전하게 처리하는지를 감독하고 있습니다.</li>
</ol>
<p>③ 위탁업무의 내용이나 수탁자가 변경될 경우에는 지체없이 본 개인정보 처리방침을 통하여 공개하도록 하겠습니다.</p>
<p>제6조(개인정보의 파기절차 및 파기방법)</p>
<p>① &lt; Quartz &gt; 은(는) 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체없이 해당 개인정보를 파기합니다.</p>
<p>② 정보주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.</p>
<ol>
<li>법령 근거 :</li>
<li>보존하는 개인정보 항목 : 계좌정보, 거래날짜</li>
</ol>
<p>③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.</p>
<ol>
<li><p>파기절차
&lt; Quartz &gt; 은(는) 파기 사유가 발생한 개인정보를 선정하고, &lt; Quartz &gt; 의 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.</p>
</li>
<li><p>파기방법</p>
</li>
</ol>
<p>전자적 파일 형태의 정보는 기록을 재생할 수 없는 기술적 방법을 사용합니다</p>
<p>제7조(정보주체와 법정대리인의 권리·의무 및 그 행사방법에 관한 사항)</p>
<p>① 정보주체는 Quartz에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다.</p>
<p>② 제1항에 따른 권리 행사는Quartz에 대해 「개인정보 보호법」 시행령 제41조제1항에 따라 서면, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 Quartz은(는) 이에 대해 지체 없이 조치하겠습니다.</p>
<p>③ 제1항에 따른 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다.이 경우 “개인정보 처리 방법에 관한 고시(제2020-7호)” 별지 제11호 서식에 따른 위임장을 제출하셔야 합니다.</p>
<p>④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 의하여 정보주체의 권리가 제한 될 수 있습니다.</p>
<p>⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.</p>
<p>⑥ Quartz은(는) 정보주체 권리에 따른 열람의 요구, 정정·삭제의 요구, 처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.</p>
<p>제8조(개인정보의 안전성 확보조치에 관한 사항)</p>
<p>&lt; Quartz &gt;은(는) 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.</p>
<ol>
<li><p>내부관리계획의 수립 및 시행
개인정보의 안전한 처리를 위하여 내부관리계획을 수립하고 시행하고 있습니다.</p>
</li>
<li><p>개인정보에 대한 접근 제한
개인정보를 처리하는 데이터베이스시스템에 대한 접근권한의 부여,변경,말소를 통하여 개인정보에 대한 접근통제를 위하여 필요한 조치를 하고 있으며 침입차단시스템을 이용하여 외부로부터의 무단 접근을 통제하고 있습니다.</p>
</li>
</ol>
<p>제9조(개인정보를 자동으로 수집하는 장치의 설치·운영 및 그 거부에 관한 사항)</p>
<p>Quartz 은(는) 정보주체의 이용정보를 저장하고 수시로 불러오는 ‘쿠키(cookie)’를 사용하지 않습니다.</p>
<p>제10조(행태정보의 수집·이용·제공 및 거부 등에 관한 사항)</p>
<p>행태정보의 수집·이용·제공 및 거부등에 관한 사항</p>
<p>&lt;개인정보처리자명&gt;은(는) 온라인 맞춤형 광고 등을 위한 행태정보를 수집·이용·제공하지 않습니다.</p>
<p>제11조(추가적인 이용·제공 판단기준)</p>
<p>&lt; Quartz &gt; 은(는) ｢개인정보 보호법｣ 제15조제3항 및 제17조제4항에 따라 ｢개인정보 보호법 시행령｣ 제14조의2에 따른 사항을 고려하여 정보주체의 동의 없이 개인정보를 추가적으로 이용·제공할 수 있습니다.
이에 따라 &lt; Quartz &gt; 가(이) 정보주체의 동의 없이 추가적인 이용·제공을 하기 위해서 다음과 같은 사항을 고려하였습니다.
▶ 개인정보를 추가적으로 이용·제공하려는 목적이 당초 수집 목적과 관련성이 있는지 여부</p>
<p>▶ 개인정보를 수집한 정황 또는 처리 관행에 비추어 볼 때 추가적인 이용·제공에 대한 예측 가능성이 있는지 여부</p>
<p>▶ 개인정보의 추가적인 이용·제공이 정보주체의 이익을 부당하게 침해하는지 여부</p>
<p>▶ 가명처리 또는 암호화 등 안전성 확보에 필요한 조치를 하였는지 여부</p>
<p>※ 추가적인 이용·제공 시 고려사항에 대한 판단기준은 사업자/단체 스스로 자율적으로 판단하여 작성·공개함</p>
<p>제12조(가명정보를 처리하는 경우 가명정보 처리에 관한 사항)</p>
<p>&lt; Quartz &gt; 은(는) 다음과 같은 목적으로 가명정보를 처리하고 있습니다.</p>
<p>▶ 가명정보의 처리 목적</p>
<ul>
<li>직접작성 가능합니다.</li>
</ul>
<p>▶ 가명정보의 처리 및 보유기간</p>
<ul>
<li>직접작성 가능합니다.</li>
</ul>
<p>▶ 가명정보의 제3자 제공에 관한 사항(해당되는 경우에만 작성)</p>
<ul>
<li>직접작성 가능합니다.</li>
</ul>
<p>▶ 가명정보 처리의 위탁에 관한 사항(해당되는 경우에만 작성)</p>
<ul>
<li>직접작성 가능합니다.</li>
</ul>
<p>▶ 가명처리하는 개인정보의 항목</p>
<ul>
<li>직접작성 가능합니다.</li>
</ul>
<p>▶ 법 제28조의4(가명정보에 대한 안전조치 의무 등)에 따른 가명정보의 안전성 확보조치에 관한 사항</p>
<ul>
<li>직접작성 가능합니다.</li>
</ul>
<p>제13조 (개인정보 보호책임자에 관한 사항)</p>
<p>① Quartz 은(는) 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.</p>
<p>▶ 개인정보 보호책임자
성명 :유서경
직책 :대표
직급 :대표
연락처 :01089928527, <a href="mailto:skyu.dev@gmail.com">skyu.dev@gmail.com</a>,
※ 개인정보 보호 담당부서로 연결됩니다.</p>
<p>▶ 개인정보 보호 담당부서
부서명 :
담당자 :
연락처 :, ,
② 정보주체께서는 Quartz 의 서비스(또는 사업)을 이용하시면서 발생한 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다. Quartz 은(는) 정보주체의 문의에 대해 지체 없이 답변 및 처리해드릴 것입니다.</p>
<p>제14조(국내대리인의 지정)</p>
<p>정보주체는 ｢개인정보 보호법｣ 제39조의11에 따라 지정된 &lt; Quartz &gt;의 국내대리인에게 개인정보 관련 고충처리 등의 업무를 위하여 연락을 취할 수 있습니다. &lt; Quartz &gt;은(는) 정보주체의 개인정보 관련 고충처리 등 개인정보 보호책임자의 업무 등을 신속하게 처리할 수 있도록 노력하겠습니다.</p>
<p>▶ &lt; Quartz &gt; 은(는) ｢개인정보 보호법｣ 제39조의11에 따라 국내대리인을 지정하였습니다.</p>
<ul>
<li><p>국내대리인의 성명 : [대리인 성명_직접입력] (법인의 경우 법인명, 대표자의 성명)</p>
</li>
<li><p>국내대리인의 주소 : [대리인 주소_직접입력] (법인의 경우 영업소 소재지)</p>
</li>
<li><p>국내대리인의 전화번호 : [대리인 전화번호_직접입력]</p>
</li>
<li><p>국내대리인의 전자우편 주소 : [대리인 전자우편_직접입력]</p>
</li>
</ul>
<p>제15조(개인정보의 열람청구를 접수·처리하는 부서)
정보주체는 ｢개인정보 보호법｣ 제35조에 따른 개인정보의 열람 청구를 아래의 부서에 할 수 있습니다.
&lt; Quartz &gt;은(는) 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.</p>
<p>▶ 개인정보 열람청구 접수·처리 부서
부서명 :
담당자 :
연락처 : , ,</p>
<p>제16조(정보주체의 권익침해에 대한 구제방법)</p>
<p>정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고, 상담에 대하여는 아래의 기관에 문의하시기 바랍니다.</p>
<ol>
<li>개인정보분쟁조정위원회 : (국번없이) 1833-6972 (<a href="http://www.kopico.go.kr">www.kopico.go.kr</a>)</li>
<li>개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)</li>
<li>대검찰청 : (국번없이) 1301 (<a href="http://www.spo.go.kr">www.spo.go.kr</a>)</li>
<li>경찰청 : (국번없이) 182 (ecrm.cyber.go.kr)</li>
</ol>
<p>「개인정보보호법」제35조(개인정보의 열람), 제36조(개인정보의 정정·삭제), 제37조(개인정보의 처리정지 등)의 규정에 의한 요구에 대 하여 공공기관의 장이 행한 처분 또는 부작위로 인하여 권리 또는 이익의 침해를 받은 자는 행정심판법이 정하는 바에 따라 행정심판을 청구할 수 있습니다.</p>
<p>※ 행정심판에 대해 자세한 사항은 중앙행정심판위원회(<a href="http://www.simpan.go.kr">www.simpan.go.kr</a>) 홈페이지를 참고하시기 바랍니다.</p>
<p>제17조(영상정보처리기기 운영·관리에 관한 사항)
① &lt; Quartz &gt;은(는) 아래와 같이 영상정보처리기기를 설치·운영하고 있습니다.</p>
<p>1.영상정보처리기기 설치근거·목적 : &lt; Quartz &gt;의</p>
<p>2.설치 대수, 설치 위치, 촬영 범위 :
설치대수 : 대
설치위치 :
촬영범위 :
3.관리책임자, 담당부서 및 영상정보에 대한 접근권한자 :</p>
<p>4.영상정보 촬영시간, 보관기간, 보관장소, 처리방법
촬영시간 : 시간
보관기간 : 촬영시부터
보관장소 및 처리방법 :
5.영상정보 확인 방법 및 장소 :</p>
<p>6.정보주체의 영상정보 열람 등 요구에 대한 조치 : 개인영상정보 열람.존재확인 청구서로 신청하여야 하며, 정보주체 자신이 촬영된 경우 또는 명백히 정보주체의 생명.신체.재산 이익을 위해 필요한 경우에 한해 열람을 허용함</p>
<p>7.영상정보 보호를 위한 기술적.관리적.물리적 조치 :</p>
<p>제18조(개인정보 처리방침 변경)</p>
<p>① 이 개인정보처리방침은 2022년 12월 1부터 적용됩니다.</p>
<p>② 이전의 개인정보 처리방침은 아래에서 확인하실 수 있습니다.</p>
<p>예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)</p>
<p>예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)</p>
<p>예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Design Pattern] UI Architecture를 적용하여 코드를 분리하자 : MVC, MVP]]></title>
            <link>https://velog.io/@skyu_dev/Design-Pattern-UI-Architecture%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%B6%84%EB%A6%AC%ED%95%98%EC%9E%90-MVC-MVP</link>
            <guid>https://velog.io/@skyu_dev/Design-Pattern-UI-Architecture%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%B6%84%EB%A6%AC%ED%95%98%EC%9E%90-MVC-MVP</guid>
            <pubDate>Fri, 25 Nov 2022 03:15:58 GMT</pubDate>
            <description><![CDATA[<h1 id="0-introduction">#0 Introduction</h1>
<p>유난히 <a href="https://github.com/SeokyoungYou/javascript-bridge/tree/SeokyoungYou">우테코 프리코스 4 주차</a>에서 배우고 나쁜 습관들을 고친 경험이 많은 것 같다. 마지막 주차이기도 하고 지난 주차에 처음으로 피어 리뷰를 받으면서 나는 아직 공부해야 하는 것이 많다는 것을 정말 많이 느꼈다. 지난 <a href="https://github.com/SeokyoungYou/javascript-lotto/tree/SeokyoungYou">3 주차 미션</a>에서는 비슷한 일을 하는 로직과 데이터를 모아 클래스로 만드는 것에 집중했었다. 이번 미션에서는 요구사항들과 지난 미션에서 느낀점을 보완하기 위해 데이터를 관리하는 비즈니스 로직과 사용자와 상호작용하는 User Interface 로직을 분리해보려 한다.</p>
<blockquote>
<p>클래스 내의 비즈니스 로직과 UI 로직을 분리하자!</p>
</blockquote>
<h1 id="1-design-pattern-적용하기">#1 Design Pattern 적용하기</h1>
<p>다른 우테코 프리코스 참여자 분들의 코드 리뷰를 하면서 가장 많이 본 용어는 <code>MVC 패턴</code> 이었다. 또한 이번 미션의 요구 사항에는 <code>BridgeGame</code> 클래스 내에서 사용자와 상호작용하는 <code>InputView</code>  <code>OutputView</code> 객체를 사용하지 않을 것으로 명시되어 있다. 이번 미션에서는 적절한 디자인 패턴을 적용해보는 것을 목표로 삼았다.</p>
<h2 id="11-desgin-pattern이란">#1.1 Desgin Pattern이란?</h2>
<p>많은 개발자들은 SW를 설계할 때 비슷한 문제와 고민을 반복적으로 겪는다. 이를 해결하기 위해 설계를 <strong>올바르고 빠르게</strong> 만들어주는 도구가 디자인 패턴이다. 이 패턴들을 공부할 때 <a href="https://readystory.tistory.com/114">다음 4 가지 요소들을 함께 고려하면 수월할 것</a>이라고 한다. </p>
<blockquote>
<ol>
<li>패턴 이름(Pattern Name): 패턴 이름은 솔루션을 담고 있다.</li>
<li>문제(Problem): 해결할 문제가 무엇인지 파악한다.</li>
<li>해결 방법 &amp; 구현(Solution &amp; Implementation)</li>
<li>결과(Consequence): 디자인 패턴을 적용해서 얻는 결과와 장단점을 파악한다.</li>
</ol>
</blockquote>
<h1 id="2-desgin-patterns-for-ui-architectures">#2 Desgin Patterns for UI Architectures</h1>
<p>UI, 데이터 및 논리 제어를 구현하는 SW를 위한 디자인 패턴은 다양하다. 그 중 MVC와 MVP에 대해 알아보고 이번 미션에서 MVP 패턴을 적용하게 된 이유에 대해 설명해보려 한다.</p>
<p>MVC와 MVP는 공통적으로 비즈니스 로직과 화면을 구분하는데 중점을 두어 <strong>관심사를 분리</strong>한다. 이 개념은 얼마전 <a href="https://youtu.be/fR8tsJ2r7Eg">토스의 개발자 컨퍼런스 영상</a>에서 보았던 더 나은 프론트엔드 컴포넌트를 만들고 분리하는 방법들과 맞닿아 있다고 생각한다. Headless 기반의 추상화는 디자인에 의존하는 UI와 데이터를 분리하는 방법인데, 이 방법론에서는 headless UI component라는 기능은 있지만 스타일이 없는 컴포넌트를 제시한다.</p>
<blockquote>
<p>결국 프론트엔드 개발에서 겪는 반복적인 문제들을 해결하기 위한 다양한 방법론들도 UI 디자인 패턴들에서 유래한다.</p>
</blockquote>
<h2 id="21-mvcmodel-view-controller">#2.1 MVC(Model-View-Controller)</h2>
<p>MVC는 가장 대중화된 UI 디자인 패턴이며, 코드를 Model-View-Controller로 분리하여 역할을 나눈다. 사실 &quot;이것이 MVC다!&quot;라는 명확한 기준이 있는 것 같지는 않고 사람마다 다르게 해석하여 적용하고 있는 것 같다. 이 글에서는 내가 공부하고 이해한 기준으로 작성할 예정이다.</p>
<blockquote>
<p>사용자의 입력을 받아 다리(bridge)를 건너는 게임을 앱으로 만든다고 가정해보자.</p>
</blockquote>
<h3 id="211-패턴-이름-확인하기">#2.1.1 패턴 이름 확인하기</h3>
<h4 id="model데이터">Model(데이터)</h4>
<p>앱이 다루는 가변적인 데이터이다. 프론트엔드에서는 화면에 나타내는 데이터, 서버의 API와 DB로부터 전달받는 데이터를 이야기한다.
ex) 사용자의 입력값, 사용자가 건널 다리의 정보 등</p>
<h4 id="view화면">View(화면)</h4>
<p>앱의 데이터를 사용자에게 보여주는 화면이다. 프론트엔드에서는 HTML, CSS로 만들어져 브라우저에 렌더링되는 코드를 이야기한다.
ex) 사용자와 상호 작용하는 부분</p>
<h4 id="controller명령">Controller(명령)</h4>
<p>Model의 데이터를 받아 View에 나타내고 View로부터 사용자의 입력을 받아 Model을 변경하는 중간 다리 역할을 한다. 모델을 변경하지 않고도 데이터를 다른 방식으로 나타내고 싶을 때 컨트롤러에서 이를 처리하기도 한다.</p>
<h3 id="212-3-문제-상황과-해결-방법">#2.1.2-3 문제 상황과 해결 방법</h3>
<p>MVC 패턴은 각각의 역할을 나누어 코드를 분리하여 유지보수와 개발 효율성을 높이고 싶어서 도입하였으며, 다음과 같은 동작 순서를 가진다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/224c80a9-d0ee-476b-b52f-074fcdf135f5/image.png" alt=""></p>
<blockquote>
<p>사용자의 action &gt; <strong>Controller</strong>가 action을 확인하고 <strong>Model</strong>을 업데이트 &gt; <strong>Controller</strong>가 Model의 데이터를 나타낼 <strong>View</strong>를 선택 &gt; <strong>View</strong>는 <strong>Model</strong>을 사용하여 데이터를 나타냄</p>
</blockquote>
<h3 id="214-결과consequence">#2.1.4 결과(Consequence)</h3>
<ul>
<li>장점: 패턴이 단순하다.</li>
<li>단점: View와 Model 사이의 의존성이 높다.</li>
</ul>
<h2 id="22-mvpmodel-view-presenter">#2.2 MVP(Model-View-Presenter)</h2>
<p>나는 UI와 데이터 로직을 나누기 위해 MVC 패턴을 도입하려 했으나, 여전히 View와 Model 사이에 의존성이 있는 것을 보고 MVP 패턴에 대해 추가로 공부해보기로 하였다. </p>
<h3 id="221-패턴-이름-확인하기">#2.2.1 패턴 이름 확인하기</h3>
<p>Model, View의 개념은 MVC와 동일하다.</p>
<h4 id="presenter명령">Presenter(명령)</h4>
<p>View에서 요청한 정보로 Model을 가공하여 View에게 전달한다. Presenter가 View &amp; Model의 인스턴스를 가지고 있어 둘을 연결하는 역할을 한다.</p>
<h3 id="222-3-문제-상황과-해결-방법">#2.2.2-3 문제 상황과 해결 방법</h3>
<p>MVC 패턴에서 나타나는 View와 Model의 의존성 문제를 해결하기 위해 Presenter를 통해서만 View에 데이터를 전달한다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/82f5c5f3-80ba-4871-aba6-31ea74262344/image.png" alt=""></p>
<blockquote>
<p>사용자의 action &gt; <strong>View</strong>가 action을 확인하고 <strong>Presenter</strong>에 데이터 요청 &gt; <strong>Presneter</strong>가 <strong>Model</strong>에 데이터 요청 &gt; <strong>Model</strong>이 <strong>Presenter</strong>의 데이터 요청에 응답 &gt; <strong>Presneter</strong>기 <strong>View</strong>의 데이터 응답 &gt; <strong>View</strong>는 <strong>Presenter</strong>가 제공한 데이터를 화면에 그림</p>
</blockquote>
<h3 id="214-결과consequence-1">#2.1.4 결과(Consequence)</h3>
<ul>
<li>장점: View와 Model의 의존성이 없다.</li>
<li>단점: 앱이 복잡해질수록 Viewdhk Presenter 사이의 의존성이 강해진다.<h2 id="23-mvc-vs-mvp-적용할-패턴-선정하기">#2.3 MVC vs MVP: 적용할 패턴 선정하기</h2>
<img src="https://velog.velcdn.com/images/skyu_dev/post/f155fbc2-10f5-4b82-a50b-2cf9769921af/image.png" alt="">
나는 2 가지의 이유로 두 패턴 중 MVP를 선정하였다. 처음부터 패턴을 선정하기보다는 비즈니스-UI 로직을 분리하는 코드를 짜보면서 내가 추구하는 방향성과 MVP 패턴이 더 잘 맞는다고 생각하였다.</li>
</ul>
<ol>
<li><p>미션 요구사항 충족
우테코 4 주차 미션에서는 <code>InputView</code> <code>OutputView</code> 객체를 사용하고, <code>BridgeGame</code> 클래스에서는 View 관련 코드를 작성하지 말 것이라는 요구사항이 있었다. MVC에서는 View를 제외한 컴포넌트들에서 모두 UI를 업데이트할 수 있으므로 적합하지 않다고 판단하였고, MVP를 사용하여 <code>BridgeGame</code> 클래스를 Model로 사용하는 것으로 결정하였다.</p>
</li>
<li><p>사용자 action이 발생하는 위치 관점
사용자의 action은 MVC에서는 Controller, MVP에서는 View를 통해 들어온다. 나는 이 의미를 <code>inputView</code>를 Controller로 사용하여 MVC로 사용하거나, <code>InputView</code>를 View로 사용하여 MVP 패턴을 적용하는 것으로 받아들였다. 나는 후자가 요구사항에 더 적합하다고 생각하였으므로 최종적으로 MVP 패턴을 선택하게 되었다.</p>
</li>
</ol>
<h1 id="3-다리-건너기-게임에-mvp-적용하기">#3 다리 건너기 게임에 MVP 적용하기</h1>
<p>사실 개발자마다 디자인 패턴을 조금씩 다르게 이해하고 있으므로 이름만으로 패턴을 나누는 것은 무의미하다고 생각한다. 내가 본 미션에서 적용한 디자인 패턴은 아래와 같으며, <a href="https://github.com/SeokyoungYou/javascript-bridge/tree/SeokyoungYou">이 링크에서 우테코 4주차 코드</a>의 결과물을 확인할 수 있다.</p>
<ul>
<li>해결할 문제: 비즈니스-UI 로직의 분리. View(UI)와 Model(데이터) 컴포넌트의 완전한 분리</li>
<li>Model: 다리 건너기 게임과 관련된 데이터를 다룬다.<ul>
<li>Bridge: 건너야할 다리의 정보</li>
<li>Player: 플레이어의 움직임과 관련된 정보</li>
</ul>
</li>
<li>View: UI 로직을 다룬다.<ul>
<li>InputVew: 사용자의 입력(action) 받기</li>
<li>outputVew: 사용자에게 보여줄 메시지 출력하기</li>
</ul>
</li>
<li>Presenter: 앱의 흐름을 다루고 View-Model을 이어주는 역할을 한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/5d7a10db-b9a6-4082-9124-79bc265793bc/image.png" alt=""></p>
<h1 id="마치며">마치며</h1>
<p>프론트엔드의 컴포넌트 디자인 패턴을 배우면서 &quot;프레임워크/라이브러리에 종속된 학습법이 아닌가?&quot;라고 고민한 적이 있었다. 프리코스를 통해 보편적인 UI 디자인 패턴을 배우면서, 개발자들이 왜 이러한 개념을 만들어냈는지를 함께 고민해보니 그 부분이 어느정도 해소된 것 같다. 바닐라 자바스크립트를 넘어서 다음 리액트 프로젝트에서는 이러한 패턴을 적용하여 코드를 분리해보는 연습을 할 예정이다.</p>
<h4 id="참고자료">참고자료</h4>
<p><a href="https://developer.mozilla.org/ko/docs/Glossary/MVC">MDN: MVC 용어사전</a>
<a href="https://velog.io/@teo/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-MV-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94">프론트엔드에서 MV* 아키텍쳐란 무엇인가요?</a>
<a href="https://readystory.tistory.com/114">디자인 패턴(Design Pattern)이란?</a>
<a href="https://beomy.tistory.com/43">[디자인패턴] MVC, MVP, MVVM 비교</a>
<a href="https://youtu.be/fR8tsJ2r7Eg">토스ㅣSLASH 22 - Effective Component 지속 가능한 성장과 컴포넌트</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Clean Code] 함수의 길이 줄이기(2): If/else ➡️ Bitwise shift/Array & Object mapping 으로 대체해보자]]></title>
            <link>https://velog.io/@skyu_dev/Clean-Code-%ED%95%A8%EC%88%98%EC%9D%98-%EA%B8%B8%EC%9D%B4-%EC%A4%84%EC%9D%B4%EA%B8%B01-ifelse%EC%99%80-switch-%EB%AC%B8-object-mapping-%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%B4%EB%B3%B4%EC%9E%90-hgg9ztv1</link>
            <guid>https://velog.io/@skyu_dev/Clean-Code-%ED%95%A8%EC%88%98%EC%9D%98-%EA%B8%B8%EC%9D%B4-%EC%A4%84%EC%9D%B4%EA%B8%B01-ifelse%EC%99%80-switch-%EB%AC%B8-object-mapping-%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%B4%EB%B3%B4%EC%9E%90-hgg9ztv1</guid>
            <pubDate>Mon, 21 Nov 2022 11:02:23 GMT</pubDate>
            <description><![CDATA[<h1 id="0-introduction">#0 Introduction</h1>
<p><a href="https://velog.io/@skyu_dev/Clean-Code-%ED%95%A8%EC%88%98%EC%9D%98-%EA%B8%B8%EC%9D%B4-%EC%A4%84%EC%9D%B4%EA%B8%B01-ifelse%EC%99%80-switch-%EB%AC%B8-object-mapping-%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%B4%EB%B3%B4%EC%9E%90">지난 글</a>에 이어서 분기 처리 코드를 줄이는 방법 중 boolean 조건을 가질 때 처리 방법에 대해 알아보자. 예시 코드는 <a href="https://github.com/SeokyoungYou/javascript-bridge/tree/SeokyoungYou">우테코 프리코스 4 주차 미션</a>에서 가져왔다. </p>
<p>불리언도 문자열과 마찬가지로 object mapping을 사용하여 분기 처리를 할 수 있다. 다만, 자바스크립트에서는 key를 문자열로만 저장하므로 아래와 같이 dynamic key를 사용하여야 boolean key를 적용할 수 있다.</p>
<pre><code class="language-js">const MAP = {
  [true]: // 해당 코드,
  [false]: // 해당 코드,
};</code></pre>
<p>하지만 비교해야 할 boolean 값들이 여러 개라면 어떻게 처리해야 할까?  예를 들어 2 개의 불리언 상태를 사용한다면 우리는 4 개의 경우의 수를 생각할 수 있다. 그러나 boolean 조건을 연산한 결과는 결국 <code>true</code> <code>false</code> 두 개의 결과만을 가지므로 우리는 4 개가 아닌 2 개의 dynamic key만을 갖게 되는 문제가 발생한다.</p>
<blockquote>
<p>여러 boolean 값을 하나의 string으로 처리하여 이전 글과 동일하게 object mapping을 사용해보자!</p>
</blockquote>
<h1 id="1-boolean-조건문의-분기-처리">#1 Boolean 조건문의 분기 처리</h1>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/1d63025c-0494-4307-9b99-241b3186274a/image.jpeg" alt=""></p>
<h2 id="11-ifelse-문을-사용하기">#1.1 If/else 문을 사용하기</h2>
<p>우리는 일반적으로 두 개의 boolean 상태 <code>isFinish</code> <code>isMove</code> 를 연산하여 아래와 같은 분기 처리를 한다. Boolean 상태가 많아질수록 코드가 점점 더 길어질 것이며, 조건문에 각 상태의 <code>true</code> <code>false</code> 여부가 뒤섞여 가독성도 나빠질 것이다.</p>
<pre><code class="language-js">checkContinueMove() {
  const { isFinish, isMove } = this.getBooleans();
  if (!isFinish &amp;&amp; isMove) {
    this.getPlayerMove();
  } else if (!isMove) {
    this.getGameCommand();
  } else if (isFinish &amp;&amp; isMove) {
    this.quit();
  }
}</code></pre>
<h2 id="12-비트-연산과-배열-사용하기">#1.2 비트 연산과 배열 사용하기</h2>
<p>이 문제를 해결하기 위해서 다음의 방법을 사용할 수 있다.</p>
<blockquote>
<ol>
<li>여러 boolean 상태 ➡️ 하나의 숫자로 나타내기</li>
<li>해당 숫자를 array의 index로 사용하여 mapping</li>
</ol>
</blockquote>
<p>참과 거짓으로 나타나는 boolean 값은 컴퓨터에서는 1과 0의 숫자로 인식된다. 아래 그림의 좌측과 같이 N 개의 boolean 상태들을 나열하면 1과 0의 집합이 되고, 이들을 이진수의 각 N -1 자리수에 대입하면 하나의 이진수로 대체할 수 있다. 
<img src="https://velog.velcdn.com/images/skyu_dev/post/f56277fb-5973-4a41-9df1-69f194109fd6/image.png" alt=""></p>
<h3 id="121-비트-연산">#1.2.1 비트 연산</h3>
<p>앞서 다룬 <code>isFinish</code> <code>isMove</code> 2 개의 boolean 상태를 다시 예시로 들어보자. 각 boolean 상태는 0 과 1 두 개의 값을 가지며 경우의 수는 4 가지이다. </p>
<h4 id="left-shift-n-개의-boolean-저장하기">Left shift(&lt;&lt;): N 개의 boolean 저장하기</h4>
<p>각 boolean 값을 하나의 이진수에 저장하기 위해서는 bitwise left shift(&lt;&lt;) 연산자를 사용한다. n 번 shift(&lt;&lt;)하면 이진수의 n 자리에 저장된다.</p>
<pre><code class="language-js">const binary1 = true; // 이진수: 1, 십진수:1
const binary2 = true &lt;&lt; 1; // 이진수: 10, 십진수:2
const binary3 = true &lt;&lt; 2; // 이진수: 100, 십진수:4
const binary4 = true &lt;&lt; 2 | true ; // 이진수: 101, 십진수:5</code></pre>
<h4 id="or-n-개의-boolean-➡️-1-개의-숫자-변환하기">OR(|): N 개의 boolean ➡️ 1 개의 숫자 변환하기</h4>
<p>이를 활용하여 우리는 두 개의 boolean 값을 아래와 같이 하나의 <code>playerState</code>로 나타낼 수 있다. 이 값은 4 가지 경우의 수를 가지며 십진수로 0, 1, 2, 3로 나타난다. </p>
<pre><code class="language-js">const playerState = (isFinish &lt;&lt; 1) | isMove;</code></pre>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/05d4f3fe-829d-415a-8ff4-1e95abbcc5db/image.png" alt=""></p>
<h3 id="122-boolean-조건문-분기-처리하기">#1.2.2 Boolean 조건문 분기 처리하기</h3>
<p>이를 활용하여 우리는 #1.1의 메인로직을 아래와 같이 간결하게 나타낼 수 있다. </p>
<pre><code class="language-js">// Main logic
checkContinueMove() {
  const { isFinish, isMove } = this.getBooleans();
  const playerState = createPlayerState(isFinish, isMove);
  PLAYER_STATE_FN[playerState](this);
}
</code></pre>
<p>비트 연산을 활용하여 두 개의 boolean 값을 하나의 숫자로 나타내고, <code>PLAYER_STATE_ARRAY</code>에서 해당 <code>PLAYER_STATE</code>를 인덱스로 하는 값을 찾는다. 그리고 이전 글과 같이 object mapping으로 함수를 key로 가져와 활용하면 분기 처리 로직을 단순화할 수 있다.</p>
<pre><code class="language-js">// Bitwise shift
const createPlayerState = (isFinish, isMove) =&gt; {
  const state = (isFinish &lt;&lt; 1) | isMove;
  return PLAYER_STATE_ARRAY[state];
};

// Array mapping
const PLAYER_STATE_ARRAY = [
  &quot;retry&quot;,
  &quot;continue&quot;,
  &quot;retry&quot;,
  &quot;success&quot;,
];

// Object mapping
const PLAYER_STATE_FN = {
  retry(gamePresenter) {
    gamePresenter.getGameCommand();
  },
  continue(gamePresenter) {
    gamePresenter.getPlayerMove();
  },
  success(gamePresenter) {
    gamePresenter.quit();
  },
};
</code></pre>
<p>예를들어 <code>isFinish=true</code> <code>isMove=false</code> 인 경우,
<code>playerState</code>는 2로 반환되고 배열의 세 번째 값인 <code>retry</code>가 맵핑되어
<code>gamePresenter.getGameCommand()</code> 함수가 실행된다.</p>
<h3 id="121-비트-연산-1">#1.2.1+ 비트 연산</h3>
<h4 id="and-1-개의-boolean-상태-가져오기">AND(&amp;): 1 개의 boolean 상태 가져오기</h4>
<p>추가로 and 연산자를 활용하면 하나로 합친 이진수 값에서 해당 자리 수의 boolean 값을 가져올 수도 있다.</p>
<pre><code class="language-js">const isFinish = playerState &amp; ( 1 &lt;&lt; 1 );</code></pre>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/fc20acf4-b6b9-4f3e-b5f2-dee23c88580b/image.png" alt=""></p>
<h1 id="마치며">마치며</h1>
<p>비트 연산의 개념만 알고 직접 적용해보지는 못했었는데 분기 처리에 활용해보니 왜 기초 CS 지식이 중요한지 알게되었다. 여전히 코드가 얽혀있고 복잡하지만 한 번에 독파한다는 생각보다는 이렇게 하나씩 개선해나가는 것도 좋은 방법이라 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Clean Code] 함수의 길이 줄이기(1): if/else 혹은 switch 문 ➡️ object mapping 으로 변경해보자]]></title>
            <link>https://velog.io/@skyu_dev/Clean-Code-%ED%95%A8%EC%88%98%EC%9D%98-%EA%B8%B8%EC%9D%B4-%EC%A4%84%EC%9D%B4%EA%B8%B01-ifelse%EC%99%80-switch-%EB%AC%B8-object-mapping-%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@skyu_dev/Clean-Code-%ED%95%A8%EC%88%98%EC%9D%98-%EA%B8%B8%EC%9D%B4-%EC%A4%84%EC%9D%B4%EA%B8%B01-ifelse%EC%99%80-switch-%EB%AC%B8-object-mapping-%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 21 Nov 2022 09:05:59 GMT</pubDate>
            <description><![CDATA[<h1 id="0-introduction">#0 Introduction</h1>
<p>어느덧 우테코 프리코스 마지막 주차에 들어섰다. 4 주라는 시간이 짧게 느껴질 정도로 정말 많은 것을 공부했고 코드를 짜는 방식도 발전했다고 느낀다. 혼자 개발을 할 때는 제약 사항없이 떠오르는대로 타이핑했다면 최근에는 리팩토링에 조금 더 신경을 써서 가독성이 좋고 유지보수가 편한 코드를 만드려고 노력하고 있다.</p>
<p>이 글에서는 함수의 역할을 단순화하고 길이를 줄이는 방법에 대해 설명하려 한다. <a href="https://github.com/SeokyoungYou/javascript-bridge/tree/SeokyoungYou">우테코 프리코스 4 주차 미션</a>에서는 아래와 같은 제약 사항이 있었다. </p>
<blockquote>
<p>⚠️ 제약 사항</p>
</blockquote>
<ul>
<li>함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.</li>
<li>함수(또는 메서드)가 한 가지 일만 잘하도록 구현한다.</li>
<li>indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.<ul>
<li>예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.</li>
<li>힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.</li>
</ul>
</li>
</ul>
<p>함수의 길이가 길어지는 예시 중 하나인 분기 처리문을 그동안 사용했던 <code>if/else</code> 외의 다른 방법을 사용하여 구현해보자.</p>
<h2 id="01-제약사항의-eslint-설정">#0.1 제약사항의 ESLint 설정</h2>
<p>#0에서 제시한 제약 사항에 대해 ESLint 규칙을 설정하면 아래와 같다.</p>
<pre><code class="language-js">rules: {
  &quot;max-depth&quot;: [&quot;error&quot;, 2],
  &quot;max-lines-per-function&quot;: [&quot;error&quot;, 10],
}</code></pre>
<h1 id="1-string-조건문의-분기-처리">#1. String 조건문의 분기 처리</h1>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/4928d49b-066a-4bdf-9183-d8b3744d4f37/image.jpeg" alt="">
아래와 같은 <code>INPUT_TYPE</code>에 따라 각각 다른 함수를 호출하는 분기 처리를 한다고 가정해보자.</p>
<pre><code class="language-js">const INPUT_TYPE = {
  SIZE: &quot;size&quot;,
  MOVING: &quot;moving&quot;,
  GAME_COMMAND: &quot;gameCommand&quot;,
};</code></pre>
<h2 id="11-ifelse-문-사용하기">#1.1 If/else 문 사용하기</h2>
<p>사용자로부터 받은 <code>input</code> 값을 처리하는 <code>handleInput</code> 함수는 아래와 같이 조건문으로 나타낼 수 있다. <code>INPUT_TYPE</code> 의 문자열을 조건으로 받아 각각 다른 함수를 직접 호출한다. 이 때 <code>Validation</code> 함수에서 에러가 발생할 경우 try-catch 문을 사용해 에러를 출력하고 각각의 폴백 함수를 호출한다.</p>
<p>안타깝게도 이 코드는 길이도 길고, 분기 처리 이후 try-catch 로직이 반복된다는 문제가 있다.</p>
<blockquote>
<p><strong>반복되는 로직</strong> 
inputType 분기처리 &gt; try <code>Validation &amp; 다음 함수 호출</code> / catch <code>error 메시지 출력 &amp; 폴백 함수 호출</code></p>
</blockquote>
<pre><code class="language-js">handleInput(input, inputType) {
  if (inputType === &quot;size&quot;) {
    try {
      Validation.size(input);
      this.createBridge(input);
    } catch (errorMsg) {
      OutputView.printError(errorMsg);
      this.getBridgeSize();
    }
  } else if (inputType === &quot;moving&quot;) {
    try {
      Validation.moving(input);
      this.checkmove(input);
    } catch (errorMsg) {
      OutputView.printError(errorMsg);
      this.getPlayerMove();
    }
  } else if (inputType === &quot;gameCommand&quot;) {
    try {
      Validation.gameCommand(input);
      this.Validation.checkRetryInput(input);
    } catch (errorMsg) {
      OutputView.printError(errorMsg);
      this.getPlayerMove();
    }
  }
}</code></pre>
<h2 id="12-switch-문-사용하기">#1.2 Switch 문 사용하기</h2>
<p>If/else 대신 switch 문을 사용해도 분기 처리 이후 try-catch 로직이 반복될 것을 예상할 수 있다. 또한 케이스가 많아질수록 함수의 길이는 점점 더 길어질 것이다..</p>
<pre><code class="language-js">handleInput(input, inputType) {
  switch (inputType) {
    case &#39;size&#39;:  
      ...
      [break]
    case &#39;moving&#39;:  
      ...
      [break]
    case &#39;gameCommand&#39;:  
      ...
      [break]
    default:
      ...
      [break]
}</code></pre>
<h2 id="13-object-mapping-사용하기">#1.3 Object mapping 사용하기</h2>
<p>그렇다면 어떤 방법을 사용해야 <code>handleInput</code> 함수의 분기 처리를 10 줄 이내로 만들 수 있을까? 나는 다음의 <a href="https://youtu.be/toUlXhTZZ8w">[프롱트] 유튜브 참고 영상</a>을 보고 문자열이 아니라 함수도 객체로 맵핑할 수 있다는 것을 배웠다. Object mapping을 사용하면 분기 처리를 간결하게 만들어 메인 로직을 간단하게 정리할 수 있다.</p>
<h3 id="131-메인-로직">#1.3.1 메인 로직</h3>
<p>각 분기 처리에서 중복되는 함수를 <code>Validation</code>, <code>INPUT_TRY_FN</code>, <code>INPUT_CATCH_FN</code> 객체로 만들고, <code>inputType</code> 을 key 값으로 받아 각 함수를 실행한다.</p>
<pre><code class="language-js">handleInput(input, inputType) {
  try {
    Validation[inputType](input);
    INPUT_TRY_FN[inputType](this, input);
  } catch (errorMsg) {
    OutputView.printError(errorMsg);
    INPUT_CATCH_FN[inputType](this);
  }
}</code></pre>
<h3 id="132-object-mapping을-사용한-함수-분기-실행">#1.3.2 Object mapping을 사용한 함수 분기 실행</h3>
<h4 id="함수를-key로-object-mapping">함수를 key로 object mapping</h4>
<p>중복되는 try-catch 로직은 <code>INPUT_TRY_FN</code> <code>INPUT_CATCH_FN</code> 객체에 각 함수를 저장한다. <code>inputType</code> 를 객체의 key 이름으로 하여 타입에 따라 해당 함수가 실행되도록 하는 것이다.</p>
<pre><code class="language-js">const INPUT_TRY_FN = {
  size(gamePresenter, size) {
    gamePresenter.createBridge(size);
  },
  moving(gamePresenter, selectedMove) {
    gamePresenter.checkmove(selectedMove);
  },
  gameCommand(gamePresenter, retry) {
    gamePresenter.checkRetryInput(retry);
  },
};

const INPUT_CATCH_FN = {
  size(gamePresenter) {
    gamePresenter.getBridgeSize();
  },
  moving(gamePresenter) {
    gamePresenter.getPlayerMove();
  },
  gameCommand(gamePresenter) {
    gamePresenter.getGameCommand();
  },
};</code></pre>
<p>예를 들어 <code>INPUT_CATCH_FN[size](this);</code>를 호출하면 <code>gamePresenter.getPlayerMove();</code>가 실행된다.</p>
<h4 id="class의-static-method-사용">Class의 static method 사용</h4>
<p>Input 값의 검증을 위한 로직은 Validation 클래스를 사용하여 재사용성을 높였다. 이 경우에도 <code>inputType</code>을 클래스의 메소드 이름으로 사용하면 분기 처리를 간단하게 할 수 있다.</p>
<pre><code class="language-js">class Validation {
  static validate({ condition, message }) {
    if (!condition) {
      // throw new Error(message);
      throw message;
    }
  }

  static size(number) {
    const isInRange = number &gt;= BRIDGE.SIZE_MIN &amp;&amp; number &lt;= BRIDGE.SIZE_MAX;
    this.validate({ condition: isInRange, message: INPUT_VAL.SIZE_ERROR });
  }

  static moving(string) {
    const isMove = string === USER_INPUT.UP || string === USER_INPUT.DOWN;
    this.validate({ condition: isMove, message: INPUT_VAL.MOVING_ERROR });
  }

  static gameCommand(string) {
    const isRetry = string === USER_INPUT.RETRY || string === USER_INPUT.QUIT;
    this.validate({ condition: isRetry, message: INPUT_VAL.RETRY_ERROR });
  }
}</code></pre>
<h1 id="마치며">마치며</h1>
<p>타 로직들은 함수가 하나의 일만 하도록 잘게 쪼개주면 대부분 함수의 길이를 10 줄 이내로 만족했다. 코드의 반복을 줄이기 위해 분기 처리를 할 때 다시 함수의 길이가 길어지면서 굉장히 당황했던 기억이 있다. Object mapping을 사용하여 메인 로직을 추상화하는 것에는 성공한 것 같다.</p>
<p>문자열의 분기 처리는 함수/메소드의 이름을 key로 사용하여 해결했다. 그러나 조건문이 string이 아닌 boolean 값일 때에는 어떻게 처리해야 할까? 이 내용은 다음 글에 설명하려 한다.</p>
<h4 id="참고-자료">참고 자료</h4>
<p><a href="https://youtu.be/toUlXhTZZ8w">if만 제거했을뿐인데.. 클린코드라니</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Jest] 테스트 코드로 JS 의 기능 및 로직 점검하기]]></title>
            <link>https://velog.io/@skyu_dev/Jest-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-JS%EC%9D%98-%EA%B8%B0%EB%8A%A5-%EC%A0%90%EA%B2%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/Jest-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-JS%EC%9D%98-%EA%B8%B0%EB%8A%A5-%EC%A0%90%EA%B2%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 06 Nov 2022 13:46:46 GMT</pubDate>
            <description><![CDATA[<h1 id="0-introduction">0. Introduction</h1>
<p>Testing이란 코드의 에러를 확인하고 추후 나타날 수 있는 이슈와 버그를 예측하는 과정이다. 지금까지의 나는 <code>console.log</code>나 터미널에 뜬 에러를 확인하는 방법으로 이를 대응해왔다. 더 개발자다운 방법으로 테스팅은 여러 툴을 사용하여 자동화할 수 있다. 이 글에서는 우테코 프리코스 2 주차를 수강하며 공부한 Jest에 대해 간결하게 정리해보려 한다.</p>
<h1 id="1-testing">1. Testing</h1>
<h2 id="11-testing-단위에-따라-분류하기">1.1 Testing 단위에 따라 분류하기</h2>
<p>테스트는 단위에 따라 아래와 같이 3 가지로 나눌 수 있다. 아무런 의존성 없이 하나의 로직을 테스트하는 경우를 unit test, 다른 함수에 의존성을 가진 로직을 테스트하는 경우를 integration test, 그리고 하나의 큰 기능을 테스트하는 경우를 E2E test라고 부른다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/d3580a37-ba4b-457d-ad5b-916a60ef1852/image.png" alt=""></p>
<h2 id="12-testing-환경">1.2 Testing 환경</h2>
<p>Test Runner는 일반적으로 테스트를 실행하고 결과를 보여주는 환경이다. Assertion Library를 여기에 추가하면 mock, spy, stub 등 테스트에 필요한 유틸리티를 활용할 수 있다. 더불어 Headless Browser를 사용하게 되면  실제 브라우저 환경에서 발생하는 interaction도 가상으로 테스트해볼 수 있다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/053d52d6-6020-4d9c-bb2f-62da8d774526/image.png" alt=""></p>
<blockquote>
<p>이 글에서는 Jest를 사용한 Unit + Integration test를 다룰 예정이다.</p>
</blockquote>
<h1 id="2-jest란">2. Jest란?</h1>
<p>Jest는 페이스북에서 만든 JavaScript testing framework이다. config를 따로 설정하지 않아도 빠르게 테스팅 환경을 만들 수 있다는 것이 큰 장점이다. 2 장에서의 기본 개념과 코드는 <a href="https://youtu.be/g4MdUjxA-S4">코딩앙마님의 Jest 강의</a>와 <a href="https://www.udemy.com/course/javascript-zw/">유데미의 JS 강의</a>중 테스팅 모듈 파트를 참고하여 작성하였다.</p>
<h2 id="21-설치-방법-및-사용-방법">2.1 설치 방법 및 사용 방법</h2>
<h3 id="211-jest-설치하기">2.1.1 Jest 설치하기</h3>
<ol>
<li><p><code>$ npm i --save-dev jest</code></p>
<p> 개발자 도구이므로 <code>-save-dev</code>를 붙여 설치한다.</p>
</li>
<li><p><code>$ npm test</code> 로 테스트 실행</p>
<pre><code class="language-json">package.json
 &quot;scripts&quot;: {
   &quot;test&quot;: &quot;jest&quot;
 },</code></pre>
</li>
</ol>
<h3 id="212-jest-사용하기">2.1.2 Jest 사용하기</h3>
<p>Jest는 <code>.test</code> <code>.spec</code>가 포함되거나 <code>__test__</code> 폴더 내에 있는 파일을 감지하여 테스트를 실행한다.</p>
<p>예를 들어 아래와 add 함수를 테스팅한다면,</p>
<pre><code class="language-js">fn.js // 테스트할 함수

const fn = {
  add: (num1, num2) =&gt; num1 + num2,
};

module.exports = fn; // 모듈화하여 내보내기</code></pre>
<p>아래와 같이 테스트 코드를 작성할 수 있다.</p>
<ul>
<li><code>expect(테스트할 함수).toBe(기대하는 결과값)</code></li>
</ul>
<pre><code class="language-js">fn.test.js // 테스트 코드

const { default: test } = require(&quot;node:test&quot;);
const fn = require(&quot;./fn&quot;);

test(&quot;2 더하기 3은 5야.&quot;, () =&gt; {
  expect(fn.add(2,3)).toBe(5);
});</code></pre>
<h2 id="21-유용한-matchers">2.1 유용한 matchers</h2>
<p>Matcher는 테스트 대상 함수에서 반환된 값을 예측하기 위해 사용한다.</p>
<h3 id="211-primitive-value">2.1.1 Primitive value</h3>
<p>예측하는 값이 원시값(number, string, boolean 등)일 때는 다음의 macher를 자주 사용한다.</p>
<ul>
<li><code>toBe()</code> : 해당 값과 일치하면 통과</li>
<li><code>toBeNull()</code> <code>toBeUndefined()</code> <code>toBeDefined()</code> :  인 경우 통과</li>
<li><code>toBeTruthy()</code> <code>toBeFasly()</code> : boolean 값 판별</li>
<li><code>toBeGreaterThan</code> 등… : 이상, 이하, 초과, 미만</li>
<li><code>toMatch(/H/)</code> : 정규 표현식으로 문자열 판단</li>
</ul>
<h3 id="212-reference-value">2.1.2 Reference value</h3>
<ul>
<li><code>toEqual()</code> : 참조값(객체, 배열)을 비교할 때. 해당 값 포함하면 true</li>
<li><code>toStrictEqual()</code> : 더 엄격한 비교. 완전히 똑같아야 true</li>
<li><code>toContain()</code> : 배열에서 아이템 포함되어 있는지<ul>
<li>ex) <code>expect([&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]).toContain(&quot;a&quot;)</code><h3 id="213-error-발생-여부">2.1.3 Error 발생 여부</h3>
테스트에서 <code>throw new Error(&quot;Error msg&quot;)</code> 등의 에러 발생 여부를 체크하기 위해서는 아래와 같은 코드를 작성한다. 에러 메시지를 특정하여 잡아내는 것도 가능하다.<pre><code class="language-js">fn.test.js // 테스트 코드
const fn = require(&quot;./fn&quot;);
</code></pre>
</li>
</ul>
</li>
</ul>
<p>test(&quot;에러가 발생하나요?&quot;, () =&gt; {
  expect(()=&gt; fn.throwErr()).toThrow(&quot;Error number 1&quot;) // 같은 에러메시지어야 true
});</p>
<pre><code>
## 2.2 비동기 코드 테스트하기
Jest에서는 테스트 코드가 끝까지 실행되면 비동기 로직을 기다리지 않고 테스트가 종료된다. 따라서 비동기 코드는 별도로 작업을 해주어야 테스트가 가능하다.
### 2.2.1 Callback 패턴
- test 함수에 **done** callback을 추가하여 비동기 로직이 끝나는 부분 명시해주기
-  error 감지하고 싶으면 try-catch 로 감싸야 한다.

```jsx
fn.js // 테스트 대상
const fn = {
  getName: (callback) =&gt; {
    const name = &quot;Mike&quot;;
    setTimeout(() =&gt; {
      callback(name);
    }, 3000); // 3 초 후에 이름 넘겨주기
  },
};</code></pre><pre><code class="language-jsx">fn.test.js // 테스트 코드
const fn = require(&quot;./fn&quot;);

test(&quot;3 초 후에 받아온 이름은 Mike&quot;, (done) =&gt; {
  function callback(name) {
    try {
      expect(name).toBe(&quot;Mike&quot;);
      done();
    } catch (error) {
      done();
    }
  }
  fn.getName(callback); // 여기서 콜백 실행함
});</code></pre>
<h3 id="222-promise-패턴">2.2.2 Promise 패턴</h3>
<ul>
<li>done을 넘겨주지 않아도 되어 더 간결하나, 프로미스를 <strong>return</strong> 해야함</li>
</ul>
<pre><code class="language-jsx">fn.js // 테스트 대상
const fn = {
  getName: () =&gt; {
    const name = &quot;Mike&quot;;
    return new Promise((res, rej) =&gt; {
      setTimeout(() =&gt; {
        res(name);
                // error 보내려면 rej(&quot;error&quot;);
      }, 3000); // 3 초 후에 이름 넘겨주기
    });
  },
};</code></pre>
<pre><code class="language-jsx">fn.test.js // 테스트 코드
// resolves, rejects
test(&quot;3 초 후에 받아온 이름은 Mike&quot;, () =&gt; {
    // 방법 1
  return fn.getName().then((name) =&gt; {
   expect(name).toBe(&quot;Mike&quot;);
  });

    // 방법 2
  return expect(fn.getName()).resolves.toBe(&quot;Mike&quot;);

    // Error 잡으려면
    return expect(fn.getName()).rejects.toMatch(&quot;error&quot;); // 3초 후에 에러가 나면 성공
});</code></pre>
<h3 id="233-async---await-패턴">2.3.3 Async - await 패턴</h3>
<p>#2.3.2의 promise 패턴과 동일한 경우를 async - await 패턴으로도 작성할 수 있으며, 이 방법도 resolves matcher를 사용할 수 있다.</p>
<pre><code class="language-jsx">fn.test.js // 테스트 코드
test(&quot;3 초 후에 받아온 이름은 Mike&quot;, async () =&gt; {
  const name = await fn.getName();
  expect(name).toBe(&quot;Mike&quot;);
});</code></pre>
<h2 id="23-testing-그룹화-및-전후-작업-추가하기">2.3 Testing 그룹화 및 전후 작업 추가하기</h2>
<h3 id="231-describe-사용하기">2.3.1 Describe 사용하기</h3>
<p>여러 테스트 함수를 작성할 때 관련된 테스트끼리 묶기 위해 <code>describe</code> 함수를 사용할 수 있다.</p>
<pre><code class="language-js">describe(&quot;입력 A: 예외 처리 테스트&quot;, () =&gt; {
  test(&quot;공백만 입력되면 오류가 발생합니다&quot;, () =&gt; {});
  test(&quot;문자를 입력하면 오류가 발생합니다&quot;, () =&gt; {});
  test(&quot;3자리가 아닌 숫자를 입력하면 오류가 발생합니다&quot;, () =&gt; {});

});
describe(&quot;입력 B: 예외 처리 테스트&quot;, () =&gt; {
  test(&quot;1이나 2 이외의 값을 입력하면 오류가 발생합니다&quot;, () =&gt; {});
});
</code></pre>
<ul>
<li><code>test.only()</code> : 해당 테스트만 실행하고 싶은 경우</li>
<li><code>test.skip()</code>: 해당 테스트를 건너뛰려는 경우</li>
</ul>
<h3 id="232-테스트-전후-작업-추가하기">2.3.2 테스트 전후 작업 추가하기</h3>
<p>관련된 테스트끼리는 동일한 사전 혹은 사후 작업이 필요한 경우가 있다. 이 경우에는 <code>beforeEach()</code> <code>afterEach()</code>를 사용한다. 만약 모든 테스트를 통틀어 한 번만 필요한 작업(ex. DB 연결)이라면 <code>beforeAll()</code> <code>afterAll()</code> 을 사용할 수 있다.</p>
<h3 id="233-반복적인-테스트는-each-사용하기">2.3.3 반복적인 테스트는 each 사용하기</h3>
<p>validation 함수와 같이 각각 다른 문자열을 반복적으로 확인하는 테스트는 빈번히 발생한다. 이 경우 <code>each</code>에 배열을 인자로 하여 테스트 콜백함수에서 <code>value</code>를 받아 테스트를 진행할 수 있다. </p>
<pre><code class="language-js">test.each([&quot;R&quot;,1,&quot;UD&quot;])(&quot;Moving validation 테스트&quot;, (value) =&gt; {
  expect(() =&gt; Validation.moving(value)).toThrow(INPUT_VAL.MOVING_ERROR);
});</code></pre>
<h2 id="24-mock-function-만들기">2.4 Mock function 만들기</h2>
<p>테스트를 위해 제작하는 모형의 함수를 mock function이라고 부른다. 모형의 함수를 사용하는 이유는 실제 함수를 구현하려면 시간이 오래 소요되고 third party package의 함수에 의한 오류로 테스트가 실패할 수도 있기 때문이다. 모형 함수는 우리가 JS 코드로 생성하거나 Jest에서 제공하는 mock 기능을 활용할 수도 있다.</p>
<h3 id="241-js로-mock-funtion-만들기">2.4.1 JS로 mock funtion 만들기</h3>
<p>API에서 데이터를 fetching해오는 함수를 사용하여 테스트를 작성한다고 가정해보자. fetching 과정에서 오류가 발생할 수도 있으므로 데이터를 받아오는 기능을 모형으로 아래와 같이 구현할 수 있다.</p>
<pre><code class="language-jsx">__mocks__/http.js //가짜함수 생성
const fetchData = () =&gt; {
  return Promise.resolve({ title: &#39;delectus aut autem&#39; });
};

exports.fetchData = fetchData;</code></pre>
<h3 id="241-jest로-mock-funtion-만들기">2.4.1 Jest로 mock funtion 만들기</h3>
<p>Jest의 mock funtion을 만들기 위해서는 <code>jest.fn()</code>으로 함수를 할당한다. 이 함수의 mock object는 다음 정보들을 가지고 있다.</p>
<ul>
<li><code>mock.results</code> : 각 호출 시 리턴된 값을 배열로 저장</li>
<li><code>mock.calls</code> : mock Fn의 호출 횟수, 호출 전달인수<pre><code class="language-js">const mockFn = jest.fn((num) =&gt; num + 1); // mock function으로 만들기
</code></pre>
</li>
</ul>
<p>mockFn(1);
mockFn(10);
test(&quot;두번째로 호출된 함수의 첫번째 인수는 1입니다.&quot;, () =&gt; {
    console.log(mockFn.mock.calls); // [ [1], [10]]
      console.log(mockFn.mock.calls[1][0]); // 10 
     console.log(mockFn.mock.results); // [ { type: &#39;return&#39;, value: 2 }, { type: &#39;return&#39;, value: 11 } ]<br>});</p>
<pre><code>

모형 함수의 로직을 아예 만들지 않고 아래와 같이 리턴값만 지정할 수도 있다. 만약 mock function을 비동기 함수로 반환하고 싶다면 `mockResolvedValue`를 사용하면 된다.
```js
const mockFn = jest.fn();

mockFn
  .mockReturnValueOnce(true)
  .mockReturnValueOnce(false)
  .mockReturnValue(true);

const result = [1, 2, 3].filter((num) =&gt; mockFn(num));

test(&quot;홀수는 1,3&quot;, () =&gt; {
  expect(result).toStrictEqual([1, 3]);
});</code></pre><p>만약 외부 함수를 mock function으로 지정한다면, 해당 함수의 실제 로직이 동작하지 않고 테스트 코드에서 지정한대로 함수가 모형으로 동작하게 된다.</p>
<h3 id="243-mock-function-테스트하기">2.4.3 Mock function 테스트하기</h3>
<p>Mock function은 아래와 같은 matcher를 사용하여 호출 횟수, 호출한 인수 등을 테스트할 수 있다.</p>
<ul>
<li><code>expect(mockFn).toBeCalled()</code> : 1 번 이상 호출되면 참<ul>
<li><code>toBeCalledTimes(3)</code> : 정확히 3번 호출된 경우 참</li>
<li><code>toBeCalledWith(10,20)</code> : 인수로 10 ,20을 받은 함수가 있는가?</li>
<li><code>lastCalledWith(30,40)</code> : 마지막으로 실행된 함수의 인수가 30, 40 인가?<h2 id="25-spy-function-만들기">2.5 Spy function 만들기</h2>
Mock function은 함수의 로직 구현을 가짜로 하여 효율적인 테스트를 돕는다. 만약 외부에서 작성한 로직으로 테스트를 하는 경우, 해당 함수(method of object)의 호출 여부 &amp; 호출 인자를 알아야 하는 경우가 있다. 이 경우 method에 spy를 붙인다는 의미로 spyOn을 사용하여 해당 함수를 spy function으로 만든다.</li>
</ul>
</li>
</ul>
<p><code>jest.spyOn(object, methodName)</code></p>
<pre><code class="language-js">test(&#39;계산기 객체의 더하기 함수 테스트&#39;, () =&gt; {
   // object
   const calculator = {
      add: (a, b) =&gt; a + b, // method
   };

   // calculator.add() method에 spy를 붙이기
   const spyFn = jest.spyOn(calculator, &#39;add&#39;);

   // Spy를 붙인 함수를 실행하면
   const result = calculator.add(1, 2);

   expect(spyFn).toBeCalledTimes(1); // 호출 횟수 테스트하기
   expect(spyFn).toBeCalledWith(1, 2); // 호출된 인자 테스트하기
});</code></pre>
<h1 id="3-실제-사용-예시">3. 실제 사용 예시</h1>
<p><a href="https://github.com/SeokyoungYou/javascript-baseball/tree/SeokyoungYou">우테코 프리코스 2 주차</a>에서 Jest를 사용하여 docs/README.md에 정리한 기능 목록의 동작을 테스트 코드로 확인하는 작업을 하였다. 그 중 두 개의 테스트 코드를 소개해보려 한다.</p>
<h2 id="31-regex를-사용하여-세-자리-숫자-검증">3.1 Regex를 사용하여 세 자리 숫자 검증</h2>
<ul>
<li>검증 대상: App object의 <code>pickComputerNum</code> method</li>
<li>검증 기능<ul>
<li><ol>
<li>1-9 사이의 3 자리 자연수인가?</li>
</ol>
</li>
<li><ol start="2">
<li>서로 다른 수로 이루어져 있는가?</li>
</ol>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/5edb1af3-7a65-46ce-8242-bb5f364a6bbc/image.png" alt="">
검증 기능 1은 <a href="https://regex101.com/">regex</a>와 <code>toMatch</code> matcher를 사용하여 간단하게 테스트하였다. 다만 이 수가 서로 다른 수로 이루어졌는지 판별하는 정규 표현식은 찾지 못하여, 검증 기능 2는 중복 수를 판별하고 해당 배열이 비었는지 확인하는 테스트를 추가하였다.</p>
<pre><code class="language-js">const App = require(&quot;../src/App&quot;);

  test(&quot;중복 없는 세자리 수 고르기&quot;, () =&gt; {
    const app = new App();

    const num = app.pickComputerNum();
    // 검증 2. 중복 숫자 확인
    let answerArr = num.split(&quot;&quot;);
    let duplicates = answerArr.filter((value, index) =&gt; {
      return index !== answerArr.indexOf(value);
    });

    expect(num).toMatch(/^[1-9]{3}$/); // 검증 1. 세자리 숫자 정규표현식
    expect(duplicates.length).toBe(0); // 검증 2. 중복 array가 비어있어야 한다
  });</code></pre>
<h2 id="32-예외-처리의-에러-메시지-검증">3.2 예외 처리의 에러 메시지 검증</h2>
<ul>
<li>검증 대상: App object의 <code>validateUserNum</code> <code>validateRestartNum</code></li>
<li>검증 기능<ul>
<li>각 입력에 대한 예외 처리 로직이 잘 구현되었는가? &gt; 에러 메시지 일치 여부 확인</li>
</ul>
</li>
</ul>
<pre><code class="language-js">
describe(&quot;입력 A: 예외 처리 테스트&quot;, () =&gt; {
  const app = new App();

  test(&quot;공백만 입력되면 오류가 발생합니다&quot;, () =&gt; {
    expect(() =&gt; app.validateUserNum(&quot;  &quot;)).toThrow(&quot;숫자를 입력해주세요&quot;);
  });

  test(&quot;문자를 입력하면 오류가 발생합니다&quot;, () =&gt; {
    expect(() =&gt; app.validateUserNum(&quot;가나다&quot;)).toThrow(
      &quot;문자를 제외한 숫자만을 입력해주세요&quot;
    );
  });

  test(&quot;3자리가 아닌 숫자를 입력하면 오류가 발생합니다&quot;, () =&gt; {
    expect(() =&gt; app.validateUserNum(&quot;1 3&quot;)).toThrow(
      &quot;입력한 숫자가 3 자리가 아닙니다&quot;
    );
    expect(() =&gt; app.validateUserNum(&quot;5984&quot;)).toThrow(
      &quot;입력한 숫자가 3 자리가 아닙니다&quot;
    );
  });
});

describe(&quot;입력 B: 예외 처리 테스트&quot;, () =&gt; {
  const app = new App();

  test(&quot;1이나 2 이외의 값을 입력하면 오류가 발생합니다&quot;, () =&gt; {
    expect(() =&gt; app.validateRestartNum(&quot;8&quot;)).toThrow(
      &quot;1 과 2 중 하나를 입력해주세요&quot;
    );
    expect(app.validateRestartNum(&quot; 1 &quot;)).toBe(&quot;1&quot;);
  });
});


</code></pre>
<h1 id="마치며">마치며</h1>
<p>이전에도 Jest를 사용해보고 싶었으나 테스트를 어디에 적용해야 하는지 감이 오지않고 테스트 코드 작성에 시간 소모가 많다고 생각하여 도입하지 못하고 있었다. 이번 테스팅 경험으로 실제로 버그가 발생하는 함수를 발견할 수 있었으며, 기능을 구현한 후에도 로직을 더 면밀히 살펴볼 수 있었다. React 프로젝트에서 Jest 도입이 가능하므로 이후 다른 웹 앱 프로젝트에서도 적용한다면 버그를 줄이는 역할을 톡톡히 해낼 것으로 예상한다.</p>
<h4 id="참고-자료">참고 자료</h4>
<p><a href="https://jestjs.io/">Jest 공식 문서</a>
<a href="https://youtu.be/g4MdUjxA-S4">코딩앙마 Jest 강의</a>
<a href="https://www.udemy.com/course/javascript-zw/">Udemy JS 강의</a> 중 Testing 모듈
<a href="https://regex101.com/">regex 연습</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git/GitHub]  자주 사용하는 용어와 커맨드를 제대로 알아보자]]></title>
            <link>https://velog.io/@skyu_dev/Git-Git-GitHub-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-add-commit-push-%EB%A1%9C%EB%B4%87%EC%97%90%EC%84%9C-%EB%B2%97%EC%96%B4%EB%82%98%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/Git-Git-GitHub-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-add-commit-push-%EB%A1%9C%EB%B4%87%EC%97%90%EC%84%9C-%EB%B2%97%EC%96%B4%EB%82%98%EA%B8%B0</guid>
            <pubDate>Sun, 06 Nov 2022 04:23:57 GMT</pubDate>
            <description><![CDATA[<h1 id="0-intro">0. Intro</h1>
<p>우테코 프리코스 5기에 참여하면서 fork, pull request 등 그동안 깃/깃허브에서 사용해보지 않은 커맨드를 접해보고 있다. 사실 깃을 사용하면서 충돌 메시지가 나는 것이 두려워서 &lt;add - commit - push 로봇&gt;이 되고 있었다. 우테코 1주차 공통 피드백 자료와 그동안 공부했던 자료들을 복기하면서 이번 기회에 깃과 친해져보려 한다. </p>
<h1 id="1-용어-정리">1. 용어 정리</h1>
<h2 id="11-git과-github">1.1 Git과 GitHub</h2>
<ul>
<li>Git: version control system<ul>
<li>프로젝트 내 파일들을 누가, 언제, 무엇을 바꾸었는지 트래킹</li>
</ul>
</li>
<li>Github: git provider. git repository를 위한 웹 기반 호스팅 서비스<ul>
<li>git files and git changes들을 업로드하고 공유하는 공간</li>
<li>bitbucket, gitlab에서도 같은 기능을 함</li>
</ul>
</li>
</ul>
<h2 id="12-git-repository">1.2 Git Repository</h2>
<p>Git은 git repository(저장소) 내의 파일들을 관리한다. 저장소는 두 가지 방법으로 만들 수 있다.</p>
<ol>
<li><p>아직 버전관리를 하지 않는 로컬 디렉토리 하나를 선택해서 Git 저장소를 적용</p>
<ul>
<li>GitHub에 새로운 저장소 만들어서 url 생성<pre><code class="language-js">$ git init //.git 하위 디렉토리를 만들어 저장소에 필요한 뼈대 파일 생성 혹은 초기화
$ git remote add origin &lt;remote repo url&gt; //remote repo url = origin으로 설정
$ git remote -v
$ git add .
$ git commit // 파일 버전관리 시작
$ git push -u origin main </code></pre>
</li>
</ul>
</li>
<li><p>다른 어딘가에서 Git 저장소를 Clone</p>
<ul>
<li>다른 프로젝트에 참여하거나 git repository를 복사하고 싶을 때<pre><code class="language-js">$ git clone &lt;remote repo url&gt; //reomte repo를 로컬로 복제하기
or
$ git clone &lt;remote repo url.git&gt; &lt;newname&gt; // 새로운 디렉토리 이름으로 복제하기 </code></pre>
</li>
</ul>
</li>
</ol>
<h3 id="121-origin이란">1.2.1 Origin이란?</h3>
<p>지금까지는 많은 생각없이 origin이라는 용어를 사용해왔다. 이 origin의 의미는 무엇일까? <strong>Origin은 우리 로컬 저장소와 연결된 원격 저장소의 url</strong>을 의미한다. 따라서 아래의 커맨드들은 다음과 같은 의미를 가진다.</p>
<pre><code class="language-js">$ git remote add origin &lt;remote repo url&gt; // 원격 서버의 origin을 해당 url로 설정하겠다고 git에게 알려줌
$ git push origin main // origin url의 main 브랜치로 commit을 푸시하겠다
$ git pull origin main // origin url의 main 브랜치에서 commit을 가져오겠다</code></pre>
<blockquote>
<p>origin = alias(별칭) of remote repository url</p>
</blockquote>
<pre><code class="language-js">$git remote -v // 커맨드로 origin이 가리키는 url 확인</code></pre>
<p>remote = origin이 참조하는 깃 커맨드</p>
<h3 id="122-upstream이란">1.2.2 Upstream이란?</h3>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/3458140d-aca5-4a9e-8b90-fd352c6fb718/image.png" alt=""></p>
<p>새로운 저장소를 만들고 원격 저장소에 커밋을 푸시할 때 <code>$ git push -u origin main</code> 와 같이 upstream을 초기에 설정해주어야 한다(<code>-u</code>는 <code>--set-upstream</code> 을 의미). Upstream(상류)는 <strong>저장소간의 관계를 의미하는 상대적인 개념</strong>이다. </p>
<ul>
<li><p>개인 프로젝트</p>
<ul>
<li>원격 저장소가 upstream, 로컬 저장소가 downstream</li>
<li>우리는 상류(원격 저장소)에 커밋을 <code>push</code>하고, 상류로부터 <code>pull</code> 커맨드를 사용해 커밋을 받아온다.</li>
</ul>
</li>
<li><p>팀 프로젝트(Fork)</p>
<ul>
<li>팀프로젝트 원격 저장소가 upstream, 나의 원격 저장소가 downstream</li>
<li>나의 원격 저장소가 upstream, 로컬 저장소가 downstream</li>
<li>우리의 원격 저장소에는 <code>fork</code>를 한 팀 프로젝트 원격 저장소로 push할 권한이 없음. 대신 <code>PR(Pull Request)</code>을 보내고 merge를 기다려야 함</li>
</ul>
</li>
<li><p>팀 프로젝트(Clone)</p>
<ul>
<li>팀프로젝트 원격 저장소가 upstream, 나의 로컬 저장소가 downstream</li>
</ul>
</li>
</ul>
<h2 id="13-commit">1.3 Commit</h2>
<p>Commit은 일종의 <strong>save point</strong>이다. 그 시점의 프로젝트 파일들을 묶어 <strong>snapshot</strong>을 찍고 git에 변경 사항과 그 시간을 저장할 수 있다. 커밋과 관련된 커맨드는 아래와 같으며 우리는 add &gt; commit &gt; push 사이클을 자주 사용하게 된다. 여기서 주의할 점은 git은 변경 사항만 저장하는 것이 아니라 그 시점의 파일을 통째로 저장한다.</p>
<ul>
<li>add: 커밋하고 싶은 것들만 묶어서 stage에 올림</li>
<li>commit: 세이브 포인트. 저장을 원하는 파일들을 묶어서 커밋 명령</li>
<li>push: 현재 세이브 데이터(커밋)를 내 원격 서버 저장소로 올림</li>
</ul>
<h2 id="14-local-repository의-3-가지-영역">1.4 Local repository의 3 가지 영역</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/ca07f970-4132-40f5-9770-a59ec9bf7bee/image.png" alt=""></p>
<p>로컬 저장소에서 파일은 3 가지 영역(상태)로 관리되며, <strong>Working directory→ Staging Area → Repository Area</strong>의 흐름을 가진다 </p>
<ul>
<li>working directory: 파일을 수정하고 작업하는 영역</li>
<li>staging area: commit할 파일의 snapshot을 만드는 영역</li>
<li>repository area: commit들의 히스토리를 기록하는 영역<blockquote>
<p><code>$tree .git</code>: working directory 구조 확인
<code>$git.log</code>: commit history 확인</p>
</blockquote>
</li>
</ul>
<h2 id="14-branch와-merge">1.4 Branch와 merge</h2>
<h3 id="141-branch란">1.4.1 Branch란?</h3>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/9119caa8-32e1-4ca2-b25c-4bf7cae34409/image.png" alt="">
[사진 출처: <a href="https://www.atlassian.com/ko/git/tutorials/using-branches/git-merge%5D">https://www.atlassian.com/ko/git/tutorials/using-branches/git-merge]</a></p>
<p>하나의 프로젝트에 대해 여러 버전을 만들거나 기능 개발을 위해 독립적인 공간이 필요할 때 branch(브랜치)를 생성한다.</p>
<ul>
<li>feature_x 브랜치 만들고 해당 브랜치로 HEAD 옮기기<ul>
<li><code>$git checkout -b feature_x</code></li>
</ul>
</li>
<li>main으로 돌아오기 / 브랜치 이동하기<ul>
<li><code>$git checkout main</code></li>
<li><code>$git checkout &lt;branch name&gt;</code></li>
</ul>
</li>
<li>브랜치 삭제하기<ul>
<li><code>$git branch -d &lt;branch name&gt;</code></li>
</ul>
</li>
<li>브랜치를 원격 저장소에 전송해야 다른사람들이 접근할 수 있음<ul>
<li><code>$git push origin &lt;branch name&gt;</code></li>
</ul>
</li>
</ul>
<blockquote>
<p>HEAD: 내가 위치해있는 commit</p>
</blockquote>
<h3 id="142-merge-갱신과-병합">1.4.2 Merge: 갱신과 병합</h3>
<p>하나의 브랜치를 현재 브랜치(HEAD)와 병합하기 위해 <code>merge</code> 를 사용한다.</p>
<ul>
<li>로컬 저장소를 원격 저장소에 맞춰 갱신하기 <code>git pull</code><ul>
<li>원격 저장소의 변경 내용을 로컬에서 받고 병합: <strong>pull = fetch + merge</strong></li>
<li><code>$git pull origin &lt;branch name&gt;</code></li>
</ul>
</li>
<li>다른 브랜치의 변경 내용을 현재 브랜치로 병합하기 <code>$git merge &lt;branch name&gt;</code></li>
<li>변경 내용 병합 전에 어떻게 바뀌었는지 비교도 가능 <code>$git diff &lt;원래 브랜치&gt; &lt;비교대상브랜치&gt;</code></li>
</ul>
<p>두 과정 모두 git은 자동으로 변경 내용을 병합하려하지만 가끔 conflict 발생하므로, git이 알려주는 충돌 부분을 <strong>직접 수정</strong>해서 병합 가능한 상태 만들어야 한다. </p>
<h1 id="2-자주-사용하는-git-clicommand-line-interface-흐름-파악하기">2. 자주 사용하는 Git CLI(Command Line Interface) 흐름 파악하기</h1>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/35542a08-f740-46db-a0f0-20b3330709ed/image.png" alt="">
내가 자주 사용하는 깃의 커맨드를 위와 같이 그림으로 표현해보았다. 자주 헷갈리는 로컬 저장소 내에서 사용하는 커맨드와 로컬 - 원격 저장소 간에 사용하는 커맨드를 한 눈에 구분할 수 있다.</p>
<h1 id="3-내가-자주-겪은-문제들">3. 내가 자주 겪은 문제들</h1>
<p>내가 깃허브를 사용하면서 자주 겪고있는 문제들을 정리해보려 한다. 참고로 VS code에서 파일 옆에 c 표시가 생기는 것은 충돌이 발생했다는 뜻이다.</p>
<h2 id="31-원격-저장소가-로컬-저장소의-commit보다-앞설-경우">3.1 원격 저장소가 로컬 저장소의 commit보다 앞설 경우</h2>
<p>서버 저장소(origin/main)가 내 로컬(main)보다 더 앞선 커밋을 가지고 있는 경우, 로컬 저장소에서 작업을 시작하기 전에 <code>git pull</code>을 진행해야 한다. 그러나 pull을  잊고 로컬에서 새로운 commit을 만든 경우 merge하라는 경고가 뜨게 된다. 그 경우 아래와 같이 해결하면 된다.</p>
<ul>
<li><code>$git push origin &lt;branch name&gt;</code>을 입력하면 merge하라는 충돌이 뜬다.<ol>
<li><code>i</code>를 누른다 (commit message를 입력하기 위해)<ol start="2">
<li>merge에 대한 message를 입력한다.</li>
</ol>
</li>
<li><code>esc</code>를 누른다.</li>
<li><code>:wq</code>를 입력한다.<ol start="5">
<li><code>enter</code>를 누른다  → merge 완료</li>
</ol>
</li>
</ol>
</li>
<li><code>$git push origin &lt;branch name&gt; --force</code> : 혹은 강제로 푸시할 수도 있다.<h2 id="32-이전-commit-덮어쓰기">3.2 이전 commit 덮어쓰기</h2>
Commit 메시지를 수정하지 않고 변경사항만 덮어쓰고 싶은 경우가 발생할 경우 아래와 같이 해결하면 된다. 이 방법은 가장 최근 commit만 수정 가능하다.</li>
<li><code>$git commit --amend --no-edit</code> (메시지 수정 없다는 뜻) → <code>$git push origin &lt;branch name&gt; --force</code> 강제 푸시<h2 id="33-fork-해온-저장소의-최신-커밋-받아오기">3.3 Fork 해온 저장소의 최신 커밋 받아오기</h2>
팀 프로젝트를 진행하면서 팀의 원격 저장소가 변경되었을 때(팀원 코드 merge, 새로운 세팅) 최신 커밋들을 받아와야 하는 경우가 있었다. <code>pull</code> 커맨드를 사용하는 것보다 아래와 같이 <code>fetch</code>와 <code>merge</code>를 따로 사용하는 것이 이해하기 편하여 정리하였다.</li>
</ul>
<p>먼저 로컬 저장소에서 작업하던 내용을 커밋으로 올려야한다. 피치 못하게 커밋을 할 수 없는 경우
<code>git stash</code>를 사용하여 임시 저장한다.(<code>git stash pop</code>으로 불러올 수 있다)</p>
<ol>
<li><p>팀의 원격 저장소의 url 별칭 만들기
나의 로컬 저장소는 기본적으로 <code>origin</code>으로 나의 원격 저장소 url을 별칭으로 가지고 있다. 보통 <code>upstream</code>이라는 별칭으로 팀 저장소의 url을 저장하나 다른 이름을 사용하여도 된다.</p>
<p><code>git remote add upstream</code> // upstream 별칭을 만든다
<code>git remote add upstream [팀 저장소 url]</code> // upstream에 팀 저장소 url을 저장한다.
<code>git remote -v</code> // origin이 나의 원격 저장소, upstream이 팀의 원격 저장소로 설정되어있으면 된다.</p>
</li>
<li><p>팀의 원격 저장소에서 최신 커밋을 가져오자
<code>git fetch upstream</code> // 최신 커밋 가져오기</p>
<p>커밋을 가져올 브랜치는 로컬에서 따로 생성해주어야 한다. 내 로컬 브랜치에서 같은 이름을 사용하려고 해도 생성해야 하는 것 같다. (이 부분은 추후 알아볼 예정이다.)
<code>git checkout (-b) [로컬 브랜치]</code> // 최신 커밋을 받을 로컬 저장소 브랜치
<code>git merge upstream/팀브랜치</code> // 팀 원격 저장소 중 커밋을 받아올 브랜치를 설정한다.</p>
</li>
<li><p>나의 원격 저장소에 머지 내용을 올린다.
<code>git push origin [로컬 브랜치]</code> </p>
</li>
</ol>
<blockquote>
<p>예를들어, 팀의 원격 저장소의 <code>develop</code> 브랜치의 커밋 내용을 나의 <code>teamDevelop</code> 브랜치로 가져오려면
<code>git fetch upstream</code> &gt; <code>git checkout -b teamDevelop</code> &gt; <code>git merage upstream/develop</code> &gt; conflict 해결 &gt;<code>git push origin teamDevelop</code></p>
</blockquote>
<blockquote>
<p>conflict는 VScode source control에서 해결 가능하며 아래와 같이 <code>~n</code>으로 터미널에서 충돌이 난 파일 개수(n)을 확인할 수 있다. 숫자가 모두 없어지면 해당 내용을 커밋하고 나의 원격 저장소에 올리면 머지가 종료된다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/0901b3b7-4f63-4914-8a5e-e7d8d14b584d/image.png" alt=""></p>
<h1 id="마치며">마치며</h1>
<p>추가로 좋은 커밋 메시지를 위해 우테코에서는  <a href="https://gist.github.com/stephenparish/9941e89d80e2bc58a153">the AngularJS commit conventions</a> 을 권장하는데 이에 대해서는 따로 글로 정리할 예정이다.</p>
<p>Git/GitHub를 사용하는 이유는 개발을 하다 되돌아가기 위한 세이브 포인트를 만들기 위해서이다. 이제는 깃에 대한 두려움을 줄이고 커맨드를 이해하면서 사용하는 습관을 들이려 한다.</p>
<h4 id="참고자료">참고자료</h4>
<p><a href="https://git-scm.com/book/ko/v2">Git - Book</a>
<a href="http://marklodato.github.io/visual-git-guide/index-ko.html">A Visual Git Reference</a>
<a href="https://rogerdudler.github.io/git-guide/index.ko.html">git - 간편 안내서</a>
<a href="https://nomadcoders.co/git-for-beginners">모두를 위한 깃 &amp; 깃허브</a>
<a href="https://wayhome25.github.io/git/2017/07/08/git-first-pull-request-story/">git 초보를 위한 풀리퀘스트(pull request) 방법</a>
<a href="https://json.postype.com/post/210431">[Git] Fork 한 repository 최신으로 동기화하기</a>
출처를 표기한 그림 외의 자료는 직접 제작하였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WIQ(1)] 코로나 격리하면서 쌍둥이 퀴즈 토이 프로젝트 만든 이야기]]></title>
            <link>https://velog.io/@skyu_dev/WIQ1-%EC%BD%94%EB%A1%9C%EB%82%98-%EA%B2%A9%EB%A6%AC-%EA%B8%B0%EA%B0%84%EC%97%90-%EC%8C%8D%EB%91%A5%EC%9D%B4-%ED%80%B4%EC%A6%88-%ED%86%A0%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-TS-Recoil-%EC%97%B0%EC%8A%B5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/WIQ1-%EC%BD%94%EB%A1%9C%EB%82%98-%EA%B2%A9%EB%A6%AC-%EA%B8%B0%EA%B0%84%EC%97%90-%EC%8C%8D%EB%91%A5%EC%9D%B4-%ED%80%B4%EC%A6%88-%ED%86%A0%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-TS-Recoil-%EC%97%B0%EC%8A%B5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 28 Oct 2022 09:35:50 GMT</pubDate>
            <description><![CDATA[<h1 id="0-intro">0. Intro</h1>
<p>내가 슈퍼 면역이 아니라니... 왠지 주말동안 몸이 너무 아프다 싶었는데, 쌍둥이 언니가 확진 판정을 받았다는 소식을 듣고 병원에 가보니 나도 양성이라고 하였다(사실 내가 먼저 아팠으나 자가 키트로는 음성이었다). 이번 주에는 노마드 코더 TS 챌린지, 우테코 프리코스 시작, 이력서 작성 등 최근 들어 가장 바쁜 일정이 기다리고 있는데 이 중요한 시기에 코로나라니.. 이왕 격리해야 한다면 골골 아프면서 강의를 들을 바에 토이 프로젝트를 하나 만드는게 낫다는 판단이 들었다. 이 글은 자가 격리를 하면서 갓생을 산 프론트엔드 개발자의 이야기이다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/b2b4eceb-4749-4ff4-ae0d-f3fe0233801f/image.png" alt=""></p>
<h1 id="1-프로젝트-배경">1. 프로젝트 배경</h1>
<p>프로젝트 아이디어는 많지만 &quot;이것만 배우고!&quot;라는 생각에 계속 미루는 경우가 많았다. 그 중 가장 해보고 싶었던 <strong>쌍둥이 맞추기 퀴즈</strong> 프로젝트를 선정하였다. 오랜만에 지인들을 만나 심심할 때마다 내 갤러리에 있는 50 여장의 쌍둥이 맞추기 사진으로 게임을 하곤 한다. 특히 명절이나 공휴일 시즌에 누가 더 많이 맞추는지를 체크하면 더욱 재미있는데, 이에서 착안한 프로젝트가 바로 &quot;Who is Quartz?&quot; 이다.</p>
<p>프로젝트의 구현 기능은 <a href="https://github.com/SeokyoungYou/who-is-quartz">깃허브 저장소</a>의 README.md에 적어두었으며 배포된 웹사이트는 아래 링크에서 확인할 수 있다. 이글에는 프로젝트를 하며 느낀 개발기를 적으려 한다.</p>
<blockquote>
<p>웹사이트 결과물: <a href="https://seokyoungyou.github.io/who-is-quartz/">https://seokyoungyou.github.io/who-is-quartz/</a></p>
</blockquote>
<h1 id="2-프로젝트의-개발-목적">2. 프로젝트의 개발 목적</h1>
<p>내가 이 토이 프로젝트를 만들면서 점검하고 공부하고 싶은 5 가지 기술/라이브러리 스택을 중요도 순으로 나열해보았다.</p>
<blockquote>
<ol>
<li>TypeScript 사용하여 런타임 에러 줄이기</li>
<li>Recoil을 사용한 전역 상태 관리</li>
<li>쉬운 유지 보수를 위한 component 구성</li>
<li>일관성 있는 git commit log</li>
<li>Firebase를 사용한 간단한 백엔드 구현</li>
</ol>
</blockquote>
<p>1-3 내용을 아래 3 장에서 자세히 다룰 예정이다. 4 번의 일관성 있는 커밋을 위해서는 우테코에서 사용하는 <a href="https://gist.github.com/stephenparish/9941e89d80e2bc58a153">AngularJS Commit Message Conventions </a>을 사용하였으며, README.md 파일에 정리한 기능 목록 단위로 커밋하는 것은 부족하지만 일관성 있게 목적을 나누어 커밋 제목을 올리는 것에는 성공하였다. 5 번은 이전 프로젝트를 하면서도 겪었던 백엔드 구현의 어려움을 줄이기 위해 <strong>Firebase의 Cloud Firestore</strong>를 사용였고, 별도의 작업을 거의 하지 않고 사용자의 닉네임-점수가 저장되는 DB를 쉽게 구현할 수 있었다.</p>
<h2 id="21-typescript-사용하여-런타임-에러-줄이기">2.1. TypeScript 사용하여 런타임 에러 줄이기</h2>
<p>이 프로젝트에서 가장 중요하게 여긴 것은 &quot;타입스크립트를 제대로 활용하기&quot;이다. 지난<a href="https://velog.io/@skyu_dev/TypeScript-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%B4%9D%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0"> 3 주 동안 타입스크립트를 공부한 이야기를 정리</a>하면서 객체 지향적인 사고를 기를 수 있었고, 타입을 미리 선언하여 런타임 에러를 방지하는 연습도 할 수 있었다.</p>
<h3 id="211-약간은-객체-지향적이게된-코드">2.1.1 약간은 객체 지향적이게된 코드</h3>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/40c65470-841f-4bf1-937c-b7ae2d358ed0/image.gif" alt="">
다음은 이모티콘을 클릭하면 style이 토글되는 애니메이션이다. Fontawsome에서 가져온 아이콘을 사용할 때 하나의 IconState를 정의하는 <code>IconInterface</code>를 만들어 color와 inconName을 함께 관리하였다. 다음은 하나의 아이콘의 state를 변경하는 로직이다.</p>
<pre><code class="language-tsx">// Define type
interface IconInterface {
  color: string;
  iconName: IconDefinition;
}
function HomeIcons() {
  // Icon state
  const [firstIconState, setFirstIconState] = useState&lt;boolean&gt;(false);

  // Create Icons
  let firstIcon: IconInterface = {
    color: &quot;black&quot;,
    iconName: BLANK_FACE_ICON,
  };

  // Toggle icons by clicking event
  if (firstIconState) {
    firstIcon.color = &quot;#6BCB77&quot;;
    firstIcon.iconName = SMILE_FACE_ICON;
  }
  const toggleIcon = (icon: &quot;first&quot; | &quot;second&quot;) =&gt; {
    if (icon === &quot;first&quot;) {
      setFirstIconState((prev) =&gt; !prev);
    } else {
      setSecondIconState((prev) =&gt; !prev); // 두 번째 아이콘에 대한 로직(이 글에서는 지움)
    }
  };
  return (
    &lt;Icons&gt;
      &lt;FontAwesomeIcon
        icon={firstIcon.iconName}
        onClick={toggleIcon.bind(null, &quot;first&quot;)}
        style={{ color: firstIcon.color, cursor: &quot;pointer&quot; }}
      /&gt;
    &lt;/Icons&gt;
  );
}
export default HomeIcons;</code></pre>
<h3 id="212-react에-ts-활용하기">2.1.2 React에 TS 활용하기</h3>
<p>리액트에서 TS를 활용하는 대표적인 예시 중 하나는 하위 컴포넌트 props의 타입 정의이다.
<code>&lt;DBResultComp users={users} /&gt;</code>가 다음과 같은 prop을 가질 때 이에 대한 generic type을 추가하는 작업이 필요하다. 이 때 Firebase의 DB에서 가져오는 데이터인 <code>users</code>의 타입 정의는 firebase에서 제공하지 않으므로, 구글링을 통해 <code>DocumentData</code> 를 임포트하여 사용하였다.</p>
<pre><code class="language-tsx">DBResultComp.tsx

import { DocumentData } from &quot;@google-cloud/firestore&quot;;

interface DBResultCompProps {
  users: DocumentData[];
}
const DBResultComp: React.FC&lt;DBResultCompProps&gt; = ({ users }) =&gt; {
  return (
    &lt;&gt;
      &lt;ScrollWrapper&gt;
        {users.map((u, i) =&gt; {
          return (
            &lt;UserWrapper key={i}&gt;
              &lt;UserName&gt;{u.user}&lt;/UserName&gt;
              &lt;UserScore&gt;{u.score} 점&lt;/UserScore&gt;
            &lt;/UserWrapper&gt;
          );
        })}
      &lt;/ScrollWrapper&gt;
    &lt;/&gt;
  );
};
export default DBResultComp;</code></pre>
<h2 id="22-recoil을-사용한-전역-상태-관리">2.2. Recoil을 사용한 전역 상태 관리</h2>
<p>이 프로젝트에서 두 번째로 중요하게 여긴 것은 명료한 전역 상태 관리 방법이며, local/global state의 분류 기준을 router가 매칭되는 메인 컴포넌트 3 개로 나누었다. </p>
<blockquote>
<p>동일한 라우팅 컴포넌트 내 = local state, 다른 라우팅 컴포넌트와 상태를 공유 = global state</p>
</blockquote>
<p>개인적으로 이 방법이 추후 리팩토링할 때도 더 편했다. 상태 관리 툴로 Recoil을 사용한 이유는 토이 프로젝트에서 간편하게 사용할 수 있는 리액트 상태관리 라이브러리이기 때문이다. Redux는 보일러 플레이트가 커서 조금 더 규모가 큰 프로젝트에 적합하다고 생각한다.</p>
<pre><code class="language-tsx">App.tsx
const App: React.FC = () =&gt; {
  return (
    &lt;Wrapper&gt;
      &lt;BrowserRouter basename={process.env.PUBLIC_URL}&gt;
        &lt;Routes&gt;
          &lt;Route path=&quot;/quiz/:id&quot; element={&lt;QuizScreen /&gt;} /&gt;
          &lt;Route path=&quot;/result&quot; element={&lt;ResultScreen /&gt;} /&gt;
          &lt;Route path=&quot;/&quot; element={&lt;Home /&gt;} /&gt;
        &lt;/Routes&gt;
      &lt;/BrowserRouter&gt;
    &lt;/Wrapper&gt;
  );
};</code></pre>
<p>3 개의 메인 컴포넌트(Home, QuizScreen, ResultScreen)에서 발생하는 상태 이동은 아래와 같다. Fig 1.1은 로컬 상태, Fig 1.2는 전역 상태의 이동을 도식하였다. 4 가지 전역 상태는 다음과 같으며 메인 컴포넌트 사이에서 상태 공유가 발생한다.</p>
<ul>
<li><code>quizDataState</code>: DB에서 가져온 퀴즈 배열</li>
<li><code>routesSelector</code>: DB에서 가져온 퀴즈 배열 기반으로 quiz route를 담은 array 생성</li>
<li><code>currRouteState</code>: 사용자가 현재 몇 번째 퀴즈를 풀고 있는지 route 저장</li>
<li><code>scoreState</code>: 사용자가 맞춘 퀴즈 점수 정보를 가진 배열</li>
</ul>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/c0783093-bcfe-4bc0-93c9-82928ba29f7d/image.png" alt=""></p>
<h3 id="221-atom">2.2.1 Atom</h3>
<p>Atom은 우리가 사용하는 state를 담는 저장소이다. Recoil을 만든 페이스북에서는 이를 비눗방울로 표현하는데, 우리가 전역으로 사용할 상태를 atom 비눗방울에 담아 어디든지 사용할 수 있다는 비유적 표현이다. 나는 본 프로젝트에서 <code>quizDataState</code>, <code>currRouteState</code>, <code>scoreState</code>를 모두 atom에 저장하였으며 그 중<code>quizDataState</code> 예시 코드는 다음과 같다.</p>
<pre><code class="language-ts">atom.ts // atom 만들기
interface QuizImg {
  url: string;
  answer: Answer;
}
export interface Quiz {
  quizId: string;
  quizName: string;
  images: QuizImg[];
}

export const quizDataState = atom&lt;Quiz[]&gt;({
  key: &quot;quizDataState&quot;, //JSON data from fakeDB
  default: [],
});  </code></pre>
<pre><code class="language-tsx">Home.tsx // post: quizDataState에 퀴즈 배열 저장하기
 const setQuiz = useSetRecoilState&lt;Quiz[]&gt;(quizDataState);
  useEffect(() =&gt; {
    fetch(url)
      .then((response) =&gt; response.json())
      .then((json) =&gt; {
        setQuiz(json);
      })
      .catch((error) =&gt; console.log(error));
  }, []);</code></pre>
<pre><code class="language-tsx">QuizScreen.tsx // get: quizDataState 접근하여 사용하기
const quiz = useRecoilValue&lt;Quiz[]&gt;(quizDataState);</code></pre>
<h3 id="222-selector">2.2.2 Selector</h3>
<p>4 개의 전역 상태 중 <code>routesSelector</code>는 <code>quizDataState</code>에 의존하는 특성을 가지는데, 이 때 Recoil의 selector를 활용할 수 있다. Selector에서는 원하는 atom의 값을 뽑아서 사용할 수 있으며, <code>get</code>을 사용하여 atom의 형태를 변환한 값을 리턴할 수 있다.</p>
<ul>
<li><code>routesSelector</code>: DB에서 가져온 퀴즈 배열 기반으로 quiz route를 담은 array 생성</li>
</ul>
<pre><code class="language-ts">export const routesSelctor = selector&lt;string[]&gt;({
  key: &quot;quizRoutes&quot;, //Route array refer JSON data: depends on quizDataState
  get: ({ get }) =&gt; {
    const quiz = get(quizDataState); // atom의 quizData array를 사용하여
    let quizRoutes: string[] = [];
    quiz.forEach((quiz) =&gt; {
      quizRoutes.push(`/quiz/${quiz.quizId}`);
    });
    quizRoutes.push(&quot;/result&quot;);
    return quizRoutes; // Routes가 담긴 array를 반환한다.
  },
});</code></pre>
<h2 id="23-쉬운-유지-보수를-위한-component-구성">2.3. 쉬운 유지 보수를 위한 component 구성</h2>
<p>마지막으로 다른 프로젝트들을 해보면서 가장 보완해보고 싶었던 유지 보수 관리가 쉬운 컴포넌트 구성이다. 유튜브에서 <a href="https://youtu.be/HYgKBvLr49c"> 컴포넌트, 다시 생각하기</a> 컨퍼런스를 듣고 Fig. 2와 같이 이 프로젝트의 컴포넌트 구성을 그리게 되었고, 여러 CSS 프레임워크를 거쳐 다시 CSS-in-JS로 돌아오게 되었다. </p>
<blockquote>
<p>이 프로젝트에서 더 나은 리팩토링에 기여한 툴은 두 가지라고 생각한다.</p>
</blockquote>
<ol>
<li>CSS-in-JS: 컴포넌트를 이동할 때 관련 CSS가 에디터에 표시되는 것이 편했다. css 파일을 사용하는 경우 클래스 명을 일일히 확인해야 하는 번거로움이 있었다.</li>
<li>TypeScript: 컴포넌트간에 state가 이동할 때 타입이 명시되어 있어 헷갈리지 않고 따로 주석을 달지 않아도 되어 편했다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/0ddd0b2c-53cc-4b60-98b0-4b244afe5fd4/image.png" alt=""></p>
<h3 id="231-quiz-data의-관리">2.3.1 Quiz data의 관리</h3>
<p>이 프로젝트의 퀴즈 데이터의 관리를 Fig 3으로 나타내보았다. 먼저, JSON file의 퀴즈 배열을 <code>quizDataState</code>로 fetching하여 전역 상태로 quiz set을 가져온다. QuizScreen 마다 퀴즈 배열 중 하나의 퀴즈를 가져와야 하고  정보는 <code>currQuiz</code>에 담겨야 한다. 이 정보는 QuizScreen이 가징 고유한 id인 <code>currRouteState</code>로 <code>quizDataState</code> 배열의 인덱스에 접근하여 가져온다. <code>currRouteState</code>를 전역 변수로 설정한 이유는 QuizScreen이 다른 메인 컴포넌트보다 더 복잡하므로 prop chain이 길어질 것을 대비하는 목적으로 사용하였으며, 추후 3.1 에 나오는 버그 해결에도 활용되는 것을 확인할 수 있다.</p>
<blockquote>
<p>Local quiz data = currQuiz : quizDataState 의 quiz array + 현재 위치의 고유한 값인 currRouteState 를 조합하</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/05ca80c6-62b2-4dc3-a955-f897cf7ba5a2/image.png" alt=""></p>
<h1 id="3-자잘한-버그-및-uiux-개선">3. 자잘한 버그 및 UI/UX 개선</h1>
<h2 id="31-점수--문제-개수가-되는-버그">3.1. 점수 &gt; 문제 개수가 되는 버그</h2>
<p>웹사이트를 배포하고 테스트하던 중 Fig 4.와 같이 <code>&lt;4 문제 중 5 문제를 맞추는&gt;</code> 버그가 발생하였다. 이 문제는 아래와 같은 상황에서 발생한다.</p>
<blockquote>
<p>홈 &gt; 퀴즈를 풀어서 1 문제를 맞춘다 &gt; 뒤로가기 &gt; 홈 &gt; 퀴즈 재도전 &gt; 다 맞춤 &gt; 5 문제를 맞추었습니다!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/b894eb53-65c2-44d0-afbd-4466b4fb0464/image.png" alt=""></p>
<p>이를 해결하기 위해 전역 상태인 점수<code>scoreState</code>와 QuizScreen마다 가지는 고유한 id <code>currRouteState</code>를 초기화 하는 위치를 변경하였다. 기존에는 ResultScreen &gt; Home으로 이동 시 초기화했지만, 이 로직으로 홈 컴포넌트가 렌더링되면 실행하도록 바꾸어 아래와 같이 버그를 해결하였다. </p>
<p>&lt;버그 해결 전&gt;
<img src="https://velog.velcdn.com/images/skyu_dev/post/751c50ea-58cb-4587-831d-96356658af12/image.gif" alt="">
&lt;버그 해결 후&gt;
<img src="https://velog.velcdn.com/images/skyu_dev/post/65a1c43e-84b2-4969-a27e-ea6123e8df2a/image.gif" alt=""></p>
<h2 id="32-닉네임-입력-후-form-숨기기">3.2. 닉네임 입력 후 form 숨기기</h2>
<p>Firebase DB를 관리하면서 닉네임이 없는 유저들을 간혹 발견하였다. <code>결과 제출하기</code> 버튼을 두 번 눌렀을 때 발생하는 버그로 input이 빈 상태 submit event가 추가로 발생하는 것이다. 이를 해결하기 위해 <code>required</code> 속성을 추가하고, submit event가 일어난 후에는 form을 닫는 것으로 수정하였다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/44d140c9-5bca-40ef-b0e6-e3b5369a3f5e/image.gif" alt=""></p>
<h1 id="4-work-to-do">4. Work to do</h1>
<p>새로운 개념들을 활용하면서 프로젝트를 만드니 정말 재미있었다. 하지만 짧은 기간에 만든 코드여서 개선할 부분이 정말 많고 앞으로 더 공부하면서 프로젝트를 디벨롭하려 한다. 현재 가장 개선하고 싶은 부분은 두 가지이다.</p>
<h2 id="41-useeffect-를-사용한-상태-초기화-로직의-의존성을-줄일-수-있을까">4.1 useEffect() 를 사용한 상태 초기화 로직의 의존성을 줄일 수 있을까?</h2>
<p>이 프로젝트에서 가장 복잡한 컴포넌트인 QuizScreend을 보면 정말 많은 로컬/전역 상태들이 얽혀있다. 이 로컬 상태들이 하위 컴포넌트로 가지 못하고 남아있는 이유는 <code>localScore</code>, <code>currRoute</code>에 의존성을 가진 상태로 초기화가 필요하기 때문이다. 이 부분은 상태 초기화나 <code>useEffect</code> hook의 대안에 대해 더 공부를 해야 할 것 같다.</p>
<pre><code class="language-tsx">QuizScreen.tsx
const QuizScreen: React.FC = () =&gt; {
  // routes info
  const navigate = useNavigate();
  const routes = useRecoilValue(routesSelctor);
  const { pathname } = useLocation();
  const quizId = pathname.split(&quot;/&quot;)[2];
  const currRoute = useRecoilValue&lt;number&gt;(currRouteState);
  // quiz info
  const quiz = useRecoilValue&lt;Quiz[]&gt;(quizDataState);
  const [currQiuz, setCurrQuiz] = useState&lt;Quiz&gt;(quiz[currRoute]);
  // scores info
  const [scores, setScores] = useRecoilState&lt;Score[]&gt;(scoreState);
  const [correct, setCorrect] = useState&lt;boolean | null&gt;(null);
  const [localScore, setLocalScore] = useState&lt;Score&gt;({});
  // Handle Img Background: color and pointer-event
  const [pointerEvent, setPointerEvent] = useState&lt;PointerEvent&gt;({});

  // After Submit Btn Clicked: Notice answer and fetch scores
  useEffect(() =&gt; {
    if (Object.keys(localScore).length !== 0) {
      setPointerEvent({ &quot;pointer-events&quot;: &quot;none&quot; });
      setCorrect(null);
      setScores((prev) =&gt; [...prev, localScore]);
    }
  }, [localScore]);

  // After Next Btn Clicked: Initialize for next quiz
  useEffect(() =&gt; {
    setCurrQuiz(quiz[currRoute]);
    if (routes[currRoute] === &quot;/result&quot;) {
      navigate(`/result`, { state: { isFromHome: false } });
    } else {
      navigate(`${routes[currRoute]}`);
    }
    setLocalScore({});
    setPointerEvent({});
  }, [currRoute]);

  return (
    &lt;Wrapper&gt;
      &lt;TitleWrapper&gt;
        &lt;Title&gt;{currQiuz.quizName}&lt;/Title&gt;
        &lt;span&gt;답을 선택한 후 제출 버튼을 눌러주세요!&lt;/span&gt;
      &lt;/TitleWrapper&gt;
      &lt;QuizComp
        pointerEvent={pointerEvent}
        currQiuz={currQiuz}
        setCorrect={setCorrect}
      /&gt;
      &lt;QuizBtns
        localScore={localScore}
        correct={correct}
        setLocalScore={setLocalScore}
      /&gt;
    &lt;/Wrapper&gt;
  );
};

export default QuizScreen;</code></pre>
<h2 id="42-이미지-로딩-개선">4.2. 이미지 로딩 개선</h2>
<p>이미지 최적화없이 사진을 그대로 사용하다보니 현재는 사진이 로딩되는 모습이 눈에 보이게 느리다. 이미지 로딩이 본 프로젝트의 성능을 좌우하기 때문에 이미지 최적화, pre-loading 등을 활용하여 이를 개선할 예정이다.</p>
<h1 id="마치며">마치며</h1>
<p>그동안 프로젝트 만들기 위해서는 1 달 이상의 시간이 소요될 것이라 생각하며 미루고 있었는데, 이렇게 짧은 시간 안에 만족스러운 결과물이 나왔다는 것이 고무적이다. 앞으로 공부해보고 싶은 기술이 있으면 바로 작은 토이 프로젝트를 만드는 습관을 가져야겠다. 추가로 공휴일마다 지인들을 즐겁게 할 수 있는 수단이 생겨 매우 기쁘다!</p>
<h4 id="참고-자료">참고 자료</h4>
<p><a href="https://youtu.be/HYgKBvLr49c">FEConf 2021 A Track [A3] 컴포넌트, 다시 생각하기</a>
<a href="https://recoiljs.org/docs/introduction/core-concepts">Recoil 공식 문서</a>
<a href="https://velog.io/@juno7803/Recoil-Recoil-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0">[Recoil] Recoil 200% 활용하기</a>
<a href="https://tech.osci.kr/2022/06/16/recoil-state-management-of-react/">Recoil, 리액트의 상태관리 라이브러리</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 타입스크립트 총정리하기]]></title>
            <link>https://velog.io/@skyu_dev/TypeScript-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%B4%9D%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/TypeScript-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%B4%9D%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 28 Oct 2022 07:24:06 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/skyu_dev/post/ac967d5f-ffb8-4f32-9140-f2a78a7e3d1b/image.png" alt=""></p>
<p>약 3 주간 TypeScript를 공부한 개념들을 총정리해보려 한다. 올해 초 React + Typescript 조합을 배우기 위해 수강했던 강의는 유익했지만, 왜 TS를 사용하고 어디에 적용하는게 유용한지 알지 못한 상태로 <a href="https://github.com/SeokyoungYou/nomflix-clone/tree/a1241a48cfd2c9942266afc8718d0a54ff312c83">프로젝트</a>의  코드를 따라 작성하기만 바빴던 기억이 있다. C/C++로 코딩을 시작했기 때문에 정적 타입에 대해 친숙하지만, JS로 더 오랜 기간동안 개발하면서 놓치고 있던 타입 안정성을 보완하기 위해 TS를 제대로 배워보았다.</p>
<h1 id="1-typescript란">1. TypeScript란?</h1>
<h2 id="11-typescript를-왜-사용해야-하는가">1.1 TypeScript를 왜 사용해야 하는가?</h2>
<p>웹 프로그래밍에 사용 가능한 언어인 JS는 dynamic, weakly typed language이다. JS는 자율성을 가지지만 아래와 같은 버그가 발생하여 우리를 오류로부터 보호해주지 못한다. 두 숫자를 더하는 로직을 만들 때 의도치 않게 number 대신 string이 들어간 경우, JS에서는 에러를 발생시키지 않고 두 string을 concat하여 &quot;53&quot;을 만들어낸다.</p>
<pre><code class="language-js">console.log(5 + 3) // 8 : add
console.log(&quot;5&quot; + &quot;3&quot;) // &quot;53&quot; : concat -&gt; bug!</code></pre>
<p>다른 예시로 string + number가 가능하고 함수 호출 시 parameter의 개수가 모자라도 오류가 발생하지 않으며, 타입 체킹을 코드가 실행중일 때 진행하므로 Runtime error만을 발생시킨다. 이렇게 의도치 않은 방식의 버그가 발생할 때 코드를 보호하기 위해 강력한 타입 안정성이 필요하다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/6764057c-7d4d-4bb4-84ed-aa680160adb8/image.png" alt=""> [Fig. 1 Static Typed Vs Dynamic Typed 언어의 타입 체킹(출처: Ref[1])]</p>
<h2 id="12-typescript는-어떻게-동작하는가">1.2 TypeScript는 어떻게 동작하는가?</h2>
<p>TS는 새로운 언어가 아니라 JS + static type을 더하여 Java, C/C++, Rust과 같이 사용할 수 있게 한다. 브라우저는 JS만 실행할 수 있으므로 TS를 사용하려면 TS ➡️ JS로 컴파일하는 과정이 필요하다. TS에서는 위와 같은 에러가 감지되면 런타임이 아닌 코드 실행 전인 컴파일 시에 감지하여 Compile error를 발생시켜 우리의 코드를 더 강력하게 보호할 수 있다.</p>
<p>또한 TS의 강력한 기능 중 하나인 <strong>Type Inference</strong>는 우리가 타입을 명시하지 않아도 자동으로 추론한다. 타입 선언 여부는 개발자가 결정하며 필요한 경우를 제외하고는 TS가 추론하게 두는 것이 좋다고 한다.</p>
<h1 id="2-types">2. Types</h1>
<p>그렇다면 TS에서는 어떻게 타입을 명시할 수 있을까? 아래와 같이 타입을 명시할 변수 이름 옆에 <code>변수명:type</code>으로 표기해주면 된다. TS에서 다루는 타입들을 자세히 알아보자</p>
<pre><code class="language-ts">let names: string = &quot;Quartz&quot;; 

function add(a: number, b:number) {
 return a + b; 
}</code></pre>
<h2 id="21-primitives">2.1 Primitives</h2>
<p>우리가 JS에서도 자주 사용하는 <code>string</code>, <code>number</code> 그리고<code>boolean</code>을 그대로 사용할 수 있다.</p>
<h2 id="22-object-and-array">2.2 Object and Array</h2>
<p>객체나 배열의 타입을 선언할 때는 이들이 담고 있는 item들의 타입도 함께 명시해야 한다.</p>
<pre><code class="language-ts">const resultContainer: { res: number } = {
  res: result,
};

const names: string[] = [&quot;Max&quot; ,&quot;Quartz&quot;, &quot;Belle&quot;];</code></pre>
<p>배열의 타입을 선언할 때 <code>string[]</code> = <code>Array&lt;string&gt;</code> 로도 나타낼 수 있는데 이에 대한 generic type과 함수의 타입 선언은 추후 4장에서 상세히 설명할 예정이다.</p>
<h2 id="23-ts에만-존재하는-types">2.3 TS에만 존재하는 types</h2>
<p>JS에는 존재하지 않지만 TS에는 존재하는 유용한 타입들에 대해 간략히 정리해보려 한다.</p>
<ul>
<li><code>any</code> : 모든 타입을 수용한다. TS 를 더이상 사용하지 않고 빠져나오고 싶을 때 쓰는 타입이므로 신중히 사용해야 한다.</li>
<li>Tuple: 특정 위치에 특정 타입 있는 custom array 만들 때<ul>
<li>API에서 데이터 가져올 때 가끔 사용함<pre><code class="language-ts">const player: [string, number, boolean] = [&quot;nico&quot;, 12, false];</code></pre>
</li>
</ul>
</li>
<li><code>unknown</code> : 어떤 타입인지 미리 모르는 변수</li>
<li>Literal type: 타입을 특정 value로 정하기</li>
<li>Union type: 여러 개의 타입 허용 → 더 유연한 타입 설정<pre><code class="language-ts">  // lieteral &amp; union type
  type PrintMode = &quot;console&quot; | &quot;alert&quot;;</code></pre>
</li>
<li>Enum: Literal + Union 조합의 대안. 여러 모드가 있을 때 사용<ul>
<li>다른 언어에는 있지만 JS에는 존재하지 않는다<pre><code class="language-ts">                //0    //1
enum OutputMode { CONSOLE, ALERT };
PrintMode === OutputMode.CONSOLE </code></pre>
</li>
</ul>
</li>
<li>Optional property <code>?</code><ul>
<li>속성이 undefined일 수 있는 경우</li>
</ul>
</li>
</ul>
<h1 id="3-type을-저장하는-3-가지-방법">3. Type을 저장하는 3 가지 방법</h1>
<p>타입을 변수로 분리하여 저장하는 것에는 3 가지 방법이 있으며 나는 아래와 같은 표를 근거로 object의 타입을 저장할 때는 <code>interface</code>를 사용하고 나머지는 <code>type</code>을 사용한다는 기준을 세웠다. 
<img src="https://velog.velcdn.com/images/skyu_dev/post/056df308-4f2e-472f-a305-fd67d90b720b/image.png" alt=""></p>
<h2 id="31-type-alias">3.1 Type Alias</h2>
<p>Type Alias는 <code>type</code> 키워드를 사용하여 타입을 별칭을 변수로 저장하는 방법이다. 함수, 원시 타입, 객체, 배열 등 다양한 형태의 타입을 저장할 수 있다.</p>
<pre><code class="language-ts">type Player = { // type alias로 저장하고
    name: string,
}

const quartz: Player = { // 인스턴스 만들어서 사용하기
    name : &quot;quartz&quot;
} </code></pre>
<h2 id="32-interface">3.2 Interface</h2>
<p>Object의 타입만을 저장할 수 있으며 객체 생성 방법은 type alias와 동일하다.</p>
<pre><code class="language-ts">interface Player { // interface로 저장하고
    name: string,
}

const quartz: Player = { // 인스턴스 만들어서 사용하기
    name : &quot;quartz&quot;
} </code></pre>
<h2 id="33-abstract-class">3.3 Abstract class</h2>
<p>JS에 존재하는 개념인 class에 추상화 개념을 얹어서 인스턴스 생성은 불가능하지만 blueprint를 작성하여 다른 클래스에 상속이 가능하다. 클래스처럼 prop, method의 접근 제한을 둘 수 있으며 컴파일 시 일반 클래스로 변환된다.</p>
<pre><code class="language-ts">abstract class User {
  constructor( 
        private firstName: string, 
        protected lastName: string,
        public nickName: string
    ) { } 
    abstract getLastName(): void // abstract method
    getFullName(){ // 일반 method
        return `${this.firstName} ${this.lastName}`
    }
}</code></pre>
<h1 id="4-type-of-function">4. Type of Function</h1>
<h2 id="41-call-signature">4.1 Call signature</h2>
<p>함수의 타입을 설명하기 위해서는 arguments와 return type을 모두 명시해야 하며 이를 call signature라고 한다. 일반 함수와 화살표형 함수 모두 아래와 같이 나타낼 수 있으며 type alias를 사용하여 더 간편하게 사용이 가능하다.</p>
<pre><code class="language-ts">                    //arg type.  //return type    
function playerNaker(name:string) : Player {}
const playerNaker = (name:string) : Player =&gt; {}

type Add = (a:number, b:number) =&gt; number; // call signature
const add : Add = (a,b) =&gt; a+b </code></pre>
<p>함수의 return type 중 리턴이 없는 경우는 <code>void</code>, 절대 리턴하지 않는 경우는 <code>never</code>를 사용한다.</p>
<p>함수는 여러 개의 call signature를 가질 수도 있는데 이를 overloading이라고 하며 아래와 같이 표현할 수 있다.</p>
<pre><code class="language-ts">// A. 다른 타입의 같은 개수 파라미터(이 경우가 많음)
type Push = {
  (path: string):void // 1
    (path: string, state: object): void //2
}

const push:Push = (config) =&gt; {
    if(typeof config === &quot;string&quot;) console.log(config) //1
    else console.log(config.path) //2
}

// B.같은 타입의 다른 개수 파라미터(드뭄)
type Add = {
    (a:number, b:number) : number
    (a:number, b:number, c:number) : number
}

const add:Add = (a, b, c?:number) =&gt;{ //c param이 optional 이라고 표기
    if(c) a+b+c
    return a+b
}</code></pre>
<h2 id="42-generic">4.2 Generic</h2>
<p>함수의 call signature를 작성할 때 input type이 명확하지 않은 경우가 있다. 이때 <code>any</code>를 사용하면 TS의 장점이 사라지는데 이를 해결하기 위해 generic type을 사용한다. 아래의 call signature은 input으로 들어오는 타입을 그대로 return type에 활용할 수 있는 generic type을 사용한 예시이다.</p>
<pre><code class="language-ts">//1. call signature에서 사용
function returnFirst&lt;T&gt;(a: T[]):T {
    return a[0]       
}</code></pre>
<p>Generic type의 사용  방법은</p>
<ol>
<li><code>&lt;T&gt;</code> 로 generic 사용을 표기하고 (T가 아닌 어떤 문자도 사용 가능하다)</li>
<li>그 문자(T)를 타입으로 사용하면 된다.</li>
</ol>
<p>Generic은 함수 뿐만 아니라 type alias, interface에서도 사용이 가능하며, React와 같은 라이브러리나 여러 패키지에서 흔히 마주칠 수 있다.</p>
<pre><code class="language-ts">//2. type alias에서 사용
type ReturnFirst = {
  &lt;T&gt;(arr: T[]): T //parameter로 들어오는 타입 유추해줌
}
const returnFirst: ReturnFirst =(arr)=&gt;{
  return a[0]
}

//3. interface에서 사용
interface Items&lt;T&gt; {
  [key: string]: T;
}</code></pre>
<h1 id="5-tsconfigjson">5. tsconfig.json</h1>
<p>여기까지 TS의 사용 방법과 예시를 간결하게 정리해보았다. TS ➡️ JS로 컴파일하는 setting을 하기 위해서는 <code>tsconfig.json</code> 파일을 수정하면 되고, React와 같은 라이브러리를 사용하면 기본 설정이 자동으로 되어있다. 이 파일에서는 컴파일할 자바스크립트의 버전, 기본으로 추가되는 라이브러리, JS 파일 혼용 여부, strict mode 등을 설정한다.</p>
<h1 id="6-react-with-typescript">6. React with TypeScript</h1>
<p>리액트 프로젝트에서 TS를 사용하여 타입을 명시하는 대표적인 예시로 아래의 3 가지가 있다.</p>
<h2 id="61-component-porps의-generic-type-명시">6.1 Component porps의 generic type 명시</h2>
<p>다음과 같이 props를 받는 <code>&lt;UserResultComp /&gt;</code>가 있다고 하자. TS를 사용하는 경우 이 함수형 컴포넌크가 받는 props의 타입을 명시해주어야 한다. </p>
<pre><code class="language-tsx">&lt;UserResultComp countCorrect={countCorrect} setIsSubmit={setIsSubmit} getDBUsers={getDBUsers} /&gt;</code></pre>
<p>리액트의 함수형 컴포넌트는 <code>React.FC</code> 라는 generic type을 가지며 TS는 이 타입이 명시되면 리액트의 컴포넌트가 받는 default props에 대한 정보들을 갖게 된다. 우리는 기존 <code>React.FC</code> generic type에 커스텀 props 를 추가하여 <code>React.FC&lt;{ prop1: type }&gt;</code>으로 명시할 수 있다.</p>
<pre><code class="language-tsx">

interface UserResultCompProps {
  countCorrect: number;
  setIsSubmit: React.Dispatch&lt;React.SetStateAction&lt;boolean&gt;&gt;;
  getDBUsers(): Promise&lt;void&gt;;
}

const UserResultComp: React.FC&lt;UserResultCompProps&gt; = ({
  countCorrect,
  setIsSubmit,
  getDBUsers,
}) =&gt; {
  return null;
};
export default UserResultComp;
</code></pre>
<h2 id="62-state의-generic-type-명시">6.2 State의 generic type 명시</h2>
<p>두 번째<code>useState</code> 또한 generic type을 사용하는 예시이다. 타입을 명시하지 않으면 아래와 같은 에러가 발생할 수 있으므로 타입을 표기해주어야 한다.</p>
<pre><code class="language-tsx">const [todos, setTodos] = useState([]); // 타입 안쓰면 never[] type이 됨 = error: 항상 비어있어야 한다는 뜻이 됨

// 사용 예시
const [todos, setTodos] = useState&lt;string[]&gt;([]);
const [item, setItem] = useState&lt;number&gt;([]);</code></pre>
<h2 id="63-event의-type-명시">6.3 Event의 type 명시</h2>
<p>Click event listener나 form data를 가져올 때도 해당 event data의 타입을 명시해야 하며, <code>React.FormEvent</code>,<code>React.MouseEvent</code> 등 TS가 추론해주는 기능을 활용하여 명시하면 된다.</p>
<h4 id="참고자료reference">참고자료(Reference)</h4>
<p>1 <a href="https://www.baeldung.com/cs/statically-vs-dynamically-typed-languages">Statically Typed Vs Dynamically Typed Languages</a></p>
<p>2 <a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html">TypeScript 공식 문서</a></p>
<p>3 <a href="https://nomadcoders.co/typescript-for-beginners">노마드 코더 강의: 타입스크립트로 블록체인 만들기</a></p>
<p>4 Udemy JS, React 강의</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Navigation] useEffect가 아닌 useFocusEffect 사용하여 stack 구조 화면 초기화하기]]></title>
            <link>https://velog.io/@skyu_dev/React-Navigation-Bottom-Tab%EC%9D%98-Require-cycle-warning-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/React-Navigation-Bottom-Tab%EC%9D%98-Require-cycle-warning-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 10 Oct 2022 18:47:15 GMT</pubDate>
            <description><![CDATA[<p>이 글에서는 React Native 프로젝트의 라우팅을 위해 사용한 React Navigation 라이브러리에서 발생한 컴포넌트 초기화 이슈 해결 과정에 대해 설명해보려 한다.</p>
<h1 id="1-routing은-되는데-왜-컴포넌트-초기화가-안될까">1. Routing은 되는데 왜 컴포넌트 초기화가 안될까?</h1>
<h2 id="1-1-navigation-구성-및-구현-코드">1-1. Navigation 구성 및 구현 코드</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/94c3c1ed-fbaa-4048-8926-9bcab7e8ddd2/image.png" alt=""></p>
<p>나는 [Fig. 1]의 UI를 구현하기 위해 하단 네비게이션 바가 필요했고, React Navigation에서 제공하는 <a href="https://reactnavigation.org/docs/bottom-tab-navigator/">Bottom Tabs Navigatior</a>를 사용하여 비교적 빠르게 구현할 수 있었다. React Navigation는 React Router과 유사하게 <code>Tab.Navigator</code>가 각<code>Tab.Screen</code>의 컴포턴트를 렌더링해주는 방식이다.(💡여기서 나는 React Router와 동작이 같은 것이라 생각했고 삽질은 이제 시작된다...^^)</p>
<p>공식 문서를 참고하여 [Fig.2]와 같은 구조로 네비게이션을 아래와 같이 구현할 수 있다.</p>
<pre><code class="language-js">import { NavigationContainer } from &quot;@react-navigation/native&quot;;
import { createBottomTabNavigator } from &quot;@react-navigation/bottom-tabs&quot;;

const Tab = createBottomTabNavigator();

export default function MainContainer() {
  return (
    &lt;NavigationContainer&gt;
      &lt;Tab.Navigator&gt;
        &lt;Tab.Screen name={techTreeName} component={TechTree} /&gt;
        &lt;Tab.Screen name={homeName} component={Home} /&gt;
        &lt;Tab.Screen name={myPageName} component={Mypage} /&gt;
      &lt;/Tab.Navigator&gt;
    &lt;/NavigationContainer&gt;
  );
}</code></pre>
<h2 id="1-2-문제-발생-페이지-전환-시-state가-초기화되지-않음">1-2. 문제 발생: 페이지 전환 시 state가 초기화되지 않음</h2>
<p>네비게이션을 구현 완료한 후 각 페이지의 구조를 짜던 중 페이지가 전환될 때 Home의 state가 초기화되지 않고 그대로 남아있는 버그를 발견했다. 아래 영상을 보면 Home calendar에서 선택한 날짜(day state)에 표기되는 보라색 선이 페이지 전환 후에도 그대로 남아있는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/77c57ce4-dee5-46a5-b8cd-1a713d282956/image.gif" alt=""></p>
<h2 id="1-3-useeffect를-이용한-component-re-render-시도">1-3. useEffect를 이용한 component re-render 시도</h2>
<p>처음으로는 리액트 프로젝트에서 함수의 조건부 실행에 가장 많이 사용되는 <code>useEffect</code>를 시도해보았다.</p>
<ul>
<li><p>방법 1. 컴포넌트가 마운트되는 최초 1 회에 초기화
  나의 가정: 화면이 전환되면 컴포넌트가 마운트되므로 초기화 될 것이다. ➡️ 실패</p>
<blockquote>
<p>곰곰히 생각해본다면 state는 컴포넌트가 리렌더링될 때마다 초기화되어야 하는데 화면이 전환되어도 남아있다는 것은 리렌더링이 일어나지 않고 있다는 뜻이다.</p>
</blockquote>
<pre><code class="language-js">useEffect(() =&gt; {
  setDays(markedDays);
}, []);</code></pre>
</li>
<li><p>방법 2. 페이지 내 새로고침 버튼을 만들어서 useEffect에 의존성을 추가하여 초기화
  혹시 react native에서 useEffect hook을 다르게 구현하고 있나? 라는 의심이 들어 Home 화면 내에 refresh state를 만들었다. useEffect에 이 의존성을 추가하면 초기화는 가능하지만, 매 화면마다 refresh state를 전역으로 관리해야 하는 엄청난 번거로움이 따른다. ➡️ 절반만 성공</p>
</li>
</ul>
<pre><code class="language-js">  let [reRender, setRerender] = useState(true);

  // Rerendering button
  // ✉️ setRerender를 전역 상태로 관리해서 navigation tab 바뀔 때마다 rerendering
  const refreshClicked = () =&gt; {
    setRerender((prev) =&gt; !prev);
  };
  // fetching data and set today at first rendering
  useEffect(() =&gt; {
    setDays(markedDays);
  }, [reRender]);</code></pre>
<h1 id="2-web과-mobile-app의-화면-전환은--다르다">2. Web과 Mobile App의 화면 전환은  다르다</h1>
<p>1-3.의 방법 1에서 화면이 전환되어도 리렌더링이 일어나지 않는다는 사실을 깨닫고 내가 React Native를 React와 너무 동일시하고 있었다는 문제를 자각했다. React Native는 자바스크립트를 사용하여 React와 비슷한 개발 환경을 사용하지만 엄연히 구동 방식이 다른 프레임워크이다.</p>
<ul>
<li>React in web: Virtual DOM을 이용하여 브라우저 DOM을 업데이트</li>
<li>React Native in mobile: Mobile API를 이용해서 모바일 UI를 업데이트</li>
</ul>
<p>그렇다. React Native는 브라우저 환경이 아니므로 DOM이 아니라 모바일 API를 사용한다. 그러면 모바일 상에서 화면 전환은 어떻게 일어날까?</p>
<h2 id="2-1-react-router-vs-react-navigator">2-1. React Router vs React Navigator</h2>
<p>React에서 사용하는 두 가지 라우팅 라이브러리를 비교하여 웹과 모바일 앱의 차이점을 자세히 알아보자. A/B/C 스크린(컴포넌트)의 화면 전환을 간단하게 [Fig. 3]과 같은 그림으로 나타내보았다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/d229cb48-e216-4917-8659-03531bf01c3d/image.png" alt=""></p>
<h3 id="2-1-1-react-router">2-1-1. React Router</h3>
<p>웹 개발자에게는 아주 익숙한 React Router를 먼저 살펴보자. React는 SPA 기반으로 React Router에서는 컴포넌트 속성에 설정된 URL과 현재 경로가 일치하면 해당하는 컴포넌트를 렌더링한다. [Fig. 3]의 좌측과 같이 화면 A ↔️ B의 전환은 각 컴포넌트가 교체되면서 일어난다. 즉 DOM에 컴포넌트를 끼워넣고 빼면서 mount/unmount 되므로, 다시 화면 A로 되돌아갔을 때 컴포넌트가 mount되며 자체적으로 초기화된다.</p>
<h3 id="2-1-2-react-navigation">2-1-2. React Navigation</h3>
<p>모바일 앱에 사용되는 React Navigator에서는 stack 구조로 되어있어 화면을 이동하면 전 화면이 사라지는 것이 아니라 기존의 화면 위에 새로운 화면이 쌓인다. 따라서 화면을 클릭하면 stack에 push되고 뒤로가기를 클릭하면 pop되면서 이전 화면이 등장한다. 이 구조의 특이한 점은 [Fig. 3]의 우측과 같이 화면 A가 B로 전환되어도 A가 unmount 되지 않고 그대로 stack에 남아있다(<a href="https://reactnavigation.org/docs/navigation-lifecycle">공식 문서 참조</a>). 즉 화면 A로 재전환되어도 A는 이미 mount 상태이므로 초기화가 일어나지 않으므로 앞서 발생한 문제가 야기된 것이다.</p>
<blockquote>
<p>React Router: 화면 전환 = 기존 컴포넌트 unmount 후 새 컴포넌트 mount
React Navigator: 화면 전환 = 기존 컴포넌트가 stack에서 mount된 상태로 유지되면서 새 컴포넌트가 스택의 가장 위로 pop</p>
</blockquote>
<h1 id="3-그렇다면-어떻게-해결해야-할까">3. 그렇다면 어떻게 해결해야 할까?</h1>
<h2 id="3-1-navigation-event-활용하기">3-1. Navigation event 활용하기</h2>
<p>React Navigation 공식 문서에는 친절하게도 해결 방법이 잘 나와있다. React Navigation에서는 사용자의 스크린 in/out에 대한 event를 focus/blur로 제공하며 [Fig. 4]로 쉽게 이해할 수 있다. 이 이벤트는 두 가지 방법으로 구현할 수 있으며 나는 사용이 더 간편한 <code>useFocusEffect</code> hook을 활용하였다.
(다만 버전 5부터 이 해결법을 제시하고 있으므로 그 아래 버전을 사용한다면 해당 공식 문서 참조를 추천한다.)
<img src="https://velog.velcdn.com/images/skyu_dev/post/e1fd737b-55b7-4b33-a5e7-4a0f50111a96/image.png" alt=""></p>
<ul>
<li><p>useEffect 사용</p>
<pre><code class="language-js">React.useEffect(
  () =&gt; navigation.addListener(&#39;focus&#39;, () =&gt; alert(&#39;Screen was focused&#39;)),
  []
);
React.useEffect(
  () =&gt; navigation.addListener(&#39;blur&#39;, () =&gt; alert(&#39;Screen was unfocused&#39;)),
  []
);</code></pre>
</li>
<li><p>useFocusEffect 사용</p>
<pre><code class="language-js">import { useFocusEffect } from &#39;@react-navigation/native&#39;;
useFocusEffect(
  React.useCallback(() =&gt; {
    // Do something when the screen is focused

    return () =&gt; {
      // Do something when the screen is unfocused
      // Useful for cleanup functions
    };
  }, [])</code></pre>
<h2 id="3-2-usefocuseffect를-사용한-화면-전환-시-초기화-문제-해결">3-2. useFocusEffect를 사용한 화면 전환 시 초기화 문제 해결</h2>
<p>나는 day state의 초기화가 필요한 Home 컴포넌트 내부에 아래 코드와 같이 useFocusEffect hook을 추가하였다. 아래 영상을 통해 #1-2.와는 다르게 선택된 날짜의 보라색 선이 화면 재전환 시 사라지는 것을 확인할 수 있다.</p>
<pre><code class="language-js">useFocusEffect(
  useCallback(() =&gt; {
    setDays(markedDays);
  }, [])
);</code></pre>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/ca581e96-c2c7-47f0-8137-e5adc44b99f4/image.gif" alt=""></p>
</li>
</ul>
<h1 id="마치며">마치며</h1>
<p>리액트에 비해 상대적으로 유저가 많이 적은 리액트 네이티브를 사용하다보니 디버깅이 굉장히 까다롭다고 느끼고 있다. 이 프로젝트를 하며 느끼는 점은 어렵긴 하지만 공식 문서를 직접 읽어봐야 한다는 것이다. 내가 원하는 내용을 10을 얻기 위해서 적게는 50 많게는 100정도의 양을 읽어야 하지만, 혼자 삽질하는 것보다는 백만배 낫다는 것을 뼈져리게 느끼고 있다. 클라이언트 상태 관리도 이렇게 어려운데 서버 데이터가 들어오면 어떨지... ^^</p>
<p>추가로 Home이 아닌 다른 화면에서는 초기화가 되지 않는 점을 잘 활용할 수 있을 것 같다. 아마 react navigation 팀이 의도한 사용법이지 않을까... </p>
<p><em>오늘의 교훈: 공식 문서를 열심히 읽자!(이런 짤이 있는 걸 보니 나만 보기 싫어하는 건 아닌가보다..^^)</em>
<img src="https://velog.velcdn.com/images/skyu_dev/post/55bcc614-d1b7-429b-a33b-8645b23dfb6e/image.jpeg" alt=""></p>
<h4 id="참고-자료">참고 자료</h4>
<p><a href="https://reactnavigation.org/docs/navigation-lifecycle">https://reactnavigation.org/docs/navigation-lifecycle</a>
<a href="https://im-developer.tistory.com/215">https://im-developer.tistory.com/215</a>
<a href="https://daesiker.tistory.com/39">https://daesiker.tistory.com/39</a>
<a href="https://velog.io/@soyi47/React-Component%EC%9D%98-Lifecycle">https://velog.io/@soyi47/React-Component의-Lifecycle</a>
이 글에 사용된 모든 자료는 직접 제작하였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[나의 FE 로드맵(2)] 두 달 간의 학습 회고록 & 프로젝트 및 로드맵 재설정]]></title>
            <link>https://velog.io/@skyu_dev/%EB%82%98%EB%A7%8C%EC%9D%98-FE-%EB%A1%9C%EB%93%9C%EB%A7%B52-%EB%91%90-%EB%8B%AC-%EA%B0%84%EC%9D%98-%ED%95%99%EC%8A%B5-%ED%9B%84%EA%B8%B0-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90-%EC%A7%91%EC%A4%91%ED%95%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%AC%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@skyu_dev/%EB%82%98%EB%A7%8C%EC%9D%98-FE-%EB%A1%9C%EB%93%9C%EB%A7%B52-%EB%91%90-%EB%8B%AC-%EA%B0%84%EC%9D%98-%ED%95%99%EC%8A%B5-%ED%9B%84%EA%B8%B0-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90-%EC%A7%91%EC%A4%91%ED%95%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%AC%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sun, 09 Oct 2022 10:53:35 GMT</pubDate>
            <description><![CDATA[<h1 id="1-회고록">1. 회고록</h1>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/d148a695-db76-4dc9-9a63-2bfab996d48a/image.png" alt=""></p>
<p>처음으로 웹 개발에만 몰두한지 벌써 2 개월이 지났다. 내가 만들고 싶었던 서비스들을 구현하는 과정은 재밌었지만 생각보다 더 웹 개발 기반이 약하다는 것을 느꼈고, 프로젝트들을 구현하는 시간보다 JavaScript와 React의 기초를 다지는 것에 더 많은 시간을 투자하고 있다. 그리고 진행중인 프로젝트 업데이트 현황을 아래에서 자세히 설명하려 한다.</p>
<h2 id="1-1-feel-your-space-회고록">1-1. Feel your space 회고록</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/502f691f-0b7c-4bbb-a057-fb62ba1c1766/image.gif" alt=""> [Project demo 1. 현재 구현된 기능들. 영상 hover 시 미리듣기도 구현함]</p>
<p>오랜만에 갖게 된 나만의 시간이어서인지 이것저것 배워보고 싶었던 욕심이 컸기에 정말 많은 시행착오를 겪었다. 그 중 가장 나의 골머리를 앓게 한 것은 SSR, SSG를 배워보고 싶어 시작한 Next.js 풀스택 프로젝트인 <a href="https://velog.io/@skyu_dev/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D-1-%ED%95%98%EA%B3%A0%EC%8B%B6%EC%9D%80-%EA%B1%B8-%EC%95%88%ED%95%98%EA%B8%B0%EA%B0%80-%EC%89%BD%EC%A7%80-%EC%95%8A%EC%95%84">Feel your space</a>였다. 토이 프로젝트가 아닌 내가 실제로 런칭하고 싶은 서비스여서 Next.js, TailwindCSS, GraphQL, Apollo Client 등 최신 기술 스택들을 모두 때려 넣어 진행하였고, 디자인에도 많은 공을 들였다. 실제로 <a href="https://velog.io/@skyu_dev/GraphQL-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C-YouTube-API%EB%A1%9C-%EB%82%98%EB%A7%8C%EC%9D%98-API-%EB%A7%8C%EB%93%A4%EA%B8%B0">Youtube REST API ➡️ GraphQL</a>로 바꾸는 백엔드를 먼저 구축해놓았지만 Youtube API의 일일 할당량 제한에 막히는 절망스러운 상황이 발생했고, 특정 키워드의 영상들을 DB에 저장하고 admin에서만 해당 API를 사용하는 우회 방법을 사용하기로 했다.</p>
<p>그러나 프로젝트 크기가 점점 커지면서 겉핥기식으로 구현만 해내는 회의감이 강하게 들었고, 내가 공부해야 할 대상이 기초 JS, React, 비동기 통신, 상태 관리 등 프론트엔드 개발의 기반이라는 것을 깨닫게 되었다. 따라서 본 프로젝트는 다른 토이 프로젝트들을 해본 후 다시 진행하는 것으로 일시 정지하였고, 다른 개인 프로젝트인 <strong>Who is Quartz?</strong>를 진행할 예정이다.</p>
<h2 id="1-2-post-black-belt-회고록">1-2. Post Black Belt 회고록</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/388d4550-6313-48fb-9eeb-84d79de48450/image.gif" alt=""> [Project demo 2. 현재 구현된 기능들]</p>
<p>주짓수 도장의 백엔드 개발자 관원과 함께하는 이 프로젝트는 내가 프론트엔드 개발에 조금 더 집중할 수 있게 해주었다. <a href="https://velog.io/@skyu_dev/Post-Black-Belt2-%EC%95%B1-%EB%A7%8C%EB%93%9C%EB%8A%94%EB%8D%B0-%EC%99%9C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EB%BD%91%EC%9D%84%EA%B9%8C-Web-App-%EA%B0%9C%EB%B0%9C%EA%B8%B0">웹 ➡️ 모바일 개발로 전환하는 과정</a>이 힘들기는 했지만 JS로 앱 개발이 가능하다는 것에 큰 흥미를 느끼고 있다.</p>
<p>이 프로젝트에서는 나의 좋지 않은 습관 중 하나인 빠른 개발을 위해 공식 문서보다는 번역 &amp; 정리가 친절한 블로그 글을 참고하는 습관을 개선하고 있다. 그 계기 중 첫 번째는 공교롭게도 React Native용 프레임워크인 
<a href="https://blog.expo.dev/sunsetting-the-web-ui-for-expo-cli-ab12936d2206">Expo에서 올해 7월부터 expo-cli의 웹 UI 서비스를 종료</a>하면서 앱 빌드를 위해 발표한 새로운 문서를 반강제로 직접 읽어야 했었다. 두 번째는 React Native 자체 사용자 층이 좁아 사용 예시들이 적기 때문에 <a href="https://github.com/wix/react-native-calendars">react-native-calendar</a> 모듈을 일기 형식으로 커스터마이징하기 위해 깃허브를 직접 참고하였다. 시간과 노력을 더 쏟아야 하지만 오히려 이 방법이 안전한 루트이겠다는 생각이 들었고 그동안의 내 모습을 잠시 반성하였다...</p>
<h2 id="총-회고록">총 회고록</h2>
<p>지난 두 달 간의 개발 공부 끝에 나는 아래와 같은 3 가지 개선점을 찾았고, 이 점들을 보완해서 로드맵과 프로젝트를 재설정하려 한다.</p>
<blockquote>
<ol>
<li>풀스택이 아닌 프론트엔드 개발에 집중하자</li>
<li>구현에 치중하지 말고 JS와 React 기초를 다지자</li>
<li>한 프로젝트에 새로운 기술 스택을 너무 많이 담지 말자</li>
</ol>
</blockquote>
<h1 id="2-로드맵-재설정20221009-ver">2. 로드맵 재설정(2022.10.09 ver)</h1>
<p>로드맵은 notion에서 지속적으로 관리할 예정이며 현재 기술 사용 가능 정도를 Status, 역량 카테고리를 Tag로 분류하였다.</p>
<h2 id="2-1-프론트엔드와-직접적으로-연관된-역량">2-1. 프론트엔드와 직접적으로 연관된 역량</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/96cf00f9-af72-4c9a-80d8-f4765d06d3cd/image.png" alt=""></p>
<h2 id="2-2-기타-개발-역량">2-2. 기타 개발 역량</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/26a27dad-a94c-4a30-b36c-67824c9e58e1/image.png" alt=""></p>
<h1 id="3-올해-목표">3. 올해 목표</h1>
<p>올해가 3 달 남은 시점에서 내가 이루고 싶은 것은 JS와 React 기초를 탄탄히 하고, 2 개의 프로젝트를 완성하여 포트폴리오를 작성하는 것이다.</p>
<h2 id="3-1-js-react-기초-공부">3-1. JS, React 기초 공부</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/ac2685af-afed-46cf-94b6-361aaed9774a/image.png" alt="">
현재 Udemy에서 JS와 React 두 가지 강의를 수강하고 있고, 아는 내용이라고 생각하더라도 수업을 들으려 노력하고 있다. 그동안 빠른 취업을 위해 프로젝트를 구현하는 강의만 수강하여서 기초를 많이 놓치고 있었다는 것을 다시금 깨달았다. 이 내용들은 개인 노션에 정리중이고 시간이 나면 중요한 개념들은 틈틈히 벨로그에 글을 올리면서 복기할 예정이다.</p>
<h2 id="3-2-토이-프로젝트">3-2. 토이 프로젝트</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/2facf20e-645d-4426-9ecc-79789d77c91d/image.png" alt="">
토이 프로젝트는 위에서 이야기한 내용들과 같이 2 가지를 진행할 예정이다.</p>
<h3 id="post-black-belt현재-진행중">Post Black Belt(현재 진행중)</h3>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/58510f4b-84f8-4d8a-a2b1-55cde9983117/image.png" alt=""></p>
<h3 id="who-is-quartz기획-예정">Who is Quartz?(기획 예정)</h3>
<p>이 프로젝트는 최대한 백엔드없이 gh-pages로 간단하게 배포하고, 만약 필요하다면 내가 구축하는 것이 아니라 Firebase를 사용하여 간단하게 구현할 예정이다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/ba378b5d-3741-4b4a-aacd-deab7a45cb07/image.png" alt=""></p>
<p>지난 두 달이 번뇌와 절망이 가득한 나날들이었지만 내가 하고 싶은 공부를 한다는 것에는 많은 행복과 보람을 느꼈던 것 같다. 사실 프로젝트가 중단되고 다시 기반을 쌓으면서 해낸 것이 없다는 고민이 있었는데 벨로그에 정리한 글들을 보니 생각보다 잘해내고 열심히 산 것 같다. 올해가 마무리될 때 쯤에는 더 발전한 모습이길 간절히 희망한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PBB(2)] 앱📱 만드는데 왜 프론트엔드 개발자를 뽑을까? | Web ➡️ Mobile App 전환하기]]></title>
            <link>https://velog.io/@skyu_dev/Post-Black-Belt2-%EC%95%B1-%EB%A7%8C%EB%93%9C%EB%8A%94%EB%8D%B0-%EC%99%9C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EB%BD%91%EC%9D%84%EA%B9%8C-Web-App-%EA%B0%9C%EB%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@skyu_dev/Post-Black-Belt2-%EC%95%B1-%EB%A7%8C%EB%93%9C%EB%8A%94%EB%8D%B0-%EC%99%9C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EB%BD%91%EC%9D%84%EA%B9%8C-Web-App-%EA%B0%9C%EB%B0%9C%EA%B8%B0</guid>
            <pubDate>Tue, 04 Oct 2022 09:39:55 GMT</pubDate>
            <description><![CDATA[<h1 id="1-우리-프로젝트를-앱으로-바꿔야-할-것-같은데">(1) 우리 프로젝트를 앱으로 바꿔야 할 것 같은데?</h1>
<p>청천벽력⛈같은 팀원의 말이었다. 어느 정도 나도 모바일 앱의 필요성을 느끼고 있었지만 더 이상 웹 프로젝트의 진행이 의미가 없을 것 같다는 결론이었다. 이 배경에는 아래와 같은 2 가지 이유가 있었다.</p>
<blockquote>
<ol>
<li>기존의 주짓수 기술을 분류하는 서비스는 기술마다 많은 일러스트를 필요로 하며, 각 기술을 단 하나의 일러스트로 표현하기에는 많은 주관이 들어간다.</li>
<li>주짓수 입문자인 기획자들이 주짓수 초급자부터 상급자까지 아우르는 서비스를 만들기에는 한계가 있다.</li>
</ol>
</blockquote>
<p>따라서 우리는 <strong>주짓수 입문자들을 위한 일기</strong>를 주제로 Post Black Belt 프로젝트를 변경하였고 사용 빈도와 푸쉬 알림 등을 고려하여 모바일 앱 개발을 결정하게 되었다. 나에게는 앱 개발의 과정조차도 익숙하지 않았는데 백엔드 서버는 동일하고 클라이언트가 웹 브라우저에서 앱으로 변경된다는 것이 가장 큰 차이점이라고 한다. </p>
<p>비록 지금이 내가 해보고 싶은 것들을 실현하는 기간이라 하더라도 <strong>&quot;프론트엔드 개발자에게 앱을 만드는 경험이 과연 필요할 것인가?&quot;</strong> 라는 의문점을 해결하기 위해 이 글을 작성하였고,
리액트 네이티브를 사용하여 나의 첫 앱 개발을 하기로 결정한 과정을 앞으로 설명해보려 한다.</p>
<h1 id="2-앱을-만드는-기업에서-왜-웹-개발자를-뽑는걸까">(2) 앱을 만드는 기업에서 왜 웹 개발자를 뽑는걸까?</h1>
<p>우리가 생각하는 굴지의 IT 기업들 중 우아한 형제들, 당근 마켓, 카카오 페이, 토스 등은 실 사용자들이 대부분 모바일 앱에 분포되어있다. 면밀히 살펴보면 그들이 가진 웹 페이지는 회사 소개나 채용 공고와 같이 실제 서비스와는 동떨어져 있는 경우가 많은데 왜 이런 기업들은 앱 개발자 뿐만이 아니라 웹 개발자들도 채용하는 것일까?</p>
<h2 id="모바일-어플리케이션을-개발하는-방법">모바일 어플리케이션을 개발하는 방법</h2>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/798759c3-68ae-4271-9a8f-674dee029819/image.png" alt="">
앱 개발은 크게 3 가지로 나눌 수 있으며 각각에 대한 간단한 설명은 아래와 같다.</p>
<h3 id="1-native-app">1. Native App</h3>
<p>각각의 운영체제에서 각각 다른 프로그래밍 언어로 개발하여 외적인 지원없이 OS에서 그대로 실행됨</p>
<ul>
<li>안드로이드: 안드로이드 스튜디오 개발 환경에서 Java, Kotlin 언어 사용</li>
<li>iOS: MacOS Xcode 개발환경에서 Object-C, Swift 언어 사용</li>
</ul>
<p>모바일 앱의 Look and Feel을 잘 살릴 수 있으나, OS별 일의 중복성이라는 단점이 존재함</p>
<h3 id="2-hybrid-appweb-view">2. Hybrid App(Web View)</h3>
<p>WebView UI component를 사용하여 웹 기술을 그대로 사용할 수 있음
여전히 네이티브 지식이 필요하고 웹뷰에서 동작하므로 성능이 느리고 네이티브 API를 모두 활용할 수 없다는 단점이 있음</p>
<h3 id="3-cross-platform-app">3. Cross Platform App</h3>
<p>하나의 프로그래밍 언어로 두 운영체제에서 동작하는 앱을 빌드
ex) React Native(JS), Flutter(Dart)</p>
<ul>
<li>React Native: React와 동일하게 자바스크립트를 사용하여 개발<ul>
<li>React in web: Virtual DOM을 이용하여 브라우저 DOM을 업데이트</li>
<li>React Native in mobile: Mobile API를 이용해서 모바일 UI를 업데이트</li>
</ul>
</li>
</ul>
<p>하나의 언어로 여러 플랫폼에서 동작하는 앱을 개발할 수 있다는 장점이 있으나, 하이브리드 앱과 동일하게 네이티브 앱의 성능을 모두 살릴 수 없으며 지나치게 크로스 플랫폼에 의존해야 하여 디버깅이 까다로워질 수 있다. 리액트 네이티브를 개발한 페이스북에서도 위와 같은 이유로 실제 사용량을 줄이고 있는 추세라고 한다.</p>
<h2 id="국내-기업들의-모바일-앱-개발">국내 기업들의 모바일 앱 개발</h2>
<p>실제 기업에서는 3 가지 중 하나를 채택하기보다는 혼용하여 서비스마다 더 나은 방식으로 구현하고 있다고 한다.</p>
<ul>
<li>네이티브 앱 + 하이브리드 앱(웹뷰): 당근마켓, 우아한 형제들, 카카오페이</li>
<li>네이티브 앱 + 크로스 플랫폼: 토스, 직방, 우아한 형제들<h2 id="나는-어떤-방식의-앱-개발을-선정해야-할까">나는 어떤 방식의 앱 개발을 선정해야 할까?</h2>
위의 내용들을 확인하고 다시 채용 공고들을 살펴보니 앱 개발도 나의 영역에 들어올 수 있다는 생각이 들었다. 그렇다면 어떤 방법이 지금 내가 공부하고 있는 내용들을 최대한 수용할 수 있을까? 나는 아래와 같은 이유로 Cross Platform App 중 React  Native를 사용한 앱 개발을 선정하였다.<blockquote>
<p>React Native 선정 이유</p>
</blockquote>
</li>
</ul>
<ol>
<li>Native app에 대한 지식이 전무함</li>
<li>현재 공부중인 React의 기능(ex. hooks)을 그대로 활용할 수 있음</li>
<li>Android, iOS 모두 한 번에 개발(+Web)</li>
</ol>
<h1 id="3-web-➡️-mobile-app으로-ui-변경하기">(3) Web ➡️ Mobile App으로 UI 변경하기</h1>
<p>이제 개발 환경이 변경되었으니 UI를 다시 만들 시간이다. 모바일 UI를 짜면서 느낀 웹과 앱의 가장 큰 차이점은 2 가지다.</p>
<ol>
<li>화면의 종횡비가 반대다.</li>
<li>Interaction의 기준: web은 click과 hover, 앱은 touch </li>
</ol>
<p>아래는 테크 트리 화면과 테크 상세 화면의 웹 vs 모바일 UI를 비교한 예시이다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/d9d921ab-177e-421a-ab4f-03d0829af690/image.png" alt="">
Figure 1.의 테크 트리 페이지는 단순히 화면의 비율만을 변경하여 쉽게 UI 변경이 이루어졌다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/ede6aea7-71f1-4594-8437-993d051aabac/image.png" alt="">
Figure.2에 경우 동일하게 상세 기술을 선택할 때 동일하게 toggle을 사용하여 기술 아이템들이 확장되게 할 수 있었으나, 정보가 비교적 적게 담기는 모바일 특성상 많은 스크롤링을 야기해 적합하지 않다고 생각하였다. 따라서 우측과 같이 손가락의 좌우 스크롤링을 사용하여 기술 아이템들이 확장되도록 변경하였다.</p>
<h3 id="가장-아쉬웠던-점-커서-애니메이션의-무덤행">가장 아쉬웠던 점: 커서 애니메이션의 무덤행....</h3>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/0364e9f1-0143-4e9c-aba6-b3619083a344/image.gif" alt="">
UI를 변경하면서 가장 아쉬웠던 점은 내가 웹 기획시 가장 공을 들였던 커서 애니메이션이었다. 동적 애니메이션 + 상태 관리(Redux) 기술의 사용을 공부하기 위해 도입하였던 UI 컴포넌트였는데, 모바일은 사용자의 손가락 움직임을 따라가지 않으므로 재활용할 수 있는 방법이 거의 없었다. 이 기술은 아쉽지만 추후 다른 프로젝트에 더 발전시켜서 활용하는 것을 기약하였다.</p>
<h1 id="4-모바일스러운-ui로-수정하기">(4) 모바일스러운 UI로 수정하기</h1>
<p>앞서 언급한 웹과의 2 가지 차이점 외에도 모바일 UI 개발 시 수많은 부분들을 고려해야 한다. 그 중 직접 기기에 앱을 임시로 올려보고 찾은 수정사항을 하나 소개한다.
<img src="https://velog.velcdn.com/images/skyu_dev/post/4ae8b85e-2a06-4079-9b97-c521b03c4238/image.png" alt="">
모바일은 글을 작성할 때 물리적인 키보드가 없으므로 화면 하단에서 키보드가 플로팅된다. 따라서 Figure 3.의 좌측과 같이 버튼을 배치하면 화면이 가려지거나 작성하는 박스가 시야에서 사라져버리는 문제가 발생한다. 이 경우 우측과 같이 버튼을 상단에 배치하는 것이 더 &quot;모바일스러운 UI&quot;라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/skyu_dev/post/f89c55d3-8e07-4e62-8422-27dd72c3fddd/image.png" alt="">
이 점들을 바탕으로 Figma를 사용하여 대부분의 UI 재구성이 완료되었으며 본격적으로 React Native를 활용한 개발을 시작해보려 한다.</p>
<h4 id="참고자료">참고자료</h4>
<p><a href="https://youtu.be/2AS0WAOX8_8">모바일 앱 개발의 현재와 미래 (네이티브, 하이브리드, 크로스 플랫폼 앱 장단점과 전망)</a>
<a href="https://medium.com/daangn/%EB%8B%B9%EA%B7%BC%EB%A7%88%EC%BC%93%EC%97%90-%EC%9B%B9-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-1-%ED%8C%8C%EC%9D%BC-%EA%B8%B0%EB%B0%98-%EC%9B%B9%EB%B7%B0-d312b17e697c">당근마켓에 웹 프로젝트 배포하기 #1 — 파일 기반 웹뷰</a>
<a href="https://youtu.be/b_6CjuvVg8o">토스ㅣSLASH 22 - 미친 생산성을 위한 React Native</a>
<a href="https://blog.kakaopay.com/180">서비스의 첫 인상을 만드는 FE 개발 문화와 일하는 방식</a></p>
]]></description>
        </item>
    </channel>
</rss>