<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sooknow</title>
        <link>https://velog.io/</link>
        <description>keep on pushing</description>
        <lastBuildDate>Tue, 24 Mar 2026 13:00:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sooknow</title>
            <url>https://velog.velcdn.com/images/fromjs_toyou/profile/20140ed7-4cea-41cb-9718-af7a76b7a113/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sooknow. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/fromjs_toyou" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Three.js / 장면과 구조]]></title>
            <link>https://velog.io/@fromjs_toyou/Three.js-%EC%9E%A5%EB%A9%B4%EA%B3%BC-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@fromjs_toyou/Three.js-%EC%9E%A5%EB%A9%B4%EA%B3%BC-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Tue, 24 Mar 2026 13:00:42 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 Three.js에서 화면을 구성하기 위한 기본요소인 Scene, Camera, Renderer에 대해서 알아봅니다.</p>
<h3 id="scene">Scene</h3>
<p>Three.js의 가장 기본이 되는 객체로, 모든 3D 개체가 배치되는 공간입니다. 화면에 보이는 모든 요소는 Scene에 추가되어야 합니다.</p>
<pre><code class="language-js">const scene = new THREE.scene();
scene.add(element);</code></pre>
<h3 id="camera">Camera</h3>
<p>Scene을 보는 시점을 정의합니다. Three.js에서는 여러 카메라를 제공하는데, 대표적으로 PerspectiveCamera와 OrthographicCamera를 제공합니다.</p>
<h4 id="perspectivecamera">PerspectiveCamera</h4>
<ul>
<li><p>원근감을 적용해서 객체를 투영하는 카메라로, 3D 공간감을 표현합니다.</p>
<pre><code class="language-js">const camera = new Three.PerspectiveCamera(fov, aspect, near, far);</code></pre>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/15a9ce44-8cdf-48e9-84fb-f7c468bdcb03/image.PNG" alt=""></p>
</li>
<li><p><code>fov</code> : 시야각. 커질수록 화면에 많은 영역을 출력합니다.</p>
<ul>
<li>기본값은 50으로 사람의 시야와 유사한 45~75사이의 값을 사용합니다.</li>
</ul>
</li>
<li><p><code>aspect</code> : 카메라의 종횡비. 가로와 세로의 비율</p>
<ul>
<li>window.innerWidth / window.innerHeight를 사용합니다.</li>
</ul>
</li>
<li><p><code>near</code>,<code>far</code> : 카메라로 볼 수 있는 최소, 최대의 거리. 범위밖은 렌더링되지 않습니다.</p>
</li>
</ul>
<h4 id="orthographiccamera">OrthographicCamera</h4>
<ul>
<li>perspectiveCamera와 반대로 원근감없이 평면적인 투영을 적용하는 카메라 입니다.</li>
</ul>
<pre><code class="language-js">const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);</code></pre>
<ul>
<li><strong>left</strong>, <strong>right</strong>, <strong>top</strong>, <strong>bottom</strong>: 카메라가 볼 수 있는 영역의 좌표</li>
<li><strong>near</strong>, <strong>far</strong>: 카메라로 볼 수 있는 최소, 최대 거리</li>
</ul>
<h3 id="renderer">Renderer</h3>
<p>Scene과 Camera를 연결해서 실제 화면에 보여지는 이미지를 생성합니다. canvas를 사용하여 HTML에 추가하고 reneder 메서드를 이요해서 scene과 camera를 연결하여 3D 그래픽을 출력합니다.</p>
<pre><code class="language-js">const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 화면 렌더링
renderer.render(scene, camera);</code></pre>
<p>특정 캔버스 요소에 화면을 렌더링 하기 위해서 렌더러의 속성값으로 해당 캔버스 요소를 전달해야 합니다.</p>
<ul>
<li>canvas가 아닌 요소에는 화면이 렌더링되지 않습니다.</li>
</ul>
<pre><code class="language-jsx">const $canvas = document.getElementById(&#39;canvas&#39;)
const renderer = new THREE.WebGLRenderer({ **canvas: $canvas** })</code></pre>
<p>캔버스의 스타일 속성을 주어 화면의 크기를 조정할 수 있습니다. 화면의 깨짐 현상을 방지하고 비율을 유지하기 위하여 카메라와 렌더러의 속성값을 캔버스의 크기에 맞추어 변경해주어야 합니다.</p>
<pre><code class="language-jsx">const camera = new THREE.PerspectiveCamera(50, $canvas.clientWidth / $canvas.clientHeight, 0.1, 1000);

renderer.setSize($canvas.clientWidth, $canvas.clientHeight);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ingest pipeline 용례]]></title>
            <link>https://velog.io/@fromjs_toyou/Ingest-pipeline-%EC%9A%A9%EB%A1%80</link>
            <guid>https://velog.io/@fromjs_toyou/Ingest-pipeline-%EC%9A%A9%EB%A1%80</guid>
            <pubDate>Fri, 20 Feb 2026 08:43:16 GMT</pubDate>
            <description><![CDATA[<h1 id="ingest-pipeline-용례">ingest pipeline 용례</h1>
<h2 id="1-하나의-인덱스에서-필드-조작">1. 하나의 인덱스에서 필드 조작</h2>
<p>기본적인 조작 순서는 아래 순서와 같다.</p>
<ul>
<li>만약 새로운 이름의 인덱스를 만드는 것이라면 2과정을 생략한다.</li>
<li>하나의 필드를 쪼개기, 여러개의 필드 붙이기는 3번 과정만 다르고 나머지과정은 동일하다.</li>
</ul>
<ol>
<li>원본 인덱스(A)로부터 backup 인덱스를 만든다.</li>
<li>A 인덱스 삭제</li>
<li>ingest 파이프라인을 생성한다.
3-1. 파이프라인이 잘 동작하는지 시뮬레이션</li>
<li>backup인덱스 -&gt;  A 인덱스로 리인덱싱(dest에 pipeline 지정)한다.</li>
</ol>
<h3 id="하나의-필드---여러개의-필드로-쪼개기">하나의 필드 -&gt; 여러개의 필드로 쪼개기</h3>
<ol>
<li>원본인덱스로부터 backup 인덱스를 만든다.<pre><code class="language-json">POST _reindex
{
 &quot;source&quot;: {
     &quot;index&quot;: &quot;A&quot;
 },
 &quot;dest&quot;: {
     &quot;index&quot;: &quot;A-backup&quot;
 }
}</code></pre>
</li>
<li>A 삭제<pre><code class="language-json">DELETE A</code></pre>
</li>
<li>==🔴ingest pipeline을 생성한다.==</li>
</ol>
<ul>
<li><code>split</code> 메서드를 사용해서 구분자 기준으로 나눈다. =&gt; 배열</li>
<li><code>painless script</code>로 나누어진 결과배열의 값을 새로운 필드에 담는다.</li>
<li>원본 필드를 삭제한다.
아래 예시는 <code>Game</code> 필드는 “2025 Summer”로 값이 들어있는데 이걸 <code>year</code>, <code>season</code>으로 나눈다.<pre><code class="language-json">PUT _ingest/pipeline/split-field
{
  &quot;description&quot;: &quot;split one field into two&quot;,
  &quot;processors&quot;: [
    {
      &quot;split&quot;: {
          &quot;field&quot;: &quot;Games&quot;,
          &quot;separator&quot;: &quot; &quot;
      }
    },
    {
      &quot;script&quot;: {
          &quot;lang&quot;: &quot;painless&quot;,
          &quot;source&quot;: &quot;&quot;&quot;
              ctx.year = ctx.Games[0];
              ctx.season = ctx.Games[1];
              &quot;&quot;&quot;
      }
    },
    {
      &quot;remove&quot;: {
          &quot;field&quot;: &quot;Games&quot;,
          &quot;ignore_missing&quot;: false
      }
    }
  ]
}
</code></pre>
</li>
</ul>
<pre><code>
3-1. 파이프라인 시뮬레이션
리인덱싱으로 강을 건너기 전에, 연습데이터를 넣어서 파이프라인이 잘 만들어 졌는지 확인한다.
```json
POST _ingest/pipeline/파이프라인이름/_simulate
{
    &quot;docs&quot;: [
        {
            &quot;_source&quot;: {
                &quot;Games&quot;: &quot;1998 Summer&quot;
            }
        }
    ]
}</code></pre><ol start="4">
<li>리인덱싱
dest에 pipeline을 지정한다.<pre><code class="language-json">POST _reindex
{
 &quot;source&quot;: {
     &quot;index&quot;:&quot;olympic-events&quot;
 },
 &quot;dest&quot;: {
     &quot;index&quot;: &quot;splitted-olympic&quot;,
     &quot;pipeline&quot;: &quot;split-field&quot; // 파이프라인 이름
 }
}</code></pre>
</li>
</ol>
<h3 id="여러개의-필드를-하나로-합치기">여러개의 필드를 하나로 합치기</h3>
<p>나머지 과정은 동일하므로 ingest pipeline 구현부만 살펴보자.</p>
<ul>
<li><code>set</code>  프로세서  : 두 필드를 합쳐 ==🟡새로운 필드를 추가==한다.</li>
<li><code>remove</code> : 기존의 필드는 이제 제거한다. <pre><code class="language-json">PUT _ingest/pipeline/reunion-pipeline
{
  &quot;description&quot;: &quot;reunion year and season into games&quot;,
  &quot;processors&quot;: [
    {
      &quot;set&quot;: {
          &quot;field&quot;: &quot;games&quot;,
          &quot;value&quot;: &quot;{{year}}&amp;{{season}}&quot;
      }
    },
    {
      &quot;remove&quot;: {
          &quot;field&quot;: [&quot;year&quot;, &quot;season&quot;],
          &quot;ignore_missing&quot;: true
      }
    }
  ]
}
</code></pre>
</li>
</ul>
<pre><code>
---

## 2. 두개의 인덱스를 합쳐 필드 늘리기

📌 policy는 B(2번 인덱스)의 입장에서 “어떤 데이터를 나누어 줄지”를 결정하고, pipeline은 A(1번 인덱스)입장에서 에게 새정보를 어떤 형태로 붙일지를 결정한다.
📌 policy는 반드시 Excute 해야한다!

1. policy 정의하기
2. policy 실행하기
3. pipeline 정의하기
4. 인덱스 생성하

1번 Index

| A   | B   | C   |
|-----|-----|-----|


2번 index

| A   | D   |
|-----|-----|


- 여기서 교집합 필드는 A

| A   | B   | C   | D   |
|-----|-----|-----|-----|
|     |     |     |     |

1-1.policy 정의하기
```json
PUT _enrich/policy/정책
{
  &quot;match&quot;: {
    &quot;indices&quot;: &quot;인덱스B&quot;,
    &quot;match_field&quot;: &quot;교집합 필드&quot;, // 새로운 문서에서 읽을 값
    &quot;enrich_fields&quot;:[&quot;채울 필드&quot;]
  }
}</code></pre><p>1-2.policy 실행</p>
<pre><code class="language-json">POST _enrich/policy/정책/_execute</code></pre>
<p>2-1. 파이프라인 정의</p>
<pre><code class="language-json">PUT /_ingest/pipeline/파이프라인_이름
{
  &quot;processors&quot;: [
    {
      &quot;enrich&quot;: {
        &quot;policy_name&quot;: &quot;정책&quot;,
        &quot;field&quot;: &quot;교집합 필드&quot;, // 기존 문서에서 읽을 값
        &quot;target_field&quot;: &quot;새로운 필드&quot; // 다리 필드와 채울 필드가 이 필드의 하위에 생성됩니다.
      }
    }
  ]
}</code></pre>
<p>3.최종 인덱스 생성</p>
<pre><code class="language-json">POST _reindex
{
  &quot;source&quot;: {
    &quot;index&quot;: &quot;A인덱스&quot;
  },
  &quot;dest&quot;: {
    &quot;index&quot;: &quot;C 인덱스&quot;,
    &quot;pipeline&quot;: &quot;enrich-pipeline&quot;
  }
}

# 먄약 기존의 인덱스(A) 자체를 확장해야 한다면 
POST 인덱스A/_update_by_query?pipeline=enrich-pipeline&amp;wait_for_completion=false

</code></pre>
<h3 id="flatten-field">flatten field</h3>
<p><code>enrich</code> 프로세서는 기본적으로 ==중첩 구조==로 합친다. 그래서 추가되는 필드는 루트에 생성되는 것이 아니라 교집합 필드의 하위로 생성되게 된다. 다른 말로 하면, enrich 프로세서는 target_field로 지정된 필드 ==안에== 조회된 데이터를 집어넣는다.</p>
<p> 따라서 문제에 따라 ==🟡모든 필드를 최상위 루트로 끌어올리는 평탄화 작업==이 필요하다.
평탄화를 위해서는<code>script</code>프로세서를 추가하여 하위 필드를 꺼내고 불필요해진 target_field를 삭제해야 한다.</p>
<p>1번 인덱스(A, B, C)에 2번 인덱스(A, D, E)의 데이터를 합치는 상황을 예를 들어 보자.</p>
<p>3번째 pipeline을 만드는 과정이 다음과 같이 변경된다.</p>
<ol>
<li>enrich 메서드를 사용하되, target_field를 temp로 지정한다.<pre><code>temp
ㄴ D
ㄴ E</code></pre></li>
<li>painless script를 사용해서 하위필드를 루트 필드로 만든다.</li>
<li>임시 temp 필드를 삭제한다.<pre><code class="language-json">PUT /_ingest/pipeline/flatten-enrich-pipeline
{
&quot;processors&quot;: [
 {
   &quot;enrich&quot;: {
     &quot;policy_name&quot;: &quot;your_policy_name&quot;,
     &quot;field&quot;: &quot;A&quot;,
     &quot;target_field&quot;: &quot;tmp_enrich&quot; 
   }
 },
 {
   &quot;script&quot;: {
     &quot;description&quot;: &quot;하위 필드를 최상위로 복사&quot;,
     &quot;lang&quot;: &quot;painless&quot;,
     &quot;source&quot;: &quot;&quot;&quot;
       if (ctx.tmp_enrich != null) {
         ctx.D = ctx.tmp_enrich.D;
         ctx.E = ctx.tmp_enrich.E;
       }
     &quot;&quot;&quot;
   }
 },
 {
   &quot;remove&quot;: {
     &quot;description&quot;: &quot;임시 타겟 필드 삭제&quot;,
     &quot;field&quot;: &quot;tmp_enrich&quot;,
     &quot;ignore_missing&quot;: true
   }
 }
]
}</code></pre>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Aggregation Sorting]]></title>
            <link>https://velog.io/@fromjs_toyou/Aggregation-Sorting</link>
            <guid>https://velog.io/@fromjs_toyou/Aggregation-Sorting</guid>
            <pubDate>Thu, 19 Feb 2026 07:13:57 GMT</pubDate>
            <description><![CDATA[<p>order 객체 내부에 <code>“무엇을 기준으로”:”어떻게 정렬할 것인지”</code>를 작성한다.</p>
<p><strong><code>무엇을 기준으로</code></strong></p>
<ul>
<li><p>내장된 기준 (meta data)</p>
<ul>
<li><p><code>_count</code>  : 각 버킷에 담긴 문서의 갯수</p>
</li>
<li><p><code>_key</code> : 각 버킷의 이름(알파벳, 숫자 크기 순, 날짜순)</p>
</li>
<li><p>→ 추가적인 연산이 없어서 성능이 매우 빠르다.</p>
<pre><code class="language-json">  GET web_traffic/_search
  {
    &quot;size&quot;: 0,
    &quot;aggs&quot;: {
      &quot;no_status_code&quot;: {
        &quot;terms&quot;: {
          &quot;field&quot;: &quot;http.response.status_code&quot;,
          &quot;order&quot;: {
            &quot;_key&quot;: &quot;asc&quot; // 상태코드 숫자 값을 기준으로 오름차순 정렬
          }
        }
      }
    }
  }</code></pre>
</li>
</ul>
</li>
<li><p>하위 집계의 결과값(sub-aggregation)</p>
<ul>
<li><p>먼저 하위집계들의 값을 계산한뒤, 그 결과수치를 가지고 부모 버킷들의 순서를 정한다.</p>
<pre><code class="language-json">&quot;aggs&quot;: {
&quot;group_by_category&quot;: {
  &quot;terms&quot;: {
    &quot;field&quot;: &quot;category.keyword&quot;,
    &quot;order&quot;: { &quot;avg_price&quot;: &quot;desc&quot; } // 하위 집계인 avg_price 결과로 정렬
  },
  &quot;aggs&quot;: {
    &quot;avg_price&quot;: { &quot;avg&quot;: { &quot;field&quot;: &quot;price&quot; } }
  }
}
}</code></pre>
</li>
</ul>
</li>
</ul>
<p><code>📌주의할 점</code> </p>
<ul>
<li>만약 percentiles, stats 처럼 결과값이 여러개인 집계를 정렬의 기준으로 사용하게 될 경우, 이름 뒤에 구체적인 지표를 적어줘야 함.</li>
</ul>
<pre><code>GET web_traffic/_search
{
  &quot;size&quot;:  0,
  &quot;aggs&quot;: {
    &quot;group_by_res_code&quot;: {
      &quot;terms&quot;: {
        &quot;field&quot;: &quot;http.response.status_code&quot;,
        &quot;order&quot;: {
          &quot;each_runtime.50&quot;: &quot;asc&quot;
        }
      },
      &quot;aggs&quot;: {
          &quot;each_runtime&quot;: {
            &quot;percentiles&quot;: {
              &quot;field&quot;: &quot;runtime_ms&quot;,
              &quot;percents&quot;: [
                50
              ]
            }
          }
        }
    }
  }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Virtualized List]]></title>
            <link>https://velog.io/@fromjs_toyou/Virtualized-List</link>
            <guid>https://velog.io/@fromjs_toyou/Virtualized-List</guid>
            <pubDate>Sun, 01 Feb 2026 06:39:07 GMT</pubDate>
            <description><![CDATA[<p>가상 리스트, 가상 스크롤, 목록 가상화 등 다양한 이름으로 불리는 최적화 방식에 대해서 알아보자.</p>
<p>이  방식은 동적인 목록을 렌더링할때, 특히 대량의 리스트를 랜더링할 때, 전체 목록을 렌더링하지 않고 화면에 “보이는” 컨텐츠 들만 랜더링하는 방식이다.</p>
<p>목록의 요소들을 가상화하기 위해서는 윈도우를 고정시키고 목록 주변에서 윈도우를 움직여야 한다.</p>
<pre><code class="language-tsx">
// 리스트 아이템 하나의 높이
const ItemHeightSize = 50;

// 
function RecylerView({items}: Props) {

    // 목록의 시작인덱스, 끝 인덱스를 상태로 관리하며
    // 사용자가 스크롤할때마다 보여줄 목록의 인덱스를 업데이트 한다.
    // 여기서 보여줄 목록의 갯수는 20개
    const [visiableStartIndex, setVisiableStartIndex] = useState(0);
    const [visiableEndIndex, setVisiableEndIndex] = useState(20);


    const containerRef = useRef&lt;HTMLDivElement&gt;(null);


    // 스크롤 할때마다 start, end index update
    const handleScroll = () =&gt; {
        if(containerRef.current) {
        const scrollTop:number = containerRef.current.scrollTop;
        const newStartIndex = Math.floor(scrollTop/ ItemHeightSize);
        const visibleItemCount = Math.ceil(window.innerHeight/ ItemHeightSize);
        const newEndIndex = newStartIndex + visibleItemCount;
        setVisiableStartIndex(newStartIndex);
        setVisiableEndIndex(newEndIndex);
        }
    }


    // 이벤트 핸들러, 최초 1회 등록
    useEffect(() =&gt; {
        if(containerRef.current){
            containerRef.current.addEventListener(&quot;scroll&quot;, handleScroll)

        return () =&gt; {
            if(containerRef.current){
                 containerRef.current.removeEventListener(&quot;scroll&quot;, () =&gt; {})
            }
        }
        }
    }, [])


    return(

        // container : 스크롤을 위한 큰 돔 엘리먼트
        &lt;div style={{ overflowY:&quot;scroll&quot;, height:&quot;100vh&quot;}} ref={containerRef}&gt;
        // 이녀석이 Window : relative 속성을 가지는 작은 컨테이너 돔 엘리먼트
            &lt;ul style={{ height: ItemHeightSize * items.length, position:&quot;relative&quot; , width:500}}&gt;
                {items.slice(visiableStartIndex, visiableEndIndex).map((item: number, index: number) =&gt; (
                    // 컨테이너 내부에 위치하고
                    // absolute 포지션 속성을 가지고
                    // top | left | width |. height 속성을 가지는 자식 요소들
                    &lt;li key={index} style={{height: `${ItemHeightSize}px`, position: &quot;absolute&quot;, top: (visiableStartIndex+index)*ItemHeightSize}}&gt;{item}&lt;/li&gt;
                ))}
            &lt;/ul&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<hr>
<p><a href="https://patterns-dev-kr.github.io/performance-patterns/list-virtualization/">https://patterns-dev-kr.github.io/performance-patterns/list-virtualization/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[boosting,  score 조작]]></title>
            <link>https://velog.io/@fromjs_toyou/boosting-score-%EC%A1%B0%EC%9E%91</link>
            <guid>https://velog.io/@fromjs_toyou/boosting-score-%EC%A1%B0%EC%9E%91</guid>
            <pubDate>Wed, 28 Jan 2026 02:12:58 GMT</pubDate>
            <description><![CDATA[<p>elastic search에서 검색결과 통계에 사용되는 score 점수를 조정할 수 있는 것들 모음</p>
<h2 id="특정-필드에-가중치-두기">특정 필드에 가중치 두기</h2>
<pre><code class="language-json">GET blogs/_search
{
    &quot;query&quot;: {
        &quot;multi_match&quot;: {
            &quot;query&quot;: &quot;shay banon&quot;,
            &quot;fields&quot;: [
                &quot;title^3&quot;, // 이 필드에 3배 가중치
                &quot;content&quot;
            ]
        }
    }
}</code></pre>
<h2 id="인덱스에-가중치-두기">인덱스에 가중치 두기</h2>
<p>다중 인덱스 검색시, 특정 인덱스에 포함된 문서들에 가중치 두기</p>
<pre><code class="language-json">GET blogs*/_search
{
    &quot;indices_boost&quot;: [
        { &quot;blogs-2022&quot;: 2.0 }, // 이 인덱스의 점수는 2배
        { &quot;blogs-2021&quot;: 1.5 } // 이 인덱스의 점수는 1.5배
    ]
}</code></pre>
<h2 id="점수-고정시키기">점수 고정시키기</h2>
<p>특정 조건에 맞는 모든 결과의 점수를 동일하게 고정시킨다.</p>
<ul>
<li>장점: 조건에 맞는 문서들에 대해 score를 계산해야 하는 시간이 단축됨</li>
</ul>
<p>일반적인 쿼리는 검색어가 얼마나 자주 등장하는지(TF-IDF나 BM25 알고리즘)에 따라 결과마다 점수가 다 다fmek다. 하지만 <code>constant_score</code>는 <strong>&quot;조건에 맞기만 하면 점수는 무조건 X점!&quot;</strong>이라고 못 박는 것이다!</p>
<p>아래 예시는 monica가 작성한 모든 블로그 게시물은 점수를 1.5로 고정한다.</p>
<pre><code class="language-json">GET blogs/_search
{
    &quot;query&quot;: {
        &quot;constant_score&quot;: {
            &quot;filter&quot;: {
                &quot;term&quot;: { &quot;authors.first_name&quot;: &quot;monica&quot; }
            },
            &quot;boost&quot;: 1.5
} } }</code></pre>
<h2 id="score에-필드값-반영하기">score에 필드값 반영하기</h2>
<p>painelss scriping을 사용해서 기존의 필드값으로 score를 계산한다.</p>
<p>ex. 키가 큰 사람에게 가중치 주기</p>
<pre><code class="language-json">GET my_web_logs/_search
{
    &quot;query&quot;: {
        &quot;script_score&quot;: {
            &quot;query&quot;: {
                &quot;match&quot;: { &quot;message&quot;: &quot;elasticsearch&quot; }
            },
            &quot;script&quot;: {
            //검색결과 문서들의 score를 ‘resp_ms’필드의 값으로 나누어라.
                &quot;source&quot;: &quot;_score / doc[&#39;resp_ms&#39;].value&quot;
            }
} } }</code></pre>
<hr>
<h2 id="cf-쿼리-결과에-대한-설명-출력하기">cf) 쿼리 결과에 대한 설명 출력하기</h2>
<p><code>explain=true</code> 옵션을 통해 검색결과 계산에 대한 자세한 설명을 볼 수 잇다.</p>
<pre><code class="language-json">GET blogs/_search?explain=true
{
    &quot;query&quot;: {
        &quot;multi_match&quot; : {
            &quot;query&quot; : &quot;shay banon&quot;,
            &quot;fields&quot; : [ &quot;title^3&quot;, &quot;content&quot; ]
        }
    }
}</code></pre>
<pre><code class="language-json">// 이런식의 explanation 필드가 결과에 추가됨
&quot;_explanation&quot; : {
          &quot;value&quot; : 13.830835,
          &quot;description&quot; : &quot;max of:&quot;,
          &quot;details&quot; : [
            {
              &quot;value&quot; : 11.835554,
              &quot;description&quot; : &quot;sum of:&quot;,
              &quot;details&quot; : [
                {
                  &quot;value&quot; : 6.22925,
                  &quot;description&quot; : &quot;weight(content:shay in 483) [PerFieldSimilarity], result of:&quot;,
                  &quot;details&quot; : [
                    {
                      &quot;value&quot; : 6.22925,
                      &quot;description&quot; : &quot;score(freq=5.0), computed as boost * idf * tf from:&quot;,
                      &quot;details&quot; : [
                        {</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Shard Request Cache, Node Query Cache]]></title>
            <link>https://velog.io/@fromjs_toyou/Shard-Request-Cache-Node-Query-Cache</link>
            <guid>https://velog.io/@fromjs_toyou/Shard-Request-Cache-Node-Query-Cache</guid>
            <pubDate>Wed, 28 Jan 2026 02:10:03 GMT</pubDate>
            <description><![CDATA[<h1 id="cache">cache</h1>
<h2 id="q-샤드레벨의-캐시와-노드-레벨의-캐시의-차이">Q. 샤드레벨의 캐시와 노드 레벨의 캐시의 차이</h2>
<p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/shard-request-cache">공식 문서</a></p>
<h3 id="shard-request-cache">shard request cache</h3>
<p>검색 요청이 인덱스/다중 인덱스에서 실행될때, 각 샤드들은 검색을 로컬에서 실행하고 난뒤 결과를 coordinating node로 반환한다. coordinating 노드는 결과를 수집하여 global 결과 집합으로 만들어낸다.</p>
<p>이 과정 속에서 샤드레벨의 요청에 대한 로컬 응답값은 각 샤드의 local cache로 저장된다.
이 local cache는 샤드내 문서의 변화가 생기는 순간(refresh) 무효화 된다. -&gt; 즉, shard reqest cache는 변화가 거의 없는 정적인 데이터의 캐시 환경에서 사용하는 것이 좋다.</p>
<p><strong>특징</strong></p>
<ul>
<li>샤드 단위로 전체 결과를 캐싱한다.</li>
<li><code>size:0</code>일때 캐시된다.<ul>
<li>=&gt; aggs, suggest를 사용할때</li>
<li>즉, size=0을 사용하면 es에서는 이를 통계 데이터 요청으로 간주하고 복잡한 계산을 하지 않기 위해 그 결과값을 샤드 요청 캐시에 넣어두는 것.</li>
</ul>
</li>
<li><code>hits</code> 결과는 캐싱하지 않지만, <code>hits.total</code>은 캐싱한다.<ul>
<li>무거운 실제 문서(hits)는 저장하지 않고, 조건에 맞는 문서가 몇개인지 “숫자 결과(hits.total)”만 저장</li>
</ul>
</li>
<li>Date Range 또는 Histogram 질의 시 now를 사용하게 되면 캐싱하지 않는다.<ul>
<li>캐시의 핵심은 “동일 질문, 동일 결과”인데, now를 사용하면 이 원칙이 깨지므로..</li>
</ul>
</li>
</ul>
<p><strong>활성화 및 설정 방법</strong></p>
<ul>
<li>index 단위 캐시 활성화<pre><code class="language-json">⠀PUT index_01/_settings 
{
  &quot;index.requests.cache.enable&quot;: &quot;true&quot;
}</code></pre>
</li>
<li>query 단위 캐시 활성화<pre><code class="language-json">⠀PUT index_01/_search?request_cache=true 
{
  &quot;size&quot;: 0, 
      &quot;aggs&quot;: {
          &quot;animal&quot;: {
              &quot;terms&quot;: {
                  &quot;field&quot;: &quot;species&quot;
              }
      }
  }
}</code></pre>
</li>
<li>캐시 사이즈 설정은 <code>elasticsearch.yml</code>에서 설정할 수 있다.<ul>
<li><code>indices.requests.cache.size</code>: JVM Heap의 몇 %를 캐시 공간으로 사용할지 설정 (default: 1%)</li>
</ul>
</li>
</ul>
<h3 id="node-query-cache">node query cache</h3>
<p>es에서는 빈번하게 요청되는 <code>filter query</code>의 응답속도를 개선하기 위해서 cache를 사용하고, 이때 사용되는 캐시가 <code>node query cache</code>이다.
이는 문서 자체를 캐싱하는 것이 아니라 응답값(true/false)형태만 bitset 형태로 캐싱한다.
때문에 score 계산이 필요한 <code>query, aggs</code>는 <code>node query cache</code>가 적용되지 않는다.</p>
<ul>
<li>현재 쿼리 뿐 아니라 다른 쿼리에서도 재사용되므로 응답 속도를 개선할 수 있다.</li>
<li>LRU 정책(가장 오래 참조되지 않은 페이지를 교체)으로 동작</li>
<li><code>filter qeury</code>만 캐싱된다</li>
</ul>
<p><strong>활성화 및 설정방법</strong></p>
<p>활성화 및 캐시 사이즈 설정은 Elasticsearch Cluster내의 <strong>모든 데이터 노드</strong>에 적용해야 하며 <code>elasticsearch.yml</code>에서 설정할 수 있다.</p>
<ul>
<li>index.queries.cache.enabled: 활성화 유무 <em>(default: true)</em></li>
<li>indices.queries.cache.size: JVM Heap의 몇 %를 캐시 공간으로 사용할지 설정 <em>(default: 10%)</em></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] Spotit 2차 회고]]></title>
            <link>https://velog.io/@fromjs_toyou/%ED%9A%8C%EA%B3%A0-Spotit-2%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@fromjs_toyou/%ED%9A%8C%EA%B3%A0-Spotit-2%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 26 Oct 2025 07:34:33 GMT</pubDate>
            <description><![CDATA[<p>스팟잇 MVP 배포가 끝나고 리뷰위크에서 취합된 피드백을 바탕으로 약 2달여간 운영 및 개선을 진행했습니다. 오늘 회고글에서는 어떤 점을 개선하였는지 살펴보며 협업을 되돌아보도록 하겠습니다.</p>
<h2 id="피드백-수집-및-개선">피드백 수집 및 개선</h2>
<p>일주일간 약 50+여명의 사용자들로부터 전체 시나리오에 대한 피드백을 들을 수 있었어요. 확실히 여러 직군(PM, FE, BE, PD)의 동료들이 피드백을 받을 수 있어서인지, UX에 관한 부분 뿐 아니라 개발자의 시선으로 성능과 관련한 피드백도 들을 수 있어서 뜻깊은 시간이였습니다.  나름 신경썼던 부분(버튼 인터렉션이나 실시간 알림 기능)에 대한 긍정적인 피드백도 있어서 매우 뿌듯했습니다.</p>
<p>피드백을 바탕으로 비즈니스 적으로는 다음과 같은 개선을 진행했어요!</p>
<ul>
<li>노쇼 정책 추가</li>
<li>카테고리 선택 필터링 개선 : 선택개수 헬퍼 추가, 미리보기 삭제</li>
<li>예약 프로세스 개선 : 예약 확인용 모달을 약관을 포함한 페이지로 확대</li>
<li>팝업 카드 컴포넌트 구조 개선 : 정보의 중요성 순서대로 ui 변경, 웨이팅중인 팀수 추가</li>
<li>유저 기능 및 설정 페이지 확대 : 공지사항/약관동의/고객센터 추가 &amp; 회원탈퇴기능 개선</li>
<li>모바일용 랜딩페이지 추가</li>
</ul>
<p>기술적으로는 아래와 같은 개선을 진행했어요!</p>
<ul>
<li>홈화면의 필터와 Url을 동기화하는 로직을 추가하여 여러 페이지 이동시에도 처음 필터가 유지되도록 개선했어요. </li>
<li>쌓인 여러개의 알림 목록을 차례로 삭제할 때,낙관적 업데이트 방식을 사용하여 사용자 체감 반응성을 높였어요.</li>
<li>운영, 개발, 로컬 환경을 분리하고 GitHub Actions를 이용해 자동화된 배포 환경을 구축했습니다.환경이 분리되면서 불필요한 로그 노출로 인한 보안 문제를 방지하기 위해 공용 로깅 모듈을 개발했으며,운영 환경에서는 Sentry를 도입하여 장애 발생 시 신속하게 원인을 파악하고 대응할 수 있는 모니터링 체계를 마련했습니다.</li>
<li>대기순번 Polling 방식에 지수적 백오프를 추가하여 유연하게 요청주기를 변경하였어요. 서버 부하를 줄이는데 도움이 되었습니다.</li>
<li>(모바일 환경)지도 페이지에서 확대/축소시 발생하던 컴포넌트 침범 문제를 해결하였어요.</li>
</ul>
<h2 id="생산성-및-협업-툴-활용">생산성 및 협업 툴 활용</h2>
<ul>
<li>Figma UI 시안을 버전별로 분리하고, 변경사항과 해당 변경사항이 영향을 줄 페이지를 정리해서 전달하는 방식으로 소통 구조를 변경했어요. 이전에는 변경사항이 어제 발생했는지, 어떤 부분을 변경해야 하는지 명확하지 않아서 이를 메신저로 확인하는데 추가적인 시간이 필요했어요. 2차 phase에서는 위 방식이 아주 효과적으로 적용되어서 디자이너-FE개발자간 불필요한 질의응답시간이 줄었어요. </li>
<li>API 문서를 Github-Wiki에서 Swagger로 변경했어요. api 스펙 변경사항을 보다 빠르게 확인할 수 있다는 점은 장점이었지만, 보다 문서가 간결화된 만큼 문서에 드러나지 않는 점을 묻고 확인하기 위한 소통시간은 더 필요했던것 같습니다. wiki와 병행해서 쓰면 더 좋을것 같다고 생각했어요.</li>
<li>유비쿼터스 언어를 정리한 시트를 만들었는데 후반부에 가니 회의시간도 점차 잦고 길어지게 되었어요. 자연스럽게 시트를 들여다보지 않아도 즉각적으로 서로의 언어를 이해하게 되었습니다. 그래도 더 큰 규모, 더 전문적인 도메인의 프로젝트에서는 필요성이 있을것이라 생각합니다.</li>
</ul>
<h2 id="배운점">배운점</h2>
<ul>
<li>두번째 phase에 노쇼 정책이 추가되었어요. 처음에는 큰 영향이 없을 것이라고 생각했는데, 생각보다 만만치 않았습니다. 예약가능여부를 파악하기 위한 사용자 상태가 늘어난것은 물론이고, 알림정책, 스케쥴링 등 프로젝트 전체에 걸쳐 변경사항이 가장 많았던 개선사항이였어요.
QA시에도 노쇼정책이 정상적으로 동작하는지를 가장 중점적으로 테스트했고요. 이 과정을 통해 비즈니스 의사결정 하나가 개발 전반에 얼마나 큰 영향을 미칠수 있는지를 실감했습니다.</li>
<li>컴포넌트를 변경, 유지보수 하면서 변경에 유연한 컴포넌트의 가치를 꺠닫게되었습니다. 처음에 구조를 잡을 때부터 확장성과 독립성을 고려한 설계가 중요하다는 것을 눈물로 깨달았어요. props구조를 단순화하고, 상태와 관심사를 분리하는 방향으로 리팩터링을 진행했어요.</li>
</ul>
<hr>
<ul>
<li>이 활동은 <a href="https://ject.kr/">젝트</a>에서 진행한 프로젝트입니다  <img src="https://velog.velcdn.com/images/fromjs_toyou/post/8994998f-1af4-448e-ba70-d06668b34ea6/image.svg" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Optimistic Update로 사용자 체감 반응성 높이기]]></title>
            <link>https://velog.io/@fromjs_toyou/Optimistic-Update%EB%A1%9C-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%B2%B4%EA%B0%90-%EB%B0%98%EC%9D%91%EC%84%B1-%EB%86%92%EC%9D%B4%EA%B8%B0</link>
            <guid>https://velog.io/@fromjs_toyou/Optimistic-Update%EB%A1%9C-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%B2%B4%EA%B0%90-%EB%B0%98%EC%9D%91%EC%84%B1-%EB%86%92%EC%9D%B4%EA%B8%B0</guid>
            <pubDate>Tue, 16 Sep 2025 08:44:28 GMT</pubDate>
            <description><![CDATA[<h1 id="intro">intro</h1>
<p>스팟잇 서비스에서는 알림 기능을 지원하고 있어요. 서비스 특성상 여러개의 알림이 짧은 시간안에 도착할 때가 많은 데, 이때 삭제 버튼을 눌러 알림을 차례차례 지워나갈때, 덜걱거리는 듯한 사용성이 있다는 피드백을 받게되었어요.</p>
<p>이번 글에서는 낙관적 업데이트를 적용하여 알림 삭제시의 사용성을 개선한 경우를 소개보겠습니다.</p>
<h1 id="optimistic-update낙관적-업데이트란">Optimistic Update(낙관적 업데이트)란?</h1>
<p>낙관적 업데이트는 웹 어플리케이션에서 <strong>사용자 경험을 향상</strong>시키기 위해 사용되는 개념입니다.</p>
<p>프론트엔드에서 일반적으로 상태가 업데이트 되는 방식은 다음과 같습니다.</p>
<ol>
<li>사용자가 어떤 동작을 수행합니다. (예: 스크랩, 좋아요, 삭제 등)</li>
<li>서버에 변경사항을 요청하고 응답을 기다립니다.</li>
<li>서버의 응답에 따라 UI를 업데이트합니다.</li>
</ol>
<p>이런 방식은 사용자가 서버의 응답이 오고 UI가 업데이트 될때까지 기다려야 하기때문에, 네트워크지연이나 서버의 응답속도가 느릴경우 사용자 경험이 저하될 수 있습니다.</p>
<h3 id="낙관적-업데이트">낙관적 업데이트</h3>
<p>낙관적 업데이트는 이런 문제를 해결하는 방법으로 <strong>사용자의 동작에 대한 응답을 기다리지않고 UI를 먼저 업데이트</strong>하는 것입니다. 만약 응답이 실패로 돌아온다면 오류 메세지를 보여주고 기존의 UI상태로 롤백합니다. </p>
<p>낙관적으로 생각하여 응답이 성공할 것이라고 예측하고, 미리 UI를 업데이트한다고 이해하면 이름이 꽤 직관적이죠.</p>
<p>사용자는 동작을 수행하자 마자 UI 변경이 되는 빠른 피드백을 받을 수 있으므로 애플리케이션이 보다 빠르게 반응한다고 느끼게 됩니다.</p>
<p>그런데 이런 질문이 떠오를 수도 있어요.</p>
<blockquote>
<p>서버 응답을 기다리는게 그렇게 유의미한 차이가 나나요?</p>
</blockquote>
<p>낙관적 업데이트는 <code>네트워크 round-trip</code>에 의한 지연을 UI 피드백에서 제거하는 전략이에요. </p>
<p>여기서 <code>네트워크 round-trip time(RTT)</code>이란 클라이언트 -&gt; 서버 요청 전송 -&gt; 서버에서 처리 -&gt; 응답 수신까지 한번 왕복하는데 걸리는 시간을 의미하는데, 여기에선 TCP 연결, TLS handshake, 패킷 왕복지연, DB 조회/연산/비즈니스로직 처리, 클라이언트로의 패킷 전달 과정이 모두 포함되어요. 만약 물리적으로 더 원거리의 리전서버라면 300~500ms까지 차이날 수 있어요.</p>
<p><strong>UX 기준으로 100ms이면 사용자가 멈칫거림을 인식하는 수준이니 꽤 유의미한 수치</strong>에요.</p>
<blockquote>
<p>서버의 성능을 높이면 되지 않나요?</p>
</blockquote>
<p> 서버의 응답이 아무리 빠르더라도 사용자가 <strong>어떤 네트워크 환경에 처해있느냐</strong>에 다라서 애플리케이션의 응답속도가 달라져요. 예를들어 지하철의 공공와이파이로 접속했다면, 어떤 경우 네트워크가 불안정 할 수도 있습니다.</p>
<p> 서버의 응답에만 의존해서 상태를 업데이트 하는것이 아닌 낙관적 업데이트와 같은 로직을 활용하여 다양한 환경의 사용자 경험에 대비하는 것이 바람직할것 같아요.</p>
<h2 id="생활-속의-낙관적-업데이트-예시">생활 속의 낙관적 업데이트 예시</h2>
<p>대표적인 낙관적 업데이트를 사용한 예시는 채팅창에서도 찾아볼 수 있습니다.</p>
<p>디스코드에서 채팅이 완전히 전송되기 전임에도, 채팅창에는 이미 메세지가 나타납니다. 보통 흐린 글씨로 &#39;아직 전송중인 상태&#39;를 알려주고, 서버에서 메세지전송이 성공적으로 처리되면 진한 글씨로 바뀌어 정상적인 메세지로 보이게 되요. </p>
<p>사용자는 입력 후 즉시 피드백을 받기 때문에 서비스가 훨씬 빠르고 즉각적으로 반응한다고 느끼게 되는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/87484f2c-a36a-4f53-926a-7ff05410c0b8/image.gif" alt=""></p>
<h1 id="실제-서비스에-낙관적-업데이트-적용하기">실제 서비스에 낙관적 업데이트 적용하기</h1>
<h2 id="업데이트-전-체감-시간-측정하기">업데이트 전 체감 시간 측정하기</h2>
<p>사용자가 체감하는 시간은 <code>삭제 버튼 클릭(시작지점)</code> ~ <code>UI 갱신 완료 (완료 지점)</code>사이의 시간이에요. </p>
<p>아래 코드에서는 브라우저<code>Performance api</code>를 사용하여 시작 지점과 완료지점 사이의 시간차이를 측정하였습니다.</p>
<pre><code class="language-typescript">export default function NotificationCardList() {
  const currentDeleteIdRef = useRef&lt;number | null&gt;(null);
  const { data, hasNextPage, isFetchingNextPage, fetchNextPage } =
    useNotificationList();
  const { mutate: deleteNotification } = useDeleteNotification();

  ...

  const handleDeleteClick = (notification: NotificationType) =&gt; {
    const { notificationId } = notification;

    // 1. 클릭 시작
    performance.mark(`notification_delete_${notificationId}_start`);

    // 2.상태 없데이트
    currentDeleteIdRef.current = notificationId; // 현재 삭제할 알림id 기록
    deleteNotification(notificationId);
  };

  // api응답으로 ui 갱신 시점
  useEffect(() =&gt; {
    if (data) {
      const deleteId = currentDeleteIdRef.current;

      // 삭제할 아이템이 여전히 목록에 존재하는지 확인
      const stillExists = data.content.some(
        noti =&gt; noti.notificationId === deleteId
      );

      if (deleteId &amp;&amp; !stillExists) {
        performance.mark(`notification_delete_${deleteId}_end`);
        performance.measure(
          `notification_delete_${deleteId}_latency`,
          `notification_delete_${deleteId}_start`,
          `notification_delete_${deleteId}_end`
        );

        const entries = performance.getEntriesByName(
          `notification_delete_${deleteId}_latency`
        );

        const duration = entries[0]?.duration;

        if (duration) {
          logger.debug(
            `[Perf] Notification ${deleteId} delete latency: ${duration} ms`
          );
        }
      }
    }
  }, [data]);

  return (
      &lt;NotificationCardListView
        data={data.content}
        handleDelete={handleDeleteClick}
        lastElementRef={lastElementRef}
      /&gt;
  );
}
</code></pre>
<h4 id="1457ms"><code>145.7ms</code></h4>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/056edcf7-090b-49d0-8f69-fa32752a6312/image.png" alt=""></p>
<h2 id="낙관적-업데이트-적용하기">낙관적 업데이트 적용하기</h2>
<pre><code class="language-typescript">
// NotificationCardList.tsx
 ...
  const handleDeleteClick = (notification: NotificationType) =&gt; {
    const { notificationId } = notification;
    deleteNotification(notificationId);
  };
...</code></pre>
<p>알림 삭제 로직은 Tanstack-Query의 useMutation을 감싼 훅인 useDeleteNotification에서 처리됩니다.useMutation의 인자로 전달하는 객체에  onMutate을 추가해줍니다. </p>
<p><code>onMutate</code>은 mutationFn이 실행되기 직전에 호출되는 훅입니다. 즉, 서버에 삭제 요청을 보내기 직전에 미리 UI를 업데이트해 줍니다. onMutate의 리턴값으로는 서버 응답이 실패햇을 경우 되돌릴 데이터를 전달해줍니다.</p>
<pre><code class="language-typescript">// useDeleteNotification.ts
  onMutate: async (notificationId: number) =&gt; {
      performance.mark(`delete_${notificationId}_start`);
      // 1. 현재 쿼리 백업
      const prevData = queryClient.getQueryData&lt;NotificationListData&gt;(
        NOTIFICATION_LIST_QUERY_KEY
      );

      // 2. 알림 목록에서 해당 ID를 제거
      if (prevData) {
        queryClient.setQueryData(NOTIFICATION_LIST_QUERY_KEY, {
          ...prevData,
          pages: prevData.pages.map(page =&gt; ({
            ...page,
            content: page.content.filter(
              n =&gt; n.notificationId !== notificationId
            ),
          })),
        });
      }

      performance.mark(`delete_${notificationId}_end`);
      performance.measure(
        `delete_${notificationId}_perceived_latency`,
        `delete_${notificationId}_start`,
        `delete_${notificationId}_end`
      );

      const duration = performance
        .getEntriesByName(`delete_${notificationId}_perceived_latency`)
        .at(-1)?.duration;

      logger.debug(`[Perf] perceived latency: ${duration} ms`);

      // 3. 에러시 롤백할 데이터
      return { prevData };
    },
</code></pre>
<p><code>onMutate</code>에서 전달된 삭제전 알림목록의 데이터는 onError의 context에 담겨 전달됩니다.
알림 삭제에 실패했을 경우, 적절한 알림 메세지와 함께 삭제전 UI로 되돌려주는 작업을 수행해줍니다.</p>
<pre><code class="language-typescript">  onError: (error, notificationId, context) =&gt; {
      logger.error(&#39;[onError]:&#39;, error.message);
      toast.error(&#39;알림 삭제에 실패했습니다.&#39;);

      if (context?.prevData) {
        queryClient.setQueryData(NOTIFICATION_LIST_QUERY_KEY, context.prevData);
      }
    },</code></pre>
<p>추가적으로 <code>onSettled</code>는 성공혹은 실패 후, invalidate Query (쿼리무효화) -&gt; refetch가 완료되고 난 후 호출됩니다. 최종적으로 <strong>서버와 데이터 동기화가 완료된 시점</strong>이므로 여기에 완료 마커를 찍어 확인해보겠습니다.</p>
<pre><code class="language-typescript">    onSettled: (data, error, notificationId) =&gt; {
      performance.mark(`delete_${notificationId}_consistency_end`);
      performance.measure(
        `delete_${notificationId}_consistency_latency`,
        `delete_${notificationId}_start`,
        `delete_${notificationId}_consistency_end`
      );

      const duration = performance
        .getEntriesByName(`delete_${notificationId}_consistency_latency`)
        .at(-1)?.duration;

      logger.debug(`[Perf] consistency latency: ${duration} ms`);
    },</code></pre>
<h4 id="사용자-체감-지연-시간perceived_latency-077ms">사용자 체감 지연 시간(perceived_latency): <code>0.77ms</code></h4>
<p>사용자 체감 지연시간은 버튼 클릭 이후 ui가 변경되는 것을 확인할 수 있는 시간입니다.
보다 정확한 측정으로 위해 콘솔, 그리고 Performance 탭에서 알림 삭제 동작을 녹화하려 성능을 측정해았습니다. 그결과 0.77ms로 아주 빠른 반응시간을 확인할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/42ba1dab-74c5-452e-8882-13bec0608b41/image.png" alt=""></p>
<h4 id="일관성-지연-시간consistency_latency">일관성 지연 시간(consistency_latency)</h4>
<p>일관성 지연 시간은  낙관적 업데이트(optimistic update)와 같은 기법을 사용했을 때, 사용자에게 보여지는 UI가 실제 서버 데이터와 완벽하게 일치하는 데 걸리는 총 시간을 의미합니다.</p>
<p>사용자에게는 0ms로 빠른 체감 지연 시간을 제공하지만, 실제 시스템 내부에서는 여러 네트워크 요청과 데이터 동기화 과정으로 인해 더 긴 일관성 지연 시간이 발생합니다.
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/ef088507-7841-475d-b658-c938ebdc3b35/image.png" alt=""></p>
<h2 id="결론">결론</h2>
<p><strong>낙관적 업데이트 이전 (145.7ms)</strong>: 사용자가 삭제 버튼을 누르면, 실제 서버의 응답을 기다린 후에야 UI가 업데이트됩니다. 이 과정에서 네트워크 지연 시간이 포함되어 사용자는 145.7ms라는 긴 시간을 기다려야 합니다.</p>
<p><strong>낙관적 업데이트 이후 (0.5ms)</strong>: 사용자가 삭제 버튼을 누르자마자, 서버 응답과 관계없이 클라이언트에서 즉시 UI를 업데이트합니다. 네트워크 요청은 백그라운드에서 이루어지기 때문에, 사용자는 즉각적인 피드백을 받게 되고 체감 시간은 0.5ms로 측정됩니다.</p>
<p>UX 관점에서 약 145ms 이상의 개선으로 사용자가 확실히 체감할 수 있는 유의미한 개선사항을 만들어내었어요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[카카오 로그인 구현하기 ]]></title>
            <link>https://velog.io/@fromjs_toyou/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@fromjs_toyou/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 04 Sep 2025 08:24:54 GMT</pubDate>
            <description><![CDATA[<h1 id="intro">intro</h1>
<p>스팟잇서비스는 팝업스토어의 정보제공과 예약을 도와주는 서비스입니다. 유저기능을 구상하면서 사용자가 팝업현장에서 웨이팅하는 상황을 고려하여 복잡한 로컬 회원가입보다 간편한 카카오로그인방식을 선택하였습니다. 이번 글에서는 프론트엔드에서 카카오 로그인 방식을 구현한 방법과 트러블슈팅경험을 설명해보겠습니다. </p>
<p>(카카오 개발자 페이지에서 앱 등록과 권한 설정은 이미 되어 있다는 가정하에 글을 작성하였습니다.)</p>
<h1 id="oauth-20-이해하기">OAuth 2.0 이해하기</h1>
<p>우리는 새로운 웹사이트나 앱을 사용할 때, 구글,네이버,카카오와 같은 소셜계정으로 간편하에 회원가입 및 로그인을 할 수 있습니다. 이 떄 사용되는 프로토콜이 바로 OAuth입니다. 이 방법은 소셜사이트에서 나의 정보중 일부를 제3의 서비스와 공유해서 가능한 것 입니다. 이렇듯 OAuth는 서비스들 사이에서 안전하게 고객의 데이터를 주고 받기 위한 배경을 가지고 탄생했습니다.</p>
<blockquote>
<p>OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다. 
<strong>즉, 제 3자의 클라이언트에게 보호된 리소스를 제한적으로 접하게 해주는 프레임워크이다.</strong></p>
</blockquote>
<p>참고로 2010년에 OAuth 1.0에 대한 공식 표준안이 발표되었는데, 여기에는 문제점이 있었습니다. 대표적으로 복잡한 구현, 모바일 어플리케이션에서의 지원 부족, 만료되지 않은 AccessToken으로 인한 보안취약점들이 존재했습니다. 이런 Oauth 1.0의 문제점을 보완하여 2012년에 <a href="https://oauth.net/2/">Oauth 2.0</a>이 발표되었습니다.</p>
<h2 id="oauth-20은-어떻게-리소스를-공유하나요">Oauth 2.0은 어떻게 리소스를 공유하나요?</h2>
<p>Oauth의 구성요소를 먼저 살펴볼게요.</p>
<ul>
<li><p><strong>Resource owner (리소스 소유자)</strong> : 우리 서비스의 <code>사용자</code></p>
</li>
<li><p><strong>Client(클라이언트)</strong> 
  → 사용자의 정보에 접근하려는 제3의 서비스(=우리 서비스)
  → 이름이 클라이언트인 이유는 우리 서비스 서버가 <code>Resource Server</code>에게 필요한 자원을 요청하고 응답하는 관계이기 때문이에요.</p>
<ul>
<li><strong>Authorization Server(인증서버)</strong><ul>
<li>권한을 부여해주는 서버</li>
<li>클라이언트의 접근을 관리하는 서버</li>
<li><code>사용자</code>는 이 서버로 id, pw를 넘겨서 <code>Authorization Code</code>를 발급받아요.</li>
<li><code>Client(우리서버)</code> : 이 서버로 <code>Authorization Code</code>를 넘겨서 <code>Token</code>을 발급받아요.</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Resource Server(리소스서버)</strong></p>
<ul>
<li>사용자의 개인 정보를 가지고 있는 애플리케이션 ( = 구글, 카카오, 네이버)</li>
<li>리소스 소유자의 데이터를 관리하는 서버</li>
<li><code>Client(우리서버)</code>는 <code>Token</code>을 이 서버로 넘겨 <code>개인정보(보호된 리소스)</code>를 응답받아요.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/8e8ab342-5514-441c-964d-9a3a4ce078e7/image.png" alt=""></p>
<p>(1) 리소스 소유자가 클라이언트에게 인증을 허가해요</p>
<ul>
<li>구글, 카카오와 같은 소셜계정에 로그인하는 과정으로, 이때 리소스 소유자는 클라이언트와 어떤 정보를 공유할 지 선택할 수 있어요.</li>
</ul>
<p>(2) 리소스 소유자의 동의가 확인되면, 인증 서버는 클라이언트에게 AccessToken과 RefreshToken을 발급해요.</p>
<ul>
<li>Access Token<ul>
<li>자원에 대한 접근 권한을 Resource Owner가 인가하였음을 나타내는 자격증명</li>
</ul>
</li>
<li>Refresh Token<ul>
<li><code>Client(우리서버)</code>는 Authorization Server로 부터 access token(비교적 짧은 만료기간을 가짐) 과 refresh token(비교적 긴 만료기간을 가짐)을 함께 부여 받아요.</li>
<li>access token은 보안상 만료기간이 짧기 때문에 얼마 지나지 않아 만료되면 사용자는 로그인을 다시 시도해야해요.
그러나 refresh token이 있다면 access token이 만료될 때 refresh token을 통해 access token을 재발급 받아 재 로그인 할 필요없게끔 해요.</li>
</ul>
</li>
</ul>
<h1 id="카카오-로그인-구현하기">카카오 로그인 구현하기</h1>
<p><a href="https://developers.kakao.com/docs/latest/ko/kakaologin/common">카카오 개발자도구의 문서</a>에는 OAuth 2.0 프레임워크로 카카오로그인 과정이 친절하게 안내되어있습니다. 위 내용이 조금 어려웠라도 문서를 차분히 읽어내려가면 충분히 이해할 수 있어요. </p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/387d7648-b0d2-4ec0-96b8-d8988463b1b9/image.png" alt=""></p>
<p>해당 과정을 이해했다면 이제 구현에 들어가면 됩니다. 이 과정에서 Authorization Endpoint(인가요청주소)와 Redirect URI를 다루게 됩니다.</p>
<p><strong>Authorization endpoint(인가 요청 주소)</strong></p>
<p>인가 요청 주소는 보통 프론트가 호출하며 사용자를 카카오 로그인 화면으로 보내기 위해 카카오가 제공하는 URL 입니다. 보통 아래 처럼 생겼습니다.</p>
<pre><code class="language-text">https://kauth.kakao.com/oauth/authorize?client_id=...&amp;redirect_uri=...</code></pre>
<p><strong>Redirect URI</strong></p>
<p>인가 요청 주소를 살펴보면 쿼리로 <code>redirect_uri</code>가 있는 것을 알 수 있습니다. <code>Redirect URI</code>는 카카오 로그인후 사용자를 돌려보낼 프론트 or 서버주소가 되며,사용자가 id와 password를 입력해서 받은 <code>Authcode(인가코드)</code>가 함께 동봉됩니다.<br>백엔드 개발자와 함께 의논하여 Redirect URI를 어느쪽으로 할지 정하면 됩니다. <code>Redirect URI</code>에 프론트주소를 넣게되면 프론트가 인가코드를 받아서 백엔드서버를 다시 호출하는 구조이고, 서버 주소를 넣으면 서버가 바로 인가코드를 받아서 처리하면 됩니다.</p>
<ul>
<li><strong>형태</strong>:<ul>
<li>프론트 라우팅 주소: <code>https://myapp.com/oauth/callback</code></li>
<li>서버 API 주소: <code>https://api.myapp.com/auth/kakao/callback</code></li>
</ul>
</li>
</ul>
<p>저희 팀에서는 인가코드가 노출되는 것을 막기위해 서버 측으로 redirect uri를 설정해주도록 하겠습니다. 개념은 복잡했지만 프론트에서는 사용자가 로그인 버튼을 누르면 인가요청주소를 열어주기만 하면, 모든 과정은 서버에서 처리된다음 우리 서비스의 토큰을 쿠키에 실어 다시 프론트페이지로 리디렉션하도록 구성했어요.</p>
<h2 id="구현-과정">구현 과정</h2>
<p>카카오 로그인 과정에서 고민한 부분은 보호된 경로(로그인된 사용자만 볼수 있음)로 접근시 로그인 후 다시 되돌아가도록 하는 것이였어요.<strong>스팟잇 서비스의 &quot;현장 대기&quot;기능에서는 QR 코드를 통해 바로 웨이팅폼 페이지로 접근할 수 있는데, 이때 로그인 되지 않은 사용자의 경우 로그인 후 다시 웨이팅 페이지로 돌아와야 합니다.</strong> 때문에 <code>useKakaoLoginUrl</code>훅에서 로그인 후 되돌아갈 페이지정보를 서버에게 <code>state</code>에 담아 넘겨주도록 <code>Authorization endpoint</code>를 만드는 방식으로 구현했어요.</p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/25d326ed-0ebe-4f58-8fd3-dbaa53bad959/image.jpeg" alt=""></p>
<p>카카오 로그인 버튼을 누르면 <code>Authorization endpoint</code>를 생성하여 로그인 창을 엽니다.</p>
<pre><code class="language-typescript">export default function Login() {
  const kakaoLoginUrl = useKakaoLoginUrl({
    clientId: process.env.NEXT_PUBLIC_CLIENT_ID!,
    redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URL!,
  });

  const handleLogin = () =&gt; {
    if (kakaoLoginUrl) {
      window.location.href = kakaoLoginUrl;
    }
  };

  return (
    &lt;div&gt;
        /** ... */ 
        &lt;KakaoLoginButton onClick={handleLogin} disabled={!kakaoLoginUrl} /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>useKakaoLoginUrl 훅에서는 현재 url의 쿼리스트링에서 redirect_path값을 가져와 <code>Authorization endpoint</code>의 state값에 담아줍니다. 이렇게 하면 서버측에서 로그인을 완료한후 다시 redirect_path값으로 사용자를 되돌려보내줄 수 있어요.</p>
<pre><code class="language-typescript">
export function useKakaoLoginUrl({
  clientId,
  redirectUri,
}: {
  clientId: string;
  redirectUri: string;
}) {
  const [url, setUrl] = useState(&#39;&#39;);

  useEffect(() =&gt; {
    if (typeof window === &#39;undefined&#39;) return;

    const params = new URLSearchParams(window.location.search);
    const redirectPath = params.get(&#39;redirect_path&#39;);
    const redirectPathAfterLogin = redirectPath
      ? `/kakao?redirect_path=${redirectPath}`
      : &#39;/kakao&#39;;

    const kakaoUrl =
      `https://kauth.kakao.com/oauth/authorize` +
      `?client_id=${clientId}` +
      `&amp;redirect_uri=${encodeURIComponent(redirectUri)}` +
      `&amp;response_type=code` +
      `&amp;state=${encodeURIComponent(redirectPathAfterLogin)}`;

    setUrl(kakaoUrl);
  }, [clientId, redirectUri]);

  return url;
}</code></pre>
<p>서버에서는 사용자를 로그인 처리한 뒤, 성공했다면 <code>/kakao</code>로, 실패했다면 실패 코드를 query에 넣어 <code>/login/fail?reason=CODE</code>로 리다이렉트 해줍니다. 실패하는 케이스는 백엔드 개발자와 의논하여 실패Code에 따라 메세지를 매핑하여 로그인이 처리된 상황을 안내할 수 있도록 했어요.</p>
<pre><code class="language-typescript">// 카카오 로그인 실패시 보여줄 페이지

export default async function LoginFailPage({
  searchParams,
}: {
  searchParams: Promise&lt;{ reason: KakaoLoginFailType }&gt;;
}) {
  const { reason } = await searchParams;
  const DEFAULT_FAIL_MESSAGE = &#39;알 수 없는 오류가 발생했어요.&#39;;
  const message = KAKAO_LOGIN_FAIL_REASON_MAP[reason] ?? DEFAULT_FAIL_MESSAGE;

  return (
    &lt;di&gt;

      {/* 제목 및 메시지 */}
      &lt;h1 &gt;로그인 실패&lt;/h1&gt;
      &lt;p&gt;{message}&lt;/p&gt;

  /**
    자세한 ui 구현 내용은 생략
  */
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>성공할 경우 거쳐가는 <code>/kakao?redirect_path={path}</code>페이지에서는 (1) 유저 정보를 받아오고 (2) 클라이언트의 전역 상태를 업데이트 하는 과정을 진행합니다. 마지막으로 모든 과정이 마무리되면 로그인전 접근하려던 페이지로 라우팅시켜줍니다.</p>
<pre><code class="language-typescript">// 로그인 성공시 페이지
export default function Kakao() {
  const {
    data: user,
    isError,
    isSuccess,
  } = useQuery({
    queryKey: [&#39;auth&#39;, &#39;user&#39;],
    queryFn: () =&gt; getUserApi(),
    retry: false,
    staleTime: 5 * 60 * 1000, // 5분
    gcTime: 30 * 60 * 1000, // 30분
    refetchOnWindowFocus: false,
    select: data =&gt; (data ? { email: data.email, nickname: data.name } : null),
    throwOnError: false,
  });
  const router = useRouter();
  const searchParams = useSearchParams();
  const redirectTo = searchParams.get(&#39;redirect_path&#39;) || &#39;/&#39;;
  const setUser = useUserStore(state =&gt; state.setUser);
  const clearUser = useUserStore(state =&gt; state.clearUser);

  // ❗ useEffect로 분리: isError 처리
  useEffect(() =&gt; {
    if (isError) {
      clearUser();
    }
  }, [isError, clearUser]);

  // ❗ useEffect로 분리: 성공 시 사용자 설정 + 라우터 이동
  useEffect(() =&gt; {
    if (user &amp;&amp; isSuccess) {
      setUser({
        email: user.email,
        nickname: user.nickname,
        role: &#39;user&#39;,
      });
      router.replace(redirectTo);
    }
  }, [user, isSuccess, setUser, router]);

  return (
    &lt;div&gt;
      {/* 자세한 ui 스타일링은 생략 */}
      &lt;p &gt;로그인 중이이에요&lt;/p&gt;
    &lt;/div&gt;
  );
}

</code></pre>
<h1 id="🔥-트러블슈팅">🔥 트러블슈팅</h1>
<hr>
<h3 id="📚참고자료">📚참고자료</h3>
<ul>
<li><a href="https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-OAuth-20-%EA%B0%9C%EB%85%90-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC">OAuth 2.0 개념 - 그림으로 이해하기 쉽게 설명
출처: https://inpa.tistory.com/entry/WEB-📚-OAuth-20-개념-💯-정리 [Inpa Dev 👨‍💻:티스토리]</a></li>
<li><a href="https://developers.kakao.com/docs/latest/ko/kakaologin/common">Kakao Developers</a></li>
<li><a href="https://docs.tosspayments.com/resources/glossary/oauth">toss-Oauth 프레임워크</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[GTM으로 GA4·Clarity 이벤트 관리하기]]></title>
            <link>https://velog.io/@fromjs_toyou/GTM%EC%9C%BC%EB%A1%9C-GA4Clarity-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@fromjs_toyou/GTM%EC%9C%BC%EB%A1%9C-GA4Clarity-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 03 Sep 2025 08:07:45 GMT</pubDate>
            <description><![CDATA[<h1 id="intro">intro</h1>
<p>스팟잇 프로젝트중, PM으로부터 분석툴연결을 위한 요청이 들어왔습니다. </p>
<p>MVP 단계에서는 애플리케이션에 Google Analytics 스크립트를 직접 삽입하는 방식으로 이벤트를 추적했지만 새로운 분석툴이 추가될 때마다 코드를 수정하고 배포해야하는 번거로움이 있었습니다. 특히 GA 커스텀 이벤트뿐 아니라 Microsoft Clarity같은 도구의 연동요청이 추가되면서 추적환경을 한곳에서 관리하는 GTM 방식으로 전환하게 되었습니다.</p>
<p>이번 글에서는 Google Analytics(GA)와 Microsoft Clarity를 Google Tag Manager(GTM)로 관리하고 이를 프론트에서 연결하는 방법을 알아보겠습니다.</p>
<hr>
<h2 id="✏️-ga와-microsoft-clarity">✏️ GA와 Microsoft Clarity</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th><strong>Google Analytics (GA4)</strong></th>
<th><strong>Microsoft Clarity</strong></th>
</tr>
</thead>
<tbody><tr>
<td>하는 일</td>
<td><strong>숫자 집계</strong>: 몇 명이 들어왔고, 어떤 버튼을 눌렀는지 카운트</td>
<td><strong>화면 기록</strong>: 사용자가 페이지를 어떻게 움직였는지 영상/히트맵으로 보여줌</td>
</tr>
<tr>
<td>주로 쓰는 곳</td>
<td>- 페이지뷰, 이벤트 클릭 수, 전환율 확인<br>- 유입 경로(검색/광고) 분석</td>
<td>- 버튼/링크가 실제로 잘 눌리는지 확인<br>- 어디서 스크롤 멈추고, 어디서 많이 나가는지 확인</td>
</tr>
<tr>
<td>강점</td>
<td>- <strong>정량 데이터</strong> (숫자 기반 지표) 확보에 강함<br>- 광고·마케팅과 잘 연동됨</td>
<td>- <strong>정성 데이터</strong> (행동 패턴) 파악 가능<br>- 문제 상황을 영상으로 바로 재현 가능</td>
</tr>
<tr>
<td>단점</td>
<td>- 왜 그런 행동을 했는지는 알기 어려움</td>
<td>- 얼마나 많은 사용자가 그런 행동을 했는지는 약함</td>
</tr>
<tr>
<td>개발자 시선</td>
<td>“이 버튼 클릭 수를 로그로 남겨서 나중에 볼 수 있다”</td>
<td>“유저가 버튼을 못 찾아서 클릭을 연타했구나” 같은 걸 영상으로 확인 가능</td>
</tr>
</tbody></table>
<hr>
<h1 id="🚀-gtm">🚀 GTM</h1>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/63df298c-ab77-410b-8078-3d87c0fa4fbe/image.png" alt=""></p>
<p>어플리케이션과 GTM을 연결하면 앱에서 일어나는 이벤트들을 GTM으로 보낼 수 있습니다.  앱이 이벤트를 보내면 GTM은 이벤트가 등록된 <code>트리거</code>가 있는지 확인하고 매칭되면 그 트리거와 연결된 <code>태그</code>정보를 GA 와 같은 분석툴에 전달할 수 있습니다.</p>
<h2 id="react-프로젝트에-gtm-연결하기">react 프로젝트에 gtm 연결하기</h2>
<p>리엑트를 사용한다면 아래 패키지로 손쉽게 태그 매니저를 붙일 수 있습니다.</p>
<pre><code class="language-bash">npm install react-gtm-module

# (optional) 타입스크립트를 사용중이라면
npm install --save-dev @types/react-gtm-module</code></pre>
<p><code>react-gtm-module</code>라이브러리가 대신 GTM 스니펫을 DOM에 주입해줄 수 있도록 <code>GTMInit</code>컴포넌트를 만들고, Next를 사용하고 있으므로 루트 layout에 삽입해줍니다.</p>
<pre><code class="language-typescript">// GTMInit.tsx
&#39;use client&#39;;

import { useEffect } from &#39;react&#39;;
import TagManager from &#39;react-gtm-module&#39;;

export default function GTMInit() {
  useEffect(() =&gt; {
    TagManager.initialize({
      gtmId: process.env.NEXT_PUBLIC_GTM_ID!,
    });
  }, []);
  return null;
}

//layout.tsx
export default function RootLayout({
  children,
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;) {

  return (
    &lt;html lang=&quot;ko&quot; &gt;
      &lt;body &gt;
        &lt;GTMInit /&gt; // ✅
        {children}
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
<h2 id="트리거와-태그-생성하기">트리거와 태그 생성하기</h2>
<p>먼저 트리거와 태그와의 관계를 짚고 넘어가 보도록 하겠습니다.</p>
<h4 id="태그는-무엇을-할지를-결정한다">태그는 &quot;무엇을 할지&quot;를 결정한다.</h4>
<ul>
<li>예: GA4 이벤트 전송, Meta Pixel 전환, Hotjar 기록 시작 등</li>
</ul>
<h4 id="트리거는-언제-실행할지를-결정한다">트리거는 &quot;언제 실행할지&quot;를 결정한다.</h4>
<ul>
<li>페이지 로드(All Pages), 특정 버튼 클릭, 커스텀 이벤트(dataLayer.push), 스크롤 50% 도달 등</li>
</ul>
<p>즉, 트리거가 조건을 만족하면 태그가 실행됩니다.</p>
<p>스팟잇은 팝업 스토어의 정보제공과 예약을 도와주는 서비스인데요. 이번에 PM이 요청한 것은 <strong>팝업 상세페이지의 예약하기 버튼이 클릭될 때마다 팝업 스토어의 식별자를 담은 이벤트를 수집</strong>하는 것이였습니다.</p>
<p>따라서 사용자가 &quot;웨이팅하기&quot;버튼을 클릭했을 때, 선택된 팝업ID와 추가 데이터들을 아래와 같이 넘겨주도록 하겠습니다.</p>
<pre><code class="language-typescript"> const handleWaitingClick = async (e: React.MouseEvent&lt;HTMLButtonElement&gt;) =&gt; {
    if (status === &#39;NONE&#39;) { // 웨이팅하기 상태일때

      const { text, url } = extractLinkMetaFromButton(e);
      // GTM
      TagManager.dataLayer({
        dataLayer: {
          event: &#39;popup_click&#39;,
          popup_id: String(popupId),
          link_text: text,
          link_url: url,
        },
      });

      ...
    }
  };
</code></pre>
<h2 id="트리거-만들기">트리거 만들기</h2>
<p>GTM 콘솔창에서 트리거를 생성할 수 있습니다. 트리거 이름은 알아보기 쉽게 지으면 됩니다. <strong>중요한점은 <code>이벤트 이름(popup_click)</code>과 <code>dataLayer</code>에서 푸쉬된 event의 이름이 동일해야 한다는 것</strong>입니다.</p>
<p>(해당 과정이 올바르지 않다면 디버깅 콘솔에서 트리거링 되지않습니다.)</p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/2cc06c05-b114-40c0-92b3-2e34cd8a3885/image.png" alt=""></p>
<p>동작원리를 간단하게 살펴보면 그 이유를 알 수 있습니다.</p>
<p>(1) 사용자가 웨이팅 버튼을 클릭합니다.
(2) 버튼에 연결된 이벤트 핸들러는 내부적으로 아래처럼 실행되며, 데이터 계층(dataLayer)에 이벤트 객체가 쌓이게 됩니다.</p>
<pre><code class="language-typescript">window.dataLayer.push({
  event: &#39;popup_click&#39;, // ✅
  popup_id: &#39;8&#39;,
  link_text: &#39;웨이팅하기&#39;,
  link_url: &#39;https://spotit.co.kr/detail/8&#39;,
});</code></pre>
<p>(3) GTM 컨테이너는 항상 <code>window.dataLayer</code>를 감시중인데, (2) 번이 실행되면 GTM은 객체안의 event필드의 값을 확인해서 등록된<code>popup_click</code>이벤트가 있는지 찾습니다.
(4) 매칭된 트리거가 있다면, 해당 트리거에 연결된 태그(ex-GA4 이벤트)를 실행합니다.</p>
<h2 id="ga4-태그-만들기">GA4 태그 만들기</h2>
<p>트리거링 될때 GA4가 동작하도록 태그 유형을 선택하고 측정ID를 넣어줍니다.
    - 측정 ID는 데이터 스트림(Data Streams) 메뉴의 상세 화면에서 (G-xxxx) 형태로 확인할수 있습니다.
    - 해당 ID를 올바르게 입력해야 GTM -&gt; GA4로 이벤트가 전달될 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/6fe5de44-326e-4a03-9fce-97d5c7095f11/image.png" alt=""></p>
<h2 id="변수-생성하기">변수 생성하기</h2>
<p>다음으로 이벤트 매개변수를 설정해줍니다. 이때 <code>DLV 변수</code>를 새롭게 만들어야 합니다.</p>
<h3 id="datalayer-방식에서-사용하는-변수-만들기">DataLayer 방식에서 사용하는 변수 만들기</h3>
<p>우리는<code>dataLayer.push()</code>로 밀어넣은 값들을 GTM 안에서 꺼내서 써야 합니다. 이때 <code>popup_id</code>와 같은 <strong>서비스레이어 변수들은 GTM이 기본적으로 읽어오지 못합니다. <code>popup_id</code>를 GTM 변수로 등록해야 태그에서 활용할 수 있습니다.</strong></p>
<p>콘솔창의 &quot;변수&quot;에서 새로 만들기 버튼을 클릭해 변수를 구성할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/7245e7e9-b6a8-4f44-a45d-8e848713d8aa/image.png" alt=""></p>
<p>이제 만든 변수들은 태그에서 활용할 수 있습니다. 태그 -&gt; 변수 선택란에서 새롭게 만든 변수를 선택해주면 됩니다.
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/85f46edd-8fe0-47c2-859e-b0a90e4e4fd1/image.png" alt=""></p>
<h3 id="built-in-variables-와의-차이">Built-In Variables 와의 차이</h3>
<p>사용자가 클릭하거나 페이지를 이동할 때 <strong>브라우저 DOM에서 바로 읽을 수 있는 변수들</strong>은 기본 변수로 다룰 수 있습니다.</p>
<p><strong>Click 관련</strong></p>
<ul>
<li>Click Element</li>
<li>Click Classes</li>
<li>Click ID</li>
<li>Click Text</li>
<li>Click URL</li>
</ul>
<p><strong>Form 관련</strong></p>
<ul>
<li>Form Element</li>
<li>Form Classes</li>
<li>Form ID</li>
<li>Form Target</li>
</ul>
<p><strong>Page 관련</strong></p>
<ul>
<li>Page URL</li>
<li>Page Hostname</li>
<li>Page Path</li>
</ul>
<p><strong>기타</strong></p>
<ul>
<li>Referrer</li>
<li>History Source</li>
</ul>
<p>ex)</p>
<pre><code class="language-html">&lt;button id=&quot;reserve&quot; class=&quot;btn&quot;&gt;웨이팅하기&lt;/button&gt;</code></pre>
<p>이런 버튼이 클릭된다면, <code>Click Text = &quot;웨이팅하기&quot;, Click ID = reserve, Click Classes = btn</code> 처럼 읽어올 수 있습니다.</p>
<h3 id="두-방식-모두-잘-동작한다면-어떤-기준으로-선택을-해야-할까요">두 방식 모두 잘 동작한다면 어떤 기준으로 선택을 해야 할까요?</h3>
<table>
<thead>
<tr>
<th>DOM 변수 방식 (Click Text, Click ID, Page URL 등)</th>
<th>DataLayer 방식 (dataLayer.push)</th>
</tr>
</thead>
<tbody><tr>
<td>🚀  개발자가 코드를 건드리지 않아도 GTM에서 세팅이 가능합니다.</td>
<td>🚀  이벤트 이름(popup_click)과 속성(popup_id, amount)은 서비스 로직에 종속되므로 UI 구조 바뀌어도 영향이 적습니다.</td>
</tr>
<tr>
<td>⚠️ DOM 구조나 텍스트가 바뀌면 이벤트가 꺠질 수 있습니다.</td>
<td>🚀  정확한 비즈니스 데이터(예약 ID, 결제 금액, 유저 상태 등)를 안정적으로 전달 가능</td>
</tr>
<tr>
<td>⚠️ CSS class, ID 네이밍 규칙 바뀌면 동일하게 깨짐</td>
<td>🚀  분석 툴/마케팅 툴 추가 시에도 동일한 데이터 재활용 가능 → 확장성 ↑</td>
</tr>
<tr>
<td>⚠️ PM/디자이너가 UI를 자주 바꾸는 프로젝트에서는 유지보수 부담 ↑</td>
<td>⚠️ 처음에는 개발자가 이벤트를 심어줘야 해서 초기 세팅 비용 ↑</td>
</tr>
</tbody></table>
<p>만약 짧게 사용하려는 테스트나 캠페인이라면 DOM 변수방식이, 장기적인 서비스운영과 주요 이벤트 추적에서는 DataLayer 방식이 유리해 보입니다.</p>
<p><strong>이번 경우는 웨이팅이라는 핵심 로직의 추적이 필요하므로 DataLayer 기반으로 이벤트</strong>를 설계하여 유지보수성을 높이는 방식을 선택하였습니다.</p>
<h2 id="clarity-태그-생성">Clarity 태그 생성</h2>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/b288efff-dd9f-455f-b16e-1a3bb79e5f2c/image.png" alt=""></p>
<p>템플릿 갤러리에서 Microsoft Clarity (by Microsoft) 선택후, Project ID를 넣으면 태그를 쉽게 생성할 수 있습니다.</p>
<hr>
<h1 id="✅-확인하기">✅ 확인하기</h1>
<h2 id="1-gtm-preview">1. GTM Preview</h2>
<blockquote>
<p>트리거/태그의 발화여부를 1차로 확인하는 방법</p>
</blockquote>
<p>태그 관리자 페이지의 미리보기 버튼을 눌러 프리뷰 모드를 활성화할 수 있습니다.
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/131b2849-0e31-4b73-9884-31f9007277ea/image.png" alt=""></p>
<p>Tags 탭에서 popup_click 태그가 fired 되었는지 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/455913f0-178e-48e2-a139-88ea24c6fd77/image.png" alt=""></p>
<h2 id="2-ga4-debugview">2. GA4 DebugView</h2>
<blockquote>
<p>GTM에서 GA4까지 이벤트가 실제로 전송되었는지 확인하는 2차 방법</p>
</blockquote>
<p>GA4    콘솔의 DebugView를 통해 확인할 수 있습니다. 디버그뷰를 보기 위해서는 GTM 프리뷰 모드를 유지하거나 크롬 확장프로그램에서 GA Debugger를 켜야합니다.</p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/69aa9926-6063-43f7-8e82-6fa1e62aeeb5/image.png" alt=""></p>
<h2 id="3-브라우저-network-탭-확인">3. 브라우저 Network 탭 확인</h2>
<blockquote>
<p>실제 데이터가 GA 서버로 전송됐는지 확인하는 최종 방법</p>
</blockquote>
<p>버튼을 클릭시에 <code>collect?...&amp;en=popup_click</code>요청이 발생하는지 확인합니다.</p>
<ul>
<li>쿼리파라미터에 <code>tid</code>와 같은 이벤트 파라이터가 포함되어 있어야 정상입니다.
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/b24a4d77-2ffc-47fa-a8a0-ebbaae8dbd19/image.png" alt=""></li>
</ul>
<hr>
<h1 id="배운점">배운점</h1>
<p>제품에 GA와 같은 분석툴을 붙여보면서, 단순히 이벤트 로그를 남기는 작업이 아니라 제품을 더 잘 이해하기 위한 기반을 만드는 과정이라는 것을 느꼈습니다. 종종 이런 툴을 추가해주는 작업이 부가적인 일로 생각하기 쉬운데, 실제로는 이 데이터들은 PM과 디자이너가 제품을 개선하는 근거가 되기 때문에 프로덕트의 전체 품질과 직결되는 사항이란 것을 깨닫게 되었습니다. 또한 앞으로는 이벤트 하나를 심을 때도 “이 값이 단순 카운팅을 넘어서, 사용자 여정이나 전환 흐름을 어떻게 설명할 수 있을까?” 같은 <strong>제품 관점의 질문</strong>을 가져야 한다는 걸 배웠습니다.</p>
<hr>
<h3 id="참고자료-및-이미지-출처">참고자료 및 이미지 출처</h3>
<ul>
<li><a href="https://velog.io/@yunsungyang-omc/React-%EA%B7%B8%EB%A1%9C%EC%8A%A4-%ED%95%B4%ED%82%B9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C">(React) 그로스 해킹 프론트엔드 : GTM을 사용해 마케팅 지표 로깅하기</a></li>
<li><a href="https://goo-gy.github.io/2021-04-28-google-tag-manager">React 프로젝트에 Google Tag Manager 설정하기</a></li>
<li><a href="https://yozm.wishket.com/magazine/detail/1888/">구글 태그 관리자, 어떻게 쓰면 좋을까요?</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spotit 회고]]></title>
            <link>https://velog.io/@fromjs_toyou/Spotit-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@fromjs_toyou/Spotit-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 02 Sep 2025 08:11:44 GMT</pubDate>
            <description><![CDATA[<p>서로 다른 직군의 7명의 팀원이 모여 완성한 spotit 프로젝트의 MVP가 완성되었습니다. 부트캠프를 하며 개발자로만 이루어진 팀만 경험하다가, 비개발 직군과 함께한 협업은 완전히 다른 느낌이었습니다. 그만큼 미숙한 점이 많았지만 분명 성장한 점이 눈에 띄일만큼 소중한 경험이었습니다.</p>
<p>글을 쓰는 지금은 팀 리뷰와 파트리뷰를 모두 완료한 상태인데, 팀원들의 회고글을 읽어보며 다시 깨닫게 되는 부분이 있었습니다. 개인 리뷰를 적으며 동료리뷰중 인상깊었던 점들도 차례로 풀어보겠습니다.</p>
<h1 id="기술">기술</h1>
<h2 id="zustand">Zustand</h2>
<p>상태관리 라이브러리인 <code>Zustand</code>를 다뤄보았습니다. <code>Redux</code>에 비해 보일러 플레이트가 적고 직관적으로 상태를 정의할 수 있어서 빠르게 적응할 수 있었습니다. 특히 중간에 유저 정보 유지를 위해 <code>persist</code>를 도입해야 했었는데 zustand에서는 해당 기능이 내장되어 있어서 추가로 라이브러리가 필요하지 않았던 점이 편리했습니다.</p>
<summary><strong> 🔥 전역 유저 상태 관리를 위한 useUserStore</strong></summary>

<p><code>hasHydrated</code> : zustand persist로 로컬 스토리지의 값이 클라이언트 상태에 복원되었는지 여부를 알기 위함.</p>
<ul>
<li>Next.js 같은 SSR 환경에서는 서버 렌더링 시점에는 localStorage가 없어서 항상 기본값(initialUserState) 으로만 그려지고, 이후 클라이언트에서 persist 미들웨어가 동작하면서 localStorage에 저장된 유저 상태가 복원됩니다.</li>
<li>이때 서버에서 그려진 HTML과 클라이언트에서 복원된 상태가 달라서 hydration mismatch 경고나 UI 깜빡임이 발생했습니다.</li>
</ul>
<p><code>onRehydrateStorage</code> 훅에서 복원 완료 시 <code>_setHasHydrated(true)</code> 실행 🔜 스토어 안의 <code>_hasHydrated</code> 값이 true로 바뀌며 hydration 이 완료됨을 알려줍니다.</p>
<blockquote>
<h3 id="onrehydratestorage의-시그니처"><code>onRehydrateStorage</code>의 시그니처</h3>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/9d4bdc39-72d7-472f-9c44-cdc9060389da/image.png" alt=""></p>
<ol>
<li>첫번째 함수 : rehydration이 시작될 때 호출됨</li>
<li>두번째 함수 : 첫번째 함수가 리턴하는 콜백으로 rehydration이 끝났을 때 호출됨</li>
</ol>
</blockquote>
<pre><code class="language-typescript">type Store = {
  userState: UserState;
  clearUser: () =&gt; void;
  setUser: (user: User) =&gt; void;
  _hasHydrated: boolean; // client에서 hydration완료 여부 확인용
  _setHasHydrated: (v: boolean) =&gt; void;
};

export const useUserStore = create&lt;Store&gt;()(
  persist(
    set =&gt; ({
      userState: initialUserState,
      clearUser: () =&gt; set({ userState: initialUserState }),
      setUser: (user: User) =&gt; set({ userState: { isLoggedIn: true, user } }),
      _hasHydrated: false,
      _setHasHydrated: v =&gt; set({ _hasHydrated: v }),
    }),
    {
      name: &#39;user&#39;,
      storage:
        typeof window !== &#39;undefined&#39;
          ? createJSONStorage(() =&gt; localStorage)
          : undefined,
      onRehydrateStorage: () =&gt; state =&gt; {
        state?._setHasHydrated(true);
      },
    }
  )
);

export function useUserHydrated() {
  return useUserStore(s =&gt; s._hasHydrated);
}</code></pre>
<p><code>useUserHydrated</code>로 hydration상태를 읽어서, 복원이 끝나기 전까지는 로딩만 보여줌으로써 mismatch 방지할 수 있었습니다.</p>
<pre><code class="language-typescript">  const AuthGuard = dynamic(() =&gt; import(&#39;@/features/auth/lib/AuthGuard&#39;), {
  ssr: false,
  loading: () =&gt; (
    &lt;div&gt;
      &lt;div&gt;
        유저정보 확인 중이에요…
      &lt;/div&gt;
    &lt;/div&gt;
  ),
});

export default function AuthRequiredLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const hasHydrated = useUserHydrated();

  if (!hasHydrated) return null;
  return &lt;AuthGuard&gt;{children}&lt;/AuthGuard&gt;;
}</code></pre>
<h2 id="tanstack-query">TanStack Query</h2>
<p>서버 상태 관리를 위한 <code>@tanstack/react-query</code>를 더 깊게 다뤄보았습니다. 이전에도 사용 경험이 있었지만, 이번에는 <strong>v4 → v5</strong> 업그레이드를 진행하면서 쿼리 옵션과 다양한 훅을 더 깊이 이해할 수 있었습니다. 기존에는 <code>useQuery</code>, <code>useMutation</code> 정도만 사용했는데, 이번에는 <strong>Suspense 기반 훅</strong>을 활용하며 로딩/에러 상태 처리와 에러 핸들링 방식을 보다 세밀하게 다룰 수 있게 되었습니다.</p>
<p>특히 v5로 업그레이드되면서 <code>useQuery</code> 훅 옵션으로 제공되던 <code>onSuccess</code>, <code>onError</code>(및 <code>onSettled</code>) 콜백 사용을 지양하는 흐름이 강해졌습니다. 따라서 <strong>데이터 패칭</strong>과 <strong>사이드 이펙트 처리</strong>를 분리하여, 커스텀 훅을 통해 보다 <strong>명시적</strong>으로 제어하는 방식을 적용하였습니다.</p>
<blockquote>
<h3 id="왜-deprecated또는-지양되었을까">왜 deprecated(또는 지양)되었을까?</h3>
<p><strong>① 사이드 이펙트의 명시적 분리 부족</strong><br>쿼리 옵션에 사이드 이펙트를 넣으면 <em>데이터 fetching</em>과 <em>부수 효과</em>가 암묵적으로 결합되어 컴포넌트의 책임이 모호해집니다.</p>
<p><strong>② 리렌더링 및 의존성 관리 문제</strong><br>부모 컴포넌트에서 inline 콜백을 전달하면, 렌더링마다 새로운 함수 참조가 생성되어 <code>useEffect</code> 의존성에 의해 반복 실행될 위험이 있습니다.</p>
<p><strong>③ 테스트·예측 가능성 저해</strong><br>내부적으로 자동 실행되는 콜백은 실행 타이밍을 예측하기 어려워 디버깅과 유지보수가 힘들어집니다.</p>
</blockquote>
  <summary><strong> 😀 커스텀 훅 구현하기 (useQueryEffects)</strong></summary>

<pre><code class="language-typescript">import { useEffect, useRef } from &#39;react&#39;;
import type { UseSuspenseInfiniteQueryResult } from &#39;@tanstack/react-query&#39;;

type QueryEffectsOptions&lt;TData, TError&gt; = {
  onSuccess?: (data: TData) =&gt; void;
  onError?: (error: TError) =&gt; void;
  onSettled?: (data: TData | undefined, error: TError | null) =&gt; void;
};

export function useQueryEffects&lt;TData, TError&gt;(
  query: UseSuspenseInfiniteQueryResult&lt;TData, TError&gt;,
  options: QueryEffectsOptions&lt;TData, TError&gt;
) {
  const { onSuccess, onError, onSettled } = options;

  // 이전 상태를 추적하기 위한 ref
  const prevStateRef = useRef({
    isSuccess: false,
    isError: false,
    data: undefined as TData | undefined,
    error: null as TError | null,
  });

  useEffect(() =&gt; {
    const { isSuccess, isError, data, error } = query;
    const prev = prevStateRef.current;

    // 새로운 성공 전이 시에만 실행
    if (isSuccess &amp;&amp; onSuccess &amp;&amp; !prev.isSuccess) {
      onSuccess(data as TData);
    }

    // 새로운 에러 전이 시에만 실행
    if (isError &amp;&amp; onError &amp;&amp; !prev.isError) {
      onError(error as TError);
    }

    // 성공 또는 에러로 처음 전이되었을 때만 실행
    if ((isSuccess || isError) &amp;&amp; onSettled &amp;&amp; !(prev.isSuccess || prev.isError)) {
      onSettled(data, error);
    }

    // 현재 상태 저장
    prevStateRef.current = { isSuccess, isError, data, error };
  }, [
    query.isSuccess,
    query.isError,
    query.data,
    query.error,
    onSuccess,
    onError,
    onSettled,
  ]);

  return query;
}</code></pre>
<summary><strong>💡 실제 사용처에서 콜백 넘겨주기</strong></summary>

<pre><code class="language-typescript">const query = useSuspenseInfiniteQuery({
  queryKey: [&#39;popup&#39;, &#39;list&#39;, { ...request }],
  queryFn: /* ... */,
});

useQueryEffects(query, {
  onSuccess: (data) =&gt; {
    if (process.env.NEXT_PUBLIC_ENV === &#39;DEVELOP&#39;) {
      console.log(&#39;[onSuccess]:&#39;, data);
    }
  },
  onError: (error) =&gt; {
    handleNetworkError(error);
    console.error(&#39;[onError]:&#39;, error);
    throw error;
  },
  onSettled: (data, error) =&gt; {
    if (process.env.NEXT_PUBLIC_ENV === &#39;DEVELOP&#39;) {
      console.log(&#39;[onSettled]:&#39;, data, error);
    }
  },
});</code></pre>
<h2 id="nextjs-캐싱-최적화하기">Next.js 캐싱 최적화하기</h2>
<p>Next.js의 캐싱·ISR·태깅(revalidate, tags, revalidateTag)은 fetch에 최적화되어 있어 axios 대신 fetch 래퍼(API Builder) 를 만들어 사용하였습니다.</p>
<p>목표는 
(1) 요청마다 캐시 정책이 드러나게 하고(cache, next:{ revalidate, tags }) 
(2) 서버/클라이언트 인증 분기를 안전하게 처리하며 
(3) 파라미터 직렬화·타임아웃·에러 매핑을 공통화하는 것입니다</p>
<p>따라서 <a href="https://github.com/JECT-Study/JECT-3th-6team/blob/main/frontend/src/shared/lib/APIBuilder.ts"><code>API Builder</code></a>에서 설정과 체이닝을 담당하고, <a href="https://github.com/JECT-Study/JECT-3th-6team/blob/main/frontend/src/shared/lib/API.ts"><code>API.call()</code></a>로 fetch를 실행하도록 구성하였습니다.</p>
<h3 id="캐싱-옵션"><strong>캐싱 옵션</strong></h3>
<ul>
<li><code>setCache(cache: RequestCache)</code> 메서드 → no-store/force-cache 등</li>
<li><code>next(config: NextFetchRequestConfig)</code>메서드 → { revalidate, tags } 지정</li>
</ul>
<h3 id="인증-서버클라이언트-분리"><strong>인증 (서버/클라이언트 분리)</strong></h3>
<ul>
<li><code>.auth()</code>를 호출하면 내부의 <code>authInternal()</code>이 실행됩니다.</li>
<li><strong>서버(SSR/서버 컴포넌트/Route Handler)</strong>: next/headers의 cookies()로 accessToken을 읽어 Authorization 헤더를 주입.</li>
<li><strong>클라이언트</strong>: 별도 주입 없이 withCredentials = true로 쿠키 기반 인증을 사용(브라우저가 자동 전송).</li>
</ul>
<h4 id="공개-api-호출시">공개 api 호출시</h4>
<pre><code class="language-typescript">export const getPopupDetailApi = async (
  popupId
) =&gt; {
  const response = await APIBuilder.get(
    POPUP_DETAIL_ENDPOINTS.GET_POPUP_DETAIL(popupId)
  )
    .timeout(5000)
    .setCache(&#39;force-cache&#39;)
      .next({ revalidate: 300, tags: [`popup:${popupId}`] })  
      .build()
    .call&lt;PopupDetailResponseDto&gt;();

  return response.data;
};</code></pre>
<h4 id="인증이-필요한-api-호출시">인증이 필요한 api 호출시</h4>
<pre><code class="language-typescript">export default async function getUserApi() {
    const response = await (
      await APIBuilder.get(&#39;/auth/me&#39;)
        .timeout(5000)
        .withCredentials(true)
        .auth()
        .setCache(&#39;no-store&#39;) 
        .buildAsync()
    ).call&lt;UserResponse&gt;();

    return response.data;
}
</code></pre>
<h1 id="코드-품질리뷰">코드 품질/리뷰</h1>
<p>프론트/백엔드 개발은 <a href="https://github.com/JECT-Study/JECT-3th-6team"><strong>모노레포</strong></a>로 프로젝트를 관리하여 상호 코드리뷰를 통해 코드 품질을 지키려 노력하였습니다.모노레포를 적용하며 파트간 리뷰뿐 아니라 백 &lt;-&gt; 프론트간 리뷰도 진행되어 구현 사항에 관한 논의가 활발하게 진행되었던 점, 이슈트랙킹을 한눈에 볼 수 있던 점이 좋았습니다.</p>
<h2 id="신경썼던-점--코드리뷰-문화-개선"><strong>신경썼던 점 : 코드리뷰 문화 개선</strong></h2>
<p>코드 리뷰는 필연적으로 상대방의 부족한 점에 대한 지적이 포함되기 때문에, 자칫 감정적으로 상할 수 있다고 생각하였습니다.<br>그래서 리뷰 시에는 <strong>보완할 점뿐 아니라 인상 깊었던 부분에 대한 칭찬</strong>도 함께 전달하였고, 😊 같은 <strong>이모지</strong>를 활용하여 좀 더 부드럽고 긍정적인 대화가 이루어지도록 하였습니다.  </p>
<p>또한 프로젝트 중반부터는 <strong>코드 리뷰 형식을 정립</strong>하여, 리뷰의 <strong>중요도를 표기할 수 있는 간단한 형식</strong>을 제작하였습니다. 이를 통해 리뷰의 우선순위를 명확히 하고, 피드백을 보다 체계적으로 전달할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/3c21d093-082f-4331-b4af-996fa3c7b3cf/image.png" alt=""></p>
<h2 id="성장한-점"><strong>성장한 점</strong></h2>
<p>코드리뷰 통해 컴포넌트의 적절한 책임 단위의 기준을 배우고 이후에는 스스로 점검하는 습관이 생겼습니다. </p>
<ul>
<li><a href="https://github.com/JECT-Study/JECT-3th-6team/pull/68#discussion_r2195792783">함수 책임 분리 pr #33</a></li>
<li><a href="https://github.com/JECT-Study/JECT-3th-6team/pull/144#discussion_r2253660166">컴포넌트 책임 분리 pr #144</a></li>
</ul>
<h2 id="인상-깊은-피드백"><strong>인상 깊은 피드백</strong></h2>
<p>초기 리뷰 중 <a href="https://github.com/JECT-Study/JECT-3th-6team/pull/37"><strong>FSD 아키텍처 적용 시 폴더 구조를 어떻게 설계할지</strong>에 대한 논의 (pr#37)</a>가 있었습니다.<br>이 과정에서 동료와 충분히 의견을 나누고, 합의점을 도출해낸 점이 <strong>이상적인 리뷰 사례</strong>로 팀 회고에서 언급되었습니다.<br>이를 통해 코드 리뷰가 단순한 코드 수정 지적을 넘어, <strong>팀 전체의 아키텍처 방향성을 함께 만들어가는 과정</strong>이 될 수 있다는 점을 확인할 수 있었습니다.  </p>
<hr>
<h1 id="협업">협업</h1>
<p>여러 직군의 동료와 협업하며 개선해야 할 점을 몇가지 발견하였습니다.</p>
<h2 id="📍-디자이너와의-협업-과정에서의-문제와-개선-방안">📍 디자이너와의 협업 과정에서의 문제와 개선 방안</h2>
<p>프로젝트 진행 중 <strong>디자인 변경 사항이 프론트엔드 개발 과정에 원활히 반영되지 못하는 문제</strong>를 경험하였습니다.<br>이번 프로젝트는 <strong>‘UI 구현 완료 → API 연결 및 기능 개발 → QA’</strong> 의 순서로 진행되었는데, 기능 구현 단계에서 시안이 수정되었음에도 불구하고 이를 제때 공유받지 못하는 상황이 있었습니다.<br>그 결과 QA 단계에 이르러서야 변경 사항을 확인하는 경우가 발생하였습니다.  </p>
<p>CSS 수정 자체는 어렵지 않았으나, <strong>디자인 변경 사실이 개발자에게 늦게 전달된다는 점은 협업 과정의 문제</strong>라고 판단하였습니다.  </p>
<h3 id="💗-개선-방안"><strong>💗 개선 방안</strong></h3>
<ol>
<li><strong>디자인 시안을 명확히 버전으로 구분</strong>하여 관리합니다.  </li>
<li><strong>특정 컴포넌트에 변경이 있을 경우</strong>, 해당 변경이 영향을 미치는 페이지를 정리해 전달합니다.  </li>
<li><strong>프론트엔드에서는 수정 가능 마감일</strong>을 설정하여 불필요한 혼선을 줄입니다.  </li>
</ol>
<h3 id="✏️협업-툴-검토"><strong>✏️협업 툴 검토</strong></h3>
<p>추가적으로 디자이너와의 협업 효율을 높이기 위해 몇 가지 툴을 검토하였습니다.  </p>
<ul>
<li><p><strong>Design Lint (플러그인)</strong><br>: 디자인 시안에서 “디자인 시스템 규칙 위반”을 자동으로 탐지합니다.<br>(예: 디자인 시스템에는 <code>#4A90E2</code> 색상만 사용해야 하는데, 실수로 <code>#4A90E3</code>을 사용한 경우 자동으로 탐지)  </p>
</li>
<li><p><strong>Version History Notifie (플러그인, API)</strong><br>: Figma 파일의 변경 이력을 추적하고 알림으로 전달합니다.  </p>
</li>
</ul>
<h3 id="⚡️배운-점"><strong>⚡️배운 점</strong></h3>
<p>이 경험을 통해 <strong>디자인 변경 사항의 즉각적인 공유가 개발 효율에 직결된다</strong>는 점을 깨달았습니다.<br>또한 단순히 개인 간의 커뮤니케이션 문제가 아니라, <strong>프로세스와 협업 도구 차원에서 해결해야 하는 문제</strong>임을 인식하게 되었습니다.  </p>
<hr>
<h2 id="📍백엔드-협업-과정에서의-api-문서-관리-문제">📍백엔드 협업 과정에서의 API 문서 관리 문제</h2>
<p>프로젝트 진행 중 <strong>API 문서와 실제 구현 간 불일치</strong>로 인해 불필요한 디버깅 시간이 자주 발생하였습니다.<br>예를 들어,  </p>
<ol>
<li>query 타입이 변경되었거나 응답 필드명이 바뀌었음에도 문서가 최신화되지 않은 경우  </li>
<li>문서는 업데이트되었지만 프론트엔드 코드가 여전히 이전 버전 요청 방식을 사용한 경우  </li>
<li>혹은 백엔드 구현이 문서 스펙과 다르게 이루어진 경우  </li>
</ol>
<p>프론트엔드에서 직접 디버깅을 통해 원인을 찾아야 했으며, 이러한 상황이 예상보다 자주 발생하면서 협업 효율에 부정적인 영향을 주었습니다.  </p>
<h3 id="💗-개선-하기"><strong>💗 개선 하기</strong></h3>
<p>이를 개선하기 위해 <strong>문서 관리 방식을 GitHub Wiki에서 OpenAPI(Swagger) 기반으로 전환</strong>하자는 제안을 하였습니다.<br>백엔드 팀 역시 관리되지 않는 문서로 인한 문제에 공감하였으나,  </p>
<ul>
<li>Swagger를 사용하지 않은 이유는 “코드가 복잡해지고 더러워진다”는 우려 때문이었으며  </li>
<li>대안으로 “테스트 코드 기반 문서 생성 도구”를 검토하였으나 학습 곡선 문제로 도입이 지연되면서 자연히 문서 관리도 어려워졌습니다  </li>
</ul>
<p>결론적으로는 <strong>다음 페이즈부터 Swagger를 도입</strong>하기로 합의하였습니다.<br>Swagger는 기존 GitHub Wiki보다 훨씬 간략하게 스펙을 명시할 수 있어 관리 포인트를 줄이면서도 협업 효율을 높일 수 있을것 같기 때문입니다.  </p>
<h3 id="⚡️배운-점-1"><strong>⚡️배운 점</strong></h3>
<p>이 논의를 통해 <strong>문서가 친절해질수록 관리 포인트가 늘어나고, 간략해질수록 관리 포인트는 줄지만 프론트엔드에서 일정 부분 추론이 필요하다</strong>는 점을 알게 되었습니다.<br>결국 프로젝트 상황과 팀 역량에 맞는 <strong>현실적인 균형점</strong>을 찾는 것이 중요하다는 교훈을 얻었습니다.  </p>
<hr>
<h2 id="📍-비개발자-직군과의-커뮤니케이션-방식-개선하기">📍 비개발자 직군과의 커뮤니케이션 방식 개선하기</h2>
<p>비개발자 동료와 함께하는 회의에서 문제 상황을 설명할 때, 저는 주로 <strong>문제의 원인(Why)</strong> 에 초점을 맞춰 설명하였습니다.<br>하지만 원인을 설명하다 보면 자연스럽게 구현 방식까지 들어가게 되고, 이 과정에서 개발 용어 사용 빈도가 높아졌습니다.<br>이로 인해 비개발 직군 입장에서는 <strong>결론이 흐려지고 소외감을 느낄 수 있다는 점</strong>을 간과하였습니다.  </p>
<p>이러한 방식은 효율적이지 못할 뿐만 아니라, 상대방의 입장을 고려하지 못한 부적절한 커뮤니케이션 방식이라고 판단하였습니다.<br>따라서 <strong>‘왜(Why)’ 보다는 ‘어떻게(How)’에 초점을 맞추는 방식</strong>으로 접근하는 것이 바람직하다고 생각하게 되었습니다.  </p>
<h3 id="-💗-개선-방향">** 💗 개선 방향**</h3>
<ol>
<li><strong>문제 → 영향 → 선택지 구조</strong><ul>
<li>문제(Why) : 원인은 짧고 현상 위주로 설명합니다.  </li>
<li>영향(Effect) : 해당 문제가 프로젝트, 사용자, 일정에 어떤 영향을 미치는지 설명합니다.  </li>
<li>선택지(How) : 어떤 해결 방향(옵션)이 있는지를 중심으로 제안합니다.  </li>
</ul>
</li>
</ol>
<ol start="2">
<li><strong>소통의 레벨 조절</strong></li>
</ol>
<ul>
<li>디자이너·기획자와의 회의
: 사용자 경험, 일정, 우선순위, 업무 흐름에 초점을 맞춥니다.  </li>
<li>개발자 간 회의
: 구현 방식, 코드 레벨까지 상세히 논의합니다.  </li>
</ul>
<h3 id="💡-배운-점"><strong>💡 배운 점</strong></h3>
<p>이 경험을 통해, 커뮤니케이션에서 중요한 것은 <strong>상대방이 이해할 수 있는 방식으로 핵심을 전달하는 것</strong>임을 배웠습니다.<br>특히 비개발 직군과의 소통에서는 <strong>원인 설명보다 해결 방법과 영향에 집중</strong>해야 협업 효율과 신뢰를 높일 수 있다는 점을 깨달았습니다.  </p>
<hr>
<h1 id="🤯-정면으로-마주한-갈등-상황">🤯 정면으로 마주한 갈등 상황</h1>
<h2 id="상황-situation">상황 (Situation)</h2>
<p>리뷰위크 전까지 MVP를 최종 완성해야 했으나, 같은 파트 팀원의 일정이 지연되면서 전날 새벽까지 트러블슈팅과 기능 구현을 병행해야 하는 상황이 발생하였습니다.<br>사실 리뷰위크 2주 전부터 진행된 QA 과정에서도 해당 기능이 구현되지 않아 충분한 테스트가 어려웠고, 결국 일정이 연이어 밀리면서 마감 직전까지 이어진 사례였습니다.<br>또한 팀원의 일정 지연으로 인해, 원래 해당 팀원이 맡은 기능 중 일부를 제가 대신 처리해야 했습니다.  </p>
<h2 id="과제-task">과제 (Task)</h2>
<p>이 상황에서 제 과제는 단순히 마감을 맞추는 것뿐만 아니라, <strong>팀 내 일정 지연 문제의 원인을 파악하고 재발을 방지할 수 있는 협업 방식을 마련하는 것</strong>이었습니다.  </p>
<h2 id="나의-행동-action">나의 행동 (Action)</h2>
<ul>
<li>마감 직전에는 일단 감정을 누르고, 우선적으로 문제가 되는 기능을 제가 추가로 구현하며 팀의 마감 일정을 맞추는 데 집중했습니다.  </li>
<li>리뷰위크 이후에는 <strong>프론트엔드 파트 회고 방식을 활용하여 사전에 문항을 준비하고, 팀원과 함께 일정 지연이 발생한 원인에 대해 논의</strong>했습니다.  (<a href="https://reinvented-soprano-16e.notion.site/262741bdef4a80528944f6d69aac26bc?pvs=73">준비했던 회고 문항 바로가기</a>)</li>
<li>그 과정에서 팀원의 일정이 다른 스케줄과 충돌하고, 업무 우선순위 산출 방식이 저와 달랐다는 점을 이해할 수 있었습니다.</li>
<li>개선 방안으로는 <strong>QA 주간·리뷰 주간 등 마일스톤 설정</strong>, <strong>기능 머지 마감일(T-2일) 규칙화</strong>, <strong>QA/버그 픽스 전용일(T-1일) 운영</strong>을 제안하였고, 팀에서 합의 후 실제로 적용했습니다.  </li>
<li>또한 파트 회의에서 업무 우선순위를 함께 정하는 방식을 도입하여 일정 관리의 투명성을 높였습니다.  </li>
</ul>
<h2 id="결과-result">결과 (Result)</h2>
<p>이러한 과정을 통해 <strong>일정 지연 문제가 구조적으로 개선</strong>되었고, 팀 전체의 협업 방식이 한층 더 명확해졌습니다.<br>무엇보다도 갈등 상황에서는 감정적인 대응보다는 <strong>상대방의 상황을 이해하고, 제도적 개선책으로 풀어내는 접근이 효과적</strong>이라는 점을 배웠습니다.  </p>
<hr>
<h1 id="기여한-점-스스로-칭찬-할-점">기여한 점, 스스로 칭찬 할 점</h1>
<ul>
<li><p><a href="https://velog.io/@fromjs_toyou/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EA%B8%B0%EC%88%A0-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0-SSE-%EC%95%8C%EB%A6%BC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">기존의 풀링 방식서의 트래픽문제를 개선하기 위해, SSE 방식으로의 전환을 생각하고 백엔드 팀원을 설득하여 최종 MVP에 도입</a>하였습니다.그 결과 알림 반응 속도는 빨라지고 트래픽은 줄었습니다. 이를 통해 상황에 맞는 기술을 근거로 선택하고 문서화해 공유하는 방법을 배운 소중한 경험이 되었습니다. </p>
</li>
<li><p>프로젝트 초기에 환경 설정, 배포 파이프라인, 공통 유틸 작업 등 내 담당 과업 외의 영역에도 기여하였습니다. 이런 기반 작업 덕분에 다른 팀원들이 기능 구현에 더 집중할 수 있었던 점이 보람있었습니다.</p>
</li>
</ul>
<hr>
<ul>
<li>이 활동은 <a href="https://ject.kr/">젝트</a>에서 진행한 프로젝트입니다  <img src="https://velog.velcdn.com/images/fromjs_toyou/post/8994998f-1af4-448e-ba70-d06668b34ea6/image.svg" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Cookie]]></title>
            <link>https://velog.io/@fromjs_toyou/Cookie</link>
            <guid>https://velog.io/@fromjs_toyou/Cookie</guid>
            <pubDate>Mon, 18 Aug 2025 08:12:07 GMT</pubDate>
            <description><![CDATA[<h1 id="intro">intro</h1>
<p>Next.js의 서버환경에서도 토큰에 접근할 수있도록 하기 위해 쿠키를 사용하게되었다. 유저기능을 온전히 도맡아 구현하는 것이라 우여곡절이 있었지만, 대부분의 문제는 내가 쿠키에 대해서 잘 모른다는 것에 기인했다. 다음번에 구현할 때에는 무지에서 오는 삽질이 줄어들도록 공부한점을 기록해본다.</p>
<h1 id="🍪-쿠키란">🍪 쿠키란?</h1>
<ul>
<li>HTTP 쿠키란 서버가 사용자의 브라우저에 전송하는 작은 데이터 조각이다. 하나에 최대 4KB 까지 저장 할 수 있다.</li>
<li>하나의 클라이언트에 최대 300개, 도메인당 20개 제한이 있었다(과거 브라우저 기준). 현재는 브라우저마다 훨씬 완화되어 도메인당 수백 개까지 저장 가능하다.</li>
<li>브라우저는 동일한 서버에 요청을 보낼 때 저장된 데이터를 요청 헤더에 동봉하여 전송한다.</li>
<li>과거에는 클라이언트 데이터를 저장할 때 쓰였으나, 매 요청 마다 쿠키가 전송되기 때문에 오버헤드가 생겨서 최근에는 스토리지에 담긴다.</li>
<li>주로 로그인상태를 유지하는 세션관리, 혹은 사용자가 임의로 세팅한 옵션이나 테마를 저장하는데 쓰인다.<ul>
<li>유저인증에 쓰이는 JWT의 경우 Refresh Token을 쿠키에 담아 보내는 경우가 대부분이다. httpOnly, Secure 속성도 추가해서</li>
</ul>
</li>
<li>클라이언트 서버모델에서는 서버가 클라이언트의 요청에 응답할때, Set-cookie라는 응답 헤더에 브라우저가 수신해야할 쿠키정보를 명시한다.</li>
</ul>
<hr>
<h2 id="🍪--set-cookie">🍪  Set-cookie</h2>
<p>서버에서 온 응답헤더에 set-cookie가 포함되면 브라우저에서는 해당 필드에 있던 데이터를 저장한다.</p>
<p>Set-Cookie 응답헤더에는 하나의 쿠키만 담을 수 있다.따라서 여러개의 쿠키를 보내려면 아래와 같이 보내면 된다.</p>
<pre><code class="language-plain">Set-Cookie: &lt;이름&gt;=&lt;값&gt;
Set-Cookie: &lt;이름&gt;=&lt;값&gt;
Set-Cookie: &lt;이름&gt;=&lt;값&gt;</code></pre>
<p>쿠키를 받은 브라우저는 해당 쿠키를 클라이언트 컴퓨터의 하드디스크에 저장한다. 그리고 동일한 서버에 요청을 보낼 때 저장해놓은 쿠키를 Cookie라는 요청헤더에 싣어서 보낸다.</p>
<blockquote>
<p>HTTP 요청 헤더에서
<code>Cookie: &lt;이름&gt;=&lt;값&gt;; &lt;이름&gt;=&lt;값&gt;; &lt;이름&gt;=&lt;값&gt;</code>
이렇게 보내진 쿠키를 볼 수 있다. </p>
</blockquote>
<p>📌 서버에게 Set-Cookie헤더를 통해 브라우저로 쿠키를 보내는 것은 일회성 작업이다.
📌 브라우저에게 Cookie 헤더를 통해 서버로 쿠키를 돌려보내는 것은 일정시간 반복해서 수행되는 작업이다.</p>
<p>(쿠키를 들려 보내는 작업은 브라우저라는 HTTP 클라이언트만 해주는 독특한 작업!)</p>
<hr>
<h3 id="set-cookie-옵션들">Set-Cookie 옵션들</h3>
<h4 id="✏️-쿠키의-유효기간-expires종료시점">✏️ 쿠키의 유효기간 (<code>Expires=종료시점</code>)</h4>
<ul>
<li><strong>유효기간이 명시되지 않은 쿠키를 세션쿠키</strong>라고 부르며, 브라우저의 세션이 종료될 때 함께 만료된다.브라우저의 탭을 닫으면 서버가 보낸 쿠키는 만료된다</li>
<li><strong>유효기간이 명시된 쿠키를 영속쿠키(Permanent Cookie)</strong> 라고 부르며 세션과 무방하게 특정 기간, 특정 시점 까지 유효하다. 유효기간을 명시하려면, Expires속성이나 Max-Age 속성을 명시한다.</li>
</ul>
<pre><code>Set-Cookie: &lt;쿠키 이름&gt;=&lt;쿠키 값&gt;; Expires=종료 시점
Set-Cookie: &lt;쿠키 이름&gt;=&lt;쿠키 값&gt;; Max-Age=유효 기간</code></pre><h4 id="✏️-쿠키의-적용범위1-domain도메인">✏️ 쿠키의 적용범위1 (<code>Domain=도메인</code>)</h4>
<p>Domain 속성을 명시하면 서브 도메인까지 포함하여 쿠키를 돌려보낼 수 있다.</p>
<p>Domain 속성을 test.com으로 설정하면 브라우저는 a.test.com으로 부터 받은 쿠키를, b.test.com으로도 보내게 된다. 그러므로 a.test.com과 b.test.com이 쿠키를 공유하는 효과가 발생한다.</p>
<h4 id="✏️-쿠키의-적용범위2-path경로">✏️ 쿠키의 적용범위2 (<code>Path=경로</code>)</h4>
<p>Path 속성을 명시하면 쿠키의 범위를 해당 도메인의 특정 경로로 쿠키의 범위를 축소시킬 수 있다.</p>
<p>Path 속성이 /users라고 설정되어 있는 쿠키는, 브라우저가 /users를 포함한 하위 경로로 요청을 할 때만 서버로 돌려 보낸다.</p>
<blockquote>
<p>📌 path를 입력하지 않으면 루트 경로로 자동 입력된다.
📌 쿠키의 범위를 좁게 잡을 수록 보안에는 좋다.</p>
</blockquote>
<h4 id="✏️-보안속성1-secure">✏️ 보안속성1 (<code>Secure</code>)</h4>
<p>https 프로토콜 에서만 서버로 쿠키를 돌려보낸다.</p>
<p>😭 하지만 보통 개발시에는 로컬  <strong>http환경</strong>에서 작업하기 때문에 팀에서 Secure 쿠키를 사용하기로 한다면, &quot;<code>cloudflare, aws</code>&quot; 등등 ssl인증서를 발급해주는 기관을 통해 ssl인증서를 발급받아 https를 적용시켜야 한다.</p>
<p>대신 로컬에서 테스트할때 ssl 인증키를 간단하게 발급받으려면 mkcert를 통해 ssl 인증서를 발급 받으면 된다. (이 과정은 다음 포스팅에서..)</p>
<h4 id="✏️-보안속성2-httponly">✏️ 보안속성2 (<code>HttpOnly</code>)</h4>
<p>브라우저에서 자바스크립트로 document.cookie 객체를 통해 접근할 수 업다. 서드파티에서 JS 코드가 쿠키에 접근하는 것을 제한할 수 잇다.</p>
<hr>
<h2 id="🍪-서드파티-쿠키란">🍪 서드파티 쿠키란?</h2>
<ul>
<li><p>쿠키에 설정된 <strong>도메인 (Domain)</strong>을 기준으로 퍼스트 파티 쿠키와 서드파티 쿠키로 기준을 정한다.</p>
</li>
<li><p><code>현재 사용자와 접속한 페이지 != 쿠키에 설정된 도메인</code>이라면 서드파티 쿠키로 분류된다.</p>
</li>
<li><p>서드파티쿠키는 주로 <code>타게팅 광고 목적</code>으로 사용된다. </p>
</li>
<li><p>✏️ 신발 쇼핑몰 사이트를 자주 방문하게 되면 쿠키가 저장되게 되고 구글 애드센스가 이를 가져가 다른 사이트에서의 배너 광고에서 신발 광고가 화면에 나타나는 것이 서드 파티 쿠키를 이용한 원리다</p>
</li>
</ul>
<ul>
<li><p>✅ 만약 로컬에서 개발 중인데 서버가 보낸 쿠키가 확인되지 않는다면 브라우저의 서드파티 쿠키 설정을 확인해보자.</p>
</li>
<li><p>특히 Safari의 경우  <code>ITP(Intelligent Tracking Prevention)</code> 정책으로 인해 쿠키가 차단되는 경우가 있다.</p>
</li>
</ul>
<blockquote>
<p><strong>ITP 정책</strong>이란? 
서드파티쿠키를 기본적으로 차단하여 사용자의 브라우징 데이터가 외부에 노출되지 않도록 함</p>
</blockquote>
<h2 id="⚡️samesite-정책">⚡️SameSite 정책</h2>
<ul>
<li>서드파티 쿠키의 보안적 문제를 해결하기 위해 만들어진 기술</li>
<li>XSRF, CSRF 공격을 방지 할 수 있다.</li>
<li>요청 도메인과 쿠키 정보내의 도메인이 다른 크로스 사이트로 전송하는 요청에 제한을 둘 수 있다.</li>
</ul>
<h3 id="samesite-옵션">SameSite 옵션</h3>
<h4 id="none"><code>None</code></h4>
<ul>
<li>크로스 사이트 요청에도 쿠키를 전송한다. 보안적적으로 Samesite 적요을 하지 않은 쿠키와 같다</li>
<li><code>SameSite=None</code> 이려면 반드시 <code>Secure</code> 설정된 쿠키여야 한다.</li>
</ul>
<h4 id="strict"><code>Strict</code></h4>
<ul>
<li>크로스사이트 요청에는 항상 전송되지 않음. 가장 보수적</li>
</ul>
<h4 id="lax"><code>Lax</code></h4>
<ul>
<li><p>Strict에 비해서 상대적으로 느슨한다. 몇가지 예외적인 요청에는 전송한다.</p>
<p><strong>예외기준</strong>
SameSite=Lax 일때 쿠키가 예외적으로 전송되는 경우는 다음과 같다. </p>
</li>
<li><p>사용자가<code>&lt;a&gt;</code>태크를 클릭해서 302 리다이텍트를 하거나, 자바스크립트 <code>window.location.replace</code> 등으로 인해 자동으로 이루어지는 이동에선 서드 파티 쿠키가 전송된다.</p>
</li>
<li><p>하지만 <code>&lt;iframe&gt;</code> 태그나 <code>&lt;img&gt;</code> 태그를 문서에 삽입함으로서 발생하는 http 요청은 전송을 제한한다. </p>
</li>
<li><p>또한 GET 요청에 대해선 쿠키가 전송되지만, POST 나 DELELTE 요청은 제한된다.참고로 SameSite 파라미터의 보안 속성을 서드 파티 쿠키에 한하는 것이며, 퍼스트 파티 쿠키는 Lax나 Strict여도 전송된다</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[실시간 통신 기술 비교하기 / SSE 알림 기능 구현하기]]></title>
            <link>https://velog.io/@fromjs_toyou/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EA%B8%B0%EC%88%A0-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0-SSE-%EC%95%8C%EB%A6%BC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@fromjs_toyou/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EA%B8%B0%EC%88%A0-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0-SSE-%EC%95%8C%EB%A6%BC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 15 Aug 2025 08:54:06 GMT</pubDate>
            <description><![CDATA[<h1 id="🌱-intro">🌱 intro</h1>
<p>팝업 예약 서비스 프로젝트 <code>스팟잇(Spot it)</code>을 개발하면서, 상황에 맞춰 사용자에게 알림을 보내는 기능을 구현해야 하는 과제가 주어졌습니다.
알림 기능이 MVP의 주요 기능에 포함되고 빠른 구현이 필요했기에, 팀 내부에서는 비교적 단순한 방식인 폴링을 사용하기로 결정했죠.</p>
<p>저 역시 폴링 방식이 구현 속도가 빠르다는 점에는 동의했지만, 리소스가 불필요하게 소모된다는 단점을 알고도 차선책을 택해야 한다는 점이 아쉬웠습니다.
그래서 다양한 실시간 통신 기술을 조사했고, 그 중 <strong>SSE(Server-Sent Events)</strong>를 활용한 구현을 제안하여 최종 MVP에 포함시키기까지의 과정을 정리해 보려 합니다.</p>
<h1 id="✏️-실시간-통신-기술-비교하기">✏️ 실시간 통신 기술 비교하기</h1>
<h2 id="polling--long-polling">Polling / Long Polling</h2>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/f01542eb-30c5-40f0-9d9c-3c5d02ca6102/image.webp" alt=""></p>
<p><strong>폴링(Polling)</strong>  </p>
<ul>
<li>클라이언트가 일정 주기마다 HTTP 요청을 보내 서버의 상태를 확인하는 방식입니다.  </li>
<li>데이터가 없어도 서버는 즉시 응답(예: <code>&#39;없음&#39;</code>)을 보냅니다.</li>
</ul>
<p><strong>롱폴링(Long Polling)</strong>  </p>
<ul>
<li>서버에서 연결을 일정 시간 유지하는 방식입니다.<ol>
<li>클라이언트가 서버로 HTTP 요청을 보냅니다.</li>
<li>서버는 전송할 데이터가 생길 때까지 요청을 보관합니다.</li>
<li>새 데이터가 생기면 서버가 응답을 보내고 연결을 종료합니다.</li>
<li>클라이언트는 응답을 받으면 즉시 새로운 요청을 보내 위 과정을 반복합니다.</li>
</ol>
</li>
</ul>
<hr>
<h3 id="장점">장점</h3>
<ul>
<li><strong>호환성</strong> : 이전 브라우저와 HTTP/1.1에서도 동작하며, 특별한 프로토콜 지원이 필요 없습니다.</li>
<li><strong>간단한 구현</strong> : 표준 HTTP 요청을 활용하므로 WebSocket 서버나 별도의 메시징 서버 없이도 기존 웹서버에서 처리할 수 있습니다.<br>초기 단계에서 실시간 기능을 빠르게 구현할 때 비용과 복잡성을 줄일 수 있습니다.</li>
</ul>
<hr>
<h3 id="단점">단점</h3>
<ul>
<li><strong>서버 리소스 부담</strong> : 롱폴링은 요청을 오래 유지하므로 서버 메모리와 연결 자원을 점유합니다.  동시에 많은 사용자가 연결하면 커넥션 한도에 빨리 도달할 수 있습니다.</li>
<li><strong>네트워크 오버헤드</strong> : 요청마다 HTTP 헤더·쿠키를 전송해야 하므로, 데이터가 작아도 불필요한 전송 비용이 발생합니다.</li>
<li><strong>타임아웃 및 재연결 문제</strong> : 일부 브라우저, 프록시, 로드밸런서에서 긴 HTTP 연결을 끊을 수 있습니다. 이 경우 재요청이 필요하며 순간적인 응답 지연이 생길 수 있습니다.</li>
<li><strong>폴링(Short Polling) 한정</strong> : 데이터가 없어도 주기적으로 요청하므로 네트워크 트래픽이 커지고 실시간성이 떨어집니다.</li>
</ul>
<blockquote>
<p><strong>오버헤드</strong> : 처리 시간, 메모리, 네트워크 대역폭 등이 추가로 소모되는 현상<br><strong>HTTP 오버헤드</strong> : 요청·응답마다 헤더, 쿠키 등의 메타데이터 전송과 파싱에 드는 비용</p>
</blockquote>
<h3 id="어떤-상황에-사용할까">어떤 상황에 사용할까?</h3>
<ul>
<li>WebSocket, SSE가 지원되지 않는 레거시 환경</li>
<li>실시간성이 절대적으로 중요하지 않고, 데이터 변경 빈도가 낮은 서비스</li>
</ul>
<hr>
<h2 id="websocket">WebSocket</h2>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/82aa4060-915b-4ebd-a01c-3358a7fe4b44/image.webp" alt=""></p>
<ul>
<li><p>웹소켓은 HTML5 표준 기술로, 사용자의 브라우저와 서버 사이의 동적인 양방향 연결 채널을 구성합니다.</p>
<ul>
<li>기존 HTTP 요청-응답 방식은 요청한 클라이언트에게만 응답이 가능했지만, ws프로토콜을 통해 웹소켓포트에 접속한 모든 클라이언트에게 이벤트 방식으로 응답합니다.</li>
</ul>
</li>
<li><p>웹소켓 프로토콜은 접속확립에 HTTP를 사용하지만 이후의 통신은 WebSocket 독자의 프로토콜로 이루어집니다.</p>
</li>
<li><p>헤더가 작아 오버헤드가 적습니다.</p>
</li>
<li><p>장시간 접속으로 전제로 하므로, 접속한 상태라면 클라이언트, 서버로부터 데이터 송신이 가능합니다.</p>
</li>
<li><p>데이터 송신, 수신마다 각각 커넥션을 맺지 않고 하나의 커넥션으로 데이터를 송수신할 수 있습니다.</p>
</li>
<li><p>통신시에 지정되는 URL은 <code>http://www.sample.com/</code> 과 같은 형식이 아니라 <code>ws://www.sample.com/</code> 과 같은 형식이 됩니다.</p>
</li>
</ul>
<hr>
<h3 id="장점-1">장점</h3>
<ul>
<li>최소한의 오버헤드와 지연시간으로 거의 즉각적인 통신이 가능합니다.</li>
<li>많은 수의 동시연결을 효율적으로 처리하므로 트래픽이 많은 애플리케이션에 맞게 확장이 가능합니다.</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>웹소켓 프로토콜을 처리하기위해 전이중 연결과 새로운 웹소켓서버가 필요합니다.</li>
</ul>
<hr>
<h3 id="어떤-상황에-필요할까">어떤 상황에 필요할까?</h3>
<ul>
<li>실시간으로 양방향 데이터 통신이 필요한 경우</li>
<li>많은 수의 동시접속자를 수용해야하는 경우</li>
<li>브라우저에서 TCP 기반의 통신으로 확장해야 하는 경우.</li>
</ul>
<hr>
<h2 id="sseserver-sent-event">SSE(Server Sent Event)</h2>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/79771185-139c-4e14-94c0-1235790cdfde/image.webp" alt=""></p>
<ul>
<li><p>SSE는 서버의 데이터를 실시간으로, 지속적으로 스트리밍하는 기술입니다.</p>
</li>
<li><p>서버가 하나의 긴 HTTP 연결을 통해 클라이언트에 데이터를 푸시합니다 새로운 정보가 있을 때 서버는 클라이언트에 데이터를 전송하기 때문에 클라이언트가 계속해서 요청을 보낼 필요가 없습니다.</p>
</li>
<li><p>HTML5 표준안이며, 어느정도 웹소켓의 역할을 할 수 있으면서 더 가볍습니다.</p>
</li>
<li><p>양방향이 아니기 때문에 요청시 ajax로 쉽게 이요할 수 있습니다.</p>
</li>
<li><p>재접속처리와 같은 저수준의 처리가 자동으로 지원됩니다.</p>
<ul>
<li>연결이 끊어지면 EventSource가 오류를 발생시키고 자동으로 다시 연결을 시도합니다.</li>
</ul>
</li>
<li><p>IE를 제외한 브라우저 대부분을 지원합니다.(Polyfill로 IE사용 가능)</p>
</li>
</ul>
<blockquote>
<p>SSE는 서버와 클라이언트 관계에서 <strong>Pub/Sub 패턴</strong>을 구현한 예시로 이해하면 쉽다!
<code>Publisher (발행자)</code>: Spring Boot 서버. 새로운 데이터(이벤트)가 생기면 이를 발행
<code>Subscriber (구독자)</code>: 클라이언트(브라우저). 서버의 특정 주소(토픽)를 구독하고 있다가, 발행된 데이터를 받아서 처리.
<code>Topic/Channel (토픽/채널)</code>: 클라이언트가 구독을 요청하는 SSE 엔드포인트 URL (/api/events)</p>
</blockquote>
<h3 id="어떤-상황에-필요할까-1">어떤 상황에 필요할까?</h3>
<ul>
<li>효율적인 단방향 통신이 필요한 경우</li>
<li>실시간 데이터 스트리밍에 HTTP를 사용하려는 경우</li>
</ul>
<hr>
<h1 id="✅-왜-sse-방식을-선택하였는가">✅ 왜 SSE 방식을 선택하였는가?</h1>
<p>구현하려는 알림 기능은 다음과 같은 특성을 가집니다.</p>
<ol>
<li><strong>서버 → 클라이언트 방향</strong>으로 데이터 전송이 필요</li>
<li>클라이언트에서 서버로의 데이터 전송은 불필요</li>
<li><strong>실시간성</strong>이 중요</li>
</ol>
<p>실시간 전송은 <strong>Socket</strong>을 통해서도 구현할 수 있습니다. 하지만 서비스 특성상 <strong>팝업 현장에서 모바일 브라우저로 접속하는 사용자가 대부분</strong>일 것으로 예상되었습니다.  </p>
<p>이 경우 Socket은  </p>
<ul>
<li>배터리 소모가 크고  </li>
<li>기본적으로 <strong>자동 재접속 기능을 지원하지 않는다</strong>는 단점이 있습니다.  </li>
</ul>
<p>반면, <strong>SSE(Server-Sent Events)</strong>는  </p>
<ul>
<li>단방향 전송에 특화되어 있어 리소스 소모가 적고  </li>
<li>연결이 끊겨도 <strong>기본적으로 3초마다 자동 재접속</strong>을 지원합니다.  </li>
</ul>
<p>따라서 서비스 환경과 요구사항을 고려했을 때, <strong>SSE가 보다 적합한 방식</strong>이라고 판단하여 선택하였습니다.</p>
<hr>
<h1 id="🚀-클라이언트측-sse-구현">🚀 클라이언트측 SSE 구현</h1>
<ul>
<li>브라우저는 <code>EventSource</code>를 통해 서버와 <strong>단방향 스트림</strong>을 열고, 서버는 이벤트가 생길 때마다 <code>text/event-stream</code> 포맷으로 푸시합니다.</li>
</ul>
<h3 id="연결-및-데이터-수신">연결 및 데이터 수신</h3>
<p>1️⃣ 알림 컴포넌트가 마운트 되었을 때, 단한번 서버로 연결을 요청합니다.</p>
<pre><code class="language-typescript">
const eventSourceRef = useRef&lt;EventSource | null&gt;(null);

useEffect(() =&gt; {
    if (!isLoggedIn) return;
    if (eventSourceRef.current) return;

    const eventSource = new EventSource(
      `${process.env.NEXT_PUBLIC_API_URL}/notifications/stream`,
      { withCredentials: true }
    );
    eventSourceRef.current = eventSource;

   ...

},[])
</code></pre>
<p>2️⃣  연결 직후 서버는 더미이벤트를 한번 보내 브라우저 onOpen 이벤트가 트리거 되도록 하여 연결 초기의 안정성을 확보합니다.</p>
<ul>
<li><p>브라우저에서는 초기 연결직후 서버가 아무 바이트도 보내지 않으면, 네트워크 탭에서 pending으로 보이지만 클라이언트에서는 연결이 활성화되었는지 판단이 어려웠습니다.</p>
</li>
<li><p>즉, <strong>TCP 연결은 성립</strong>했지만 <strong>SSE 스트림이 ‘활성’ 상태로 인식되지 않았기</strong> 때문입니다.</p>
</li>
</ul>
<blockquote>
<p>왜 이런가? </p>
<ul>
<li><strong>SSE는 라인 기반 스트리밍</strong>이며, 브라우저 구현체는 최소 한 라인의 SSE 프레임(주석 포함)을 <strong>수신해야 ‘연결 활성’ 상태로 전이</strong>하는 경우가 있습니다.</li>
<li>일부 <strong>중간 프록시/로드밸런서</strong>는 바디가 전혀 흘러가지 않는 <strong>idle 연결을 조기 종료</strong>하거나 버퍼링할 수 있습니다.</li>
<li>초기 한 바이트라도 보내야 <strong>버퍼 플러시</strong>가 일어나고, 브라우저가 파서를 기동하여 <code>onopen</code>/<code>readyState</code>를 정상화합니다.</li>
</ul>
</blockquote>
<p>3️⃣ 서버에서는 2가지 종류의 알림을 송신합니다.</p>
<ul>
<li><p><code>💗 하트비트 이벤트 : event.type === &#39;ping&#39;</code></p>
<ul>
<li>로드 밸런서 등의 타임아웃을 방지하기 위해 실제 보낼 데이터가 없더라도 주기적으로 의미 없는 데이터를 보내서 연결이 살아있음을 알립니다.</li>
</ul>
</li>
<li><p><code>📨 실제 알림 이벤트 : event.type === &#39;notification&#39;</code></p>
<ul>
<li>실제 UI에 업데이트 될 알림 메세지 입니다.<pre><code class="language-typescript">// 연결유지용
eventSource.addEventListener(&#39;ping&#39;, event =&gt;
console.log(&#39;💗 PING&#39;, event.data)
);
</code></pre>
</li>
</ul>
</li>
</ul>
<p>// 알림 수신용
eventSource.addEventListener(&#39;notification&#39;, event =&gt; {
      try {
        const notification = JSON.parse(event.data);
        addNotification(notification);
      } catch (error) {
        console.warn(&#39;알림 메세지 데이터 파싱실패&#39;, error);
      }
    });</p>
<pre><code>
### 연결 종료하기
브라우저에서 SSE 연결은 **EventSource**를 통해 이루어지며, 이 연결은 현재 열린 **페이지의 생명주기**에 종속됩니다. 따라서 사용자가 다른 페이지로 이동하거나, 새로고침하거나, 탭을 닫으면 브라우저는 해당 페이지와 관련된 리소스(JS 객체, 네트워크 연결 등)를 정리하며,  이 과정에서 SSE 연결도 함께 종료됩니다.

MPA에서는 페이지 이동 시 브라우저가 전체를 새로 로드하므로, 기존 SSE 연결이 자동으로 끊어집니다.반면 SPA에서는 클라이언트 라우팅으로 페이지 전환이 이루어져 JS 런타임이 유지되기 때문에, 한 번 생성한 SSE 연결이 계속 살아있게 됩니다.

스팟잇 서비스는 **React 기반의 SPA** 환경에서 구현되어 있습니다.SSE 연결은 알림을 담당하는 컴포넌트의 생명주기와 맞물려 동작하도록 구성해야 합니다.  
즉, 해당 컴포넌트가 페이지 이동으로 인해 화면에서 **unmount**되는 시점에   SSE 연결을 명시적으로 끊어주는 것이 중요합니다.  

만약 연결을 적절히 종료하지 않으면, 백그라운드에서 불필요한 연결이 유지되거나 메모리 누수가 발생할 수 있습니다.

```typescript
useEffect(() =&gt; {

  ... 

    eventSource.onerror = error =&gt; {
      if (eventSource.readyState === EventSource.CLOSED) {
        console.log(&#39;SSE 연결 정상 종료(페이지 이탈/새로고침)&#39;);
      } else {
        console.error(&#39;SSE 오류&#39;, error);
      }
    };

    // 새로고침시 정상 종료
    const handleUnload = () =&gt; eventSource.close();
    window.addEventListener(&#39;beforeunload&#39;, handleUnload);

    // 페이지 언마운트 시 연결 해제
    return () =&gt; {
      window.removeEventListener(&#39;beforeunload&#39;, handleUnload);
      eventSource.close();
      eventSourceRef.current = null;
      console.log(&#39;연결 해제&#39;);
    };
  }, []);
}

</code></pre><h4 id="구현-화면">구현 화면</h4>
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/984952ed-b966-466e-a04e-d0dfa286f4d4/image.png" width="500" />



<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://medium.com/@asharsaleem4/long-polling-vs-server-sent-events-vs-websockets-a-comprehensive-guide-fb27c8e610d0">롱 폴링 vs 서버 전송 이벤트 vs 웹 소켓: 종합 가이드</a></li>
<li><a href="https://dev.to/karanpratapsingh/system-design-long-polling-websockets-server-sent-events-sse-1hip">시스템 설계: 롱 폴링, 웹 소켓, 서버 전송 이벤트(SSE)</a></li>
<li><a href="https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-Polling-Long-Polling-Server-Sent-Event-WebSocket-%EC%9A%94%EC%95%BD-%EC%A0%95%EB%A6%AC">서버의 event를 클라이언트로 보내는 4가지 방법</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 커리 아니고 커링 ]]></title>
            <link>https://velog.io/@fromjs_toyou/JavaScript-Curring-Pattern</link>
            <guid>https://velog.io/@fromjs_toyou/JavaScript-Curring-Pattern</guid>
            <pubDate>Tue, 22 Jul 2025 05:53:27 GMT</pubDate>
            <description><![CDATA[<h1 id="javascript-디자인-패턴--커링패턴">Javascript 디자인 패턴 : 커링패턴</h1>
<p>여러개의 입력을 받는 함수를 한개의입력만 받는 여러개의 함수로 변환하는 것이다. 유연한 함수를 선언해 재사용성을 향상시킬 수 있지만, 성능이슈가 발생할 수 있다.</p>
<p>커링패턴은 함수영 프로그래밍에서 자주 사용되는 기법 중 하나인데 하스켈이나 스칼라 같은 함수형 프로그래밍 언어에는 기본적으로 내장되어 있다고 한다. 자바스크립트의 유연한 문법으로 이를 구현할 수 있다.</p>
<h3 id="장점">장점</h3>
<ul>
<li>일부 매개변수를 미리 설정하고 나머지 매개변수에 대해서는 나중에 처리함으로써 함수의 재사용성이 높아짐</li>
<li>크고 복잡한 함수를 작고 관리하기 쉬운 단위로 분할<h3 id="단점">단점</h3>
</li>
<li>매개 변수가 많아질 수록 추가적인 함수호출과 메모리할당을 발생시켜 성능이슈가 발생</li>
<li>함수 호출 구조가 복잡해지고 여러단계로 나뉘어 있어서 추적이 더 어려워질 수 있음.</li>
</ul>
<h3 id="커링함수-예시">커링함수 예시</h3>
<pre><code class="language-javascript">const applyDiscount = (할인율) =&gt; (가격) =&gt; 가격 - 가격 * 할인율

// 1. 변동성이 낮은 인자를 우선으로 받고, 높은 인자는 뒷순서로 받는 것이 좋다.
// 할인율이 0.1이라는 것을 기억하는 클로저
const tenPercentDiscount = applyDiscount(0.1)
// 두번째 인자를 나중에 전달하여 함수를 지연실행
const discountedPrice = tenPercentDiscount(1000)
console.log(discountedPrice)

// 2. 연속해서 호출 가능
const directlyDiscountedPrice = applyDiscount(0.1)(1000)
console.log(directlyDiscountedPrice)
</code></pre>
<h2 id="활용하기">활용하기</h2>
<h3 id="이벤트-핸들러-간소화">이벤트 핸들러 간소화</h3>
<p>더 이상 <code>onClick={(e) =&gt; handleItemClick(itemId)}</code> 와 같이 선언하지 않아도 된다.</p>
<pre><code class="language-jsx">function Component() {

    const handleItemClick = itemId =&gt; (event) =&gt; {
        console.log(itemId, event.target.value)
    }

    return (
        &lt;div&gt;
            {[&#39;item1&#39;, &#39;item2&#39;, &#39;item3&#39;].map(itemId =&gt; (
                &lt;button key={itemId} onClick={handleItemClick(itemId)}&gt;
                    Click {itemId}
                &lt;/button&gt;
            ))}
        &lt;/div&gt;
    )
}</code></pre>
<h3 id="api-호출처리">api 호출처리</h3>
<pre><code class="language-javascript">const createEndPoint = (base) =&gt; endpoint =&gt; params =&gt; {
    const query = new URLSearchParams(params).toString();
    return `${base}/${endpoint}?${query}`
}

// 기본 api url
const baseAPI = createEndPoint(&#39;https://example.com/api&#39;);

// 엔드포인트 확장
const fetchUser = baseAPI(&#39;users&#39;);
const fetchPost= baseAPI(&#39;posts&#39;);


const userAPIPath = fetchUser({id: 1, name: &#39;lee&#39;});</code></pre>
<h3 id="고차-컴포넌트">고차 컴포넌트</h3>
<pre><code class="language-javascript">// 커링 함수를 이용한 HOC 선언
function withLoading(WrappedComponent) {
    return function({ isLoading, ...rest }) {
        if (isLoading) {
            return &lt;div&gt;Loading...&lt;/div&gt;;
        } else {
            return &lt;WrappedComponent {...rest} /&gt;;
        }
    };
}

// 예시 컴포넌트 선언
function MyComponent({ data }) {
    return (
        &lt;div&gt;
            &lt;h1&gt;My Component&lt;/h1&gt;
            &lt;p&gt;{data}&lt;/p&gt;
        &lt;/div&gt;
    );
}

// 사용 예시
const MyComponentWithLoading = withLoading(MyComponent);

function App() {
    // 데이터를 가져오는 상태 시뮬레이션
    const [loading, setLoading] = useState(true);
    const [data, setData] = useState(null);


    return (
        &lt;MyComponentWithLoading isLoading={loading} data={data} /&gt;
    );
}</code></pre>
<h3 id="팩토리-패턴">팩토리 패턴</h3>
<pre><code class="language-jsx">// 커링 함수를 사용한 컴포넌트 생성 함수
const createComponent = (Component) =&gt; (properties) =&gt; {
    return &lt;Component {...properties} /&gt;;
};

// 커링 함수 사용하여 특정 컴포넌트 생성 함수 만들기
const createButton = createComponent(&quot;button&quot;); // HTML button 요소
const createLabel = createComponent(&quot;label&quot;); // HTML label 요소

// 개별 컴포넌트 생성
const BlueButton = () =&gt;
    createButton({ style: { color: &quot;blue&quot; }, children: &quot;Click me&quot; });
const LargeLabel = () =&gt;
    createLabel({ style: { fontSize: &quot;large&quot; }, children: &quot;Label text&quot; });

// 사용 예시
function App() {
    return (
        &lt;div&gt;
            &lt;BlueButton /&gt;
            &lt;LargeLabel /&gt;
        &lt;/div&gt;
    );
}</code></pre>
<h3 id="validator">validator</h3>
<p>유효성 검사를 하기 위해 필요가 값은 크게 두가지 인데</p>
<ul>
<li>유효한 범위를 결정지은 값 (1번)</li>
<li>실제 입력된 값 (2번)</li>
</ul>
<p>커링함수를 이용하여 1번이 결정된 함수를 미리 만들어 두고, 함수의 실행은 실제로 값이 입력된 시점에 이루어지도록 하는 방법이다.</p>
<pre><code class="language-tsx">// 커링함수를 사용한 유효성 검사 함수
const minLength = (min: number) =&gt; (value: string) =&gt; {
    return value.length &gt;= min ? undefined : `최소 ${min}글자 이상 입력해주세요.`;
}

const maxLength = (max: number) =&gt; (value: string) =&gt; {
    return value.length &lt;= max ? undefined : `최대 ${max}글자 이하로 입력해주세요.`;
}


// 컴포넌트를 사용하는 쪽에서 첫번째 인자를 전달.
&lt;TextField
        source=&quot;name&quot;
        label=&quot;이름&quot;
        validateArray={[minLength(2), maxLength(6)]}
/&gt;


// TextField 내부에서 useEffect를 통해 값이 입력될때
// 모든 유효성 검사가 이루어지게끔 함.
React.useEffect(() =&gt; {
    const errors = validateArray.map((validateFn: any, _i: number) =&gt; {
      if(value[source] !== undefined){
        return validateFn(value[source]);
      }
    })

    const filteredErrors = errors.find((error: any) =&gt; error !== undefined);
    setError(filteredErrors);

  }, [value[source]]);

</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[💫 돕당 프로젝트 회고]]></title>
            <link>https://velog.io/@fromjs_toyou/%EB%8F%95%EB%8B%B9-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@fromjs_toyou/%EB%8F%95%EB%8B%B9-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 12 May 2025 07:31:54 GMT</pubDate>
            <description><![CDATA[<p>백엔드와 함께하는 최종 프로젝트의 배포와 발표가 마무리 되었다.
이렇게 달려야 했던 프로젝트가 있었나 싶을정도로 벅찬 일정안에 무사히 모든 기능 구현과 테스트가 완료되어 기쁜 마음이다. 많은 것을 배우고 경험한 프로젝트이니 회고로 기록해보자. </p>
<blockquote>
<p>프로젝트 결과물 <strong>돕당 (DOPDANG)</strong> 
🚀 배포 주소 : <a href="https://www.dopdang.shop/">https://www.dopdang.shop/</a></p>
</blockquote>
<hr>
<h1 id="프로젝트-준비">프로젝트 준비</h1>
<h2 id="팀매칭">팀매칭</h2>
<p>이전의 두 프로젝트는 운영진이 임의로 결성한 팀원들과 함께 팀프로젝트를 진행했는데, 마지막 최종프로젝트는 프론트엔드 구성원간 자율적으로 팀을 구성한 후 백엔드 팀원을 뽑는 방식이였다. 나는 놀랍게도,, 이걸 몰랐다..! 😅</p>
<p>다행히도 자율 팀결성 공지가 있기전 몇몇 개발자분들이 합류제안을 주셔서 알수있었다. 결론적으로는 같이 스터디을 했던 팀원들과 최종 프로젝트를 하게되었다. 초반에는 감사한 마음에 들떳기도 했지만, 더욱 열심히 참여해서 내 몫을 해내야겠다는 생각에 이내 마음이 바빠졌다.</p>
<h1 id="프로젝트-시작">프로젝트 시작</h1>
<h2 id="💡-기획">💡 기획</h2>
<h3 id="1️⃣-문제-파악">1️⃣ 문제 파악</h3>
<p>이번 프로젝트는 팀원들 모두 <strong>기술적으로 성장하고자 하는 의지</strong>가 강력했다. 백엔드 분들도 마찬가지셨는데, 백엔드에서 요구하는 기술사항(실시간통신, 동시성 문제 처리, 시간기반 로직 구현)에 맞추느라 아이디어가 한정되어 나오기도했다. </p>
<p>이를 모두 구현할 수 있는 서비스가 실시간 경매 서비스여서 초반에는 이에 맞춰 아이디어가 디벨롭 되었는데, 조사를 할수록 기존 서비스의 문제를 해결하기 보다는 <strong>바퀴를 다시 발명하고 있다</strong>는 느낌이 강하게 들었다.</p>
<p>그래서 다시 한발 떨어져서 고민해보았다. </p>
<p>&quot;<strong>기술스택에 집착하지 말고 내가 진짜 필요했던 게 뭘까?</strong> &quot; </p>
<p>펫트너 프로젝트에서 느꼈듯, 기술은 현실의 문제를 해결하기위한 도구이니까 기술에 매몰되지 말고 자유롭게 생각해보기로 했다.</p>
<p>이 당시 나는 코드리뷰에 대한 갈증에 목말라 있었다. 이게 잘짠 코드일까 항상 고민했고 팀원들에게 코드리뷰를 요청하기도했다. 이때 들었던 생각은 좀 더 전문적인 개발자에게 내 고민을 속시원히 털어놓고, 코드리뷰를 받을 수 있었으면 좋겠다는 것이였다. </p>
<p>여기에서 착안하여 <strong>전문가 매칭 플랫폼 &lt;돕당&gt;</strong>이 탄생했다.</p>
<h3 id="2️⃣-서비스-구체화">2️⃣ 서비스 구체화</h3>
<p>지금까지 만든 서비스 중, 가장 기획단계가 어렵고 복잡하였다고 단언할 수 있다. </p>
<p>먼저 돕당 서비스를 <code>도움이 필요한 사람(의뢰인)</code>과 <code>서비스제공자(전문가)</code>를 쉽게 연결해줄 수 있는 플랫폼으로 골자를 잡고 난뒤 이에 필요한 세부 기능사항과 유저플로우를 구체화했다.
돕당은 <code>서비스 요청</code>부터 <code>전문가 상담 및 견적서 작성</code> → <code>계약</code> → <code>구매</code> → <code>작업 진행</code> → <code>서비스완료 및 리뷰</code> 까지의 단계가** 아주 촘촘하고 유기적으로 연결**되어 있다. 프로젝트 진행과정 내내 여러 서비스 사용 사례를 가정해 보면서 여러번 수정을 거치며 빈 구멍을 촘촘히 메꾸려 노력했다. </p>
<p>서비스는 크게 다음과 같이 구성된다.
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/70371516-342d-4f7c-81d3-f9cfa7e1c677/image.png" alt=""></p>
<ul>
<li>🏪 <code>스토어</code> : 유/무형의 서비스와 상품을 사고 팔 수 있다.</li>
<li>👨‍💻<code>전문가</code> : 각 카테고리별 전문적인 능력을 가진 전문가의 프로필을 볼 수 있고 서비스를 요청할 수 있다.</li>
<li>📁 <code>프로젝트</code> : 돕당의 의뢰인들이 만든 요청서를 볼 수 있다. </li>
<li>💬 <code>실시간 채팅</code>: 의뢰인과 전문가가 만나 상담할 수 있으며, 대화를 통해 가격을 흥정할 수 있다.</li>
<li>💰 <code>결제</code> : 스토어 상품과 전문가와의 계약금액을 정산할 수 있다.</li>
<li>🗝️ <code>마이페이지</code> : 유저정보, 전문가 포트폴리오, 현재 프로젝트의 진행도를 확인하고 관리할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/b6a922bf-8ee4-4ddd-b53e-e45a37251d63/image.png" alt=""></p>
<p>위 사진에서도 볼 수 있듯, 의뢰인과 전문가. 두 역할이 있다는 말은 의뢰진행이 시작될 수 있는 갈래가 2가지가 있다는 말이다..! 
<strong>두가지 시작점을 가지면서도 <code>요청서 - 견적서 - 계약서</code>로 연결되는 플로우를 동일하게 가져가게 할 수 있도록</strong> 하느라 고민을 많이 했다.</p>
<blockquote>
<p>이해를 돕는 <a href="https://reinvented-soprano-16e.notion.site/Dopdang-1d6741bdef4a8049b40cfd013a54cc11">돕당 프로젝트 소개서</a>도 작성했다!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/62350296-318c-4a73-adbc-662db9a4fe7d/image.png" alt=""></p>
<p>꽤나 복잡한 기능 구조도가 만들어졌다..! </p>
<h2 id="🎨-디자인">🎨 디자인</h2>
<p>프론트에서는 이전 프로젝트의 반복적으로 만들어지는 컴포넌트에 대한 문제점을 해결하고자 했다.팀장님께서 아토믹 디자인 패턴의 사용을 제안했고 팀원 모두가 이를 숙지한 뒤 프로젝트에 적용하였다.이에 관환 경험은 <a href="">여기</a>에 자세히 남겨두었다..! </p>
<p>디자인은 피그마를 사용하여 진행했는데, 이미 디자인에 익숙한 팀원분이 있어서 이전 프로젝트보다 훨씬 수월하게 할 수 있었다.
디자인 시스템을 구축하고 이를 바탕으로 공통 컴포넌트를 만든뒤 페이지에 적용하는 방식으로 작업햇는데, 페이지 수가 많은 것에 비해 빠르게 작업을 할 수 있었다.</p>
<table>
<thead>
<tr>
<th>돕당 디자인 시스템</th>
<th>공통 컴포넌트</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/fromjs_toyou/post/3945eee1-029c-4039-b111-30db9ad57b67/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/fromjs_toyou/post/6b19c355-9d6c-4183-afae-d7d113804052/image.png" alt=""></td>
</tr>
</tbody></table>
<p>이번 디자인에서 특히 고려된 점은 <strong>명도 웹접근성 가이드</strong>를 따랐다는 것이다.</p>
<blockquote>
<p>웹 접근성은 장애인이나 고령자는 물론 어떤 사용자들도 웹사이트에서 제공하는 정보를 비장애인과 동등하게 접근하고 활용할 수 있도록 하자는 개념이다. 
저시력자, 고령자 등도 인식할 수 있도록 콘텐츠와 배경 간의 명도 대비는 4:5:1 이상이어야 한다.
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/d01332bc-041e-4746-805b-f990e16f3674/image.png" alt=""></p>
</blockquote>
<p>크롬의 LightHouse에서도 명도대비(Contrast Ratio)에 대한 지표를 제공하고 있고, 이는 <code>WCAG(Web Content Accessibility Guidelines)</code>의  기준에 맞는지 검사하는 지표 중 하나이다. LightHouse의 &quot;Accessibility&quot;카테고리안에 있고 문제가 있다면 아래사진 처럼 <code>&quot;Background and foreground colors do not have a sufficient contrast ratio.&quot;</code>란 문구를 띄워준다. (즉, 배경과 텍스트의 글씨의 명도대비가 알아볼 수 있을 정도로 뚜렷해야함!)</p>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/7410a191-61ef-4d0c-8ce5-22a239002f08/image.png" alt=""></p>
<p>이번 경험을 통해 명도대비같은 디자인적인 접근성요소를 신경써야겠다고 마음먹게되었다. 그 이유는 프론트개발자의 일이 단순히 예쁜 UI가 아니라 <strong>실제로 사용가능한 웹을 만드는 일에</strong> 직결되기 때문이다. 초반에는 이러한 부분이 디자인적인 요소로만 느껴질 수 있지만 조금만 더 익숙해지면 색상선택, 상태변화(hover, focus)등에서도 &quot;접근성을 위한 선택&quot;을 하게된다. 예를들면 hover상에 일때 단순히 배경색 변화만 있다면 색각이상자는 차이를 느끼지 못할 수 있다. border, font-weight 과 같은 보조적 요소를 함께 넣으면 더 나은 웹을 만들 수 있다. </p>
<h2 id="개발">개발</h2>
<p>개발 관련하여 구현하면서 느꼈던 점은 아래 글에 싣을 예정이다.</p>
<ul>
<li>Next.js 미들웨어 처리하기</li>
<li>아토믹 디자인 패턴 적용기</li>
<li>Axios대신 fetch 사용하기( API Builder 패턴)</li>
<li>실시간 채팅, 결제 기능 구현하기</li>
</ul>
<hr>
<h1 id="keep-problem-try-emotion-회고로-정리하기">Keep, Problem, Try, Emotion 회고로 정리하기</h1>
<h2 id="1️⃣--keep--잘했던-점-앞으로의-프로젝트에도-유지해야할-사항">1️⃣  KEEP : 잘했던 점, 앞으로의 프로젝트에도 유지해야할 사항</h2>
<h3 id="기술적으로-도전하기">기술적으로 도전하기</h3>
<p><code>jejumonth프로젝트</code>에서 남긴 과제로 &quot;기술적으로 도전하기&quot;라는 항목이 있었다. 이번 돕당 프로젝트를 통해 기존의 CRUD 말고도 다양한 구현과제가 주어져서 힘들기도 했지만 뿌듯했던 기억이다. 돕당에서는 전문가페이지와 스토어 페이지를 비롯하여 핵심기능이라고 할수있는 <strong>실시간 채팅기능</strong>과 <strong>결제기능</strong>을 맡게되었다.  </p>
<p>실시간 채팅의 경우 HTTP를 이용한 통신과 달리 웹소켓을 사용하여 이루어지는 양방향 통신이기에 이에 대한 공부를 선행한뒤 구현을 시작했다. 프론트측에서는 <code>socket.io-client</code> 혹은 <code>STOMP</code>를 사용하는 방법 2가지가 있었는데 백엔드와의 호환성을 위해 <code>STOMP</code>와 <code>SockJs</code>를 활용하여 구현했다. 실시간채팅의 경우 클라이언트측의 코드 구현은 복잡한 편이 아니었다고 생각한다. 오히려 백엔드측 redis에서 실시간성 관련 오류가 자주 발생해서 이를 해결하느라 고생했다. 
생각보다 어려웠던 점은 채팅방 페이지를 구성하기 위한 정보들이 다양했는데( 요청서 정보, 채팅방 목록 정보, 견적서 정보, 전문가 정보 등) 백엔드 api 를 설계할때 필요한 데이터가 충분히 정리되어있지 않아서 이를 조율하고 커뮤니케이션하느라 일정이 딜레이되었다. 백엔드가 API 설계하기전 팀장님께서 Figma페이지를 확인후 누락되는 정보가 없도록 해달라고 부탁하셨는데 앞으로의 일정에 꼭 필요한 말이었다..! 그럼에도 불구하고 앞으로는 <strong>복잡한 페이지 설계시에는 프론트와 백엔드가 의논하는 시간을 충분히 가진 후 구현에 착수</strong>해야겠다는 생각이 들었다.</p>
<p>두번째로 (1)의뢰비용과 (2)스토어 상품비용의 청구를 위해 TossPayment를 활용한 결제기능을 구현했다. 담당 백엔드 분이 이미 결제기능을 구현해본 경험이 있으셔서 구현 과정이 생각보다 어렵지는 않았지만, 최종 결제 완료 페이지를 하나의 경로에서 처리하려다 tosspayment-api 구조상 그게 불가능하다는 것을 뒤늦게 알게되어 중간에 삽질하느라 시간을 꽤 썼었다. (리디렉션이 안되는 이슈..) 
이와는 별개로 결제창을 띄우기 부터 결제완료까지의 과정은 <a href="https://docs.tosspayments.com/guides/v2/get-started">tosspayment 문서</a>에 친절하게 설명되어 있어 보면서 감탄했던 기억이 있다..!  구현하면서 신경썼던 점은 사용자의 결제관련 정보를 클라이언트 측에 노출하지 않기위해 SSR를 사용해 보안상 안전하게 만드려고 했다. 때문에 데이터 패칭 레이어, 페이지 랜더링 레이어, 결제 로직 레이어로 분리해서 결제페이지를 완성했다.</p>
<p>이외에도 Next.js를 사용하면서 middleware를 사용해 URL 접근 제한을 구현한것, axios를 사용하지 않고 스터디에서 배웠던 API Builder를 활용해서 처리한것, 프로젝트에서 Storybook을 적용한 것 등 배운 점이 많아서 더 뿌듯했다..!</p>
<h2 id="2️⃣-problem--아쉬웠던-점-앞으로-개선해야할-사항">2️⃣ PROBLEM : 아쉬웠던 점, 앞으로 개선해야할 사항</h2>
<h3 id="일정-계산-능력-기르기">일정 계산 능력 기르기</h3>
<p>프로젝트 진행기간 동안 일정을 보수적으로 잡았다고 생각했는데 생각보다 여유가 없어서 당황했다..! 약 3주+a의 기간동안 실질적으로 개발할 수 있는 시간이 15일 내외 정도였다. 이 기간동안 3명이서 1000개 넘는 커밋을 쌓았으니 꽤나 달렸다고 볼 수 있겠다. </p>
<p>개발일정 초반에는 공통 컴포넌트를 만들고 이후 남은 일주일간 나머지 기능을 처리하기로 일정을 잡았다.
먼저 약 3일간비교적 빠르게 구현할 수 있는 스토어와 전문가 페이지를 만들었다. 등록과정을 팀원분께서 맡아주셔서(압도적 감사🙏) 이후 4일간 결제와 채팅 페이지 구현에 집중할 수 있었다.  나머지 2일간의 QA기간동안 잔 버그를 고치고 최종 발표영상녹화까지 마무리했다. </p>
<p>바쁜 일정이였고, 생각보다 시간이 남는 날에는 미리미리 이후 일을 해두었지만 절대 여유있는 일정은 아니였다..!
팀장님이 일정 산출을 꾸준히 해주시고 중간중간 푸쉬해주지 않았다면 자칫 더 늦어졌을 수도 있다고 생각한다. 
일정을 산출 할때는 구현할 기능을 더 상세히 쪼개고 이에 복잡도와 버퍼시간을 더해 시간을 잡아야겠다. 그리고 반드시 이 기간안에는 끝낼 수 있다는 마음가짐은 필수다 👨‍💻</p>
<h2 id="3️⃣-try--다음-프로젝트에-적용해볼-action-item">3️⃣ TRY : 다음 프로젝트에 적용해볼 Action Item</h2>
<h3 id="보다-독립적인-프론트엔드로-도약하기">보다 독립적인 프론트엔드로 도약하기</h3>
<p>이번 프로젝트를 하며 더욱 여실히 느꼈던 점은 &quot;프론트엔드 독립성 확보&quot;의 중요성이였다. 보통은 백엔드가 api를 만들고 나서야 프론트작업이 시작할 수 있다. 하지만 독립적으로 API 없이도 개발이 가능한 환경이 구성된다면 프론트는 백엔드 작업이 끝나기 전에 UI구현, 상태관리, 테스트를 마칠 수 있을 것이며 이는 팀 전체 개발 사이클이 빨라지는데 도움을 줄 것이다.  특히 서버 상태 로딩/에러처리같은 경우 테스팅하기가 까다로운데 이를 처리하면서 독립적인 프론트엔드 개발에 대한 니즈가 생기게 되었다. 다음 프로젝트에서는 MSW와 같은 모킹라이브러리를 활용해서 API에 비교적 덜 끌려다니며, 더 안정적이고 주도적으로 개발하고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LeetCode : Longest Valid Parentheses / 문제풀이와 최적화과정]]></title>
            <link>https://velog.io/@fromjs_toyou/LeetCode-Longest-Valid-Parentheses-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4%EC%99%80-%EC%B5%9C%EC%A0%81%ED%99%94%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@fromjs_toyou/LeetCode-Longest-Valid-Parentheses-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4%EC%99%80-%EC%B5%9C%EC%A0%81%ED%99%94%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Mon, 21 Apr 2025 12:42:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://leetcode.com/problems/longest-valid-parentheses/description/">Longest Valid Parentheses 문제 바로가기 🚀 </a></p>
</blockquote>
<h2 id="문제-설명">문제 설명</h2>
<p>올바른 괄호 찾기 문제의 응용버전으로 주어진 괄호로 이루어진 문자열중, 올바르게 연결된 괄호의 <strong>최대길이</strong>를 반환하는 문제입니다.</p>
<ul>
<li>괄호는 소괄호로만 이루어져 있습니다.</li>
</ul>
<table>
<thead>
<tr>
<th>💬 input</th>
<th>✅ output</th>
</tr>
</thead>
<tbody><tr>
<td><code>(()</code></td>
<td>2</td>
</tr>
<tr>
<td>)()())</td>
<td>4</td>
</tr>
<tr>
<td>()(()</td>
<td>(🔥주의 4가 아니라 )2</td>
</tr>
</tbody></table>
<hr>
<h3 id="💡-접근-및-1차-풀이">💡 접근 및 1차 풀이</h3>
<p>(1) 스택을 사용해서 올바른 괄호인지를 판별하고 
(2) 배열에 올바른 괄호인지를 저장하고 <code>(올바른 괄호이면 1, 올바르지 않은 괄호이면 0)</code>
(3) 배열을 훝으며 가장 긴 1의 길이를 리턴</p>
<p>괄호 판별문제는 전형적인 Stack을 사용한 문제기 때문에 큰 고민없이 1차 답안을 제출하였습니다.</p>
<pre><code class="language-javascript">/**
 * @param {string} : 괄호로 이루어진 문자열
 * @return {number} : 가장 긴 연속된 올바른 괄호의 길이
 *
 * example
 * ()(() : 4가 아니라 2
 * note : [ 1, 1, 0, 1, 1 ]
 */
var longestValidParentheses = function(s) {

    // (1) 올바른 괄호인지 판단하여 Note를 작성
    const note = new Array(s.length); // 올바른 괄호이면1, 올바르지 않은 괄호이면 0
    const stack = [];
    s.split(&#39;&#39;).forEach((paranth, index) =&gt; {

        if(paranth === &#39;)&#39; &amp;&amp; stack[stack.length-1]&amp;&amp;stack[stack.length-1][0] === &#39;(&#39;){
            const [popedParanth, popedIndex]= stack.pop();
            note[index] = 1;
            note[popedIndex] = 1;
        }else{
            stack.push([paranth,index])
        }
    })

    while(stack.length &gt;0){
        const [last, lastIndex] = stack.pop();
        note[lastIndex] = 0 ;
    }


    // (2) note에서 연속적으로 나타나틑 1의 가장 긴 길이를 리턴
    // 🍀 연속적인 올바른 수열의 길이?
    // 1 -&gt; 1 인경우 현재 길이에 +1
    // 0 -&gt; 1 인경우 현재길이를 초기화 하고 1

    let max = 0 ; // 최종적으로 반환해야 하는 값
    let curr = 0 ;
    note.forEach((num, index) =&gt; {
        if(index == 0 ){
            if(num === 1) {curr += 1;
                max = Math.max(max, curr);}
            return ;
        }

        if(num === 1){
            const lastNum = note[index-1];
            if(lastNum === 1){
                curr += 1;
                max = Math.max(max, curr);
                return ;
            }else if(lastNum === 0 ){
                curr= 1;
                max = Math.max(max, curr);
            }
        }
    })

    return max;
};</code></pre>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/ce566ca5-2c3b-4e8c-b65c-dbd3b230d950/image.png" alt=""></p>
<p>문제는 맞췄지만 처참한 효율성 점수에 충격을 받고 다시 고민을 해보았습니다.</p>
<h2 id="🔥-2차-풀이">🔥 2차 풀이</h2>
<p>이전에 풀이 방식은 크게 두가지 파트로 나누어져 있습니다.
(1) 올바른 괄호인지 판별하기
(2) <code>note</code>배열에서 가장 긴 연속적인 1의 길이 찾기</p>
<p>두번째 파트인 연속적인 1의 길이를 좀 더 효율적으로 찾는 방법을 없을까 고민해봅시다.
이전에는 배열을 순회하며 연속적으로 1이 나오는 최대길이를 업데이트 하는 방식을 사용했기 때문에 최대 O(N)의 시간복잡도를 가집니다.</p>
<p>그런데 note배열의 경우 아래와 같은 특징을 가집니다.</p>
<ul>
<li><code>[0,1,1,1,0,1,1,0,0,0,1,1]</code> 처럼 0과 1로만 이루어져 있다.</li>
<li>0인 부분은 굳이 훝지 않아도 된다.</li>
</ul>
<p>때문에 note 배열을 문자열로 합친뒤, 다시 0을 기준으로 split 해준다면 좀더 빠르게 1로 만 이루어진 문자열을 구할 수 있을 것이라 생각했습니다.</p>
<p>위의 코드에서 (2) 번 부분을 아래처럼 바꿀 수 있겠습니다.</p>
<pre><code class="language-javascript">    // note에서 연속적으로 나타나틑 1의 가장 긴 길이를 리턴
    // 0을 기준으로 자르고 남은 1로 이루어진 문자열 배열중 가장 긴 길이를 ㄹ반환.
    return note.join(&#39;&#39;).split(&#39;0&#39;).reduce((max, curr) =&gt; curr.length  &gt; max ? curr.length  : max, 0)
</code></pre>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/a4c322d1-4982-457b-ba82-26d489a7f38c/image.png" alt=""></p>
<p>이전에 비해 17ms에서 11ms로 조금 나아졌지만 더 나은 방법이 있을것 같습니다.</p>
<p>현재처럼 단계를 1,2 단계로 나누지 말고 단일루프내에서 길이를 구해보겠습니다. </p>
<h2 id="🚀-3차-풀이">🚀 3차 풀이</h2>
<h3 id="리팩토링-방향">리팩토링 방향</h3>
<ol>
<li>추가 메모리인 note 배열을 제거한다.</li>
<li>stack에 괄호가 아닌, index를 담는다.</li>
<li>문자열을 여러번 순회하지 않고 한번의 순회로 최대 길이를 계산한다.</li>
</ol>
<p>이전 코드와 달라지점은 pop을 하는 조건인데, 이전 코드에서는 괄호가 올바르게 짝을 이룰경우 괄호쌍을 pop 하였습니다.. 리팩토링된 코드에서는 올바르지 않은 쌍이라도 닫는 괄호일 경우 Pop을 일으키도록 설계하였습니다. </p>
<p>여기서 중요한 점은 무턱대고 pop을 하는것이 아닌 <strong>짝을 맞추는 도중 실패했을 때 새로운 유효구간을 정하는 용도로 pop 후 현재 index를 push</strong> 한다는 것입니다.</p>
<p>즉, stack이 비어있다면 더이상 유효하지 않은 괄호문자열이란 뜻이고(이전 코드에서 <code>note[i] === 0</code> 이 되버리는 부분), 새로운 인덱스를 기준점으로 초기화해야하므로 pop -&gt; push(i) 가 일어나는 것입니다.</p>
<h4 id="📌-stack이-비는-상황">📌 stack이 비는 상황</h4>
<p>1️⃣. 문자열이 <code>&#39;)&#39;</code>로 시작할 때 
예: <code>&quot;)(()())&quot;</code></p>
<p>첫 글자가 닫는 괄호이므로, 스택은 [-1]에서 pop이 되고 비어지게됩니다..</p>
<p>2️⃣. 괄호의 짝이 맞지 않을 때
예: <code>(()))(())</code></p>
<p>5번째에서 닫는 괄호가 추가적으로 등장합니다. 짝이 될 여는괄호(<code>(</code>)는 이미 다 pop 되었으므로, 이때 한번더 pop하면 -1이 나가면서 스택이 비어지게 됩니다.</p>
<pre><code class="language-javascript">var longestValidParentheses = function(s) {
    let max = 0 ;
      let stack = [-1]
    for (let i = 0; i &lt; s.length; i++) {
        if (s[i] === &#39;(&#39;) {
            stack.push(i);
        } else {
            stack.pop();

            if (stack.length === 0) {
                // 올바른 시작 기준점이 사라졌으면 현재 인덱스를 기준점으로 새로 세움
                stack.push(i);
            } else {
                // 현재 인덱스와 스택 마지막 값의 차이가 유효한 길이
                max = Math.max(max, i - stack[stack.length - 1]);
            }
        }
    }
    return max;
};
</code></pre>
<table>
<thead>
<tr>
<th>시도</th>
<th>최종</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/fromjs_toyou/post/0f78c642-7cd4-436b-beec-59f920a42d51/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/fromjs_toyou/post/5fd02e4c-2a5b-4415-98bb-bb9d768f5a2b/image.png" alt=""></td>
</tr>
</tbody></table>
<blockquote>
<p>😭 최종적으로 1ms 이내로 수행시간이 줄어들게 되었습니다.</p>
</blockquote>
<p>이전에 스터디원들과 함께 한문제를 어떻게 풀지 여러 각도에서 고민해본적이 있었는데 이를 개인 공부에도 적용시켜보면서 풀어보았습니다.
하루에 여러문제를 풀 수도 있겠지만, 수행시간과 메모리 관점에서 어떻게하면 적은 메모리로 빨리 풀 수 있을지 생각하다보니 
자료구조와 알고리즘의 개념을 체화하는데 더 도움이 되었던것 같습니다.
앞으로도 이 방법을 종종 사용해봐야겠다는 생각이 듭니다... </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Zustand] Next.js프로젝트에서 Zustand 사용하기]]></title>
            <link>https://velog.io/@fromjs_toyou/Zustand-Next.js%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Zustand-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@fromjs_toyou/Zustand-Next.js%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Zustand-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 09 Apr 2025 01:57:42 GMT</pubDate>
            <description><![CDATA[<h2 id="zustand-기본-사용법--store-생성하고-상태-조작하기">Zustand 기본 사용법 : store 생성하고 상태 조작하기</h2>
<h3 id="create으로-스토어-생성하기"><code>create</code>으로 스토어 생성하기</h3>
<pre><code class="language-typescript">import { create } from &#39;zustand&#39;;

export const use이름Store = create((set, get) =&gt; {
   return {
     상태 : 초깃값,
     액션: 함수
   }
 })</code></pre>
<p>create함수는 스토어(store)를 생성하여 반환한다.
스토어는 상태와 그 상태를 조작하는 액션으로 이루어져 있다.</p>
<pre><code class="language-typescript">
export const useCounterStore = create&lt;{
  count: number;
  increase: () =&gt; void;
  decrease: () =&gt; void;
}&gt;((set, get) =&gt; ({
  count: 0, // 상태와 상태의 초깃값
  increase: () =&gt; {
    const { count } = get(); // get함수는 상태와 액션을 가진 스토어를 가져올 수 있다.
    set({ count: count + 1 }); // set함수를 호출( = 변경할 상태를 속성으로 포함한 객체를 전달)
  },
  decrease: () =&gt; {
    // 혹은 콜백함수를 사용하면 state를 바로 가져올 수 있다.
    set((state) =&gt; ({ count: state.count - 1 }));
  },
}));</code></pre>
<p>store의 타입정의를 state와 action으로 나누어서 작성한다음 &amp;연산으로 합칠 수 있다.</p>
<pre><code class="language-typescript">type CounterState = {
  count: number;
};

type CounterActions = {
  increase: () =&gt; void;
  decrease: () =&gt; void;
};

type CounterStore = CounterState &amp; CounterActions;

export const useCounterStore = create&lt;CounterStore&gt;((set, get) =&gt; ({
(...)
</code></pre>
<h3 id="스토어-사용하기">스토어 사용하기</h3>
<pre><code class="language-typescript">&#39;use client&#39;;
import { useCounterStore } from &#39;@/stores/counter-store&#39;;

export default function Home() {
  const count = useCounterStore((state) =&gt; state.count);
  const increase = useCounterStore((state) =&gt; state.increase);
  const decrease = useCounterStore((state) =&gt; state.decrease);

  // 권장하지 않는 방법 : store 객체 자체를 얻기 -&gt; 사용하지 않는 상태가 변경되어도 리랜더링이 일어남
  // const store = useCounterStore();

  return (
    &lt;div className=&quot; min-h-screen p-8 pb-20 gap-16 sm:p-20 &quot;&gt;
      hello &amp;nbsp;
      &lt;button onClick={() =&gt; increase()}&gt; increase&lt;/button&gt;
      &amp;nbsp;
      &lt;button onClick={() =&gt; decrease()}&gt; decrease&lt;/button&gt;
      &amp;nbsp;
      &lt;div&gt;{count}&lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h2 id="zustand-응용">Zustand 응용</h2>
<h3 id="액션-분리하기">액션 분리하기</h3>
<p>단일 스토어의 액션을 많이 사용하면, 액션만을 분리해서 관리하는 패턴을 활용할 수 있다.
바로 액션들을 actions라는 객체안에 담아서 관리하면 된다.</p>
<pre><code class="language-typescript">type CounterActions = {
  actions: {
    increase: () =&gt; void;
    decrease: () =&gt; void;
  };
};

export const useCounterStore = create&lt;CounterStore&gt;((set, get) =&gt; ({
  count: 0, // 상태와 상태의 초깃값
  actions: {
    increase: () =&gt; {
      set((state) =&gt; ({ count: state.count + 1 }));
    },
    decrease: () =&gt; {
      set((state) =&gt; ({ count: state.count - 1 }));
    },
  },
}));
</code></pre>
<p>사용처에서는 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-typescript">// 기존방식
  const increase = useCounterStore((state) =&gt; state.increase);
  const decrease = useCounterStore((state) =&gt; state.decrease);

// actions만 모은 객체를 내보낸 방식
  const {increase, decrease} = useCounterStore((state) =&gt; state.actions);
</code></pre>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://www.heropy.dev/p/n74Tgc">https://www.heropy.dev/p/n74Tgc</a>
<a href="https://zustand.docs.pmnd.rs/getting-started/introduction">https://zustand.docs.pmnd.rs/getting-started/introduction</a>
<a href="https://zustand.docs.pmnd.rs/guides/nextjs">https://zustand.docs.pmnd.rs/guides/nextjs</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compound Component 패턴으로 checkbox 만들기]]></title>
            <link>https://velog.io/@fromjs_toyou/Compound-Component-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-checkbox-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@fromjs_toyou/Compound-Component-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-checkbox-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 19 Mar 2025 01:32:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/9d0e32cc-1ce0-41a9-bd2e-53136d9959d7/image.png" alt=""></p>
<p>일반적으로 위와 같이 체크 박스 컴포넌트를 만들어 주세요! 라는 요청이 오면 아래와 같이 만들 수 있다.</p>
<pre><code class="language-tsx">interface CheckboxProps {
  label: string;
  isChecked: boolean;
  onChange: (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; void;
}

export default function Checkbox(props: CheckboxProps) {
  const {label, isChecked, onChange} = props;
  return (
    &lt;div className=&quot;flex gap-1 justify-center items-center&quot;&gt;
      &lt;input type=&quot;checkbox&quot; className=&quot;w-4 h-4&quot; checked={isChecked} onChange={onChange} /&gt;
      &lt;label className=&quot;text-gray-500&quot;&gt;{label}&lt;/label&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>그런데 만약 아래와 같이 요청이 변경된다면 어떨까?</p>
<p>😬 체크박스 라벨 하위로 서브라벨을 달아주세요!
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/b5a3e1af-854a-48f4-bd1f-13f5c96ed9a0/image.png" alt=""></p>
<p>😜 서브라벨이 옆에 있는 컴포넌트도 만들어 주세요!
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/a61c88ea-7b33-42ee-a281-a318a95ca73a/image.png" alt=""></p>
<p>🤔 체크박스를 오른쪽으로 옯겨주세요!
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/1afa962c-5de0-40b7-9f5c-a8df28954781/image.png" alt=""></p>
<p>😉 서브라벨이 없고 체크박스가 오른쪽에 있는 컴포넌트도 만들어 주세요!
<img src="https://velog.velcdn.com/images/fromjs_toyou/post/32d0414e-a633-4134-81ea-440018ed199b/image.png" alt=""></p>
<p>체크박스는 쓰임새가 다양한 컴포넌트 이기 때문에 위처럼 다양한 사용예시가 있을 수 있다. 이를 각각의 상황마다 프롭으로 받는다면 프롭의 종류가 너무 다양해질 수 있고, 사용처에서 컴포넌트의 결과물을 예측하기 쉽지 않을 수 있다.</p>
<p>이럴 때 컴파운드 컴포넌트 패턴을 사용해서 사용처에서 직접 조립해서 만들 수 있도록 해보자. </p>
<h2 id="compound-component">Compound Component</h2>
<p>컴파운드 컴포넌트 패턴을 사용하면 유연하고 재사용 가능한 컴포넌트를 설계할 수 있고, 가독성과 유지 보수성을 높일 수 있다. 이름이 어렵지만 쉽게 이해하자면 자주 사용하는 html 태그인 select, options또한 컴포넌트 패턴의 일부이다!</p>
<p>이제 좀더 다양한 요구사항에 맞출 수 있는 컴포넌트로 checkbox를 변신시켜 보자.</p>
<pre><code class="language-typescript">
interface CheckboxContextProps {
  id: string;

  isChecked: boolean;
  disabled: boolean;
  size: &#39;small&#39; | &#39;medium&#39; | &#39;large&#39;;
  theme: &#39;primary&#39; | &#39;secondary&#39;;
  onChange: (checked: boolean) =&gt; void;
}

type CheckboxProps = CheckboxContextProps &amp; React.PropsWithChildren&lt;object&gt;;

const CheckboxContext = createContext&lt;CheckboxContextProps&gt;({
  id: &#39;&#39;,

  isChecked: false,
  size: &#39;medium&#39;,
  disabled: false,
  theme: &#39;primary&#39;,
  onChange: () =&gt; {},
});

const useCheckboxContext = () =&gt; useContext(CheckboxContext);

const CheckboxWrapper = ({
  id,
  isChecked,
  onChange,
  size,
  disabled,
  theme,
  children,
}: CheckboxProps) =&gt; {
  const value = {isChecked, onChange, id, size, theme, disabled};
  return (
    &lt;CheckboxContext.Provider value={value}&gt;
      &lt;div className=&quot;flex justify-center items-center gap-2&quot;&gt;{children}&lt;/div&gt;
    &lt;/CheckboxContext.Provider&gt;
  );
};</code></pre>
<p>아래의 checkbox, label 컴포넌트는 provider로 부터 값을 주입받아 동적으로 스타일링 될것 이다.</p>
<pre><code class="language-typescript">const Checkbox = ({...props}) =&gt; {
  const {id, isChecked, onChange, size, disabled, theme} = useCheckboxContext();
  const sizeVariation = {
    small: &#39;w-4 h-4&#39;,
    medium: &#39;w-5 h-5&#39;,
    large: &#39;w-6 h-6&#39;,
  };

  const themeVariation = {
    primary: &#39;checked:bg-primary focus:ring-primary &#39;,
    secondary: &#39;checked:bg-secondary focus:ring-secondary &#39;,
  };

  const handleKeyDown = (e: React.KeyboardEvent&lt;HTMLInputElement&gt;) =&gt; {
    if (e.key === &#39;Enter&#39; &amp;&amp; !disabled) {
      e.preventDefault();
      onChange(!isChecked);
    }
  };
  return (
    &lt;input
      id={id}
      type=&quot;checkbox&quot;
      checked={isChecked}
      onChange={(e: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; onChange(e.target.checked)}
      aria-labelledby={`${id}-label`}
      disabled={disabled}
      onKeyDown={handleKeyDown}
      className={`appearance-none border border-gray-300 border-solid rounded-md
        focus:ring-2 focus:ring-opacity-50 cursor-pointer
        ${sizeVariation[size]} ${themeVariation[theme]} 
      `}
      {...props}
    /&gt;
  );
};

const Label = ({children}: {children: React.ReactNode}) =&gt; {
  const {id, size, theme} = useCheckboxContext();
  const sizeVariation = {
    small: &#39;text-sm&#39;,
    medium: &#39;text-base&#39;,
    large: &#39;text-lg&#39;,
  };

  const themeVariation = {
    primary: &#39;text-primary&#39;,
    secondary: &#39;text-secondary&#39;,
  };
  return (
    &lt;label htmlFor={id} className={`${sizeVariation[size]} ${themeVariation[theme]}`}&gt;
      {children}
    &lt;/label&gt;
  );
};

CheckboxWrapper.Checkbox = Checkbox;
CheckboxWrapper.Label = Label;

export default CheckboxWrapper;
</code></pre>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/4a300cf9-67b1-4e9b-bd4f-103da8bdb381/image.png" alt=""></p>
<p>이제 사용처에서 label과 Checkbox의 위치를 자유롭게 조정할 수 있다.</p>
<pre><code class="language-typescript">&lt;CheckboxWrapper
        id=&quot;checkbox&quot;
        isChecked={check}
        onChange={(v: boolean) =&gt; setCheck(v)}
        size=&quot;medium&quot;
        theme=&quot;primary&quot;
        disabled={false}
      &gt;
          // 해당 부분의 순서를 변경해주면 됨
        &lt;CheckboxWrapper.Checkbox /&gt;
        &lt;CheckboxWrapper.Label&gt;checkbox&lt;/CheckboxWrapper.Label&gt; 

&lt;/CheckboxWrapper&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TypeScript의 구조적 타이핑]]></title>
            <link>https://velog.io/@fromjs_toyou/TypeScript%EC%9D%98-%EA%B5%AC%EC%A1%B0%EC%A0%81-%ED%83%80%EC%9D%B4%ED%95%91</link>
            <guid>https://velog.io/@fromjs_toyou/TypeScript%EC%9D%98-%EA%B5%AC%EC%A1%B0%EC%A0%81-%ED%83%80%EC%9D%B4%ED%95%91</guid>
            <pubDate>Wed, 19 Feb 2025 17:19:43 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/Dev-Ment/ts-study">데브먼트 타입스크립트 스터디</a>가 끝나고 최종 발표를 위해 정리한 글 입니다.</p>
<hr>
<h2 id="💫-intro-타입스크립트의-타입-시스템">💫 intro: 타입스크립트의 타입 시스템</h2>
<p>우리가 배운 Javascript는 동적 타입 언어입니다. Javascript에서는 컴파일시 타입을 정하지 않고 런타임까지 타입에 대한 결정을 끌고 갈수 있어 유연성이 높습니다.</p>
<p>타입스크립트는 정적 타입 언어입니다. Typescript 뿐 아니라 Java나 C와 같은 정적 타입  언어는 코드를 실행하기 전에 정적으로 변수의 타입을 결정합니다. 때문에 타입에러로 인한 문제를 컴파일 타임에 알 수 있어 안정성이 높습니다.</p>
<p>TypeScript는 정적 타입언어로써의 장점을 가진 동시에 Java, C와 구별되어 동작하는 부분이 존재합니다.</p>
<p>먼저 타입스크립트는 점진적 타이핑을 지원합니다. 아래 예시를 통해 이해해봅시다.</p>
<pre><code class="language-tsx">let a : number = 1;
a.toUpperCase() // ERROR!</code></pre>
<p>정적 타입 언어의 특징을 가진 typescript는 두번째 줄에서 에러를 표시합니다.  변수 a를 number 타입으로 정의했기 때문에 a에 toUpperCase 같은 문자열 메서드를 사용할 수 없기 때문입니다.</p>
<p>그렇다면 다음과 같은 상황에서는 어떨까요?</p>
<pre><code class="language-tsx">let a = 1;
a.toUpperCase(); // ERROR!</code></pre>
<p>타입스크립트는 여전히 에러를 표시합니다. a의 타입을 지정해주지 않았는데도 말이죠.</p>
<p>타입스크립트는 변수의 타입을 지정하지 않아도 초깃값을 기준으로 타입을 추론해줍니다. 이러한 특징을 통해 우리는 자바스크립트 코드에 타입스크립트를 점진적으로 적용할 수 있습니다.</p>
<p>타입스크립트가 다른 타입시스템과 구별되는 또 다른 점은 구조적 타이핑을 따른다는 점 입니다. </p>
<p>해당 글(발표)에서는 타입스크립트의 구조적(서브) 타이핑의 특징과 이로인해 발생하는 문제와 해결과정, 그리고 이를 거스르는 예시를 본격적으로 다루도록 하겠습니다.</p>
<h1 id="✏️-구조적-타이핑">✏️ 구조적 타이핑</h1>
<p>구조적 타이핑을 좀 더 잘 이해하기 위해서 우리는 이와 구별되는 명목적 타이핑에대해서 알 필요가 있습니다.</p>
<p>명목적 타이핑 (혹은 명목적 타입시스템)에서는 값과 객체는 하나의 구체적인 타입을 가지고 있으며 각각의 타입은 이름으로 구분됩니다. 동일한 멤버(필드)를 가지고 있더라도 타입의 이름이 다르다면 다른 타입으로 판단합니다</p>
<pre><code class="language-tsx">class Pet {
  name: string;
  breed: string;
}

class Dog {
  name: string;
  breed: string;
}

// ❌ 명목적 타이핑 - Not OK
// Dog은 Pet과 서로 다른 이름의 타입을 가지고 있기 때문에 호환불가합니다.
let pet: Pet = new Dog();</code></pre>
<p>반면 구조적 타이핑은 구조, 즉 멤버로 타입을 구분합니다. </p>
<pre><code class="language-tsx">class Pet { // name과 breed를 가지고 있다면 Pet 타입 입니다.
  name: string;
  breed: string;
}

class Dog { // name과 breed, age를 가지고 있다면 Dog 타입입니다.
  name: string;
  breed: string;
    age: number;
}

// ✅ 구조적 타이핑 - OK
// Dog은 Pet과 호환가능한 멤버 &quot;name&quot;, &quot;breed&quot;를 가지고 있기 때문에 호환가능합니다.
let pet: Pet = new Dog();</code></pre>
<p>이처럼 타입스크립트에서의 타입시스템은 집합으로 이해할 수 있습니다. </p>
<p>우리는 위 코드를 통해 자연스럽게 타입스크립트의 구조적 서브타이핑을 접하게 되었습니다. Dog타입의 인스턴스가 Pet타입의 pet 변수에 할당이 가능한 이유는 객체의 이름이 아닌 속성으로 타입을 구분했기 때문입니다.</p>
<p>타입스크립트를 지탱하는 중요한 개념인 구조적 서브타이핑은 “이름이 다른 객체라도 가진 속성이 동일하다면(name, breed) 타입 스크립트에서는 호환이 가능하다”라는 개념이죠.</p>
<p>타입스크립트는 명목적 타이핑대신 구조적 타이핑의 개념을 선택함으로써 기존의 javascript로 작성된 코드들이 typescript로 전환하는데 적응력을 높였습니다. </p>
<p>&lt;책내용 참고해서 내용 조금더 보충하기&gt;</p>
<p>요약하자면 </p>
<p>1️⃣ 구조적 타이핑은 적당히 엄격하고 적당히 느슨한 타입시스템입니다. (명목적타이핑과 같이 “확실하게 올바른” 타입검사를 목표로 하는 시스템이 아닙니다.)</p>
<p>2️⃣ 타입스크립트가 구조적 타이핑을 사용하는 이유는 Javascript가 가진 언어적 특성 + 하위 호환성을 지키기 위한 슈퍼셋언어라는 디자인 목표 때문입니다.</p>
<h1 id="🚀-구조적-타이핑으로-인해-발생하는-문제점">🚀 구조적 타이핑으로 인해 발생하는 문제점</h1>
<p>typescript의 이러한 구조적 타이핑은 때로는 예상치 못한 결과를 야기하기도 합니다. 아래 두가지 예시를 통해 이해해봅시다.</p>
<h2 id="📚-예시1">📚 예시1.</h2>
<pre><code class="language-tsx">interface Cube {
  width: number;
  height: number;
  depth: number;
}

function addLines(c: Cube) { // 정육면체의 모든 모서리의 합을 구하는 함수
  let total = 0;

  for (const axis of Object.keys(c)) {
    // 🚨 ERROR
    const length = c[axis];
    total += length;
  }

  return total;
}</code></pre>
<p><code>Cube</code> 인터페이스의 모든 멤버는 number타입을 가집니다. 그러나 구조적 타이핑의 특징 때문에  addLine의 인자c에는 <code>width</code>, <code>height</code>, <code>depth</code>와 추가적인 멤버를 가진 객체 또한 들어올 수 있습니다. 위 코드에서는 <code>c[axis]</code>타입이 string일 수 도 있다는 에러가 발생합니다.</p>
<p>마치 아래와 같은 상황일 때 말이죠!</p>
<pre><code class="language-tsx">const namedCube = {
  width: 6,
  height: 5,
  depth: 4,
  name: &quot;SweetCube&quot;, // string 타입의 추가 속성이 정의되었다
};

addLines(namedCube); // ✅ OK 컴파일 에러가 발생하지 않으나 런타임에서 문제가 발생할 수 있습니다.</code></pre>
<p>즉, 타입스크립트는 <code>c[axis]</code>가 어떤 속성을 지닐지 알 수 없으며 <code>c[axis]</code>의 타입을 <code>number</code>라고 확정할수 없어서 에러를 발생시킵니다. </p>
<p>구조적 타이핑은 명목적 타이핑에 비해 타입 안정성은 떨어지지만 유연합니다.</p>
<p>하지만 이처럼 보다 <code>엄격한 타입안정성을</code> 필요로 할 때가 있습니다. 이제부터 위의 문제를 해결하면서 타입스크립트이 여러 문법을 소개해보도록 하겠습니다.</p>
<h2 id="🔑-as-타입-단언">🔑 as 타입 단언</h2>
<blockquote>
<p>Typescript에서는 <code>as</code> 키워드를 사용한 타입단업 문법을 통해 타입을 강제할 수 있습니다. 타입단언은 개발자가 해당 값의 타입을 더 잘 파악할 수 있을 때 사용되며 강제 형변환과 유사한 기능을 제공합니다.</p>
</blockquote>
<pre><code class="language-tsx">interface Cube {
  width: number;
  height: number;
  depth: number;
}

const addLines = (c: Cube) =&gt; {
  let total = 0;

  for (const axis of Object.keys(c)) {
    const length = c[axis as keyof Cube];
    total += length;
  }

  return total;
};

addLines({
  width: 10,
  height: 10,
  depth: 10,
});</code></pre>
<p>첫번째 방법은 얻은 객체의 key에 타입단언을 추가하는 방식입니다.</p>
<p><code>axis as keyof Cube</code> 는 Cube의 key를 타입으로 가져오게됩니다. 이렇게 되면 <code>c[axis]</code> 는 number만이 존재한다는 것을 추론하게 됩니다.</p>
<h2 id="🔑-as-타입-단언과-제네릭">🔑 as 타입 단언과 제네릭</h2>
<pre><code class="language-tsx">interface Cube {
  width: number;
  height: number;
  depth: number;
}

const addLines = (c: Cube) =&gt; {
  let total = 0;

  for (const axis of Object.keys(c) as Array&lt;keyof Cube&gt;) {
    const length = c[axis];
    total += length;
  }

  return total;
};

addLines({
  width: 10,
  height: 10,
  depth: 10,
});</code></pre>
<p>두번째 방법은 key의 <strong>배열</strong>에 타입단언을 추가하는 방식입니다.위 예시에서는  <code>Object.keys(c)</code>로 반환되는 배열의 타입을 지정합니다. <code>as Array&lt;keyof Cube&gt;</code> <a href="https://www.typescriptlang.org/ko/docs/handbook/2/generics.html">제네릭</a>을 이용하여 배열의 각요소가 Cube의 key 타입임을 단언함으로써 동일하게 추론이 가능합니다. </p>
<h2 id="🔑-인덱스-시그니처">🔑 인덱스 시그니처</h2>
<blockquote>
<p><strong>인덱스 시그니처</strong>는 속성 이름을 알 수 없지만, 속성값의 타입은 알 수 있을 때 사용합니다.
 <code>[key: K]: T</code> 형태로 선언하며, 키는 타입 <code>K</code>, 값은 타입 <code>T</code>를 가집니다.</p>
</blockquote>
<pre><code class="language-tsx">interface Cube {
  width: number;
  height: number;
  depth: number;
  [key:string]: number;
}

function addLines(c: Cube) { // 정육면체의 모든 모서리의 합을 구하는 함수
  let total = 0;

  for (const axis of Object.keys(c)) {
    const length = c[axis];
    total += length;
  }

  return total;
}</code></pre>
<p><code>Cube</code> 인터페이스에 인덱스 시그니처를 추가하여 c의 멤버 타입을 number로 추론시킬 수 있습니다.</p>
<h2 id="🔑-타입-가드">🔑 타입 가드</h2>
<pre><code class="language-tsx">interface Cube {
  width: number;
  height: number;
  depth: number;
}

function addLines(c: Cube) {
  // 정육면체의 모든 모서리의 합을 구하는 함수
  let total = 0;

  for (const axis of Object.keys(c)) {
    if (typeof axis === &#39;number&#39;) {
      const length = c[axis];
      total += length;
    }
  }
  return total;
}
</code></pre>
<p>타입가드란 특정 타입을 가질 수 밖에 없는 상황을 유도하는 방식을 통해 타입을 좁히는 방법입니다. </p>
<p>위의 예시에서는 javascript의 typeof연산자를 통해  axis가 number타입인 경우에만  더하기 연산을수행하도록 할 수 있습니다.</p>
<p>단,  typeof 연산자는 자바스크립트의 타입시스템만 대응할 수 있습니다. typeof 연산자와 배열을 사용할 경우 object로 판별되어 버리는 것과 같이 복잡한 타입검증에는 명확한 한계를 지닙니다.</p>
<p>✅<code>typeof</code>연산자는 <strong>원시타입</strong>을 좁히는 용도로만 사용합시다.</p>
<h2 id="📚-예시2">📚 예시2</h2>
<pre><code class="language-tsx">interface Point2D {
  x: number;
  y: number;
}

interface Point3D {
  x: number;
  y: number;
  z: number;
}

function handlePoint2D(point: Point2D) {}

function getPoint3D(): Point3D {
  return { x: 0, y: 0, z: 0 };
}

handlePoint2D(getPoint3D()); // ✅ OK </code></pre>
<p>구조적 타이핑은 명시된 타입(Point2D)외에 추가적인 멤버를 갖는 것을 허용합니다. </p>
<p>때문에 Point2D 타입의 인자를 다룰것으로 예상되는 함수에 Point3D 타입의 매개변수를 전달해도 문제가 발생하지 않습니다. 이는 명백한 개발자의 실수이지만 타입스크립트는 이러한 실수를 허용해줍니다.</p>
<h2 id="🔑-branded-type-기법">🔑 Branded Type 기법</h2>
<pre><code class="language-tsx">type Brand&lt;K, T&gt; = K &amp; { _brand: T };

type Point2D = Brand&lt;
  {
    x: number;
    y: number;
  },
  &#39;Point2D&#39;&gt;;

type Point3D = Brand &lt;{
  // 브랜드 멤버
  __brand: &#39;Point3D&#39;;
  x: number;
  y: number;
  z: number;
}, &#39;Point3D&#39;&gt;

function handlePoint2D(point: Point2D) {}

function getPoint3D(): Point3D {
  return &lt;Point3D&gt;{ x: 0, y: 0, z: 0 };
}

handlePoint2D(getPoint3D()); 
// ERROR! Point3D 형식의 인수는 Point2D 형식의 매개 변수에 할당될 수 없습니다.</code></pre>
<p>Branded Type 은 개발자가 매개변수로 정의한 타입 외에는 호환되지 않도록 유니크한 멤버를 추가하여 타입을 강제하는 기법입니다.</p>
<p>위와 같은 예시 외에도 온도나 화폐단위(원,달러, 엔화)와 같이 <code>number</code> 타입이지만 서로 다른 의미를 가질 수 있어 “명시적인” 구분이 필요할 때 사용할 수 있습니다.</p>
<h1 id="🚀-구조적-서브-타이핑의-예외">🚀 구조적 서브 타이핑의 예외</h1>
<pre><code class="language-tsx">interface Point2D {
  x: number;
  y: number;
}

interface Point3D {
  x: number;
  y: number;
  z: number;
}

function handlePoint2D(point: Point2D) {}

function getPoint3D(): Point3D {
  return &lt;Point3D&gt;{ x: 0, y: 0, z: 0 };
}

const point3d = getPoint3D();
handlePoint2D(point3d); // ✅ OK
handlePoint2D({ x: 0, y: 0, z: 0 }); // ERROR!!</code></pre>
<p>가장 마지막 줄의 코드는 왜 타입호환이 되지 않는 것일까요? </p>
<p>타입스크립트에서 함수의 인자로 객체리터럴이 전달될때는 잉여속성체크라는 기능이 발동하게 됩니다.</p>
<p>즉, 객체 리터럴이 전달될때는 잉여속성체크가 발동되어 구조적 서브타이핑이 적용되지 않습니다.</p>
<blockquote>
<p>ERROR : 개체 리터럴은 <strong>알려진 속성만 지정</strong>할 수 있으며 <code>Point2D</code> 형식에 <code>z</code> 이(가) 없습니다.</p>
</blockquote>
<h2 id="❓-왜-이렇게-동작하는-것일까요">❓ 왜 이렇게 동작하는 것일까요?</h2>
<p>이를 이해하려면 타입스크립트 컴파일러의 동작원리에 대해서 알아야 합니다. ( 더 궁금하다면, 저희 스터디 문서를 참고하셔도 됩니다! <a href="https://github.com/Dev-Ment/ts-study/blob/main/6%E1%84%8C%E1%85%A1%E1%86%BC_%E1%84%90%E1%85%A1%E1%84%8B%E1%85%B5%E1%86%B8%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%B8%E1%84%90%E1%85%B3_%E1%84%8F%E1%85%A5%E1%86%B7%E1%84%91%E1%85%A1%E1%84%8B%E1%85%B5%E1%86%AF/%EA%B9%80%EC%98%81%EB%AF%BC.md"><code>영민님의 멋진 정리</code></a> )</p>
<p>간략히 말하자면, TypeScript 컴파일러는 TypeScript 소스코드를 AST (Abstract Syntax Tree)로 변환한 뒤, 타입 검사를 수행하고, 그 후 JavaScript 소스코드로 변환합니다.</p>
<p>타입스크립트 깃허브의 compiler 디렉토리에서 AST를 JavaScript 소스코드로 변환하는 과정의 코드를 살펴볼수 있는데요. 타입호환의 예외가 발생하는 지점의 코드를 살펴보면, 함수에 인자로 들어온 값이 <code>FreshLiteral</code> 인지 아닌지 여부에 따라 조건분기가 발생하여 타입 호환 허용 여부가 결정됩니다.</p>
<pre><code class="language-tsx">/** 함수 매개변수에 전달된 값이 FreshLiteral인 경우 true가 됩니다. */
const isPerformingExcessPropertyChecks =
    getObjectFlags(source) &amp; ObjectFlags.FreshLiteral;

if (isPerformingExcessPropertyChecks) {
    /** 이 경우 아래 로직이 실행되는데,
     * hasExcessProperties() 함수는
     * excess property가 있는 경우 에러를 반환하게 됩니다.
     * 즉, property가 정확히 일치하는 경우만 허용하는 것으로
     * 타입 호환을 허용하지 않는 것과 같은 의미입니다. */
    if (hasExcessProperties(source as FreshObjectLiteralType)) {
        reportError();
    }
}
/**
 * FreshLiteral이 아닌 경우 위 분기를 skip하게 되며,
 * 타입 호환을 허용하게 됩니다. */</code></pre>
<p>타입스크립트는 <a href="https://radlohead.gitbook.io/typescript-deep-dive/type-system/freshness">신선도</a>(Freshness)라는 개념을 사용하여  타입 호환의 예외를 처리합니다.</p>
<ul>
<li>모든 object는 초기에 신선(fresh)하다</li>
<li>타입 단언, 타입 추론에 의해 객체 리터럴이 확장되면 신선도(freshness)가 사라진다.</li>
<li>객체가 fresh 할 경우 타입 호환이 허용되지 않는다.</li>
</ul>
<p>즉, 객체 리터럴로 전달( <code>handlePoint2D({ x: 0, y: 0, z: 0 });</code> )할경우 freshness가 지속되어 타입호환이 허용되지 않는것 입니다.</p>
<hr>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://www.un-defined.dev/post/typescript/structural-typing">https://www.un-defined.dev/post/typescript/structural-typing</a></p>
<p><a href="https://toss.tech/article/typescript-type-compatibility">https://toss.tech/article/typescript-type-compatibility</a></p>
<p><a href="https://www.youtube.com/watch?v=kMuJz6N-Grw">https://www.youtube.com/watch?v=kMuJz6N-Grw</a></p>
<p><a href="https://github.com/Dev-Ment/ts-study/blob/main/2%EC%9E%A5_%ED%83%80%EC%9E%85/%EC%A0%84%EC%84%B1%EC%9A%B0.md">https://github.com/Dev-Ment/ts-study/blob/main/2%EC%9E%A5_%ED%83%80%EC%9E%85/%EC%A0%84%EC%84%B1%EC%9A%B0.md</a></p>
<p><a href="https://github.com/Dev-Ment/ts-study/blob/main/4%EC%9E%A5_%ED%83%80%EC%9E%85%ED%99%95%EC%9E%A5%ED%95%98%EA%B8%B0.%EC%A2%81%ED%9E%88%EA%B8%B0/%EC%9D%B4%EC%A0%95%EC%88%98.md">https://github.com/Dev-Ment/ts-study/blob/main/4%EC%9E%A5_%ED%83%80%EC%9E%85%ED%99%95%EC%9E%A5%ED%95%98%EA%B8%B0.%EC%A2%81%ED%9E%88%EA%B8%B0/%EC%9D%B4%EC%A0%95%EC%88%98.md</a></p>
<p><a href="https://velog.io/@sa02045/%EC%99%9C-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EA%B5%AC%EC%A1%B0%EC%A0%81-%ED%83%80%EC%9D%B4%ED%95%91%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C-5d632vd6">https://velog.io/@sa02045/%EC%99%9C-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EA%B5%AC%EC%A1%B0%EC%A0%81-%ED%83%80%EC%9D%B4%ED%95%91%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C-5d632vd6</a></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vue.js] TIL : v-model, setup script, 생명주기 훅]]></title>
            <link>https://velog.io/@fromjs_toyou/Vue.js-TIL</link>
            <guid>https://velog.io/@fromjs_toyou/Vue.js-TIL</guid>
            <pubDate>Wed, 19 Feb 2025 00:26:38 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣-v-model과-양방향-바인딩">1️⃣ v-model과 양방향 바인딩</h2>
<p><code>v-model</code>디렉티브는 <code>양방향 데이터 바인딩</code>을 지원하는 속성이다.</p>
<p><code>양방향 데이터 바인딩</code>이란 <strong>화면의 데이터와 뷰 인스턴스가 항상 일치</strong>하는 것이다.</p>
<pre><code class="language-javascript">&lt;template&gt;
  &lt;input type=&quot;text&quot; v-model=&quot;inputText&quot;&gt;
&lt;/template&gt;

&lt;script setup&gt;
import {ref} from &#39;vue&#39;;

const inputText = ref(&#39;&#39;);
&lt;/script&gt;</code></pre>
<p><code>v-model</code>디렉티브는 간편한 양방향 바인딩기능을 지원하지만, 내부적으로는 아래 코드와 같은 구조로 동작한다.</p>
<pre><code class="language-javascript">&lt;template&gt;
  &lt;input type=&quot;text&quot; :value=&#39;inputText&#39; @input=&#39;inputText=$event.target.value&quot; /&gt;
&lt;/template&gt;

&lt;script setup&gt;
import {ref} from &#39;vue&#39;;

const inputText = ref(&#39;&#39;);
&lt;/script&gt;</code></pre>
<h2 id="2️⃣-setup-script-에서-data-computed-methods-등의-속성은-사용불가">2️⃣ setup script 에서 data, computed, methods 등의 속성은 사용불가</h2>
<p>대신, ref, computed 함수, defineEmits, defineExpose 등의 기능을 사용하여 대체할 수 있다. 이때 컴포넌트 옵션들의 의존성이 순서에 의해 결정되기 때문에 옵션 정의 시에 아래 예시와 같은 순서를 따라야한다.</p>
<pre><code class="language-javascript">&lt;script setup&gt;
import { ref, computed, watch } from &#39;vue&#39;;

// 1. props 정의: 컴포넌트에 전달되는 프롭스
const props = defineProps({
  // props 옵션들...
});

// 2. 데이터 선언: ref, reactive 등을 사용하여 컴포넌트의 반응형 데이터 선언
const count = ref(0);

// 3. 컴포지션 함수: 컴포넌트의 로직을 재사용 가능한 컴포지션 함수로 추출, 재사용 가능
const doubleCount = computed(() =&gt; {
  return count.value * 2;
});

// 4. 컴포넌트 옵션: 컴포넌트 옵션을 직접 사용하지 않고 선언된 변수와 함수로 로직을 작성
// computed 옵션 대신 computed 함수, watch 속성 대신 watch 함수 ...
watch(count, (newValue, oldValue) =&gt; {
  console.log(&#39;Count changed:&#39;, newValue, oldValue);
});

// 5. 기타 코드: 필요에 따른 기타 로직, 함수 정의
// 필요한 추가 로직이나 함수 등...
&lt;/script&gt;</code></pre>
<h2 id="3️⃣-생명주기-훅">3️⃣ 생명주기 훅</h2>
<p><img src="https://velog.velcdn.com/images/fromjs_toyou/post/fed85303-7fd1-4beb-a9c9-cc158b4fb837/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>