<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>DEVElog</title>
        <link>https://velog.io/</link>
        <description>주먹펴고 일어서서 코딩해</description>
        <lastBuildDate>Thu, 16 Jan 2025 21:07:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>DEVElog</title>
            <url>https://velog.velcdn.com/images/ferrari_roma/profile/9833f2e2-789e-423e-9041-e32fb2ccf26e/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. DEVElog. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ferrari_roma" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[리액트 네이티브에서 중첩 모달로 발생한 충돌]]></title>
            <link>https://velog.io/@ferrari_roma/Handling-multiple-modals-in-React-Native</link>
            <guid>https://velog.io/@ferrari_roma/Handling-multiple-modals-in-React-Native</guid>
            <pubDate>Thu, 16 Jan 2025 21:07:10 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 작업하던 중 정말 난감한 오류를 마주쳤다. iOS 시뮬레이터에서 Expo 앱이 충돌하는 문제였는데, <a href="https://github.com/react-navigation/react-navigation/issues/11270">React Navigation 이슈</a> 에서 언급된 사례와 비슷한 상황이었다. 문제는 내가 다른 팀원이 작성한 코드를 리팩토링하던 중 발생했다. 해당 코드는 여러 개의 모달을 동시에 렌더링하고 있었고, 모달 안의 버튼을 클릭하면 다른 경로(route)의 화면으로 이동해야 했다. 그런데 버튼을 클릭하자마자 Expo 앱이 바로 충돌해버렸다.</p>
<p>특히나 충돌 로그가 전혀 남지 않았기 때문에 몇 번이고 같은 에러를 발생시키면서 원인을 찾으려 해도 찾을 수 없었다. 에러 로그 대신에 다음 사진처럼 생긴 macOS의 충돌 보고서만 표시되었다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/4f6de57b-478f-41db-9382-ed6d195c10b9/image.png" alt="error report"></p>
<p>문제를 파악하는 데 꽤 오랜 시간이 걸렸다. 다행히도 GitHub가 항상 그렇듯 날 살려주었다.. 처음에는 react-native-modal 레포지토리로 가서 비슷한 문제를 겪은 사람들이 있는지 확인했다. 그리고 이슈 탭과 FAQ 섹션에서 힌트를 얻을 수 있었다.</p>
<p>결론적으로, React Native는 여러 모달을 동시에 렌더링하는 것을 잘 처리하지 못한다는 것이 문제였다. React Native 자체의 한계로 인해 이런 상황에서 충돌이 발생할 수 있었다. 우리 코드에서는 이미 이를 우회하기 위한 트릭을 사용하고 있었지만, 이 방식에서 다른 경로로 네비게이션을 하는데 에러가 발생했다. 모달과 모달이 위치하고 있는 스크린이 완전히 unmount되기 전에 네비게이션을 시도하면서 충돌이 발생한 것으로 보였다.
GitHub에서 추천된 몇 가지 해결책을 시도해봤지만 나에게는 효과가 없었다. 예를 들어 setTimeout이 있었다. 몇몇 분들은 상태 업데이트가 완료될 때까지 비동기적으로 기다리는 걸로 해결을 했다고 했지만 나에게는 효과가 없었다.</p>
<p>결국, 나는 겹쳐진 모달을 분리하고 onModalHide 이벤트를 활용하는 방식으로 문제를 해결했다. 아래는 초기 코드이다.</p>
<pre><code class="language-tsx">// ChlidrenModal.tsx
const ChildrenModal = ({isVisible, ...}:PropsInterface) =&gt; {
  ...
  return (
      &lt;Modal isVisible={isModalVisible} 
            onClickBtn=
            {()=&gt;navigation.navigate(&#39;OtherRouteScreen&#39;)} ... /&gt;
  )
}

// ParentsModal.tsx
const ParentsModal = ({isVisible, ...}:PropsInterface) =&gt; {
  const [isModalVisible, setIsModalVisible] = useState(false);
  ...
  return (
      &lt;&gt;
            &lt;Modal isVisible={isVisible} /&gt;
          &lt;ChildrenModal isVisible={isModalVisible} 
            onClickBtn=
            {()=&gt;navigation.navigate(&#39;OtherRouteScreen&#39;)} ... /&gt;
      &lt;/&gt;
  )
}

// RandomScreen.tsx
const RandomScreen = () =&gt; {
  const [isParentsModalVisible, setIsParentsModalVisible] = useState(false);
  ...
  return (
      &lt;&gt;
      ...
      &lt;ParentsModal isVisible={isParentsModalVisible} ... /&gt;
    &lt;/&gt;
  )
}</code></pre>
<p>위 코드에서는 ParentsModal과 ChildrenModal이 중첩되어 있었다. 일단 먼저 분리를 해서 중첩된 모달을 분리했다.</p>
<pre><code class="language-tsx">// ChlidrenModal.tsx didn&#39;t change

// ParentsModal.tsx
const ParentsModal = ({isVisible, ...}:PropsInterface) =&gt; {
  ...
  return (
    &lt;Modal isVisible={isVisible} /&gt;
  )
}

// RandomScreen.tsx
const RandomScreen = () =&gt; {
  const [isParentsModalVisible, setIsParentsModalVisible] = useState(false);
  const [isChildrenModalVisible, setIsChildrenModalVisible] = useState(false);
  ...
  return (
      &lt;&gt;
      ...
      &lt;ParentsModal ... /&gt;
      &lt;ChildrenModal ... /&gt;
    &lt;/&gt;
  )
}</code></pre>
<p>이후 <code>onModalHide</code>과 useRef를 활용해서 모달이 닫힐 때 시나리오에 따라 분기처리를 했다.</p>
<pre><code class="language-tsx">// ParentsModal.tsx and ChlidrenModal.tsx  didn&#39;t change

// RandomScreen.tsx
const RandomScreen = () =&gt; {
  const [isParentsModalVisible, setIsParentsModalVisible] = useState(false);
  const [isChildrenModalVisible, setIsChildrenModalVisible] = useState(false);
  const conditionInModalHideEvent = useRef({
            ...
            isClickedNavigationBtn: false,
          }
        )

  const onClickBtn = () =&gt; {
    conditionInModalHideEvent.curretn.isClickedNavigationBtn = true;
    ...
  }

  const onModalHide = () =&gt; {
    // condition with ref
    if(conditionInModalHideEvent.curretn.isClickedNavigationBtn) {
      conditionInModalHideEvent.curretn.isClickedNavigationBtn = false;
      navigation.navigate(&#39;other route&#39;);
    }
  }

  ...
  return (
      &lt;&gt;
      ...
      &lt;ParentsModal ... /&gt;
      &lt;ChildrenModal onClickBtn={onClickBtn} onModalHide={onModalHide} ... /&gt;
    &lt;/&gt;
  )
}</code></pre>
<p>여기서 useState 대신 useRef를 사용했다. 그 이유는 불필요한 리렌더링을 피하고 싶었기 때문이다. <code>onModalHide</code>에서 조건을 확인할 때 state를 사용하면 조건문에서 렌더링이 발생한다. 즉, <code>onModalHide</code>에서는 navigation을 하려고 하는데, 스크린에서는 다시 리렌더링을 하려는 상황이 발생한다는 것이다. 이는 원래 문제가 일어나는 이유로 추측하는 그 조건을 또 다시 충족하게 되는 것이다.
또 다른 이유는 UI와 직접적으로 관련된 것이 아니고 단순히 논리적인 흐름을 제어하기 위한 것이기 때문이다. 그렇기 때문에 useRef가 더 적합하다고 판단했다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/0ab06b60-6951-444d-9526-523ab8094eb0/image.gif" alt=""></p>
<p>수정 후에는 완벽하게 작동했다! 더 나은 방법이 있겠지만, expo crash report 창이 눈 앞에 나타났을 때를 생각해 보면 아주아주 다행이다.</p>
<hr/>

<p><em>참고 자료</em>
<a href="https://github.com/react-native-modal/react-native-modal?tab=readme-ov-file#i-cant-show-multiple-modals-at-the-same-time">react native modal repo</a>
<a href="https://github.com/react-navigation/react-navigation/issues/11270">react navigation issue</a>
<a href="https://github.com/react-native-modal/react-native-modal/issues/30">react native modal issue</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[flexbox에서 왜 justify는 content고, align은 items일까?]]></title>
            <link>https://velog.io/@ferrari_roma/justify%EB%8A%94-%EC%99%9C-content%EA%B3%A0-align%EC%9D%80-items%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@ferrari_roma/justify%EB%8A%94-%EC%99%9C-content%EA%B3%A0-align%EC%9D%80-items%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Thu, 17 Oct 2024 17:05:56 GMT</pubDate>
            <description><![CDATA[<p>&#39;justify는 왜 content고, align은 items일까?&#39;, &#39;왜 justify-items는 flex에서 사용하지 않을까?&#39;, &#39;왜 grid에서는 justify-items를 사용할까?&#39;
위 내용들은 궁금해진지 오래됐지만, 이제서야 머릿속에서 정리가 되는 질문에 대한 나만의 답이다. 항상 경험적으로 이렇게 사용했고 궁금해 질때마다 mdn을 읽고서 &#39;맞네, 맞아&#39;하고 시간이 지나면 까먹는 나를 발견하고서 나의 말로 내가 직접 정리해 두면 좋을 거 같아서 정리해 본다.
기본적인 flexbox, grid 그리고 main/cross axis을 알고 읽어야 이해가 될 것이다. 이에 대한 설명은 없으니 양해를 바란다.</p>
<h1 id="justifyalign-content속성은-줄line의-간격을-컨트롤하는-속성이다"><code>justify/align-content</code>속성은 줄(line)의 간격을 컨트롤하는 속성이다.</h1>
<p>&#39;flexbox&#39;는 main axis(메인축)를 따라 아이템들이 채워져 나가는 것이다. 즉 메인축을 따라 여러 &#39;줄&#39;(line)이 쌓여간다. 이런 이유로 메인축은 줄의 간격을 컨트롤하는 것이 기본이 된다. 이 때문에 우리는 으레 메인축의 배치를 <code>justify-content</code>로 조정했던 것이다.
<img src="https://velog.velcdn.com/images/ferrari_roma/post/f69560a3-ec32-4408-bf68-54c8ac0e63a8/image.png" alt="">
<em>flex-wrap을 설정하지 않는 이상 무조건 메인축을 따라 &#39;줄&#39;이 쌓인다.</em></p>
<p>즉, <code>justify-content</code>는 각 아이템의 위치를 옮기는 것이 아니라, 메인축을 따라 늘어선 라인의 간격을 조정한다. 그래서 이 속성을 수정할 때마다 라인 사이사이의 &#39;간격&#39;이 달라진다.
하지만 cross axis(교차축)의 배치는 <code>flex-wrap</code> 속성을 설정하지 않고는 여러 &#39;줄&#39;로 쌓아나갈 수가 없다. <code>flex-wrap</code> 속성을 설정해서 교차축에 여러 &#39;줄&#39;을 배치하게 되면 그때 되어서야 줄 사이의 &#39;간격&#39;을 컨트롤 하는 <code>align-content</code>를 사용할 수 있다. 즉, &#39;content&#39;는 줄의 &#39;간격&#39;을 컨트롤한다. 여기서 &#39;justify&#39;는 메인축을 담당하는 것이고 &#39;align&#39;은 교차축을 담당하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/473cf0ee-1326-49a0-8691-65cb285f05db/image.png" alt="">
<em>&#39;justify-content: space-evenly&#39;일 때 줄 사이의 &#39;간격&#39;이 변한다.</em></p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/5c05fcad-12e0-4c15-95ac-c2d1981fca79/image.png" alt="">
<em>flex-wrap을 설정하고 나서 &#39;align-content&#39;를 설정해주면 교차축의 &#39;간격&#39;이 변한다.</em></p>
<br/>

<p>&#39;grid&#39;에서도 똑같다. &#39;grid&#39;에서도 &#39;content&#39;속성은 줄의 &#39;간격&#39;을 조정한다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/32946516-116e-4530-9edd-f734ed7cb43b/image.png" alt="">
<em>기본 그리드 설정</em></p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/43423101-754c-49a3-ae6f-98a56910ac6e/image.png" alt="">
<em>&#39;justify-content: space-evenly&#39;일 때 그리드 줄의 &#39;간격&#39;이 변한다.</em></p>
<h1 id="justifyalign-items속성은-각-줄-혹은-셀cell-안의-아이템의-배치를-컨트롤하는-속성이다"><code>justify/align-items</code>속성은 각 줄 혹은 셀(cell) 안의 아이템의 배치를 컨트롤하는 속성이다.</h1>
<p>&#39;items&#39;는 줄을 컨트롤 하지 않는다. 그래서 줄 사이의 &#39;간격&#39;을 조정할 수는 없다. &#39;items&#39;의 역할은 각 줄 혹은 셀의 아이템을 어떻게 배치하는지를 컨트롤하는 속성이다. 이 때문에 <code>justify-items</code>는 &#39;flexbox&#39;에서 무시된다. 메인축의 줄을 따라 아이템이 늘어서기 때문에 <code>justify-items</code>가 적용될 여백 자체가 존재하지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/178a158a-a140-4df8-b59d-dc24b1a48318/image.png" alt="">
<em>div 갯수를 줄여서 보면 메인축(가로)를 따라서 줄이 늘어서 있는 걸 확인할 수 있다.
여기서 메인축(가로)의 여백 자체가 존재하지 않는다.</em>
<img src="https://velog.velcdn.com/images/ferrari_roma/post/7029b571-8e3c-4ae0-a1c9-ff9c0f22f644/image.png" alt="">
<em>그래서 <code>justify-items: flex-start</code>를 해도 무시된다.</em></p>
<p>생각을 해보면 우리가 메인축을 따라 늘어선 줄 자체의 너비를 설정하는 방법은 해당 축에 위치하는 요소의 너비나 높이를 조절하는 것 빼고는 방법이 없다. 즉, 그 요소의 너비와 높이가 메인축을 따라 늘어선 줄의 너비가 되는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/e8ed705d-ce89-491f-b484-72ff4b1d84ac/image.png" alt="">
<em>줄의 너비를 바꿔줄 수 있는 방법은 해당 요소의 width를 늘리는 방법 밖에 없다. 그 외의 여백 공간을 메인축에 줄 방법이 없다.</em></p>
<br/>

<p>반대로 교차축의 여백은 충분하다. 그렇기 때문에 그 줄을 따라 <code>align-items</code>가 적용된다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/c4b8e047-1f4e-45f7-b245-f1d05be9b099/image.png" alt="">
<em>&#39;align-items&#39;를 설정하지 않은 &#39;flexbox&#39;이다.</em></p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/434304ba-733d-402a-8d81-84baf8a82931/image.png" alt="">
<em>&#39;align-items&#39;를 설정한 &#39;flexbox&#39;이다. 메인축/교차축을 따라 늘어선 줄의 &#39;간격&#39;은 변함이 없다.</em></p>
<br/>

<p>그래서 &#39;grid&#39;에서는 <code>justify-items</code>가 잘 적용된다. 각 셀 안에서 메인축 따라 움직일 여백만 확보해주면 아주 잘 적용이 된다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/46cfd8f9-80e2-4a36-95cd-b4ea9783992e/image.png" alt="">
<em>각각의 셀 안에 메인축을 따라 여백이 넉넉하기 때문에 &#39;justify-items: flex-end&#39;가 잘 적용된다.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[touch event를 활용한 Drag and Drop 시에 두 번 터치해야 하는 문제]]></title>
            <link>https://velog.io/@ferrari_roma/touch-event%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Drag-and-Drop-%EC%8B%9C%EC%97%90-%EB%91%90-%EB%B2%88-%ED%84%B0%EC%B9%98%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@ferrari_roma/touch-event%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Drag-and-Drop-%EC%8B%9C%EC%97%90-%EB%91%90-%EB%B2%88-%ED%84%B0%EC%B9%98%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Wed, 31 Jul 2024 10:57:30 GMT</pubDate>
            <description><![CDATA[<h1 id="문제">문제</h1>
<p>위 gif는 <a href="https://github.com/atlassian/react-beautiful-dnd/issues/2111">여기</a>에서 가져온 짤이다. touch event로 Drag and Drop(DnD)을 할 때 첫 번째 터치에서는 기대하는 움직임이 나오질 않고 반드시 두 번 터치해야지 원하는 움직임이 나온다.
나 역시 이런 문제를 만났다. 일단 PC 브라우저에서는 잘 작동을 했는데 모바일 사파리 브라우저에서 이런 문제가 발생했다.</p>
<h1 id="왜-이런-문제가-발생할까">왜 이런 문제가 발생할까?</h1>
<p>일단 <a href="https://www.w3.org/TR/touch-events/#the-touchmove-event">공식문서</a>에서 살펴볼 수 있듯이 리액트의 문제라기 보다는 <strong>DOM이 문제의 원인</strong>이다. </p>
<p>DnD를 할 때 <code>touchstart</code> 이벤트가 발생하고 나서 <code>touchmove</code>, 마지막으로 <code>touchend</code> 이벤트가 발생하는데 <code>touchmove</code>와 <code>touchend</code>의 <code>event.target</code>은 최초 <code>touchstart</code>의 <code>event.target</code>으로 고정된다. 해당 <code>event.target</code>이 상호작용 영역 밖으로 움직이는 경우에도, 심지어 <code>document</code>나 <code>window</code>에서 제거되는 경우에도 동일하게 적용된다.
다른 mouse event나 pointer event는 <code>event.target</code>가 항상 가장 최근에 상호작용한 요소로 최신화 되기 때문에 이런 문제가 발생하지 않았던 것이었다.</p>
<p>모달 안에서 DnD를 진행할 때 <code>createPortal</code>을 하는 것이 대표적인 예시이다. 리액트에서 dragging되는 요소를 portal을 통해 새로운 위치에 렌더링 하는 경우가 있다. 특정 요소를 dragging하게 되면 리렌더링 되면서 기존 요소가 DOM tree에서 제거되고 portal을 통해 새로운 위치에 다시 렌더링 된다.
이때 기존 요소에서 <code>touchstart</code> 이벤트가 발생하고 DOM tree에서 제거되기 때문에 이후에 발생하는 <code>touchmove</code>와 <code>touchend</code> 이벤트는 DOM tree를 따라 버블링이 일어나지 않는다.</p>
<h1 id="해결책">해결책</h1>
<p>내가 읽었던 여러 글에서는 대표적으로 두 가지 방법을 제시했다. 하나는 아래 예시 코드처럼 <code>touchstart</code> 콜백 안에서 해당 <code>event.target</code>에 <code>touchmove</code>와 <code>touchend</code>에 대한 이벤트 리스터를 등록하는 것이다. 아래는 <a href="https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed/34980275#34980275">스택 오버플로우</a>에 제시된 예시코드이다.</p>
<pre><code class="language-js">element.addEventListener(&quot;touchstart&quot;, (event) =&gt; {
    const onTouchMove = () =&gt; {
        // handle touchmove here
    }
    const onTouchEnd = () =&gt; {
        event.target.removeEventListener(&quot;touchmove&quot;, onTouchMove);
        event.target.removeEventListener(&quot;touchend&quot;, onTouchEnd);
        // handle touchend here
    }
    event.target.addEventListener(&quot;touchmove&quot;, onTouchMove);
    event.target.addEventListener(&quot;touchend&quot;, onTouchEnd);
    // handle touchstart here
});</code></pre>
<p>여러 문서에서 이러한 방법을 <code>event.target</code>에 직접적으로(directly) 등록하는 방법으로 지칭하고 있다.</p>
<p>다른 하나는 touch event 말고 이런 증상이 없는 pointer event를 사용하라는 것이다.</p>
<h1 id="react-beautiful-dnd에서-문제-해결하기">React Beautiful DnD에서 문제 해결하기</h1>
<p>나는 DnD를 React Beautiful DnD(RBD)을 사용해서 구현하였는데, RBD에서도 이 문제가 해결되지 않고 존재했다. RBD 깃허브 이슈를 찾아다니다 보니 많은 사람들이 이 문제를 공유하였고 대부분 첫 번째 방법으로 문제를 해결했다. </p>
<p>RBD는 DnD를 감지하는 sensor api를 커스텀 할 수 있다. 그래서 RBD 문서를 보면 어떤 사용자는 손의 제스쳐를 인식하도록 해서 DnD를 하는 사람도 있고 심지어는 뇌파를 이용해서 DnD를 구현한 사람도 있었다.<del>(진짜임)</del></p>
<p>그래서 기존의 sensor api 대신에 touchstart 이벤트 발생 시에 <code>event.target</code>에 이벤트를 직접 바인딩 해주도록 기존 코드를 커스텀 해서 해결하였다.</p>
<br>
<br>



<pre><code class="language-tsx">import {
  DragDropContext,
  useKeyboardSensor,
  useMouseSensor,
} from &#39;react-beautiful-dnd&#39;;
import useTouchSensor from &#39;./custom-sensors/use-touch-sensor&#39;;

...
return (
    &lt;DragDropContext
      enableDefaultSensors={false}
      sensors={[useMouseSensor, useKeyboardSensor, useTouchSensor]}
      ....
     &lt;/DragDropContext&gt;
);</code></pre>
<p>방법은 먼저 <code>DragDropContext</code>의 enableDefaultSensors를 false로 해서 기존 센서를 off로 하고 사용할 센서들을 sensors에 추가하면 된다. RBD에는 작성되어 있는 keyboard, mouse, touch 센서가 있긴 하지만 이걸 사용한다고 해서 문제가 해결되지 않았다. 12.0.0의 베타 10에서 11버전으로 수정될 때 타겟에 직접 이벤트를 추가하는 코드가 사라졌기 때문이다.</p>
<br>

<pre><code class="language-js">// v12.0.0-beta.10
function bindCapturingEvents(target: HTMLElement) {
  ...

  const unbindTarget = bindEvents(target, getTargetBindings(args), options);

  ...
}

// v12.0.0-beta.11
function bindCapturingEvents() {
  ...

  const unbindTarget = bindEvents(window, getHandleBindings(args), options);

  ...
}</code></pre>
<br>
<br>


<p>그래서 베타 10버전의 코드를 되살리는 방향으로 훅을 작성하고 sensor에 추가하면 된다. 아래는 기존 RBD의 use touch sensor api를 바탕으로 베타 10버전의 코드를 추가해 준 코드다. </p>
<pre><code class="language-tsx">import * as React from &#39;react&#39;;
import { useCallback, useMemo } from &#39;use-memo-one&#39;;
import type { Position } from &#39;css-box-model&#39;;
import {
  DraggableId,
  FluidDragActions,
  PreDragActions,
  SensorAPI,
} from &#39;react-beautiful-dnd&#39;;

type TouchWithForce = Touch &amp; {
  force: number;
};

type Idle = {
  type: &#39;IDLE&#39;;
};

type Pending = {
  type: &#39;PENDING&#39;;
  point: Position;
  actions: PreDragActions;
  longPressTimerId: NodeJS.Timeout;
};

type Dragging = {
  type: &#39;DRAGGING&#39;;
  actions: FluidDragActions;
  hasMoved: boolean;
};

type Phase = Idle | Pending | Dragging;

type PredicateFn&lt;T&gt; = (value: T) =&gt; boolean;

function findIndex&lt;T&gt;(list: T[], predicate: PredicateFn&lt;T&gt;): number {
  if (list.findIndex) {
    return list.findIndex(predicate);
  }

  // Using a for loop so that we can exit early
  for (let i = 0; i &lt; list.length; i++) {
    if (predicate(list[i])) {
      return i;
    }
  }
  return -1;
}

function find&lt;T&gt;(list: T[], predicate: PredicateFn&lt;T&gt;): T | undefined {
  if (list.find) {
    return list.find(predicate);
  }
  const index: number = findIndex(list, predicate);
  if (index !== -1) {
    return list[index];
  }
  return undefined;
}

const supportedPageVisibilityEventName: string = ((): string =&gt; {
  const base = &#39;visibilitychange&#39;;

  if (typeof document === &#39;undefined&#39;) {
    return base;
  }

  const candidates: string[] = [
    base,
    `ms${base}`,
    `webkit${base}`,
    `moz${base}`,
    `o${base}`,
  ];

  const supported: string | undefined = find(
    candidates,
    (eventName: string): boolean =&gt; `on${eventName}` in document,
  );

  return supported || base;
})();

const idle: Idle = { type: &#39;IDLE&#39; };

export const timeForLongPress = 120;
export const forcePressThreshold = 0.15;

type GetBindingArgs = {
  cancel: () =&gt; void;
  completed: () =&gt; void;
  getPhase: () =&gt; Phase;
};

function getWindowBindings({
  cancel,
  getPhase,
}: GetBindingArgs): EventBinding[] {
  return [
    {
      eventName: &#39;orientationchange&#39; as keyof HTMLElementEventMap,
      fn: cancel,
    },
    {
      eventName: &#39;resize&#39;,
      fn: cancel,
    },
    {
      eventName: &#39;contextmenu&#39;,
      fn: (event: Event) =&gt; {
        event.preventDefault();
      },
    },
    {
      eventName: &#39;keydown&#39;,
      fn: (event: KeyboardEvent) =&gt; {
        if (getPhase().type !== &#39;DRAGGING&#39;) {
          cancel();
          return;
        }
        if (event.key === &#39;Escape&#39;) {
          event.preventDefault();
        }
        cancel();
      },
    },
    {
      eventName: supportedPageVisibilityEventName as keyof HTMLElementEventMap,
      fn: cancel,
    },
  ];
}

function getHandleBindings({
  cancel,
  completed,
  getPhase,
}: GetBindingArgs): EventBinding[] {
  return [
    {
      eventName: &#39;touchmove&#39;,
      options: { capture: false },
      fn: (event: TouchEvent) =&gt; {
        const phase: Phase = getPhase();
        if (phase.type !== &#39;DRAGGING&#39;) {
          cancel();
          return;
        }

        phase.hasMoved = true;

        const { clientX, clientY } = event.touches[0];

        const point: Position = {
          x: clientX,
          y: clientY,
        };

        event.preventDefault();
        phase.actions.move(point);
      },
    },
    {
      eventName: &#39;touchend&#39;,
      fn: (event: TouchEvent) =&gt; {
        const phase: Phase = getPhase();
        if (phase.type !== &#39;DRAGGING&#39;) {
          cancel();
          return;
        }
        event.preventDefault();
        phase.actions.drop({ shouldBlockNextClick: true });
        completed();
      },
    },
    {
      eventName: &#39;touchcancel&#39;,
      fn: (event: TouchEvent) =&gt; {
        if (getPhase().type !== &#39;DRAGGING&#39;) {
          cancel();
          return;
        }
        event.preventDefault();
        cancel();
      },
    },
    {
      eventName: &#39;touchforcechange&#39; as keyof HTMLElementEventMap,
      fn: (event: TouchEvent) =&gt; {
        const phase: Phase = getPhase();

        if (phase.type === &#39;IDLE&#39;) {
          throw Error(&#39;invariant&#39;);
        }

        const touch: TouchWithForce = event.touches[0] as TouchWithForce;

        if (!touch) {
          return;
        }

        const isForcePress: boolean = touch.force &gt;= forcePressThreshold;

        if (!isForcePress) {
          return;
        }

        const shouldRespect: boolean = phase.actions.shouldRespectForcePress();

        if (phase.type === &#39;PENDING&#39;) {
          if (shouldRespect) {
            cancel();
          }
          return;
        }
        if (shouldRespect) {
          if (phase.hasMoved) {
            event.preventDefault();
            return;
          }
          cancel();
          return;
        }
        event.preventDefault();
      },
    },
    {
      eventName: supportedPageVisibilityEventName as keyof HTMLElementEventMap,
      fn: cancel,
    },
  ];
}
type EventOptions = {
  passive?: boolean;
  capture?: boolean;
  once?: boolean;
};

type NewType = (
  event: KeyboardEvent &amp; TouchEvent &amp; EventListenerOrEventListenerObject,
) =&gt; void;

type EventBinding = {
  eventName: keyof HTMLElementEventMap;
  fn: NewType;
  options?: EventOptions;
};

function getOptions(
  shared?: EventOptions,
  fromBinding?: EventOptions,
): EventOptions {
  return {
    ...shared,
    ...fromBinding,
  };
}

type UnbindFn = () =&gt; void;

function bindEvents(
  el: HTMLElement | Window,
  bindings: EventBinding[],
  sharedOptions?: EventOptions,
) {

  const unbindings: UnbindFn[] = bindings.map(
    (binding: EventBinding): UnbindFn =&gt; {
      const options = getOptions(sharedOptions, binding.options);

      el.addEventListener(
        binding.eventName,
        binding.fn as EventListenerOrEventListenerObject,
        options,
      );

      return function unbind() {
        el.removeEventListener(
          binding.eventName,
          binding.fn as EventListenerOrEventListenerObject,
          options,
        );
      };
    },
  );

  return function unbindAll() {
    unbindings.forEach((unbind: UnbindFn) =&gt; {
      unbind();
    });
  };
}

export default function useTouchSensor(api: SensorAPI) {
  const phaseRef = React.useRef&lt;Phase&gt;(idle);
  const unbindEventsRef = React.useRef&lt;() =&gt; void&gt;(() =&gt; null);

  const getPhase = useCallback(function getPhase(): Phase {
    return phaseRef.current;
  }, []);

  const setPhase = useCallback(function setPhase(phase: Phase) {
    phaseRef.current = phase;
  }, []);

  const startCaptureBinding = useMemo(
    () =&gt; ({
      eventName: &#39;touchstart&#39; as keyof HTMLElementEventMap,
      fn: function onTouchStart(event: TouchEvent) {
        if (event.defaultPrevented) {
          return;
        }

        const draggableId: DraggableId | null =
          api.findClosestDraggableId(event);

        if (!draggableId) {
          return;
        }

        const actions: PreDragActions | null = api.tryGetLock(
          draggableId,
          // eslint-disable-next-line no-use-before-define
          stop,
          { sourceEvent: event },
        );

        if (!actions) {
          return;
        }

        const touch: Touch = event.touches[0];
        const { clientX, clientY } = touch;
        const point: Position = {
          x: clientX,
          y: clientY,
        };
        const dragHandleId = api.findClosestDraggableId(event);
        if (!dragHandleId) {
          throw Error(&#39;Touch sensor unable to find drag dragHandleId&#39;);
        }
        const handle: HTMLElement | null = document.querySelector(
          `[data-rbd-drag-handle-draggable-id=&#39;${dragHandleId}&#39;]`,
        );
        if (!handle) {
          throw Error(&#39;Touch sensor unable to find drag handle&#39;);
        }

        unbindEventsRef.current();
        // eslint-disable-next-line no-use-before-define
        startPendingDrag(actions, point, handle);
      },
    }),
    [api],
  );

  const listenForCapture = useCallback(
    function listenForCapture() {
      const options = {
        capture: true,
        passive: false,
      };

      unbindEventsRef.current = bindEvents(
        window,
        [startCaptureBinding],
        options,
      );
    },
    [startCaptureBinding],
  );

  const stop = useCallback(() =&gt; {
    const { current } = phaseRef;
    if (current.type === &#39;IDLE&#39;) {
      return;
    }

    if (current.type === &#39;PENDING&#39;) {
      clearTimeout(current.longPressTimerId);
    }

    setPhase(idle);
    unbindEventsRef.current();

    listenForCapture();
  }, [listenForCapture, setPhase]);

  const cancel = useCallback(() =&gt; {
    const phase: Phase = phaseRef.current;
    stop();
    if (phase.type === &#39;DRAGGING&#39;) {
      phase.actions.cancel({ shouldBlockNextClick: true });
    }
    if (phase.type === &#39;PENDING&#39;) {
      phase.actions.abort();
    }
  }, [stop]);

  const bindCapturingEvents = useCallback(
    function bindCapturingEvents(target: HTMLElement) {
      const options = { capture: true, passive: false };
      const args: GetBindingArgs = {
        cancel,
        completed: stop,
        getPhase,
      };

      const unbindTarget = bindEvents(target, getHandleBindings(args), options);
      const unbindTargetWindow = bindEvents(
        window,
        getHandleBindings(args),
        options,
      );
      const unbindWindow = bindEvents(window, getWindowBindings(args), options);

      unbindEventsRef.current = function unbindAll() {
        unbindTarget();
        unbindTargetWindow();
        unbindWindow();
      };
    },
    [cancel, getPhase, stop],
  );

  const startDragging = useCallback(
    function startDragging() {
      const phase: Phase = getPhase();
      if (phase.type !== &#39;PENDING&#39;) {
        throw Error(`Cannot start dragging from phase ${phase.type}`);
      }

      const actions: FluidDragActions = phase.actions.fluidLift(phase.point);

      setPhase({
        type: &#39;DRAGGING&#39;,
        actions,
        hasMoved: false,
      });
    },
    [getPhase, setPhase],
  );

  const startPendingDrag = useCallback(
    function startPendingDrag(
      actions: PreDragActions,
      point: Position,
      target: HTMLElement,
    ) {
      if (getPhase().type !== &#39;IDLE&#39;) {
        throw Error(&#39;Expected to move from IDLE to PENDING drag&#39;);
      }

      const longPressTimerId = setTimeout(startDragging, timeForLongPress);

      setPhase({
        type: &#39;PENDING&#39;,
        point,
        actions,
        longPressTimerId,
      });

      bindCapturingEvents(target);
    },
    [bindCapturingEvents, getPhase, setPhase, startDragging],
  );

  React.useLayoutEffect(
    function mount() {
      listenForCapture();

      return function unmount() {
        unbindEventsRef.current();
        const phase: Phase = getPhase();
        if (phase.type === &#39;PENDING&#39;) {
          clearTimeout(phase.longPressTimerId);
          setPhase(idle);
        }
      };
    },
    [getPhase, listenForCapture, setPhase],
  );

  React.useLayoutEffect(function webkitHack() {
    const unbind = bindEvents(window, [
      {
        eventName: &#39;touchmove&#39;,
        fn: () =&gt; {
          return;
        },
        options: { capture: false, passive: false },
      },
    ]);

    return unbind;
  }, []);
}</code></pre>
<h1 id="마치며">마치며</h1>
<p>몰랐지만 DnD의 대표적인 이슈였다. <a href="https://github.com/facebook/react/issues/13113">RBD</a> 저자도 질문했던 이슈였고 Dan Abramov도 해당 이슈에 답변을 달았었다. 처음에는 이것 때문에 아예 &#39;모바일 버전을 포기해야 하나&#39; 생각도 했지만 대표적인 이슈이다 보니 이슈란에 선배 개발자들의 원인에 대한 분석과 여러 좋은 해결책들이 많았다. 그래서 처음에는 &#39;왜 touchevent만 이렇게 되고 mouseevent는 멀쩡하게 작동하는 걸까?&#39;, &#39;왜 portal을 탈 때 요소가 제거되는거지?&#39;처럼 질문이 꼬리의 꼬리를 물었었지만, 덕분에 지금은 대부분이 말끔히 해소되었다. 물론, 아이폰 safari 브라우저에서도 아주 잘 작동된다.</p>
<br>
<br>




<p><em>참고문서</em>
<a href="https://github.com/atlassian/react-beautiful-dnd/issues/2456">On mobile, re-render the dragged element while it is being dragged. Need to drag twice to drag.</a></p>
<p><a href="https://github.com/atlassian/react-beautiful-dnd/issues/2111">Not able to drag and drop on mobile using React Portal</a></p>
<p><a href="https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed/34980275#34980275">Touch Move event don&#39;t fire after Touch Start target is removed</a></p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Touch/target">Touch: target property</a></p>
<p><a href="https://github.com/atlassian/react-beautiful-dnd/issues/2111">Not able to drag and drop on mobile using React Portal</a></p>
<p><a href="https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d">Drag-Drop+Reparenting-Solution-React</a></p>
<p><a href="https://github.com/facebook/react/issues/13113">Moving to React Portal after touchstart swallows future touch events</a></p>
<p><a href="https://www.w3.org/TR/touch-events/#the-touchmove-event">the touchmove event</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Storybook에 React Navigation 환경 셋업해주기]]></title>
            <link>https://velog.io/@ferrari_roma/Storybook%EC%97%90-React-Navigation-%ED%99%98%EA%B2%BD-%EC%85%8B%EC%97%85%ED%95%B4%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@ferrari_roma/Storybook%EC%97%90-React-Navigation-%ED%99%98%EA%B2%BD-%EC%85%8B%EC%97%85%ED%95%B4%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Thu, 18 Jul 2024 17:42:54 GMT</pubDate>
            <description><![CDATA[<p>React Navigation을 도입해서 각 Screen 컴포넌트를 이어주고, type도 지정을 해주니까 Storybook 빌드에서 에러가 발생하면서 실행이 되지 않았다. 그래서 찾아보니, Navigation에 대한 환경을 설정해주지 않아서 그런 것이었다.</p>
<p>그런데 스토리북 컴포넌트 안에서 useNavigation과 useRoute 훅을 사용해서 해당 컴포넌트가 필요로 하는 navgation, route를 전달해 주었는데도 이런 에러가 발생한 것이었다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/ed849c4c-1140-4044-9c71-1661482c333c/image.png" alt=""></p>
<p>기본적으로 Navigation은 부모 Screen 컴포넌트로부터 필요한 props를 받기 때문에 이렇게 useNavigation으로는 해결되지 않는다.</p>
<p>기존 Storybook의 <code>preview.tsx</code>는 다음과 같았다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;
import type { Preview } from &#39;@storybook/react&#39;;
import { ThemeContextProvider } from &#39;../src/context/theme&#39;;
import { INITIAL_VIEWPORTS } from &#39;@storybook/addon-viewport&#39;;
import { QueryClientProvider, QueryCache, QueryClient } from &#39;react-query&#39;;
import { ReactQueryDevtools } from &#39;react-query/devtools&#39;;

const queryClient = new QueryClient();

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: &#39;^on[A-Z].*&#39; },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
    viewport: {
      viewports: INITIAL_VIEWPORTS,
      defaultViewport: &#39;iphone6&#39;,
    },
  },
  decorators: [
    (Story, context) =&gt; {
      return (
        &lt;QueryClientProvider client={queryClient}&gt;
          &lt;ThemeContextProvider&gt;
            &lt;style&gt;
              {`html, body, #storybook-root {
              height: 100%;`}
            &lt;/style&gt;
            &lt;Story {...context} /&gt;
            &lt;ReactQueryDevtools /&gt;
          &lt;/ThemeContextProvider&gt;
        &lt;/QueryClientProvider&gt;
      );
    },
  ],
};

export default preview;
</code></pre>
<p>데코레이터 프로퍼티를 보면 어떤 스토리북 문서든지 동일한 처리를 해주는 것으로 되어있다. 여기서 네비게이션 컨텍스트 설정을 해주기로 했다. 일단 환경이 될 부모 Screen 컴포넌트를 먼저 만들어 주었다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;
import { NavigationContainer } from &#39;@react-navigation/native&#39;;
import { createStackNavigator } from &#39;@react-navigation/stack&#39;;

const StoryBookStack = createStackNavigator();

export const NavigationDecorator = ({
  Story,
  context,
}: {
  Story: (param: any) =&gt; JSX.Element;
  context: any;
}) =&gt; {
  const Renderer = () =&gt; &lt;Story {...context} /&gt;;
  return (
    &lt;NavigationContainer independent={true}&gt;
      &lt;StoryBookStack.Navigator&gt;
        &lt;StoryBookStack.Screen
          component={Renderer}
          name=&#39;MyStorybookScreen&#39;
          options={{ header: () =&gt; null }}
        /&gt;
      &lt;/StoryBookStack.Navigator&gt;
    &lt;/NavigationContainer&gt;
  );
};
</code></pre>
<p>이렇게 만든 네비게이션 데코레이터 컴포넌트로 <code>preview.tsx</code>에 적용해 보았다. 물론 모든 스토리북 문서에 이 데코레이터 컴포넌트를 적용할 수 있겠지만, atom, molecule과 같이 네비게이션이 적용되지 않는 하위 레이어에서는 굳이 네비게이션 데코레이터를 적용할 필요가 없다는 생각이 들어서 분기처리를 하기로 했다.</p>
<p>console을 찍어보다 보니 context에서 문서의 경로를 얻을 수 있었다. 그래서 screens 파일에 있는 문서들에 한해서만 네비게이션 데코레이터 컴포넌트를 적용하였다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;
import { type Preview } from &#39;@storybook/react&#39;;
import { ThemeContextProvider } from &#39;../src/context/theme&#39;;
import { INITIAL_VIEWPORTS } from &#39;@storybook/addon-viewport&#39;;
import { QueryClientProvider, QueryClient } from &#39;react-query&#39;;
import { NavigationDecorator } from &#39;./NavigationDecorator&#39;;

const queryClient = new QueryClient();

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: &#39;^on[A-Z].*&#39; },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
    viewport: {
      viewports: INITIAL_VIEWPORTS,
      defaultViewport: &#39;iphone6&#39;,
    },
  },
  decorators: [
    (Story, context) =&gt; {
      const fileName = context.parameters.fileName;
      const isScreen = fileName.includes(&#39;screens&#39;);

      if (isScreen) {
        return (
          &lt;QueryClientProvider client={queryClient}&gt;
            &lt;ThemeContextProvider&gt;
              &lt;style&gt;
                {`html, body, #storybook-root {
            height: 100%; #storybook-root &gt; div {
                height: 100%;
              }}`}
              &lt;/style&gt;
              &lt;NavigationDecorator story={Story} /&gt;
            &lt;/ThemeContextProvider&gt;
          &lt;/QueryClientProvider&gt;
        );
      }
      return (
        &lt;QueryClientProvider client={queryClient}&gt;
          &lt;ThemeContextProvider&gt;
            &lt;style&gt;
              {`html, body, #storybook-root {
          height: 100%; }`}
            &lt;/style&gt;
            &lt;Story {...context} /&gt;
          &lt;/ThemeContextProvider&gt;
        &lt;/QueryClientProvider&gt;
      );
    },
  ],
};

export default preview;
</code></pre>
<p>프로젝트가 좀 커지다 보니까, 각각 atom, molecule에 대한 스토리북 문서가 작업에 거의 필수였다. 새로운 컴포넌트 작업을 해야 하는데, 잘 돌아가던 스토리북 문서가 먹통이 되는 바람에 어떤 atom과 molecule을 어떤 attributes를 설정해서 사용해야 하는지 확인할 방법이 직접 하나씩 해보는 것 말고는 없었다. 그래서 시작한 디버깅이었는데 잘 처리되어서 다행이다.</p>
<p>_&lt;참고&gt;_
<a href="https://davidl.fr/blog/react-navigation-object-storybook">React Navigation 5 and Storybook</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Navigation의 useNavigation타입하기]]></title>
            <link>https://velog.io/@ferrari_roma/React-Navigation-useNavigation-%ED%83%80%EC%9E%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ferrari_roma/React-Navigation-useNavigation-%ED%83%80%EC%9E%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 01 Feb 2024 07:19:24 GMT</pubDate>
            <description><![CDATA[<p><em>24.6.23자 수정</em></p>
<p>예시로 작성할 라우터 구조는 다음과 같다.</p>
<ul>
<li>RootRoute<ul>
<li>MainRoute<ul>
<li>Home</li>
</ul>
</li>
<li>ChatRoute</li>
<li>AuthRoute<ul>
<li>Email</li>
<li>Phone</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>예시 코드는 &#39;Phone&#39;에서 &#39;Home&#39;으로 간다고 가정한다.</p>
<pre><code class="language-tsx">/* MainRoute.tsx */
export type MainRouteParamList = {
  Home: undefined;
};

const Stack = createStackNavigator&lt;MainRouteParamList&gt;();

const MainRoute = () =&gt; {
  return (
    &lt;Stack.Navigator&gt;
      &lt;Stack.Screen name={&#39;Home&#39;} component={HomeScreen} /&gt;
    &lt;/Stack.Navigator&gt;
  );
}

/* AuthRoute.tsx */
export type AuthRouteParamList = {
  Email: undefined;
  Phone: undefined;
};

const Stack = createStackNavigator&lt;AuthRouteParamList&gt;();

const AuthRoute = () =&gt; {
  return (
    &lt;Stack.Navigator&gt;
      &lt;Stack.Screen name={&#39;Email&#39;} component={EmailAuthScreen} /&gt;
      &lt;Stack.Screen name={&#39;Phone&#39;} component={PhoneAuthScreen} /&gt;
    &lt;/Stack.Navigator&gt;
  );
}

/* RootRoute.tsx */
export type RootRouteParamList = {
  MainRoute: NavigatorScreenParams&lt;MainRouteParamList&gt;;
  ChatRoute: NavigatorScreenParams&lt;ChatRouteParamList&gt;;
  AuthRoute: NavigatorScreenParams&lt;AuthRouteParamList&gt;;
};

const Stack = createStackNavigator&lt;RootRouteParamList&gt;();

const RootRoute = () =&gt; {
  return (
    &lt;Stack.Navigator&gt;
      &lt;Stack.Screen name={&#39;MainRoute&#39;} component={MainRoute} /&gt;
      &lt;Stack.Screen name={&#39;ChatRoute&#39;} component={ChatRoute} /&gt;
      &lt;Stack.Screen name={&#39;AuthRoute&#39;} component={AuthRoute} /&gt;
    &lt;/Stack.Navigator&gt;
  );</code></pre>
<p>예시의 RootRoute는 하위에 세 가지 Route가 중첩되어 있는 구조이다. 하위에 중첩된 Route가 있다면 <code>NavigatorScreenParams</code>을 사용해서 중첩 Route를 타입해준다. 
각각의 라우터에서는 하위 스크린과 라우터에 대한 타입을 <code>type</code> 형식으로 해야 한다. <code>interface</code>는 안 된다. 위 예시에서의 <code>RootRouteParamList</code>, <code>AuthRouteParamList</code> 등이다. 이 타입을 활용해서 <code>createStackNavigator()</code> 메소드를 호출한다.</p>
<pre><code class="language-tsx">/* PhoneAuthScreen.tsx */
interface PhoneAuthProps
  extends StackScreenProps&lt;AuthRouteParamList, &#39;Phone&#39;&gt; {}

const PhoneAuthScreen = ({ route }: PhoneAuthProps) =&gt; {
  const { navigation, route } =
    useNavigation&lt;StackNavigationProps&lt;MainRouteParamList&gt;&gt;();
  ...

  navigation.navigate(&#39;Home&#39;);
}</code></pre>
<p>다뤄볼 상황은 <code>Phone</code>이 있는 <code>AuthRoute</code>에서 <code>Home</code>이 있는 <code>MainRoute</code>로 가는, 즉 형제 Router로 가는 상황이다. <code>getParent()</code> 메소드를 호출해서 부모 navigation에 접근해서 갈수도 있지만, useNavigation을 사용해서 갈수도 있다.</p>
<p>이 글을 수정하기 전에는 <code>useNavigation</code>에 <code>CompositeNavigationProps</code>를 사용하는 것으로 예시코드를 작성했었는데, 공식문서를 읽고 또 읽다보니 굳이 <code>CompositeNavigationProps</code>를 굳이 사용하지 않아도 된다는 것을 알게 되었다. 지금처럼 필요한 Route를 <code>useNavigation</code> 제네릭으로 할당하면 된다.</p>
<p>그럼 <code>CompositeNavigationProps</code>은 언제 사용할까? 대표적으로 Tab navigation 안에 중첩된 Stack navigation에서 Tab navigation의 메소드와 Stack navigation의 메소드를 모두 사용하고 싶을 때 사용한다. 즉, 위 예시처럼 단순히 Route 간 이동을 원하거나 특정 Route로 <code>reset()</code> 메소드를 사용하고 싶을 때는 위 예시처럼 <code>CompositeNavigationProps</code>를 사용하지 않아도 잘 작동한다.</p>
<p><a href="https://reactnavigation.org/docs/typescript">React Navigation Type checking with TypeScript</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[10개월 간 BDD를 하면서 느낀 점]]></title>
            <link>https://velog.io/@ferrari_roma/10%EA%B0%9C%EC%9B%94-%EA%B0%84-BDD%EB%A5%BC-%ED%95%98%EB%A9%B4%EC%84%9C-%EB%8A%90%EB%82%80-%EC%A0%90</link>
            <guid>https://velog.io/@ferrari_roma/10%EA%B0%9C%EC%9B%94-%EA%B0%84-BDD%EB%A5%BC-%ED%95%98%EB%A9%B4%EC%84%9C-%EB%8A%90%EB%82%80-%EC%A0%90</guid>
            <pubDate>Thu, 04 Jan 2024 11:11:09 GMT</pubDate>
            <description><![CDATA[<p>개인 프로젝트를 하면서 TDD를 하고 있다. 간단하지만 풀스택 프로젝트여서 &#39;백엔드를 만들 때 적용했던 TDD를 프론트엔드에도 적용해 보자!&#39;하는 단순한 생각으로 시도했다. 동료 분과 Nest에서 작성했던 jest 코드를 바탕으로 하면 되겠지&#39;하는 생각으로 덤벼들었지만, 프론트엔드는 프론트엔드 만의 특징들이 있었고 이런저런 레퍼런스 그리고 컨퍼런스 영상들을 찾아보고서 <code>BDD(Behavior Driven Development)</code>를 기준으로 작성하기로 했다. 주로 Describe-Content-It 템플릿으로 작성을 했고 중점은 사용자가 화면에서 어떤 행동을 할 지에 맞춰서 테스트 코드를 작성하자는 것이었다. 이 글은 이런 규칙 속에서 약 10개월 간 BDD를 하기 위해 무작정 맨 땅에 헤딩을 한 중간 기록을 남겨보고자 한다. 방법론적인 글이 아니라, 하면서 느낀 점을 기록하는 글이기 때문에 예시 코드는 거의 없을 거 같다.</p>
<h1 id="최근-하고-있는-bdd-개발-사이클">최근 하고 있는 BDD 개발 사이클</h1>
<p>기록을 남겨야 겠다고 생각했던 결정적인 이유는 최근 들어서는 꽤 이런 개발 방법도 가치가 있다는 것을 느끼고 있기 때문이다. 처음에는 그냥 화면에 나오는 하나하나의 요소들을 가지고 테스트를 작성했다. 예를 들어 라벨과 인풋 태그가 있다면 라벨과 인풋 태그의 &#39;존재&#39;를 검증하는 테스트 코드를 작성해 보았다. 하지만 가면 갈수록 이런 테스트가 무슨 의미가 있을까에 대한 회의가 들었다. 그에 대한 고민을 하다 보니, 유저가 실질적으로 컴포넌트와 상호작용하는 부분을 중심으로 테스트 코드를 작성해 보기 시작했다. </p>
<p>이런 고민 끝에 최근에 내가 개발하는 사이클은 다음과 같다. 실제 사례를 들어보면, 최근 input태그에 입력하고 엔터를 누르면 카테고리 태그가 추가되는 등의 태그와 관련된 UI를 만들었다. 이때 테스트 코드를 작성할 때 대략적으로 사용자가 이 UI에서 어떤 상호작용을 할지 고민을 해보았다.</p>
<blockquote>
<ol>
<li>태그 input에 유저가 새로운 태그를 입력</li>
<li>태그를 입력하고 enter를 눌러 태그 추가</li>
<li>태그를 클릭하여 태그 삭제</li>
<li>예외 사항을 고민해 보니, 이미 존재하는 태그와 동일한 값의 태그는 추가되지 않아야 하고,</li>
<li>빈 input 값이면 태그는 추가되지 않아야 한다.</li>
</ol>
</blockquote>
<p>위 다섯 가지는 실제로 테스트 코드를 작성하기 전에 간략하게 적어본 수도 코드이다. 이 값은 절대적이지는 않고 작성 과정에서 몇 가지 사항들이 수정, 추가될 수 있다. 그리고 이에 따라 테스트 코드를 하나 작성한다. input에 유저의 입력을 테스트 한다. <code>getByRole</code>로 input태그를 테스트 환경 스크린에서 가지고 와서 <code>userEvent</code>나 <code>fireEvent</code>를 사용하여 input에 값이 잘 입력되는지 확인한다. 하지만 이 테스트는 반드시 실패한다. 왜냐하면 input 태그를 아직 만들어 두지 않았기 때문이다. 그럼 input 태그를 만들고 리액트 환경이기 때문에 input을 위한 상태와 설정자 함수를 설정한다. 이렇게 하면 1번 상황은 테스트가 통과될 것이다. 이후 코드를 실행시켜 UI에서 실제로 내가 예상한 동작이 잘 되는지 확인한다. 2번도 똑같이 테스트 코드를 작성하고, 실패하고, UI를 그에 맞게 작성하고, 테스트가 통과되면 실제 UI를 확인한다. 이 방법으로 5번까지 코드를 쭉 작성했다.</p>
<h1 id="내가-느낀-효용">내가 느낀 효용</h1>
<p>이렇게 개발을 했을 때 글로만 봤던 몇 가지 효용을 느낄 수 있었다. 가장 먼저 몰입도가 올라갔다. 예전에 글에서 TDD를 하면 능률이 오른다는 말을 보고 이해를 잘 못했다. 그런데 실제로 몰입도가 올라가면서 능률이 올랐는데, 포인트는 이것이다. BDD를 하다 보니 상기한 내용처럼 <code>정형화된 개발 순서</code>가 생겼고, 그에 따라 개발 과정이 매우 직관적이어진다. 이전에는 UI를 개발했다가 유저와 상호작용하는 핸들러를 만들고, CSS작업 좀 하다가 핸들러에 예외 처리하다가 안되면 다시 돌아가서 만져보고 뭔가 코드가 지저분 한 것 같다, 아니면 재사용하기 어려워진 것 같다 할 땐 UI와 로직을 분리하는 등 그냥 머리에 떠오르는 대로 작업을 한 것 같다. 이것도 나름의 장점이 있었을 태지만, 개인적으로 정형화된 개발 순서에 따라 하나씩 해결해 간다는 느낌이 훨씬 몰입이 되었다.</p>
<p>다음으로는 하나의 문서가 작성되는 것이다. 하나의 <code>describe</code> 블록은 해당 스크린이고 <code>Context</code>는 상황을 나눈다. 그리고 <code>it</code>은 각 상황에서 유저가 겪을 수 있는 다양한 상호작용을 나타낸다. 즉, 해당 컴포넌트 안에서 유저가 마주칠 수 있는 여러 상황에 따라 테스트 코드를 작성하다 보니 하나의 문서가 완성된다. 내가 빠뜨린 예외 사항은 어떤 것이 있고, 이 부분은 조금 더 보완해야 한다는 것이 한 눈에 들어온다. 심지어는 동료 분이 작성한 코드가 이해가 안 될 때는 테스트 코드를 먼저 읽기도 했다. 읽다 보면 어떤 상황에 유저에게 어떤 행동을 기대하는 코드인지, 유저에게 어떤 것을 제공하기 위한 코드인지 파악하기 쉬워졌다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/2a235192-848e-4530-a6b7-bbd6861b561c/image.png" alt=""></p>
<p>또 유저 입장에서 어떤 상황을 겪을지를 생각하다 보니, 코드를 작성할 때 유저 관점에서 많은 생각을 하게 되었다. &#39;최초 UI를 설계할 때는 몰랐지만, 특정 부분은 유저 입장에서는 헷갈릴 것 같다&#39;, &#39;이 부분은 유저 입장에서는 있으나 마나 한 버튼인 것 같은데 왜 존재할까?&#39;와 같이 UI대로 그저 화면에 그리는 것이 아니라 이 UI에서 유저가 어떤 상호작용을 할지 계속 생각하게 되면서, 더 목적이 분명한 코드를 작성할 수 있었다.</p>
<p>이런 매력에 빠져서 최근에 BDD를 통해 더 몰입도가 높은, 그리고 하나하나의 사이클을 달성할 때 마다 오는 즐거움과 성취감을 느끼며 개발을 하고 있다. 이런 즐거움을 느낀지는 오래 되지 않았지만 분명 새로운 경험이었고, 이 글을 적게 된 가장 큰 이유이다.</p>
<h1 id="여기까지-오면서-겪은-불편함-어려움">여기까지 오면서 겪은 불편함, 어려움</h1>
<p>뭐든지 말은 쉽다. 하지만 여기에 더 많은 중간 과정들이 추가된다. 반복되는 테스트 코드들은 <code>beforeAll</code>이나 <code>beforeEach</code>에 함수로 묶어주어 테스트 코드를 좀 더 쾌적하게 만들고, 생각대로 UI가 작동하지 않는 부분은 다시금 수정하는 과정을 거친다. 그리고 모킹을 해야 하는 부분은 모킹을 하지 않게 설계를 수정할 수 있는지, 설계를 수정하는 것이 실제 코드에서 불합리함을 유발한다면 모킹을 어느 정도까지 해야 하는지, 외부 패키지를 사용한다면 그 부분은 어떻게 처리를 할 것인지, 어떻게 테스트를 해야 하는지 모르는 부분은 React Testing Library Documents들을 보면서 이런 저런 시도를 하는데 많은 시간을 써야 한다. 즉, 나는 빠르게 UI를 구현하고 싶은데 BDD를 하면 그렇게 할 수 없다. 단순히 기능을 완성하는 것도 테스트 코드 3,4줄은 추가적으로 만들기 마련이다. 이런 부분 때문에, 재미를 느끼기 전까지는 테스트 코드를 작성하면서 &#39;배보다 배꼽&#39;인 것 같은 생각이 훨씬 더 자주 들었다. 그러면서 회의감에 사로 잡힐 때도 있고, 굳이 테스트를 작성해야 하는지에 대한 의문도 자주 생겨, 회의에서 자주 테스트에 대한 고충을 토로하기도 했다.
특히나 익숙해 지면 이런 부분이 덜하지만 처음에는 아주 간단한 테스트 코드 하나를 작성하는데도 상당한 시간과 노력이 든다. 하나부터 열까지 찾아보면서 &#39;이 단순한 테스트가 도대체 왜 테스트가 통과가 되지 않는 것이지?&#39;, &#39;각각의 메소드의 차이는 뭐지?&#39;하는 막막함에, 재미를 느끼기 전에 훨씬 먼저 TDD의 단점을 온 몸으로 느끼게 될 것이다. 나 또한 그랬다.</p>
<h1 id="모두에게-권하고-싶지는-않다">모두에게 권하고 싶지는 않다.</h1>
<p>그래서 각자의 상황과 처지에 맞게 시도해 보라고 권하고 싶다. 나야 개인 프로젝트이고, 동료 개발자와 함께 서로 해보고 싶은 것을 마음 껏 해보는 프로젝트라서 아무것도 모르는데 시도해 본 것이다. 하지만 한 번도 테스트를 해보지 않은 분들이라면 실패와 삽질을 너그러이 받아들일 충분한 마음의 여유가 있는 분들에게 권하고 싶다. 앞서 말했듯이 경험이 없기 때문에 별 것도 아닌 것 같은 부분에서 어마어마한 삽질을 하는 경우도 허다하다. 그렇다고 겁 먹을 필요는 없다. 나도 호기심에 이끌려서 그냥 했다. 그래서 그냥 여러 번 부딪히고 찾아보고, 동료 분과 이야기를 나누며 큰 틀에서의 컨벤션을 정하고, 그 안에서 일정한 시행착오를 겪고 나서야 어느 정도의 효용을 느꼈다. 유데미와 같은 강의를 이용한다면 이런 시행착오의 시간을 훨씬 줄일 수 있을 것이지만, 이것 역시 개인의 취향일 테니 잘 고민해 보길 바란다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React-Beautiful-DnD에서 offset이 발생할 때]]></title>
            <link>https://velog.io/@ferrari_roma/React-Beautiful-DnD%EC%97%90%EC%84%9C-offset%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%A0-%EB%95%8C</link>
            <guid>https://velog.io/@ferrari_roma/React-Beautiful-DnD%EC%97%90%EC%84%9C-offset%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%A0-%EB%95%8C</guid>
            <pubDate>Sun, 22 Oct 2023 14:48:41 GMT</pubDate>
            <description><![CDATA[<p>React-Beautiful-DnD(RBD)를 사용하는 과정에서 위 화면과 같은 버그가 발생했다. 상황은 다음 예시 코드와 같다.</p>
<br/>

<pre><code class="language-jsx">&lt;div&gt;
    &lt;div style=&quot;height: 100px;&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;transform: translateY(0);&quot;&gt;
        &lt;!-- RBD의 Draggable이 여기 위치했을 때 문제가 발생! --&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre>
<br/>

<p>부모 요소에 <code>transform</code> 요소가 추가되어 있다면, drag 발생 시 draggable 요소에 offset이 발생한다.</p>
<p>시간을 들여서 해결을 하려고 했지만 마땅한 해결책을 찾을 수 없던 무렵 RBD의 issue 탭에서 같은 문제를 겪은 동지들을 발견하였다.</p>
<p>먼저 왜 이런 문제가 발생했는지 먼저 살펴보도록 하자.</p>
<br/>

<h1 id="positionfixed">position:fixed</h1>
<blockquote>
<p>We leave elements in place when dragging. We apply <code>position: fixed</code> on elements when we are moving them around.</p>
</blockquote>
<br/>

<p>이유는 draggable 요소를 drag할 때, 해당 요소에 <code>position: fixed</code>가 적용되기 때문이었다. 그래서 부모 요소의 <code>transform</code> 요소에 영향을 받아 offset이 발생한 것이다.</p>
<p>이 문제를 어떻게 해결할까?</p>
<br/>

<h1 id="drag할-때만-잠깐-다른-곳으로-가자">Drag할 때만 잠깐 다른 곳으로 가자!</h1>
<p>가장 중요한 컨셉은 Draggable 요소가 Drag상태 일 때만 <code>transform</code> 요소를 가지고 있는 부모에서 다른 부모로 갔다 오는 것이다.</p>
<p>코드와 함께 하나하나씩 살펴보자.</p>
<br/>

<h2 id="또-다른-div를-만들자">또 다른 div를 만들자</h2>
<p>우선, draggable 요소가 drag될 때 도망갈 또 다른 <code>div</code>를 <code>index.html</code>에 만든다. 이때 꼭 다음과 같은 style 값을 주어야 한다.</p>
<br/>

<pre><code class="language-html">&lt;div id=&quot;draggable&quot; style=&quot;position: fixed; pointer-events: none; width: 100%; height: 100%; top: 0;&quot;&gt;&lt;/div&gt;</code></pre>
<br/>

<p>top이 반드시 있어야 한다. 이걸 넣지 않은 코드가 원본 코드인데, 그 부분 때문에 <code>root div</code> 아래에 id가 <code>draggable</code>인 div가 있어서 매우 불편함을 느끼게 된다.</p>
<p>Draggable 요소가 drag 상태일 때 여기 위치하고, drag 상태가 아닐 때 원래의 위치로 돌아갈 것이다.</p>
<br/>

<h2 id="분기-처리를-해주자">분기 처리를 해주자</h2>
<p>drag가 될 때 새로 만들어 둔 <code>div</code>에 위치할 수 있도록 조건문을 작성해 주자. 앞서 살펴본 것처럼 draggable 요소가 drag상태 일 때 해당 요소에 <code>position:fixed</code>가 할당된다는 것을 이용해서 분기를 한다.</p>
<br/>

<pre><code class="language-js">// 새로 만든 div를 변수에 할당
const dragEl = document.getElementById(&#39;draggable&#39;);

const optionalPortal = (style, element) =&gt; {
  // drag상태 일 때 position이 fixed로 할당되는 것을 활용해서 분기처리
  if (style.position === &#39;fixed&#39;) {
    // drag상태 일 때는 createPortal로 새로 만든 div로 옮겨준다.
    return createPortal(element, dragEl);
  }
  return element;
};</code></pre>
<br/>

<pre><code class="language-jsx">// jsx에서 optionalPortal을 통해 Draggable 요소를 렌더링
&lt;Draggable draggableId={id} index={idx} key={id}&gt;
  {(provided) =&gt;
    optionalPortal(
      provided.draggableProps.style,
      (
        &lt;DraggableContainer
          {...provided.draggableProps}
          ref={provided.innerRef}
          &gt;
          ...
        &lt;/DraggableContainer&gt;
      )
    )
  }
&lt;/Draggable&gt;</code></pre>
<br/>

<p>코드에도 나와 있지만 <code>createPortal</code>을 사용해서 새로 만든 <code>div</code>로 옮겨준다. 즉, 새로운 <code>div</code>에 drag되고 있는 모달창이 되는 것이다.</p>
<br/>

<h1 id="결론">결론</h1>
<p>github issue를 뒤적이다 만난 사막의 오아시스. 그런 코멘트들을 적용해 보고, 더 나은 방법으로 조금 커스텀을 해서 최종적으로 커스텀 훅을 만들어서 사용하고 있다.</p>
<br/>

<pre><code class="language-ts">// typescript version
const useDraggableInPortal = () =&gt; {
  const element = useRef&lt;HTMLDivElement&gt;(
    document.getElementById(&#39;draggable&#39;) as HTMLDivElement,
  ).current;

  return (render: (provided: DraggableProvided) =&gt; ReactElement) =&gt;
    (provided: DraggableProvided) =&gt; {
      const result = render(provided);
      const style = provided.draggableProps.style as DraggingStyle;
      if (style.position === &#39;fixed&#39;) {
        return createPortal(result, element);
      }
      return result;
    };
};</code></pre>
<br/>

<pre><code class="language-tsx">// component.tsx

const optionalPortal = useDraggableInPortal();

...

&lt;Draggable draggableId={id)} index={idx} key={id}&gt;
  {optionalPortal((provided) =&gt; (
    &lt;DraggableContainer
      {...provided.draggableProps}
      ref={provided.innerRef}
      &gt;
      {/* some react code */}
    &lt;/DraggableContainer&gt;
  ))}
&lt;/Draggable&gt;</code></pre>
<br/>

<p>RBD의 draggable의 특징과 이를 이용한 해결책은 정말 놀라웠다. gpt나 다른 블로그, RBD 공식 레퍼런스(github markdown 문서들)에서도 해결책이 없어서 긴 시간의 삽질을 했다. 이 글이 같은 문제를 만난 개발자들에게 조금의 insight라도 되었으면 하는 마음이다.</p>
<br/>
<br/>

<p>&lt;참고자료 및 출처&gt;
<a href="https://github.com/atlassian/react-beautiful-dnd/issues/1422">https://github.com/atlassian/react-beautiful-dnd/issues/1422</a>
<a href="https://github.com/atlassian/react-beautiful-dnd/issues/128">https://github.com/atlassian/react-beautiful-dnd/issues/128</a>
<a href="https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md">https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트에서 dangerouslySetInnerHTML  사용하기]]></title>
            <link>https://velog.io/@ferrari_roma/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C-dangerouslySetInnerHTML-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ferrari_roma/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C-dangerouslySetInnerHTML-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 17 Jul 2023 06:56:59 GMT</pubDate>
            <description><![CDATA[<p>dangerouslySetInnerHTML과 관련된 내용을 찾아보다가 잘 정리된 글이 있어서 번역하며 공부해 보았다. 거의 대부분의 번역하였지만 몇몇 부분은 중복된 내용이거나 크게 중요하지 않은 내용이라 생략했다. 원문은 아래 링크를 참고하면 된다.</p>
<p><a href="https://blog.logrocket.com/using-dangerouslysetinnerhtml-in-a-react-application/">Using dangerouslySetInnerHTML in a React application - LogRocket Blog</a></p>
<h1 id="리액트에서-dangerouslysetinnerhtml-사용하기">리액트에서 dangerouslySetInnerHTML 사용하기</h1>
<p>selector를 사용해서 HTML element를 가져온 다음, <code>innerHTML</code>을 사용해서 설정하는 대신에 <code>dangerouslySetInnerHTML</code>을 사용하면 element를 직접 사용할 수 있습니다. <code>dangerouslySetInnerHTML</code>를 사용하면 리액트는 특정 엘리먼트가 동적이라는 것을 알고, 그 노드의 children에 대한 virtualDOM과의 비교를 건너뛰게 됩니다. 그로인한 추가적인 성능을 얻을 수 있습니다.</p>
<p>하지만 이름에서 알 수 있듯이, 이걸 사용하면 XSS 공격에 취약해 질 수 있습니다. 유저가 제출한 컨탠츠를 렌더링 하거나 서드파티 소스로부터 데이터를 fetching한다면 이 부분이 문제가 됩니다.</p>
<pre><code class="language-jsx">const App = () =&gt; {
  const data = &#39;lorem &lt;b&gt;ipsum&lt;/b&gt;&#39;;

  return (
    &lt;div&gt;
      {data}
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<pre><code class="language-jsx">const App = () =&gt; {
  const data = &#39;lorem &lt;b&gt;ipsum&lt;/b&gt;&#39;;

  return (
    &lt;div
      dangerouslySetInnerHTML={{__html: data}}
    /&gt;
  );
}

export default App;</code></pre>
<p><code>_html</code>키를 가진 object형태로 <code>dangerouslySetInnerHTML</code>에 넘겨야 합니다. 다른 부분으로는 <code>dangerouslySetInnerHTML</code>을 사용하는 element는 어떠한 children도 없어야 하므로 <code>&lt;div&gt; element</code>를 <code>self-closing tag</code>로서 사용해야 합니다.</p>
<p>객체로 넘기는 것은 개발자가 documentation을 살펴보지 않아서 발생하는 잠재적인 위험을 방지하기 위한 안전장치 입니다.</p>
<h1 id="안전하게-dangerouslysetinnerhtml-사용하기">안전하게 <code>dangerouslySetInnerHTML</code> 사용하기</h1>
<p>위의 간단한 예시에서는 어떤 위험도 없습니다. 그러나 HTML element가 스크립트를 실행하는 경우가 있습니다. 비록 예시가 멀쩡해 보여도 어떻게 html element가 악성 스크립트를 작동시킬 수 있는지 증명할 수 있습니다. 아래 예시에서는 <code>ipsum</code> 부분에 마우스가 호버링 되면 alert가 작동됩니다.</p>
<pre><code class="language-jsx">const App = () =&gt; {
  const data = `lorem &lt;b onmouseover=&quot;alert(&#39;mouseover&#39;);&quot;&gt;ipsum&lt;/b&gt;`;

  return (
    &lt;div
      dangerouslySetInnerHTML={{__html: data}}
    /&gt;
  );
}

export default App;

const App = () =&gt; {
  const data = `lorem ipsum &lt;img src=&quot;&quot; onerror=&quot;alert(&#39;message&#39;);&quot; /&gt;`;

  return (
    &lt;div
      dangerouslySetInnerHTML={{__html: data}}
    /&gt;
  );
}

export default App;</code></pre>
<p>다행히도 html 코드에서 잠재적인 악성코드를 추적해서 안전한<code>(sanitized: 소독된)</code> 버전으로 출력하게 해주는 도구가 있습니다. DOMPurify 입니다.</p>
<pre><code class="language-jsx">// Original
lorem &lt;b onmouseover=&quot;alert(&#39;mouseover&#39;);&quot;&gt;ipsum&lt;/b&gt;

// Sanitized
lorem &lt;b&gt;ipsum&lt;/b&gt;</code></pre>
<pre><code class="language-jsx">// Original
lorem ipsum &lt;img src=&quot;&quot; onerror=&quot;alert(&#39;message&#39;);&quot; /&gt;

// Sanitized
lorem ipsum &lt;img src=&quot;&quot;&gt;</code></pre>
<p>바로 <code>DOMPurify</code>를 사용하는 것입니다.</p>
<pre><code class="language-jsx">import DOMPurify from &#39;dompurify&#39;

const App = () =&gt; {
  const data = `lorem &lt;b onmouseover=&quot;alert(&#39;mouseover&#39;);&quot;&gt;ipsum&lt;/b&gt;`
  const sanitizedData = () =&gt; ({
    __html: DOMPurify.sanitize(data)
  })

  return (
    &lt;div
      dangerouslySetInnerHTML={sanitizedData()}
    /&gt;
  );
}

export default App;</code></pre>
<p>예상대로, sanitize가 되면서 bold text에 호버링을 해도 작동되는 함수가 없습니다.</p>
<p>DOMPurify는 DOM tree가 필요한데, 노드 환경에서는 돔 트리가 없기 때문에 우리는 jsdom 패키지를 사용해서 window 객체를 만들고 DOMPurify를 초기화 해야 합니다. 또는 DOMPurify나 jsdom 패키지를 캡슐화한 <code>isomorphic-dompurify</code> 패키지를 사용해야 합니다.</p>
<pre><code class="language-jsx">const createDOMPurify = require(&#39;dompurify&#39;);
const { JSDOM } = require(&#39;jsdom&#39;);

const window = new JSDOM(&#39;&#39;).window;
const DOMPurify = createDOMPurify(window);

const clean = DOMPurify.sanitize(dirty);</code></pre>
<h1 id="마무리">마무리</h1>
<p>innerHTML을 대체하는 dangerouslySetInnerHTML은 사용에 주의가 필요합니다. 이름에서도 알 수 있듯이 사용에 위험이 따르지만 sanitize를 통해서 리액트에서 렌더링 될 때 예기치 않은 스크립트가 실행되지 않도록 사용해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[마구마구 뒷북치는 React17 변경점]]></title>
            <link>https://velog.io/@ferrari_roma/%EB%A7%88%EA%B5%AC%EB%A7%88%EA%B5%AC-%EB%92%B7%EB%B6%81%EC%B9%98%EB%8A%94-React17-%EB%B3%80%EA%B2%BD%EC%A0%90</link>
            <guid>https://velog.io/@ferrari_roma/%EB%A7%88%EA%B5%AC%EB%A7%88%EA%B5%AC-%EB%92%B7%EB%B6%81%EC%B9%98%EB%8A%94-React17-%EB%B3%80%EA%B2%BD%EC%A0%90</guid>
            <pubDate>Thu, 04 May 2023 00:42:00 GMT</pubDate>
            <description><![CDATA[<p>지금 리액트는 18버전이다. 정확히는 18.2.0 버전인데, 22년 6월에 나왔다. 나온 지 10개월이 지난 지금 17 변경점을 알아보는 이유는 최근에 18 버전의 변경점을 알아보던 와중에 &#39;그럼 17변경점은 뭐지?&#39;하는 생각이 들어 찾아보니 새로 알게 된 것이 있었다. 많은 것이 바뀐 건 아니지만 그래도 새로운 내용이라서, 시간이 지났어도 정리해 본다.</p>
<h1 id="gradual-update">Gradual Update</h1>
<p>여러 레퍼런스에서 17 버전의 업데이트를 &#39;업데이트를 위한 업데이트&#39;라고 표현을 했다. 큰 새로운 기능은 없지만, 미래를 위한 업데이트라고 볼 수 있는 개선된 부분들이 있다. 그 중 하나가 이 점진적 업데이트(Gradual Update)이다. 점진적 업데이트는 두 개 이상의 React가 하나의 어플리케이션에서 함께 구동되는 것을 말한다. 장점은 새로 React가 업데이트 됐을 때 한 번에 모든 걸 업데이트 할 필요 없다는 것이다. 그러나 이것은 여지를 주는 것이지 추천하는 것은 아니다. 대부분 상황에서는 늘 하나의 React 버전을 사용하도록 노력해야 한다. 단일 버전을 사용하는 것이 복잡도를 낮추고 React core를 두 번 다운로드 하는 일을 막을 수 있다. </p>
<blockquote>
</blockquote>
<p>Normally, we encourage you to use a single version of React across your whole app.
Using a single version of React removes a lot of complexity.
It is also essential to ensure the best experience for your users who don&#39;t have to download the code twice. Always prefer using one React if you can.</p>
<h3 id="event-delegation">Event Delegation</h3>
<p>사실 이건 이전에도 할 수 있었다. React는 특정 DOM으로부터 Virtual DOM을 만들고 관리하기 때문에 독립적으로 하나의 페이지에서 복수의 React를 사용하거나 중첩된 구조로 사용할 수 있었다. 하지만 이 경우에 기술적인 문제가 있는데, 바로 이벤트 위임과 관련된 문제였다. 17버전 이전은 React Tree 내부의 event.stopPropagation이 다른 React Tree로의 이벤트 위임을 막을 수가 없었다.
그래서 React를 나누어 사용할 때 UI Tree를 추적할 때 예상치 못한 일들이 발생했던 것이다. 원인은 React 17 이전에는 두 개의 React에 있는 이벤트 리스너가 document 레벨에 부착됐기 때문이다. 17버전부터는 이벤트 리스너를 Root에 붙이는 것으로 수정했다.</p>
<h1 id="import-react">Import React</h1>
<p>원래 jsx를 사용하기 위해서 우리는 매번 React를 Import 해줘야 했다. jsx코드가 React.createElement() 코드로 변환해야 하기 때문이다.
하지만 17버전부터는 React를 따로 Import 하지 않아도 된다. 주요 변경점이라기 보다는 추가된 기능이다. 이렇게 가능한 이유는 새로운 jsx 트랜스폼 방식이 지원됐기 때문이다. 새로운 트랜스폼은 React를 import 하지 않아도 되는 것 외에도, 이전 jsx 트랜스폼에 비해 번들링 시 번들 크기를 약간 개선할 수 있다.</p>
<blockquote>
<p>Upgrading to the new transform is completely optional, but it has a few benefits:</p>
</blockquote>
<ul>
<li>With the new transform, you can use JSX without importing React.</li>
<li>Depending on your setup, its compiled output may slightly improve the bundle size.</li>
<li>It will enable future improvements that reduce the number of concepts you need to learn React.</li>
</ul>
<p><em>예시코드는 <a href="https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html">Introducing the New JSX Transform</a>에서 가져왔습니다.</em></p>
<pre><code class="language-js">import React from &#39;react&#39;;

function App() {
  return &lt;h1&gt;Hello World&lt;/h1&gt;;
}</code></pre>
<p>원래는 위와 같은 코드는 jsx가 내부적으로 javascript 코드로 변환한다.</p>
<pre><code class="language-js">import React from &#39;react&#39;;

function App() {
  return React.createElement(&#39;h1&#39;, null, &#39;Hello World&#39;);
}</code></pre>
<p>하지만 이 방식은 완벽하지 않다. React를 import 해야 하는 것과는 별개로 createElement()의 성능 문제가 있다. 이런 문제를 해결한 새로운 jsx 트랜스폼은 다음으로 jsx를 변환한다.</p>
<pre><code class="language-js">// Inserted by a compiler (don&#39;t import it yourself!)
import { jsx as _jsx } from &#39;react/jsx-runtime&#39;;

function App() {
  return _jsx(&#39;h1&#39;, { children: &#39;Hello World&#39; });
}</code></pre>
<p>이제 새로운 jsx 트랜스폼은 컴파일 과정에 자동으로 위와 같은 jsx를 변환하는 코드가 추가된다. 새로운 트랜스폼은 &#39;react/jsx-runtime&#39; 모듈에서 자동으로 가져오기 때문에 jsx를 사용할 때 따로 React를 import 하지 않아도 되는 것이다.</p>
<h1 id="생명주기-메소드">생명주기 메소드</h1>
<p>리액트 17에 와서는 componentWillMount와 componentWillUpdate, componentWillReceiveProps 라이프 사이클이 deprecated 됐다. 몇 일 전에 우연히 제로초님 블로그의 <a href="https://www.zerocho.com/category/React/post/579b5ec26958781500ed9955">React의 생명 주기(Life Cycle)</a> 글을 읽다가 이 부분을 알게 되었다. 사실 여기서부터 &#39;그럼 React17 버전에 이것 말고 뭐가 또 바뀌었을까?&#39;하는 궁금함이 생겨서 찾다가 이 글로 정리하게 되었다.</p>
<h1 id="event-pooling">Event Pooling</h1>
<p>이벤트 풀링이 없어졌다. 리액트에서는 다양한 브라우저 환경에서도 동일하게 이벤트를 처리하기 위해서 이벤트 래퍼인 SyntheticEvent(합성 이벤트)를 제공한다. 17 이전에서는 이 합성 이벤트 객체를 성능상 이유로 재사용했다. 그래서 사용 이후에는 null로 초기화를 했는데, 그래서 다음과 같은 코드에서는 이벤트에 접근할 수 없었다.</p>
<pre><code class="language-js">// code from https://ko.legacy.reactjs.org/docs/legacy-event-pooling.html
function handleChange(e) {
  // This won&#39;t work because the event object gets reused.
  setTimeout(() =&gt; {
    console.log(e.target.value); // Too late! 여기서 null이 출력된다.
  }, 100);
}</code></pre>
<p>그래서 여기서 합성 이벤트 객체 접근을 하기 위해서는 persist 메소드를 사용해야 했다.</p>
<pre><code class="language-js">function handleChange(e) {
  // Prevents React from resetting its properties:
  e.persist(); // 이걸 사용해야 아래의 콘솔에서 이벤트 객체에 접근할 수 있었다.

  setTimeout(() =&gt; {
    console.log(e.target.value); // Works
  }, 100);
}</code></pre>
<p>하지만 17부터는 합성 이벤트 객체를 재사용하지 않는다. 그래서 위의 코드처럼 persist를 사용하지 않고도 개발자가 생각한 대로 이벤트 객체에 접근할 수 있다. 성능 향상을 위해서지만 최신 브라우저에서는 이에 대한 효과가 미미하고 사용자의 혼란을 유발했기 때문에 삭제된 것 같다. 또 이에 대한 근본적인 버그를 해결했다는 이야기도 있다. 물론 persist 메소드는 여전히 남아있지만 호출해도 아무런 동작을 하지 않는다.</p>
<h1 id="useeffect-clean-up-timing">useEffect clean up timing</h1>
<p>useEffect는 렌더링 이후에 비동기적으로 동작한다. 동기적인 작업이 필요할 때는 useLayoutEffect를 사용하면 된다. 하지만 16버전까지의 useEffect에는 문제가 하나 있었는데 클린업(return부분)은 동기적으로 동작했다. 이게 문제가 되는 이유는 클린업 부분에 무거운 동작을 하는 함수라도 있다면 unmount하기 전에 클린업을 기다렸다가 unmount를 하고 다음 화면을 렌더링 할 수 있다.
17부터는 이런 것을 보완하기 위해서 클린업 함수도 항상 비동기적으로 실행된다. 그리고 모든 useEffect는 클린업 처리가 끝나고 나서 호출된다.</p>
<h1 id="마무리">마무리</h1>
<p>17버전에 바뀐 게 많이 없다는데, 그럼 다른 버전에서는 얼마나 많은 걸까? 18버전 변경점도 다시 상세히 살펴보아야겠다. 특히나 useEffect 부분은 부트캠프 시 사람들끼리 모여서 직접 로그 찍어보면서 실험해 보기도 했는데 이 부분이 17버전 업데이트 내용이라는 것, 그리고 왜 그렇게 되었는지에 대해서 알아보면서 머리에 깊이 각인되었다. 뒷북이라도 정리를 했음에 다행스러움을 느낀다.</p>
<p>참고자료
<a href="https://leo.works/2012130/">https://leo.works/2012130/</a></p>
<p><a href="https://yamoo9.github.io/react-master/lecture/r-version-17.html#%E1%84%89%E1%85%A2%E1%84%85%E1%85%A9%E1%84%8B%E1%85%AE%E1%86%AB-%E1%84%80%E1%85%A5%E1%86%AB-%E1%84%8B%E1%85%A5%E1%86%B9%E1%84%8B%E1%85%B3%E1%86%B7">https://yamoo9.github.io/react-master/lecture/r-version-17.html#%E1%84%89%E1%85%A2%E1%84%85%E1%85%A9%E1%84%8B%E1%85%AE%E1%86%AB-%E1%84%80%E1%85%A5%E1%86%AB-%E1%84%8B%E1%85%A5%E1%86%B9%E1%84%8B%E1%85%B3%E1%86%B7</a></p>
<p><a href="https://github.com/reactjs/react-gradual-upgrade-demo/">https://github.com/reactjs/react-gradual-upgrade-demo/</a></p>
<p><a href="https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html">https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html</a></p>
<p><a href="https://ko.legacy.reactjs.org/docs/handling-events.html">https://ko.legacy.reactjs.org/docs/handling-events.html</a></p>
<p><a href="https://ko.legacy.reactjs.org/docs/events.html#gatsby-focus-wrapper">https://ko.legacy.reactjs.org/docs/events.html#gatsby-focus-wrapper</a></p>
<p><a href="https://ko.legacy.reactjs.org/docs/legacy-event-pooling.html">https://ko.legacy.reactjs.org/docs/legacy-event-pooling.html</a></p>
<p><a href="https://ko.legacy.reactjs.org/docs/hooks-reference.html#useeffect">https://ko.legacy.reactjs.org/docs/hooks-reference.html#useeffect</a></p>
<p><a href="https://www.zerocho.com/category/React/post/579b5ec26958781500ed9955">https://www.zerocho.com/category/React/post/579b5ec26958781500ed9955</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS의 DI Container]]></title>
            <link>https://velog.io/@ferrari_roma/Nest%EC%9D%98-DI-container</link>
            <guid>https://velog.io/@ferrari_roma/Nest%EC%9D%98-DI-container</guid>
            <pubDate>Tue, 11 Apr 2023 19:00:09 GMT</pubDate>
            <description><![CDATA[<p>Nest로 백엔드를 구축하면서 초반에 가장 많이 만났던 에러가 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">Error: Nest can&#39;t resolve dependencies of ...</span> 였다. Dependency를 해결을 못해서 에러가 발생한 것이다. nest를 잘 모르는 상태에서 express를 다루는 것처럼 작업을 하니까 이런 에러는 뭘 하든지 계속 발생했다. &#39;의존성이 잘못된 거 같은데 이걸 어떻게 해결하는 거야..?&#39;하는 생각에 검색에 검색을 해보았지만 Nest의 사용법을 몰라서 이 문제를 근본적으로는 해결할 수 없다. 무수한 삽질의 시간을 보내고 Nest의 기본적인 부분을 공부하고 나서야 이 문제를 이해하고 해결할 수 있었다. Nest 작업이 끝난지 꽤 지났지만 더 늦기 전에 기록을 해봐야겠다.</p>
<h1 id="dependency">Dependency</h1>
<p>dependency, 의존성은 뭘까? 이것 먼저 확인해 보고 가자.</p>
<pre><code class="language-ts">class Engine {
  hp(input:number) {
    return `This vehicle has ${input} horsepower`;
  }
}

class Truck {
  truckInfo:Car
  constructor() {
    this.truckInfo = new Engine();
  }

  getHp(hp:number) {
    return this.truckInfo.hp(hp)
  }
}

const truck = new Truck();
console.log(truck.getHp(40)); // &quot;This vehicle has 40 horsepower&quot;</code></pre>
<p>간단한 예시 코드를 작성해 봤다. Truck 클래스 안에 있는 truckInfo라는 프로퍼티는 생성자 함수 안에서 Engine 클래스의 인스턴스를 할당 받는다. Truck은 스스로 Engine에 대한 종속성을 생성하고 있다. 이런 구조는 Truck이 Engine에 의존하는 형태이다. 이런 것을 종속성이라고 한다. 의존하는 관계, 그것이 종속성이다.
이처럼 종속성을 가지고 있으면 몇 가지 단점이 발생한다. Engine 코드가 수정이 됐을 때 Truck의 코드도 수정해야 할 가능성이 높고, 테스트 코드를 작성한다고 했을 때도 종속성을 가지고 있기 때문에 unit test의 독립성을 떨어뜨린다.
테스트 속도 면에서, 그리고 편의성 면에서도 단점이 있는데 이는 실제 서비스 코드를 떠올려 보자. 보통 service 클래스에서 repository 클래스를 이용해서 DB에 CRUD를 한다. 위 예시 코드 처럼 service 클래스가 repository 클래스에 종속되어 있다면, 테스트 환경에서도 실제 DB에 CRUD를 할 것이고 메모리를 사용하는 것 보다는 상대적으로 속도가 느리다. 하지만 가짜 데이터와 가짜 repository 클래스를 만들어서 모킹을 하고 메모리에서 CRUD를 하면 속도도 상대적으로 더 빠르고, 실제 DB를 사용하는 것이 아니기 때문에 훨씬 더 편하기도 하다. 이렇게 하기 위해서는 IoC를 적용해야 한다.</p>
<h1 id="ioc">IoC</h1>
<p>Inversion of Control(IoC)은 우리말로 &#39;제어 역전&#39;이라고 한다. 말이 좀 생소하거나 어려울 수 있지만 말 뜻을 그대로 받아들이면 된다. Truck 클래스 입장에서는 Engine 클래스가 필요하지만 생성자 함수에서 직접 초기화 하지 않으면서 스스로 종속성을 생성하지 않는 것이다. 즉, 누군가가 외부에서 이를 제어하는 것이다. 위 예시 코드를 조금 수정하면서 알아보자.</p>
<pre><code class="language-ts">class Engine {
  hp(input:number) {
    return `This vehicle has ${input} horsepower`;
  }
}

class Truck {
  constructor(public truckInfo:Engine) {}

  getHp(hp:number) {
    return this.truckInfo.hp(hp)
  }
}

const engine = new Engine()
const truck = new Truck(engine);
console.log(truck.getHp(40)); // &quot;This vehicle has 40 horsepower&quot;</code></pre>
<p>이렇게 생성자 함수에서는 Engine 클래스로 타입만 해주면 된다. 이런 방식으로 사용하면 <strong>사용하는 부분에서 Truck 클래스를 제어</strong>하게 되고 이 부분에서 제어의 역전이 발생한 것이다. 또 Truck에는 Engine으로 된 타입만 들어오면 코드가 돌아가니까 테스트를 위해 가짜 클래스를 연결하는 것, 즉 모킹하는 것도 편리해진다. 이를 통해서 앞에서 말했던 문제들을 해결할 수 있다. 하지만 이것도 나름의 단점이 있다.</p>
<p>마지막에 Engine 인스턴스를 생성하고 Truck 인스턴스를 생성하는 부분이 있는데, 만약 규모가 큰 어플리케이션에서 종속성이 복잡할 때는 저 부분이 여러 줄로 많아질 것이다. 그런 부분에서 Nest는 DI Container를 사용하면 된다.</p>
<h1 id="di-container">DI Container</h1>
<p>DI(Dependency Injection)는 우리말로 &#39;의존성 주입&#39;이라고 한다. 의존성을 주입하는 것 역시 IoC처럼 뜻을 그대로 이해하면 된다. 외부에서 의존성을 주입해 주는 것이다. 어떻게 보면 IoC를 사용하는 것에 관한 이야기이다. Nest는 프레임워크에서 DI를 <strong>DI Container</strong>를 사용해서 해주고 있고, 이 컨테이너가 제어권을 가지고 있는 것이다(IoC). 위 예시처럼 사용자가 일일이 인스턴스를 만드는 것이 아니라 프레임워크가 어플리케이션이 시작될 때 해준다. DI 컨테이너는 다음과 같은 과정을 거쳐서 제어권을 가지고 의존성을 주입해 준다.</p>
<blockquote>
<ol>
<li>Nest는 시작 단계에서 DI 컨테이너에 사용하는 모든 클래스를 등록한다.</li>
<li>컨테이너는 각각의 클래스가 의존하는 클래스도 함께 등록한다. 
예) Truck 👉 Engine</li>
<li>그리고 나서 우리가 특정 클래스의 인스턴스를 요청하면 컨테이너가 우리를 위해 인스턴스를 생성해 준다.</li>
<li>이 과정에서 컨테이너는 &#39;2번&#39;에서 미리 등록해 둔 의존성에 따라 관련된 모든 의존성이 해결된 인스턴스를 준다. 위 예시 코드에서 일일이 인스턴스를 생성하고 주입해주는 것을 컨테이너가 다 해주는 것이다.</li>
<li>한 번 만든 이런 인스턴스는 컨테이너에 저장되어 있다가 필요하면 <strong>재사용</strong>한다.</li>
</ol>
</blockquote>
<p>위 코드를 Nest 코드로 조금 바꿔서 이 부분을 확인해 보자.</p>
<pre><code class="language-ts">// engine.module.ts
import { Module } from &#39;@nestjs/common&#39;;
import { EngineService } from &#39;./engine.service&#39;;

@Module({
  providers: [EngineService],
  exports: [EngineService], // 다른 module에서 참조하기 위해서는 꼭 exports에 EngineService 클래스를 넣어주어야 한다.
})
export class EngineModule {}

// engine.service.ts
import { Injectable } from &#39;@nestjs/common&#39;;

@Injectable() // Injectable 데코레이터를 사용해야 1에서 2번까지를 Nest가 진행해 준다.
export class EngineService {
  hp(input: number) {
    return `This vehicle has ${input} horsepower`;
  }
}

// truck.Module.ts
import { Module } from &#39;@nestjs/common&#39;;
import { EngineModule } from &#39;src/engine/engine.module&#39;;
import { TruckService } from &#39;./truck.service&#39;;

@Module({
  imports: [EngineModule],  // exports한 EngineService를 사용하기 위해서는 EngineModule을 imports에 넣어줘야 한다.
  providers: [TruckService],
})
export class TruckModule {}

// truck.service.ts
import { Injectable } from &#39;@nestjs/common&#39;;
import { EngineService } from &#39;src/engine/engine.service&#39;;

@Injectable()
export class TruckService {
  constructor(private engineService: EngineService) {}  // EngineModule을 imports 했기 때문에 그곳에서 exports한 EngineService를 사용할 수 있다.

  getHp(hp:number) {
    return this.engineService.hp(hp)
  }
}</code></pre>
<p>위 예시 코드를 앞서 알아본 단계에 따라 분석해 보자. 가장 먼저 Injectable 데코레이터가 사용된 EngineService와 TruckService가 컨테이너에 초기화 된다. 이때 TruckService는 EngineService를 의존하고 있기 때문에 컨테이너에서도 이 부분을 확인해 둔다. 이까지가 2번 단계에 해당하는 작업이다.
그 다음에 우리가 TruckService를 예시 코드에는 없는 TruckControler 클래스에서 참조한다고 가정해 보자. 이후 우리가 TruckController를 사용하면 컨테이너는 이 세 단계에 걸친 의존성을 제어권을 가지고 의존성을 주입해서 우리에게 TruckController의 인스턴스를 준다.</p>
<h1 id="이걸-왜-할까">이걸 왜 할까?</h1>
<p>방법보다 중요한 것이 왜 하는지이다. 이 부분은 사실 앞에서도 설명을 했지만 여기서는 내 경험 상 어떤 부분이 편리했는지를 알려주고 싶다. 바로 테스트 코드이다. 정말로 테스트 코드를 작성하는 것에서 매우 편리해진다.</p>
<pre><code class="language-ts">// truck.service.spec.ts
import { Test, TestingModule } from &#39;@nestjs/testing&#39;;
import { TruckService } from &#39;./truck.service&#39;;
import { EngineService } from &#39;../engine/engine.service&#39;;

describe(&#39;TruckService&#39;, () =&gt; {
  let service: TruckService;
  let fakeEngineService: EngineService;

  beforeEach(async () =&gt; {
    fakeEngineService = {
        hp(input: number) {
          return `This vehicle has ${input} horsepower`;
        }
    }

    const module: TestingModule = await Test.createTestingModule({
      providers: [TruckService],
      { provide: EngineService, useValue: fakeEngineService },
    }).compile();

    service = module.get&lt;TruckService&gt;(TruckService);
  });

  describe(&#39;getHp&#39;, () =&gt; {
    it(&#39;returns sentence with hp value&#39;, () =&gt; {
        expect(service.getHp(40)).toBe(&#39;This vehicle has 40 horsepower&#39;)
    })
  })
});</code></pre>
<p>위 테스트 코드를 보면 fakeEngineService라는 변수에 getHp 메소드를 구현했다. 이후 module에 EngineService가 호출될 땐 fakeEngineService를 사용하도록 했다. 즉, 모킹(mocking)을 한 것이다. EngineService의 생성자 함수에서 타입만 해주었기 때문에, 실제로 사용하는 테스트 파일에서 EngineService의 타입을 가지고 있는 가짜 EngineService를 모킹할 수 있었다.
나는 맨 처음 봤던 예시 코드처럼 코드를 작성하고 테스트 코드를 짜려고 했었다. 그때 에러가 엄청 떴다. 왜 뜨는지도 모른 채로 정말 삽질만 엄청 했었다. 이렇게 Nest가 어떻게 돌아가는지를 알게 된 후로는 그 규칙에 맞게 코드를 작성하였고 unit test에서 각 테스트 간 독립성을 유지하는 것이 굉장히 쉬워졌다. 의존성 주입과 제어 역전을 통해서 결합도가 느슨한 코드를 작성해서 얻은 큰 이점이다.</p>
<p>사실 테스트 코드를 쉽게 하기 위해서 IoC, DI를 사용한다면 주객이 전도된 것이지만, Nest에서 IoC, DI가 어떻게 사용되는지를 알고 난 후 제대로 사용했을 때 이 부분에서 가장 큰 차이를 느낄 수 있을 것이다. 적어도 나는 그랬다. 혹여나 나처럼 맨땅에 Nest로 헤딩하는 분들이 있다면 이 글을 통해서 Nest와 그 안에서 일어나는 IoC, DI가 어떻게 이루어 지는지, 그리고 그렇게 코드를 작성했을 때 어떤 이점이 있는지를 조금이나마 이해할 수 있었으면 좋겠다.</p>
<p><em>참고자료</em>
<a href="https://develogs.tistory.com/19">https://develogs.tistory.com/19</a>
<a href="https://velog.io/@server30sopt/IoC-Container">https://velog.io/@server30sopt/IoC-Container</a>
Udemy Nest 강좌 : <a href="https://www.udemy.com/course/nestjs-the-complete-developers-guide/">NestJS: The Complete Developer&#39;s Guide</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[굳이 screen을 왜 쓸까?]]></title>
            <link>https://velog.io/@ferrari_roma/%EA%B5%B3%EC%9D%B4-screen%EC%9D%84-%EC%99%9C-%EC%93%B8%EA%B9%8C</link>
            <guid>https://velog.io/@ferrari_roma/%EA%B5%B3%EC%9D%B4-screen%EC%9D%84-%EC%99%9C-%EC%93%B8%EA%B9%8C</guid>
            <pubDate>Tue, 21 Mar 2023 16:22:33 GMT</pubDate>
            <description><![CDATA[<p>테스트 코드를 작성하다가 이것저것 궁금해져서 찾아본 결과를 정리한다.</p>
<p>img 태그를 검증하고 싶은데 이걸 어떻게 검증을 할 수 있을지에 대한 궁금함이었다. 찾아보니 document.querySelector을 사용해서 img 태그에 접근을 했다. 여기서 든 첫 번째 의문은 &#39;react jsx에 대한 테스트인데 document로 접근하는 것이 과연 최선의 선택일까?&#39;였다. 그에 대한 문제는 아래와 같은 에러로 어느 정도 확인이 됐다.
<img src="https://velog.velcdn.com/images/ferrari_roma/post/bd759fe9-aef6-4d87-9e1a-60a16c0803b8/image.png" alt=""></p>
<blockquote>
<p>노드에 직접적인 접근은 피하세요. Testing Library의 메소드를 사용해 주세요.</p>
</blockquote>
<p>그래서 다른 방법으로 container를 써볼까 했다. render 메소드를 이용해서 렌더링을 하고 나면 HTML Element를 가지는 container를 반환하는데 method들도 다 가지고 있으니 testing library를 통한 접근이 아닐까 하는 생각이 들었다.</p>
<p>하지만 이 방법 역시 다음과 같은 에러가 발생했다. 
<img src="https://velog.velcdn.com/images/ferrari_roma/post/608f96ea-30c6-48d5-bbe0-edc7cbd13ea5/image.png" alt="">
여기서는 container를 사용하는 것도 피하라고 했다. 아마 같은 이유였던 것 같다. 결국에는 HTML에 직접 접근하는 것으로 판단하는 것 같았다. 여기서는 getByRole()와 같은 메소드를 사용하길 권장했다.</p>
<p>하지만 getByRole()을 사용 해보니 다음과 같은 에러가 발생했다.
<img src="https://velog.velcdn.com/images/ferrari_roma/post/b2046542-ba8e-4908-b4fb-ac92c5147ee9/image.png" alt=""></p>
<p>여기서 Screen이라는 친구를 처음 봤다. 이쯤 되니 &#39;굳이 screen을 왜 거쳐야 할까?&#39;에 대해서 궁금해졌다.</p>
<p>testing-library 문서를 확인해 보니 Queries 항목에 있었다. Queries는 뭘까?</p>
<blockquote>
<p>Queries는 페이지에서 Testing Library가 당신에게 elements를 찾을 수 있게 해주는 메소드 입니다.</p>
</blockquote>
<p>그리고 Screen은 다음과 같은 설명이 있었다.</p>
<blockquote>
<p>DOM Testing Library에서 exports되는 모든 메소드들은 첫 번째 arguments로 container를 받습니다. 왜냐하면 전체 document.body를 조회하는 것은 매우 흔한데, DOM Testing Library는 또한 document.body에 미리 바인딩 된 모든 query를 가진 screen을 exports 합니다.</p>
</blockquote>
<p>이것만 보고는 의문이 확실히 가시지 않았다. testing-library의 github을 보고도 100% 납득되진 않았다.</p>
<blockquote>
<p>이 규칙은 render 결과로부터 구조분해를 하는 것 보다 screen 객체로부터 직접 빌트인 queries 메소드들을 사용해서 테스트를 작성하는 것을 강제하는 목적을 가지고 있습니다.</p>
</blockquote>
<h2 id="왜-그렇게-강제를-하는-것일까"><strong>&#39;왜 그렇게 강제를 하는 것일까?&#39;</strong></h2>
<p>이 부분이 결국은 해소가 되지 않아서 chatGPT에게 물어봤다. gpt는 두 가지 측면에서 사용하는 것이 추천된다고 했다.</p>
<blockquote>
<p>Q. getByRole()과 같은 메소드로 바로 접근이 가능한데 왜 screen이라는 object를 거쳐야 할까요?</p>
</blockquote>
<p>A. 두 가지 이유가 크게 있습니다. 안정성 문제와 가독성의 측면 입니다.
screen object는 테스트 환경에서 DOM과 함께 작동하기 쉽게 만들어 줍니다.
screen.getByRole()을 사용하는 것은 getByRole()을 사용하는 것 보다 추가적인 기능을 제공합니다. screen은 그 페이지가 나타날 때까지 기다리는 방법을 제공하고 이는 비동기적인 행동을 테스트 하는 것에서 중요한 부분입니다. 추가적으로 screen을 사용하는 것은 테스트 환경에서 중복 queries를 관리하기 더 쉽게 만들어 주고, 더 읽기 좋고 관리하기 좋은 테스트 코드를 작성할 수 있습니다.</p>
<p>기능적인 차이는 앞에 있는 부분에서 알 수 있었다. 차이는 screen을 거치면 페이지가 나타날 때까지 기다린다는 것과 그 결과 비동기적인 부분에서 확실한 테스트를 보장한다는 것이다.</p>
<h1 id="마무리">마무리</h1>
<p>궁금해서 찾아보고 다 영어라서 해석하다 보니 시간이 꽤나 걸렸지만 정답을 chatGPT에서 얻어서 살짝 서운했달까?.. 그래도 덕분에 이것저것 읽어 보면서 배울 수 있는 시간이 되었다. 또 결국엔 답을 찾았기 때문에 &#39;그래서 뭐가 다른데?&#39;라는 생각으로 끝까지 물어 뜯은 보람이 있었다.</p>
<p>그리고 찾은 결과들을 바탕으로 작성을 하니 테스트가 잘 통과했다!</p>
<pre><code class="language-ts">describe(&#39;Welcome&#39;, () =&gt; {
  describe(&#39;유저가 로그인을 안했을 경우&#39;, () =&gt; {
    it(&#39;타이틀과 로그인 버튼을 렌더링 한다.&#39;, () =&gt; {
      const { container } = render(
        &lt;div&gt;
          &lt;h1&gt;EXTREME TODO&lt;/h1&gt;
          &lt;div&gt;
            &lt;img
              src=&quot;../../public/2x/btn_google_signin_dark_pressed_web@2x.png&quot;
              alt=&quot;google login button&quot;
            &gt;&lt;/img&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      );

      const googleImage = screen.getByAltText(
        /google login button/i
      ) as HTMLImageElement;

      expect(container).toHaveTextContent(&#39;EXTREME TODO&#39;);
      expect(googleImage.src).toContain(&#39;btn_google_signin&#39;);
    });
  });
});</code></pre>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/f454af61-82e7-4258-a7f5-0ec2f9d85afb/image.png" alt=""></p>
<p>&lt;참고자료&gt;
<a href="https://medium.com/@drake_beth/how-to-test-images-in-react-a70053b1634a">https://medium.com/@drake_beth/how-to-test-images-in-react-a70053b1634a</a></p>
<p><a href="https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-screen-queries.md">https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-screen-queries.md</a></p>
<p><a href="https://testing-library.com/docs/queries/about/#screen">https://testing-library.com/docs/queries/about/#screen</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LeetCode] 206. Reverse Linked List]]></title>
            <link>https://velog.io/@ferrari_roma/LeetCode-206.-Reverse-Linked-List</link>
            <guid>https://velog.io/@ferrari_roma/LeetCode-206.-Reverse-Linked-List</guid>
            <pubDate>Tue, 07 Mar 2023 15:18:46 GMT</pubDate>
            <description><![CDATA[<p><a href="https://leetcode.com/problems/reverse-linked-list/">https://leetcode.com/problems/reverse-linked-list/</a></p>
<h1 id="문제에-대한-이해">문제에 대한 이해</h1>
<p>주어진 Linked List를 반대로 뒤집는 문제입니다.</p>
<h1 id="경계조건-및-input-output-생각해보기">경계조건 및 Input, Output 생각해보기</h1>
<blockquote>
<p><strong>경계조건</strong>
  input된 값이 null이면 return 합니다.</p>
</blockquote>
<blockquote>
<p><strong>Input, Output</strong>
  Input : [1, 2, 3]
  Output : [3, 2, 1]
  Input : [4, 2, 7, 1, 3]
  Output : [3, 1, 7, 2, 4]</p>
</blockquote>
<h1 id="수도코드를-작성">수도코드를 작성</h1>
<pre><code class="language-ts">// 1. reverseFunc 함수를 만듭니다. 
// 이 함수는 재귀를 실행하는 함수 입니다.
// Input으로는 ListNode를 두 개 받습니다.
// 명칭은 각각 prevNode, nextNode로 짓습니다.

// 2. reverseFunc 함수의 역할은 말 그대로 &#39;뒤집는&#39; 역할입니다.
// ListNode는 val, next로 이루어지는데,
// prevNode의 val를 nextNode의 next에 할당합니다.

// 예를 들어서 [1,2,3]이 있다고 합시다.
// 처음에는 null과 val가 1인 node를 reverseFunc에 넣습니다.
// val가 1인 node의 next는 val가 2인 node를 가리키고 있습니다.
// 이 next에 null을 할당합니다.
// 그리고 reverseFunc에 val가 1인 node와 val가 2인 node를 넣습니다.
// 앞서 작동한 로직이 그대로 작동됩니다.
// 2인 node의 next는 3을 가리키고 있는데, 1인 node를 할당해 줍니다.

// 3. reverseFunc를 재귀적으로 호출합니다.

// 4. 탈출 조건으로는 nextNode가 null이면 return 해줍니다.</code></pre>
<h1 id="코드-작성하기">코드 작성하기</h1>
<pre><code class="language-ts">/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function reverseList(head: ListNode | null): ListNode | null {
    if(!head) return null;

    const reverseFunc = (prevNode: ListNode | null, nextNode: ListNode | null): ListNode | null =&gt; {
        if(!nextNode) return prevNode;

          // 재귀적으로 reverseFunc을 호출하는 과정에서 
        // nextNode의 next를 저장할 필요가 있어서 변수를 추가했습니다.
        const originalNext = nextNode?.next;
        nextNode ? nextNode.next = prevNode : null;

        return reverseFunc(nextNode,originalNext);
    }

    return reverseFunc(null, head);
};</code></pre>
<h1 id="마무리">마무리</h1>
<p>재귀 유형을 잘못 풀어서 이번 문제를 선택했습니다. Easy이지만 잘 못하는 유형이라서 2시간 가량 걸렸습니다. 반복문을 선택할지 고민도 했었는데 재귀를 연습하는 차원에서 이렇게 풀었습니다. 
수도 코드에서 최대한 상세하게 재귀 함수를 작성하고, 그 부분을 그대로 구현하고자 고민을 하다 보니 다음과 같은 코드를 완성했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 22년 43주차 - 단위 테스트,  통합 테스트,  인수 테스트]]></title>
            <link>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-43%EC%A3%BC%EC%B0%A8-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9D%B8%EC%88%98-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-43%EC%A3%BC%EC%B0%A8-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9D%B8%EC%88%98-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Wed, 02 Nov 2022 07:39:40 GMT</pubDate>
            <description><![CDATA[<p>44주 차가 되어서야 적는 43주 차 WIL이다. 한 게 없어서 안 적은 것은 아니고, nestJS로 백엔드를 빌드하는 과정이 처음이기도 하고 테스트 코드를 작성하는 것은 더 처음이라 사소한 선택에 있어서도 많은 고민이 있었고, 관련된 레퍼런스들을 찾아 보고 정리하다 보니 일주일이 훌쩍 가버렸다. 그러면서 정체된 작업을 어떻게 해서든지 앞으로 밀고 가려고 잡고 있다 보니 WIL이 좀 밀렸다. 
당연히 처음이라서 오래 걸리는 건 알지만, TS가 생산성을 올려주는 건지 원론적인 의문이 생겼다ㅋㅋㅋ 더 공부하고 경험이 쌓인다면 이런 의문을 한 나를 어리석게 여기겠지.. 그래서 오늘은 그간 정리한 레퍼런스 중 하나를 여기에 옮겨볼까 한다.</p>
<h1 id="어쩌고-박스-테스트">어쩌고 박스 테스트</h1>
<p>테스트 코드를 작성하기 위해서 이런 저런 문서들을 찾다 보니까 화이트 박스 테스트, 블랙 박스 테스트라는 용어들이 등장했다. 그래서 이게 뭔지를 찾아보다 보니까 기존에 E2E 테스트, Unit 테스트의 이분법적인 분류가 아닌 조금 더 큰 틀에서의 테스트 분류에 대해서 접하게 됐다. <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">단위 테스트, 통합 테스트, 인수 테스트</span>가 그것이다. 오늘은 해당 내용에 대해서 정리를 해볼까 한다.</p>
<h1 id="단위-테스트">단위 테스트</h1>
<p>단위 테스트(Unit Test)는 하나의 클래스나 함수의 범주에서 작은 단위의 기능을 검증하는 테스트다. 테스트 대상을 어느 정도의 단위로 하라고 명시된 것은 없지만 단위의 크기가 작으면 작을수록 복잡성은 낮아지고 디버깅도 쉬워진다. 요즘 많이 사용되는 Test Driven Development에서 하는 &#39;Test&#39;도 이 단위 테스트를 의미한다. 해당 부분만 독립적으로 테스트를 하기 때문에 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">어떤 코드의 문제가 있는지 신속히 확인할 수 있다.</span></p>
<p>하지만 하나의 어플리케이션은 하나의 클래스나 함수로 작동되는 경우는 거의 없다고 볼 수 있다. 즉, 여러 클래스와 함수가 함께 맞물려 작동되는 경우가 대부분인데 이런 부분에서 단위 테스트의 불편함이 있다. 예를 들어 다른 모듈과 객체나 메시지, 데이터를 주고 받아야 한다면 이를 테스트 하기 위해선 동일한 형식의 가짜 객체, 데이터를 준비해야 하는데, 이걸 stub이라고 한다.</p>
<p>이런 부분에서 단위 테스트는 소프트웨어 내부 구조나 구현 방법을 고려하여 개발자 관점에서 테스트를 진행한다. 그러므로 단위 테스트는 소프트웨어 내부 코드에 관련한 지식을 반드시 알고 있어야 하는 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">화이트 박스 테스트</span>이다.</p>
<h3 id="화이트-박스-테스트">화이트 박스 테스트</h3>
<p>소프트웨어 혹은 제품의 내부 구조나 동작을 세밀하게 검사하는 테스트 방식으로 외부에서 요구사항에 따른 예상 결괏값을 테스트하는 것과는 다르게 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">내부 소스 코드</span>를 테스트하는 기법으로 사용자가 들여다 볼 수 없는 구간의 코드 단위를 테스트 한다.
즉, 개발자가 소프트웨어 또는 컴포넌트 등의 로직에 대한 테스트를 수행하기 위해 설계 단계에서 요구된 사항을 확인하는 개발자 관점의 단위 테스팅 기법이다.</p>
<h3 id="블랙-박스-테스트">블랙 박스 테스트</h3>
<p>소프트웨어의 내부 구조나 작동 원리를 모르는 상태에서 소프트웨어의 동작을 검사하는 방법이다. 검사 진행에 있어서 해당 소프트웨어의 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">내부 코드 및 구조에 대한 정보는 필요하지 않고</span> 요구 사항 검사를 위해 공개된 설계도 등의 대외적으로 공개된 사항들을 통해 검사를 진행한다.
즉, 정리하면 개발자 입장이 아닌 사용자 입장에서 소프트웨어 혹은 제품에 대한 요구사항과 결과물이 일치하는지 확인하기 위한 테스트 기법이다.</p>
<h1 id="통합-테스트">통합 테스트</h1>
<p>통합 테스트(Integration Test)는 앞서 살펴 본 단위 테스트 보다 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">더 큰 규모의 작동을 테스트 하기 위해</span> 여러 모듈들을 모아 개발자의 의도대로 잘 맞물려 돌아가는지 확인하기 위한 테스트이다. 이 과정에서는 단위 테스트가 끝난 모듈을 통합하는 과정에서 생길 오류를 찾을 수 있다. 예를 들어서 통합 테스트 환경에서는 싱글 코어 CPU에서는 잘 실행되나 쿼드 코어 CPU에서는 잘 실행되지 않을 수 있고, 외부 라이브러리와 같이 개발자가 직접 변경하기 어려운 부분까지 통합해서 테스트를 하면 또 문제가 생길 수 있다. 이런 부분 때문에 해당 모듈이 단위 테스트를 모두 통과했다고 통합 테스트에서 문제가 안 생긴다는 보장은 절대 없다. </p>
<p>통합 테스트 또한 단점이 있는데, 단위 테스트 보다 많은 코드들을 통합해서 테스트를 하는 것이다 보니 개별 코드에 대한 신뢰성이 떨어질 수 있고, 에러가 발생했을 때 어떤 코드가 어떤 코드의 영향을 받아서 에러가 발생했는지 명확히 파악하기가 힘들 수 있다. 이런 부분은 유지 보수의 난이도를 높인다.</p>
<h1 id="인수-테스트">인수 테스트</h1>
<p>인수 테스트(Acceptance Test)는 실제 사용자 환경에서, 사용자의 입장으로 하는 테스트이다. 개발 완료 단계에서 시스템의 인수를 위해 기능적/비기능적 요구사항을 사용자의 입장에서 테스트를 하고 개발이 완료됐음을 증명한다.</p>
<p>테스트는 시나리오를 바탕으로 진행한다. 이 시나리오는 개발자가 직접 작성할 수도 있지만, 다른 의사소통 집단에서 받아서 하는 경우가 보통이다. 이처럼 정해진 시나리오에 따라 어플리케이션이 잘 작동하는지를 확인하기 때문에 통합 테스트와는 분류가 다르다. 시나리오는 주로 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">누가, 어떤 목적으로, 무엇을 하는가</span>를 중점으로 작성이 되는데, 개발에서 이런 부분은 API를 통해서 잘 드러난다. 그래서 인수 테스트는 주로 API를 확인하는 방식으로 이루어 진다. 실제 사용자 관점에서 테스트를 하기 때문에 E2E 형식으로 진행한다.</p>
<p>소프트웨어를 인수할 때는 내부의 코드의 품질 보다 시나리오에 따라 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">잘 작동하는지</span>를 보는 블랙 박스 테스트이다.</p>
<p><em>참고자료</em>
<a href="https://tecoble.techcourse.co.kr/post/2021-05-25-unit-test-vs-integration-test-vs-acceptance-test/">https://tecoble.techcourse.co.kr/post/2021-05-25-unit-test-vs-integration-test-vs-acceptance-test/</a></p>
<p><a href="https://mangkyu.tistory.com/143">https://mangkyu.tistory.com/143</a></p>
<p><a href="https://catsbi.oopy.io/7c084479-c9d0-44a1-acb9-f6b43a19e332">https://catsbi.oopy.io/7c084479-c9d0-44a1-acb9-f6b43a19e332</a></p>
<p><a href="https://support.suresofttech.com/ko/support/solutions/articles/5000478109-%EC%8A%A4%ED%85%81-stub-%ED%95%A8%EC%88%98%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94-">https://support.suresofttech.com/ko/support/solutions/articles/5000478109-%EC%8A%A4%ED%85%81-stub-%ED%95%A8%EC%88%98%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94-</a></p>
<p><a href="https://blog.sengwoolee.dev/49">https://blog.sengwoolee.dev/49</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 타겟 넘버]]></title>
            <link>https://velog.io/@ferrari_roma/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%83%80%EA%B2%9F-%EB%84%98%EB%B2%84</link>
            <guid>https://velog.io/@ferrari_roma/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%83%80%EA%B2%9F-%EB%84%98%EB%B2%84</guid>
            <pubDate>Mon, 24 Oct 2022 22:56:13 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/43165">타겟 넘버</a>
트리에 대해서 공부를 했으니 트리에 관련된 문제를 풀어보았다.</p>
<p>이 문제는 주어진 배열 요소를 하나씩 더하거나 빼서 최종적으로 target과 같은 값이 되는 경우의 수를 찾는 문제이다. &#39;경우의 수&#39;에서 알 수 있다시피 DFS를 통해서 해결을 할 수 있는 문제다. DFS를 구현하는 방법은 스택이 있고, 이번에는 재귀를 사용했다.</p>
<p>시간 복잡도는 leaf node의 갯수 만큼이기 때문에 O(2^배열의 길이)이다. 아래는 배열[1, 2]이 주어질 때 해당 노드에 대해 문제와 같은 조건으로 트리를 그려보았다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/e5478f4f-a8be-447a-9231-a43374a6c12a/image.png" alt=""></p>
<p>배열의 길이가 2기 때문에 leaf node가 2의 제곱인 것을 알 수 있다.</p>
<pre><code class="language-js">function solution(numbers, target) {
  let answer = 0;
  const length = numbers.length;

  const DFS = (count, sum) =&gt; {
    if (count === length) {
      if (sum === target) answer++;
      return;
    }
    DFS(count + 1, sum + numbers[count]);
    DFS(count + 1, sum - numbers[count]);
  };

  DFS(0,0);
  return answer;
}</code></pre>
<p>함수 DFS를 재귀적으로 호출하면서 다음 요소를 더하고 빼면서 호출한다. 배열의 길이 만큼 호출이 됐고 그 합의 결과가 target과 동일하다면 카운트를 해준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 22년 42주차 - 면접]]></title>
            <link>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-42%EC%A3%BC%EC%B0%A8-%EB%A9%B4%EC%A0%91</link>
            <guid>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-42%EC%A3%BC%EC%B0%A8-%EB%A9%B4%EC%A0%91</guid>
            <pubDate>Mon, 24 Oct 2022 12:33:35 GMT</pubDate>
            <description><![CDATA[<p>42주차는 면접 준비를 하느라 커밋을 하루에 하나도 못할 때가 대부분이었다. 그래서 이번에는 면접 준비 과정을 돌아보고 내가 부족했던 점에 대한 성찰을 해보려고 한다.</p>
<h1 id="어쩌다-마주친-면접">어쩌다 마주친 면접</h1>
<p>항해99가 끝나고 원서를 이곳저곳에 넣었다. 역시나 부족함이 많아서 대부분은 탈락을 했는데 의외인 곳에 서류가 붙었다. 붙고 나서 온 메일을 보니 풀스택 주니어 개발자라는 사실을 알게 되었다. 내가 왜 진작 몰랐냐면 항해99의 채용 플랫폼인 port99에는 따로 &#39;풀스택&#39;이라는 워딩이 없었지만, wanted나 다른 채용 플랫폼에는 기재가 되어 있었다. 일단 나는 백엔드가 재미없거나 관심이 없는 사람은 아니지만 혹시나 사측에서 내 원서를 잘 못 읽으신 건가 하고 질문 메일을 보내기도 했다. 이력서에는 백엔드에 대한 내용이 하나도 없었기 때문이다.</p>
<p>해당 회사는 주니어들은 풀스택으로 온보딩을 한다고 하였다. 그래서 풀스택을 해도 괜찮으면 오시라는 답이 왔다. 나 역시 프론트가 더 재밌긴 하지만 백엔드도 재미있기 때문에 면접에 응하겠다고 했다.</p>
<h1 id="준비는-하는데">준비는 하는데,</h1>
<p>문제는 면접에 코테가 있었다. 코테에 대한 준비를 거의 안하고 있던 터라 걱정이 앞섰고 이에 대한 준비를 하려고 해당 회사는 어떤 서비스를 만드는지 확인해보려고 했다. 어쩌면 &#39;회사에 필요한 알고리즘이 있지 않을까?&#39;하는 생각을 했기 때문이다. 하지만 이 회사는 국내 서비스는 없고, 해외에서 서비스를 제공하는 회사였고 대부분의 수익은 북미 지역에서 발생하는 회사였다. 그래서 App store에 가도 해당 application이 검색 조차 안됐다. notion을 자세히 읽어봐도 뚜렷한 단서가 없어서 일단 자주 나오는 유형을 위주로 좀 풀었다. 그리고 CS에 대한 질문들, 뻔한 질문들, 예를 들면 자기소개 같은 질문들을 준비했다.</p>
<p>하지만 준비 내내 들었던 느낌은, 이쪽 분야에 와서는 처음 하는 면접이었기 때문에 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">&#39;무엇을, 어디까지, 어떻게 준비해야 할까?&#39;</span> 하는 답답함이 있었다. 모든 게 다 막연했고 손에 잡히지 않았다. 그래서 준비를 하면서도 찝찝한 느낌이 굉장히 많았다.</p>
<h1 id="인생은-실전이야">인생은 실전이야</h1>
<p>면접은 회사에서 봤다. <del>고로 대구에 사는 나는 서울까지 갔다는 사실..</del> 힘들었지만 채용의 기회기도 하고 면접을 준비하면서 느낀 답답함을 해소하고 싶었다.
회사는 구로 디지털 단지에 있었는데 공유 오피스를 사용했다. 공유 오피스도 처음 가봐서 기억에 남았다. 사무실은 전체적으로 깔끔한 이미지었다.</p>
<h3 id="영어-문제는-처음이지">영어 문제는 처음이지?</h3>
<p>처음 들어가자 마자 바로 코딩 테스트를 봤다. 절대 어려운 문제는 아니었다. 하지만 여기서 굉장히 당황했던 부분이 외국 코딩 테스트 사이트에서 문제를 풀어야 했다. 사실 영어를 아예 못하는 건 아니라서 문제가 요구하는 바는 해석을 했지만 그래서 어떤 문제인지를 알려주는 예시 부분은 해석을 못했다. 😱 그래서 어떻게 보면 문제를 이해하는 단계부터 문제가 생긴 것이다. 첫 면접에서부터 이런 상황이라 적지 않게 당황했지만 그래도 대표님께서 화이트 보드에 그림을 그려보게 하면서까지 문제를 이해시켜주셔서 다행히 문제를 코드로 풀어낼 수 있었고 테스트도 통과했었다.</p>
<p>하지만 코드의 품질을 생각하고 문제를 푼 것은 아니었기 때문에 특정 부분에 대한 코드를 개선해 보라는 두 번째 과제를 받았고, 개선을 위해 시나리오는 떠올렸으나 그 시나리오를 코드로 나타내진 못했다. 이 부분은 너무 아쉬웠다.</p>
<h3 id="회사에-대한-질문">회사에 대한 질문</h3>
<p>그 다음은 이력서를 바탕으로 문답을 했었다. 하지만 이 부분에서도 만족할 만한 답변을 못했다고 생각한다. 특히나 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">&#39;당사에 입사하게 되면 기여할 수 있는 부분은 어떤 부분이라고 생각하나요?&#39;</span> 라는 질문에는 더 그런 생각이 든다. 내가 이 회사의 서비스를 사용해보지도 못했고 어떤 서비스를 하는지도 구체적으로는 모르기 때문에 &#39;이 회사의 이런 부분에서 흥미로웠고, 그 부분에 대해 저 역시 제가 가진 어떤 스택을 가지고 기여해보고 싶습니다.&#39;라고 말을 하기가 어려웠다. 이 부분에 대해서는 앞으로도 많은 고민을 해볼 거 같다. 앞으로 다른 회사의 면접에서도 물어볼 거 같은데 이 부분에 대한 답은 오래 고민을 해야 나올 거 같다.</p>
<p>이후에는 내가 회사에 대한 질문을 하고 마무리가 되었다. 사람은 직감이란 게 있지 않은가. 내 직감에는 면접에서 충분히 보여주지 못했기 때문에 떨어질 거 같았다. 그래서 어차피 안볼 거 아까 문제에 대한 개선된 코드를 배워보고자 여쭤보았고 잘 알려주셔서 감사했다.</p>
<p>시간은 한 시간 반? 사십분? 정도 본 거 같다. 처음에 길면 2,3시간 정도 볼 거 같다고 해서 &#39;어떻게 그 시간 만큼 면접을 볼까?&#39; 하는 생각도 했지만 생각보다 금방 시간이 흘렀다. 그만큼 면접장에 들어가서 집중을 하니 시간이 금방 흘렀다.</p>
<h1 id="느낀-점">느낀 점</h1>
<p>결과적으로는 <strong>탈락했다.</strong> 오늘 탈락 메일을 받게 되었고 나 역시 그게 맞다고 생각을 한다.
그래도 느낀 점이 굉장히 많았다. 단순하게 그 사무실에 있는 개발자 분들을 직접 보면서 나도 얼른 커리어를 시작하고 싶다는 생각부터 코딩 테스트를 하면서 다양한 환경에서, 여러 가지를 고려하며 좋은 코드로 개선하는 작업도 꼭 해봐야겠다는 생각도 들었다. 또한 면접을 볼 때 어떤 질문들에 대해서는 오래 고민을 해야 하는 부분이 있고 그런 부분도 더 준비를 해야 한다는 것을 배웠다. 이것 말고도 말로 표현은 못하지만 머리와 마음으로 느낀 것들이 굉장히 많았다. 지금은 탈락했지만 오히려 앞으로 나아갈 원동력을 얻은 느낌이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 22년 41주차 - Tree 탐색방법]]></title>
            <link>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-41%EC%A3%BC%EC%B0%A8-Tree-%ED%83%90%EC%83%89%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-41%EC%A3%BC%EC%B0%A8-Tree-%ED%83%90%EC%83%89%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 16 Oct 2022 20:23:28 GMT</pubDate>
            <description><![CDATA[<p>지난 주에는 트리를 직접 코드로 구현을 해보았는데, 사실 트리를 직접 만들어 보는 것도 중요하지만 이런 자료구조를 탐색하는 방법도 굉장히 중요하다. 실제로 우리가 코딩 테스트에서 마주하는 문제는 탐색에 대한 부분이기 때문이다. 이 탐색에도 여러 방법들이 있는데 오늘은 그런 방법을 정리해 볼 생각이다.</p>
<h1 id="중위-순회">중위 순회</h1>
<p>중위 순회(In-order traversal)는 왼쪽 자식 노드, 나(Root Node), 오른쪽 자식 노드 순으로 방문을 하게 된다. 즉 나를 중간에 방문하기 때문에 중위 순회라고 생각하면 기억하기 편하다. 아래 예시 트리가 있다.
<img src="https://velog.velcdn.com/images/ferrari_roma/post/ac616ac6-eb66-4a5d-9a19-c56a9e255b53/image.png" alt=""></p>
<p>중위 순회에서는 예시 코드를 다음과 같은 순서로 탐색한다. <em>(2가 두 개인 관계로 leaf node를 2&#39;로 표현)</em>
2&#39; 👉 7 👉 5 👉 6 👉 11 👉 2 👉 5 👉 4 👉 9</p>
<p>내가 트리가 되었다고 생각을 하고 순서에 대한 상황극을 해보자.</p>
<blockquote>
<p>2: 자 지금부터 중위 순회를 해보겠어요. 보자, 일단 중위 순회는 왼쪽 자식 노드, 그리고 나, 마지막에 오른쪽 자식 노드이니까 7, 너부터 한 번 확인해 봐.
7: 네, 아버지. 2&#39;야. 할아버지 말씀 잘 들었지? 왼쪽 자식 노드인 네가 첫 순서란다.
2&#39;: 예. 제가 <strong>첫 번째</strong>군요!
7: 그래 그리고 내가 <strong>두 번째</strong>구나. 세 번째는 오른쪽 자식, 6아. 너가 이어서 하거라.
6: 예, 아버지. 근데 저는 두 자식이 있는데 어떻게 하죠?
7: 아버지가 한대로, 왼쪽 손자 5를 먼저 하고 그리고 너, 마지막에 오른쪽 손자 11을 하면 되겠구나.
5: 할아버지! 그러면 제가 <strong>세 번째</strong>인가요?
7: 그렇지!
6: 아, 그러면 제가 <strong>네 번째</strong>가 되겠군요!
11: 아빠, 저는 그럼 <strong>다섯 번째</strong>에요.</p>
</blockquote>
<p>이런 식으로 가는 것이다. 정말 순서를 딱딱 지켜서 해보면 크게 헷갈리지 않는다.</p>
<h1 id="전위-순회">전위 순회</h1>
<p>중위 순회(Pre-order traversal)는 내가 가운데 가는 방법이니 전위 순회는 바로 내가 가장 먼저 나온다는 것을 알 수 있다. 즉 나, 왼쪽 자식 노드, 오른쪽 자식 노드 순으로 탐색을 한다. 이 방법을 위 예시에 그대로 적용시켜 보자.
2 👉 7 👉 2&#39; 👉 6 👉 5 👉 11 👉 5 👉 9 👉 4</p>
<h1 id="후위-순회">후위 순회</h1>
<p>후위 순회(Post-order traversal)는 내가 가장 마지막에 나오는 방법으로, 왼쪽 노드, 오른쪽 노드, 마지막에 내 차례이다. 이 방법을 위 예시에 적용시켜 보도록 하자.
2&#39; 👉 5 👉 11 👉 6 👉 7 👉 4 👉 9 👉 5 👉 2</p>
<h1 id="레벨-순회">레벨 순회</h1>
<p>레벨 순회(Level-order traversal)는 우리가 익히 들어 본 BFS(Breadth First Search)를 말한다. 같은 레벨에 있는 노드들을 먼저 순회하는 방법이다. 이 방법을 위 예시에 적용시켜 보자.
2 👉 7 👉 5 👉 2&#39; 👉 6 👉 9 👉 5 👉 11 👉 4</p>
<h2 id="각-방법에-적합한-자료구조">각 방법에 적합한 자료구조</h2>
<p>첫 세 가지 방법은 Leaf node까지 찍고 올라오는 형태가 되기 때문에 DFS(Depth First Search) 방법이라고 할 수 있다. 그렇기 때문에 스택를 이용하면 된다. 때에 따라서는 재귀를 이용해서 이를 구현할 수 있을 것이다. 
하지만 레벨 순회는 차례대로 처리를 해주어야 하기 때문에, 큐를 이용해서 먼저 들어온 것 처리하고 다음 레벨 노드를 후 순위로 등록해 주고 그 다음 같은 레벨 노드를 탐색하고 동일하게 그 자식 노드를 후 순위로 등록해 주면서 레벨 단위로 탐색을 해 나간다.</p>
<h1 id="마치며">마치며</h1>
<p>이번 주는 지난 주의 트리 공부의 연장선상이었다. 문제를 직접 풀어보면서 &#39;이게 DFS구나, 이래서 스택과 재귀를 쓴다는 거구나!&#39;, &#39;이게 BFS구나, 이래서 큐를 사용하구나!&#39; 하는 생각을 참 많이 했다. 물론 두 문제 다 내 머리로 한 번에 풀 수 없어서 검색의 힘을 빌렸지만 해당 코드를 이해하는 것도 트리 문제가 처음인 내겐 힘들었다. 그래도 이해를 하는 순간순간이 참 즐거웠다. </p>
<p>자료구조 공부는 대부분 이런 거 같다. &#39;왜 코드가 이렇게 되는거지?&#39; 싶다가도 이해를 하는 순간 &#39;만든 사람 진짜 천재 아냐?&#39; 하는 생각과 함께 기쁨의 박수를 치게 된다. 이번에도 그랬다. 특히나 DFS는 금방 이해했지만 BFS는 진짜 쉽지 않았다. 그래도 이해를 하고 나니 이런 이유 때문에 큐를 쓴다는 것을 확실하게 이해하게 되었다.</p>
<p>아직 갈 길이 먼 만큼 얼른 복습하고 다음 문제들을 풀어보면서 트리에 더 빠져들어 봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 22년 40주차 - Tree]]></title>
            <link>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-40%EC%A3%BC%EC%B0%A8-Tree</link>
            <guid>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-40%EC%A3%BC%EC%B0%A8-Tree</guid>
            <pubDate>Mon, 10 Oct 2022 19:40:03 GMT</pubDate>
            <description><![CDATA[<p>이번 주는 자료구조 공부를 했다. 제로초님 블로그의 <em>알고리즘</em> 란에 있는 lv1~lv3 문제들을 풀어야겠다 싶어서 풀다가 DP 유형에서 탁 막혔다. 아직 Tree나 DP 유형은 그 기초조차 공부한 적이 없다. 나오면 풀어보고 모르겠으면 공부해야 겠다는 생각이었지만 막상 나오니까 &#39;모르면 손 조차 못 대는구나.&#39; 하는 생각이 들었다. 그래서 일단 막힌 김에 다시 기초를 잡아야 겠다는 생각이 들어서 일단 Tree 자료구조를 공부했다.</p>
<h1 id="너-종류가-많구나">너, 종류가 많구나?</h1>
<p>힙 트리, 이진 트리, 이진 탐색 트리 등등 트리는 종류가 다양했다. 처음엔 뭘 공부하면 좋을까 고민을 했는데, 결국엔 트리구조에서 노드를 어떻게 삽입하는지, 탐색은 어떻게 하는지는 모두 유사했다. 그래서 기본적인 것을 알게 되면 나머지는 그때그때 맞춰서 해줄 수 있는 것이었다. 그래서 일단은 이진 탐색 트리를 만들어 보았고, 이해를 어느 정도 한 이후에 다른 트리에 대해 알아보았다.</p>
<h3 id="1-이진트리">1. 이진트리</h3>
<p>이름부터가 두 개인 거 같은가? 그렇다면 이해를 다 한 것이다. 이진트리는 이름 그대로 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">최대 자식 노드 수가 두 개인 트리</span>를 말한다. 
<img src="https://velog.velcdn.com/images/ferrari_roma/post/05795bac-20d0-46a8-ae5c-7e7298f3abd8/image.png" alt="">
같은 원리로 최대 노드 개수가 3개 라면 삼진 트리라고 부른다.</p>
<h3 id="2-완전이진트리">2. 완전이진트리</h3>
<p>완전이진트리는 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">왼쪽의 빈 노드부터 차근차근 채워나가는 트리</span>를 말한다. 
<img src="https://velog.velcdn.com/images/ferrari_roma/post/846a4004-3e04-4179-be97-53924857a722/image.png" alt="">
이런 규칙 상 오른쪽 자식 노드가 있는 부모 노드는 왼쪽 자식 노드가 무조건 있다.</p>
<h3 id="3-힙-트리">3. 힙 트리</h3>
<p>힙 트리는 완전 이진 트리의 일종으로 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">부모 노드가 자식 노드들 보다 일정하게 작거나 크다</span>. 힙은 최소 힙과 최대 힙이 있다. 최소와 최대를 나누는 기준은 부모 노드가 자식 노드들보다 큰 지, 작은 지이다. 
<img src="https://gmlwjd9405.github.io/images/data-structure-heap/types-of-heap.png"></img>
이런 특징 때문에 힙 트리는 반 정렬된 상태이다.</p>
<h4 id="3-1-최소-힙">3-1. 최소 힙</h4>
<p>최소 힙은 부모 노드보다 자식 노드들의 값이 클 때를 말한다.</p>
<h4 id="3-2-최대-힙">3-2. 최대 힙</h4>
<p>최대 힙은 부모 노드보다 자식 노드들의 값이 작을 때를 말한다.</p>
<h3 id="4-이진탐색트리">4. 이진탐색트리</h3>
<p>이진 탐색 트리는 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">왼쪽 자식 노드는 부모 노드 보다 작고, 오른쪽 자식 노드는 부모 노드 보다 크다는 규칙이 있다</span>.
<img src="https://velog.velcdn.com/images/ferrari_roma/post/2c0304b6-93c8-429c-9cca-c8945756fbbf/image.png" alt=""></p>
<h1 id="클래스로-만들어-본-이진탐색트리">클래스로 만들어 본 이진탐색트리</h1>
<p>이진탐색트리는 조건이 있다보니 insert를 할 때 재귀를 통해서 계속 기존 노드들과 값을 비교해 내려가야 한다. 그런 부분에서 어쩌면 가장 까다롭다고 생각했고 그래서 이진탐색트리로 공부를 해두면 다른 트리도 쉽게 구현할 수 있으리라 생각을 했다.</p>
<p>아래는 class로 만들어 본 이진탐색트리이다.</p>
<pre><code class="language-js">class Node {
  constructor(data) {
    this.data = data;
    this.right = null;
    this.left = null;
  }
  // data가 해당 노드의 데이터 보다 작으면 왼쪽에 삽입을 하는 private method를 호출하고 아니면 오른쪽에 삽입을 하는 private method를 호출하면 된다.
  insert(data) {
    return data &lt;= this.data ? this._insertLeft(data) : this._insertRight(data);
  }
  _insertRight(data) {
    return this.right ? this.right.insert(data) : (this.right = new Node(data));
  }
  _insertLeft(data) {
    return this.left ? this.left.insert(data) : (this.left = new Node(data));
  }
  // find를 했을 때 찾는 데이터가 노드의 데이터 보다 작으면 왼쪽으로 탐색하는 private method를 호출하고 크면 오른쪽으로 탐색하는 private method를 호출한다.
  find(data) {
    if (this.data === data) return this;
    return data &lt;= this.data ? this._findLeft(data) : this._findRight(data);
  }
  _findRight(data) {
    return this.right ? this.right.find(data) : null;
  }
  _findLeft(data) {
    return this.left ? this.left.find(data) : null;
  }
}</code></pre>
<p>이진탐색트리는 규칙이 명확하기 때문에 탐색시간이 길어 봤자 트리의 깊이, 즉 O(logn)이다. </p>
<h1 id="마무리">마무리</h1>
<p>트리도 내가 예전부터 공부해보려고 했던 부분인데 어찌 계속 손이 안가서 못하고 있었다가 이번에 하게 되었다. 생각해보면 C언어로 연결리스트와 스택을 공부한 뒤로 처음으로 공부하는 자료구조이다. JS로는 처음 공부하는 자료구조였다. 얼마나 그 동안 안한거야.. 반성을 해본다😢
사실 이런 기본적인 구현도 중요하지만 실질적으로 문제를 풀 수 있는 BFS, DFS도 중요하기 때문에 이제 시작이라 생각하고 계속 공부하고 있다.</p>
<p><em>참고자료</em>
<a href="https://jomuljomul.tistory.com/entry/%EC%99%84%EC%A0%84%EC%9D%B4%EC%A7%84%ED%8A%B8%EB%A6%ACComplete-Binary-Tree%EB%9E%80">https://jomuljomul.tistory.com/entry/%EC%99%84%EC%A0%84%EC%9D%B4%EC%A7%84%ED%8A%B8%EB%A6%ACComplete-Binary-Tree%EB%9E%80</a>
<a href="https://gmlwjd9405.github.io/2018/05/10/data-structure-heap.html">https://gmlwjd9405.github.io/2018/05/10/data-structure-heap.html</a>
<a href="https://velog.io/@kimkevin90/Javascript%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-tree-%EA%B5%AC%ED%98%84">https://velog.io/@kimkevin90/Javascript%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-tree-%EA%B5%AC%ED%98%84</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 22년 39주차 - JS의 Class]]></title>
            <link>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-39%EC%A3%BC%EC%B0%A8-JS%EC%9D%98-Class</link>
            <guid>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-39%EC%A3%BC%EC%B0%A8-JS%EC%9D%98-Class</guid>
            <pubDate>Sun, 02 Oct 2022 14:34:09 GMT</pubDate>
            <description><![CDATA[<p>이번 주는 내가 그토록 보고 싶었던 Class파트 인강을 봤다. 나는 기본적으로 inflearn 김영보 선생님의 JS강의를 보고 있는데, Class는 가장 마지막 부분에 비동기와 함께 위치하고 있다. 그래서 이제서야 그곳에 도착한 것이다!
이때까지는 Class을 사용해야 할 때 마다 Javascript tutorial과 같은 사이트에서 찾아봤는데, 시간이 지나면서 이전에 느꼈던 지식의 파편화를 느꼈다. 필요한 부분을 위주로 읽다 보니, &#39;저기서 저렇고 여기서 이렇고&#39;하는 정보들이 머리에서 뒤섞인 것이다. 그 상태에서 최근에 Typescript강의에서 interface를 이야기 하면서 abstact Class에 대해서 이야기를 하면서 그 상태가 절정에 달했다!</p>
<p>이번에 Class 인강을 본 것은 그 파편들을 정리하는 과정이었다. &#39;이래서 그때 그게 안된 거구나!&#39;하는 느낌이 몇 번 들었다. 오늘은 확실한 정리를 위해서 WIL에서 Class에 대해 다시 한 번 정리하는 시간을 가져볼까 한다. 여러 글에서 기본적인 Class에 대한 사용에 대해서 잘 설명을 해두었기 때문에, 나는 내가 Aha-Point라고 생각한 부분을 기준으로 작성해볼까 한다.</p>
<h1 id="oopobject-oriented-programming">OOP(Object-Oriented Programming)</h1>
<p>객체 지향 프로그래밍을 지칭하는 말인데, 보통은 Class를 객체 지향의 대표적인 예라고 한다. 김영보 선생님 역시 그렇게 소개를 하면서 한 가지를 강조하셨다.</p>
<blockquote>
<p>Class가 객체지향이라기 보다는 OOP를 Class를 통해서 구현한다는 표현이 더 정확하다.</p>
</blockquote>
<p>당연한 이야기이지만 Class에 집중하다 보면 그 주객이 전도되기 쉬운데, 이 말을 듣고서는 그 시선에서 강의를 들을 수 있었다.</p>
<p>이 시선에서 객체 지향이 뭘까에 대한 답을 생각해 보자!
Object가 있어서 JS의 프로퍼티들로 구성되어 있는 Object를 떠올릴 수 있지만, 이 Object는 그 Object가 아니다. OOP의 Object는 개념적인 것이다. 행위와 속성으로 객체의 특성을 표현하는 방법인데, 아래와 같은 문장으로 예를 들어보자.</p>
<blockquote>
</blockquote>
<p>쌀밥을 먹다.
주꾸미를 먹다.
삼겹살을 먹다.
치토스를 먹다.</p>
<p>여기서 <strong>행위</strong>는 &#39;먹다&#39;는 것이고 <strong>속성</strong>은 &#39;쌀밥&#39;, &#39;주꾸미&#39;, &#39;삼겹살&#39;, &#39;치토스&#39;이다. &#39;먹다&#39;라는 공통의 행위를 나타내는 네 개의 다른 문장이지만 각각의 속성이 다른 것이다.</p>
<p>이것을 코드로 나타낸다면 객체가 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">클래스</span>이고, &#39;먹다&#39;를 나타내는 행위는 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">메소드</span>이고, 각각의 음식들은 
<span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">프로퍼티</span>가 되는 것이다.</p>
<p>즉 객체지향이라는 개념적인 것들을 클래스, 메소드, 프로퍼티로 구현한 것이다.</p>
<h3 id="잊지말자-oop">잊지말자 OOP!</h3>
<p>JS도 ECMA Script의 스펙에 OOP라고 작성이 되어있는 <strong>엄연한 OOP 프로그래밍 언어</strong>이다. 
하지만 나 뿐만 아니라 많은 초보 JS 개발자들은 이 사실 자체를 잊고 공부를 하는 경우가 매우 잦다. 그 이유는 여러 가지겠지만 다음 두 가지가 가장 큰 이유인 것 같다.</p>
<blockquote>
</blockquote>
<ol>
<li>JS 라이브러리, 프레임워크들에서 Class를 잘 사용하지 않아서</li>
<li>Class는 단순히 sugar문법이라는 의견이 있어서 </li>
</ol>
<p>실제로 자주 쓰인다면 다들 배우겠지만 확실히 JS에서 Class를 타 언어들 보다 잘 안 쓰는 것은 맞다. </p>
<p>하지만 JS는 OOP이고 OOP를 구현하는 대표적인 것이 Class라는 것, 그리고 Class는 sugar문법이 아니라 앞으로 Class를 더 활용하게 될 것이라는 추세적인 부분까지 고려해서 추가되었다고 볼 수 있다는 점이 Class를 배워야 할 이유인 거 같다.</p>
<h3 id="js라는-특징을-살린-class">JS라는 특징을 살린 Class</h3>
<p>다른 언어와 OOP의 개념은 동일하지만 클래스의 구조와 구현 방법이 다르다. property에 메소드를 연결하는 구조가 그 대표적인 예이다. 하지만 선생님은 결코 다른 언어와 비교할 필요가 없다고 한다. 애시당초 언어가 다르고 그 언어의 구조가, 클래스의 구조가 다르기 때문이다. 오히려 JS의 특징인 prototype을 잘 살린 부분이다.</p>
<h1 id="아파트를-지어보자">아파트를 지어보자</h1>
<p>이번 강의에서는 객체 지향의 중요한 부분인 상속에 대해서 아파트에 비유를 했다. 지금 생각해보면 prototype을 통해서 계층적으로 이루어진 JS Class의 특징을 잘 살린 비유인 것 같다.</p>
<pre><code class="language-js">// 슈퍼 클래스
class Book {
  set setTitle(title) {
    this.title = title;
  }
}
// 서브 클래스
class CallTitle extends Book {
  get getTitle() {
    return this.title;
  }
}
// 인스턴스 생성
const instance = new CallTitle();
// setter로 인스턴스 프로퍼티 할당
instance.setTitle = &quot;Class배워보기&quot;;
// getter로 인스턴스 프로퍼티 읽기
console.log(instance.getTitle);</code></pre>
<p>Class에서도 getter, setter를 일반 객체처럼 사용할 수 있다는 것을 보여주기 위해서 위와 같이 예제 코드를 작성해 보았다. 위 코드가 어떻게 구성되어 있는지 엔진에서 살펴보도록 하자.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/3d21c84b-5ecf-48d5-80e0-9c7da18fcffa/image.png" alt=""></p>
<p>엔진을 보면 서브 클래스(자식 클래스)인 CallTitle의 prototype에는(1층) getTitle이 자리잡고 있다. 그리고 한 단계 더 내려간 [[Prototype]](2층)에는 슈퍼 클래스(부모 클래스)인 Book의 메소드인 setTitle이 자리잡고 있는 것을 확인할 수 있다.
이처럼 상속을 하면 서브 클래스의 [[Prototype]]가 슈퍼 클래스의 메소드들을 참조하게 된다. 이런 식으로 계층을 이루면서 1층과 2층을 모두 사용할 수 있게 되는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/d7db3779-caa6-455b-ac1b-b062bb01cc62/image.png" alt="">
이렇게 상속을 받게 된 CallTitle로 인스턴스를 생성해서 할당한 instance를 엔진에서 살펴보면 위와 같은 구조를 가진다. 매우 잘 계층화된 구조를 확인할 수 있다.
추가적으로 여기서 title처럼 instance에 바로 설정되는 프로퍼티들을 <strong>인스턴스 프로퍼티</strong>라고 부른다.</p>
<h3 id="super">super</h3>
<p>super키워드를 사용하면 슈퍼 클래스의 메소드를 호출할 수 있다. 부모 클래스라고 하는 것 보다 슈퍼 클래스라고 표현하는 것에 익숙해 진다면 super 키워드의 쓰임에 대해서는 자연스럽게 다가올 것이다.</p>
<pre><code class="language-js">class Book {
  get getTitle() {
    return &quot;Super Class 배우기&quot;;
  }
}

class CallTitle extends Book {
  get getTitle() {
    const super__title = super.getTitle; // super키워드를 통해서 슈퍼 클래스에 접근
    console.log(super__title); // console :: Super Class 배우기
    return &quot;Sub Class 배우기&quot;;
  }
}
const instance = new CallTitle();

console.log(instance.getTitle); // console :: Sub Class 배우기</code></pre>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/adb64dfd-955b-46ab-86d3-f66f97c7ddd9/image.png" alt=""></p>
<p>원래 instance.getTitle이라고 하면 &quot;Sub Class 배우기&quot;만 출력이 돼야 하지만 super 키워드를 통해서 슈퍼 클래스의 getTitle 메소드도 호출하고 있기 때문에 &quot;Super Class 배우기&quot;도 출력할 수 있다.</p>
<h1 id="contructor">contructor</h1>
<p>이 강의를 듣고 이해하기 전에 가장 어지러웠던 constructor. 이와 더불어 this에 대해서 알아볼까 한다. <del>벌써 어지럽다.</del></p>
<pre><code class="language-js">class Book{
  constructor(title){
    this.title = title;
  }
}
const obj = new Book(&quot;Class배워보기&quot;);</code></pre>
<p>여기서 엔진은 Class 키워드를 만나면 function 키워드를 만난 것처럼 객체를 만든다. 그리고 그 prototype에 메소드를 연결하고 프로퍼티들도 설정한다. 그런데 new 연산자로 인스턴스를 생성한다면 엔진은 어떻게 작동할까?</p>
<blockquote>
</blockquote>
<ol>
<li>기존의 JS에서 작동했듯이 new 연산자를 통해서 Point 클래스 오브젝트의 constructor를 호출해 준다. </li>
<li>함수에서 파라미터를 넘겨주는 것처럼 파라미터 값을 constructor로 넘겨준다. 이렇게 되면서 title 파라미터 값은 &quot;Class배워보기&quot;가 된다.</li>
<li>엔진은 빈 오브젝트{}를 생성하게 되는데 이것이 <strong>인스턴스</strong>이다. 이렇게 빈 객체를 만든 이후에 프로퍼티 이름과 값을 설정하여 인스턴스 구조를 만들게 된다.(prototype.[[Prototype]]과 같은 구조를 말함)</li>
<li>이 다음에 <strong>constructor을 실행</strong>하게 된다. 인스턴스에서 this는 인스턴스를 참조한다는 사실을 알고 있을 것이다. 여기서도 동일하다. 이미 인스턴스는 만들어져 있기 때문에 this가 인스턴스를 참조할 수 있다.</li>
<li>위에서 title 파라미터 값은 &quot;Class배워보기&quot;이기 때문에 title 프로퍼티 값은 &quot;Class배워보기&quot;가 된다.</li>
</ol>
<p>이런 과정을 거쳐서 Class를 통해서 인스턴스가 만들어 진다. </p>
<h3 id="constructor를-작성해-주지-않은-경우">constructor를 작성해 주지 않은 경우</h3>
<p>constructor가 없는 경우도 왕왕 있다. 하지만 ES5에서는 이런 경우가 일반적이었다. 왜냐하면 ES5까지는 constructor를 작성할 수 없었기 때문이다. ES6부터는 작성할 수 있게 되면서 Class를 작성할 때 인스턴스 프로퍼티를 작성하여 초깃값을 설정하는 부분에서 constructor를 사용할 수 있게 되었다. 다시 본론으로 돌아가서 <strong>constructor가 없다면 엔진은 무엇을 호출할까?</strong></p>
<p>엔진이 Class키워드를 만나서 클래스 오브젝트를 생성할 때 기본적으로 constructor는 클래스 전체를 참조하도록 환경을 만든다. 그래서 constructor를 작성하지 않는다면 디폴트로 설정된 prototype.constructor를 사용하기 때문에 인스턴스를 생성할 수 있다. ES6부터는 constructor를 작성할 수 있다고 했는데 constructor를 작성하게 되면 디폴트로 Class를 전체를 참조하는 prototype.constructor를 오버라이드 하게 된다.</p>
<h2 id="상속에서의-constructor">상속에서의 constructor</h2>
<p>나는 상속을 받는 상황에서 cosntructor가 조금 더 복잡하게 다가왔다. 어떤 경우에는 앞서 살펴본 super 키워드를 통해서 서브 클래스에서 this를 바인딩 해줘야 하고 어떤 경우는 그냥 써도 되었기 때문이다.
이런 부분에 대해서는 경우를 나누어 생각을 해주면 이해가 더 편했다.</p>
<blockquote>
</blockquote>
<ol>
<li>슈퍼 클래스와 서브 클래스 모두 constructor가 없는 경우</li>
<li>슈퍼 클래스만 constructor가 있는 경우</li>
<li>서브 클래스만 constructor가 있는 경우</li>
<li>슈퍼 클래스와 서브 클래스 모두 constructor가 있는 경우</li>
</ol>
<h3 id="슈퍼-클래스와-서브-클래스-모두-constructor가-없는-경우">슈퍼 클래스와 서브 클래스 모두 constructor가 없는 경우</h3>
<p>이렇게 되면 default constructor가 호출된다. super를 설명하면서 쓴 예시를 다시 한 번 확인해보자.</p>
<pre><code class="language-js">class Book {
  get getTitle() {
    return &quot;Super Class 배우기&quot;;
  }
}

class CallTitle extends Book {
  get getTitle() {
    const super__title = super.getTitle;
    console.log(super__title);
    return &quot;Sub Class 배우기&quot;;
  }
}
const instance = new CallTitle();

console.log(instance.getTitle);</code></pre>
<p>이 예시에서는 Book, CallTitle 모두 constructor가 없다. 이렇게 되면 엔진은 디폴트로 설정된, CallTitle 전체를 참조하는 constructor를 호출하게 되고 이어서 Book 클래스의 constructor를 호출하게 된다. 하지만 Book도 constructor가 작성되어 있지 않기 때문에 디폴트로 설정된, Book 전체를 참조하는 constructor가 호출된다.
즉, 이 경우는 각 constructor를 모두 호출하게 된다. 이렇게 하는 이유는 instance를 생성하기 위해서는 prototype의 constructor를 호출해야 하는데, 상속을 받는 경우에는 상위 클래스의 메소드까지 계층적으로 [[Prototype]]에 연결해야 하기 때문에 모든 constructor를 호출해 주는 것이다.</p>
<h3 id="슈퍼-클래스만-constructor가-있는-경우">슈퍼 클래스만 constructor가 있는 경우</h3>
<p>이런 경우에는 파라미터를 슈퍼로 넘겨주게 된다.</p>
<pre><code class="language-js">class Book {
  constructor(title) {
    this.title = title;
  }
}

class CallTitle extends Book {}
const instance = new CallTitle(&quot;Class 배우기&quot;);</code></pre>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/1a7b3c4a-4508-45d8-b1db-0979229a99cd/image.png" alt=""></p>
<p>인스턴스를 만들 때, 서브 클래스의 constructor가 호출되고 이어서 슈퍼 클래스의 constructor가 호출되는데 이때 파라미터 값을 슈퍼 클래스의 constructor의 파라미터로 넘겨준다. 인스턴스의 인스턴스 프로퍼티로 파라미터 값이 할당된 모습이다.</p>
<h3 id="서브-클래스만-constructor가-있는-경우">서브 클래스만 constructor가 있는 경우</h3>
<pre><code class="language-js">class Book {}

class CallTitle extends Book {
  constructor(title) {
    this.title = title; // Uncaught ReferenceError: Must call super constructor in derived class before accessing &#39;this&#39; or returning from derived constructor
  }
}
const instance = new CallTitle(&quot;Class 배우기&quot;);</code></pre>
<p>에러가 발생한다!! <strong>서브 클래스에만 constructor가 있는 경우는 있을 수 없다. 결코!!</strong></p>
<h3 id="슈퍼-클래스와-서브-클래스-모두-constructor가-있는-경우">슈퍼 클래스와 서브 클래스 모두 constructor가 있는 경우</h3>
<p>이 경우가 super()를 호출해야 하는 경우이다. </p>
<pre><code class="language-js">class Book {
  constructor(price) {
    this.price = price;
  }
}

class CallTitle extends Book {
  constructor(price, title) {
    super(price);
    this.title = title;
  }
}
const instance = new CallTitle(10000, &quot;Class 배우기&quot;);</code></pre>
<p><img src="https://velog.velcdn.com/images/ferrari_roma/post/c8c8ba70-5b84-4e62-b78a-d34c8833ef72/image.png" alt=""></p>
<p>이렇게 서브의 constructor에서 this를 사용하려면 this를 사용하기 전에 명시적으로 슈퍼의 constructor를 super()로 호출해야 한다.
이 이유는 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">&#39;this는 인스턴스를 참조한다&#39;</span>는 사실과도 연결이 된다!</p>
<p>서브 클래스의 constructor 안에서 super()로 Book클래스를 호출해 주기 전에는 <strong>Book 클래스의 인스턴스가 형성되지 않은 상태</strong>이기 때문에 this를 사용할 수가 없다. super()를 이용해서 먼저 호출해 주어 Book 클래스의 인스턴스를 생성해 주어야 this.title의 this를 생성할 수 있는 것이다.</p>
<p>이 부분이 내가 가장 어려워 했던 부분이었는데 ECMA Script를 기반으로 JS를 설명해 주니 굉장히 쉽게 이해할 수 있었다.</p>
<h1 id="마무리">마무리</h1>
<p>나 역시 Class를 잘 안 쓰는 사람 중 한 명이었다. 어려워서도 있지만 잘 몰라서가 훨씬 컸다. 하지만 nestJS에서도 그렇고 JS나 TS에서 더 생산성 있는 코딩을 하기 위해서는 Class는 반드시 거쳐야 하는 관문이었다. JS를 시작한 지도 거의 1년이 다 되어 가는데 드디어 그 관문을 거친 것 같아서 매우 기쁘다. 물론 잘 사용하는 것은 지금부터의 내 노력이지만 말이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 22년 38주차 - https]]></title>
            <link>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-38%EC%A3%BC%EC%B0%A8-https</link>
            <guid>https://velog.io/@ferrari_roma/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-22%EB%85%84-38%EC%A3%BC%EC%B0%A8-https</guid>
            <pubDate>Sun, 25 Sep 2022 17:55:04 GMT</pubDate>
            <description><![CDATA[<p>이번 주는 모의면접을 준비하면서 CS 공부를 많이 했다. 혼자하기 보다는 하나의 주제를 정해서 서로 발표를 하는 기회가 있으면 더 좋을 거 같아서 스터디를 만들었다. 내가 맡은 부분은 https였다. 공부를 하다 보니까 이전에 대칭키, 비대칭키에 대한 것도 다시 공부해 볼 수 있었다. 오늘은 공부한 내용을 정리해 보도록 하겠다.</p>
<h1 id="https">https?</h1>
<h3 id="https-전에-http">https 전에 http</h3>
<blockquote>
<p>O, 이게 숫자 0, 한글 ㅇ, 영문자 O 중 무얼 나타내는지를 알려면 이게 한글인지, 영어인지, 숫자인지를 알아야 한다. 
<em>* *얄팍한 코딩사전</em> **</p>
</blockquote>
<p>위 예시처럼 http 역시 인터넷에서 우리가 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">어떤 방식으로 소통을 할 건지에 대한 약속</span>이다. headers와 body가 있고 거기에는 뭐가 들어가는지에 대한 약속인 것이다. 이걸 url 앞에 http라고 써주면서 소통한다고 명시해 주는 것이다.</p>
<p>http의 단점은 암호화가 되지 않는다는 것이다. 우리가 로그인을 한다고 한다면 서버에 id와 pw가 전달될 탠데, http를 통해서 서버와 통신을 하면 중간에서 제 3자가 탈취했을 때 내 id, pw가 그대로 노출된다.</p>
<h3 id="http--secure--https">http + secure = https</h3>
<p>https는 보안이 되는 http이다. 두 가지 부분에서 보안이 된다고 할 수 있다.</p>
<blockquote>
</blockquote>
<ol>
<li>http처럼 id, pw가 그대로 서버에 전달되는 것이 아니라 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">암호화</span>가 되어서 서버에 전달해 준다. </li>
<li>해당 사이트가 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">믿을 만한 사이트인지</span>를 보장해 준다.</li>
</ol>
<p>첫 번째는 바로 이해하겠지만 두 번째 부분은 피싱 사이트를 예로 들 수 있다. 구글이 아니라 구굴이라는 피싱 사이트에 우리가 접속했는데 그게 피싱 사이트인지 모르고 로그인을 하면 그대로 개인정보가 노출된다. 하지만 피싱 사이트인지를 미리 알 수 있다면 이런 사고를 예방할 수 있다.
<img src="https://velog.velcdn.com/images/ferrari_roma/post/437ec280-fe2c-4b14-b6db-82a1d881a0be/image.png" alt=""></p>
<p>http 사이트로 접속하거나 https가 제대로 작동되지 않는 사이트에 접속을 하면 url 창에는 위와 같은 경고 메시지가 뜬다. 이것을 통해서 이 사이트가 신뢰할 만한 사이트인지를 알 수 있는 것이다.</p>
<p>하지만 여기서 의문이 들 수 있다. ** &quot;어떻게 이게 그 사이트의 신뢰성을 보장해 주지?&quot; **</p>
<h1 id="대칭키-비대칭키">대칭키, 비대칭키</h1>
<p>여기서 알아야 하는 것이 대칭키와 비대칭키이다. 다음 스텝에서 설명할 CA와 SSL를 이해하기 위해서는 이 두 가지 종류의 키를 꼭 알아야 한다.</p>
<h3 id="대칭키">대칭키</h3>
<p>먼저 대칭키를 알아보자. 예를 들기 위해서 영문자와 숫자로 대칭되는 암호화 테이블을 만들어 보자.</p>
<blockquote>
</blockquote>
<p>1 = A
2 = P
3 = L
4 = E</p>
<p>위와 같은 규칙이 있다면, apple이라는 단어를 12234라고 암호화를 할 수 있다. 이 암호를 주고 받는 상대도 같은 암호화 테이블을 가지고 있다면 12234를 복호화 해서 apple이라는 걸 알 수 있다. </p>
<p>대칭키는 이와 같은 방식이다. 서로 같은 키를 가지고 그 키를 넣고 일련의 알고리즘을 거치면서 암호화와 복호화가 되는 것이다. <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">A키로 암호화 된 것은 A키로만 복호화를 할 수 있다는 것이다.</span></p>
<p>대칭키 방법은 키가 탈취만 되지 않으면 안전한 방식이다. 하지만 최초에 통신을 할 때 서로 키를 주고 받는 과정에서 탈취가 당해버린다면 제 3자도 중간에서 통신 내용을 확인할 수 있다.</p>
<h3 id="비대칭키">비대칭키</h3>
<p>이런 방식을 보완하기 위해서 1970년대 훌륭한 수학자들이 고안한 방식이 비대칭키 방식이다. 비대칭키라는 이름대로 이 방식은 키가 두 개 있다. 하나는 서버에 숨겨져 있어서 개인키라고 부르고, 다른 하나는 유저들에게 공개하기 때문에 공개키라고 부른다. 그래서 비대칭키는 공개키 방식이라고도 불린다.</p>
<p>대칭키와 다르게 비대칭키는 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">A키로 암호화를 하면 B키로만 복호화를 할 수 있다.</span> 반대로 B키로 암호화를 하면 A키로만 복호화를 할 수 있다. 그렇기 때문에 유저가 공개키로 암호화를 해서 서버에 보낼 때 서버 안에 있는 개인키로만 복호화를 할 수 있기 때문에 누군가 탈취를 한다고 해도 내용을 볼 수가 없다. 비대칭키의 방식은 대칭키 보다 더 복잡하기 때문에 컴퓨터의 자원을 훨씬 더 많이 소모한다.</p>
<p>이렇게 비대칭키를 통해서 위에서 말한 첫 번째 요소를 충족한다고 하면 두 번째 요소, 이 사이트가 신뢰할 만한 사이트라는 것은 또 어떻게 증명할 수 있을까? 이 부분을 해결해 주는 것이 CA와 SSL이다.</p>
<h1 id="http에-ca의-ssl을-더해-구현되는-https">HTTP에 CA의 SSL을 더해 구현되는 HTTPS</h1>
<p>CA는 certification authority, 즉 인증기관을 말한다. CA는 엄격한 과정을 거쳐서 인증이 된 기관인데 이 기관이 하는 일은 믿을 만한 회사들에게 SSL인증서를 주는 것이다. 이런 간단한 정보를 바탕으로 어떤 순서로, 그리고 어떤 방식으로 비대칭키를 써서 https 통신이 이루어 지는지를 알아보자.</p>
<h3 id="저-진짭니다-진짜라고요">저 진짭니다. 진짜라고요!</h3>
<p>네이버나 구글 같은 곳은 CA에서 진짜라고 인증을 해준다. 그런 인증을 SSL인증서를 통해서 하는데 이 인증서도 <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">CA서버의 개인키로 암호화</span>가 되어있다. 이 인증서에는 네이버나 구글의 공개키가 들어있다.</p>
<h3 id="똑똑똑-공개키-왔습니다">똑똑똑, 공개키 왔습니다.</h3>
<p>최초에 클라이언트와 서버가 통신을 할 때 먼저 핸드 셰이크를 한다. 클라이언트에서는 랜덤한 문자열을 생성해서 서버로 넘기면, 서버 역시 랜덤한 문자열과 함께 이 SSL인증서를 클라이언트에 전달해 준다.</p>
<h3 id="얘-진짜야">얘 진짜야?</h3>
<p>브라우저에는 CA 공개키가 내장되어 있다. 이 CA 공개키로 SSL인증서가 풀린다면, 해당 사이트는 인증된 기관이라는 것을 증명함과 동시에 해당 사이트의 공개키를 유저에게 전달할 수 있다.</p>
<p>위 과정을 통해서 클라이언트와 서버는 안전하게 비공개키를 주고 받을 수 있음과 동시에 인증된 사이트라는 것을 증명할 수 있다.</p>
<h1 id="매번-비대칭키를-쓸-순-없다">매번 비대칭키를 쓸 순 없다.</h1>
<p>하지만 앞서 말했다시피 이렇게 비대칭키 방식은 컴퓨터의 자원을 더 많이 소모한다고 했다. 특히나 우리가 간단하게 서버와 통신을 한다고 해도 주고 받는 기본적인 string이 꽤 있는데, 이걸 매번 비대칭키로 암호화와 복호화를 한다고 하면 컴퓨터가 힘들어 할 것이다. <span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">그래서 보통은 대칭키를 통해서 암호화를 한다. </span></p>
<h3 id="대칭키는-탈취-당하면-소용-없잖아요">대칭키는 탈취 당하면 소용 없잖아요?</h3>
<p><span style="background-color:rgba(0,0,0,0.2);padding:0.2rem;font-size:1rem;border-radius:5px">그래서 대칭키를 주고 받을 때 비대칭키를 사용한다.</span> 이렇게 듣고 나면 무슨 소리인지 이해가 안될 것이다. 
우리가 맨 처음에 핸드 셰이크를 할 때 랜덤한 문자열을 클라이언트와 서버 양 측에서 생성해서 주고 받았었다. 클라이언트에서는 이제 이 두 문자열을 혼합해서 어떤 임시 키를 만든다. 
그리고 이 키를 네이버, 구글의 공개키를 이용해서 암호화를 한 후 서버로 보내면 양쪽에서는 일련의 과정을 거쳐서 대칭키를 만든다. 이렇게 대칭키를 공개키를 통해 암호화 해서 주고 받으면 중간에 탈취된다고 해도 그 대칭키가 노출될 일이 없다.</p>
<p>즉, 결과적으로 대칭키와 비대칭키는 양쪽의 취약점을 서로가 보완하는 형식으로 https를 구현하는 것이다.</p>
<h1 id="마치며">마치며</h1>
<p>사실 예전에도 얄팍한 코딩사전님의 영상을 통해서 https에 대한 공부를 했었다. 이걸 본다고 끝은 아니지만 큰 그림은 그린 느낌? 그때도 보고서 &#39;음.. 그렇구나&#39; 정도였지 남에게 설명할 수 있는 정도까진 아니었다. 하지만 이번에 복습을 하면서 다양한 레퍼런스도 찾아보고 정리해 보면서 https에 대해 더 깊게 이해할 수 있게 됐다.</p>
<p><strong>참고자료</strong>
<a href="https://www.youtube.com/watch?v=H6lpFRpyl14">https://www.youtube.com/watch?v=H6lpFRpyl14</a>
<a href="https://www.digicert.com/kr/what-is-ssl-tls-and-https">https://www.digicert.com/kr/what-is-ssl-tls-and-https</a>
<a href="https://velog.io/@2dh2wdk/%EC%95%94%ED%98%B8%ED%99%94-%EB%B0%A9%EC%8B%9DAES-SHA">https://velog.io/@2dh2wdk/%EC%95%94%ED%98%B8%ED%99%94-%EB%B0%A9%EC%8B%9DAES-SHA</a>
<a href="https://velog.io/@jaymee/SSL">https://velog.io/@jaymee/SSL</a>
<a href="https://velog.io/@moonyoung/HTTPS%EC%9D%98-%EC%9B%90%EB%A6%AC">https://velog.io/@moonyoung/HTTPS의-원리</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 위장]]></title>
            <link>https://velog.io/@ferrari_roma/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9C%84%EC%9E%A5</link>
            <guid>https://velog.io/@ferrari_roma/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9C%84%EC%9E%A5</guid>
            <pubDate>Thu, 22 Sep 2022 13:38:52 GMT</pubDate>
            <description><![CDATA[<p>오늘은 프로그래머스 위장 문제를 풀었다. 20분 타이머를 제고 했는데 결국에는 넘겼다.. 아니 다 푼 거 같은데 진짜 마지막 경우의 수로 해결하는 부분에서 아이디어가 떠오르지 않아서 결국엔 희정님께 물어봤다. 
<a href="https://school.programmers.co.kr/learn/courses/30/lessons/42578">프로그래머스 - 위장</a></p>
<h1 id="핵심은-map-오브젝트">핵심은 Map 오브젝트</h1>
<p>사실 처음부터 Map을 써야겠다는 생각이 들었다. Map에 있는 has, get, set과 같은 여러 좋은 메소드들을 이용하면 쉽게 풀 수 있을 거 같다는 생각을 했기 때문이다.
일단 두 부분으로 나누어 생각을 했다.</p>
<blockquote>
</blockquote>
<ol>
<li>의상 종류에 따라 갯수를 파악한다.</li>
<li>갯수를 바탕으로 경우의 수를 계산한다.</li>
</ol>
<p>Map은 이 첫 번째 부분에서 굉장히 유용했다. [[&quot;의상종류&quot;,&quot;갯수&quot;]...]로 clothes배열을 가공해서 has메소드를 이용해서 의상 종류가 있으면 갯수를 하나 더 해주고 없으면 갯수를 1로 해서 새로 set해주었다.
그렇게 아래와 같은 코드가 나왔다. </p>
<p>마지막에 values메소드로 의상 종류에 따른 갯수까지 추출해두었기 때문에 정말로 다 푼 줄 알았다...</p>
<pre><code class="language-js">function solution(clothes) {
  const setMap = new Map([]);
  const clothesArr = clothes;
  // 각각의 의상 종류가 몇 개 있는지 확인
  clothesArr.map((el) =&gt;
    setMap.has(el[1])
      ? setMap.set(el[1], setMap.get(el[1]) + 1)
      : setMap.set(el[1], 1)
  );
  const countResult = setMap.values();
}</code></pre>
<h1 id="경우의-수는-어려워">경우의 수는 어려워..</h1>
<p>저렇게 풀고 10분이 흘렀는데 어떻게 해야 할지 전혀 감이 잡히질 않았다. 메모지에 경우의 수를 계산해보면서 머리를 굴려보았지만 생각이 나질 않았다. 구글링을 해볼까 하던 찰나 희정님께 물어봤는데 다행히도 너무 잘 알려주셔서 쉽게 이해를 할 수 있었다. 
아래 코드를 통해서 확인해보자.</p>
<pre><code class="language-js">function solution(clothes) {
  const setMap = new Map([]);
  const clothesArr = clothes;
  // 각각의 의상 종류가 몇 개 있는지 확인
  clothesArr.map((el) =&gt;
    setMap.has(el[1])
      ? setMap.set(el[1], setMap.get(el[1]) + 1)
      : setMap.set(el[1], 1)
  );
  const countResult = setMap.values();

  let answer = 1;
  for (let a of countResult) {
    // 그 의상 종류를 입지 않았을 때를 추가해주어야 한다.
    answer *= a + 1;
  }
  // 아무런 의상을 입지 않은 경우를 하나 빼주어야 한다.
  return answer - 1;
}</code></pre>
<p>입지 않은 경우의 수를 포함해야 하기 때문에 바지가 2개라면 2를 곱해주는 것이 아니라 입지 않는 경우를 더해 3을 곱해주어야 하는 것이다. 그리고 의상을 아무것도 입지 않는 경우가 하나 있기 때문에 마지막에 그 경우를 하나 빼주어야 했다. 이 부분을 생각해내지 못해서 20분을 헤매다니ㅜㅜ;</p>
]]></description>
        </item>
    </channel>
</rss>