<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jess_apr.log</title>
        <link>https://velog.io/</link>
        <description>주니어 프론트엔드 개발자입니다 😎</description>
        <lastBuildDate>Mon, 02 Mar 2026 11:23:35 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jess_apr.log</title>
            <url>https://velog.velcdn.com/images/jess_apr/profile/1ebbae4f-0f0c-40d6-a9df-6e123b7eeac2/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jess_apr.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jess_apr" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[새로운 프로젝트에서 Git 브랜치 전략을 바꾼 이유: dev→staging→main에서 feature 중심 흐름으로]]></title>
            <link>https://velog.io/@jess_apr/%EC%83%88%EB%A1%9C%EC%9A%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Git-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5%EC%9D%84-%EB%B0%94%EA%BE%BC-%EC%9D%B4%EC%9C%A0-devstagingmain%EC%97%90%EC%84%9C-feature-%EC%A4%91%EC%8B%AC-%ED%9D%90%EB%A6%84%EC%9C%BC%EB%A1%9C</link>
            <guid>https://velog.io/@jess_apr/%EC%83%88%EB%A1%9C%EC%9A%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Git-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5%EC%9D%84-%EB%B0%94%EA%BE%BC-%EC%9D%B4%EC%9C%A0-devstagingmain%EC%97%90%EC%84%9C-feature-%EC%A4%91%EC%8B%AC-%ED%9D%90%EB%A6%84%EC%9C%BC%EB%A1%9C</guid>
            <pubDate>Mon, 02 Mar 2026 11:23:35 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이번에 새 프로젝트에 합류하게 되면서, 기술 스택만큼이나 초반에 중요하게 논의했었던 것은 협업 흐름이었다. 기존에 우리가 작업했던 서비스와는 달리 일정이 촉박하고 QA 시간이 제한적이어서, “어떻게 개발할지”와 함께 “어떻게 검증하고 배포할지”도 새롭게 정해야 했다.</p>
<p>그 과정에서 우리 팀은 기존에 쓰던 dev → staging → main 흐름을 그대로 가져가지 않고, 기능(feature) 단위를 끝까지 들고 가는 방식으로 git 브랜치 전략을 바꿨다. 이 글에서는</p>
<ul>
<li>기존 프로젝트에서는 왜 dev → staging → main 흐름이 잘 맞았는지</li>
<li>이번 프로젝트에서는 그 방식이 왜 문제가 되었는지</li>
<li>새로운 git 전략이 무엇을 해결하려고 했고, 대신 어떤 단점을 마주했는지</li>
</ul>
<p>를 정리해보려 한다.</p>
<hr>
<h2 id="1-기존에-사용하던-git-전략">1. 기존에 사용하던 git 전략</h2>
<p>이전 프로젝트에서 우리 팀이 가장 중요하게 생각했던 건 배포 안정성이었다. 배포 주기도 2주에 한번으로 고정되어 있었기 때문에, 개발 → 검증 → 배포 단계를 명확히 나눠서 운영하는 방식이 자연스러웠다.</p>
<p>그래서 브랜치 흐름도 단계별로 분리되어 있었다. 개발자는 dev에서 작업을 모으고 테스트했으며, QA는 staging에서 검증을 진행했다. 최종적으로 안정화되었다고 판단되면 배포일에 main에 반영하는 구조였다.</p>
<p>작업 흐름은 아래와 같았다.</p>
<ul>
<li>dev에서 feature 브랜치를 분기한다.</li>
<li>기능 개발이 끝나면 feature → dev로 머지한다.</li>
<li>개발자들이 dev에서 테스트하고 문제가 없으면 dev → staging으로 머지한다.</li>
<li>QA가 staging에서 테스트한다.</li>
<li>기능이 안정적이라고 판단되면, 배포 날짜에 맞춰 배포 리스트 기준으로 staging → main 머지를 진행한다.</li>
</ul>
<p>이 방식은 배포되는 브랜치(main)를 최대한 안정적으로 유지한다는 목적에 잘 맞았다.</p>
<hr>
<h2 id="2-새-프로젝트에서-달라진-점">2. 새 프로젝트에서 달라진 점</h2>
<p>이번에 맡게 된 새 프로젝트는 기간이 너무나도 촉박했다. 서비스 오픈까지의 시간이 짧다보니 개발 시간도 부족한데, 전체 QA를 위한 시간은 더더욱 확보하기가 어려웠다. 그렇다고 QA를 포기할 수도 없었다.</p>
<p>그래서 팀이 선택한 방향은 전체 QA를 크게 한 번 진행하는 것이 아니라, 페이지(또는 기능) 단위로 부분 QA를 최대한 많이 선행해서 최종 QA 시간을 줄이는 쪽이었다. 누군가 작업을 끝내면 코드 리뷰를 거쳐 QA로 바로 올리고, 가능한 한 빨리 검증을 시키자는 접근이다.</p>
<p>또 하나의 변화는 스코프의 유동성이었다. 일정이 촉박할수록 개발 상황에 따라 넣을 건 넣고 뺄 건 빼는 선택을 계속 하게 된다. 기능의 추가/제거가 자주 일어날 것을 전제로 하면, 배포에 포함될 범위를 유연하게 조절할 수 있어야 했다.</p>
<hr>
<h2 id="3-기존-전략이-새-프로젝트와-부딪힌-지점">3. 기존 전략이 새 프로젝트와 부딪힌 지점</h2>
<p>문제는 기존의 dev → staging → main 흐름이 이번 프로젝트의 조건과 잘 맞지 않았다는 점이다.</p>
<p>첫 번째는 부분 QA 단위로 분리하기 어렵다는 점이다. 기존 흐름에서 QA 환경은 기본적으로 dev의 스냅샷이 staging에 올라간다. 그런데 dev에는 여러 사람의 작업이 섞여 있기 때문에, QA가 특정 페이지/기능만 집중해서 검증하고 싶어도 실제로는 관련 없는 변경까지 함께 올라간 상태에서 테스트가 진행된다. 결과적으로 부분 QA를 빠르게 진행한다는 개념보다는 그냥 변경된 범위가 커진 상태로 테스트하게 된다.</p>
<p>두 번째는 QA 진행이 dev 상태에 종속된다는 점이다. dev에 누군가의 미완성 커밋이 들어가거나, 통합 중 문제가 생겨 dev가 불안정해지면 dev → staging 자체가 막힌다. 그러면 이미 완료된 작업이 있어도 QA는 기다려야 한다. 촉박한 일정에서 이런 병목은 치명적이다.</p>
<p>세 번째는 배포 스코프를 유연하게 조절하기 어렵다는 점이다. dev에 기능들이 한 번 섞이기 시작하면, “이번 배포에는 A만 넣고 B는 빼자”가 말처럼 쉽지 않다. 특히 같은 파일이나 공통 영역을 함께 건드린 경우엔 더 그렇다.</p>
<p>마지막으로, 최종 QA 시간이 짧을수록 통합본을 한 번에 올려 검증하는 방식은 리스크가 커진다. 한번에 진행해야하는 검증 범위가 크고, 작은 변경도 큰 덩어리에 섞여 들어가기 때문이다.</p>
<hr>
<h2 id="4-새롭게-제안된-git-전략">4. 새롭게 제안된 git 전략</h2>
<p>이런 조건에서 팀이 새롭게 선택한 전략은 기능 단위(feature 브랜치)를 끝까지 들고 가는 방식이었다. 핵심은 완료된 기능부터 빠르게 QA에 누적하고, 배포 포함/제외를 기능 단위로 통제하는 것이다.</p>
<p>새 흐름은 아래와 같다.</p>
<ul>
<li>main에서 feature 브랜치를 분기한다.</li>
<li>작업이 끝나면 feature → dev로 머지하여 개발자 확인/테스트를 한다.</li>
<li>dev 확인이 끝나면 feature → QA로 머지해 부분 QA를 진행한다.</li>
<li>QA가 끝나면 feature → main으로 머지해 배포에 포함한다.</li>
</ul>
<p>이 방식에서는 QA에 올릴 단위가 dev 스냅샷이 아니라 feature 브랜치가 된다. 그래서 누군가의 미완성 작업이 dev에 있더라도, 내가 완료한 기능은 dev 확인 후 QA로 바로 올려 부분 검증을 진행할 수 있다. 또한 배포 리스트가 곧 “어떤 feature를 main에 머지할지”가 되기 때문에, 촉박한 일정 속에서도 넣고 빼는 결정을 비교적 단순하게 만들 수 있다.</p>
<hr>
<h2 id="5-전략의-단점과-적용하며-마주친-문제">5. 전략의 단점과 적용하며 마주친 문제</h2>
<p>당연하게도 이 전략이 만능은 아니다. 오히려 문제를 해결하려고 바꾼 전략이 다른 비용을 만들어내기도 했다.</p>
<h3 id="5-1-브랜치-간-상태-불일치-증가-→-충돌-가능성-상승">5-1. 브랜치 간 상태 불일치 증가 → 충돌 가능성 상승</h3>
<p>기존 흐름은 dev에서 쌓인 통합본이 staging과 main으로 올라가는 형태라, 최소한 코드 흐름이 한 방향으로 이어진다. 그런데 feature 중심 흐름에서는 main, QA, dev가 각자 다른 속도로 변한다.</p>
<ul>
<li>main은 배포된 코드만 반영된다.</li>
<li>QA는 개발 완료된 feature들이 계속 누적된다.</li>
<li>dev에는 미완성 기능이나 합쳤다가 빠진 기능이 남아있을 수 있다.</li>
</ul>
<p>이 상태가 지속되면 브랜치 간 격차가 커지고, 결과적으로 머지 시 충돌이 더 자주, 크게 터질 가능성이 높아진다.</p>
<p><strong>논의한 대안</strong></p>
<ul>
<li>충돌 가능성을 높일 수 있는 공통 변경은 반드시 누가/무엇을/어디까지 수정하는지 공유하고 진행하기</li>
<li>가능한 범위에서 feature 작업 단위를 더 작게 나눠 dev/QA로 올리는 주기를 짧게 가져가기</li>
</ul>
<h3 id="5-2-의존성-문제-dev에만-있는-변경을-feature가-필요로-하는-상황">5-2. 의존성 문제: dev에만 있는 변경을 feature가 필요로 하는 상황</h3>
<p>전략을 바꾸고 작업을 시작하면서 초반에 가장 체감이 컸던 건 의존성 문제였다. dev에 누군가가 새 공통 기능을 추가했고, 내 페이지가 그걸 필요로 하는 상황이 생길 수 있다. 그런데 feature는 main에서 분기한 상태라 dev 변경을 쉽게 가져오기 애매해진다. 최악의 경우, 그 공통 작업이 main에 올라올때까지 작업을 못하는 상황이 발생할 수 있다.</p>
<p>또 feature → dev 머지에서 conflict가 발생했을 때도, dev를 끌어와서 해결하는 방향이 운영상 어렵기에 해결이 더 까다로워진다.</p>
<p><strong>논의한 대안</strong></p>
<ul>
<li>공통 변경(타입/유틸/공통 컴포넌트)은 가능하면 먼저 main까지 올리기</li>
<li>일정이 촉박할수록 페이지가 아니라 공통 먼저로 우선순위를 재조정하기</li>
</ul>
<h3 id="5-3-운영-복잡도-증가">5-3. 운영 복잡도 증가</h3>
<p>기능 단위로 여러 환경에 반영하다 보니 머지 경로가 늘어난다. 그렇다보니 “지금 이 기능은 dev/QA/main 중 어디까지 갔지?”와 같은 상태 추적이 어려워지고, 머지 누락 같은 운영 실수 가능성이 커진다.</p>
<p><strong>논의한 대안</strong></p>
<ul>
<li>feature 머지는 원칙적으로 dev → QA → main 순서를 지키는 흐름으로 통일해서 혼선을 줄이기</li>
<li>배포 리스트를 feature 단위로 명확하게 관리해 커뮤니케이션 비용을 낮추기</li>
</ul>
<h3 id="5-4-qa-환경-변동-증가-→-회귀추적-비용-증가">5-4. QA 환경 변동 증가 → 회귀/추적 비용 증가</h3>
<p>부분 QA를 빠르게 누적하려면 QA 환경이 자주 바뀌는 건 피하기 어렵다. QA 관점에서는 어제와 오늘이 다른 환경이 되기 쉬워, 회귀나 추적 비용이 늘 수 있다.</p>
<p><strong>논의한 대안</strong></p>
<ul>
<li>공통 변경처럼 영향 범위가 큰 요소를 먼저 안정화해 QA 환경 변동을 줄이기</li>
</ul>
<hr>
<h2 id="마무리하며">마무리하며</h2>
<p>여태까지 나는 한 가지 git 전략만 써봐서, 그 방식이 어느 정도 정석이라고 막연히 생각하고 있었다. 그런데 이번 프로젝트를 준비하면서 팀원들과 함께 전략을 비교하고 논의하는 과정을 겪어보니, 회사마다(그리고 프로젝트마다) 선택하는 전략이 정말 다양하고, 그 선택이 정답이 아닌 조건과 우선순위의 문제라는 걸 처음으로 체감했다.</p>
<p>이번 전략 변경도 마찬가지였다. 새 전략이 무조건 더 좋다기보다는, 우리가 마주한 조건에서 우리가 감당할 비용과 리스크를 어디에 둘지에 대한 선택이었다. 기능 단위로 QA를 분산하고 배포 스코프를 유동적으로 관리할 수 있게 된 대신, 높아진 충돌 가능성과 의존성, 운영 복잡도 같은 새로운 문제가 등장했다.</p>
<p>아직 완벽한 해답은 없다. 다만 무엇을 해결하려고 무엇을 감수했는지가 명확해졌고, 다음에 비슷한 조건의 프로젝트를 만나면 더 빠르게 판단하고 팀과 합의할 수 있을 것 같다. 이번 경험을 통해 git 전략을 그냥 정한 규칙이 아니라, 팀의 속도와 안정성을 조율하는 도구로 바라볼 수 있는 관점을 얻을 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Radix UI Tabs에서 발생한 무한 alert 이슈]]></title>
            <link>https://velog.io/@jess_apr/Radix-UI-Tabs%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EB%AC%B4%ED%95%9C-alert-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@jess_apr/Radix-UI-Tabs%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EB%AC%B4%ED%95%9C-alert-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Thu, 17 Jul 2025 13:59:06 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-상황">문제 상황</h1>
<p>회사에서 페이지를 구현하던 도중, 예상치 못한 동작으로 문제가 발생했다. 사용자가 특정 기능을 사용하지 않으면 해당 기능의 설정을 할 수 있는 탭으로 전환을 못하도록 막아야 했는데, 이에 대한 안내문구를 alert로 띄우니 이 alert가 무한 반복이 되는 것이었다. </p>
<blockquote>
<p>예상 동작: alert로 안내문구가 뜬 후 현재 탭 유지
실제 동작: 현재 탭은 유지되나, alert 창을 꺼도 계속해서 alert 창이 나타남</p>
</blockquote>
<p>문제 환경은 다음과 같았다:</p>
<ul>
<li>Radix UI(Primitives)의 Tabs 컴포넌트 사용</li>
<li>활성화된 탭에 대한 상태값은 useState로 관리</li>
<li>onValueChange 속성에 상태값을 변경하는 함수를 적용</li>
<li>특정 조건을 만족하지 않을 경우에는 alert를 띄운 후 return하여 상태값이 변경되는 것을 막음</li>
</ul>
<h1 id="문제-해결-과정">문제 해결 과정</h1>
<p>처음에는 구현 과정에서 문제가 있었을것이라 전제하고, 이 문제가 발생하는 구간을 찾기 위해 <code>console.log()</code>를 사용하거나, 조건을 하나씩 삭제하며 조건을 좁혀나갔다.</p>
<ul>
<li>똑같은 조건에서 <code>alert()</code> 대신 <code>console.log()</code>를 사용한 경우에는 무한 반복 이슈가 발생하지 않았음</li>
<li><code>alert()</code>를 사용했더라도, return을 하지 않고 상태값이 바뀌도록 하면 무한 반복되지 않음</li>
<li>setState를 사용할 때 값이 반영되지 않아야하는데, 바뀐 값이 들어가는 것인가 싶어 <code>setState(prev =&gt; prev)</code>를 추가해보았으나 문제는 똑같이 발생했다.</li>
</ul>
<p>하지만 조건을 좁혀도 원인을 특정할수가 없었다. 그래서 Radix UI 라이브러리 자체의 동작과 관련하여 발생하는 것일수도 있다는 생각을 가지고 새롭게 Next.js 프로젝트를 생성하여 동일한 조건의 Tabs 컴포넌트를 생성해보았다.</p>
<pre><code class="language-ts">// 동일한 문제가 발생하는 코드
export default function Tab() {
  const [selectedTab, setSelectedTab] = useState(&quot;tab1&quot;);

  const alertShownRef = useRef(false);

  function handleChangeValue(value: string) {
    if (value !== &quot;tab1&quot;) {
      alert(&quot;Not allowed&quot;);
      return;
    }

    setSelectedTab(value);
  }

  return (
    &lt;div&gt;
      &lt;Tabs.Root value={selectedTab} onValueChange={(value) =&gt; handleChangeValue(value)}&gt;
        &lt;Tabs.List&gt;
          &lt;Tabs.Trigger value=&quot;tab1&quot;&gt;Tab1&lt;/Tabs.Trigger&gt;
          &lt;Tabs.Trigger value=&quot;tab2&quot;&gt;Tab2&lt;/Tabs.Trigger&gt;
        &lt;/Tabs.List&gt;
        &lt;Tabs.Content value=&quot;tab1&quot;&gt;Tab1 content&lt;/Tabs.Content&gt;
        &lt;Tabs.Content value=&quot;tab2&quot;&gt;Tab2 content&lt;/Tabs.Content&gt;
      &lt;/Tabs.Root&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>아무것도 손대지 않은 새 프로젝트에서 Radix UI만 설치하고 컴포넌트를 만들어보았는데, 동일한 문제를 구현해낼 수 있었다.</p>
<p>회사 프로젝트에서는 Tabs 컴포넌트를 랩핑해서 사용하고 있어서 라이브러리와 관련된 문제라고 생각할 수 없었지만, 이렇게 순수한 상태의 컴포넌트를 사용해도 문제가 발생하는 것으로 보아 라이브러리의 동작 원리와 관련이 있을수 있겠다는 생각이 들었다.</p>
<h1 id="문제-원인">문제 원인</h1>
<p>Radix UI의 깃허브를 방문하여 코드를 열어보니 문제 원인을 확실하게 알 수 있었다.</p>
<pre><code class="language-js">const TabsTrigger = React.forwardRef&lt;TabsTriggerElement, TabsTriggerProps&gt;(
  (props: ScopedProps&lt;TabsTriggerProps&gt;, forwardedRef) =&gt; {

    // ... 생략

    return (
      &lt;RovingFocusGroup.Item
        asChild
        {...rovingFocusGroupScope}
        focusable={!disabled}
        active={isSelected}
      &gt;
        &lt;Primitive.button
          // ... props 생략
          onMouseDown={composeEventHandlers(props.onMouseDown, (event) =&gt; {
            if (!disabled &amp;&amp; event.button === 0 &amp;&amp; event.ctrlKey === false) {
              context.onValueChange(value);
            } else {
              event.preventDefault();
            }
          })}
          onKeyDown={composeEventHandlers(props.onKeyDown, (event) =&gt; {
            if ([&#39; &#39;, &#39;Enter&#39;].includes(event.key)) context.onValueChange(value);
          })}
          onFocus={composeEventHandlers(props.onFocus, () =&gt; {
            const isAutomaticActivation = context.activationMode !== &#39;manual&#39;;
            if (!isSelected &amp;&amp; !disabled &amp;&amp; isAutomaticActivation) {
              context.onValueChange(value);
            }
          })}
        /&gt;
      &lt;/RovingFocusGroup.Item&gt;
    );
  }
);</code></pre>
<p><code>Tabs.Root</code>에 전달된 <code>onValueChange</code> 함수는 내부에서 TabsProvider를 통해 <code>Tabs.Trigger</code>에 context로 전달된다.</p>
<p>이렇게 전달된 <code>onValueChange</code>는 <code>Tabs.Trigger</code>가</p>
<ol>
<li>왼쪽 마우스를 통해 클릭되었을 때 (<code>onMouseDown</code>)</li>
<li>엔터키가 눌렸을 때 (<code>onKeyDown</code>)</li>
<li>포커스되었을때 (<code>onFocus</code>)</li>
</ol>
<p>실행된다.</p>
<p>처음 탭을 클릭했을때는 <code>onMouseDown</code> 이벤트 핸들러에 의해 alert가 발생한다. 하지만 여기서 alert가 UI를 블로킹해버린다. 브라우저는 alert가 발생한 순간 모든 렌더링과 포커스 흐름을 멈추고 사용자가 상호작용을 하길 기다린다. 이때 클릭된 탭은 포커스를 잠시 잃어버렸다가, 사용자가 상호작용을 마치면 브라우저가 기억해둔 엘리먼트로 포커스를 다시 되돌린다. 그러면 <code>Tabs.Trigger</code>의 <code>onFocus</code>가 실행되며 <code>onValueChange</code>가 다시 실행된다. 그리고 alert가 발생하면 다시 포커스를 잃어버리고, 복귀시키며 또 다시 <code>onValueChange</code>를 실행하는 과정이 무한반복 되는 것이다.</p>
<p>동일한 코드에서 <code>console.log()</code>가 문제를 발생시키지 않는 이유는 UI 블로킹을 하지 않기 때문에 <code>onFocus</code>를 실행시키지 않아서이다. 문제가 발생하는 <code>Tabs.Trigger</code>에 onFocus로 로그를 남겨보면 <code>console.log()</code>는 로그가 한번만 찍히지만, <code>alert()</code>는 계속해서 로그가 찍히는 것을 확인할 수 있다.</p>
<h1 id="해결-방법">해결 방법</h1>
<p>나는 <code>useRef</code>와 <code>setTimeout()</code>을 통해 문제를 해결했다.</p>
<p><code>useRef</code> 사용한 이유는, 렌더링에 영향을 받지 않고, 또 영향을 주지 않기 때문에 렌더링 흐름에 상관없이 안전하게 값을 저장할 수 있을 것이라고 판단했기 때문이다. </p>
<pre><code class="language-ts">const alertShownRef = useRef(false);

function handleChangeValue(value: string) {
  if (value === &quot;tab2&quot;) {
    if (!alertShownRef.current) {
      alertShownRef.current = true;
      alert(&quot;Not allowed&quot;);

      setTimeout(() =&gt; {
        alertShownRef.current = false;
      }, 400);
    }
    return;
  }

  setSelectedTab(value);
}</code></pre>
<p>우선, useRef에 alert가 보여졌는지 여부를 저장한다. 탭을 클릭하면 alert가 보여지기 전에 ref 값을 확인하여 false일때만 alert를 노출한다. 조건문을 통과했다면, alert가 노출되기 전에 (UI 블로킹이 일어나기 전에) ref 값을 true로 바꾼다. 그러면 alert창이 닫히고 포커스가 탭으로 돌아가도 조건에 막혀 alert가 다시 노출되지 않는다. 탭이 닫히고 나면 setTimeout을 사용하여 조건문에 의해 alert 무한 반복이 끊길때까지 충분한 시간을 준 후 ref 값을 false로 바꾸어 사용자가 다시 클릭했을때는 alert 창이 노출될 수 있도록 해준다.</p>
<h4 id="라고-끝낼뻔-하였으나❗️❗️-더-간단하고-좋은-방법이-있었다">...라고 끝낼뻔 하였으나❗️❗️ 더 간단하고 좋은 방법이 있었다.</h4>
<pre><code class="language-js">onFocus={composeEventHandlers(props.onFocus, () =&gt; {
  const isAutomaticActivation = context.activationMode !== &#39;manual&#39;;
  if (!isSelected &amp;&amp; !disabled &amp;&amp; isAutomaticActivation) {
    context.onValueChange(value);
  }
})}</code></pre>
<p><code>Tabs.Trigger</code>의 <code>onFocus</code> 코드를 잘 읽어보면 <code>context.activationMode</code>가 &#39;manual&#39;이 아닐때만 <code>onValueChange</code>가 실행되는 것을 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/0bdcb19d-f3dc-46d5-819e-4049a556e5db/image.png" alt=""></p>
<p>Radix UI의 공식문서에서도 확인할 수 있는데, 이 <code>activationMode</code>를 &#39;manual&#39;로 설정하면 탭이 포커스 될때는 활성화되지 않는다.</p>
<pre><code>&lt;Tabs.Root value={selectedTab} onValueChange={(value) =&gt; handleChangeValue(value)} activationMode=&quot;manual&quot;&gt;</code></pre><p>이렇게 속성 하나를 추가하여 간단히 해결할 수 있었다.</p>
<p>끝~!!!</p>
<hr>
<p><strong>참고자료</strong></p>
<ul>
<li><a href="https://www.radix-ui.com/primitives/docs/components/tabs">[Radix UI - Primitives] Tabs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[메인 스레드가 해야 할 일, Web Worker에 맡겼습니다.]]></title>
            <link>https://velog.io/@jess_apr/%EB%A9%94%EC%9D%B8-%EC%8A%A4%EB%A0%88%EB%93%9C%EA%B0%80-%ED%95%B4%EC%95%BC-%ED%95%A0-%EC%9D%BC-Web-Worker%EC%97%90-%EB%A7%A1%EA%B2%BC%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@jess_apr/%EB%A9%94%EC%9D%B8-%EC%8A%A4%EB%A0%88%EB%93%9C%EA%B0%80-%ED%95%B4%EC%95%BC-%ED%95%A0-%EC%9D%BC-Web-Worker%EC%97%90-%EB%A7%A1%EA%B2%BC%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Tue, 01 Apr 2025 14:39:16 GMT</pubDate>
            <description><![CDATA[<h2 id="싱글-스레드의-한계-그리고-web-worker">싱글 스레드의 한계, 그리고 Web Worker</h2>
<p>자바스크립트는 싱글스레드 언어이다. 코드 실행, UI 렌더링, 이벤트 핸들링 등 다양한 작업을 하나의 메인 스레드가 모두 처리한다. 타이머나 이벤트 등록 같은 비동기 작업들은 이벤트 루프를 통해 Web API가 대신 처리해주지만, CPU 연산은 자바스크립트가 직접 처리해야하기 때문에 한계가 있을 수 밖에 없다. CPU 집약적인 무거운 계산을 하게되면 메인 스레드가 다른 작업을 할 수 없게 되고, UI 렌더링이나 사용자 인터렉션 등이 멈추며 화면이 버벅거리는 현상이 나타나게 된다. 이때는 <strong>Web Worker</strong> 같은 별도 스레드로 무거운 연산을 실행하여 메인 스레드가 블로킹되지 않게 할 수 있다.</p>
<h2 id="문제-상황">문제 상황</h2>
<p>프로젝트를 진행하다가 갑자기 화면 멈춤 현상이 발생했다. 크롬 확장 프로그램을 개발하던 중이었는데, 확장 프로그램의 사이드 패널을 열때마다 빈 화면이 보여지다 한참 뒤에야 렌더링되었다.</p>
<p>문제는 RSA 키를 생성하는 함수였다. 초기화 로직에 통신 암호화에 사용할 RSA 키를 생성하는 로직을 넣었는데, 계산량이 많다보니 메인 스레드가 블로킹되는 현상이 발생하고 말았다. 같은 HTML 파일을 웹 브라우저에서 실행했을때는 오래걸리진 않았는데, 확장 프로그램의 특성 상 리소스의 제한이 있었는지 사이드 패널 환경에서는 짧게는 2초, 길게는 5초 이상까지도 화면이 멈추었다. FCP가 5초 이상 걸리는 것은 UX에 아주 큰 영향을 주는만큼, RSA 생성 함수를 메인 스레드에서 분리해서 처리할 필요성이 있었다.</p>
<h2 id="web-worker-도입">Web Worker 도입</h2>
<h3 id="web-worker란">Web Worker란?</h3>
<p>Web Worker는 브라우저에서 제공하는 별도의 스레드 환경이다. 자바스크립트는 보통 웹 브라우저 위에서 돌아가기 때문에, 이렇게 브라우저에서 제공해주는 스레드를 사용하여 메인 스레드와는 독립적으로 코드를 실행할 수 있다. </p>
<p>이미지 처리와 같이 계산량이 많거나, 대량의 데이터를 파싱하는 경우, 또는 백그라운드에서 장기간 실행되어야 하는 초기화 로직들을 Web Worker에게 맡기면 UI 렌더링과 병렬 처리가 가능하기 때문에 사용자 경험을 향상시킬 수 있다.</p>
<h3 id="실제-코드-예시">실제 코드 예시</h3>
<h4 id="1-web-worker가-실행할-코드">1. Web Worker가 실행할 코드</h4>
<pre><code>// rsaWorker.ts
self.onmessage = (event) =&gt; {
  if (event.data === &#39;GENERATE_RSA_KEY&#39;) {
    const keyPair = forge.pki.rsa.generateKeyPair({ bits: 2048 });

    self.postMessage({
      publicKey: forge.pki.publicKeyToPem(keyPair.publicKey),
      privateKey: forge.pki.privateKeyToPem(keyPair.privateKey),
    });
  }
};</code></pre><p>이 코드는 Web Worker가 실행해야 하는 작업을 알려주는 코드다. 메인 스레드로부터 <code>&#39;GENERATE_RSA_KEY&#39;</code>라는 메세지를 받으면 Web Worker가 RSA 생성 함수를 실행한 뒤 <code>postMessage()</code>를 통해 결과를 다시 메인 스레드로 전송해준다.</p>
<h4 id="2-메인-스레드에서-web-worker-호출">2. 메인 스레드에서 Web Worker 호출</h4>
<pre><code>// rsaWorkerClient.ts
export function generateRSAKeyPair(): Promise&lt;{ publicKey: string; privateKey: string }&gt; {
  return new Promise((resolve, reject) =&gt; {
    if (!window.Worker) {
      return reject(new Error(&#39;Web Worker not supported.&#39;));
    }

    const worker = new Worker(new URL(&#39;./rsaWorker.ts&#39;, import.meta.url), { type: &#39;module&#39; });

    worker.onmessage = (event) =&gt; resolve(event.data);
    worker.onerror = reject;

    worker.postMessage(&#39;GENERATE_RSA_KEY&#39;);
  });
}</code></pre><p>이 함수를 호출하면, 메인 스레드가 <code>new Worker()</code>를 통해 Web Worker를 실행시키고, Web Worker가 실행해야 할 코드인 <code>rsaWorker.ts</code>를 전달해준다. 결과는 Promise로 반환하는데, worker가 제대로 결과를 반환해준다면 worker로부터 받은 데이터를 담아 성공 반환을 하고, worker가 예외를 던진다면 실패 반환을 한다. <code>resolve</code>, <code>reject</code> 콜백 설정까지 완료되었다면 Web Worker로 <code>&#39;GENERATE_RSA_KEY&#39;</code> 메세지를 보내 작업을 시작하게 한다.</p>
<blockquote>
<p>💡 알아두면 좋은 점: Web Worker는 <code>new Worker()</code>를 호출할 때마다 별도의 스레드와 메모리를 사용하기 때문에, 반복적으로 사용하는 경우에는 <strong>전역에서 재사용하거나 풀링(pooling)</strong>을 고려해야 한다. 하지만 이번 프로젝트에서는 RSA 키 생성을 앱 초기화 시점에 단 한 번만 수행해서, Web Worker를 재사용하거나 수명을 관리할 필요는 없었다.</p>
</blockquote>
<h4 id="3-react-앱-초기화-시-호출">3. React 앱 초기화 시 호출</h4>
<pre><code>// index.tsx
generateRSAKeyPair().then((keyPair) =&gt; {
  console.log(&#39;Generated keys:&#39;, keyPair);
});

const root = ReactDOM.createRoot(document.getElementById(&#39;root&#39;)!);
root.render(&lt;App /&gt;);</code></pre><p>이제 Web Worker를 호출하는 함수를 프로그램 초기화 시 실행하여 키를 생성할 수 있도록 한다.</p>
<h2 id="비동기-초기화-결과를-기다리는-구조-만들기">비동기 초기화 결과를 기다리는 구조 만들기</h2>
<p>RSA 키 생성 함수를 Web Worker로 분리하고 나니 다른 문제가 발생했다. RSA 키 생성이 완료되기 전에 RSA 키를 포함해야 하는 API 요청을 보내면 오류가 나는 것이었다. 이를 해결하기 위해 다음과 같이 키 생성이 완료될때까지 결과를 기다리는 구조를 만들었다.</p>
<ol>
<li>Worker에서 키 생성 작업 수행</li>
<li>완료되면 postMessage로 메인 스레드에 결과 전달</li>
<li>전역 이벤트(<code>window.addEventListener()</code>)를 발생시켜 RSA 키 생성이 끝났음을 알림</li>
<li>API 요청을 사용하는 컴포넌트에서는 전역 이벤트가 발생할 때까지 API 요청을 지연</li>
</ol>
<pre><code>// index.tsx
async function initApp() {
    try {
        await generateRSAKeyPair()
        // RSA 키 생성 후, RSA 키가 필요한 요청을 보낼 수 있도록 전역 이벤트 발생
        window.dispatchEvent(new Event(&#39;rsaKeyReady&#39;))
    } catch (error) {
        ...
    }
}

initApp()

const root = ReactDOM.createRoot(document.getElementById(&#39;root&#39;)!);
root.render(&lt;App /&gt;);</code></pre><p>단순히 Web Worker만 호출 하던 로직에서 Web Worker 호출 완료 후 <code>resolve</code> 콜백이 반환되면 전역 이벤트를 발생시키도록 수정해주었다. </p>
<pre><code>function Component() {
    useEffect(() =&gt; {
        const handleRSAReady = async () =&gt; {
            const isTokenValid = await checkToken()
            setIsAuthenticated(isTokenValid)
        }

        // RSA 키 생성 완료 이벤트 감지하면 토큰 확인
        window.addEventListener(&#39;rsaKeyReady&#39;, handleRSAReady)

        return () =&gt; {
            window.removeEventListener(&#39;rsaKeyReady&#39;, handleRSAReady)
        }
    }, [])

    return ( ... )
}</code></pre><p>이제 RSA 키가 필요한 API 요청들은 이렇게 전역 이벤트가 발생할때까지 기다렸다가 요청을 보내게 설정하면 된다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>머리로는 알고있었지만 사실 체감하기는 쉽지 않았던 자바스크립트의 싱글 스레드와 성능 병목을 직접 체감할 수 있는 좋은 기회였다. 덕분에 Web Worker라는 추가적인 스레드를 사용할 수 있다는 것도 배울 수 있었다.</p>
<p>Web Worker가 생각보다 도입도 어렵지 않고 실용적이라는 걸 느끼기도 했다. 앞으로 이미지 처리, 대용량 데이터 처리 등 메인 스레드가 블로킹되는 문제가 발생할 경우 잘 활용해볼 수 있을 것 같다.</p>
<hr>
<p>참고자료</p>
<ul>
<li><a href="https://developer.mozilla.org/ko/docs/Web/API/Web_Workers_API">MDN - Web Workers API</a></li>
<li><a href="https://medium.com/hcleedev/web-web-worker-%EC%82%AC%EC%9A%A9%EB%B2%95%EA%B3%BC-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90-webpack-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%AC%B8%EC%A0%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%AA%A8%ED%82%B9-2d77c5b23afe">Web: Web Worker 사용법과 주의할 점(webpack, 메모리 문제, 테스트 모킹)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[통신 암호화로 데이터 안전하게 전달하기]]></title>
            <link>https://velog.io/@jess_apr/%ED%86%B5%EC%8B%A0-%EC%95%94%ED%98%B8%ED%99%94%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EC%A0%84%EB%8B%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jess_apr/%ED%86%B5%EC%8B%A0-%EC%95%94%ED%98%B8%ED%99%94%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EC%A0%84%EB%8B%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 25 Feb 2025 12:44:12 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>작년 3월, 회사에서 통신 암호화를 구현하게 될 수 있으니 미리 암호화에 대해 공부해보라는 이야기를 들었다. 그때 공부했던 것을 블로그에 남기기도 했는데, 굉장히 흥미롭게 봤던 기억이 난다. 시간이 조금 지나긴 했지만 이번에 회사의 새로운 프로젝트에 들어가면서 좋은 기회로 <strong>통신 암호화</strong>를 구현해보게 되었다. 역시나 굉장히 재밌게 작업했고, 새롭게 배운것들도 많아서 블로그를 남기면 좋을것같다는 생각이 들었다.</p>
<h2 id="클라이언트에서-서버까지-데이터-암호화하기">클라이언트에서 서버까지 데이터 암호화하기</h2>
<p>처음에 대표님께서 end-to-end 암호화를 구현하라고 하셔서 의아했다. End-to-end encryption이라고 하면 비밀채팅에서 사용하는 그거 아닌가? 우리 서비스에는 채팅 기능이 없는데 어디에 적용을 하라는 말씀이신거지?</p>
<p>알고보니 클라이언트에서 서버까지 데이터가 평문으로 전달되는 구간이 없도록 암호화를 하라는 의미였다. 말 그대로 끝(클라이언트)에서부터 끝(서버)까지. 하지만 클라이언트에서부터 서버까지는 이미 HTTPS로 암호화가 되어 전달되지 않나? 하는 의문이 들어 질문을 드렸더니, 친절하게 설명해주셨다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/613f61f4-c1d9-40fd-bae5-0be0c6409fea/image.png" alt=""></p>
<p>로드밸런서는 요청을 받아 서버에 작업을 고르게 분산시켜주는 역할을 하는 장치다. 로드밸런서가 HTTPS 요청을 받으면, <strong>SSL Termination</strong>을 실행하여 복호화를 하게 되는데, 이렇게 되면 로드밸런서부터 서버까지는 암호화 되지 않은 평문이 이동하게 된다. 어차피 내부망 안인데 굳이 암호화를 해야하냐고 생각할 수도 있지만, 정보를 가져가려는 사람들은 어떤 방법을 써서든 가져가려고 하기 때문에 중요한 정보라면 어떤 구간이든 평문으로 이동하지 않도록 하는게 안전하다고 하셨다.</p>
<p>그렇다고 이걸 해결할 다른 방법이 없는것은 아니다. 퇴근 후 집에 와서 조금 더 찾아보니 로드밸런서부터 서버까지 HTTPS로 데이터를 전송하는 방법도 찾아볼 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/30ba2291-5e15-4718-ae9f-686e2c21cdb3/image.png" alt=""></p>
<p><strong>SSL Bridging</strong>은 로드밸런서가 받은 요청을 복호화하고 내용을 검사하여 라우팅을 결정 한 뒤, 다시 암호화를 해서 서버로 데이터를 전송한다. 이 방식은 SSL Termination에 비해 보안성은 높지만, 중간에 복호화를 하는 과정이 있으니 클라이언트부터 백엔드까지 완전한 암호화는 아니다. 그리고 로드밸런서와 백엔드 서버 둘 다 SSL 인증서를 관리해야하고 SSL 복호화를 두번하게 되면서 서버에 부하가 증가한다는 단점도 있다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/12fff4e2-aae5-4053-b827-8947b42d6c23/image.png" alt=""></p>
<p><strong>SSL Passthrough</strong>는 로드밸런서가 SSL을 요청을 복호화하지 않고 바로 서버로 전달해주는 방식이다. 세가지 방법 중 제일 안전하지만, 로드밸런서가 HTTPS를 복호화하여 Path를 확인할 수 없기때문에 경로 기반 라우팅이 불가능하다. 백엔드 서버가 한개인 작은 서버라면 일단 Path를 몰라도 서버로 보내고, 서버에서 복호화 한뒤 적절한 API 라우터로 보내는 식으로 해결할 수도 있겠지만, 하나의 도메인 아래에 여러개의 서버를 물리적으로 분리하여 운영하는 큰 서비스의 경우에는 이 방법을 적용하기가 어렵다.</p>
<p>아무튼 이러한 이유로 평문으로 전달되는 구간이 있을 수 있으니, 데이터 자체를 암호화해서 보내면 중간에 공격을 받더라도 개인정보를 안전하게 보호할 수 있게 된다. 우리나라에서는 중요한 개인정보(주민번호, 금융정보 등)는 평문으로 이동하는 구간이 없도록 법으로 정해놨기 때문에, 금융권 등 중요한 정보를 다루는 곳에서는 이런 방법을 사용한다고 한다.</p>
<h2 id="암호화를-하는-방식">암호화를 하는 방식</h2>
<h3 id="비대칭키-암호화">비대칭키 암호화</h3>
<p>비대칭키 암호화는 개인키와 공개키를 사용하기 때문에, 키를 관리하기가 용이하다. 개인키는 안전하게 보관하고, 공개키만 전달하기 때문에 전달과정에서 키가 유출될 걱정이 없어 매우 안전하다. 다만, 계산이 복잡해서 굉장히 느리고, 암호화 할 수 있는 데이터의 양이 제한적이라는 치명적인 단점이 있다. 이번 프로젝트에서 RSA-2048 알고리즘을 사용했는데, 키를 생성하는데 굉장히 오래걸린다는것을 직접 체험했다 (체감 1초 이상..). 그리고 RSA-2048 기준으로 암호화가 가능한 데이터는 고작 245byte로, 문서나 파일같은 경우는 아예 암호화가 불가능하다. </p>
<h3 id="대칭키-암호화">대칭키 암호화</h3>
<p>대칭키 암호화는 빠르고, 대량의 데이터도 암호화가 가능하다. 하지만 암호화를 하는 키와 복호화를 하는 키가 같기 때문에, 키를 전달하는 과정에서 유출이나 탈취의 위험이 있다. </p>
<p>따라서, 데이터를 대칭키로 암호화하고, 대칭키를 비대칭키로 암호화하여 안전하게 전달하는 방식을 채택했다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/9309a047-38d4-4ee9-9bb5-534f7e38df4e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/e68f9dcc-3335-47df-ba81-38d6ee4a786a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/45a5eb86-96ad-4126-a7a7-a4c50ce87839/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/ad15eeb7-cb60-4408-a835-1347586fd27b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/b5d4e07f-d90f-4901-b29c-b065cfd06145/image.png" alt=""></p>
<p>서버가 RSA Public Key를 공개해주면, 클라이언트에서 데이터를 암호화할때 사용한 AES 키를 서버의 RSA Public Key로 암호화한다. 암호화한 AES 키는 요청 헤더에 담아 서버로 전달한다.</p>
<p>만약에 서버가 보내주는 응답에 데이터 암호화가 필요하다면? 클라이언트에서 RSA 키를 생성한 후, 서버로 요청을 보낼때 요청 헤더에 클라이언트의 RSA Public Key를 담아 보내주면 된다. 그러면 서버가 데이터를 암호화하고, AES 키를 클라이언트의 RSA Public Key로 암호화하여 응답 헤더에 담아 보내준다.</p>
<h2 id="실제-구현-코드">실제 구현 코드</h2>
<h3 id="키-생성-암복호화-함수-구현">키 생성, 암/복호화 함수 구현</h3>
<p>이번 프로젝트에서는 AES-256과 RSA-2048을 사용했고, 각 암호화 알고리즘의 키를 생성하는것은 <strong>node-forge</strong> 라이브러리의 도움을 받았다. 꼭 이 라이브러리가 아니더라도, 원하는 암호화 라이브러리를 선택해서 사용하면 되고, 공식 문서와 구글링을 잘 활용하면 문제없이 함수를 구현할 수 있다. </p>
<p>중요한 것은 백엔드와 암호화 세부설정을 맞추고, 키 생성을 할때 세부설정을 제대로 넣어야한다는 것이다. iv를 백엔드에서 정한 규격과는 다르게 설정하거나, AES의 다른 모드를 사용한다거나.. 등등 백엔드와 맞춰놓은 세부설정이 단 한글자만 틀려도 복호화가 불가능해지기 때문에 자칫하면 한참동안 삽질을 하게될 수 있다.</p>
<p>그리고 위에서 말했듯 RSA는 키 생성을 할 때 시간이 굉장히 오래걸린다. 화면이 하얀 여백으로 멈춰있다가 키 생성이 완료되면 그제야 렌더링이 되는데, 이 현상을 방지하기 위해 <strong>Web Worker</strong>를 사용했다. Web Worker는 메인 스레드와 분리된 백그라운드 스레드로, 복잡한 연산을 메인 스레드 대신 처리하게 할 수 있다. 이것까지 쓰면 글이 너무 길어질것같아서, 일단 언급만 해두겠다. 나중에 기회가 된다면 다른 글로 작성하면 좋을 것 같다.</p>
<h3 id="암복호화-대상-필드-지정">암/복호화 대상 필드 지정</h3>
<p>이번에 진행한 암호화는 개인정보를 포함한 특정 데이터 필드만 암/복호화를 진행해야했다. 따라서 요청,응답 데이터의 어떤 필드가 암/복호화가 되어야하는지 미리 설정을 해주었다. 회원가입 요청을 보내는 코드로 예시를 들어보겠다.</p>
<pre><code>export interface UserSignUpRequest {
    email: string
    password: string
    profile: {
        gender: string
    }
    channel: string
}</code></pre><p>요청을 보낼 때, 이러한 구조를 가진 데이터가 서버로 보내진다. 이제 이것과 동일한 구조를 가지지만, 암호화를 해야하는 필드에만 true값을 준 객체를 같이 만들어준다.</p>
<pre><code>export const UserSignUpReqEncConfig = {
    email: true,
    password: true,
    profile: {
        gender: true,
    },
}</code></pre><p><code>email</code>과 <code>password</code>, <code>gender</code>는 암호화가 되어야하는 개인정보이기 때문에 true값을 주었다. 가입 경로를 의미하는 <code>channel</code>은 개인정보가 아니어서 암호화를 해주지 않아도 된다. 나는 값이 false인 키들도 전부 적게된다면 코드가 길어지며 오히려 가독성이 떨어질 것 같아 쓰지 않았는데, 상황에 따라 적절한 방법을 사용하면 될것같다. 객체가 아니라 암호화를 할 필드를 문자열 배열로 저장하는 등 여러가지 방법을 사용할 수 있다.</p>
<h3 id="axios-요청-설정-객체에-암복호화-옵션-추가하기">Axios 요청 설정 객체에 암/복호화 옵션 추가하기</h3>
<pre><code>interface EncryptionConfig {
    requestEncryption?: boolean
    responseEncryption?: boolean
    encryptedRequestFields?: EncryptedFieldsConfig
    encryptedResponseFields?: EncryptedFieldsConfig
}

declare module &#39;axios&#39; {
    export interface AxiosRequestConfig {
        encryptionConfig?: EncryptionConfig
    }
}</code></pre><p>이제 암/복호화할 필드에 대한 가이드를 헤더에 포함할 수 있도록 axios의 요청 설정 객체에 속성을 추가해준다. <code>requestEncryption</code>과 <code>responseEncryption</code> 속성은 각각 요청 시 암호화 필드 존재 여부, 응답의 복호화 필드 존재 여부를 의미하고, <code>encryptedRequestFields</code>와 <code>encryptedRequestFields</code>는 각각 요청에서 암호화되어야 하는 필드, 응답에서 복호화되어야 하는 필드의 정보를 담고있다. </p>
<p>예를 들어서, 위에서 정의한 <code>UserSignUpReqEncConfig</code>대로 요청 데이터를 암호화하고 싶다면, <code>requestEncryption</code>를 true로 설정하고, <code>encryptedRequestFields</code>에 <code>UserSignUpReqEncConfig</code> 객체를 넣어주면 된다.</p>
<p>요청 설정 객체에만 속성을 추가해주면, 응답 객체에서도 접근할 수 있다. 응답 객체에는 따로 설정해주지 않아도 된다.</p>
<h3 id="데이터-구조에-따라-암복호화-실행하는-함수-작성">데이터 구조에 따라 암/복호화 실행하는 함수 작성</h3>
<pre><code>export const deepCrypt = (data: unknown, aesKey: string, cryptoFn: (value: string, key: string) =&gt; string, encryptionConfig?: EncryptedFieldsConfig): unknown =&gt; {
    if (!data || encryptionConfig === undefined) return data

    // 데이터 타입이 배열이면 배열의 요소를 순회
    if (Array.isArray(data)) {
        return data.map((item) =&gt; {
            if (Array.isArray(encryptionConfig)) {
                return deepCrypt(item, aesKey, cryptoFn, encryptionConfig[0])
            }

            return item
        })
    }

    // 데이터 타입이 object면 [key, value] 배열로 변환 후 순회
    if (typeof data === &#39;object&#39;) {
        return Object.fromEntries(
            Object.entries(data).map(([key, value]) =&gt; {
                if (typeof encryptionConfig === &#39;object&#39; &amp;&amp; !Array.isArray(encryptionConfig)) {
                    const fieldConfig = encryptionConfig[key]
                    return [key, deepCrypt(value, aesKey, cryptoFn, fieldConfig)]
                }

                return [key, value]
            })
        )
    }

    // 데이터 타입이 string이고, 암호화 필드가 true라면 암호화/복호화 진행
    if (typeof data === &#39;string&#39; &amp;&amp; encryptionConfig === true) {
        if (encryptionConfig) return cryptoFn(data, aesKey)
    }

    return data
}</code></pre><p>데이터 구조는 단순하지 않다. 많은 정보를 주고받는 요청의 경우, 객체가 중첩될 수도 있고, 배열이 중첩될 수도 있으며, 객체와 배열이 함께 중첩될 수도 있다. 따라서 이 구조를 하나씩 순회하며 암/복호화가 되어야 하는 필드를 만나면 암/복호화를 실행하는 함수를 만들어주었다. 이 함수는 두가지를 전제로 한다.</p>
<blockquote>
<ol>
<li>데이터 필드는 객체, 배열 또는 문자열이다.</li>
<li>객체 또는 배열 전체를 암호화하지 않는다. 각 속성이 객체 또는 배열 데이터를 가지고 있다면, 문자열 데이터를 만날때까지 중첩된 데이터를 타고 내려간다. 즉, 문자열 값만 암호화한다.</li>
</ol>
</blockquote>
<p>함수는 네개의 인자를 받는다: 암호화를 시킬 데이터, 암호화 시킬 필드에 대한 정보를 가지고 있는 객체, 암/복호화 함수, 암호화를 시킬 때 사용할 AES 키</p>
<p>위 두가지 전제를 바탕으로 암호화를 시킬 데이터와 암호화 정보 객체를 함께 순회한다. 배열이라면 배열 요소를 순회하고, 객체라면 키,값쌍을 배열로 변환하여 순회한다. 문자열을 만나면 필드의 암호화 여부를 확인하고, 필요시 암호화한다.</p>
<p>(이 함수는 회사의 백엔드 개발자분과 합의를 한 내용을 바탕으로 작성했다. 프로젝트에 따라 객체/배열/문자열이 아닌 데이터를 암호화 할 수도 있고, 객체/배열 데이터 전체를 암호화 해야할 수도 있다. 그때는 그 조건에 맞는 함수를 구현해야 한다.)</p>
<h3 id="axios-interceptor를-사용하여-요청응답-시-암복호화-실행">Axios Interceptor를 사용하여 요청/응답 시 암/복호화 실행</h3>
<p>마지막으로, axios interceptor를 사용하여 나가고 들어오는 모든 요청과 응답을 캐치한다. </p>
<pre><code>instance.interceptors.request.use(
    (config) =&gt; {
        const encryptionConfig = config.encryptionConfig
        const rsaKeyPair = getRSAKeyPair()

        if (encryptionConfig) {
            const { requestEncryption, responseEncryption, encryptedRequestFields } = encryptionConfig

            if (requestEncryption &amp;&amp; encryptedRequestFields) {
                const aesKey = generateAESKey()

                if (config.method?.toLowerCase() === &#39;get&#39; &amp;&amp; config.params) {
                    config.params = deepCrypt(config.params, aesKey, encryptAES, encryptedRequestFields)
                } else if (config.data) {
                    config.data = deepCrypt(config.data, aesKey, encryptAES, encryptedRequestFields)
                }

                config.headers[&#39;X-AES-KEY&#39;] = encryptRSA(aesKey, import.meta.env.VITE_PUBLIC_KEY)
            }

            if (responseEncryption &amp;&amp; rsaKeyPair) {
                config.headers[&#39;X-RSA-PUBLIC-KEY&#39;] = rsaKeyPair.publicKey.replace(/\r?\n/g, &#39;\\n&#39;)
            }
        }

        return config
    },
    (error) =&gt; Promise.reject(error)
)</code></pre><p>예시는 axios request interceptor이다. 요청을 캐치해서 <code>encryptionConfig</code>를 확인 한 뒤, <code>requestEncryption</code>이 true면 AES 키를 생성하고 <code>deepCrypt</code> 함수를 실행한다. <code>get</code> 요청이라면 <code>params</code>를, 그 외 요청이라면 <code>data</code> 객체를 함수의 인자로 넘겨준다. AES키와 AES 암호화 함수, 암호화 필드 설정 객체를 함께 넘겨주면 위에서 구현한대로 <code>deepCrypt</code> 함수가 순회를 돌며 암호화가 필요한 필드를 암호화해준다. 만약 받을 응답에도 암호화가 적용되어야 한다면 서버가 데이터를 암호화 할 수 있도록 헤더에 RSA public key를 넣어준다.</p>
<p>응답도 마찬가지로 axios response interceptor를 사용해서 동일한 로직으로 작성하면 된다. 다만, 응답 헤더에서 암호화된 AES 키를 꺼내 클라이언트에서 생성한 RSA private key로 복호화해야 데이터를 복호화할 수 있다. <code>deepCrypt</code> 함수에는 AES 복호화 함수를 넣어주면 알아서 순회를 돌면서 복호화를 시켜준다.</p>
<h2 id="마치며-">마치며 ..</h2>
<p>이번 경험을 통해 암호화에 대해 정말 많이 배웠다. 대칭 암호화와 비대칭 암호화를 더 잘 이해할 수 있게 되었고, 사소한 부분에서도 보안에 신경써야 데이터를 안전하게 지킬 수 있다는 것도 알게 되었다. 그리고 무엇보다, 마냥 멀고 어렵게만 느껴졌던 &#39;보안&#39;이라는 주제가 재밌다는 것을 느낀게 가장 큰 수확이었다. 소프트웨어 엔지니어로써 성장해나가기를 바라면서 꼭 공부하고 싶었던 두 분야가 데이터와 보안이었는데, 보안 분야의 맛보기로 좋은 기회였던 것 같다. 앞으로 네트워크에 대해서 더 공부를 하면서 보안에 대해서 깊게 공부해보고싶다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[의존성 주입과 테스트 가능한 코드: 뷰모델의 의존성 줄이기]]></title>
            <link>https://velog.io/@jess_apr/%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85%EA%B3%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%BD%94%EB%93%9C-%EB%B7%B0%EB%AA%A8%EB%8D%B8%EC%9D%98-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A4%84%EC%9D%B4%EA%B8%B0</link>
            <guid>https://velog.io/@jess_apr/%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85%EA%B3%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%BD%94%EB%93%9C-%EB%B7%B0%EB%AA%A8%EB%8D%B8%EC%9D%98-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A4%84%EC%9D%B4%EA%B8%B0</guid>
            <pubDate>Thu, 23 Jan 2025 14:35:30 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>최근에 뷰모델의 유닛 테스트를 진행하며 <strong>의존성 주입(Dependency Injection)</strong>의 중요성을 실감할 수 있는 경험을 했다. 이론적으로는 알고 있었지만, 실제로 어떻게 적용하고 왜 중요한지를 깊이 이해하지 못했었는데, 이번 경험을 통해 의존성이 높은 코드가 테스트하기 어려운 이유와 이를 개선하는 방법을 배웠다. 이번 글에서는 의존성을 낮추는 방법과 이를 통해 얻을 수 있는 이점에 대해 정리해보려한다.</p>
<h2 id="recoil과-뷰모델-그리고-높은-의존성">Recoil과 뷰모델, 그리고 높은 의존성</h2>
<p>뷰모델은 뷰와 모델의 다리 역할을 하다보니, 데이터를 가져오거나 변경하는 경우가 많다. 이번에 작성했던 뷰모델 역시 필터 목록 데이터를 가져와 새로운 필터를 추가하거나, 삭제하는 로직을 포함하고 있었다.</p>
<pre><code>export const useFilterLogic = () =&gt; {
    const [selectedFilters, setSelectedFilters] = useRecoilState(selectedFiltersAtom);

    const addFilter = () =&gt; {
        // 조건에 따라 필터 추가
    };

    const deleteFilter = () =&gt; {
        // 조건에 따라 필터 삭제
    };

    return { addFilter, deleteFilter };
};</code></pre><p>초기에는 뷰모델 내부에서 Recoil 상태를 직접 사용했는데, <strong>의존성</strong>이라는 개념에 대해서 잘못 이해하고 있었기 때문이다. 당시에는 필터 목록을 props로 받으면 다른 컴포넌트에 의해 데이터를 받게되고, 그 자체로 의존성이 생긴다고 생각했다. 반면, Recoil을 사용하면 외부 데이터를 받지 않아도 코드가 실행될 수 있으니 의존성을 줄일 수 있을것이라고 판단했다.</p>
<p>그렇지만 이건 완전히 잘못된 접근이었다. 뷰모델이 Recoil이라는 특정 라이브러리에 강하게 결합되면서, 유닛 테스트 코드 작성이 매우 어려워졌던 것이다. Recoil을 mocking하려 했지만, 그 과정 자체가 복잡했을 뿐만 아니라, 기존의 mocking 방법은 주로 UI가 존재하는 컴포넌트 내의 Recoil 코드를 테스트하는데 적합한 방법이어서 UI가 없는 뷰모델에 적용하기엔 적절하지 않았다.</p>
<h2 id="의존성-주입으로-개선하기">의존성 주입으로 개선하기</h2>
<p><code>useFilterLogic</code> 내부에서 Recoil을 사용하게 되면서, 이 뷰모델은 Recoil이 전달해주는 <code>selectedFilters</code>가 아니면 동작하지 않았다. 이로 인해 Recoil 없이 해당 뷰모델을 사용하거나 테스트하는 것이 불가능했다. 하지만 의존성 주입을 적용하여 Recoil 의존성을 제거하고, 데이터를 props로 전달받도록 구조를 변경하면 이러한 문제를 해결할 수 있다.</p>
<pre><code>interface FilterLogicProps {
    selectedFilters: string[];
    setSelectedFilters: (filters: string[]) =&gt; void;
}

export const useFilterLogic = ({ selectedFilters, setSelectedFilters }: FilterLogicProps) =&gt; {
    const addFilter = () =&gt; {
        // 조건에 따라 필터 추가
    };

    const deleteFilter = () =&gt; {
        // 조건에 따라 필터 삭제
    };

    return { addFilter, deleteFilter };
};</code></pre><p>이 코드에 의존성 주입을 적용하여 얻은 이점은 다음과 같다.</p>
<ol>
<li><strong>상태 관리 방식에 독립적이다.</strong>
Recoil뿐만 아니라 Redux, Context API 등 다양한 상태 관리 방식에도 적용할 수 있다. 뷰모델이 특정 상태 관리 라이브러리에 의존하지 않기 때문에 변경에 유연하다.</li>
<li><strong>재사용성 증가</strong>
Recoil의 selectedFilters가 아니더라도, 동일한 형태의 데이터(string[])와 상태 갱신 함수만 전달받으면 재사용할 수 있다.</li>
<li><strong>유닛 테스트 간소화</strong>
복잡한 Recoil mocking 과정을 거치지 않고도 유닛 테스트를 진행할 수 있다.</li>
</ol>
<h2 id="유닛테스트-코드">유닛테스트 코드</h2>
<pre><code>describe(&#39;useFilterLogic&#39;, () =&gt; {
    test(&#39;should add a new filter&#39;, () =&gt; {
        const selectedFilters = [&#39;filter1&#39;];
        const setSelectedFilters = jest.fn();

        const { addFilter } = useFilterLogic({ selectedFilters, setSelectedFilters });
        addFilter(&#39;filter2&#39;);

        expect(setSelectedFilters).toHaveBeenCalledWith([&#39;filter1&#39;, &#39;filter2&#39;]);
    });

    test(&#39;should not add a duplicate filter&#39;, () =&gt; {
        const selectedFilters = [&#39;filter1&#39;];
        const setSelectedFilters = jest.fn();

        const { addFilter } = useFilterLogic({ selectedFilters, setSelectedFilters });
        addFilter(&#39;filter1&#39;);

        expect(setSelectedFilters).not.toHaveBeenCalled();
    });

    test(&#39;should delete an existing filter&#39;, () =&gt; {
        const selectedFilters = [&#39;filter1&#39;, &#39;filter2&#39;];
        const setSelectedFilters = jest.fn();

        const { deleteFilter } = useFilterLogic({ selectedFilters, setSelectedFilters });
        deleteFilter(&#39;filter1&#39;);

        expect(setSelectedFilters).toHaveBeenCalledWith([&#39;filter2&#39;]);
    });
});</code></pre><p>Recoil에 대한 의존성이 사라지면서, 복잡한 Recoil mocking이 필요 없어졌다. 테스트를 위해서는 단순히 <code>selectedFilters</code>와 <code>setSelectedFilters</code>의 mock을 전달하여 동작만 검증하면 된다.</p>
<ul>
<li>특정 동작(필터 추가/삭제)을 수행했을 때 올바른 값이 상태 갱신 함수(<code>setSelectedFilters</code>)로 전달되는지</li>
<li>로직이 의도한 대로 동작하는지</li>
</ul>
<p>이제 뷰모델의 테스트는 특정 상태 관리 방식에 의존하지 않기 때문에 더 단순하고 독립적으로 작성할 수 있다. 심지어 상태 관리 라이브러리를 변경하더라도, 테스트 코드를 수정할 필요 없이 그대로 활용할 수 있다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>이번 경험을 통해 <strong>의존성을 낮춘다는 것</strong>의 의미를 더 깊이 이해할 수 있었다. 단순히 데이터를 전달하는 것이 아니라, 코드의 결합도를 낮추고 테스트 가능성을 높이며 재사용성을 증대시키는 것이 의존성 주입의 핵심이다. 게다가 상태 관리 방식에 구애받지 않는 유연한 설계를 가능하게 해주어, 코드의 유지보수성까지 향상시켰다.</p>
<p>여기에 추상화까지 잘 적용한다면 재사용성과 변경 가능성을 더욱 극대화할 수 있을 것이라고 생각했다. 예를 들어, 상태 관리 방식을 완전히 추상화한 인터페이스를 통해 뷰모델이 구현 세부사항을 전혀 알 필요 없게 만드는 것도 하나의 방법이 될 수 있겠다. 다만, 조금만 복잡한 코드를 마주해도 추상화를 어떻게 해야 할지 쉽게 갈피를 잡지 못하고 헤매고 있다. 이 부분은 앞으로 더 고민하고 개선해야 할 과제로 남겨두어야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[원하는 기능이 라이브러리에 없다면? 라이브러리 코드를 활용해서 커스텀 컴포넌트를 만들어보자!]]></title>
            <link>https://velog.io/@jess_apr/%EC%9B%90%ED%95%98%EB%8A%94-%EA%B8%B0%EB%8A%A5%EC%9D%B4-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%97%90-%EC%97%86%EB%8B%A4%EB%A9%B4-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%BD%94%EB%93%9C%EB%A5%BC-%EA%B0%80%EC%A0%B8%EC%99%80%EC%84%9C-%EB%B9%84%EC%8A%B7%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@jess_apr/%EC%9B%90%ED%95%98%EB%8A%94-%EA%B8%B0%EB%8A%A5%EC%9D%B4-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%97%90-%EC%97%86%EB%8B%A4%EB%A9%B4-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%BD%94%EB%93%9C%EB%A5%BC-%EA%B0%80%EC%A0%B8%EC%99%80%EC%84%9C-%EB%B9%84%EC%8A%B7%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 21 Dec 2024 10:06:16 GMT</pubDate>
            <description><![CDATA[<p>회사에서 작업하던 React Native 프로젝트가 이제 거의 막바지에 다다랐다. 앱을 만드는 동안에는 안드로이드를 기준으로 잡고 작업을 했었는데, 앱 출시는 안드로이드와 iOS 둘 다 해야하니 iOS에 맞춰 UI나 기능을 수정하는 작업을 하고있다. 당연하게도 안드로이드와 iOS는 다른 운영체제이기 때문에 안드로이드에서 잘 됐던 기능들이나 문제없이 나오던 화면들이 iOS에서는 잘 안되거나 레이아웃이 뒤틀리는 현상이 있다. 이번에 작업하면서 가장 손이 많이 갔던 부분은 키보드가 올라왔을 때 이를 감지하고 화면이 줄어들게 하는 것이었는데, 안드로이드는 기본적으로 키보드가 나타났을 때 화면 레이아웃을 자동으로 조절해 따로 설정을 해 줄 필요가 없지만, iOS는 이 작업을 해주지 않기 때문에 개발자가 수동으로 조정을 해줘야 하기 때문이다. </p>
<p>React Native 프로젝트를 처음 진행하는 것이다보니 대부분은 <code>&lt;KeyboardAvoidingView /&gt;</code> 태그가 누락된 기본적인 실수가 문제였고, 큰 어려움 없이 해결이 되었다. 바텀시트 라이브러리가 적용된 부분만 제외하고.</p>
<p>현재 프로젝트에서는 바텀시트를 다양하게 활용하기 위해 <code>@gorhom/bottom-sheet</code> 라이브러리를 사용하고 있다. 이렇게 바텀시트가 적용된 부분에서는 <code>&lt;KeyboardAvoidingView /&gt;</code> 태그를 적용해도 키보드 감지가 제대로 되지 않는 경우가 있어서, <code>&lt;TextInput /&gt;</code> 대신 라이브러리 자체에서 제공해주는 <code>&lt;BottomSheetTextInput /&gt;</code>을 사용해서 문제를 해결했다. 그런데 댓글을 입력하는 바텀시트 모달에서는 멘션 기능을 위해 또 다른 라이브러리의 <code>&lt;MentionInput /&gt;</code> 태그를 사용하고 있어서 <code>&lt;BottomSheetTextInput /&gt;</code> 태그를 적용해줄 수가 없었다.</p>
<h2 id="문제-해결을-위해-생각했던-여러-방안들">문제 해결을 위해 생각했던 여러 방안들...</h2>
<p>일부 바텀시트에서는 따로 <code>&lt;BottomSheetTextInput /&gt;</code>을 적용하지 않아도 키보드 감지가 되기도 해서 태그를 적용하지 않고 키보드 감지가 되도록 하기 위해 <code>&lt;KeyboardAvoidingView /&gt;</code>의 <code>behavior</code> 속성을 이리저리 바꿔보거나 바텀시트 모달 최상위 컴포넌트의 속성을 바꿔보았지만 해결이 되지 않았다.</p>
<p><code>&lt;MentionInput /&gt;</code>을 <code>&lt;BottomSheetTextInput /&gt;</code>로 감싸보거나, <code>&lt;BottomSheetTextInput /&gt;</code>를 안보이게 숨겨놓고 <code>&lt;MentionInput /&gt;</code>이 포커스되면 <code>&lt;BottomSheetTextInput /&gt;</code>도 함께 포커스 되도록 설정해보기도 포커스를 했다. 전자는 당연히 안됐고, 후자는 <code>&lt;BottomSheetTextInput /&gt;</code>가 <code>&lt;MentionInput /&gt;</code>의 포커스를 뺏어가며 댓글 작성이 불가능해져 적용할 수 없었다. </p>
<p>그렇다고 라이브러리를 바꾸거나 라이브러리 없이 기능을 구현하기엔 시간이 부족했다. 곰곰히 고민을 하다가 라이브러리 공식문서의 <code>&lt;BottomSheetTextInput /&gt;</code>에 대한 설명이 아래와 같이 써있는 것을 보고 다른 해결 방안을 생각해냈다. </p>
<blockquote>
<p>A pre-integrated <strong>TextInput</strong> that communicate with internal functionalities to allow Keyboard handling to work.</p>
</blockquote>
<p><code>&lt;BottomSheetTextInput /&gt;</code>이 <code>&lt;TextInput /&gt;</code>에 어떤 코드를 추가해서 만든 컴포넌트라면 <code>&lt;TextInput /&gt;</code>을 <code>&lt;MentionInput /&gt;</code>으로 대체해서 맞춤 컴포넌트로 만들 수 있지 않을까?</p>
<h2 id="라이브러리-코드를-가져와서-맞춤-컴포넌트로-재탄생시키기">라이브러리 코드를 가져와서 맞춤 컴포넌트로 재탄생시키기</h2>
<pre><code>const BottomSheetTextInputComponent = forwardRef&lt;
  TextInput,
  BottomSheetTextInputProps
&gt;(({ onFocus, onBlur, ...rest }, ref) =&gt; {
  const { shouldHandleKeyboardEvents } = useBottomSheetInternal();

  const handleOnFocus = useCallback(
    (args: NativeSyntheticEvent&lt;TextInputFocusEventData&gt;) =&gt; {
      shouldHandleKeyboardEvents.value = true;
      if (onFocus) {
        onFocus(args);
      }
    },
    [onFocus, shouldHandleKeyboardEvents]
  );

  const handleOnBlur = useCallback(
    (args: NativeSyntheticEvent&lt;TextInputFocusEventData&gt;) =&gt; {
      shouldHandleKeyboardEvents.value = false;
      if (onBlur) {
        onBlur(args);
      }
    },
    [onBlur, shouldHandleKeyboardEvents]
  );

  useEffect(() =&gt; {
    return () =&gt; {
      shouldHandleKeyboardEvents.value = false;
    };
  }, [shouldHandleKeyboardEvents]);

  return (
    &lt;TextInput
      ref={ref}
      onFocus={handleOnFocus}
      onBlur={handleOnBlur}
      {...rest}
    /&gt;
  );
});</code></pre><p>라이브러리에서 찾아본 <code>&lt;BottomSheetTextInput /&gt;</code>의 코드는 생각보다 간단했다. <code>useBottomSheetInternal</code>을 타고 올라가보면 바텀시트와 관련된 내부 상태를 제공해주는 훅이라는것을 알 수 있는데, 여기서 키보드와 관련된 이벤트 처리 여부를 제어하기 위한 상태인 <code>shouldHandleKeyboardEvents</code>를 가져오고있다. 그리고 input이 포커스되면 이 상태를 true로 바꿔주고, 포커스가 해제되면 false로 바꿔 키보드가 올라오고 내려가는것을 제대로 감지할 수 있게 해준다.</p>
<p>이 코드를 node_modules에서 직접 바꾸는 행위는 이후 이 컴포넌트를 사용할 때 문제를 발생시킬 수 있기 때문에 절대 하면 안된다. 대신 코드를 복사한 뒤, 새로운 컴포넌트를 새로 만든 후 사용하면 된다.</p>
<pre><code>interface BottomSheetMentionInputProps {
    value: string;
    onChange: (text: string) =&gt; void;
    placeholder?: string;
    partTypes?: PartType[];
    onLayout?: (event: LayoutChangeEvent) =&gt; void;
    onContentSizeChange?: (event: NativeSyntheticEvent&lt;TextInputContentSizeChangeEventData&gt;) =&gt; void;
    style?: StyleProp&lt;TextStyle&gt;;
}

function BottomSheetMentionInput({ value, onChange, placeholder, partTypes, onLayout, onContentSizeChange, style }: BottomSheetMentionInputProps) {
    const { shouldHandleKeyboardEvents } = useBottomSheetInternal();

    const handleOnFocus = useCallback(() =&gt; {
        shouldHandleKeyboardEvents.value = true;
    }, [shouldHandleKeyboardEvents]);

    const handleOnBlur = useCallback(() =&gt; {
        shouldHandleKeyboardEvents.value = false;
    }, [shouldHandleKeyboardEvents]);

    return (
        &lt;MentionInput
            value={value}
            onChange={onChange}
            placeholder={placeholder}
            placeholderTextColor={&#39;#CCCCCC&#39;}
            style={[styles.input, style]}
            partTypes={partTypes}
            onContentSizeChange={onContentSizeChange}
            onFocus={handleOnFocus}
            onLayout={onLayout}
            onBlur={handleOnBlur}
        /&gt;
    );
}</code></pre><p>이렇게 복사한 코드를 살짝 바꿔 내가 원하는대로 구현했다. 포커스가 되거나 해제될때 따로 이벤트를 전달해 줄 필요가 없기 때문에 props에서 <code>onFocus</code>, <code>onBlur</code>는 뺐고, 부모 컴포넌트에서 인풋을 제어할 일도 없어 <code>ref</code>도 제외했다. 대신 <code>&lt;MentionInput /&gt;</code>에 필수로 전달해주어야하는 <code>partTypes</code>를 추가하고, 그 외 필요한 것들을 props로 받았다. 가장 중요한 <code>shouldHandleKeyboardEvents</code> 상태를 변경해주는 함수는 동일하게 구현했다.</p>
<p>이렇게 구현한 컴포넌트를 <code>&lt;MentionInput /&gt;</code> 대신 적용해주었더니 <code>&lt;BottomSheetTextInput /&gt;</code>를 적용해준 것과 동일하게 키보드 감지가 아주 잘 되었다. 얏호!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React hooks는 어떻게 작동하나요? 궁금하니까 소스코드를 들춰보았습니다. (1)]]></title>
            <link>https://velog.io/@jess_apr/React-hooks%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9E%91%EB%8F%99%ED%95%98%EB%82%98%EC%9A%94-%EA%B6%81%EA%B8%88%ED%95%98%EB%8B%88%EA%B9%8C-%EC%86%8C%EC%8A%A4%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%93%A4%EC%B6%B0%EB%B3%B4%EC%95%98%EC%8A%B5%EB%8B%88%EB%8B%A4.-1</link>
            <guid>https://velog.io/@jess_apr/React-hooks%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9E%91%EB%8F%99%ED%95%98%EB%82%98%EC%9A%94-%EA%B6%81%EA%B8%88%ED%95%98%EB%8B%88%EA%B9%8C-%EC%86%8C%EC%8A%A4%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%93%A4%EC%B6%B0%EB%B3%B4%EC%95%98%EC%8A%B5%EB%8B%88%EB%8B%A4.-1</guid>
            <pubDate>Thu, 05 Dec 2024 14:28:39 GMT</pubDate>
            <description><![CDATA[<p>React hooks의 구현코드를 살펴보면 모든 훅에서 공통적으로 사용하는 함수가 보인다. 어떤 훅이든 초기 렌더링을 하는 단계에서는 <code>mountWorkInProgressHook()</code>이라는 함수를 부르고, 이후 재렌더링을 하는 단계에서는 <code>updateWorkInProgressHook()</code>을 부른다. 이 두 함수를 먼저 살펴보겠다.</p>
<h2 id="mountworkinprogresshook">mountWorkInProgressHook()</h2>
<pre><code>function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}</code></pre><h3 id="1-hook-객체-초기화">1. Hook 객체 초기화</h3>
<p><code>mountWorkInProgressHook</code> 함수가 호출되면, 우선 새로운 hook 객체를 생성한 후 초기화를 한다. Hook 객체에는 5개의 속성이 존재한다.</p>
<ul>
<li><code>memoizedState</code>: Hook의 현재 상태 또는 캐싱된 값을 저장. 컴포넌트가 렌더링 될 때 이 값을  참조한다.</li>
<li><code>baseState</code>: 이전 렌더링을 기준으로, 업데이트가 처리되기 전의 상태값을 저장.</li>
<li><code>baseQueue</code>: 렌더링 중 중단, 우선순위 작업 추가 등의 상황이 발생하여 처리되지 못한 상태 업데이트들을 이후에 이어서 작업할 수 있도록 저장하는 queue.</li>
<li><code>queue</code>: Hook에서 발생하는 업데이트를 순차적으로 관리하는 queue. </li>
<li><code>next</code>: 다음 hook으로의 참조를 저장. Hook들의 호출 순서를 보장해준다.</li>
</ul>
<p><code>next</code> 속성은 다음 호출될 hook 객체를 가리키는 포인터 역할을 하므로, hook이 연결리스트로 관리됨을 알 수 있다. 그리고 <code>memoizedState</code>에 hook이 관리하는 핵심 데이터가 저장된다. 이 두 속성은 모든 React hooks에서 필요하기 때문에, 특정 hook에 국한되지 않고 사용된다.</p>
<p>반면, 상태 관리 hook(<code>useState</code>, <code>useReducer</code>)은 렌더링 흐름을 직접적으로 제어하기 때문에 상태 변경의 순서와 값의 일관성이 더욱 엄격하게 관리되어야 한다. 따라서 <code>baseState</code>, <code>baseQueue</code>, <code>queue</code> 속성을 추가적으로 사용하여 상태 업데이트를 일관되게 관리한다. 이 속성들에 대해서는 이후 <code>useState</code>, <code>useReducer</code> hook의 코드를 살펴볼 때 더 자세히 알아보도록 하겠다.</p>
<h3 id="2-1-생성된-hook-객체가-첫번째일-경우-연결리스트의-시작점으로-설정">2-1. 생성된 hook 객체가 첫번째일 경우, 연결리스트의 시작점으로 설정</h3>
<p><code>workInProgressHook</code>은 연결 리스트의 마지막 Hook 노드를 가리키는 포인터이다. 만약 이 값이 null이라면 현재 렌더링 중인 컴포넌트에서 이 hook 객체가 첫번째로 생성되었다는 뜻이다. 그러니 Hook 객체를 연결리스트로 관리하는 속성인 <code>currentlyRenderingFiber.memoizedState</code>에 시작점으로 저장하고, 동시에 <code>workInProgressHook</code>에도 저장하여 해당 hook을 가리키도록 설정한다.</p>
<h3 id="2-2-생성된-hook-객체가-첫번째가-아닐-경우-기존-연결-리스트의-끝에-연결">2-2. 생성된 hook 객체가 첫번째가 아닐 경우, 기존 연결 리스트의 끝에 연결</h3>
<p>이미 연결리스트에 등록된 hook 객체가 있다면, React는 연결리스트의 끝에 새로운 hook 객체를 추가하여 실행된 순서대로 hook이 등록되도록 한다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/d7965160-8b55-4a7b-9ded-0bf9ab9f399d/image.png" alt=""></p>
<p>이 한줄의 코드는 두 단계로 나눠서 보면 이해하기가 더 쉽다. </p>
<p>우선, <code>workInProgressHook</code> 포인터는 업데이트가 안됐으므로, 연결리스트의 마지막 hook 객체를 가리키고 있을 것이다. 이 객체의 <code>next</code>에 새로 생성한 hook 객체를 등록한다. 그러면 hook 객체가 생성된 순서대로 연결리스트가 이어지며 순서를 보장할 수 있게 된다.</p>
<p>이후 <code>workInProgressHook</code>이 가장 최근에 생성한 hook을 가리키도록 <code>workInProgressHook.next</code>로 포인터를 이동해준다. 포인터가 항상 가장 마지막에 생성된 hook을 가리키게 함으로써 새로운 hook이 추가되더라도 연결리스트의 마지막에 정확하게 추가할 수 있게 된다.</p>
<h3 id="4-생성한-hook-객체-리턴">4. 생성한 hook 객체 리턴</h3>
<p>React Hook 함수가 참조할 수 있도록 마지막에 새롭게 생성한 hook 객체(<code>workInProgressHook</code>)를 리턴해준다.</p>
<hr>
<p><code>mountWorkInProgressHook()</code>에서 사용된 변수들이 어떤것을 가리키는지 조금 헷갈려서 설명을 덧붙여본다. 아래에는 세개의 <code>useState</code> hook을 사용하는 컴포넌트의 예시 코드이다.</p>
<pre><code>function MyComponent() {
  const [count, setCount] = useState(0);
  const [inputValue, setInputValue] = useState(&quot;&quot;);
  const [isLoading, setIsLoading] = useState(false);

  return (...)
}</code></pre><p><code>currentlyRenderingFiber</code>는 현재 렌더링 중인 컴포넌트를 나타내는 Fiber로, 컴포넌트와 그 상태를 저장한다. 따라서 <code>MyComponent</code>의 초기 렌더링이 끝나면 <code>currentlyRenderingFiber</code>에는 이렇게 컴포넌트 안에서 실행된 hook의 정보가 저장될 것이다.</p>
<pre><code>const currentlyRenderingFiber = {
  memoizedState: {
    memoizedState: 0,  // count
    next: {
      memoizedState: &quot;&quot;,  // inputValue
      next: {
        memoizedState: false,  // isLoading
        next: null,
      },
    },
  },
};</code></pre><p><code>currentlyRenderingFiber.memoizedState</code>는 fiber 노드가 관리하는 hook 연결리스트를 의미한다. 연결리스트의 시작점이 되는 hook 객체가 값으로 저장되지만, 그 안의 <code>next</code> 속성을 통해 연결리스트의 모든 hook 객체가 연결되어 접근할 수 있게된다.</p>
<p><code>workInProgressHook</code>는 단순하게 현재 어떤 hook을 처리하는지를 가리킨다. 예를 들어서, <code>MyComponent</code>가 처음 렌더링되는 과정에서 <code>inputValue</code>를 저장하는 <code>useState</code>가 실행될 차례라고 생각해보자.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/5b7819af-d0ae-4029-bfc4-358eee805af3/image.png" alt=""></p>
<p>그럼 <code>workInProgressHook</code>은 실행 순서가 꼬이지 않도록 현재 <code>inputValue</code>를 저장하는 <code>useState</code>가 실행중임을 가리킨다.</p>
<hr>
<h2 id="updateworkinprogresshook">updateWorkInProgressHook()</h2>
<pre><code>function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        throw new Error(
          &#39;Update hook called on initial render. This is likely a bug in React. Please file an issue.&#39;,
        );
      } else {
        throw new Error(&#39;Rendered more hooks than during the previous render.&#39;);
      }
    }

    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }

  return workInProgressHook;
}</code></pre><h3 id="1-이전-렌더링-상태-확인">1. 이전 렌더링 상태 확인</h3>
<pre><code>  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }</code></pre><p>React가 컴포넌트를 렌더링할 때, 이전 렌더링 시에 생성된 hook들의 상태를 기억하고 있어야 한다. 그래야 hook의 값이 변했는지, 호출 순서가 바뀌진 않았는지를 판단할 수 있기 때문이다. <code>currentHook</code>과 <code>nextCurrentHook</code>은 이전에 렌더링 된 상태를 추적하는 역할을 한다. <code>currentHook</code> 이전 렌더링 시점의 현재 hook를 가리키고, <code>nextCurrentHook</code>은 이전 렌더링 시점의 다음 hook을 가리킨다.</p>
<h4 id="1-1-currenthook이-null이라면-이전-렌더링-정보에서-연결리스트의-시작점을-가져와-설정">1-1. currentHook이 null이라면, 이전 렌더링 정보에서 연결리스트의 시작점을 가져와 설정</h4>
<p>컴포넌트의 첫번째 hook을 처리하는 중이라면, 아직 아무것도 추적되지 않았기 때문에 <code>currentHook</code>의 값이 null이다. 따라서 이전에 렌더링된 hook 연결리스트의 시작점을 가져와야 한다.</p>
<p><code>currentlyRenderingFiber</code>의 <code>alternate</code> 속성은 컴포넌트가 이전에 렌더링되었을때의 상태를 저장하는 속성이다. 따라서 <code>alternate</code> 속성을 확인 후, 이전 렌더링 정보가 있다면 <code>nextCurrentHook</code>에 이전 렌더링 상태의 <code>memoizedState</code> 값을 할당해준다.</p>
<p>오류나 비정상적인 상태가 발생해 이전 렌더링의 상태값이 null이라면, 이후에 에러를 핸들링할 수 있도록 <code>nextCurrentHook</code>에 null 값을 할당해준다.</p>
<h4 id="1-2-currenthook이-있다면-이전-렌더링-상태의-포인터-이동">1-2. currentHook이 있다면 이전 렌더링 상태의 포인터 이동</h4>
<p>첫번째 hook이 실행될때 <code>current.memoizedState</code>를 가져왔으므로, 그 다음 hook부터는 따라갈 <code>next</code> 속성이 존재한다. <code>nextCurrentHook</code>에 <code>currentHook.next</code>를 할당해주어 hook이 순서대로 실행될 수 있도록 한다.</p>
<h3 id="2-작업을-진행할-hook-객체-확인">2. 작업을 진행할 hook 객체 확인</h3>
<pre><code>  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }</code></pre><p><code>nextWorkInProgressHook</code>은 현재 렌더링중인 컴포넌트에서 작업을 처리하고자 하는 hook 객체를 저장한다. 작업을 진행할 hook이 첫번째라면 현재 렌더링중인 fiber의 <code>memoizedState</code> 속성을 불러와 시작점을 설정해준다. 첫번째로 실행된 hook이 아니라면 <code>next</code> 속성으로 포인터를 이동해준다.</p>
<h3 id="3-hook-상태-갱신-또는-새로운-hook-생성">3. Hook 상태 갱신 또는 새로운 hook 생성</h3>
<h4 id="3-1-실행할-hook이-있다면-포인터를-이동하여-상태-갱신">3-1. 실행할 hook이 있다면 포인터를 이동하여 상태 갱신</h4>
<pre><code>  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  }</code></pre><p>작업을 처리하고자 하는 hook을 <code>nextWorkInProgressHook</code>에 담아두었으니, 이것을 실제로 실행할 수 있도록 <code>workInProgressHook</code> 포인터를 이동시킨다. 이전 렌더링 상태를 추적하는 <code>currentHook</code>도 포인터를 함께 이동해주어 순서와 상태가 제대로 추적될 수 있게 해준다.</p>
<h4 id="3-2-실행할-hook이-없고-이전-렌더링의-이어지는-hook도-없다면-예외-처리">3-2. 실행할 hook이 없고, 이전 렌더링의 이어지는 hook도 없다면 예외 처리</h4>
<pre><code>  else {
    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        throw new Error(
          &#39;Update hook called on initial render. This is likely a bug in React. Please file an issue.&#39;,
        );
      } else {
        throw new Error(&#39;Rendered more hooks than during the previous render.&#39;);
      }
    }
    ...
  }</code></pre><p>실행할 hook이 없다면, 이전 렌더링에서 이어지는 hook을 확인한다. 이전 렌더링 상태에서도 더 이상 불러올 hook이 없다면 다음과 같은 이유로 예외 처리를 한다.</p>
<p>이전 렌더링 상태 자체가 존재하지 않는다면, 초기 렌더링에서 훅을 업데이트 하려는 경우이다. 초기 렌더링은 이전 상태나 업데이트 큐가 없고, 모든 hook을 새로 생성해야하며, fiber 구조가 완전히 준비되지 않아 hook의 초기값을 렌더링 중에 계산하기 때문에 초기 렌더링 시 hook을 업데이트하게 되면 호출 순서가 꼬이거나 무한 렌더링 루프에 빠질 위험이 생긴다. 이를 방지하기 위해 <strong>&quot;Update hook called on initial render. This is likely a bug in React. Please file an issue.&quot;</strong> 에러를 띄워준다. useState를 선언한 후, setState 함수를 useEffect나 다른 함수 안에서 부르지 않고 바로 호출하는 경우에는 초기 렌더링이 실행될때 hook을 업데이트 하게 되어 해당 에러가 발생한다.</p>
<pre><code>const ErrorComponent = () =&gt; {
  const [count, setCount] = useState(0);

  // 렌더링 중 상태 업데이트 → 에러 발생
  setCount((prev) =&gt; prev + 1);

  return &lt;div&gt;Count: {count}&lt;/div&gt;;
};</code></pre><p>이전 렌더링 상태는 존재한다면, 현재 렌더링 중 호출된 hook이 이전 렌더링에서 호출된 hook보다 많은 경우이다. 이 경우에는 hook의 호출 순서가 어긋나 상태를 제대로 관리할 수 없게 될 가능성이 높아지므로 <strong>&quot;Rendered more hooks than during the previous render.&quot;</strong> 에러를 띄워준다. 조건문이나 반복문 안에서 hook을 호출하는 경우, 컴포넌트가 렌더링될때마다 호출되는 hook의 개수가 달라질 수 있기 때문에 해당 에러가 발생한다.</p>
<pre><code>const ErrorComponent = () =&gt; {
  const [shouldRunEffect, setShouldRunEffect] = useState(false);

  if (shouldRunEffect) {
    // 조건문 안에서 훅을 호출 (에러 발생)
    useEffect(() =&gt; {
      console.log(&quot;Effect executed&quot;);
    }, []);
  }

  return (
    &lt;button onClick={() =&gt; setShouldRunEffect(true)}&gt;
      Toggle Effect
    &lt;/button&gt;
  );
};</code></pre><p>(Hook 호출 규칙을 어기게 되면 빨갛게 에러가 떠서 골머리를 앓았는데, 여기서 만나니 반갑다 😚. 에러가 뜨는 이유를 알았으니 앞으로는 조금 더 잘 피해가거나, 디버깅이 쉬워질 것같다!)</p>
<h4 id="3-3-실행할-hook은-없으나-이전-렌더링의-이어지는-hook이-있을-경우-새-hook-객체-생성">3-3. 실행할 hook은 없으나, 이전 렌더링의 이어지는 hook이 있을 경우 새 hook 객체 생성</h4>
<pre><code>  {
    ...
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }</code></pre><p>현재 렌더링에서 실행할 hook이 없더라도, 이전 렌더링 상태에서 hook이 존재한다면 이를 기반으로 새 hook을 안전하게 생성해서 일관성을 유지할 수 있다. 따라서 이전 렌더링 상태를 기반으로 새 hook 객체를 생성한 후, <code>workInProgressHook</code>이 null이라면 처음 실행된 hook이므로 현재 Fiber 노드의 <code>memoizedState</code>에 저장하고, <code>workInProgressHook</code>이 존재한다면 해당 hook 객체에 이어서 연결해준다.</p>
<h3 id="4-업데이트된-hook-객체-리턴">4. 업데이트된 hook 객체 리턴</h3>
<p>함수가 현재 렌더링 중인 컴포넌트의 Hook 상태를 찾고, 이전 렌더링의 Hook 상태와 연결 리스트를 따라가면서 동기화하며 hook 객체를 업데이트했다. 따라서 업데이트한 hook 객체를 리턴해 React Hook 함수가 참조할 수 있도록 한다.</p>
<hr>
<p><strong>참고자료</strong></p>
<ul>
<li><a href="https://incepter.github.io/how-react-works/docs/react-dom/how.hooks.work/">How hooks work</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Hooks는 어떻게 작동하나요? 궁금하니까 소스코드를 들춰보았습니다. - 기초개념]]></title>
            <link>https://velog.io/@jess_apr/React-hooks%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9E%91%EB%8F%99%ED%95%98%EB%82%98%EC%9A%94-%EA%B6%81%EA%B8%88%ED%95%98%EB%8B%88%EA%B9%8C-%EC%86%8C%EC%8A%A4%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%93%A4%EC%B6%B0%EB%B3%B4%EC%95%98%EC%8A%B5%EB%8B%88%EB%8B%A4.-%EA%B8%B0%EC%B4%88%EA%B0%9C%EB%85%90%ED%8E%B8</link>
            <guid>https://velog.io/@jess_apr/React-hooks%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9E%91%EB%8F%99%ED%95%98%EB%82%98%EC%9A%94-%EA%B6%81%EA%B8%88%ED%95%98%EB%8B%88%EA%B9%8C-%EC%86%8C%EC%8A%A4%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%93%A4%EC%B6%B0%EB%B3%B4%EC%95%98%EC%8A%B5%EB%8B%88%EB%8B%A4.-%EA%B8%B0%EC%B4%88%EA%B0%9C%EB%85%90%ED%8E%B8</guid>
            <pubDate>Thu, 28 Nov 2024 14:29:12 GMT</pubDate>
            <description><![CDATA[<p>최근, <code>useMemo</code>와 <code>useCallback</code>을 쓸 일이 잦아지면서 React Hooks에 대해 복습을 하며 노션 글을 정리하고있었다. 그러다 문득 들어간 한 블로그에서 모든 리액트 훅이 비슷하게 작동한다는 뉘앙스의 문장을 스쳐보게 되었다. 처음 볼때는 아무 생각이 없었는데, 이상하게 그 문장이 자꾸 자기전에도 씻을때도 밥을 먹을때도 머릿속에 스멀스멀 떠오르는 것이었다. <code>useMemo</code>, <code>useCallback</code>은 그렇다고 쳐도, <code>useRef</code>나 <code>useEffect</code>는 아주 다른 역할을 하는데 동작 원리가 어떻게 비슷하다는거지? 자꾸 생각이 나길래 궁금해서 리액트 깃허브에 들어가보게 되었다. 물론 세계 각지의 코딩 천재들이 작성한 코드는 아주 어려웠기때문에^_^ 오픈소스 코드 맛보기도 해볼겸, 궁금증도 풀겸 하며 살짝 발만 담궈보았다.</p>
<hr>
<h2 id="fiber란">Fiber란?</h2>
<p>본격적으로 글을 작성하기 전에 한가지 짚고 넘어가야하는 개념이 있다. 바로 <strong>Fiber</strong>라는 개념이다.</p>
<p>React에서 &quot;렌더링&quot;이란 흔히 2단계로 나뉜다.</p>
<p><strong>1. Render Phase (렌더 단계)</strong>
React 컴포넌트 함수를 호출하여 새로운 React element tree(Virtual DOM)를 생성하고, 이전 트리와 비교하는 단계 (브라우저의 실제 DOM에는 아무 영향도 주지 않음)</p>
<p><strong>2. Commit Phase (커밋 단계)</strong>
변경된 내용을 브라우저의 실제 DOM에 반영하는 단계. DOM 조작, 화면 업데이트, ref 콜백 호출 등이 이 시점에 일어난다.</p>
<p><strong>Reconciliation(재조정) 작업</strong>은 렌더 단계에서 React가 수행하는 핵심 작업으로, 이전 렌더 트리와 새로운 트리를 비교하여 어떤 부분을 업데이트할지 계산하는 과정이다.</p>
<ol>
<li>트리 비교 (diffing): 새롭게 생성된 트리와 이전 트리를 비교하며, 같은 위치에 있는 노드들의 타입을 기준으로 판단한다.<ul>
<li>타입이 다르면 해당 서브트리를 완전히 교체한다.</li>
<li>타입이 같으면, props를 비교하고, 필요시 하위 요소들도 재귀적으로 비교한다.</li>
<li>리스트 요소의 경우, key 속성을 활용해 어떤 항목이 추가/삭제/재배열되었는지 판단한다.</li>
</ul>
</li>
<li>변경 사항 계산: 비교 결과를 바탕으로 어떤 DOM 노드가 생성, 업데이트, 제거되어야 하는지를 결정한다.</li>
</ol>
<p>React 15까지는 <strong>Stack Reconciler</strong>라는 방식으로 재조정 작업을 수행했다. 이 방식은 컴포넌트 트리를 재귀 호출로 탐색하고 렌더링하는 구조였는데, 한 번 렌더링이 시작되면 모든 작업을 끝마칠 때까지 중단할 수 없었다.
이로 인해 브라우저의 메인 스레드를 장시간 점유하게 되고, 그동안 사용자 이벤트나 애니메이션 같은 중요한 작업이 차단되는 문제가 발생했다. 또한, 모든 작업이 동일한 우선순위로 처리되었기 때문에, 급한 작업(예: 사용자 입력 응답)이 덜 중요한 작업(예: 백그라운드 데이터 렌더링)에 의해 지연될 수 있었다.</p>
<p>이러한 문제를 해결하기 위해 React 16부터는 <strong>Fiber Reconciler</strong>가 도입되었다. Fiber는 컴포넌트 트리를 Fiber 노드라는 작은 단위로 나누고, 이를 반복하는 방식으로 처리한다. (일반적으로 하나의 Fiber 노드는 하나의 React 컴포넌트를 나타내지만, 상황에 따라 1:1로 대응되지 않을 수도 있다.) Fiber는 각 작업 단위에 우선순위를 부여할 수 있어, 사용자 입력처럼 시급한 작업은 먼저 처리하고, 상대적으로 덜 중요한 작업은 나중에 처리할 수 있다.
또한, 작업이 노드 단위로 쪼개져 있기 때문에, 작업을 도중에 일시 중단 할 수 있고, 필요한 경우 나중에 이어서 재개 하는 것도 가능하다. 이로 인해 React는 사용자 경험을 해치지 않으면서도 큰 렌더링 작업을 효율적으로 분산시킬 수 있게 되었다.</p>
<h2 id="fiber와-react-hooks의-관계">Fiber와 React Hooks의 관계</h2>
<p>아무튼 이 Fiber라는 개념이 왜 React hooks와 관계가 무슨 있냐하면, 바로 React가 Fiber 노드의 <code>memoizedState</code>라는 필드를 사용하여 hook들의 상태 데이터를 관리한다는 사실이다. 그러니까, 어떤 hook이든 Fiber 노드를 기반으로 호출 순서가 추적되고, hook에 넣은 값은 <code>memoizedState</code>에 저장되며, 상황에 따라 <code>memoizedState</code> 안에 저장된 값이 반환되거나 업데이트된다.</p>
<h3 id="그런데-왜-fiber가-react-hooks를-관리하지">그런데 왜 Fiber가 React Hooks를 관리하지?</h3>
<p>Reconciler는 컴포넌트가 어떻게 변화하고 업데이트할지를 결정한다. 따라서 컴포넌트의 상태와 렌더링에 밀접하게 연관된 hook이 Fiber reconciler에 의해 관리되는 것은 자연스러운 현상이다. </p>
<h3 id="fiber가-react-hooks를-어떻게-관리하는데">Fiber가 React Hooks를 어떻게 관리하는데?</h3>
<p>컴포넌트가 처음 렌더링이 되고 hooks가 호출되면, React는 Hook 객체를 생성하여 Fiber의 <code>memoizedState</code>에 연결 리스트 형태로 객체를 추가한다. 두번째 렌더링부터는 이 <code>memoizedState</code> 연결 리스트를 순회하며 Hook의 호출 순서에 따라 저장된 Hook 객체를 탐색한다. 상태가 업데이트되지 않으면 값을 그대로 사용하고, 상태값이 업데이트되었다면 새로운 값을 계산하여 <code>memoizedState</code>에 저장한다.</p>
<p>여기서 <strong>&quot;최상위에서만 Hooks를 호출해야 한다&quot;</strong>는 React Hooks의 규칙을 이해할 수 있다. 조건이나 반복문, 일반 함수 등에 의해 Hook이 순서대로 호출되지 않으면 연결리스트에서 Hook 객체와 상태값이 제대로 매핑이 되지않아 잘못된 상태를 참조할 수 있으니 이러한 규칙이 생긴 것이다. </p>
<h3 id="개인적으로-궁금해서-조금-더-찾아본-정보">개인적으로 궁금해서 조금 더 찾아본 정보</h3>
<ol>
<li><p>왜 React Hooks를 키-값 쌍이 아닌 연결리스트로 관리할까?
우선, 연결리스트는 키-값 쌍에 비해 Hook의 추가 및 삭제가 간단하며, 중간에 일시 중단을 하고 이후에 중단된 지점부터 이어나갈 수 있는 구조를 제공한다. 그리고 키-값 쌍으로 Hooks를 관리하게 되면 사용자가 키를 관리해야하는 번거로움이 생기고, 같은 컴포넌트에서 동일한 Hook을 여러번 호출해도 충돌하지 않는 고유 키를 생성하기 위해 추가적인 로직이 필요하다. 키-값 쌍으로 Hooks를 관리하면 최상위가 아니더라도 Hooks를 불러올 수 있지않나? 하는 생각이 있었지만, 그것보다도 연결리스트로 Hooks를 관리하는 것이 Fiber 아키텍처에 훨씬 더 최적화 된 방법이라는 것을 알게되었다.</p>
</li>
<li><p>왜 Hooks는 함수 컴포넌트 내에서만 호출해야 할까?
리액트의 클래스 컴포넌트에서는 상태와 생명주기 메서드가 분리되어있다. 상태와 생명주기를 통합하여 관리하는 hook의 목적과는 맞지 않다. 따라서 클래스 컴포넌트의 생명주기 메서드와 충돌을 피하기 위해 클래스 컴포넌트에서는 React Hooks를 사용할 수 없다. 또, 함수 컴포넌트 외부에서 Hooks를 호출하려 하면 이를 등록하고 관리할 Fiber 노드가 없기 때문에 사용이 불가능하다. React Hooks가 함수 컴포넌트에서 사용하기 위해 만들어졌다는 것은 알고있었지만, 이렇게 찾아보면서 Hooks가 어떻게 함수 컴포넌트 사용에 최적화되어 설계되었는지 알 수 있어 참 재밌다고 생각했다.</p>
</li>
</ol>
<p>이러한 내용들을 살펴보니 왜 그 블로그글에 훅들이 비슷한 과정으로 작동한다는 내용이 있었던 것인지 알겠다. 물론 동일한 도구를 사용한다는 것이지, 동일한 로직으로 작동한다는 뜻은 아니었지만. 따라서 이 글은 각 hook들이 <code>memoizedState</code>에 저장된 값을 어떤 상황에서 어떻게 사용하는지에 대해 살펴보려 한다.</p>
<h2 id="react-hooks의-내부-구현-코드가-위치한-곳">React hooks의 내부 구현 코드가 위치한 곳</h2>
<p>React hooks가 구현되어 있는 파일은 <code>react/packages/react-reconciler/src/ReactFiberHooks.js</code> 경로에서 찾을 수 있었다. 위에서 말했다시피, React hooks의 상태와 동작 관리가 Fiber Reconciler와 관련이 있기 때문에, Reconciler 계층에서 hook이 구현된다. </p>
<hr>
<p>글을 쓰기 전에는 코드만 보고 하나로 끝내려고 했는데, 코드 구현의 기반이 되는 개념에 대해서 알아보다보니 함께 기록을 남기면 좋을것같다는 생각이 들어 나눠서 쓰게 되었다. 다음 글에서부터는 리액트 깃허브에서 찾아온 코드를 살펴보도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Masonry List에서 발생한 무한 로딩 이슈 - Debouncing으로 해결하기]]></title>
            <link>https://velog.io/@jess_apr/Masonry-List%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EB%AC%B4%ED%95%9C-%EB%A1%9C%EB%94%A9-%EC%9D%B4%EC%8A%88-debouncing%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jess_apr/Masonry-List%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EB%AC%B4%ED%95%9C-%EB%A1%9C%EB%94%A9-%EC%9D%B4%EC%8A%88-debouncing%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 18 Oct 2024 14:27:48 GMT</pubDate>
            <description><![CDATA[<p>지난주 클라이언트 분께 전달드린 산출물에 대한 피드백을 받았다. &quot;무한 로딩&quot;, &quot;멈춤 현상&quot;이라는 단어가 몇번 중복되어 눈에 띄었다. 특정 페이지에서 무한 로딩이 자꾸 발생하고, 앱이 멈춰 사용이 불편하다는 것이었다. 다른 마이너한 이슈들은 다음 산출물 전달일까지 고치면 되겠지만, 해당 이슈는 빨리 처리를 해야할 것 같아 코드를 열어보았다.</p>
<h2 id="문제-상황">문제 상황</h2>
<p>새로 추가한 &#39;검색 페이지&#39;에서 이슈가 발생하였다. 검색 페이지로 이동하여 검색창에 키워드를 입력하고, 결과에 대한 첫 10개의 데이터를 받기까지는 문제가 없다. 하지만 스크롤을 조금만이라도 내리면 무한 로딩이 시작되었다.</p>
<h2 id="문제-분석-과정">문제 분석 과정</h2>
<p>(1) 스크롤을 내릴때 (2) 로딩이 발생 - 이 두가지 상황을 보니 무한 스크롤을 구현하기 위해 사용한 <code>useInfiniteQuery</code>와 관련된 문제라는 생각이 들었다. 우선 <code>useInfiniteQuery</code>의 <code>QueryFn</code> 속성에 넣은 함수가 API 요청을 하고 있어 <code>QueryFn</code> 안에 로그를 찍었다. 그리고 다음 페이지를 요청하거나 리로딩을 할 때도 <code>QueryFn</code>이 호출되니 <code>fetchNextPage</code>와 <code>refetch</code>가 호출되는 부분에도 로그를 찍었다.</p>
<p>초기 데이터를 가져오거나, 쿼리키가 바뀌어 데이터가 불러와지는 상황은 전혀 문제가 없었다. 리로딩을 하는 상황도 마찬가지로 API가 한번만 호출되었다. 문제는 <code>fetchNextPage</code>였다. 스크롤을 움직일때마다 무수히 많은 스크롤이벤트가 발생하며 <code>QueryFn</code>을 호출하고있었다.</p>
<p>무한 스크롤을 사용하는 페이지가 여러개 있었는데, 다른 페이지에서는 해당 문제가 전혀 없었다. 왜 이 페이지에서만 문제가 생겼을까 하고 차이점을 찾아보니 다른 페이지들은 <code>&lt;FlatList /&gt;</code>를 사용하고 있었고, 문제가 발생한 페이지에서는 <code>&lt;MasonryList /&gt;</code> 라는 태그를 사용하고 있었다.</p>
<h3 id="flatlist-vs-masonry-list">FlatList vs Masonry List</h3>
<p>Masonry List란 Masonry 레이아웃을 구현할 수 있도록 해주는 커스텀 컴포넌트를 말한다. 일종의 그리드 시스템인데, 일반적인 그리드 시스템을 사용하는 FlatList와는 차이가 있다. 
<img src="https://velog.velcdn.com/images/jess_apr/post/b3423659-8282-43f9-884d-b1328f484adc/image.png" alt=""></p>
<ul>
<li>FlatList: 일정한 그리드 레이아웃을 사용한다. 모든 아이템이 동일한 높이와 너비를 가지기 때문에 아이템의 크기가 다를 경우 높이와 너비가 맞춰지며 빈 공간이 생길 수 있다.</li>
<li>Masonry List: 아이템의 각 열이 서로 다른 높이로 자연스럽게 정렬된다. 아이템이 동일하지 않은 높이를 가져도 각 열의 높이를 유동적으로 맞춰주어 빈 공간이 생기지 않는다.</li>
</ul>
<p>열의 높이가 정해지는 방식은 다르다는 점만 빼면 목록을 보여줄때 사용된다는 쓰임새가 비슷하기도 하고, 내가 사용한 <code>react-native-masonry-list</code>라는 라이브러리를 보면 태그의 속성이 FlatList와 완전히 동일하다. 그래서 당연히 Masonry List가 FlatList를 확장해서 만든것이라고 생각했던 것이 나의 큰 실수였다.</p>
<pre><code>// 다른 페이지
&lt;FlatList
    data={data}
    keyExtractor={item =&gt; item.id}
    numColumns={2}
    renderItem={renderItem}
    onEndReachedThreshold={0.5}
    onEndReached={() =&gt; handleFetchNextPage()}
/&gt;

// 검색 페이지
&lt;MasonryList
    data={data}
    keyExtractor={item =&gt; item.id}
    numColumns={2}
    renderItem={renderItem}
    onEndReachedThreshold={0.5}
    onEndReached={() =&gt; handleFetchNextPage()}
/&gt;</code></pre><p>이렇게 비슷하게 생겼어도, 각각의 리스트가 스크롤을 감지하는 방법은 너무나도 달랐다.</p>
<p><strong>FlatList는 단순한 그리드 레이아웃을 사용</strong>하기 때문에 스크롤 뷰의 전체 높이와 현재 스크롤 위치를 비교하는 방식으로 스크롤의 끝에 도달했는지를 판단한다. 이 방식은 상대적으로 단순하기 때문에 위치를 정확하게 감지할 수 있다. 그리고 FlatList는 <code>onEndReached</code> 이벤트를 한 번만 트리거하도록 설계되어서 스크롤 위치가 다시 바뀌지 않는 한 함수를 추가로 호출하지 않는다. 이러한 설계가 직관적인 계산 방식과 맞물려 스크롤이 마지막에 도달했을때 함수를 중복없이 한번만 호출하게 된다.</p>
<p><strong>Masonry List는 비대칭 그리드 레이아웃을 사용</strong>하기 때문에 스크롤 계산 방식이 훨씬 더 복잡하다. 열마다 높이가 다르기 때문에 각 열의 끝을 계산하는 과정이 FlatList보다는 덜 정확하고, 스크롤 뷰의 크기가 일정하지 않아 스크롤 끝에 도달했다고 하는 판단하는 시점이 매번 달라지기 때문에 <code>onEndReached</code>가 중복되어 호출될 가능성이 높아진다. 또, 한 열은 끝에 도달했는데 다른 열은 끝에 도달하지 않는 상황도 생길 수 있는데, 이런 FlatList에서는 일어나지 않을 예외적인 상황에서도 중복 호출이 될 수 있다. </p>
<p><code>react-native-masonry-list</code> 라이브러리의 깃허브 페이지에는 <code>onEndReached</code> 속성이 <code>FlatList</code> 방식과 똑같이 동작한다고 했지만, 나의 케이스는 어찌되었든 예외 상황이었다. 어떤 방식으로 스크롤 위치를 감지하는지 정확히는 모르겠지만 여러 상황을 종합해봤을때 <code>FlatList</code>의 속성을 상속받거나 작동 원리가 완전하게 동일하지는 않은 것 같아 그냥 안전 장치를 추가하기로 했다.</p>
<h2 id="문제-해결-debouncing-적용">문제 해결: Debouncing 적용</h2>
<p>스크롤 이벤트가 여러번 발생해도 API 호출을 딱 한번만 하도록 하기 위해 Debouncing을 적용하기로 했다. 스크롤 이벤트가 계속 발생하다가 마지막 이벤트가 발생한 뒤 일정 시간이 지나면 API 호출이 발생할 것이다. 스크롤에 손가락을 얹고 스크롤을 떼기까지 마지막 이벤트 단 한번만 호출이 되는 것이다. 따라서 사용자 입장에서는 FlatList와 큰 차이를 못 느끼게 된다.</p>
<p>lodash 라이브러리를 사용할수도 있었겠지만, 한번 사용하자고 라이브러리를 설치하기는 싫어서 그냥 간단하게 함수로 구현했다.</p>
<pre><code>function debounce(func: Function, delay: number) {
    let timeout: NodeJS.Timeout;

    return (...args: any[]) =&gt; {
        if (timeout) clearTimeout(timeout);  // 기존 타이머를 제거
        timeout = setTimeout(() =&gt; {         // 새로운 타이머 시작
            func(...args);                   // 지연 시간이 지나면 원래 함수 호출
        }, delay);
    };
}</code></pre><ul>
<li>timeout 변수에는 setTimeout으로 반환된 타이머 Id를 저장. 이 값은 기존의 타이머를 취소하거나 새로 시작할 때 사용.</li>
<li>이미 timeout 변수에 할당된 값이 있으면 해당 Id값을 사용하여 기존 타이머를 제거</li>
<li>기존 타이머 제거 후 새로운 타이머 시작</li>
<li>만약 지정한 시간동안 debounce 함수가 호출되지 않는다면 콜백함수를 실행</li>
</ul>
<pre><code>const handleFetchNextPage = useCallback(
    debounce(() =&gt; {
        if (!hasNextPage) return;

        if (!isFetchingNextPage) {
            fetchNextPage();
        }
    }, 200), [isFetchingNextPage, fetchNextPage, hasNextPage],
);</code></pre><p>문제가 되는 <code>fetchNextPage()</code> 함수를 감싸주었다. 그리고 혹시라도 페이지의 다른 상태들이 변경되면서 리렌더링이 발생하여 함수가 재생성되면 타이머가 꼬일 수 있기 때문에 <code>useCallback</code>을 사용하였다. 최신 상태가 반영되어야 하는 <code>isFetchingNextPage</code>, <code>fetchNextPage</code>, <code>hasNextPage</code> 값만 의존성 배열에 넣어주었다.</p>
<hr>
<p>처음 피드백을 받았을때는 살짝 당황했었는데, 생각보다 금방 문제를 해결해서 다행이었다. 잘 몰랐던 Masonry List에 대한 개념도 알게되었고, 아무생각없이 UI 라이브러리를 사용하는 것에 대해 경각심을 다시 깨우친 계기가 되었다. UI와 관련된 라이브러리는 지원이 멈출 경우 호환성 문제도 있고, 내가 직접 원하는대로 커스텀하기에는 한계가 있어 사용을 최대한 안하려고 한다. 하지만 직접 구현하지 못하는 부분이 분명 있기에 사용할때 더 잘 알아보고 사용하도록 노력해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Promise.all()을 사용하여 갯수가 유동적인 비동기 작업 처리하기]]></title>
            <link>https://velog.io/@jess_apr/Promise.all%EB%A1%9C-%EC%97%B0%EC%86%8D%EB%90%98%EB%8A%94-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jess_apr/Promise.all%EB%A1%9C-%EC%97%B0%EC%86%8D%EB%90%98%EB%8A%94-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 24 Aug 2024 06:50:12 GMT</pubDate>
            <description><![CDATA[<p>회사 프로젝트의 게시판 부분을 작업하면서 이미지를 여러장 받아 업로드를 해야하는 작업과 마주쳤다. 이미지는 다음과 같은 과정을 거쳐 AWS S3 서버에 저장된다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/96179569-4ef0-4ad0-b96b-4db0c6396cdb/image.png" alt=""></p>
<ol>
<li>백엔드 서버에 S3 접근 권한인 Presigned Url을 요청한다.</li>
<li>백엔드 서버가 Presigned Url을 받아와 다른 필요한 정보(리소스 id 등)와 함께 리턴해준다.</li>
<li>Presigned Url을 통해 S3에 이미지를 업로드한다.</li>
<li>이미지 업로드가 성공적으로 완료되면 백엔드 서버에 업로드된 이미지의 리소스 id를 보내 성공적으로 이미지가 업로드되었음을 알린다.</li>
</ol>
<p><em>*이미지 업로드가 메인인 글이 아니기 때문에 간단하게 적었다. 사진과 설명의 숫자는 다르지만 순서는 같다.</em></p>
<p>이러한 과정에서 프론트가 서버와 클라우드에 보내는 3번의 요청이 모두 비동기 작업이어서 처음에는 복잡한 작업이 될 것이라고 생각했었다. 하지만 단순하게 이전 작업이 성공하면 다음 작업을 진행하면 되는거라 <code>then()</code>과 <code>catch()</code>를 이용해 쉽게 해결했다.</p>
<p>오히려 어려웠던 부분은 이미지를 여러장 보내려고 할 때 이 다음으로 진행되는 작업이었다. 이미지를 한장만 업로드하는 경우에는 업로드 작업을 한번만 진행하고 끝내면 되는데, 여러장을 업로드하려면 이미지마다 S3 url을 받아와 업로드 해야하기 때문에 업로드를 하고자하는 이미지의 수만큼 작업을 반복해주어야 했다. 이 부분에서는 각 이미지의 업로드 성공 여부가 다음 업로드에 영향을 미치지 않기 때문에 <code>then()</code>과 <code>catch()</code>로는 해결할 수 없었다. 처음에는 각각의 작업이 Promise니까 그냥 반복문으로 돌려주면 되겠거니 하며 코드를 작성했다.</p>
<pre><code>async function uploadmage(images: File[]) {
    // 업로드가 성공한 이미지의 정보를 담을 배열
    const imageArr: UploadedImageType[] = []

    // 여러장의 이미지가 담긴 배열을 돌며 한장씩 업로드 시도
    for (const image of images) {
        fetchImageUpload({
            type: UploadImageType.BOARD,
            file: image,
        }).then((res) =&gt; imageArr.push(res))  
        // 업로드가 성공하면 응답으로 이미지가 저장된 url과 resource id가 돌아온다. 프론트에서 사용하기 위해 배열에 담아준다.
    }

    return imageArr
}</code></pre><p>업로드 할 이미지의 배열을 받아 반복문을 돌며 업로드를 하고, 업로드가 성공한 이미지의 url과 resource id를 클라이언트에서 사용할 수 있게 배열에 담아주는 함수이다. 이 작업의 결과는 빈 배열이었다. 함수가 비동기 작업의 결과가 돌아올때까지 기다리지 않고 <code>imageArr</code>를 반환했기 때문이다. 지금 생각해보면 비동기 작업에 <code>await</code>이나 다른 어떤 처리도 안했으니 당연한 결과인데, 그때는 이미지 업로드 작업을 하면서 Promise를 여러번 쓰다보니 될거라고 헷갈렸던 것 같다.</p>
<p>빈 배열을 보면서 비동기 작업이 처리되지 않았다는 것은 대충 예측했는데, 이상하게도 한참을 삽질했다. <code>Promise.all()</code>을 한번도 써보지 않아서 이걸 쓸 생각을 못했다. 몇시간을 날리고 나서야 <code>Promise.all()</code>을 떠올렸고, 적용을 해봤다.</p>
<p>이미지 업로드 작업들이 이미 정의가 되어있는 비동기 함수들이면 간단하게 <code>Promise.all()</code>의 인자로 함수들을 담은 배열을 넣어주기만 하면 되겠지만, 이거는 작업마다 몇번씩 반복이 될지 정해지지 않았기 때문에 그렇게 할 수는 없었다. 그래서 일단 반복문을 사용해서 각 이미지를 업로드 하는 작업을 Promise 객체로 만들어주었다.</p>
<pre><code>// 이미지를 받아 해당 이미지를 업로드하는 과정을 Promise 객체로 만들어주는 함수
const promise = (image: File, uploadArr: UploadedImageType[]) =&gt;
    new Promise&lt;void&gt;((resolve, reject) =&gt; {
        fetchImageUpload({
            type: UploadImageType.BOARD,
            file: image,
        })
        .then(() =&gt; {
            uploadArr.push(res)
            resolve()
        })
        .catch(() =&gt; reject())
    })

// 업로드하고자 하는 이미지의 수만큼 배열을 돌며 각 이미지를 업로드하는 Promise 객체를 생성
const promiseArr = []
const imageArr: UploadedImageType[] = []
for (const image of images) {
    const p = promise(image, imageArr)
    promiseArr.push(p)
}</code></pre><p>참고로 여기서 Promise 객체를 생성 할 때 <code>then()</code>에 들어가는 콜백 함수의 마지막에는 <code>resolve()</code> 함수를, <code>catch()</code>에 들어하는 콜백 함수의 마지막에는 <code>reject()</code> 함수를 넣어주어야 한다. 넣어주지 않으면 Promise가 성공/실패 여부를 판단하지 못해 콜백함수 내부에 작성한 작업이 진행되지 않는다. 나같은 경우에는 api 요청이 성공했음에도 <code>imageArr</code>가 빈 배열로 돌아와서 또 삽질을 했다.</p>
<p>이제 Promise 객체들이 담긴 배열을 <code>Promise.all()</code>로 실행해준다.</p>
<pre><code>async function uploadRequestImage(images: File[]) {
    const promise = (image: File, uploadArr: UploadedImageType[]) =&gt;
        new Promise&lt;void&gt;((resolve, reject) =&gt; {
            fetchImageUpload({
                type: UploadImageType.BOARD,
                file: image,
            })
                .then((res) =&gt; {
                    uploadArr.push(res)
                    resolve()
                })
                .catch(() =&gt; reject())
        })

        const promiseArr = []
        const imageArr: UploadedImageType[] = []

        for (const image of images) {
            const p = promise(image, imageArr)
            promiseArr.push(p)
        }

        await Promise.all(promiseArr)
        return imageArr
}</code></pre><p><code>Promise.all()</code>에도 잊지말고 <code>await</code>을 붙여 작업이 완료될때까지 함수가 기다릴 수 있도록 해준다. 그러면 정상적으로 값이 다 들어있는 배열을 받을 수 있게 된다.</p>
<hr>
<p>취업을 하고 실무를 하며 Promise의 동작은 어느정도 이해했다고 생각했는데, 이번 작업은 그게 나의 오만이었음을 다시 한번 깨닫게 해주었다. 텍스트로 복잡한 작업의 순서를 매기려다보니 조금만 정신에 힘을 빼도 이 흐름을 따라가기가 쉽지 않다. 블로그 글을 적으면서 코드를 다시보니 정말 바보같은 실수로 한참을 삽질했다고 생각되어 조금 슬프지만, 그래도 <code>Promise.all()</code>을 더 잘 이해할 수 계기가 되어서 좋았다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVVM 패턴으로 비즈니스 로직 분리하기 (3)]]></title>
            <link>https://velog.io/@jess_apr/MVVM-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-3</link>
            <guid>https://velog.io/@jess_apr/MVVM-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-3</guid>
            <pubDate>Thu, 15 Aug 2024 13:56:12 GMT</pubDate>
            <description><![CDATA[<h2 id="앞으로-더-생각해봐야-할-것들">앞으로 더 생각해봐야 할 것들</h2>
<p>열심히 고민해서 첫 비즈니스 로직 분리를 했으나, 솔직히 말해서 잘 분리된 코드인지는 모르겠다. 처음이니 당연히 잘 분리되어있지 않을 것이라 생각한다. 아직 실력이 부족해서 어디가 잘못되었고 어떻게 고쳐야 할 지 모를 뿐. (일단 이런 고민을 시작했고 어떤 형태로든 해낸 것에 대해서는 나 자신의 머리를 한번 쓰다듬어주고!) 이후로는 코드를 어떻게 더 발전시켜야 할 지 고려해야 할 것들을 적으며 글을 마무리해보려 한다.</p>
<h4 id="1-mvvm-패턴이-프론트엔드-코드에서-비즈니스-로직을-분리하는-데-적합한-패턴인가">1. MVVM 패턴이 프론트엔드 코드에서 비즈니스 로직을 분리하는 데 적합한 패턴인가?</h4>
<p>이 부분에 대해서는 이전에도 글을 적은 적이 있고, 꾸준히 고민해왔던 문제다. 객체 지향 프로그래밍도 좋고, 비즈니스 로직을 분리하는 것도 좋은데, 최선의 방법이 MVVM 패턴인가? 코드를 작성하면서 너무 어려웠기 때문이다. Api 하나를 불러오는데도 여러 함수와 클래스가 엮이다보니 비동기 작업을 하는 것도 힘들었고, 상황이 바뀔때마다 익숙치않은 방법으로 코드를 짜느라 시간이 더 오래 걸렸다. 이게 내 실력이 부족한걸 감안하더라도, 꼭 필요하지 않은데 MVVM을 사용해서 간단하게 작성할 수 있는 코드를 복잡하게 돌아가게 한 건 아닌지 헷갈렸다. 이 부분에 대해서는 아직 지식과 경험이 너무 부족하기 때문에 어떤 부분이 문제가 되는지조차 알 수가 없다. 코딩을  계속 하면서 직접 어려움에 부딪히고, 그 어려움에 대한 해결방법을 찾으며 배우는수밖에 없다.</p>
<h4 id="2-viewmodel은-function-component로-작성되어야-하는가-class로-작성되어야하는가">2. ViewModel은 function component로 작성되어야 하는가, class로 작성되어야하는가?</h4>
<p>이번에는 ViewModel을 function component로 작성했지만, 작성 이후 여러 부분들이 찜찜하게 남아있다. MVVM 패턴을 적용하고 나면 ViewModel과 Model은 View 없이도 테스트를 진행할 수 있고, 이 테스트가 더욱 쉬워진다고 들었다. 하지만 Model은 테스트가 쉬웠지만 ViewModel은 useState같은 리액트 훅 때문에 테스트를 진행하기가 마냥 쉽지만은 않았다. 그리고 훅을 사용하기 위해 리액트 엘리먼트를 반환하지도 않는데 컴포넌트를 사용하는 것이 맞는건가 싶기도 했다. 다음에 연습용 프로젝트를 하나 만들어서 클래스 ViewModel을 사용하는 MVVM 패턴을 적용해봐야겠다.</p>
<h4 id="3-api는-어디서-불러야-하는가">3. Api는 어디서 불러야 하는가?</h4>
<p>Api는 Model보다는 ViewModel에서 부르는게 맞는것 같다. Model은 비즈니스 로직을 다루는 객체로, 다른것에 의존해서는 안되기 때문이다. 그런데 ViewModel에서 부르더라도 지금처럼 단순하게 함수로 만들어서 api 요청을 보내는게 충분한지, 아니면 api 요청을 담당하는 Repository 객체를 따로 만들어야 하는지는 잘 모르겠다. Repository 객체에 대해서 더 공부해봐야겠다.</p>
<h4 id="4-뷰로직은-어디에-포함되어야-하는가">4. 뷰로직은 어디에 포함되어야 하는가?</h4>
<p>화면에서 어떤 조건이 충족되면 모달을 보여주거나, 데이터를 특정 포맷으로 변형하여 보여주는 것은 뷰로직에 포함된다. 뷰로직은 UI와 관련이 있지만, 위에 작성했던 예시처럼 함수로 작성될 수도 있고 그 수가 많아지면 컴포넌트가 복잡해질 수도 있다. 복잡하지 않아도 뷰로직은 뷰로부터 분리해야 할지, 코드가 길어질때만 분리할지. 분리를 하게 된다면 ViewController 객체를 만들지, 그냥 ViewModel에 포함시킬지. 직접 코드를 작성하며 느낀것은, 작은 프로젝트라면 굳이 분리를 하지 않아도 될 것 같고 프로젝트가 커지면 뷰로직도 따로 분리하는 게 나을 것 같다는 것이었다. 그 크기가 어느정도여야 될지는 역시 경험이 필요할 것 같다.</p>
<h2 id="글을-마무리하며">글을 마무리하며</h2>
<p>클린 아키텍처라는 책을 처음 읽었을 때 내용을 하나도 못알아들었는데, 언젠가 그 내용이 이해가 가는 날이 왔으면 좋겠다. 아키텍처는 계속 고민하겠지만 짧은 시간 내에 답이 나올 것들은 아니니까 경험을 쌓아가면서 다른 것을 공부해보려고 한다. 이 다음에는 클린 코드에 대해 고민해보고 싶다. 마침 클린 코드 책을 빌려왔다. 숲을 공부해봤으니 다음은 나무를 봐보는것으로 하자 😚</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVVM 패턴으로 비즈니스 로직 분리하기 (2)]]></title>
            <link>https://velog.io/@jess_apr/MVVM-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-2</link>
            <guid>https://velog.io/@jess_apr/MVVM-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-2</guid>
            <pubDate>Thu, 15 Aug 2024 13:54:18 GMT</pubDate>
            <description><![CDATA[<h2 id="비즈니스-로직-분리하기">비즈니스 로직 분리하기</h2>
<p>처음 MVVM을 접했을 때 가장 어려웠던 개념이 ViewModel이었기때문에 우선은 View와 Model을 먼저 분리해야겠다고 생각했다. View는 화면에 보여지는 <code>TSX</code> 부분이라고 생각하니 쉽게 감이 잡혔고, Model은 데이터를 중심으로 분리해나가기로 했다.</p>
<pre><code>class ProductDetailModel implements ProductDetailAction {
    private readonly id: string
    private readonly title: string
    private readonly content: string
    private readonly price: number
    private readonly discount: number
    private readonly discountPrice: number
    private readonly registeredDate: string

    constructor(raw: ProductDataType) {
        this.id = raw.id
        this.title = raw.title
        this.content = raw.content
        this.price = raw.price
        this.discount = raw.discount
        this.discountPrice = raw.price - (raw.price * (raw.discount * 0.01))
        this.registeredDate = raw.registeredDate
    }

    getProductDetailData = () =&gt; ({
        id: this.id,
        title: this.title,
        content: this.content,
        price: this.price,
        discount: this.discount,
        discountPrice: this.discountPrice,
        this.registeredDate: this.registeredDate
    })

}</code></pre><p>서버로부터 받아온 데이터는 <code>ProductDetailModel</code>이라는 클래스 안에 저장된다. 그리고 이 데이터를 기반으로 새로운 값을 만들어내거나 수정, 삭제 등 데이터를 가공하는 모든 로직이 Model 안에서 진행된다. 예시 코드에서는 서버에서 정가와 할인율을 받아와 할인가를 구하는 코드가 포함되었다.</p>
<p>상품등록일의 포맷을 변경하는 함수는 Model에서 실행할까 고민을 했는데, 포맷을 변경하는 것은 단순히 View를 위한 로직이라고 판단되어 포함하지 않았다. 만약에 등록일을 수정하는 기능이 생긴다고 하면 Date 객체의 문자열 포맷으로 Model에 저장되고 서버에 보내져야하지, &#39;0000년 0월 0일&#39;의 포맷으로 처리를 할 수 있는 것이 아니기 때문이다.</p>
<h2 id="viewmodel-코드-작성하기">ViewModel 코드 작성하기</h2>
<p>ViewModel의 코드는 View가 Model에 직접적으로 접근할 수 없게 하는 것에 목적을 두고 작성을 하기로 했다. 하지만 그 전에 가장 고민을 많이 했던 포인트가 두가지가 있었다.</p>
<ol>
<li><p>Api 코드는 어떻게 작성을 해야할까?
Api는 데이터와 관련된 로직이니 Model에 포함해야 할까, 아니면 직접적으로 데이터를 가공하는 것은 아니니 ViewModel에 포함해야 할까? 나도 이러한 고민을 했고, 다른 사람들도 이 부분에 대한 의견이 나뉘어 있었다. 실제 코드 예시를 찾아보니 Kotlin이나 Swift에서는 api를 부르는 레이어(Repository, Service 등)가 따로 있고, 그 레이어를 ViewModel이 불러오는 형태가 많이 보였다. 몇몇 타입스크립트 코드 예시는 Model에서 불러오는 것도 있었지만, (적어도 내가 찾아봤을때는) ViewModel에서 불러오는 예시의 수보다 훨씬 적어서 더 많이 쓰이는 형태를 사용해보기로 했다. 따라서 api 요청을 하는 함수를 작성하고, 그 함수를 ViewModel에서 불러오는 방식을 적용했다.</p>
</li>
<li><p>ViewModel은 class로 만들까, function component로 만들까?
이 질문에 대한 답은 api 코드를 ViewModel이 부르게 되면서 결정하게 되었다. ViewModel 내부에서 api를 부르는 메서드는 비동기 함수로 작성이 되는데, 이 비동기 함수를 바로 View로 전달하게 되면 View마다 비동기 함수를 다루는 코드를 작성해야 했다. ViewModel 내부에서 useState를 사용하여 api가 돌려주는 값을 저장하여 View에서 바로 사용할 수 있게 하기 위해 function component로 작성하기로 했다.</p>
</li>
</ol>
<h3 id="api-요청-함수">Api 요청 함수</h3>
<pre><code>// 서버에 상품 데이터를 요청하는 api
async function getProductDetail(productId: string) {
    const getProductDetail = async () =&gt; {
        try {
            const productData = await Axios.get( ... )
            return new ProductDetailModel(productData.data)
        } catch(error) {
            console.error(error)
        }
    }

    return getProductDetail
}

// 장바구니에 상품을 담는 api 요청
async function postProductToCart(productId: string) {
    await Axios.post( ... )
}</code></pre><p>서버에 상품 데이터를 요청하는 api는 서버로부터 답을 성공적으로 받아오게 되면 이 값을 Model에 넣어 리턴해준다. </p>
<p>장바구니에 상품을 담는 api 요청은 서버로 보내기만 하면 되기 때문에 그냥 <code>POST</code> 요청을 보내는 단순한 함수다. 만약에 장바구니의 내용을 처음 렌더링 될때만 서버에서 받아오고, 그 데이터를 계속 이용하고 싶다면 장바구니 Model을 생성하여 사용하면 된다. <code>POST</code> 요청이 성공했다면 그 정보를 클라이언트 단에서 장바구니 Model에 바로 업데이트 해주어 추가적인 get 요청 없이 사용하는 것이다. 새로고침을 하지 않는 한 서버에 <code>GET</code> 요청을 하지 않아도 된다는 장점이 있지만, 싱글톤 패턴 등을 사용하여 Model을 전역에서 사용할 수 있도록 만들어야하고, 데이터가 계속 캐싱되어있어야한다는 단점도 있어 상황마다 장단점을 잘 따져보고 적용해야 한다. 현재 프로젝트에 이 방식을 적용한 부분이 있기는 한데, 이것까지 적으면 글이 너무 복잡해질 것 같아 짧게 기록만 하고 넘어간다.</p>
<h3 id="내가-작성한-viewmodel">내가 작성한 ViewModel</h3>
<pre><code>function ProductDetailViewModel(productId: string): ProductDetailVM {
    const [productDetailModel, setProductDetailModel] = useState&lt;ProductDetailModel&gt;()
    const [productDetailData, setProductDetailData] = useState&lt;{
        id: string
        title: string
        content: string
        price: number
        discount: number
        discountPrice: number
        registeredDate: string
    }&gt;({        
        id: &#39;&#39;
        title: &#39;&#39;
        content: &#39;&#39;
        price: 0
        discount: 0
        discountPrice: 0
        registeredDate: &#39;&#39;
    })

    useEffect(() =&gt; {
        setProductDetailData(produceDetailModel.getProductDetailData())
    }, [productDetailModel])

    async function getProductDetailData() {
        try {
            const data = await getProductDetail(productId)
            setProductDetailModel(data)
            setProductDetailData(data.getProductDetailData())
        } catch(error) {
            console.error(error)
        }
    }

    async function addProductToCart() {
        try {
            await postProductToCart(productId)
        } catch(error) {
            console.error(error)
        }
    }

    return { productDetailData, getProductDetailData, addProductToCart } 
}</code></pre><p><code>GET</code> 요청을 보낸 후, 성공 시 돌아오는 Model을 받아 <code>productDetailModel</code> state에 저장한다. 이 Model 값을 가지고 있는 state는 이후 Model의 내용을 업데이트 할 때 바로 사용할 수 있다. State값에 저장이 되어있기 때문에 업데이트 이후 다른 처리를 해주지 않아도 바뀐 데이터가 바로 반영된다.</p>
<p><code>productDetailData</code> state를 따로 만들어 둔 이유는 View가 Model의 메서드를 직접 부르게하고싶지 않아서이다. productDetailModel은 model을 가지고 있는 값이라 실제 데이터를 사용하려면 데이터를 돌려주는 메서드를 먼저 불러야하는데, 그렇게 되면 View가 Model에 직접 접근해야 하고, MVVM 규칙에 어긋나기 때문이다. 대신 이 값은 Model이 바뀌어도 바뀐 값이 반영이 되지 않기 때문에 useEffect를 사용해서 Model 값이 바뀌면 자동으로 업데이트되도록 해주었다.</p>
<h2 id="분리-후의-view">분리 후의 View</h2>
<pre><code>function ProductDetailScene() {
    const { id } = useParams()
    const { productDetailData, getProductDetailData, addProductToCart } = ProductDetailViewModel(id)
    const { title, registeredDate, price, discount, discountPrice, content } = productDetailData

    useEffect(async () =&gt; {
        getProductDetailData(id)
    }, [])

    function formatDate(date: string) {
       // ... Date 객체의 날짜를 문자열로 표현한 포맷을 &#39;0000년 00월 00일&#39; 포맷으로 바꿔주는 함수
    }

    return (
        &lt;ProductDetailContainer&gt;
            &lt;ProductDetailTitle&gt;{title}&lt;/ProductDetailTitle&gt;
            &lt;ProductDetailDate&gt;
                상품등록일: {FormatDate(registeredDate)}
            &lt;/ProductDetailDate&gt;
            &lt;ProductDetailPriceContainer&gt;
                &lt;ProductDetailOriginalPrice&gt;상품 가격: {price}&lt;/ProductDetailOriginalPrice&gt;
                &lt;ProductDetailDiscount&gt;할인: {discount}%&lt;/ProductDetailDiscount&gt;
                &lt;ProductDetailDiscountPrice&gt;할인가: {discountPrice}&lt;/ProductDetailDiscountPrice&gt;
            &lt;/ProductDetailPriceContainer&gt;
            &lt;ProductDetailContent&gt;{content}&lt;/ProductDetailContent&gt;
            &lt;ProductDetailCartButton onClick={() =&gt; addProductToCart(productId)}&gt; 
                /* 장바구니 버튼 컴포넌트 내용 */
            &lt;/ProductDetailCartButton&gt;
        &lt;/ProductDetailContainer&gt;
    )
}

export default ExperienceDetailScene</code></pre><p>비즈니스 로직을 분리한 후의 View 코드이다. useEffect를 사용하여 처음 렌더링될때 상품 정보를 받아오는 ViewModel의 메서드를 한번 불러주고, ViewModel에 세팅된 정보를 사용하게 된다. 기존의 코드보다 길이도 많이 줄었고, 데이터는 단순히 불러다 쓰기만 하고 있다.</p>
<hr>
<p>MVVM 패턴으로 비즈니스 로직을 분리하면서 나름대로 최선을 다했지만, 답을 내지 못한 부분들이 많다. 이것들에 대해서는 이어서 기록을 남겨보록 하겠다. &gt;&gt; 3편에서 계속..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVVM 패턴으로 비즈니스 로직 분리하기 (1)]]></title>
            <link>https://velog.io/@jess_apr/MVVM-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@jess_apr/MVVM-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Thu, 15 Aug 2024 13:45:14 GMT</pubDate>
            <description><![CDATA[<p>소프트웨어를 설계할 때, 비즈니스 로직을 분리하는 것은 중요하다고 한다. UI와 비즈니스 로직은 변경되는 이유와 속도가 다르기 때문이다. UI는 사소한 이유로 바뀔 수 있고, 클라이언트의 요구에 따라 자주 바뀔 수 있다. 반면 핵심 비즈니스 로직은 사업에서 핵심적인 역할을 하며, 그렇기 때문에 바뀔 가능성이 비교적 낮다. </p>
<p>위키피디아에 적힌 비즈니스 로직의 정의는 이렇다.</p>
<blockquote>
<p>비즈니스 로직(Business logic)은 컴퓨터 프로그램에서 실세계의 규칙에 따라 데이터를 생성·표시·저장·변경하는 부분을 일컫는다.</p>
</blockquote>
<p>비즈니스 로직에 대해 처음 공부를 시작했을때 이 정의가 무슨말인지 이해가 잘 안갔다. 그래서 여러가지 자료를 찾아보고 읽어봤다. 내가 비즈니스 로직에 대해 알아보고 이해한 것을 토대로 간단한 예시를 들어보자면 이렇다. 청년 A는 온라인 사탕 판매 사업을 시작하려고 한다. A는 소프트웨어 엔지니어에게 가서 사탕을 판매하고, 결제를 받고, 고객의 요청에 따라 사탕 구매에 대한 영수증을 발행해주는 웹사이트를 만들어달라고 할 것이다. A가 요구하는 이러한 작업들은 소프트웨어가 해결해야 하는 문제 (또는 위키피디아에서 말하는 실세계의 규칙)인 <strong>비즈니스 규칙</strong>이다. 그리고 이 문제들을 해결하기 위해 소프트웨어 내부에 작성된 코드는 <strong>비즈니스 로직</strong>이 된다.</p>
<p>위에 적어놓은 예시에 데이터라는 단어는 전혀 들어가지 않았다. 데이터는 어디에 들어가는 것일까? 다시 구글링을 열심히 해보니 <strong>서비스의 정책이나 조건에 따라 정보를 가공, 검증, 제공하는 코드</strong>가 비즈니스 코드가 되는 것 같았다. 그렇다면 A의 웹사이트에는 이런 코드들이 비즈니스 로직이 될 것 같다. </p>
<ol>
<li>사탕에 대한 상품 정보를 제공한다. 각 사탕의 재고량을 확인하여 현재 구매할 수 있는 상품과 아닌 상품을 구분하고, 회원 할인이나 시즌할인 등 상품의 조건에 따라 할인을 적용한다.</li>
<li>고객이 장바구니에 사탕을 담으면 종류와 갯수에 따라 총 결제 금액을 계산한다.</li>
<li>결제를 하기 전에 고객으로부터 연락처와 배송지 주소를 입력받고, 입력된 정보의 유효성을 검사한다.</li>
<li>결제 정보에 따라 고객의 유저 정보를 업데이트한다. 결제 금액에 따른 포인트 적립, 전자 영수증 발행 체크 여부에 따른 영수증 정보 추가 등.</li>
</ol>
<p>물론 아직은 배우고 있는 단계라 부족할 수 있지만, 이러한 이해를 바탕으로 내가 작성한 코드를 분리해보고자 했다.</p>
<h2 id="모든것이-섞인-코드">모든것이 섞인 코드</h2>
<pre><code>function ProductDetailScene() {
    const { id } = useParams()
    const [productData, setProductData] = useState&lt;ProductDataType&gt;({
        id: &#39;&#39;,
        title: &#39;&#39;,
        content: &#39;&#39;,
        price: 0,
        discount: 0,
        registeredDate: &#39;&#39;
    })

    useEffect(() =&gt; {
        // 상품의 정보를 받아오는 로직
        const getProductDetail = async () =&gt; {
            try {
                const productData = await Axios.get( ... )
                setProductData(productData.data)
            } catch(error) {
                console.error(error)
            }
        }

        getProductDetail()
    }, [])

    async function addProductToCart() {
        // 장바구니에 상품을 담는 로직
        await Axios.post( ... )
    }

    function formatDate(date: string) {
       // ... Date 객체의 날짜를 문자열로 표현한 포맷을 &#39;0000년 00월 00일&#39; 포맷으로 바꿔주는 함수
    }

    // 받아온 정보에서 정가와 할인율을 이용하여 할인가를 계산하는 로직
    const discountPrice = productData.price - (productData.price * (productData.discount * 0.01))

    return (
        &lt;ProductDetailContainer&gt;
            &lt;ProductDetailTitle&gt;{productData.title}&lt;/ProductDetailTitle&gt;
            &lt;ProductDetailDate&gt;
                상품등록일: {FormatDate(productData.registeredDate)}
            &lt;/ProductDetailDate&gt;
            &lt;ProductDetailPriceContainer&gt;
                &lt;ProductDetailOriginalPrice&gt;상품 가격: {productData.price}&lt;/ProductDetailOriginalPrice&gt;
                &lt;ProductDetailDiscount&gt;할인: {productData.discount}%&lt;/ProductDetailDiscount&gt;
                &lt;ProductDetailDiscountPrice&gt;할인가: {discountPrice}&lt;/ProductDetailDiscountPrice&gt;
            &lt;/ProductDetailPriceContainer&gt;
            &lt;ProductDetailContent&gt;{productData.content}&lt;/ProductDetailContent&gt;
            &lt;ProductDetailCartButton onClick={() =&gt; addProductToCart(productId)}&gt; 
                /* 장바구니 버튼 컴포넌트 내용 */
            &lt;/ProductDetailCartButton&gt;
        &lt;/ProductDetailContainer&gt;
    )
}

export default ExperienceDetailScene</code></pre><p>이 코드는 상품의 정보를 보여주는 상세 페이지를 보여주는 컴포넌트 코드이다. MVVM 패턴과 비즈니스 로직에 대해 몰랐을 때는 모든 코드를 하나의 파일 안에 묶어두었는데, 코드의 주석에서 볼 수 있듯이 데이터를 불러오고, 가공하고, 화면을 그리는 모든 로직이 이 컴포넌트 안에 들어있다.</p>
<hr>
<p>여기에서 비즈니스 로직을 분리해 Model에 넣고, View와 Model을 연결하는 ViewModel 코드도 작성해보겠다. &gt;&gt; 2편에서 계속..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Axios 코드 재사용하기]]></title>
            <link>https://velog.io/@jess_apr/Axios-%EC%BD%94%EB%93%9C-%EC%9E%AC%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jess_apr/Axios-%EC%BD%94%EB%93%9C-%EC%9E%AC%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 09 Jul 2024 14:10:00 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행하면서 API 연동 작업을 시작했을 때, 데이터가 필요할 때마다 Axios 코드를 때려넣는 짓(…)은 하면 안된다는 지시를 받았다. 대신 중복코드를 줄이고, 혹시라도 코드가 수정될 일이 있다면 더 편하게 수정할 수 있도록 코드를 랩핑해야한다고 말씀해주셨다. 그래서 Axios 코드를 재사용할 수 있도록 랩핑해보았다.</p>
<pre><code class="language-tsx">const instance = axios.create({
    baseURL: BASE_URL,
    maxRedirects: 0,
    timeout: 60000,
})</code></pre>
<p>우선 모든 Axios 요청에 공통적으로 들어가는 속성들을 Axios 인스턴스에 넣어 미리 생성해주었다. 이 인스턴스를 불러서 사용할 때는 미리 적용된 설정들을 다시 작성해주지 않아도 돼서 코드의 중복을 줄일 수 있다. 무엇보다 Base URL을 매번 작성해주지 않아도 되는게 정말 편했다.</p>
<pre><code class="language-jsx">interface ApiRequest {
    method?: Method
    path?: string
    contentType?: string
    data?: unknown
    params?: unknown
    responseType?: ResponseType
    otherService?: boolean
}</code></pre>
<p>현재 진행하고 있는 프로젝트는 타입스크립트를 사용하고 있기 때문에 Axios 요청을 할 때 넣어주어야 하는 정보들을 모아 인터페이스로 선언해주었다. 다만 http 메서드들마다 다른 형태의 정보를 받으므로 속성들을 전부 옵셔널 프로퍼티로 설정했다. 이렇게 하면 요청마다 받는 객체의 속성이 달라도, 이 인터페이스 안의 속성이기만 하면 타입 오류가 나지 않는다.</p>
<pre><code class="language-tsx">import { getAccessToken } from &#39;@src/api/agents/storage&#39;
import instance from &#39;@api/agents/http/axios&#39;

async function AxiosRequest&lt;T&gt;(body: ApiRequest): Promise&lt;T&gt; {
    const { method, path, contentType, data, params, responseType, otherService  } = body

    const access = getAccessToken()
    const t = contentType ?? &#39;application/json&#39;
    const h = otherService
        ? { &#39;Content-Type&#39;: t }
        : {
            &#39;Authorization&#39;: `Bearer ${access}`,
            &#39;Content-Type&#39;: t,
        }

    try {
        return await instance.request({
            method,
            url: path,
            headers: h,
            data,
            params,
            responseType: responseType ?? &#39;json&#39;,
        })
    } catch (e) {
        return await Promise.reject(e)
    }
}

export default AxiosRequest</code></pre>
<p>요청을 보내는 코드를 <code>AxiosRequest</code>라는 이름의 함수로 랩핑해주었다. 위에서 선언한 인터페이스는 이 함수가 받는 인자의 타입으로 지정해주었다. 반환값의 타입은 어떻게 할까 고민했는데, 제네릭 타입을 써보라고 하셔서 제네릭을 사용했다. 타입스크립트를 사용하면서 <code>any</code> 타입을 사용하는 상황은 피하고 싶었는데 제네릭 타입을 사용하니 요청을 할때 각 요청이 돌려주는 데이터를 직접 설정해줄 수 있는 점이 좋았다.</p>
<p>함수 내부에서는 인자로 받은 값을 Axios 요청의 파라미터로 넣어준다. 하지만 기본적으로 많이 사용하는 속성들은 직접 적어주지 않아도 자동으로 요청에 포함되도록 해주었다. 토큰의 경우, 자동으로 로컬스토리지에서 토큰을 가져오는 함수를 사용해 헤더에 토큰 설정을 해준다. 만약에 토큰이 들어가면 안되는 요청에는 (ex. S3에 이미지를 보내는 요청) <code>otherService</code> 속성만 true로 해주면 헤더 설정을 쉽게 바꿀 수 있다. 또, 헤더의 <code>Content-type</code>이나 <code>responseType</code>과 같이 일반적으로 많이 사용하는 값이 있는 속성들은 따로 값을 넣어주지 않으면 주로 사용되는 값이 세팅되도록 해주었다.</p>
<p>파라미터가 설정되면 요청을 보낸다. 성공하면 서버로부터 돌아온 응답을 반환하고, 에러가 발생하면 에러 핸들링을 할 수 있도록 거부된 프로미스 객체를 반환한다.</p>
<pre><code class="language-tsx">interface ServiceResponse&lt;T&gt; {
    status: number;
    statusText: string;
    headers: AxiosHeaders;
    config: AxiosRequestConfig;
    data: T;
    request?: XMLHttpRequest;
}</code></pre>
<p>Axios 통신이 돌려주는 응답의 타입도 인터페이스로 선언했다. 응답들이 기본적으로 가지고 있는 속성들은 타입을 정해주었고, API마다 다르게 돌려주는 <code>data</code> 속성의 타입만 제네릭 타입으로 설정했다. </p>
<p><code>Request</code> 속성은 상황에 따라 응답 객체에 포함이 되지 않을 수도 있다고 한다 (요청은 보내졌지만 클라이언트가 응답을 받지 못했거나, 네트워크 오류가 발생한 상황 등). 따라서 옵셔널 프로퍼티로 설정하고, 클라이언트와 서버의 통신에서만 사용하는 코드이기 때문에 <code>XMLHttpRequest</code> 타입으로 설정했다. 브라우저 환경에서는 <code>XMLHttpRequest</code> 객체를 사용하고, Node.js 환경에서는 <code>ClientRequest</code> 객체를 사용한다고 하니 상황에 따라 타입을 정해주면 되겠다.</p>
<pre><code class="language-tsx">AxiosRequest&lt;ServiceResponse&lt;ApiSignInResponse&gt;&gt;({
    method: &#39;post&#39;,
    path: &#39;/auth/sign-in/&#39;,
    data: {
        email: body.email,
        password: body.password,
    },
})
.then(...)
.catch(...)</code></pre>
<p>이렇게 작성한 코드를 사용해본 예시이다. <code>DataResponseT</code> 뒤에 응답으로 돌아오는 data의 타입을 제네릭으로 넣어주었다.</p>
<p>이렇게 완성한 코드는 서버 정보를 요청하는 함수에 넣어서 재사용했다. 이미지를 S3 서버로 보내는 작업을 하면서 요청에서 헤더의 <code>Authorization</code>을 빼야하는 일이 생겼었는데, 만약에 이렇게 랩핑을 해주지 않았다면 <code>Authorization</code>이 없는 헤더를 가진 인스턴스를 따로 만들어야했었다. 한번 랩핑을 해주니 함수 안의 Axios 코드만 수정하면 되고 Axios를 부르는 곳에서는 수정을 따로 해 줄 필요가 없어서 편했다. 코드의 중복을 줄이고 유지보수성을 높이기 위해 따로 분리하여 재사용을 한다는 개념은 익숙하게 느껴지는데, 왜 개인 프로젝트를 할때는 이렇게 자주 쓰이는 Axios 코드를 분리할 생각을 못했나 싶다. 물론 코드 분리하는 과정이 처음이라 그런지 쉽지는 않았다. 하지만 한번 해봤으니 앞으로는 더 잘할수 있을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TypeScript와 Design Pattern]]></title>
            <link>https://velog.io/@jess_apr/TypeScript%EC%99%80-Design-Pattern</link>
            <guid>https://velog.io/@jess_apr/TypeScript%EC%99%80-Design-Pattern</guid>
            <pubDate>Fri, 28 Jun 2024 01:00:53 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jess_apr/post/1b3ad37a-6908-4c6f-8cbf-2f74bfd09208/image.png" alt=""></p>
<blockquote>
<p>굉장히 주관적인 의견을 담아 작성한 글이며, 아직 부족한 실력으로 인해 잘못된 정보가 포함되어있을수 있습니다.</p>
</blockquote>
<h2 id="타입스크립트에-디자인-패턴을">타입스크립트에 디자인 패턴을?</h2>
<p>내가 지금 일하고 있는 회사는 객체 지향적으로 프로그래밍 하는 것을 선호한다. 그러다보니 자연스럽게 코드에 디자인 패턴을 사용하게 되었다. 하지만 타입스크립트로 코드를 작성하다보면 디자인 패턴을 적용하기가 어려운 부분이 있다. 구글을 찾아보면 디자인 패턴 개념에 딸린 예시는 전부 Java 코드고, 실제 사용 예시를 찾아보면 Swift나 Kotlin이었다. 따라하려고 해도 언어가 제공해주는 기능이 다르다보니 한계를 마주하게 된다. 어찌저찌 지지고볶아 모양새를 만들어내면, 읽기 어렵고 고치기 어려운 코드가 되고 말았다. 그러다보니 타입스크립트로 개발을 할 때 디자인 패턴이 문제 해결을 위해 적합한 방법인지 생각해보게 되었다. 예제가 많이 없는 것은 사람들이 많이 쓰지 않는다는 말일까? 디자인 패턴은 타입스크립트라는 언어에 맞는 문제 해결 방법인가? 아니면 디자인 패턴 대신 이 문제를 해결할 수 있는 타입스크립트만의 방법이 있을까? 이 고민을 하며 나름의 답을 찾기 위해 여러 글을 읽어보았고, 나만의 개발 방향성을 찾고자 했다.</p>
<h2 id="자바스크립트는-멀티-패러다임-언어">자바스크립트는 멀티 패러다임 언어</h2>
<p>자바스크립트는 프로토타입을 기반으로 <strong>객체 지향 프로그래밍</strong>을 지원하는 언어다. 그리고 함수를 일급 객체로 다룸으로써 <strong>함수형 프로그래밍</strong> 패러다임 또한 사용할 수 있게 해준다. 구글링을 하면서 자바스크립트를 사용하는데 OOP는 크게 중요하지 않다거나, 디자인 패턴에 대해 공부하지 않아도 괜찮다는 글을 종종 봤다. (물론 그런 의견을 가진 분들도 나름의 이유를 가지고 글을 쓴 것일테고, 극단적으로 자바스크립트를 쓸 때 객체 지향을 아예 하지 말라는 말도 아닐 것이라는 것은 안다.) 하지만 자바스크립트에 OOP가 중요하지 않다면 왜 프로토타입을 기반으로 하면서까지 객체 지향 프로그래밍을 지원하게 되었는지, 왜 ES6에서 class 키워드를 추가하게 되었는지, 왜 타입스크립트에 각종 객체 지향 문법이 추가되었는지 설명이 되지 않았다. 객체 지향과 함수형 패러다임을 사용할 수 있게 만들어주었다면, 두 기능 모두를 잘 사용하는 것이 이 언어를 잘 쓰는 방법이 아닌가? </p>
<p>하지만 자바스크립트는 자바나 C++처럼 클래스를 기반으로 하는 언어들과는 다른 특성을 가진다. 위에서 언급한 함수의 일급 객체 취급이나 프로토타입 객체 지향 방식 뿐만 아니라 this의 동적 바인딩 등 자바스크립트만의 독특한 특성을 가진다. 심지어 처음 만들어졌을 당시에는 class 키워드도 없었고, 현재는 class 키워드가 생겼다고는 하지만 프로토타입 기반 객체지향의 문법적 설탕이라는 평가를 받기도 한다. 물론 타입스크립트가 생기면서 인터페이스, 접근제어자 등이 사용가능해지며 객체 지향 프로그래밍을 더욱 편하게 할 수 있게 된 것은 사실이지만, 타입스크립트는 엄연히 자바스크립트를 기반으로 생성된 언어다. 이런저런 조건을 따져봤을 때, 자바스크립트 또는 타입스크립트로는 class 기반의 객체 지향 방식이나 디자인 패턴을 따르기가 쉽지 않다. 오히려 이런 형태를 완벽하게 따라하기 위해서 더욱 어렵고 불편한 코드를 작성해야 할 수도 있다.</p>
<h2 id="내가-내린-결론">내가 내린 결론</h2>
<p>결국 내가 내린 결론은 타입스크립트로 객체 지향 프로그래밍을 하고 디자인 패턴을 쓰되, class 기반의 객체 지향 방식을 무조건적으로 따르려하지 말자는 것이다. 나는 매우 중요한 사실을 간과하고 있었는데, 객체 지향은 개념이고 프로그램을 표현하는 규칙일 뿐, 어떤 언어가 객체 지향언어라는 말은 잘못된 표현이라는 것이다 (<a href="https://namu.wiki/w/%EA%B0%9D%EC%B2%B4%20%EC%A7%80%ED%96%A5%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D">나무위키 - 객체 지향 프로그래밍</a>). 즉, 객체 지향은 특정 언어로만 할 수 있는 것이 아니다.</p>
<p>각각의 프로그래밍 방식은 자신만의 역할이 있다. 클린 아키텍처라는 책에서는 프로그래밍 패러다임들이 저마다 다른 것을 제한하는 역할을 한다고 했다. 객체 지향 프로그래밍이 함수형 프로그래밍을 대체할 수 없고, 그 반대도 마찬가지다. 그리고 어떤 하나를 쓴다고 해서 다른 하나를 쓰지 못하는 것도 아니다. 이 부분의 코드는 객체 지향으로, 저 부분의 코드는 함수형으로 작성할 수 있다. </p>
<p>객체 지향 프로그래밍도 함수형 프로그래밍도 결국은 더 읽기 쉬운 코드, 더 유지보수를 하기 쉬운 코드를 작성하기 위해 사용하는 것이다. 따라서 어떤 방식을 완벽하게 따라하는 것이 중요한 것이 아니라 그 개념을 내가 마주한 상황에 맞춰 필요한 형태로 사용하는 것이 현재로써는 옳은 방법이라고 판단된다. 함수를 일급객체로 다루는 타입스크립트의 특성을 사용하여 기존의 디자인 패턴에서 클래스를 사용하던 것을 함수로 구현할 수 있다. 또는 패턴에 설명된 모든 클래스를 구현하지 않더라도 일부 클래스를 구현해서 필요한 부분만 사용한다거나, 특정 상황에서는 (비슷한 문제라도) 디자인 패턴을 사용하지 않는게 좋은 방법일 수도 있다. 패턴을 사용하는 것보다 중요한 것은, 객체 지향 프로그래밍을 사용할 때 정말 객체 지향적으로 코드를 작성하고 있는지를 판단해야 하는 것이다. SOLID 원칙은 잘 지켜지고 있는지, 코드의 분리는 제대로 되었는지 등을 생각해야한다.</p>
<p>지금은 경력이 너무 짧다보니 좋은 선택을 할수는 없다. 하지만 나와 팀원들을 설득시킬 수 있는 코드를 짜보려고 노력해야한다. 이렇게 내가 작성하는 코드의 큰 그림을 보려고 의도적으로 의식하며 경험을 쌓아간다면 언젠가는 읽기 좋고 고치기 쉬운 코드를 작성하는 개발자가 될 수 있지 않을까?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compound Component Pattern]]></title>
            <link>https://velog.io/@jess_apr/Compound-Component-Pattern</link>
            <guid>https://velog.io/@jess_apr/Compound-Component-Pattern</guid>
            <pubDate>Tue, 25 Jun 2024 14:58:07 GMT</pubDate>
            <description><![CDATA[<h2 id="compound-component-pattern-이란">Compound Component Pattern 이란?</h2>
<p><code>Compound component pattern</code>은 리액트에서 사용할 수 있는 디자인 패턴 중 하나로, 여러개의 작은 컴포넌트들을 조합하여 하나의 큰 컴포넌트를 만든다.</p>
<p>예를 들어, 여러 페이지에서 사용하는 모달을 만든다고 해보자. 디자인은 비슷한데, 어떤 것은 제목이 있고, 어떤것은 제목이 없다. 어떤 모달은 버튼을 포함할 수도 있다. 모든 모달들이 서로 다른 디자인을 가지고 있다면 하나씩 만드는 것이 낫겠지만, 비슷한 디자인을 공유한다면 제목, 텍스트, 버튼 등의 요소들을 작은 컴포넌트로 만들어 원하는 배치대로 조합하여 사용하는 것이 편할 수 있다. 이 때 <code>Compound component pattern</code>을 사용할 수 있다.</p>
<h2 id="나의-코드">나의 코드</h2>
<p>내가 컴파운드 컴포넌트를 만들고자 한 코드는 웹사이트에서 전반적으로 사용되는 인풋 컴포넌트이다. 일관된 스타일을 가지고 있지만, 그 안에 들어가는 요소는 조금씩 다르다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/3a135a88-2311-47f9-a288-a732cf678a67/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/eea0b935-3f8f-4fd3-9165-b8c6f483032f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/ddba134a-8951-41de-8bf5-caabb046fff3/image.png" alt=""></p>
<p>이렇게 제목과 인풋이 들어가는 위치, 스타일은 동일하지만, 사용자 입력값을 받는 부분은 텍스트 인풋, 드롭다운 리스트, 태그 등 다양하다. 일부 선택사항들은 제목 옆에 ‘(선택)’이라는 표시도 해주어야 한다.</p>
<p>처음에는 하나의 공통 컴포넌트로 만들어 props를 통해 내용물을 정해주었으나, 내용물의 종류가 너무 다양해서 하나의 컴포넌트 안에 모든것을 담아내기에는 무리였다. 여러가지 방법을 고민했지만, 내용물을 다양하게 넣어줄 수 있고, 상황에 따라 배치도 자유롭게 해줄 수 있는 컴파운드 컴포넌트가 적합하다고 판단했다. </p>
<p>우선, 모든 인풋 컴포넌트가 공통으로 가지고 있는 부분을 뽑아주었다. 공통 부분은 따로 넣어주는것보다 이렇게 모아주면 재사용을 할 수 있어 코드의 중복을 줄일 수 있다고 생각했기 때문이다.</p>
<pre><code class="language-tsx">function CompoundInput(props: CompoundInputProps) {
    const { title, children, option } = props

    return (
        &lt;CompoundInputContainerBox&gt;
            &lt;CompoundInputTitleContainer&gt;
                &lt;CompoundInputTitle&gt;{title}&lt;/CompoundInputTitle&gt;
                {option !== &#39;&#39; &amp;&amp; &lt;p&gt;{option}&lt;/p&gt;}
            &lt;/CompoundInputTitleContainer&gt;
            {children}
        &lt;/CompoundInputContainerBox&gt;
    )
}</code></pre>
<p>Styled component를 사용해 기본 스타일을 정의해주고, 제목과 선택사항은 props로 받도록 했다. 그리고 실제 사용할 때 원하는 내용을 원하는 순서대로 배치할 수 있도록 인풋이 들어가야하는 부분에 <code>children</code>을 넣어주었다. </p>
<p><code>children</code> 자리에는 어떤 요소든 들어갈 수 있지만, 여러번 반복해서 쓰이는 요소들은 컴포넌트로 만들었다. 컴포넌트로 만들어준 요소는 텍스트 인풋, 드롭다운 리스트, 태그, 텍스트 에어리어 4가지이다.</p>
<p>예시) 태그 컴포넌트</p>
<pre><code class="language-tsx">function CompoundInputTag(props: CompoundInputTagProps) {
    const { placeholder, setValue, defaultTags, whiteList, maxTagCount, isHashTag } = props
    const setting = isHashTag ? {
                maxTags: maxTagCount,
                placeholder,
                editTags: null,
            }
        : {
                dropdown: {
                    enabled: 0,
                },
                maxTags: maxTagCount,
                placeholder,
                editTags: null,
                enforceWhitelist: true,
            }

    function handleSetValue(e: CustomEvent&lt;Tagify.ChangeEventData&lt;Tagify.BaseTagData&gt;&gt;) {
        const selectedList = e.detail.value.length
            ? JSON.parse(e.detail.value).map((it: { value: string }) =&gt; it.value)
            : []

        setValue(selectedList)
    }

    return (
        &lt;TagifyDropdown&gt;
            &lt;Tags
                value={defaultTags}
                settings={setting}
                whitelist={whiteList}
                onChange={(e) =&gt; handleSetValue(e)}
            /&gt;
        &lt;/TagifyDropdown&gt;
    )
}

export default CompoundInputTag</code></pre>
<p>드롭다운 태그인지 해시태그인지, 태그의 최대 갯수는 몇개인지, 태그가 없을 때 어떤 텍스트를 보여줄지 등 태그의 상태를 props로 받도록 했다. 다른 컴포넌트 안에 속한 것이 아니라 children으로 배치해주기 때문에 이러한 props들을 상위 컴포넌트를 통하지 않고 해당 props를 사용하는 컴포넌트로 바로 넣어줄 수 있다는 장점이 있다. 하나의 컴포넌트에 모든걸 표현하려면 이렇게 각각의 요소가 사용하는 모든 props들을 상위 컴포넌트가 받아서 넘겨주어야 해서 무수히 많은 props들을 받게 된다. 그리고 상위 컴포넌트는 단순히 props를 하위 컴포넌트로 전달해주는 역할만 하게 되어 props drilling이 일어난다. </p>
<pre><code class="language-tsx">function CompoundInput(props: CompoundInputProps) {
    const { title, children, option } = props

    return (
        &lt;CompoundInputContainerBox&gt;
            &lt;CompoundInputTitleContainer&gt;
                &lt;CompoundInputTitle&gt;{title}&lt;/CompoundInputTitle&gt;
                {option !== &#39;&#39; &amp;&amp; &lt;p&gt;{option}&lt;/p&gt;}
            &lt;/CompoundInputTitleContainer&gt;
            {children}
        &lt;/CompoundInputContainerBox&gt;
    )
}

CompoundInput.TextInput = CompoundInputTextInput
CompoundInput.Dropdown = CompoundInputDropdown
CompoundInput.Tag = CompoundInputTag
CompoundInput.Textarea = CompoundInputTextarea

export default CompoundInput</code></pre>
<p>구현해준 하위 컴포넌트들은 <code>CompoundInput</code> 컴포넌트의 static property로 만들어주었다. 이렇게 해주면 사용할 때 <code>CompoundInput</code>만 <code>import</code>하면 다른 요소들도 사용할 수 있다.</p>
<p>만들어진 컴파운드 컴포넌트는 이렇게 기본적인 형태로 사용하거나,</p>
<pre><code class="language-tsx">&lt;CompoundInput title=&quot;제목&quot;&gt;
    &lt;CompoundInput.TextInput
        placeholder=&quot;입력해주세요.&quot;
        register={register(&#39;title&#39;)}
    /&gt;
&lt;/CompoundInput&gt;</code></pre>
<p>특정 요소를 넣고자 하면 이렇게 children 자리에 원하는대로 넣어주기만 하면 된다.</p>
<pre><code class="language-tsx">&lt;CompoundInput title=&quot;희망 가격&quot;&gt;
    &lt;PriceContainer&gt;
        &lt;PriceInput
            value={displayPrice}
            placeholder=&quot;가격을 입력해주세요.&quot;
            onChange={(e) =&gt; handleChangePrice(e)}
        /&gt;
        &lt;PriceCurrencyOption
            currency={currency}
            setCurrency={setCurrency}
        /&gt;
    &lt;/PriceContainer&gt;
&lt;/CompoundInput&gt;</code></pre>
<p>직접 사용해보니 컴파운드 컴포넌트는 (하나의 컴포넌트에서 타입을 나누어 보여주는것에 비하면) 내용을 풀어 쓰는거나 다름없어서, 사용하는 쪽의 코드가 길어진다. 하지만 내용물이 미묘하게 다르거나 아주 다양할 경우에는 모든 상황을 한 파일 안에서 고려하기가 매우 어려워진다. 이럴 때 컴파운드 컴포넌트를 사용한다면 다양한 상황에 유연하게 대처할 수 있고, 앞서 말한 props drilling의 문제도 해결할 수 있게 된다. 다음에도 비슷하게 여러가지 형태를 가진 컴포넌트를 만나게 된다면 적절하게 사용해볼 수 있을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[다국어 텍스트가 지원되는 웹사이트  만들기]]></title>
            <link>https://velog.io/@jess_apr/%EB%8B%A4%EA%B5%AD%EC%96%B4-%ED%85%8D%EC%8A%A4%ED%8A%B8%EA%B0%80-%EC%A7%80%EC%9B%90%EB%90%98%EB%8A%94-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jess_apr/%EB%8B%A4%EA%B5%AD%EC%96%B4-%ED%85%8D%EC%8A%A4%ED%8A%B8%EA%B0%80-%EC%A7%80%EC%9B%90%EB%90%98%EB%8A%94-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 08 Apr 2024 01:31:48 GMT</pubDate>
            <description><![CDATA[<p>지금 회사에서 구현하고 있는 서비스는 해외 고객들을 타겟으로 하고 있어 다양한 언어를 지원해야 한다. 단순히 언어만 바꿔주면 되는거 아닌가 했는데, 텍스트를 하드코딩하면 당연히 안되고 생각보다 신경써야 할 부분이 많았다.</p>
<h2 id="i18next-라이브러리">i18next 라이브러리</h2>
<p>i18n은 <strong>internationalization</strong>(국제화) 라는 단어를 <strong>i+사이에 18개의 문자+n</strong>이라는 뜻으로 줄인 말로, 다양한 국가에 맞추어 쉽게 현지화를 할 수 있게 애플리케이션을 설계하는 것을 말한다. </p>
<p>웹사이트의 다국어 지원을 할 때 i18n을 적용할 수 있게 도와주는 툴을 사용하면 훨씬 더 쉽게 작업을 할 수 있다. 나는 <code>react-i18next</code>라는 프레임워크를 선택했다. 아무래도 장기적으로 운영을 해야 하는 회사 프로젝트이다보니 프로젝트에 사용할 툴을 선택할 때 현재 사용하는 유저가 많고 지원이 잘 되는지를 중점적으로 보게되는 것 같다. i18next는 Documentation도 깔끔하게 잘 되어있고, 사용하는 유저들도 많아서 정보도 많다.</p>
<h2 id="다국어-텍스트-적용하기">다국어 텍스트 적용하기</h2>
<p>우선 <code>react-i18next</code>를 설치해준다. 현재 프로젝트에서는 yarn을 사용하고 있어서 yarn으로 설치해주었다.</p>
<pre><code>yarn add react-i18next i18next</code></pre><p>설치를 하고 나면 기본적인 설정을 해준다. 여기서 <code>import</code>하고 있는 json 파일은 각 나라 언어의 텍스트가 담긴 파일이다.</p>
<pre><code>import i18n from &#39;i18next&#39;
import { initReactI18next } from &#39;react-i18next&#39;
import translationEN from &#39;./en/translation.json&#39;
import translationKO from &#39;./ko/translation.json&#39;

const resources = {
    en: {
        translation: translationEN,
    },
    ko: {
        translation: translationKO,
    },
}

i18n.use(initReactI18next).init({
    resources,
    lng: &#39;ko&#39;,
    fallbackLng: &#39;ko&#39;,
    interpolation: {
        escapeValue: false,
    },
})

export default i18n</code></pre><ul>
<li><code>lng</code>: 처음 웹사이트에 들어갔을 때 기본 설정이 되는 언어</li>
<li><code>fallbackLng</code>: 번역 파일에서 텍스트를 찾을 수 없는 경우 출력되는 언어</li>
</ul>
<p>각 언어의 텍스트는 언어마다 폴더를 생성한 후, json 파일에 담아준다. key는 자기가 원하는대로 텍스트의 의미를 담은 영어 단어로 하면 되고, value는 웹사이트에 출력될 텍스트가 들어간다. 언어마다 같은 의미의 단어들은 key가 같아야 1:1 매칭이 된다.</p>
<pre><code>// /locales/ko/translation.json
{
    &quot;header&quot;: {
        &quot;login: &quot;로그인&quot;,
        &quot;logout&quot;: &quot;로그아웃&quot;,
    }
    &quot;change&quot;: &quot;언어 변경&quot;,
}

// /locales/en/translation.json
{
    &quot;header&quot;: {
        &quot;login: &quot;Login&quot;,
        &quot;logout&quot;: &quot;Logout&quot;,
    }
    &quot;change&quot;: &quot;Change Language&quot;,
}</code></pre><p>그리고 텍스트를 사용할 곳에서 <code>useTranslation()</code>을 사용해준다.</p>
<pre><code>import { useTranslation } from &quot;react-i18next&quot;;
import i18n from &quot;locales/i18n&quot;;

function Example() {
  const { t } = useTranslation()

  return (
    &lt;&gt;
      &lt;div&gt;{t(&#39;header.login&#39;)}&lt;/div&gt;
      &lt;div&gt;{t(&#39;header.logout&#39;)}&lt;/div&gt;
    &lt;/&gt;
  )</code></pre><p>텍스트를 적용하고 나면 기본 언어로 설정된 언어가 나온다. 언어를 바꿔주고 싶다면 <code>i18n.changeLanguage(lang)</code>를 사용하면 된다.</p>
<pre><code>import { useTranslation } from &quot;react-i18next&quot;;
import i18n from &quot;locales/i18n&quot;;

function Example() {
  const { t } = useTranslation()

  function handleChangeLang() {
      i18n.language === &#39;ko&#39;
          ? i18n.changeLanguage(&quot;en&quot;)
          : i18n.changeLanguage(&quot;ko&quot;)
  }

  return (
    &lt;&gt;
      &lt;div&gt;{t(&#39;login&#39;)}&lt;/div&gt;
      &lt;div&gt;{t(&#39;logout&#39;)}&lt;/div&gt;
      &lt;button onClick={() =&gt; handleChangeLang()}&gt;{t(&#39;change&#39;)}&lt;/button&gt;
    &lt;/&gt;
  )
</code></pre><h2 id="다국어-텍스트의-특정-부분에-스타일-적용하기">다국어 텍스트의 특정 부분에 스타일 적용하기</h2>
<p>다국어 텍스트를 설정하는것 자체는 어렵지 않았는데, 한가지 문제가 있었다. 
<img src="https://velog.velcdn.com/images/jess_apr/post/4571385c-c21c-424e-a717-99d2d5a70b7b/image.png" alt=""></p>
<p>이런식으로 텍스트에 하이라이트가 들어가야 하는 부분이 있는데, json 파일에는 문자열만 들어갈 수 있어 부분적으로 태그를 넣어 스타일링을 할 수가 없었다. 하이라이트가 들어간 부분을 기준으로 하나의 문장을 여러개로 나누자니 각 언어마다 하이라이트된 단어의 개수나 하이라이트가 들어가는 부분이 다르기 때문에 그렇게 해줄수는 없었다.</p>
<h3 id="문자열-포맷팅">문자열 포맷팅</h3>
<p>그래서 각 문장에 하이라이트가 되는 부분에 차례로 숫자를 넣고, 그 숫자를 하이라이트 시킬 단어로 대체시켜주는 문자열 포맷팅 함수를 제작해주었다.</p>
<pre><code>{
    &quot;text&quot;: {
        &quot;normalText&quot;: &quot;여기서 {0}를 {1} 해보겠습니다.&quot;
        &quot;highlight1&quot;: &quot;글씨&quot;
        &quot;highlight2&quot;: &quot;하이라이트&quot;
    }
}</code></pre><p>하이라이트 될 단어들은 문장에 들어가기 전에 태그를 씌워주는 함수에 의해 하이라이트를 하기 위해 필요한 태그가 씌워지게 된다.</p>
<pre><code>export function FormatHighlightString(rep: string, str: string[]): string[] {
    const ret: string[] = []
    str.forEach((it) =&gt; ret.push(rep.replace(&#39;%s&#39;, it)))
    return ret
}</code></pre><p><code>rep</code>는 텍스트에 씌워주고 싶은 태그이다. 태그 사이에 <code>%s</code>라는 문자를 넣으면 <code>%s</code>부분이 텍스트로 대체된다. </p>
<pre><code>// 예시) p태그 씌우기
const formattedStrings = FormatHighlightString(&#39;&lt;p&gt;%s&lt;/p&gt;&#39;, [t(&#39;text.highlight1&#39;), t(&#39;text.highlight2&#39;)])</code></pre><p>태그가 씌워진 하이라이트 텍스트는 아래 문자열 포맷팅 함수에 의해 제자리로 들어가게 된다.</p>
<pre><code>export function FormatString(str: string, val: string[]) {
    let ret = str
    val.forEach((it, idx) =&gt; {
        ret = ret.replace(`{${idx}}`, it)
    })
    return ret
}</code></pre><p>위 함수는 인자를 두개 받는다. 하이라이트가 된 단어가 들어갈 문자열 <code>str</code>, 그리고 하이라이트 될 단어들이 들어있는 배열 <code>val</code>을 받게 된다. 그러면 <code>val</code> 배열을 하나씩 돌며 <code>str</code> 문자열에 들어있는 숫자에 <code>val</code> 배열의 각 숫자 인덱스에 위치하는 단어들이 들어가게 된다.</p>
<pre><code>// 적용 예시)
FormatString(t(&#39;text.normalText&#39;), formattedStrings)</code></pre><p>이렇게 해주면 언어마다 하이라이트 되는 부분이 다르거나 하이라이트 되는 단어의 개수가 다르더라도 기존 코드의 변경 없이 스타일을 적용해줄 수 있다.</p>
<pre><code>{
    &quot;text&quot;: {
        &quot;normalText&quot;: &quot;I will {0} {1} {2} here.&quot;
        &quot;highlight1&quot;: &quot;highlight&quot;
        &quot;highlight2&quot;: &quot;this&quot;
        &quot;highlight3&quot;: &quot;word&quot;
    }
}</code></pre><p>하이라이트되는 단어가 가장 많은 언어를 기준으로 <code>FormatHighlightString</code> 함수의 두번째 배열을 적어준다. 위 예시에서는 한국어가 2개, 영어가 3개이므로 영어에서의 하이라이트 단어 개수가 기준이 된다.</p>
<pre><code>const formattedStrings = const formattedStrings = FormatHighlightString(&#39;&lt;p&gt;%s&lt;/p&gt;&#39;, [t(&#39;text.highlight1&#39;), t(&#39;text.highlight2&#39;), t(&#39;text.highlight3&#39;)])

FormatString(t(&#39;text.normalText&#39;), formattedStrings)</code></pre><p>이렇게 가장 하이라이트 단어의 수가 많은 언어를 기준으로 함수를 적용하면, 그보다 적은 단어를 사용하는 언어들은 자동적으로 없는 단어를 처리하지 않게된다. 한국어에는 <code>normalText</code>에 숫자가 1까지밖에 없으므로 반복문에서 0자리와 1자리만 대치하고 끝나게 된다. <code>val</code> 배열에 단어가 세개가 들어간다 하더라도 (물론 마지막 단어는 undefined일 것이다) 마지막 단어는 반복문에서 사용되지 않는 것이다.</p>
<p>마지막으로 텍스트의 html 태그를 파싱해주는 라이브러리를 사용해서 파싱을 해주면 스타일이 적용된다. 텍스트라서 html 태그 파싱을 해주지 않으면 태그가 텍스트 그대로 출력된다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/1493fad6-f740-4502-9331-3ea8bd5e43f3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/8f6e6acc-2f1c-46d5-8f0b-89a05c187321/image.png" alt=""></p>
<p>코드 변경 없이 언어를 바꿔도 스타일이 잘 적용된다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[통신 암호화를 위한 기초 공부2: AES, RSA]]></title>
            <link>https://velog.io/@jess_apr/%ED%86%B5%EC%8B%A0-%EC%95%94%ED%98%B8%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-%EA%B8%B0%EC%B4%88-%EA%B3%B5%EB%B6%802-AES-RSA</link>
            <guid>https://velog.io/@jess_apr/%ED%86%B5%EC%8B%A0-%EC%95%94%ED%98%B8%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-%EA%B8%B0%EC%B4%88-%EA%B3%B5%EB%B6%802-AES-RSA</guid>
            <pubDate>Mon, 18 Mar 2024 13:56:57 GMT</pubDate>
            <description><![CDATA[<h1 id="암호화-방식">암호화 방식</h1>
<h2 id="aes">AES</h2>
<h3 id="aes란">AES란?</h3>
<p>Advanced Encryption Standard의 약자로, 대칭현 블록 암호화 알고리즘이다. 레인델(Rijndael) 알고리즘을 사용한다. 암호화 키는 128 비트, 192 비트, 256 비트 중 하나가 될 수 있으며, 각각 AES-256, AES-192, AES-256으로 불린다. (Key가 길수록 brute force 공격에 강함)</p>
<h3 id="대칭-키-블록-암호">대칭 키 블록 암호</h3>
<p>대칭 키 암호는 암호화와 복호화에 같은 암호 키를 쓰는 알고리즘을 말한다. 암호화 키가 노출되면 안되고 비대칭 암호보다 보안성은 떨어지지만, 시간이나 비용면에서 장점이 있다.</p>
<p>블록 암호는 대칭 키 암호의 종류 중 하나로, 암호화하고자 하는 정보를 정해진 블록 단위로 나눠 암호화 하는 시스템을 말한다.</p>
<h3 id="aes-암호화-과정">AES 암호화 과정</h3>
<p>암호화 키의 길이에 따라 SubBytes, ShiftRows, MixColumns, AddRoundKey 네 개의 과정을 10, 12, 14 라운드 실행한다. (128 → 10 라운드, 192 → 12 라운드, 256 → 14 라운드)</p>
<p>자세한 내용은 봐도 이해가 어려워 적지 않았다. 현재는 개념만 알아두려고 하는 것이기 때문에 나중에 추가로 공부가 필요하면 자세히 공부해보아야겠다.</p>
<h2 id="rsa">RSA</h2>
<h3 id="rsa란">RSA란?</h3>
<p>공개키 암호화 알고리즘 중 하나이다. 암호화뿐만 아니라 전자서명도 가능하여 전자상거래 등에 활용되고 있다. 큰 숫자는 소인수분해를 하기가 어렵다는 점을 이용하여 암호화를 하며, 보안도가 매우 높다. 다만 AES 방식에 비해 계산 집약적이고 느리다는 단점이 있다. 암호화 키의 길이에 따라 RSA-896, RSA-1024, RSA-1536, RSA-2048 등이 있고, 중요도가 높은 정보일 경우 RSA-4096이나 RSA-8192를 쓰기도 한다.</p>
<h3 id="공개-키-암호">공개 키 암호</h3>
<p>암호화와 복호화에 사용하는 키가 서로 다른 암호를 말한다. 대칭 키 암호는 암호화와 복호화에 사용되는 키가 같아 암호화 키를 암호를 복호하고자 하는 사람에게 반드시 전달해주어야 한다. 이는 보안상 허점이 될 수 있다. 공개 키 암호는 그럴 필요가 없어 보안성이 훨씬 뛰어나다. </p>
<ul>
<li>공개 키 암호로 기밀 내용 전달하는 법<ol>
<li>B가 자신의 공개 키를 공개한다.</li>
<li>A는 이 공개키로 문서를 암호화 한다.</li>
<li>암호화된 문서를 B에게 전달한다.</li>
<li>B는 자신이 가진 개인키로 문서를 해독한다.</li>
</ol>
</li>
</ul>
<p>그리고 기밀 내용의 전달 뿐 아니라 발행자 증명, 부인 방지 등에도 사용할 수 있다.</p>
<h3 id="rsa-암호화-과정">RSA 암호화 과정</h3>
<p>AES에 비해 과정을 이해하기가 쉬워 나무위키에 남겨진 글을 가져와보았다.</p>
<ol>
<li>두 소수 p,q 를 준비한다.</li>
<li>p−1, q−1과 각각 서로소인 정수 e를 준비한다.</li>
<li>ed를 (p−1)(q−1)으로 나눈 나머지가 1이 되도록 하는 d를 찾는다.</li>
<li>N=pq를 계산한 후, N과 e를 공개한다. 이들이 바로 <strong>공개키</strong>이며, 상대방이 평서문을 암호문으로 바꿀때 쓴다. 한편 d는 숨겨두는데, 이 수가 바로 <strong>개인키</strong>이며, 공개키로 상대방이 만든 암호문을 해독할 때 쓴다.</li>
<li>이제 p,q,(p−1)(q−1)는 필요 없거니와 있어 봐야 보안에 오히려 문제를 일으킬 수 있으니, 파기한다.</li>
</ol>
<p>구체적 예시는 <a href="https://namu.wiki/w/RSA%20%EC%95%94%ED%98%B8%ED%99%94#s-5">[RSA 암호화 - 5. 예시]</a>에 아주 잘 설명되어있다.</p>
<hr>
<p><strong>참고자료</strong></p>
<p><a href="https://namu.wiki/w/AES">https://namu.wiki/w/AES</a></p>
<p><a href="https://velog.io/@t1mmy_t1m/AES%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC">https://velog.io/@t1mmy_t1m/AES에-대하여</a></p>
<p><a href="https://blog.naver.com/PostView.naver?blogId=wnrjsxo&amp;logNo=221711255389">https://blog.naver.com/PostView.naver?blogId=wnrjsxo&amp;logNo=221711255389</a></p>
<p><a href="https://ko.wikipedia.org/wiki/RSA_%EC%95%94%ED%98%B8">https://ko.wikipedia.org/wiki/RSA_암호</a></p>
<p><a href="https://namu.wiki/w/RSA%20%EC%95%94%ED%98%B8%ED%99%94">https://namu.wiki/w/RSA 암호화</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[통신 암호화를 위한 기초 공부 : Base64, ByteArray]]></title>
            <link>https://velog.io/@jess_apr/%ED%86%B5%EC%8B%A0-%EC%95%94%ED%98%B8%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-%EA%B8%B0%EC%B4%88-%EA%B3%B5%EB%B6%80-Base64-bytearray</link>
            <guid>https://velog.io/@jess_apr/%ED%86%B5%EC%8B%A0-%EC%95%94%ED%98%B8%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-%EA%B8%B0%EC%B4%88-%EA%B3%B5%EB%B6%80-Base64-bytearray</guid>
            <pubDate>Sun, 10 Mar 2024 14:41:51 GMT</pubDate>
            <description><![CDATA[<h2 id="base64">Base64</h2>
<h3 id="base64란">Base64란?</h3>
<p>화면에 표시되지 않는 바이너리 데이터를 ASCII 문자열로 표현하는 인코딩 방식이다. ASCII 문자열은 거의 모든 컴퓨터 및 통신 시스템에서 지원하기 때문에 크로스 플랫폼 및 다양한 시스템 간 호환성을 보장하기 위해 사용한다.</p>
<blockquote>
<p>바이너리 데이터: 이진 데이터. 0과 1로 상태를 나타낸 데이터를 말한다.</p>
</blockquote>
<h3 id="base64의-형태">Base64의 형태</h3>
<p>영문 대문자(A ~ Z) 26개, 영문 소문자(a ~ z) 26개, 숫자 (0 ~ 9) 10개에 +, / 2개를 합쳐 64개의 ASCII 문자열로 표현한다. 마지막 두개의 기호는 +와 /가 표준이고, 변종에 따라 다른 기호를 쓸 수도 있다. 이 64개의 문자들은 iso-8859나 utf 등 어떤 charset을 사용하더라도 동일하게 표현될 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/d3d24b7e-5b74-4c88-8422-0eaccbce057c/image.png" alt=""></p>
<h3 id="base64-인코딩-방법">Base64 인코딩 방법</h3>
<ol>
<li><p>원본 문자열 또는 데이터를 바이너리 데이터로 변환한다.
<img src="https://velog.velcdn.com/images/jess_apr/post/db044ce4-d30f-4748-9ab7-88924d9960d0/image.png" alt=""></p>
</li>
<li><p>8-bit로 표현된 바이너리 데이터 3개를 그룹화 하여 24-bit buffer를 생성한 후, 6-bit 단위 4개로 잘라낸다.
<img src="https://velog.velcdn.com/images/jess_apr/post/98a11acc-be0a-4784-b0a4-7fe12993e7a4/image.png" alt=""></p>
</li>
<li><p>각 6-bit 그룹을 10진법으로 변환한다.
<img src="https://velog.velcdn.com/images/jess_apr/post/4e1f3af7-144b-4e31-a56d-efa7a16758a5/image.png" alt=""></p>
</li>
<li><p>10진법으로 변환된 값을 Base64 테이블에 해당하는 ASCII 문자에 매핑한다.
<img src="https://velog.velcdn.com/images/jess_apr/post/6b3d2bd3-d327-4211-87ca-cf1bebcf4e18/image.png" alt=""></p>
</li>
</ol>
<blockquote>
<p>💡 <strong>8-bit 이진 데이터가 3개로 나누어 떨어지지 않을땐 어떻게 하나요?</strong></p>
</blockquote>
<ol>
<li>뒤에 남는 4개의 자리수를 padding값 0000으로 채운다.</li>
<li>10진수로 변환한다.</li>
<li>Base64 ASCII 테이블에 매핑한다.</li>
<li>padding값을 == 문자열로 표현한다.
<img src="https://velog.velcdn.com/images/jess_apr/post/f8b56192-8b30-47d6-937c-bbb604f666ff/image.png" alt=""></li>
</ol>
<h3 id="base64를-사용하는-이유">Base64를 사용하는 이유</h3>
<p>인터넷 초기에는 거의 모든 커뮤니케이션이 텍스트 기반이었다. 이메일이 주 통신 수단이었기 때문에 SMTP(Simple Mail Transfer Protocol)를 기반으로 하여 모든 메시지를 7-bit ASCII로 제한하여 통신했다. 하지만 이미지, 영상과 같은 데이터를 메일 첨부 파일로 보내려면 8-bit ASCII가 필요했다. 이 때, MIME(Multipurpose Internet Mail Extension)이라는 새로운 인터넷 표준이 등장하여 8-bit 이상의 데이터를 주고받을 수 있게 해줬고, 이메일 멀티미디어 데이터를 첨부할 수 있게 되었다.</p>
<p>MIME은 Base64 인코딩 체계를 사용하여 8-bit ASCII 및 Non-ASCII 데이터를 전송했다. Base64를 사용하면 바이너리 데이터를 텍스트 기반 규격으로 다룰 수 있다는 장점이 있다. 기존 ASCII 바이너리 데이터에는 ASCII 코드에 포함되지 않는 데이터가 있어 데이터 손실이 발생할 수 있다. ASCII는 7-bit 데이터이고 바이너리 데이터는 8-bit를 사용하기 때문이다. 하지만 Base64는 제어 문자와 일부 특수 문자를 제외한 안전한 출력 문자만 이용하므로 데이터 전달에 더 적합하다. </p>
<p>최근에는 텍스트 데이터 사용만 제한되어 있거나 텍스트 데이터를 사용하는게 편리한 경우 등 데이터를 텍스트 형태로 사용하는 것이 유용한 경우에 Base64 인코딩 방법을 사용한다. 예를 들어 JSON이나 HTML은 텍스트로 이루어져 있기 때문에 모든 데이터를 텍스트로 변환해줘야 전송할 수 있다. 또, 클라이언트와 서버간 http 프로토콜을 </p>
<blockquote>
<p>사용 이유 요약</p>
</blockquote>
<ul>
<li>통신 과정에서 바이너리 데이터의 손실을 막기 위해 사용</li>
<li>문자열로 시스템간 데이터를 전달할 때 안전하게 전달하기 위해 사용</li>
<li>과거에는 8-bit ASCII와 Non-ASCII 데이터를 전송하기 위해 사용</li>
</ul>
<h2 id="bytearray">ByteArray</h2>
<h3 id="bytearray란">ByteArray란?</h3>
<p>ByteArray란 말 그대로 byte를 담고 있는 배열이다. Byte는 8bits의 메모리를 받고, 0부터 255까지의 값을 저장한다. 숫자형과 같은 원시 타입이 들어있는 배열과는 다르게 이진 데이터 그 자체를 변환하지 않고 담아낼 수 있다.</p>
<h3 id="arraybuffer">ArrayBuffer</h3>
<p>JavaScript와 TypeScript에서 이진 데이터를 담는 객체는 ArrayBuffer라고 부른다. 이름에 Array가 들어가있지만, 배열처럼 동작하는 것은 아니고 고정된 길이를 가진 연속적인 메모리 영역의 참조(reference)이다. 따라서 객체 안의 데이터를 직접 다루는 것은 불가능하고, <code>TypedArray</code>나 <code>DataView</code> 객체를 생성하여 ArrayBuffer의 내용을 읽을 수 있다.</p>
<h3 id="arraybuffer를-사용하는-이유">ArrayBuffer를 사용하는 이유</h3>
<p><code>Blob</code>은 Binary Large Object의 약자로, 주로 이미지나 오디오, 비디오 등의 멀티미디어 파일을 이진 데이터의 형태로 저장한 객체를 말한다. 보통 멀티미디어 파일의 경우 용량이 매우 크기 때문에 이러한 파일들을 데이터베이스에 효율적으로 저장하기 위해 고안된 타입이다.</p>
<p>ArrayBuffer는 이러한 커다란 멀티미디어 데이터를 브라우저에서 다룰 수 있게 해준다. 커다란 데이터를 잘게 쪼개어 전송할 때, 이 잘게 쪼개진 데이터 조각들을 일정 크기만큼 모아 출력장치로 전달해주는 역할을 하는 것이 Buffer이기 때문이다. Base64를 통해 전달된 데이터는 ArrayBuffer를 통해 Blob 타입으로 변환된다.</p>
<hr>
<p><strong>참고자료</strong></p>
<ul>
<li><a href="https://namu.wiki/w/BASE64">BASE64</a></li>
<li><a href="https://medium.com/atant/base-64-%EC%9D%B8%EC%BD%94%EB%94%A9%EC%97%90-%EB%8C%80%ED%95%B4-%EC%84%A4%EB%AA%85%ED%95%98%EC%8B%A4-%EC%88%98-%EC%9E%88%EB%82%98%EC%9A%94-a67f204bb3b2">Base 64 인코딩에 대해 설명하실 수 있나요?</a></li>
<li><a href="https://blog.naver.com/612_44kk/222457971397">&#39;Base64&#39;란?</a></li>
<li><a href="https://bito.ai/resources/java-byte-array-java-explained/">Storing and Manipulating Data Efficiently with Java Byte Arrays</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer">ArrayBuffer</a></li>
<li><a href="https://ko.javascript.info/arraybuffer-binary-arrays">ArrayBuffer, binary arrays</a></li>
<li><a href="https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-Base64-Blob-ArrayBuffer-File-%EB%8B%A4%EB%A3%A8%EA%B8%B0-%EC%A0%95%EB%A7%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%84%A4%EB%AA%85#blob_binary_large_object">Base64 / Blob / ArrayBuffer / File 다루기 총정리</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 공부 기록]]></title>
            <link>https://velog.io/@jess_apr/Next.js-%EA%B3%B5%EB%B6%80-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@jess_apr/Next.js-%EA%B3%B5%EB%B6%80-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Mon, 19 Feb 2024 14:24:25 GMT</pubDate>
            <description><![CDATA[<h3 id="nextjs란">Next.js란?</h3>
<p>Next.js는 리액트를 기반으로 한 자바스크립트 프레임워크로, 풀스택 웹 어플리케이션을 구축하기 위해 사용된다. 리액트 컴포넌트로 UI를 만들고, Next.js 사용해서 추가 기능과 최적화를 수행한다.</p>
<h3 id="nextjs는-프레임워크">Next.js는 프레임워크?</h3>
<p>리액트는 공식문서에서 라이브러리라고 소개하고 있는데 넥스트는 프레임워크라고 표현했다. 넥스트는 리액트를 기반으로 해서 만들어진 것인데, 왜 리액트는 라이브러리라고 하고 넥스트는 프레임워크라고 할까? 이에 대해 설명을 해놓은 공식 홈페이지 글을 찾을 수 있었다. <a href="https://nextjs.org/learn/react-foundations/what-is-react-and-nextjs">(링크 : About React and Next.js)</a></p>
<p>여기서 <strong>Next.js는 풀 스택 웹 어플리케이션을 빠르게 만들 수 있는 &#39;building blocks&#39;를 제공한다</strong>고 써있다. 웹 어플리케이션을 만들기 위해서는 UI, 라우팅, 데이터 fetching, 렌더링 등등의 요소가 필요하다. 리액트는 UI라는 하나의 요소를 만들기 위해 사용하는 라이브러리이다. 하지만 넥스트는 UI뿐만 아니라 렌더링, 라우팅 등 다양한 요소들을 프로젝트에 쉽게 적용할 수 있도록 기능을 제공해준다. 대신 라이브러리를 쓸 때와는 달리 그 기능을 위해 어떤 프로그램을 쓸 지는 사용자가 마음대로 정할 수 없다. 넥스트에서 UI를 만드려면 반드시 리액트 컴포넌트를 사용해야하는 것과 같다. 따라서 넥스트는 프레임워크라고 할 수 있다.</p>
<h3 id="서버-사이드-렌더링ssr">서버 사이드 렌더링(SSR)</h3>
<p>SSR을 지원한다는 사실은 넥스트의 가장 큰 특징이자 장점으로 꼽힌다. SSR은 서버에서 미리 페이지를 렌더링 해서 보내주기 때문에, CSR에 비해 초기 로딩 속도가 빠르고 검색 엔진 최적화가 가능하다.</p>
<p>하지만 넥스트는 모든 컴포넌트를 SSR로 처리하진 않는다. 넥스트는 기본적으로 모든 페이지를 pre-rendering한다. 서버에서 미리 HTML 페이지를 생성한다는 말이다. Pre-rendering에는 두가지 방법이 있다. 정적 사이트 생성(SSG)(+ISR)과 서버 사이드 렌더링(SSR)이다.</p>
<blockquote>
<p><strong>정적 사이트 생성 (Static Site Generation)</strong>
HTML을 빌드 시점에 생성한다. 이렇게 생성된 HTML은 매 요청마다 재사용된다.
<img src="https://velog.velcdn.com/images/jess_apr/post/eeee1795-8857-4b54-aac3-cbbc904255b7/image.png" alt="">
<strong>증분 정적 재생성 (Incremental Static Regeneration)</strong>
SSG와 마찬가지로 빌드 시점에 HTML을 생성하지만, 일정 주기마다 페이지를 다시 생성해준다. SSG는 데이터가 업데이트 되면 페이지를 새로 빌드해야 하지만, ISR은 빌드를 다시 하지 않아도 된다.</p>
</blockquote>
<blockquote>
<p><strong>서버 사이드 렌더링 (Server Side Rendering)</strong>
요청이 들어오면 HTML을 생성한다.
<img src="https://velog.velcdn.com/images/jess_apr/post/153f88a0-623a-4094-96da-410fbb365dc2/image.png" alt=""></p>
</blockquote>
<p>넥스트는 Static Rendering(기본값)과 Dynamic Rendering 방식이 있다. SSG를 위해서는 어떤 렌더링 방식을 사용할지, SSR을 위해서는 어떤 렌더링 방식을 사용할지 고민할 필요 없이 <code>fetch()</code> 함수의 옵션을 원하는 대로 설정해주면 자동으로 렌더링 방식을 정해준다.</p>
<p><strong>SSG</strong></p>
<pre><code>  const response = await fetch(&quot;https://...&quot;, {cache: &quot;force-cache&quot;});

  // 또는 생략 가능

  const response = await fetch(&quot;https://...&quot;);</code></pre><p><code>cache</code> 옵션을 <code>force-cache</code>로 주면 SSG 방식을 사용한다. 빌드 시점에 데이터 페칭을 진행하고, 직접 무효화하기 전까지 데이터를 캐싱한다. 기본값이어서 생략 가능하다.</p>
<p><strong>SSR</strong></p>
<pre><code>  const response = await fetch(&quot;https://...&quot;, {cache: &quot;no-store&quot;});</code></pre><p><code>cache</code> 옵션을 <code>no-store</code>로 주면 SSR 방식으로 작동한다. 불러온 데이터를 캐싱하지 않고 요청이 들어올 때마다 데이터를 패칭하여 페이지를 만들어준다.</p>
<p><strong>ISR</strong></p>
<pre><code>const response = await fetch(&quot;https://url&quot;, {next: {revalidate: 60});</code></pre><p><code>next</code> 옵션에서 <code>revalidate</code>값을 설정한다(초 단위). <code>cache</code> 옵션의 <code>force-cache</code>값이 기본으로 적용되어 빌드시에 페이지를 생성하지만, <code>revalidate</code>의 값으로 준 시간이 지나면 페이지를 새로 생성한다.</p>
<h3 id="서버-컴포넌트">서버 컴포넌트</h3>
<p>서버 컴포넌트는 서버에서만 동작하는 컴포넌트를 말한다. React 18, Next 13부터 도입된 개념으로, 이 이전 버전의 리액트는 클라이언트 컴포넌트만 사용했다. 지금도 리액트는 기본적으로 클라이언트 컴포넌트를 사용하지만, 넥스트는 특별한 설정을 하지 않을 경우 서버 컴포넌트를 사용한다.</p>
<h4 id="❗️-여기서-주의할-점">❗️ <strong>여기서 주의할 점</strong></h4>
<p>서버 컴포넌트는 SSR과 같지 않다. 서버 컴포넌트는 SSR을 대체하기 위한 기능이 아니라, 상호 협력적인 관계라고 생각하면 된다. 초기 HTML을 생성하기 위해서는 SSR이 필요하고, 서버 컴포넌트는 특정 컴포넌트가 클라이언트로 전달되지 않고 서버에서만 실행되도록 사용할 수 있다.</p>
<p>SSR은 HTML로 전달되기 때문에 refetch가 필요한 경우 HTML 전체를 다시 리렌더링 해야한다. 하지만 서버 컴포넌트는 HTML이 아니라 네트워크로 전송하기 더 적합한 JSON 유사 표기법의 형태로 클라이언트에게 전달된다. 따라서 서버 컴포넌트를 사용하면 페이지 전체를 다시 받아올 필요 없이 필요한 데이터만 리렌더링해서 클라이언트로 전달해줄 수 있다.</p>
<p>더 자세한 설명은 여기 ⬇️</p>
<ul>
<li><a href="https://leetrue.hashnode.dev/react-server-component-ssr">React Server Component 와 SSR 함께 공부해보기</a></li>
<li><a href="https://www.inflearn.com/questions/1137250/ssr%EA%B3%BC-rsc%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC-%EC%A7%88%EB%AC%B8%EC%9D%B4-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4">SSR과 RSC의 차이점에 관하여 질문이 있습니다.</a></li>
</ul>
<h4 id="서버-컴포넌트의-장점">서버 컴포넌트의 장점</h4>
<ol>
<li><strong>빠른 데이터 패칭</strong>
데이터 패칭이 일어나는 곳이 서버라서 데이터의 출처(source)와 더욱 가깝다. 따라서 데이터 패칭에 드는 시간이 짧아진다. </li>
<li><strong>향상된 보안성</strong>
민감한 데이터와 로직을 서버 측에서 관리할 수 있다.</li>
<li><strong>캐싱</strong>
페이지가 캐싱되어 여러 요청에 대해 재사용될 수 있다. 요청마다 렌더링과 데이터 패칭을 실행하는 비용을 줄일 수 있다.</li>
<li><strong>작은 번들 사이즈</strong>
클라이언트 자바스크립트 번들 사이즈를 크게 만들 가능성이 있는 dependency를 클라이언트로 전송하지 않고 사용할 수 있다.</li>
<li><strong>빠른 초기 화면 로딩과 First Contentful Paint(FCP)</strong>
자바스크립트 파싱이 완료되기 전에 서버에서 만든 HTML을 미리 보여주어 CSR에 비해 초기 화면 로딩이 빠르다.</li>
<li><strong>검색 엔진 최적화 가능</strong>
서버에서 HTML을 완성해서 보내주기 때문에 검색 엔진 봇이 데이터를 수집하기 용이하다.</li>
<li><strong>스트리밍</strong>
서버 컴포넌트는 렌더링 작업을 조각낸 후, 렌더링이 완료된 조각을 하나씩 클라이언트 측으로 전송해준다. 사용자는 전체 페이지가 전부 불러와질때까지 기다릴 필요 없이 페이지의 일부를 먼저 볼 수 있다.</li>
</ol>
<h4 id="서버-컴포넌트의-한계">서버 컴포넌트의 한계</h4>
<p>서버 컴포넌트에도 분명한 한계가 있다. 서버에서 렌더링 되기 때문에 이벤트 핸들러, useEffect, useState 등의 기능을 사용할 수 없고, 유저와의 상호작용이 어렵다는 점이다. 그렇지만 넥스트에서도 클라이언트 컴포넌트를 통해 이러한 한계를 극복할 수 있다.</p>
<h3 id="클라이언트-컴포넌트">클라이언트 컴포넌트</h3>
<p>클라이언트 컴포넌트를 사용하려면 컴포넌트 파일 최상단에 <code>&quot;use client&quot;</code>를 추가하면 된다. 그러면 리액트에서 했던것과 마찬가지로 리액트 훅과 이벤트 핸들러 등을 사용할 수 있게 된다. </p>
<p>다만, 클라이언트 컴포넌트는 클라이언트 컴포넌트만 import 할 수 있다는 것을 알아두어야 한다. 어떤 컴포넌트를 클라이언트 컴포넌트로 선언했다면, 그 컴포넌트의 자식들은 <code>&quot;use client&quot;</code>를 적어주지 않아도 전부 클라이언트 컴포넌트가 된다. 클라이언트 컴포넌트가 리렌더링이 되면 그 아래의 자식 컴포넌트들도 리렌더링이 되어야 하는데, 서버 컴포넌트는 하이드레이트 되거나 다시 렌더링을 할 수 없어 부모 컴포넌트가 바뀌어도 리렌더링이 불가능하기 때문이다.</p>
<h3 id="하이드레이션">하이드레이션</h3>
<p>넥스트에서 클라이언트 컴포넌트를 사용한다고 해서 전통적인 클라이언트 사이드 렌더링(CSR) 방식을 사용하는 것은 아니다. 서버에서 렌더링한 HTML과 번들링 된 JS 파일을 클라이언트로 보내면, 클라이언트 측에서는 이 HTML을 JS 코드와 연결하는 작업을 한다. 클라이언트가 HTML과 JS 코드를 연결해주는 이 과정을 <strong>하이드레이션</strong>이라고 부른다. 사용자는 서버에서 pre-rendering 된 페이지를 빠르게 볼 수 있고, hydration이 끝나면 상호작용이 가능해진다.
<img src="https://velog.velcdn.com/images/jess_apr/post/d3f7b4ab-e940-4f3a-b2e0-1ccd693db342/image.png" alt=""></p>
<h4 id="ssr--하이드레이션의-문제점">SSR + 하이드레이션의 문제점</h4>
<ul>
<li>데이터 가져오기 (서버) ⮕ HTML로 렌더링 (서버) ⮕  코드 불러오기 (클라이언트) ⮕ 하이드레이션 (클라이언트)</li>
</ul>
<p>이 과정에서 이전 단계가 끝나기 전에는 다음 단계가 진행될 수 없다. 따라서 사용자가 페이지 또는 원하는 정보를 늦게 받아보게 되거나, 상호작용을 할때까지 오래 기다리게 되는 현상이 생길 수 있다. 이 문제는 <code>Suspense</code>를 사용하여 해결할 수 있다.</p>
<pre><code>import { Suspense } from &#39;react&#39;
import { PostFeed, Weather } from &#39;./Components&#39;

export default function Posts() {
  return (
    &lt;section&gt;
      &lt;Suspense fallback={&lt;p&gt;Loading feed...&lt;/p&gt;}&gt;
        &lt;PostFeed /&gt;
      &lt;/Suspense&gt;
      &lt;Suspense fallback={&lt;p&gt;Loading weather...&lt;/p&gt;}&gt;
        &lt;Weather /&gt;
      &lt;/Suspense&gt;
    &lt;/section&gt;
  )
}</code></pre><p>데이터를 불러와야 하는 컴포넌트를 <code>&lt;Suspense&gt;</code>로 감싸면, 해당 컴포넌트의 데이터가 불러와지는 동안 다른 부분은 이를 기다리지 않고 스트리밍 기능을 통해 먼저 화면에 그려지고 하이드레이션이 진행될 수 있다. 데이터 패칭이 진행되는 동안에는 <code>fallback</code>에 지정해준 컴포넌트가 그려진다. 데이터 패칭이 끝나면 그 컴포넌트는 <code>fallback</code> 컴포넌트 자리에 들어가게 된다.</p>
<p>그리고 여러개의 컴포넌트가 하이드레이션이 진행되지 않은 상황에서 사용자가 한 컴포넌트를 클릭할 경우, 리액트는 해당 클릭을 기록해두었다가 사용자가 클릭한 컴포넌트에 대한 하이드레이션에 우선순위를 부여한다. 이렇게 화면 상에서 급한 부분을 먼저 하이드레이션 하는 것도 가능하게 해준다.</p>
<hr>
<p>참고자료</p>
<ul>
<li><a href="https://nextjs.org/learn-pages-router/basics/data-fetching/pre-rendering">https://nextjs.org/learn-pages-router/basics/data-fetching/pre-rendering</a></li>
<li><a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">https://nextjs.org/docs/app/building-your-application/rendering/server-components</a></li>
<li><a href="https://velog.io/@hamjw0122/Next.js-Next.js%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-Fetching%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95">https://velog.io/@hamjw0122/Next.js-Next.js%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-Fetching%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</a></li>
<li><a href="https://yozm.wishket.com/magazine/detail/2271/">https://yozm.wishket.com/magazine/detail/2271/</a></li>
<li><a href="https://velog.io/@hamjw0122/Next.js-Hydration">https://velog.io/@hamjw0122/Next.js-Hydration</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>