<?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>Tue, 18 Aug 2026 14:14:01 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[JavaScript와 브라우저 기초 복습하기]]></title>
            <link>https://velog.io/@jess_apr/JavaScript%EC%99%80-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EA%B8%B0%EC%B4%88-%EB%B3%B5%EC%8A%B5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jess_apr/JavaScript%EC%99%80-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EA%B8%B0%EC%B4%88-%EB%B3%B5%EC%8A%B5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 18 Aug 2026 14:14:01 GMT</pubDate>
            <description><![CDATA[<h2 id="javascript-동작-원리">JavaScript 동작 원리</h2>
<h3 id="call-stack과-event-loop">Call Stack과 Event Loop</h3>
<p><strong>Call Stack</strong>이란 자바스크립트 코드가 실행되는 동안 어떤 함수를 실행하고 있는지 순서를 기록하는 Data 구조이다. 후입선출(LIFO)의 구조를 가지기 때문에 함수를 실행하게 되면 그 함수의 기록이 Call Stack의 제일 위에 쌓이게 되고, 함수가 종료되면 제일 위의 기록이 제거된다.</p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/8ef8ea99-f2a7-485b-bbe0-8020f0b65be9/image.png" alt=""></p>
<p>자바스크립트는 단일 스레드 프로그래밍 언어로, 단일 Call Stack을 가지고 있다. 이 말은, 한 번에 하나의 작업만 처리할 수 있다는 뜻이고, API 요청이나 타이머 작업처럼 시간이 오래 걸리는 함수가 실행되면 후속 작업을 처리할 수 없게 된다. </p>
<p>이 문제를 해결하기 위해서 비동기 콜백과 Event Loop가 등장한다. 자바스크립트가 <code>fetch</code>, <code>setTimeout</code> 등의 작업을 만나면 이 작업이 완료될 때까지 기다리는 대신, 브라우저의 WepAPI에게 맡기고 다음 작업을 진행한다. Web API는 비동기 작업을 처리하고, 작업이 완료되면 이후 실행할 콜백을 작업의 종류에 따라 Task Queue 또는 Microtask Queue에서 대기하도록 한다. Event Loop는 Call Stack과 queue를 감시하다가, Call Stack이 비면 queue 안의 작업을 Call Stack으로 넣어주는 작업을 한다.</p>
<p>비동기 작업이 대기하는 Queue는 크게 다음과 같이 구분할 수 있다.</p>
<ul>
<li>Macrotask Queue: <code>setTimeout</code>, <code>setInterval</code></li>
<li>Microtask Queue: Promise 콜백 (<code>.then</code>, <code>.catch</code>, <code>.finally</code>)</li>
</ul>
<p>Microtask Queue는 Macrotask Queue보다 우선순위를 가지므로, Call Stack이 비면 Microtack Queue 작업이 먼저 실행되고, 그 다음 Macrotack Queue 작업이 실행된다.</p>
<h3 id="execution-context과-lexical-environment">Execution Context과 Lexical Environment</h3>
<p><strong>실행 컨텍스트(Execution Context)</strong>란 자바스크립트 엔진이 코드를 실행할 때 필요한 실행 상태를 관리하기 위한 단위이다.</p>
<p>자바스크립트 코드가 실행되기 시작하면 먼저 전역 실행 컨텍스트(Global Execution Context)가 생성되고, 함수를 호출할 때마다 해당 함수를 위한 함수 실행 컨텍스트(Function Execution Context)가 새롭게 생성된다.</p>
<p>예를 들어 다음 코드가 있다고 해보자.</p>
<pre><code>const globalValue = 10;

function foo() {
  const localValue = 20;
  console.log(globalValue + localValue);
}

foo();</code></pre><p>코드가 처음 실행되면 전역 실행 컨텍스트가 생성된다. 이후 foo()가 호출되면 자바스크립트 엔진은 foo의 코드를 실행하기 위한 새로운 함수 실행 컨텍스트를 생성한다.</p>
<p>이 실행 컨텍스트에서는 현재 실행 중인 코드가 어떤 변수와 함수에 접근할 수 있는지, 외부 스코프는 어디인지, this는 무엇인지 등 코드 실행에 필요한 상태를 관리한다.</p>
<blockquote>
<p>💡 Call Stack과 실행 컨텍스트의 관계
생성된 실행 컨텍스트는 Call Stack을 통해 관리된다. 함수가 호출되면 새로운 실행 컨텍스트가 생성되어 Call Stack에 쌓이고, 함수 실행이 끝나면 해당 실행 컨텍스트가 Stack에서 제거된다. 따라서 Call Stack의 가장 위에는 현재 실행 중인 실행 컨텍스트가 위치한다.
(Call Stack = 실행 컨텍스트 스택)</p>
</blockquote>
<p>그렇다면 <code>localValue</code>와 <code>globalValue</code>는 어디에 저장되고, 그 값이 어디에 있는지 어떻게 알 수 있을까? 실행 컨텍스트 안에서 변수와 함수 같은 식별자를 관리하고, 외부 스코프와의 연결 관계를 나타내는 데 사용되는 개념이 <strong>Lexical Environment</strong>이다.</p>
<blockquote>
<p>💡 함수가 실행되면 함수 실행 컨텍스트가 생성되고, 그 다음에 함수 Lexical Environment를 생성하여 실행 컨텍스트에 바인딩한다. 실행 컨텍스트와 Lexical Environment는 같은 것이 아니다.</p>
</blockquote>
<p>Lexical Environment는 크게 두 가지로 구성된다.</p>
<ul>
<li>Environment Record: 현재 스코프에 선언된 변수와 함수 등의 식별자를 관리한다.</li>
<li>Outer Lexical Environment Reference: 외부 Lexical Environment를 참조한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/650dd42c-fa7c-4ff3-965f-ea2ab8aaa165/image.png" alt=""></p>
<p><code>localValue</code>를 사용할 때는 현재 Lexical Environment의 Environment Record에서 바로 찾을 수 있다. 반면 <code>globalValue</code>는 foo의 Environment Record에 존재하지 않는다. 이 경우 Outer Reference를 따라 외부 Lexical Environment로 이동하여 <code>globalValue</code>를 찾는다. 외부에서도 찾지 못한다면 다시 그 외부 환경으로 이동하며 식별자를 탐색한다.</p>
<pre><code>localValue 탐색

foo Environment
└─ localValue 발견 ✓


globalValue 탐색

foo Environment
└─ 없음
   ↓ Outer
Global Environment
└─ globalValue 발견 ✓</code></pre><p>이렇게 Lexical Environment들이 외부 환경을 참조하며 연결되고, 현재 환경에서 시작해 외부 환경으로 식별자를 탐색할 수 있는 연결 관계를 Scope Chain이라고 한다.</p>
<blockquote>
<p>💡 Lexical은 코드가 작성된 위치를 의미한다. 함수가 어디에서 호출되었는지가 아니라 어디에서 정의되었는지에 따라 외부 Lexical Environment가 결정된다.</p>
</blockquote>
<h3 id="closure">Closure</h3>
<p><strong>클로저(Closure)</strong>란 함수가 자신이 정의된 위치의 Lexical Environment를 기억하여, 외부 함수의 실행이 끝난 이후에도 해당 환경의 변수에 접근할 수 있는 현상을 말한다.</p>
<pre><code>function outer() {
  const value = 10;

  function inner() {
    console.log(value);
  }

  return inner;
}

const closure = outer();
closure(); // 10</code></pre><p><code>inner</code> 함수는 <code>outer</code> 내부에서 정의되었기 때문에 자신의 Outer Lexical Environment로 <code>outer</code>의 Lexical Environment를 참조한다.</p>
<p><code>outer()</code>의 실행이 끝나면 <code>outer</code>의 실행 컨텍스트는 Call Stack에서 제거된다. 하지만 반환된 <code>inner</code> 함수가 여전히 <code>outer</code>의 Lexical Environment를 참조하고 있기 때문에 해당 환경은 사라지지 않고 유지된다.</p>
<p>따라서 나중에 <code>closure()</code>를 호출해도 inner는 자신의 Scope Chain을 따라 <code>outer</code>의 value를 찾아 사용할 수 있다.</p>
<blockquote>
<p>💡 실행 컨텍스트가 사라져도 변수에 접근할 수 있는 이유
함수 실행이 끝나 Call Stack에서 실행 컨텍스트가 제거되었다고 하더라도, 해당 함수의 Lexical Environment가 즉시 소멸하는 것은 아니다. Lexical Environment는 실행 컨텍스트에 의해 참조되기는 하지만, 독립적인 객체이기 때문에 다른 함수가 해당 환경을 계속 참조하고 있다면 사라지지 않고 남아있게 된다. Lexical Environment는 누군가에 의해 참조되지 않을 때 가비지 컬렉터에 의해 소멸한다. </p>
</blockquote>
<h3 id="객체의-참조와-불변성">객체의 참조와 불변성</h3>
<p>JavaScript에서 원시값과 객체는 값을 다루는 방식에 차이가 있다.</p>
<p>원시값은 변수에 값 자체가 저장되는 것처럼 동작하기 때문에 다른 변수에 할당한 뒤 값을 변경해도 서로 영향을 주지 않는다.</p>
<pre><code>let a = 10;
let b = a;

b = 20;

console.log(a); // 10
console.log(b); // 20</code></pre><p>반면 객체를 다른 변수에 할당하면 두 변수가 같은 객체를 참조하게 된다.</p>
<pre><code>const a = { count: 10 };
const b = a;

b.count = 20;

console.log(a.count); // 20
console.log(b.count); // 20

console.log(a === b); // true</code></pre><p>a와 b가 각각 별도의 객체를 가지고 있는 것이 아니라 동일한 객체를 참조하고 있기 때문에, b를 통해 객체를 변경하면 a에서도 변경된 값이 보인다.</p>
<p>반대로 내용이 동일하더라도 각각 새롭게 생성된 객체는 서로 다른 객체다.</p>
<pre><code>const a = { count: 10 };
const b = { count: 10 };

console.log(a === b); // false</code></pre><p>즉 객체를 비교할 때는 내부의 값이 같은지가 아니라 동일한 객체를 참조하고 있는지가 중요하다</p>
<h4 id="불변성immutability">불변성(Immutability)</h4>
<p>불변성이란 기존 값을 직접 변경하지 않고 새로운 값을 만들어 변경을 표현하는 것을 의미한다.</p>
<p>객체는 기본적으로 변경 가능한(mutable) 값이므로 다음과 같이 직접 변경할 수 있다.</p>
<pre><code>const user = {
  name: &#39;Amy&#39;,
  age: 20,
};

user.age = 21;</code></pre><p>불변성을 유지한다면 기존 객체를 수정하는 대신 새로운 객체를 만든다.</p>
<pre><code>const user = {
  name: &#39;Amy&#39;,
  age: 20,
};

const nextUser = {
  ...user,
  age: 21,
};

console.log(user === nextUser); // false</code></pre><p>이렇게 하면 기존 user는 그대로 유지하면서 변경된 값을 가진 nextUser라는 새로운 참조의 객체를 만들 수 있다.</p>
<p>참고로, Spread Syntax로 객체를 복사하면 바깥 객체는 새롭게 생성되지만, 중첩된 객체까지 새롭게 복사되는 것은 아니다 (Shallow Copy). 그래서 중첩된 값을 불변성을 유지하면서 변경하려면, 변경되는 경로의 객체도 새롭게 만들어야 한다.</p>
<pre><code>const nextUser = {
  ...user,
  address: {
    ...user.address,
    city: &#39;Busan&#39;,
  },
};</code></pre><blockquote>
<p>💡 React에서 불변성이 중요한 이유
React는 state가 변경되었는지를 판단할 때 객체 내부의 모든 값을 하나씩 비교하지 않고, 이전 값과 새로운 값의 동일성을 비교하는 방식을 활용한다. 따라서 기존 객체를 직접 변경하는 대신 새로운 객체를 만들어야 변경을 쉽게 감지할 수 있다.</p>
</blockquote>
<h2 id="브라우저-렌더링-과정">브라우저 렌더링 과정</h2>
<h3 id="dom과-cssom">DOM과 CSSOM</h3>
<p>브라우저는 서버로부터 전달받은 HTML과 CSS 파일을 그대로 화면에 그리는 것이 아니라, 브라우저가 이해하고 처리할 수 있는 구조로 변환한다. HTML은 <strong>DOM(Document Object Model)</strong>으로, CSS는 <strong>CSSOM(CSS Object Model)</strong>으로 변환된다.</p>
<p>DOM은 HTML 문서를 브라우저가 다룰 수 있도록 객체 기반의 트리 구조로 표현한 것이다.</p>
<p>예를 들어 다음과 같은 HTML이 있다면,</p>
<pre><code>&lt;html&gt;
  &lt;body&gt;
    &lt;div&gt;
      &lt;h1&gt;Hello&lt;/h1&gt;
      &lt;p&gt;World&lt;/p&gt;
    &lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre><p>브라우저는 HTML을 위에서부터 파싱하면서 각각의 요소를 Node로 만들고, 요소 간의 부모-자식 관계를 표현하는 DOM Tree를 생성한다.</p>
<pre><code>// 실제 DOM 객체를 이해하기 쉽게 단순화한 형태

const body = {
  nodeName: &#39;BODY&#39;,
  children: [
    {
      nodeName: &#39;DIV&#39;,
      children: [
        {
          nodeName: &#39;H1&#39;,
          textContent: &#39;Hello&#39;,
          children: [],
        },
        {
          nodeName: &#39;P&#39;,
          textContent: &#39;World&#39;,
          children: [],
        },
      ],
    },
  ],
};</code></pre><p>DOM이 만들어지면 JavaScript를 통해 이 구조에 접근하거나 변경할 수 있다.</p>
<pre><code>const h1 = document.querySelector(&#39;h1&#39;);

console.log(h1.nodeName);    // &quot;H1&quot;
console.log(h1.textContent); // &quot;Hello&quot;
console.log(h1.parentNode);  // &lt;div&gt;...&lt;/div&gt;</code></pre><p>즉 DOM은 단순히 HTML을 복사한 것이 아니라, HTML 문서의 구조와 내용을 JavaScript를 비롯한 프로그램이 다룰 수 있도록 표현한 객체 모델이다.</p>
<p>CSSOM은 스타일시트와 각각의 CSS 규칙을 객체 형태로 표현한 모델이다. 이를 통해 브라우저는 어떤 스타일 규칙이 존재하는지 파악하고, DOM 요소에 적용될 스타일을 계산할 수 있다.</p>
<p>예를 들어 다음 CSS가 있다면,</p>
<pre><code>body {
  font-size: 16px;
}

h1 {
  color: blue;
}</code></pre><p>브라우저는 CSS를 파싱하여 각 요소에 어떤 스타일 규칙이 적용되는지 계산할 수 있는 객체를 생성한다.</p>
<pre><code>// 실제 CSSOM 객체를 이해하기 쉽게 단순화한 형태

CSSStyleSheet {
  cssRules: [
    CSSStyleRule {
      selectorText: &#39;body&#39;,
      style: {
        fontSize: &#39;16px&#39;
      }
    },

    CSSStyleRule {
      selectorText: &#39;h1&#39;,
      style: {
        color: &#39;blue&#39;
      }
    }
  ]
}</code></pre><p>다만 실제 CSSOM은 CSS 선택자를 단순히 DOM 같은 형태의 트리로 바꾼 것이라고 이해하면 정확하지 않다. CSS에는 상속과 Cascading이 있기 때문에 브라우저는 여러 CSS 규칙을 종합하여 각 요소에 적용될 스타일을 결정한다.</p>
<h3 id="render-tree">Render Tree</h3>
<p>브라우저는 DOM과 CSSOM을 생성한 뒤, 이를 바탕으로 실제 화면을 렌더링하는 데 필요한 요소와 스타일 정보를 가진 <strong>Render Tree</strong>를 구성한다. (이 과정에서 핵심적으로 일어나는 작업이 Style Calculation라서 이 단계를 style 단계로 구분하기도 한다.)</p>
<p>DOM이 문서의 구조를 나타낸다면, Render Tree는 화면에 무엇을 그려야 하는지를 나타내는 구조라고 볼 수 있다.</p>
<pre><code>// HTML
&lt;body&gt;
  &lt;h1&gt;Hello&lt;/h1&gt;
  &lt;p&gt;World&lt;/p&gt;
  &lt;div style=&quot;display: none;&quot;&gt;Hidden&lt;/div&gt;
&lt;/body&gt;</code></pre><p>이 코드를 기반으로 파싱된 DOM에는 HTML에 존재하는 모든 요소가 포함된다. 하지만 <code>div</code>는 <code>display: none</code>이 적용되어 화면에 표시되지 않으므로 Render Tree에서 제외된다. 즉, DOM과 Render Tree는 1:1로 대응하지 않는다.</p>
<h3 id="layout">Layout</h3>
<p>Render Tree가 만들어지면 브라우저는 <strong>각 요소가 화면에서 어디에 위치하고, 어떤 크기를 가져야 하는지 계산</strong>한다. 이 과정을 <strong>Layout</strong>이라고 한다.</p>
<pre><code>&lt;div style=&quot;width: 50%; padding: 20px;&quot;&gt;
  Hello
&lt;/div&gt;</code></pre><p><code>width: 50%</code>만으로는 실제 너비가 몇 px인지 알 수 없다. 부모 요소의 크기나 viewport 크기 등 주변 요소와의 관계를 고려해야 하기 때문이다.</p>
<p>브라우저는 Layout 과정에서 이러한 값들을 계산하여 각 요소의 정확한 크기와 위치를 결정한다.</p>
<pre><code>// Render Tree
div
├─ width: 50%
└─ padding: 20px

// Layout - 화면상의 실제 위치와 크기
div
├─ x: 0px
├─ y: 0px
├─ width: 640px
└─ height: 60px</code></pre><p>Layout은 한 요소만 독립적으로 계산하는 것이 아니라 다른 요소와의 관계를 고려해서 계산한다. 따라서 이미 화면이 렌더링된 이후에도 요소의 크기나 위치에 영향을 주는 스타일이 변경되면 Layout이 다시 필요할 수 있다.</p>
<pre><code>element.style.width = &#39;500px&#39;;</code></pre><p>이처럼 변경으로 인해 요소의 위치와 크기를 다시 계산하는 것을 흔히 <strong>Reflow</strong>라고 부른다.</p>
<blockquote>
<p>💡 Render Tree와 Layout의 차이
Render Tree가 무엇을 그릴 것인지를 결정하기 위한 구조라면, Layout은 그 요소를 어디에, 어떤 크기로 그릴 것인지를 계산하는 과정이다.</p>
</blockquote>
<h3 id="paint">Paint</h3>
<p><strong>Paint</strong>는 Layout에서 계산된 정보를 바탕으로 <strong>색상, 텍스트, 테두리, 그림자 등의 시각적 요소를 실제로 그리는 단계</strong>이다.</p>
<p>예를 들어 다음과 같은 요소가 있다고 해보자.</p>
<pre><code>&lt;div
  style=&quot;
    width: 200px;
    height: 100px;
    background: blue;
    border: 1px solid black;
  &quot;
&gt;
  Hello
&lt;/div&gt;</code></pre><p>Layout 단계에서는 주로 다음과 같은 기하학적 정보가 결정된다.</p>
<pre><code>x: 0px
y: 0px
width: 200px
height: 100px</code></pre><p>이후 Paint 단계에서는 이를 바탕으로 배경, 테두리, 텍스트 등의 시각적 요소를 그린다.</p>
<pre><code>→ 배경을 blue로 그리기
→ 검은색 border 그리기
→ &quot;Hello&quot; 텍스트 그리기</code></pre><p>이미 렌더링된 이후에도 요소의 시각적인 속성이 변경되면 다시 Paint가 필요할 수 있다.</p>
<pre><code>element.style.backgroundColor = &#39;red&#39;;</code></pre><p>이 경우 background-color는 일반적으로 요소의 위치나 크기를 변경하지 않기 때문에 Layout은 다시 하지 않고 Paint부터 다시 수행할 수 있다. 이러한 다시 그리기를 <strong>Repaint</strong>라고 부른다.</p>
<blockquote>
<p>💡 Layout과 Paint의 차이
Layout은 어디에, 어떤 크기로 그릴 것인지를 계산하고, Paint는 어떤 색상과 모양으로 그릴 것인지를 결정한다.</p>
</blockquote>
<h3 id="composite">Composite</h3>
<p>Paint된 결과가 곧바로 최종 화면에 표시되는 것은 아니다. Paint 과정이 끝나면 브라우저는 <strong>그려진 결과를 바탕으로 여러 Layer를 적절한 위치와 순서로 합쳐 최종 화면을 만드는 과정을 수행</strong>한다. 이를 <strong>Composite</strong>이라고 한다.</p>
<p>브라우저는 렌더링 과정에서 필요에 따라 화면의 일부 요소를 별도의 Compositing Layer로 분리할 수 있다. 예를 들어 화면이 다음과 같은 Layer들로 나뉘었다고 생각해보자.</p>
<pre><code>Layer 3    [ Modal ]
              ↓
Layer 2    [ Header ]
              ↓
Layer 1    [ Content ]</code></pre><p>각 Layer의 내용이 준비되면 Composite 단계에서는 이들을 올바른 위치와 순서로 합성하여 사용자가 보는 최종 화면을 만든다.</p>
<pre><code>Layer 3 ──┐
Layer 2 ──┼──→ Composite ──→ 최종 화면
Layer 1 ──┘</code></pre><p>Composite가 중요한 이유는 경우에 따라 Layout과 Paint를 다시 수행하지 않고도 화면을 변경할 수 있기 때문이다. 대표적으로 transform이나 opacity를 이용한 변경은 해당 요소가 별도의 합성 레이어에서 처리되는 등의 조건이 갖춰지면, Layout이나 Paint 없이 Composite 단계에서 처리될 수 있다.</p>
<pre><code>element.style.transform = &#39;translateX(100px)&#39;;</code></pre><p>이러한 변경은 기존에 그려진 Layer를 새로운 위치에 합성하는 방식으로 처리할 수 있기 때문에 애니메이션 성능 최적화에서도 자주 활용된다.</p>
<h3 id="브라우저-렌더링-과정-요약">브라우저 렌더링 과정 요약</h3>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/c7ccf9d8-8301-4b9c-9ce6-64d60ea9506d/image.png" alt=""></p>
<hr>
<p><strong>참고자료</strong></p>
<ul>
<li><a href="https://ccomccomhan.tistory.com/300">[JS] - 자바스크립트 동작 원리 간단하고 쉽게 이해하기(콜스택, 이벤트 루프, 태스크 큐)</a></li>
<li><a href="https://medium.com/@gaurav.pandvia/understanding-javascript-function-executions-tasks-event-loop-call-stack-more-part-1-5683dea1f5ec">Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks &amp; more</a></li>
<li><a href="https://ko.javascript.info/closure">변수의 유효범위와 클로저</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/Performance/Guides/How_browsers_work">웹페이지를 표시한다는 것: 브라우저는 어떻게 동작하는가</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Server Actions와 TanStack Query]]></title>
            <link>https://velog.io/@jess_apr/Server-Actions%EC%99%80-TanStack-Query</link>
            <guid>https://velog.io/@jess_apr/Server-Actions%EC%99%80-TanStack-Query</guid>
            <pubDate>Sun, 12 Jul 2026 12:04:05 GMT</pubDate>
            <description><![CDATA[<h2 id="nextjs에서-server-actions를-사용할-수-있는데-왜-tanstack-query가-필요한가요">“Next.js에서 Server Actions를 사용할 수 있는데 왜 TanStack Query가 필요한가요?”</h2>
<p>현재 진행중인 회사 프로젝트의 초기 설계 단계에서 TanStack Query의 도입을 제안했더니 사수님께서 물어보신 질문이었다. TanStack Query를 사용해 본 경험은 있었지만, 프로젝트에 왜 필요한지 명확하게 설명하려니 선뜻 답이 나오지 않았다.</p>
<p>“사용해 보니 편해서”, “다양한 기능을 제공해서”라는 대답을 할 수는 없어 머뭇거리자, 사수님께서는 Server Actions와 TanStack Query가 담당하는 역할을 비교해 본 뒤 다음 회의에서 다시 이야기해 보자고 하셨다.</p>
<p>Server Actions를 사용하면 서버에서 데이터를 조회하거나 변경할 수 있고, 변경 이후 Next.js의 캐시를 재검증해 화면에 최신 데이터를 반영할 수도 있다. 실제로 단순한 폼 제출이나 CRUD 화면은 Server Components와 Server Actions만으로 충분히 구현할 수 있다.</p>
<p>그렇다면 우리 프로젝트에서도 TanStack Query 없이 Server Actions만 사용하면 되는 것은 아닐까?</p>
<p>이 질문에 답하기 위해 먼저 Server Actions와 TanStack Query의 역할을 비교해 보았다.</p>
<h2 id="server-actions와-tanstack-query의-역할은-다르다">Server Actions와 TanStack Query의 역할은 다르다</h2>
<p>Server Actions와 TanStack Query 둘 다 &quot;mutation 후 최신 데이터 반영&quot;이라는 작업을 실행할 수 있다.</p>
<blockquote>
<p>사용자 액션
→ 서버 데이터 변경
→ 기존 데이터 무효화
→ 최신 UI 표시</p>
</blockquote>
<p>하지만 이는 일부 기능이 겹친 것일 뿐, 실제로는 서로 다른 캐시를 다룬다. 이 둘은 담당 계층과 동작 모델이 근본적으로 다르다. </p>
<p><img src="https://velog.velcdn.com/images/jess_apr/post/ccc96813-569c-42d4-85cb-87e1e263e71f/image.png" alt=""></p>
<p>Server Action은 클라이언트에서 서버 함수를 호출해 서버 측 명령을 수행한다. 반면 TanStack Query는 서버에서 수행한 작업을 클라이언트에서 어떻게 관리하고 화면에 반영할지를 담당한다.</p>
<p>즉, 이 둘은 대체 관계가 아니다. 프로젝트의 성격에 따라 둘 중 하나를 선택할 수도 있고, 이 둘을 함께 사용할 수도 있다.</p>
<h2 id="tanstack-query-도입을-검토한-이유">TanStack Query 도입을 검토한 이유</h2>
<p>그렇다면 다음에 해야 할 것은 우리 프로젝트에 TanStack Query를 도입하는데 드는 비용과 그것으로 얻을 수 있는 이득을 비교하는 것이다.</p>
<p>보통 단순한 CRUD 페이지는 TanStack Query의 도입 비용이 이득보다 클 가능성이 높다.</p>
<ul>
<li>목록 조회가 페이지 진입이나 검색 조건 변경 시에만 발생</li>
<li>등록·수정·삭제 후 화면을 다시 렌더링하면 충분</li>
<li>동일 데이터를 여러 컴포넌트가 복잡하게 공유하지 않음</li>
</ul>
<p>이런 경우는 Server Action 실행 시 변경된 UI와 데이터를 한 번의 서버 왕복으로 반환할 수 있고, <code>revalidatePath</code>, <code>revalidateTag</code>, <code>updateTag</code> 등을 통해 간단하게 관련 데이터를 갱신할 수 있다. 클라이언트 캐시와 서버 캐시를 분리해 관리해야하고, 초기 설정과 query 함수, query key를 정의하는 비용을 굳이 지출할 필요가 없다.</p>
<p>반면 우리 프로젝트에서는 이런 요소들을 고려해야한다.</p>
<h3 id="1-사용자별-데이터가-중심인-어드민-서비스">1. 사용자별 데이터가 중심인 어드민 서비스</h3>
<p>우리 프로젝트는 사용자마다 접근 가능한 상점과 데이터가 다른 어드민 서비스이다. 이러한 데이터를 사용자 세션 동안 반복적으로 조회하고 갱신해야 한다면, 브라우저의 클라이언트 캐시에서 관리하는 방식이 자연스러울 수 있다.</p>
<p>서버 캐시는 여러 사용자의 요청이 동일한 캐시 공간을 조회할 수 있기 때문에, 사용자마다 반환 결과가 다른 데이터를 캐싱할 때는 데이터의 종류뿐 아니라 사용자, 상점, 권한 등 결과에 영향을 주는 범위까지 캐시 키나 캐시 경계에 반영해야 한다. 이 중 하나라도 누락하면 다른 사용자의 요청에 잘못된 캐시가 사용될 수 있으므로 키 설계에 더 많은 주의가 필요하다.</p>
<p>반면 TanStack Query의 캐시는 브라우저의 QueryClient 단위로 먼저 분리된다. 현재 사용자가 이미 캐시의 상위 경계로 격리되어 있기 때문에, query key는 보통 shopId, 리소스, 필터처럼 현재 클라이언트 안에서 조회 결과를 구분하는 값에 집중할 수 있다. 따라서 사용자별로 반환 결과가 달라지는 서비스에서는 서버 공용 캐시보다 캐시 키 설계 부담이 상대적으로 적다.</p>
<h3 id="2-데이터-갱신-범위와-서버-작업-비용">2. 데이터 갱신 범위와 서버 작업 비용</h3>
<p>App Router의 갱신은 전체 페이지를 새로고침하는 방식과 다르다. 서버가 갱신된 Server Component 결과를 RSC payload로 전달하면 클라이언트는 이를 기존 화면에 병합한다. 다만 갱신 과정에서 서버 렌더링, 필요한 데이터의 재조회, RSC payload 생성과 전송 등의 작업이 발생할 수 있다.</p>
<p>예를 들어 목록의 한 행만 변경한 뒤 <code>revalidatePath(&#39;/products&#39;)</code>로 경로를 무효화하면, 다음 갱신 과정에서 해당 경로의 Server Component 결과가 다시 생성될 수 있다. 태그를 사용해 캐시 무효화 범위를 세밀하게 설계할 수 있지만, 클라이언트의 특정 데이터만 직접 변경하는 방식과는 동작 모델이 다르다.</p>
<p>반면 TanStack Query는 <code>setQueryData</code>로 목록 캐시를 직접 수정하거나 <code>invalidateQueries</code>로 특정 query만 다시 조회할 수 있다. 이 경우 별도의 Server Component 갱신 없이 클라이언트의 query cache를 기준으로 화면을 업데이트할 수 있다.</p>
<h3 id="3-loading-fetching-pending-error-상태-관리">3. loading, fetching, pending, error 상태 관리</h3>
<p>Next.js도 <code>useActionState</code>, <code>useFormStatus</code>, <code>useTransition</code> 등을 통해 Server Action의 실행 상태를 관리할 수 있다. 하지만 상태의 범위가 제한적이고, 여러 액션의 상태를 통합된 형태로 관리해 주지는 않는다.</p>
<p>예를 들어 회원 정보 페이지에 정보 저장, 이메일 중복 확인, 인증번호 전송, 인증번호 확인 버튼이 각각 존재한다면 각 액션마다 별도의 <code>useActionState</code>나 <code>useTransition</code>을 선언해야 한다.</p>
<pre><code>const [isCheckingEmail, startCheckEmail] = useTransition();
const [isSendingCode, startSendCode] = useTransition();
const [isVerifyingCode, startVerifyCode] = useTransition();</code></pre><p>또한 성공 여부, 에러, 반환 데이터 등의 상태도 액션별로 직접 설계하고 관리해야 한다.</p>
<p><code>useFormStatus</code>는 가장 가까운 부모 form의 제출 상태를 제공하기 때문에, 하나의 form 안에 여러 액션이 있는 경우 어떤 버튼의 요청이 진행 중인지 추가로 구분해야 한다.</p>
<p>반면 TanStack Query의 <code>useMutation</code>은 각 요청마다 다음과 같이 다양한 상태를 동일한 인터페이스로 제공한다.</p>
<pre><code>const checkEmailMutation = useMutation({ mutationFn: checkEmail });

checkEmailMutation.isPending;
checkEmailMutation.isError;
checkEmailMutation.error;
checkEmailMutation.isSuccess;
checkEmailMutation.data;</code></pre><p>우리 프로젝트처럼 한 화면에서 여러 조회와 mutation이 동시에 발생하고, 각 버튼의 로딩·비활성화·에러 상태를 개별적으로 보여줘야 하는 경우에는 이러한 상태 관리 방식의 차이가 반복적인 구현 비용으로 이어질 수 있다.</p>
<h3 id="4-optimistic-update">4. Optimistic update</h3>
<p>Next.js에서 <code>useOptimistic()</code>을 사용하면 optimistic UI를 구현할 수 있다. 하지만 이건 해당 데이터를 소유한 컴포넌트 트리의 optimistic 표현이다. 즉, 보여지는 화면만 수정할 뿐 애플리케이션 전체의 서버 데이터 캐시를 자동으로 수정하지는 않는다. 따라서 같은 데이터를 사용하는 다른 화면이나 컴포넌트에는 변경 내용이 자동으로 반영되지 않는다.</p>
<p>반면 TanStack Query에서는 <code>setQueryData</code> 등을 통해 query cache를 직접 변경할 수 있다. 같은 QueryClient에서 동일한 query key를 구독하는 컴포넌트들은 변경된 캐시를 함께 바라보게 된다.</p>
<p>특정 데이터가 한 화면에서만 사용된다면 큰 문제가 없지만, 여러 화면이나 컴포넌트에서 공유되는 데이터를 optimistic update해야 한다면 TanStack Query의 캐시 기반 방식이 데이터 일관성을 관리하기에 더 적합할 수 있다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>정리해보면, Server Actions는 클라이언트에서 서버의 작업을 호출하고 데이터 변경 이후 서버 캐시를 갱신하는 역할을 담당한다. TanStack Query는 브라우저에서 서버 상태를 캐싱하고, 조회·갱신·무효화·에러와 로딩 상태 등의 생명주기를 관리한다.</p>
<p>정리한 내용을 다음 회의에서 공유했고, 팀에서도 필요성에 공감해 프로젝트에 TanStack Query를 도입하게 되었다. 모든 화면에 일괄적으로 적용하기보다는, 클라이언트에서 서버 상태를 반복적으로 조회하거나 여러 mutation의 상태를 관리해야 하는 화면을 중심으로 우선 적용하며 진행했다.</p>
<p>이 내용은 올해 초 프로젝트를 설계하면서 찾아보고 노션에 간단히 정리해 두었던 기록에서 시작했다. 최근 Next.js 캐싱에 관한 블로그 글을 읽다가 당시의 고민이 떠올라 다시 정리하게 되었다. 글을 작성하면서 관련 문서를 다시 확인하고 개념을 구체화하는 과정도 도움이 되었지만, 프로젝트 초기에 팀원들과 함께 공부하며 설계 방향을 고민했던 시간이 떠올라 더욱 즐거웠다.</p>
<p>사수님은 이후에도 새로운 컨벤션이나 라이브러리 도입을 제안할 때마다 이런저런 질문을 던져주신다. 그 질문을 통해 익숙하게 사용해 온 기술도 프로젝트에 어떤 가치를 주는지 다시 생각하게 되었다. 덕분에 지금은 새로운 기술을 제안하기 전에 프로젝트의 요구사항과 도입 비용을 먼저 살펴보고, 선택의 근거를 설명할 수 있도록 공부하려고 노력하고 있다. 😚</p>
<hr>
<p><strong>참고 자료</strong></p>
<ul>
<li><a href="https://nextjs.org/docs/app/guides/server-actions">Server Actions and Mutations</a></li>
<li><a href="https://nextjs.org/docs/app/getting-started/revalidating">Revalidating</a></li>
<li><a href="https://tanstack.com/query/latest/docs/framework/react/guides/mutations">Mutations</a></li>
<li><a href="https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation">Query Invalidation</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[미래를 예측하는 대신, 오늘의 개발자로 살기로 했다.]]></title>
            <link>https://velog.io/@jess_apr/%EB%AF%B8%EB%9E%98%EB%A5%BC-%EC%98%88%EC%B8%A1%ED%95%98%EB%8A%94-%EB%8C%80%EC%8B%A0-%EC%98%A4%EB%8A%98%EC%9D%98-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C-%EC%82%B4%EA%B8%B0%EB%A1%9C-%ED%96%88%EB%8B%A4</link>
            <guid>https://velog.io/@jess_apr/%EB%AF%B8%EB%9E%98%EB%A5%BC-%EC%98%88%EC%B8%A1%ED%95%98%EB%8A%94-%EB%8C%80%EC%8B%A0-%EC%98%A4%EB%8A%98%EC%9D%98-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C-%EC%82%B4%EA%B8%B0%EB%A1%9C-%ED%96%88%EB%8B%A4</guid>
            <pubDate>Mon, 22 Jun 2026 13:45:22 GMT</pubDate>
            <description><![CDATA[<h2 id="나에게-찾아온-무기력">나에게 찾아온 무기력</h2>
<p>최근 두어달간 무기력한 시간을 보냈다. 취미생활과 공부를 즐겼고 주말에도 사람들과 함께 어울려 놀기를 좋아했던 나는, 어느 순간 퇴근 후 게임조차 하기 싫어졌다. 유튜브에서 아무 영상이나 틀어놓은 채 누워 있기만 했고, 사람을 만나는 일이 버겁게 느껴져 약속을 피하기도 했다.</p>
<p>그냥 회사 다니면 다 그렇지, 라는 말로 아무렇지 않게 넘겼는데 문득 이상하다는 생각이 들었다. 회사에 다니면 다 이렇게 텅 빈 하루를 보내나? 적어도 작년의 나는 그렇지 않았다. 매일 밤 오늘의 내가 무엇을 했는지 의문을 품은 채 잠드는 일이 당연한 것은 아니라는 생각이 들었다.</p>
<p>언제부터 이렇게 무기력해졌는지 돌아보다가, 그 감정의 한편에 AI의 발전에 대한 불안이 자리하고 있다는 것을 알게 됐다. 좋은 개발자가 되고 싶어서 강의를 듣고 책을 읽었지만, 내가 시간을 들여 익힌 지식들은 이제 AI에게 한 번 질문하는 것만으로도 관련 자료가 깔끔하게 정리됐다. 나는 아무리 열심히 공부해도 거대한 지식의 일부만 이해할 수 있을 텐데, AI는 훨씬 넓은 범위의 정보를 빠르게 다루는 것처럼 보였다. 주변에서는 AI가 머지않아 개발자를 대체할테니 다른 길을 찾아야 한다는 말도 들려왔다.</p>
<p>그동안 나는 공부하면 성장할 수 있고, 성장하면 더 나은 개발자가 될 수 있다고 믿었다. 그런데 언젠가는 대체될지도 모른다는 생각이 드니, 지금 들이는 노력이 의미 없는 일처럼 느껴졌었던 것 같다. 미래에 대한 불안을 계속 들여다보는 동안, 나는 정작 오늘 해야 할 일과 내가 좋아했던 것들에서 조금씩 멀어지고 있었다.</p>
<h2 id="편리함-속에서-놓친-것">편리함 속에서 놓친 것</h2>
<p>AI를 사용하는 일은 분명 편리했다. 질문하면 짧은 시간 안에 코드와 설명을 받을 수 있었고, 막히는 부분도 이전보다 빠르게 넘어갈 수 있었다. 생산성도 좋아졌다. 화면을 구현하는 속도는 빨라졌고, 혼자서 오래 고민하는 시간도 줄었다.</p>
<p>그런데 하루가 끝날 무렵이면 묘한 공허함이 남았다. 분명 코드를 작성했고 문제도 해결했는데, 정작 나는 오늘 무엇을 했는지 명확하게 설명하기 어려웠다. 일하는 동안에는 정신없이 흘러가다가, 잠들기 직전에야 뒤늦게 정신이 돌아오는 듯한 느낌이 들기도 했다.</p>
<p>업무 결과에는 문제가 없었다. 하지만 그 결과에 도달하는 과정에서 내가 직접 고민하고 판단한 부분이 얼마나 되는지는 잘 모르겠다는 생각이 들었다. 처음에는 이렇게 일하다 보면 언젠가 대체될 수도 있다는 사실이 두려웠다. 그러나 곰곰이 생각해보니 더 걱정스러운 것은 따로 있었다. 편리함에 익숙해지는 동안 스스로 질문하고 판단하는 힘까지 약해질 수 있다는 점이었다.</p>
<p>생각이 이렇게까지 닿자, 문제는 AI가 아니었다는 것을 깨달았다. 무엇을 맡기고 무엇을 직접 고민할지 정하지도 않고, 흐름에 따라 AI를 사용하고 있던 내 태도가 영향을 줬던 것 같다. 나름대로 AI가 제시한 답을 이해하고 검토하려고 노력하긴 했으나, 어느 순간 막힌 부분을 빠르게 넘기는 데만 집중하면서 나도 모르게 사고의 과정까지 함께 넘기고 있었다.</p>
<p>내가 AI의 발전 앞에서 무력감을 느꼈던 것은, 어쩌면 개발이 내게 즐겁고 의미 있는 일이었기 때문인지도 모른다. 나는 단순히 결과물을 빠르게 만드는 것뿐 아니라, 문제를 이해하고 해결 방법을 찾아가는 과정에서도 즐거움을 느끼고 있었다.</p>
<p>미래에 개발자가 대체될지 아닐지는 지금의 내가 알 수는 없더라도, AI를 사용할 때 판단의 주도권은 놓치지 않고 싶다. 그래서 무엇을 왜 사용하는지 이해하고, 내가 내린 선택을 스스로 설명할 수 있도록 지금 할 수 있는 공부를 계속하기로 했다.</p>
<h2 id="언젠가-ai가-다-해줄-수-있을지도-모르지만">언젠가 AI가 다 해줄 수 있을지도 모르지만</h2>
<p>그렇다면 지금 내가 해야 할 공부는 무엇일까. AI 시대에 개발자로 살아남는 방법을 이야기하는 영상과 글은 많다. 하지만 누군가가 정해준 답을 따르기보다, 내가 실제로 AI를 사용하면서 불편함을 느꼈던 지점에서 출발하는 편이 더 현실적이라고 생각했다.</p>
<p>AI에게 코드를 작성해달라고 하면 그럴듯한 답은 빠르게 나온다. 문제는 그 코드가 지금 내가 처한 상황에 적합한지 판단하는 일이다. 동작하는 코드라고 해서 반드시 좋은 코드인 것은 아니고, 일반적으로 좋은 방법이라고 해서 현재의 프로젝트에도 가장 적합한 것은 아니다. 성능을 우선할 수도 있고, 유지보수성과 확장성을 더 중요하게 볼 수도 있다. 팀의 역량이나 일정, 기존 코드와의 일관성에 따라 현실적으로 선택할 수 있는 방법도 달라진다. 결국 여러 선택지 가운데 지금의 상황에 가장 적합한 방법을 고르고, 그 이유를 설명할 수 있어야 한다. 나는 아직 스스로 그 판단을 내릴 수 있는 기준이 충분하지 않다고 느꼈다.</p>
<p>그래서 다시 CS 지식을 공부해보기로 했다. CS 지식은 특정 프레임워크나 라이브러리처럼 빠르게 바뀌는 사용법보다는, 비교적 오래 유지되는 원리와 기준에 가깝다. 운영체제, 네트워크, 데이터베이스, 자료구조와 같은 기본 지식을 쌓아두면 AI가 작성한 코드가 어떻게 동작하는지 이해하고, 어떤 문제가 생길 수 있는지 판단하는 데 도움이 될 것이라고 생각했다.</p>
<p>물론 판단의 기준이 CS 지식만으로 완성되는 것은 아니다. 도메인에 대한 이해와 실제 운영 경험, 팀이 공유하는 맥락도 함께 쌓아가야 한다. 실제 개발에는 수많은 현실 제약이 존재한다. 이상적인 기준과 이러한 제약을 함께 이해한 상태여야 현실의 조건과 균형을 맞추며 가장 적절한 선택을 찾아갈 수 있을 것이다.</p>
<p>사실 이 역할은 AI가 등장하기 전에도 크게 다르지 않았다. 누군가는 언젠가 AI가 프로젝트의 모든 상황을 파악하고 가장 적합한 답까지 제시해 줄텐데, 굳이 그런 공부를 해야 하느냐고 물을 수도 있다.</p>
<p>그 미래가 실제로 올지는 나는 알지 못한다. 다만 지금의 AI가 프로젝트를 둘러싼 모든 맥락을 스스로 온전히 이해한다고 생각하지는 않는다. 그렇다고 이러한 맥락들을 AI에게 하나도 빠짐없이 설명하는것 또한 정말 쉽지 않다.</p>
<p>그렇다면 지금 내가 할 수 있는 일은 분명하다. AI가 놓칠 수 있는 맥락을 살피고, 제시된 답이 현재의 상황에 적합한지 판단하며, 서비스에 빈틈이 생기지 않도록 책임지는 것이다. AI보다 더 많은 지식을 외우는 사람이 되기보다는, 지식을 바탕으로 더 나은 질문을 하고 선택의 이유를 설명할 수 있는 개발자가 되고 싶다.</p>
<h2 id="빠르게-변하는-세상에서-내가-가고싶은-길을-가는-것">빠르게 변하는 세상에서 내가 가고싶은 길을 가는 것</h2>
<p>내가 대학교에 입학할 때는 전/화/기(전기전자, 화학공학, 기계공학)가 유행했는데 어느순간 그 관심은 컴퓨터공학으로 옮겨갔고, 다시 의대와 반도체 관련 학과가 주목받기 시작했다. 이런 변화를 지켜보며 한 가지를 알게 됐다. 지금 유망하다고 평가받는 분야가 몇 년 뒤에도 같은 위치에 있으리라는 보장은 없다는 것이다.</p>
<p>그렇다면 빠르게 변하는 세상에서 미래를 정확히 예측하려 애쓰는 일이 과연 얼마나 의미가 있을까. 미래를 대비하는 일은 필요하지만, 정답을 맞히려는 마음으로 미래만 바라볼수록 오히려 불안은 커졌다. 아직 일어나지 않은 가능성을 끊임없이 계산하는 동안, 정작 나는 아무것도 하지 못하고 있었다.</p>
<p>그래서 미래를 맞히는 대신 현재에 집중하기로 했다. 지금 내가 할 수 있는 일, 지금 내게 필요한 것, 지금 해야 하는 공부를 하나씩 해나가기로 했다. 상황이 달라지면 그때그때 필요한 것을 다시 배우고 방향을 조정하면 된다. 미래를 완벽하게 내다보지 못하더라도, 변화에 맞춰 계속 판단하고 움직일 수 있다면 어떻게든 살아갈 수 있지 않을까 생각한다.</p>
<p>주변 개발자분들, 회사 동료분들이 목공, 도배 등 다른 직종으로 전직한다고 하셨을때 많이 불안하고 흔들렸다. 하지만 내가 그 직종으로 옮겨서 어느정도 익숙해졌을쯤에도 그 직업이 지금과 같은 상황이라는걸 100% 보장할 수 없으니 결국 어느 길을 선택하더라도 불확실성은 남는다. (물론 내 생각과는 별개로 전직을 선택하신 분들은 대단하다는 생각이 들고, 그 선택을 존중한다.) 그렇다면 불확실한 미래에 떠밀려 방향을 정하기보다, 지금의 내가 중요하게 생각하는 것을 기준으로 선택하고 싶다.</p>
<p>이 생각이 정답이라고 말할 자신은 없다. 몇 년 뒤의 나는 지금과 전혀 다른 생각을 하고 있을지도 모른다. 그래도 지금의 나는 불확실한 미래를 맞히는 데 힘을 쏟기보다, 오늘 이해할 수 있는 것을 하나씩 늘려가고 싶다.</p>
<p>AI가 어디까지 발전할지, 개발자의 일이 어떻게 달라질지는 알 수 없다. 다만 그 변화에 끌려가며 불안해하기보다, AI를 사용하면서도 내가 해야 할 공부를 하고 스스로 판단하는 힘을 기르고 싶다. 미래를 예측하느라 오늘을 놓치지 말자. 지금 내가 하고 싶은 일과 해야 하는 일에 충실하자.</p>
<p>그렇게 생각하기 시작하면서, 나는 무기력에서 벗어나고 있다.</p>
]]></description>
        </item>
        <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>
    </channel>
</rss>