<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jin_evergreen.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 07 May 2026 18:04:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jin_evergreen.log</title>
            <url>https://velog.velcdn.com/images/jin_wls/profile/a1c4dd02-4298-4fb1-994c-798de64919e4/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jin_evergreen.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jin_wls" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[리액트 핵심 요소 살펴보기]]></title>
            <link>https://velog.io/@jin_wls/%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EC%9A%94%EC%86%8C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@jin_wls/%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EC%9A%94%EC%86%8C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 07 May 2026 18:04:54 GMT</pubDate>
            <description><![CDATA[<p>리액트를 사용하다 보면 JSX, Virtual DOM 등 자연스럽게 익숙해지는 개념들이 있습니다. 우리는 리액트를 활용하여 개발을 진행하지만, 리액트의 세부적인 개념들을 명확히 이해하고 사용하지는 못한 것 같습니다.</p>
<p>이번 글에서는 <strong>리액트의 핵심이 되는 주요 개념</strong>들을 하나씩 정리하며, 리액트의 동작 방식에 대해 조금이나마 이해해보려고 합니다.</p>
<hr>
<h3 id="jsx와-트랜스파일">JSX와 트랜스파일</h3>
<p>JSX를 처음 배웠을 때, 단순히 자바스크립트 안에서 HTML을 작성할 수 있는 문법 정도로만 생각했습니다. JSX가 무엇인지 정확하게 배우기 전에, 그저 리액트로 개발하려면 써야하는 것 정도로만 이해했던 것 같습니다.</p>
<p>실제로 JSX는 HTML과 매우 유사하게 생겼고, 브라우저 화면에 UI를 그린다는 점에서도 비슷합니다. 하지만 J<strong>SX는 HTML이 아닙니다.</strong> 조금 더 깊게 들여다보면, JSX는 ECMAScript 표준 문법이 아니기 때문에 브라우저가 직접적으로 이해할 수 있는 문법이 아닙니다.</p>
<p>그럼에도 우리가 리액트에서 JSX를 사용할 수 있는 이유는 <strong>트랜스파일 과정</strong>이 존재하기 때문입니다. 리액트에서는 <strong>Babel과 같은 트랜스파일러가 JSX 문법을 브라우저가 이해할 수 있는 일반 자바스크립트 코드로 변환</strong>합니다.</p>
<pre><code>// 이러한 JSX 문법은
&lt;div className=&quot;container&quot;&gt;
  &lt;h1&gt;Hello&lt;/h1&gt;
  &lt;p&gt;LET&#39;S SOPT React Study&lt;/p&gt;
&lt;/div&gt;

// 아래와 같은 형식으로 변환됩니다.React.createElement(&quot;div&quot;,
  { className: &quot;container&quot; },
  React.createElement(&quot;h1&quot;, null, &quot;Hello&quot;),
  React.createElement(&quot;p&quot;, null, &quot;LET&#39;S SOPT React Study&quot;)
);</code></pre><p><code>React.createElement</code> 는 화면에 그릴 UI 정보를 담은 React Element 객체를 생성합니다.</p>
<p>변환된 후 구조를 살펴보면, <strong>JSX는 결국 중첩된 함수 호출 구조로 변환</strong>되고 이를 통해 <strong>리액트는 UI를 트리 형태의 자바스크립트 객체 구조로 표현</strong>하고 있다는 점을 알 수 있습니다.</p>
<pre><code>const element = &lt;h1&gt;SOPT&lt;/h1&gt;;

console.log(element);

// console.log 결과
{
  type: &quot;h1&quot;,
  props: {
    children: &quot;SOPT&quot;
  }
}</code></pre><p>즉, <strong>JSX는 단순히 HTML을 그리는 문법이 아니라 UI를 자바스크립트 객체 트리로 표현하기 위한 선언형 문법</strong>에 가깝습니다.</p>
<hr>
<h3 id="virtual-dom">Virtual DOM</h3>
<p>리액트의 가장 대표적인 개념 중 하나가 Virtual DOM, 가상돔입니다. <strong>가상돔은 실제 DOM을 추상화하여, 메모리 내에 가상으로 존재하는 DOM</strong>을 의미합니다.</p>
<p>그리고 우리는 일반적으로 가상돔이 실제 DOM보다 빠르다고 알고 있습니다. 완전히 틀린 설명이라고 보기는 어렵지만, 핵심을 정확히 담고 있지도 않습니다.</p>
<p>실제로 브라우저의 DOM API 자체가 항상 느린 것은 아닙니다. 문제는 <strong>상태가 복잡해질수록, UI를 조작하는 개발자가 직접 관리하는 비용이 급격히 커진다</strong>는 것입니다.</p>
<h4 id="전통적인-dom-조작">전통적인 DOM 조작</h4>
<p>2주차 과제를 생각해보면, querySelector 등을 활용해 DOM 요소를 직접 찾고, innerText 를 수정하는 방식으로 화면 변경 과정을 모두 관리해야 했습니다.</p>
<p>그나마 2주차 과제는 간단한 편이라 괜찮았지만 규모가 조금만 더 커져도, 우리는 <strong>어떤 요소를 수정해야 하는지 매번 찾아야 하고, 상태와 화면을 동기화해야하고, 여러 UI 변경 순서도 제어해야 하는 등</strong> 작업이 더 복잡해집니다.</p>
<p>리액트는 조금 다른 방식으로 접근하여, 개발자가 모든 화면 변경 과정을 직접 작성하지 않습니다.</p>
<p>대신 <strong>현재 상태라면 어떤 UI가 되어야 하는지를 선언</strong>합니다.</p>
<pre><code>return isLoggedIn
  ? &lt;Dashboard /&gt;
  : &lt;LoginPage /&gt;;</code></pre><p>그리고 <strong>이전 UI와 새로운 UI를 비교하여 실제 DOM 변경 사항을 계산</strong>합니다. 여기서 필요한 것이 바로 가상돔입니다.</p>
<p>위에 JSX 섹션에서 언급한 것처럼, JSX는 내부적으로 객체 형태로 변환되고 컴포넌트 구조 또한 중첩된 트리 형태로 표현됩니다.</p>
<p>리액트는 <strong>상태가 변경될 때마다 새로운 UI 트리를 생성하고, 이전 트리와 비교</strong>합니다. <strong>어떤 컴포넌트가 달라졌는지, 어떤 부분이 추가 또는 삭제되었는지를 가상돔에서 계산한 뒤 실제 DOM에는 필요한 변경만 반영</strong>합니다. 이 과정이 바로 <strong>Reconciliation</strong> 입니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/b92cd989-9d15-4289-88de-950e4904d5fe/image.png" alt=""></p>
<p>그렇다면 왜 리액트는 가상돔을 사용하는지 근본적인 의문이 들 수 있습니다. 실제 DOM은 브라우저가 관리하는 복잡한 객체이고, 직접 조작 비용도 존재합니다. 하지만 <strong>가상 돔은 단순한 자바스크립트 객체이므로 비교가 쉽고, 메모리 상에서 계산 가능하기 때문에 변경 사항을 추론하기 유리</strong>합니다.</p>
<p>다시 처음으로 돌아가서 가상돔이 일반 DOM에 비해 빠르다는 것은, 항상 그렇지는 않습니다. 오히려 DOM 변경 하나만 놓고 보면 직접 DOM을 조작하는게 더 빠를 수도 있습니다. 하지만 <strong>가상 돔은 UI를 계산 가능한 데이터 구조로 만들어서, 복잡한 UI 변경 과정을 추상화할 수 있도록 한다는 것에 더 큰 의미</strong>가 있습니다.</p>
<h3 id="react-fiber">React Fiber</h3>
<p>초기 리액트 렌더링 과정은 동기적으로 한 번에 처리했습니다. 모든 작업이 하나의 작업처럼 메인 스레드에서 실행되었는데, 프로젝트 규모가 커지고 UI 트리가 커질수록 이 작업이 점점 무거워졌습니다.</p>
<p>대규모 리스트 렌더링이나 복잡한 상태 업데이트, 애니메이션과 동시에 발생하는 렌더링 등에서 브라우저가 버벅이기 시작했습니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/14744f72-d49b-4c65-8f7e-c9d0c2ff83e6/image.png" alt=""></p>
<p>이러한 문제를 해결하기 위해 <strong>React Fiber</strong>라는 개념이 등장했습니다. <strong>React Fiber는 렌더링 작업을 작은 단위로 분리하여, 필요하다면 중간에 멈추고 다시 이어서 작업</strong>하도록 합니다. 즉 <strong>렌더링 작업의 우선순위</strong>를 조절합니다.</p>
<h4 id="점진적-렌더링-incremental-rendering">점진적 렌더링 (Incremental Rendering)</h4>
<p>리액트의 대표적인 특징 중 하나는 점진적 렌더링입니다.</p>
<p>리액트는 모든 작업의 중요도를 동일하게 생각하지 않습니다. 사용자 입력, 클릭 이벤트와 같은 작업은 즉각적으로 반응해야 하기 때문에 높은 우선순위를 주고 화면 밖 컴포넌트 렌더링이나 데이터 프리패칭은 상대적으로 낮은 우선순위를 가집니다.</p>
<p><strong>Fiber는 이러한 작업들을 우선순위와 브라우저 상황에 맞게 작업을 분배</strong>합니다.</p>
<h4 id="더블-버퍼링-double-buffering">더블 버퍼링 (Double Buffering)</h4>
<p>두 번째 특징은 <strong>더블 버퍼링 구조</strong>입니다.</p>
<p>리액트는 계산 중인 UI를 보여주지 않기 위해, 현재 화면을 바로 수정하지 않습니다. <strong>보이지 않는 곳에서 UI 트리를 먼저 생성하고 모든 계산이 끝난 뒤 완성된 트리를 한 번에 교체</strong>합니다. 즉, 우리는 항상 완성된 UI만 볼 수 있습니다.</p>
<p>이러한 구조 덕분에 렌더링 도중 작업을 중단하거나 다시 이어서 수행 가능하고, 이후 다룰 Suspense 같은 기능들도 구현할 수 있습니다.</p>
<hr>
<h3 id="render-phase와-commit-phase">Render Phase와 Commit Phase</h3>
<p>리액트는 상태가 변경되었다고 해서 바로 실제 DOM을 수정하지 않습니다.</p>
<p>내부적으로 렌더링 과정을 크게 <strong>Render Phase와 Commit Phase로 나누어 처리</strong>합니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/54726b42-c0e9-4022-ad2f-968b421c3fc9/image.webp" alt=""></p>
<h4 id="render-phase">Render Phase</h4>
<p>Render Phase는 <strong>UI를 계산하는 단계</strong>입니다. state 나 props 가 변경되면 리액트는 컴포넌트 함수를 다시 실행합니다. 위에 가상돔 섹션에서 계속 언급했지만 이때 이전 트리와 새로운 트리를 비교하여 변경 사항을 계산합니다.</p>
<p>간단하게 요약하면 <strong>새로운 UI가 어떤 모습이어야 하는지 계산하는 단계</strong>입니다.</p>
<h4 id="commit-phase">Commit Phase</h4>
<p><strong>Render Phase에서 모든 계산이 끝난 후 Commit Phase가 실행</strong>됩니다.</p>
<p>Commit Phase에서는 계산된 변경 사항을 실제 DOM에 반영합니다. Render Phase는 Fiber 아키텍처 이후에 렌더링 작업을 작은 단위로 나누어 처리할 수 있게 되면서 필요하다면 중간에 멈추고 다시 수행하는게 가능하지만, <strong>Commit Phase는 중단할 수 없습니다.</strong> 실제 화면 변경 도중 작업을 멈추면 일관성 없는 UI가 보일 가능성을 차단하기 위함입니다.</p>
<p>우리는 컴포넌트 함수가 다시 실행되면 무조건 DOM에 변경이 있다고 생각합니다. 하지만 이렇게 Render Phase와 Commit Phase로 구분되어 렌더링 과정이 진행되기 때문에, <strong>변경 사항이 없다면 Commit Phase로 넘어가지 않아 DOM 변경이 없을 가능성</strong>도 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우리의 웹 서비스에 PWA 도입하기]]></title>
            <link>https://velog.io/@jin_wls/%EC%9A%B0%EB%A6%AC%EC%9D%98-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-PWA-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jin_wls/%EC%9A%B0%EB%A6%AC%EC%9D%98-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-PWA-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 01 May 2026 09:54:25 GMT</pubDate>
            <description><![CDATA[<p>모바일 웹의 접근성과 사용성을 높이는 방법에는 여러 가지가 있지만, 그 중에서도 <strong>PWA 도입</strong>은 <strong>네이티브 앱에 가까운 사용자 경험을 제공</strong>할 수 있는 효과적인 방법입니다. </p>
<p>공연 공지 서비스 AMP는 서비스 목표에 맞게 공지 알림 기능을 제공하고, 더 나은 사용자 경험을 제공하고자 PWA 도입을 결정했습니다. 이번 글에서는 <strong>Vite 환경에서 <code>vite-plugin-pwa</code> 라이브러리를 활용해 PWA 설정을 진행하며 배웠던 것과, 어떻게 진행했는지를 기록</strong>하고자 합니다.</p>
<hr>
<h3 id="pwaprogressive-web-app란">PWA(Progressive Web App)란?</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/ad3e29ce-c457-4884-9463-1c8956bbddcb/image.jpg" alt=""></p>
<p>PWA는 <strong>웹과 네이티브 앱의 장점을 결합한 애플리케이션 형태</strong>입니다. 브라우저를 통해 접속하지만 홈 화면에 아이콘을 추가하여 앱처럼 간편하게 실행할 수 있습니다. 뿐만 아니라 <strong>오프라인 동작, 푸시 알림, 브라우저 UI가 최소화된 화면 등의 네이티브 앱과 유사한 사용자 경험</strong>을 제공할 수 있도록 해줍니다.</p>
<h3 id="service-worker와-vite-plugin-pwa">Service Worker와 vite-plugin-pwa</h3>
<p>PWA에서 빼놓을 수 없는 개념이 <strong>Service Worker</strong>입니다. PWA가 네이티브 앱처럼 동작할 수 있는 이유가 바로 이 서비스 워커 때문입니다.</p>
<p>서비스 워커는 현재 보고 있는 웹페이지와는 별개로, <strong>브라우저의 백그라운드에서 실행되는 자바스크립트 파일</strong>입니다. 즉, 브라우저의 네트워크 요청을 가로채고 제어할 수 있는 백그라운드 스크립트 역할을 합니다. </p>
<ul>
<li><p><strong>오프라인 캐싱</strong> : 네트워크 연결이 끊기거나 불안정할 때, 서비스 워커가 미리 캐싱해둔 파일과 데이터를 보여줍니다.</p>
</li>
<li><p>*<em>백그라운드 동기화 및 푸시 알림 *</em>: 사용자가 웹 페이지를 닫아두어도 백그라운드에서 서버의 푸시 알림을 받을 수 있습니다. 이는 AMP 서비스에 PWA를 적용하는 최우선 목적이자 핵심 기능이기도 합니다.</p>
</li>
</ul>
<p>원래라면 이 서비스 워커를 통해 어떤 자원을 캐싱할지, 언제 업데이트할지 등 <strong>복잡한 생명주기 로직을 개발자가 직접 다 작성</strong>해야 합니다.</p>
<p>하지만 Vite 환경에서는 <strong><code>vite-plugin-pwa</code> 라이브러리를 사용하여 쉽게 PWA를 설정</strong>할 수 있습니다. <code>vite-plugin-pwa</code>는 <strong>빌드 시점에 이러한 서비스 워커를 자동으로 생성하고, <code>manifest.json</code>까지 주입</strong>해주기 때문에 개발자가 설정해야 할 요소들을 줄여줍니다.</p>
<h3 id="vite-plugin-pwa-활용-pwa-기본-요소">vite-plugin-pwa 활용 PWA 기본 요소</h3>
<p><code>vite-plugin-pwa</code> 라이브러리를 사용할 때 설정해야 하는 요소들이 몇 가지 있습니다. 설정 과정에서 직접 사용했던 옵션을 중심으로 정리해보겠습니다.</p>
<h4 id="🚀-vitepwa-플러그인-옵션">🚀 VitePWA 플러그인 옵션</h4>
<ul>
<li><p><strong><code>registerType: &#39;autoUpdate&#39;</code></strong>
새로운 버전이 배포되었을 때, 최신 서비스 워커를 자동으로 활성화하도록 설정합니다.</p>
</li>
<li><p><strong><code>devOptions: {enabled: true}</code></strong>
원래 PWA 관련 기능은 별도의 빌드 환경에서만 동작하지만, 이 옵션을 켜두면 로컬 <strong>개발 환경에서도 PWA 기능을 바로 테스트</strong>할 수 있습니다.</p>
</li>
<li><p><strong><code>includeAssets</code></strong>
서비스워커가 캐싱할 정적 에셋(로고 이미지, 파비콘 등)의 목록을 배열로 지정합니다. 해당 배열에 <strong>포함된 요소들은 미리 캐싱(<code>precache</code>)되기 때문에, 오프라인 상태에서도 아이콘이 정상적으로 보이게 해줍니다.</strong></p>
</li>
</ul>
<h4 id="📝-manifest-옵션">📝 Manifest 옵션</h4>
<p>Manifest는 브라우저에게 이 웹앱이 사용자의 기기에 설치될 때 어떻게 보여야 하는지를 알려주는 설정 명세서입니다.</p>
<ul>
<li><p><strong><code>name</code>, <code>short_name</code></strong>
<code>name</code>은 <strong>설치 팝업이나 앱 실행 화면에 표시될 전체 이름</strong>입니다. 반면 <code>short_name</code>은 설치 후 홈 화면의 아이콘 아래에 표시될 짧은 이름입니다. 개발자나 기획 측 의도에 맞는 이름을 자유롭게 설정할 수 있습니다.</p>
</li>
<li><p><strong><code>description</code></strong>
해당 웹앱에 대한 간단한 설명입니다. 검색 엔진 또는 앱 설치 프롬프트에서 활용될 수 있습니다.</p>
</li>
<li><p><strong><code>theme_color</code></strong>
상태 표시줄이나 브라우저 테마 등 OS 수준에서 앱을 감싸는 UI의 배경색을 지정합니다. 이 또한 디자인 의도에 맞게 설정할 수 있습니다.</p>
</li>
<li><p><strong><code>icons</code></strong>
앱이 기기에 설치되었을 때 표시될 아이콘들의 배열입니다. 최소한 <code>192*192</code>, <code>512*512</code> 두 가지 사이즈의 이미지를 제공합니다. 추가로 iOS 기기 대응을 위해 <code>180*180</code> 사이즈의 전용 아이콘도 포함했습니다. 내부에 있는 <code>purpose: &#39;any maskable&#39;</code> 속성은 <strong>안드로이드 환경에서 디바이스 마스킹 규칙에 맞춰 아이콘이 자연스럽게 잘릴 수 있도록</strong> 도와줍니다.</p>
</li>
</ul>
<ul>
<li><p><strong><code>display: &#39;standalone&#39;</code></strong>
앱이 실행될 때 브라우저의 URL 바 등 <strong>기본 브라우저 UI를 최소화하거나 숨깁니다.</strong> 유저는 이 앱이 웹 브라우저에서 돌아가는 것이 아니라, 네이티브 앱을 사용하는 것처럼 느끼게 합니다. (상태 표시줄까지 숨기는 <code>fullscreen</code> 등 그 외 옵션 또한 존재합니다.)</p>
</li>
<li><p><strong><code>start_url: &#39;/login&#39;</code></strong>
유저가 PWA 아이콘을 클릭해서 앱을 실행했을 때, <strong>최초로 진입할 경로</strong>를 지정할 수 있습니다. 현재 저희는 앱 실행 시 바로 로그인 화면을 마주하게끔 했습니다.</p>
</li>
<li><p><strong><code>background_color</code></strong>
앱을 처음 켰을 때 화면에 콘텐츠가 렌더링 되기 전까지 잠깐 노출되는 배경색입니다.</p>
</li>
</ul>
<h3 id="커스텀-service-worker-설정">커스텀 Service Worker 설정</h3>
<p>위에서는 라이브러리가 서비스 워커를 알아서 만들어주는 방식이라면, <strong>직접 작성한 서비스 워커 파일을 사용</strong>할 수도 있습니다.</p>
<p>AMP 서비스는 푸시 알림 기능을 직접 제어해야 했기 때문에, 자동 생성 방식보다는 커스텀 서비스 워커를 사용할 수 있는 <code>injectManifest</code> 전략을 선택했습니다.</p>
<ul>
<li><p><strong><code>strategies: &#39;injectManifest&#39;</code></strong>
<code>vite-plugin-pwa</code>의 기본 전략인 <code>generateSW</code>, 즉 자동 생성 대신 <strong>개발자가 직접 작성한 서비스 워커 코드를 사용하겠다고 선언</strong>하는 옵션입니다. 빌드 시 <code>Manifest</code>만 우리가 만든 서비스 워커 파일에 주입해줍니다.</p>
</li>
<li><p><strong><code>srcDir</code>, <code>filename</code></strong>
우리가 작성한 커스텀 서비스 워커 파일의 경로와 파일명을 선택하는 옵션입니다.</p>
</li>
</ul>
<h3 id="실제-코드로-보면">실제 코드로 보면?</h3>
<p>실제 코드로 보면, <code>vite.config.ts</code> 파일 내부 <code>plugin</code> 배열에 아래와 같이 설정했습니다.</p>
<pre><code class="language-ts">VitePWA({
        injectRegister: null,

        registerType: &#39;autoUpdate&#39;,
        devOptions: {
          enabled: true,
          type: &#39;module&#39;,
        },

        strategies: &#39;injectManifest&#39;,
        srcDir: &#39;src&#39;,
        filename: &#39;sw.ts&#39;,

        includeAssets: [
          &#39;favicon.svg&#39;,
          &#39;amp-pwa-logo-192.png&#39;,
          &#39;amp-pwa-logo-512.png&#39;,
          &#39;amp-pwa-logo-180.png&#39;,
        ],
        manifest: {
          name: &#39;AMP&#39;,
          short_name: &#39;AMP&#39;,
          description: &#39;작은 공지도 크게 울리게 공연 공지의 공식, AMP&#39;,
          start_url: &#39;/login&#39;,
          display: &#39;standalone&#39;,
          background_color: &#39;#ffffff&#39;,
          theme_color: &#39;#ffffff&#39;,
          icons: [
            {
              src: &#39;/amp-pwa-logo-180.png&#39;,
              sizes: &#39;180x180&#39;,
              type: &#39;image/png&#39;,
            },
            {
              src: &#39;/amp-pwa-logo-192.png&#39;,
              sizes: &#39;192x192&#39;,
              type: &#39;image/png&#39;,
            },
            {
              src: &#39;/amp-pwa-logo-512.png&#39;,
              sizes: &#39;512x512&#39;,
              type: &#39;image/png&#39;,
            },
            {
              src: &#39;/amp-pwa-logo-512.png&#39;,
              sizes: &#39;512x512&#39;,
              type: &#39;image/png&#39;,
              purpose: &#39;any maskable&#39;,
            },
          ],
        },
      }),</code></pre>
<hr>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/aff94803-8414-4e1d-9d8f-559872c0d5b8/image.jpg" alt=""></p>
<p>PWA 설정이 정상적으로 완료되면, 위와 같이 <strong>웹앱 형식으로 설치가 가능</strong>합니다.</p>
<p>이렇듯 <code>Vite</code> 환경에서 개발을 진행한다면, <strong><code>vite-plugin-pwa</code> 라이브러리</strong>를 통해 복잡한 설정 없이도 <strong><code>Vite</code>의 빌드 파이프라인 안에서 비교적 간단하게 PWA를 구축</strong>할 수 있었습니다.</p>
<p>각자 서비스의 상황에 맞게, 필요한 옵션과 적절한 <code>Manifest</code>를 설정하면 될 것 같습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[그라디언트가 문제? SVG 렌더링 오류 파헤치기]]></title>
            <link>https://velog.io/@jin_wls/%EA%B7%B8%EB%9D%BC%EB%94%94%EC%96%B8%ED%8A%B8%EA%B0%80-%EB%AC%B8%EC%A0%9C-SVG-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%98%A4%EB%A5%98-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@jin_wls/%EA%B7%B8%EB%9D%BC%EB%94%94%EC%96%B8%ED%8A%B8%EA%B0%80-%EB%AC%B8%EC%A0%9C-SVG-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%98%A4%EB%A5%98-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Mon, 20 Apr 2026 04:26:21 GMT</pubDate>
            <description><![CDATA[<p>개발을 진행하다보면, 때로 원인을 찾기 힘든 오류를 만나게 됩니다. </p>
<p>최근 개발을 진행하며 마주했던, SVG 관련 오류와 그 문제를 해결하고자 시도하고 고민했던 시간들을 기록하고자 합니다. </p>
<h3 id="갑자기-아이콘이-왜-사라질까">갑자기 아이콘이 왜 사라질까?</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/cdf7e799-d592-4fec-be15-4f2665abfd55/image.png" alt=""></p>
<p>문제 상황은 위와 같은 뷰에서 발생했습니다. 간단하게만 설명하면, 공연 정보를 안내하는 <strong>카드 컴포넌트가 리스트 형태로 나열된 형태</strong>입니다. 오른쪽에 그라데이션 깃발 버튼은 클릭하여 <strong>관람 예정 공연 등록 및 해제</strong>를 할 수 있는 토글 버튼입니다.</p>
<p>모바일 웹 또는 PWA 환경에서 테스트를 하던 중 처음 보는 오류를 발견했습니다. <strong>관람 예정 탭에서 특정 공연 카드의 &#39;관람 예정 해제 버튼&#39;을 누르면, 리스트가 갱신되면서 엉뚱하게도 아래에 위치한 다른 카드의 깃발 아이콘이 사라지는 현상</strong>이었습니다.</p>
<p>데스크탑 환경에서는 문제가 없어서 발견하지 못했었는데, 모바일 환경에서만 해당 오류가 발생했습니다.</p>
<hr>
<h3 id="리액트의-렌더링-동기화-문제일까">리액트의 렌더링 동기화 문제일까?</h3>
<p>가장 먼저 의심했던 것은 <strong>리액트의 리스트 렌더링 방식</strong>이었습니다. </p>
<p>리액트는 리스트 렌더링 시 <code>key</code>를 기준으로 컴포넌트의 동일성을 판단하고, <strong>동일한 <code>key</code>를 가진 컴포넌트를 재사용</strong>하려 합니다. 따라서 <code>key</code>가 <code>index</code>인 경우 동일한 <code>key</code>를 가진 하위 컴포넌트를 재사용하려고 하기 때문에, 실제 데이터와 다른 컴포넌트가 매칭되어 예상치 못한 오류가 발생할 수 있습니다.</p>
<p>이를 현재 발생한 오류 상황에 대입해보면, 리스트 렌더링 과정에서 <code>key</code>가 적절하지 않을 경우 컴포넌트가 재사용되면서 예상치 못한 문제가 발생할 수 있다고 생각했습니다.</p>
<h4 id="해결-시도">해결 시도</h4>
<p>따라서 <code>FlagButton</code> 컴포넌트에 카드의 고유 값인 <strong><code>festivalId</code>를 <code>key</code>로 할당</strong>하여, 노드를 재사용하지 않고 새로 생성하여 누락 없이 아이콘을 다시 그리도록 시도했습니다.</p>
<p>위 변경사항으로 오류가 해결되기를 기대했지만, SVG 아이콘이 사라지는 문제는 여전히 지속되었습니다. <strong>이제 렌더링 방식은 원인이 아니라는 것을 확실히 알게 되었고, 다른 원인을 생각해봐야 했습니다.</strong></p>
<hr>
<h3 id="그라디언트-svg-자체가-원인은-아닐까">그라디언트 SVG 자체가 원인은 아닐까?</h3>
<p>위에서 문제를 해결하지 못하고 다른 방향으로 접근했기에, <strong>원점</strong>으로 돌아가 처음부터 천천히 다시 생각했습니다. </p>
<p>그러던 중 한 가지 단서를 찾을 수 있었습니다. <strong>일반 단색 SVG는 사라지지 않는데, 그라디언트가 적용된 SVG에서만 오류가 발생</strong>한다는 것이었습니다. 그러면 혹시 그라디언트 SVG 자체에 원인이 있을 수 있겠다는 생각이 들어, 내부 코드를 열어봤습니다.</p>
<pre><code class="language-jsx">&lt;linearGradient id=&quot;paint0_linear_524_15277&quot; x1=&quot;9.64982&quot; y1=&quot;4.34995&quot; x2=&quot;24.4999&quot; y2=&quot;15&quot; &gt;
...
&lt;/linearGradient&gt;</code></pre>
<p>여기서 답을 찾을 수 있었습니다. 원인은 <strong>SVG 파일 내부의 <code>id</code> 속성</strong>에 있었습니다.</p>
<p>현재 모든 SVG를 <code>svgr</code>을 사용해 인라인 리액트 컴포넌트로 렌더링하고 있습니다. </p>
<ul>
<li><p>인라인으로 위 그라디언트 SVG를 여러 번 렌더링하면, DOM 트리에 <code>paint0_linear_524_15277</code>이라는 <strong>동일한 id를 가진 요소가 중복해서 생성</strong>됩니다. </p>
</li>
<li><p>이 상황에 리스트에서 요소 하나가 삭제되면, 브라우저가 <strong>삭제된 컴포넌트에 포함되어 있던 linearGradient 정의도 함께 제거</strong>합니다. 남아있는 다른 아이콘들은 여전히** 같은 id를 참조하고 있지만, 참조할 대상이 사라졌으므로 그라디언트 색상을 칠하지 못하고 투명하게 렌더링** 되었다고 판단할 수 있습니다.</p>
</li>
</ul>
<p>결국 문제의 핵심은 SVG의 id는 <strong>전체에서 공유되는 전역 값</strong>이기 때문에, 동일한 SVG를 여러 번 렌더링할 경우 예상치 못한 충돌이 발생할 수 있다는 것입니다.</p>
<hr>
<h3 id="그렇다면-어떻게-해결할-수-있을까">그렇다면 어떻게 해결할 수 있을까</h3>
<p>이제 핵심 원인으로 추측되는 요소를 찾았고, 해결 방법을 찾아야 했습니다.</p>
<p>크게 2가지 방법을 사용해볼 수 있습니다.</p>
<ul>
<li><strong>useId 활용 컴포넌트 생성</strong> : 래퍼 컴포넌트를 따로 정의하여 useId로 매번 고유한 아이디를 생성해 주입하는 방법</li>
<li><strong>img 태그로 격리</strong> : svgr 방식을 포기하고 img 태그로 격리</li>
</ul>
<h4 id="📌-useid-활용-컴포넌트-생성">📌 useId 활용 컴포넌트 생성</h4>
<pre><code class="language-tsx">import { useId } from &#39;react&#39;;

const GradientFlagIcon = () =&gt; {
  const id = useId();

  // 각각 다른 gradient id 생성
  const gradient0 = `paint0-${id}`;
  const gradient1 = `paint1-${id}`;

  return (
    &lt;svg
      width=&quot;24&quot;
      height=&quot;24&quot;
      viewBox=&quot;0 0 24 24&quot;
      fill=&quot;none&quot;
      xmlns=&quot;http://www.w3.org/2000/svg&quot;
    &gt;
      &lt;path
        d= // 생략
        fill={`url(#${gradient0})`}
      /&gt;
      &lt;path
        d= // 생략
        fill={`url(#${gradient1})`}
      /&gt;

      &lt;defs&gt;
        &lt;linearGradient
          id={gradient0}
          x1=&quot;9.64982&quot;
          y1=&quot;4.34995&quot;
          x2=&quot;24.4999&quot;
          y2=&quot;15&quot;
          gradientUnits=&quot;userSpaceOnUse&quot;
        &gt;
          &lt;stop stopColor=&quot;#ACF6D6&quot; /&gt;
          &lt;stop offset=&quot;1&quot; stopColor=&quot;#16C284&quot; /&gt;
        &lt;/linearGradient&gt;

        &lt;linearGradient
          id={gradient1}
          x1=&quot;7.68927&quot;
          y1=&quot;1&quot;
          x2=&quot;9.68927&quot;
          y2=&quot;23&quot;
          gradientUnits=&quot;userSpaceOnUse&quot;
        &gt;
          &lt;stop stopColor=&quot;#6EE7B7&quot; /&gt;
          &lt;stop offset=&quot;1&quot; stopColor=&quot;#34D399&quot; /&gt;
        &lt;/linearGradient&gt;
      &lt;/defs&gt;
    &lt;/svg&gt;
  );
};

export default GradientFlagIcon;
</code></pre>
<p>첫 번째는 id 충돌을 방지하기 위해 svgr 방식을 사용하지 않고, <strong>리액트의 <code>useId</code> 훅을 사용하여 래퍼 아이콘 컴포넌트를 따로 생성</strong>하는 것입니다.</p>
<p>렌더링마다 고유한 id를 생성하고, 해당 값을 <code>&lt;linearGradient&gt;</code>의 <code>id</code>와 <code>&lt;path&gt;</code>의 <code>fill</code> 속성에 동일하게 적용했습니다. 기존 svg 코드에서 id 할당 방식을 제외하고는 모두 동일합니다. 이렇게 하면 각 컴포넌트 인스턴스가 <strong>서로 다른 id를 가지게 되어 충돌 없이 안전하게 그라디언트를 사용</strong>할 수 있습니다. </p>
<p>하지만 이렇게 했을 때, 두 가지 단점도 존재합니다.</p>
<ul>
<li><p>아이콘 하나 때문에 따로 컴포넌트를 생성했으므로, <strong>유지보수 포인트가 늘어납니다.</strong></p>
</li>
<li><p><strong>런타임에 그라디언트 색상을 동적으로 바꿔야 하는 상황</strong>이 아니라면, 굳이 React 컴포넌트로 관리할 필요가 없습니다.</p>
</li>
</ul>
<h4 id="📌-img-태그로-격리">📌 img 태그로 격리</h4>
<p>두 번째 방식은 <strong>SVG 아이콘을 img 태그 안에 넣어서 격리</strong>하는 방식입니다.</p>
<p>현재 그라디언트 깃발 아이콘은 <strong>런타임에 색상이나 형태를 동적으로 조작할 필요가 없는 정적 아이콘</strong>입니다. 따라서 간단하게는 img 태그를 사용하여 격리해도 되겠다고 생각했습니다. <strong>img 태그로 SVG를 불러오면 브라우저는 해당 리소스를 메인 DOM과 분리된 독립적인 문서로 취급</strong>합니다.</p>
<p>따라서 <strong>외부 DOM에 있는 id와 충돌이 일어나지 않아, 동일하게 오류를 해결</strong>할 수 있습니다.</p>
<h4 id="💭-svgo-prefixids는-안될까">💭 SVGO prefixIds는 안될까?</h4>
<p>이번 오류를 해결하기 위해 찾아보던 중, SVGO의 <code>prefixIds</code> 플러그인을 발견했습니다.</p>
<p>이 플러그인도 id 충돌 방지를 위해 사용하기 때문에 언뜻 보면 이번 오류 또한 해결할 수 있을 것이라 생각했으나 <strong>실제로 시도해보니 이 상황에서는 통하지 않았습니다.</strong></p>
<p>이유는 아래와 같습니다.</p>
<ul>
<li><p>SVGO는 <strong>빌드 타임 도구</strong>입니다. <code>prefixIds</code>는 코드를 빌드할 때 <strong>파일명을 기반으로 id에 고정된 접두사</strong>를 붙여줍니다. 따라서 아이콘 간 id 중복을 막기 때문에, <strong>서로 다른 SVG 파일 간의 충돌을 막는데에는 유용</strong>합니다.</p>
</li>
<li><p>하지만 지금 우리의 상황은 런타임에 동일한 컴포넌트를 배열의 map을 돌며 여러 번 렌더링 하는 방식입니다. <strong>빌드 타임에 아무리 접두사를 붙여도, 런타임에는 똑같은 접두사가 붙은 id가 여러 번 사용되므로 사실상 동일한 오류가 발생</strong>할 수 밖에 없습니다.</p>
</li>
</ul>
<hr>
<p>물론 더 나은 해결 방법이 있을 수 있습니다. </p>
<p>다만 이번 글은 해결 방법보다는, 원인을 찾아가는 과정에 더 큰 의미가 있다고 생각합니다. <strong>생각하지 못했던 원인을 향해, 내가 생각했던 원인들을 하나씩 소거하며 다가갔다는 점</strong>에 의미가 있는 것 같습니다.</p>
<p>만약 비슷한 문제를 겪고 있다면, 이 글이 작은 힌트가 되어 조금이나마 도움이 되기를 바라는 마음입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[공지 업로드에 7초, 내가 사용자라면 기다려줄까?]]></title>
            <link>https://velog.io/@jin_wls/%EA%B3%B5%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C%EC%97%90-7%EC%B4%88-%EB%82%B4%EA%B0%80-%EC%82%AC%EC%9A%A9%EC%9E%90%EB%9D%BC%EB%A9%B4-%EA%B8%B0%EB%8B%A4%EB%A0%A4%EC%A4%84%EA%B9%8C</link>
            <guid>https://velog.io/@jin_wls/%EA%B3%B5%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C%EC%97%90-7%EC%B4%88-%EB%82%B4%EA%B0%80-%EC%82%AC%EC%9A%A9%EC%9E%90%EB%9D%BC%EB%A9%B4-%EA%B8%B0%EB%8B%A4%EB%A0%A4%EC%A4%84%EA%B9%8C</guid>
            <pubDate>Wed, 08 Apr 2026 17:49:47 GMT</pubDate>
            <description><![CDATA[<figure>
  <img src="https://velog.velcdn.com/images/jin_wls/post/76f139ad-8c3d-4077-b101-7666b6345363/image.png" />
  <figcaption>이미지 출처: 아임웹</figcaption>
</figure>

<br/>

<p>최근 앱잼 프로젝트로 진행했던 AMP 프로젝트의 스프린트 개발을 진행하고 있습니다. 앱잼 기간 내에 구현하지 못했던 기능들을 추가하고, 아직 갈 길이 멀지만 리팩토링도 함께 진행중입니다.</p>
<p>AMP 서비스의 핵심 기능을 하나만 고르자면, 공연 공지 업로드입니다. 그러다보니 이미지와 관련한 크고 작은 이슈들이 발생하고 있습니다. 이번 글에서는 여러 이슈 중 하나를 챌린징 요소로 지정하고, 이를 어떻게 해결했는지를 기록으로 남기고자 합니다.</p>
<h4 id="문제-상황">문제 상황</h4>
<p>이번 스프린트 기간에 기존 1장만 업로드 가능하던 공지 이미지가, <strong>최대 20장까지 업로드 가능</strong>하도록 변경되면서 문제가 발생했습니다.</p>
<p>하나의 이미지 용량을 5MB로 제한했음에도 불구하고, 20장을 꽉 채워서 공지 업로드를 진행했을 때 <strong>평균 7초에서 길게는 10초</strong>가 넘는 대기 시간이 걸렸습니다. 내부적으로 사용자 경험을 크게 저하하는 핵심 요소로 해당 이슈가 지적되었고, 이를 반드시 해결해야만 했습니다.</p>
<h4 id="처리-방법-선정">처리 방법 선정</h4>
<p>해당 문제의 해결을 위해 여러 방법이 논의되었습니다.</p>
<p>가장 먼저 언급되었던 방법은 <strong>API 분리</strong>입니다. 현재는 전체 공지 업로드를 처리하는 API가 하나로 구현되어 있는데, 이를 이미지 처리 API와 텍스트 처리 API로 분리하는 방식입니다. 좋은 대응 방안이지만, 백엔드와 추가적인 논의 및 작업이 필요해서 바로 적용하기는 어려웠습니다.</p>
<p>그럼 이제 API 분리가 아닌, 프론트엔드 단에서 바로 적용하여 효과를 낼 수 있는 방법을 찾아야 했습니다. 결론적으로, <strong>이미지 압축</strong>을 구현하기로 했습니다.</p>
<p>프론트엔드 단에서 먼저 이미지를 일정 용량으로 압축하고, 압축 효율이 좋은 webp 포맷으로 변환 후 서버로 전달하는 방법을 선택했습니다. </p>
<h4 id="라이브러리-선택">라이브러리 선택</h4>
<p>이미지 압축을 위해 몇 가지 라이브러리를 검토했으나, <code>browser-image-compression</code>이 가장 적절하다고 판단하여 해당 라이브러리 도입을 결정했습니다. 주요 이유는 아래와 같습니다.</p>
<ul>
<li><strong>Web Worker 기본 지원</strong> : 다중 이미지 압축 연산을 백그라운드로 넘겨, 압축 중에도 스크롤 및 타이핑 등 UI 조작이 가능합니다.</li>
<li><strong>Promise 기반 병렬 처리</strong> : <code>async/await</code> 문법을 활용하여 병렬 처리 로직을 직관적으로 구현 가능합니다.</li>
<li>*<em>낮은 도입 장벽 *</em>: 도입 시 추가로 설정해야 하는 요소가 적고, 도입 장벽이 낮습니다.</li>
</ul>
<h4 id="이미지-압축-구현">이미지 압축 구현</h4>
<p>라이브러리 사용 방법은 이미 다른 아티클 또는 많은 자료에서 찾아볼 수 있으므로, 전반적으로 어떻게 구현하였는지 간단하게만 언급하고자 합니다.</p>
<p>우선 압축 옵션을 설정하는 <code>options</code> 객체를 생성했습니다.</p>
<p>해당 <code>options</code> 객체를 활용하여 <strong>최대 용량, 해상도, WebWorker 사용 여부, 파일 타입 변환</strong> 등을 설정할 수 있습니다.</p>
<pre><code class="language-tsx">const options: Options = {
    maxSizeMB: 1,
    maxWidthOrHeight: 1920,
    useWebWorker: true,
    fileType: &#39;image/webp&#39;,
  };</code></pre>
<p>압축 옵션을 설정했다면, 이제 실제 여러 장의 이미지를 받아 압축을 진행하는 함수인 <code>compressImageFiles</code>를 구현할 차례입니다. 전체적인 코드는 다음과 같습니다.</p>
<pre><code class="language-tsx">import imageCompression, { type Options } from &#39;browser-image-compression&#39;;

export interface CompressTargetImage {
  id: string;
  file: File;
}

export const compressImageFiles = async (
  items: CompressTargetImage[],
): Promise&lt;CompressTargetImage[]&gt; =&gt; {
  const options: Options = {
    maxSizeMB: 1,
    maxWidthOrHeight: 1920,
    useWebWorker: true,
    fileType: &#39;image/webp&#39;,
  };

  return Promise.all(
    items.map(async (item) =&gt; {
      try {
        const compressedBlob = await imageCompression(item.file, options);

        // 1. 압축 후 용량이 오히려 커진 경우 원본 유지
        if (compressedBlob.size &gt;= item.file.size) {
          return { id: item.id, file: item.file };
        }

        // 2. 서버 전송을 위해 Blob을 File 객체로 변환
        const compressedFile = new File([compressedBlob], item.file.name, {
          type: compressedBlob.type,
        });

        return { id: item.id, file: compressedFile };
      } catch {
        // 3. 압축 중 에러 발생 시 원본 파일 반환 (업로드 중단 방지)
        return { id: item.id, file: item.file };
      }
    }),
  );
};</code></pre>
<p>하나씩 단계별로 뜯어서 주요 포인트만 살펴보면 다음과 같습니다.</p>
<p><strong>1. <code>Promise.all</code>을 활용한 병렬 처리</strong>
    최대 20장의 이미지를 순차적으로 압축하면 시간이 길어질 수밖에 없습니다. 따라서 <code>Promise.all</code>과 <code>map</code>을 활용해 <strong>여러 이미지를 동시에 비동기적으로 압축</strong>하도록 구현하여 처리 속도를 높였습니다. 또한 옵션에서 설정한 <code>useWebWorker</code>로 인해 각 압축 작업을 Web Worker에서 처리하여, 무거운 연산이 메인 스레드를 막지 않습니다. <strong>(압축 중에도 UI 조작은 여전히 가능)</strong>
    <br/>
<strong>2. 무조건적인 압축 하지 않기</strong>
    이미지 압축을 진행하다 보면, 이미 최적화가 잘 되어 있는 일부 파일의 경우 포맷 변환 및 압축 과정에서 오히려 결과물의 용량이 커지는 현상이 발생하기도 합니다. 이를 방지하기 위해 <code>size</code>를 비교하는 조건문을 추가하여, <strong>압축 효율이 없는 경우에는 원본 파일을 그대로 반환</strong>하도록 처리했습니다.</p>
<br/>

<p><strong>3. 기존 로직과의 호환성을 위한 <code>File</code> 객체 변환</strong>
    <code>browser-image-compression</code> 라이브러리의 결과물은 파일 이름과 같은 메타데이터가 없는 <code>Blob</code> 형태로 반환됩니다. 이를 서버에 그대로 전송하면, 기존 API 로직과 연동되지 않습니다. 따라서 기존 이미지 업로드 로직과 원활하게 연동되도록 <code>new File()</code> 생성자를 사용해 온전한 <code>File</code> 객체로 다시 변환해주었습니다.</p>
<br/>

<p><strong>4. 안전성 확보를 위한 에러 핸들링</strong>
    특정 이미지 파일이 손상되었거나, 예측 불가능한 이유로 라이브러리에서 에러가 발생할 수 있습니다. 이때 전체 업로드 프로세스가 중단되는 것을 막기 위해 <code>try-catch</code> 문법을 사용하여, <code>catch</code> 블록에서 에러를 잡고 <strong>압축에 실패한 이미지는 원본 상태 그대로 반환</strong>하여 사용자의 업로드 경험이 끊기지 않도록 방어했습니다.</p>
<h4 id="파일명-확장자-불일치-이슈-해결">파일명 확장자 불일치 이슈 해결</h4>
<p>이렇게 구현했을 때 한 가지 이슈가 발생했습니다. 바로 <strong>파일명 확장자 불일치 이슈</strong>입니다.</p>
<p>위 로직을 다시 한번 살펴보면, <code>browser-image-compression</code> 옵션에 <code>fileType: &#39;image/webp&#39;</code>를 설정했기 때문에, 압축이 완료된 결과물은 webp 형태입니다.</p>
<p>다만 이를 <code>File</code> 객체로 다시 포장하는 과정에서, 기존 원본 파일의 이름(<code>item.file.name</code>)을 그대로 할당하며 문제가 발생했습니다.</p>
<p>예를 들어, 사용자가 업로드한 원본 파일명이 photo.jpg였다고 가정하겠습니다. 위 로직을 그대로 적용하면 내용물과 MIME 타입은 webp인데, 파일 이름은 여전히 .jpg로 끝나는 확장자 불일치 상태가 됩니다. 지금 당장은 문제가 되지 않더라도, 서버 유효성 검사 실패 등 여러 사이드 이펙트가 발생할 수 있는 이슈였습니다.</p>
<p>따라서 이러한 문제를 해결하기 위해, 정규 표현식을 활용하여 기존 파일명에서 확장자를 추출해 <code>.webp</code>로 교체하는 로직을 추가했습니다.</p>
<pre><code class="language-tsx">// 1. 파일명 확장자 교체 로직
const newFileName = item.file.name.includes(&#39;.&#39;)
  ? item.file.name.replace(/\.[^/.]+$/, &#39;.webp&#39;) 
  : `${item.file.name}.webp`; // 확장자가 아예 없는 엣지 케이스 처리

// 2. 확장자가 올바르게 변경된 이름으로 File 객체 생성
const compressedFile = new File([compressedBlob], newFileName, {
  type: compressedBlob.type,
});</code></pre>
<h4 id="개선-결과">개선 결과</h4>
<p>이렇게 캡슐화한 압축 로직을 기존 이미지 업로드 훅에 활용하여 테스트를 진행했습니다.</p>
<p>현재 업로드 가능 최대 이미지 용량이 5MB 이하 최대 20장인 점을 감안하여, 2MB/3MB 이미지 각 10장씩 총 20장 업로드를 반복적으로 테스트했습니다.</p>
<p>기기 사양이나 네트워크 환경에 따라 차이가 있겠지만,</p>
<ul>
<li>기존 소요 시간 : <strong>평균 약 7.24초</strong></li>
<li>개선 후 소요 시간 : *<em>평균 약 1.65초 *</em></li>
</ul>
<p>결과적으로 업로드 대기 시간을 약 <strong>77.2%</strong> 개선할 수 있었습니다. </p>
<p>물론 이 방법이 최선의 방법이 아닐 수도 있고, 기존에 시도하고자 했던 API 분리와 같은 방법이 더 효과적일 수도 있습니다. 그러나 지금으로서 주어진 일정 내에 백엔드와의 추가적인 의존성 없이, 프론트엔드 주도적으로 유의미한 개선 결과를 냈다는 점에 의미가 있다고 생각합니다.</p>
<p>추후 서비스가 더욱 확장된다면, 백엔드 및 인프라 관점에서의 최적화도 함께 논의해 볼 계획입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우리가 모노레포를 도입한다면?]]></title>
            <link>https://velog.io/@jin_wls/%EC%9A%B0%EB%A6%AC%EA%B0%80-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%9C%EB%8B%A4%EB%A9%B4</link>
            <guid>https://velog.io/@jin_wls/%EC%9A%B0%EB%A6%AC%EA%B0%80-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%9C%EB%8B%A4%EB%A9%B4</guid>
            <pubDate>Fri, 02 Jan 2026 08:42:24 GMT</pubDate>
            <description><![CDATA[<h3 id="1-모노레포란">1. 모노레포란?</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/9694e3de-68e1-4378-a394-6d321198fc87/image.png" alt="">
모노레포(Monorepo)는 <strong>여러 프로젝트를 하나의 Git 저장소에서 관리하는 방식</strong>을 말합니다. </p>
<p>우리가 지금까지 흔히 사용했던 멀티레포(Multirepo), 즉 <strong>프로젝트나 서비스별로 각각 독립된 레포지토리에서 관리하는 방식</strong>과는 대조되는 방식입니다. </p>
<h3 id="2-모노레포는-왜-필요할까">2. 모노레포는 왜 필요할까?<img src="https://velog.velcdn.com/images/jin_wls/post/ecde0fb1-40eb-4b49-b2b9-31859dee3d0c/image.png" alt=""></h3>
<p>모노레포의 등장 배경에는** 현대 웹 개발의 복잡성<strong>이 그 시작입니다. 하나의 서비스를 개발한다고 했을 때 웹 프론트엔드, 모바일 앱, 관리자 대시보드 등 여러 프로젝트가 필요한데, 이들이 **서로 밀접하게 연관되어 있음에도 멀티레포 방식을 채택했을 때 아래와 같은 여러 문제점</strong>들이 발생합니다.</p>
<p>**[ 전통적인 멀티레포 방식의 문제점 ] **</p>
<ol>
<li>공통으로 사용되는 <strong>코드 재사용 및 공유의 어려움</strong></li>
<li>프로젝트 별로 <strong>서로 다른 패키지 의존성</strong></li>
<li>프로젝트 별 <strong>개발 환경 불일치</strong> (lint 규칙 등)</li>
<li>코드가 여러 레포지토리에 분산되고 <strong>협업의 어려움</strong></li>
</ol>
<p>위와 같은 문제로 인해, 특정 레포지토리 담당자 외의 팀원이 다른 레포지토리에서 작업을 하려면 <strong>코드와 개발 환경을 처음부터 다시 파악하기 위해 많은 시간이 소요</strong>되고, 초기 온보딩 비용 또한 증가하게 됩니다. 또한 공통된 코드에 대한 유지 보수 및 관리 난이도도 높아집니다.</p>
<h3 id="3-모노레포-도입-시-얻을-수-있는-이점">3. 모노레포 도입 시 얻을 수 있는 이점</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/67e25014-498c-446e-8f9a-337448ee5867/image.png" alt=""></p>
<p>모노레포를 잘 도입하면 여러 이점을 얻을 수 있습니다.</p>
<h4 id="코드-공유-및-재사용성-향상">코드 공유 및 재사용성 향상</h4>
<p>공통 컴포넌트가 같은 비지니스 로직이 여러 프로젝트에 걸쳐 사용되었을 때, <strong>한 곳에서 관리하고 즉시 다른 프로젝트에서 사용</strong>할 수 있습니다. 뿐만 아니라, 기존에는 오류가 발생했을 때 각각의 레포지토리에서 수정하고 반영해야 하지만 이제는 한 곳에서 관리하기 때문에 모든 프로젝트에 즉시 적용됩니다. 마지막으로 컴포넌트와 유틸리티의 버전 관리가 용이합니다.</p>
<h4 id="의존성-관리-효율화">의존성 관리 효율화</h4>
<p>전체 프로젝트의 <strong>의존성을 분산하지 않고 중앙에서 관리</strong>할 수 있습니다. 라이브러리를 사용한다고 했을 때, 모든 프로젝트에서 동일한 버전의 라이브러리를 사용할 수 있고 의존성 업데이트 시 한 번에 모든 프로젝트에 적용할 수 있습니다.</p>
<h4 id="개발-환경-통일">개발 환경 통일</h4>
<p>모든 프로젝트가 동일한 lint 규칙을 적용받기 때문에, <strong>코드 컨벤션을 동일하게 유지</strong>할 수 있습니다. 뿐만 아니라 <strong>일관된 빌드 프로세스와 동일한 테스트 환경으로 품질 관리가 용이</strong>합니다.</p>
<h4 id="리팩토링-효율화">리팩토링 효율화</h4>
<p>전체 코드 베이스에 대한 변경이나 프로젝트 간 의존성이 있는 변경도 <strong>단일 PR로 관리</strong>할 수 있습니다.</p>
<h4 id="협업-효율성-증가">협업 효율성 증가</h4>
<p>모든 프로젝트의 코드를 하나의 레포지토리에서 찾고 수정할 수 있기 때문에, <strong>프로젝트 간 지식 공유가 쉽고 일관된 코딩 스타일과 패턴을 유지</strong>할 수 있습니다.</p>
<h3 id="4-무조건-모노레포">4. 무조건 모노레포?</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/95006220-780b-4bf1-b659-fac977bb2e73/image.png" alt=""></p>
<p>사실 어떤 것이든 무조건적인 것은 없습니다. 각자의 상황에서 모노레포와 멀티레포를 선택했을 때의 <strong>장단점을 비교해보고 현재 팀의 상황에 더 적절한 방식을 선택</strong>하면 됩니다.</p>
<p>모노레포는 중앙 집중형으로 <strong>개발 환경, 코드, 컨벤션 등을 한번에 관리하고 공유하기 때문에 효율성을 극대화</strong>합니다. 그 덕분에 개발자들은 불필요하게 매번 동일한 작업을 반복하지 않아도 되고, <strong>비지니스 가치에 더욱 집중할 수 있다는 이점</strong>이 있습니다.</p>
<p>멀티레포의 경우, <strong>프로젝트를 작은 단위로 쪼개서 격리하고 권한을 세분화하기 때문에 부분적인 실패가 전체로 확산되는 것을 방지</strong>합니다. 만약 안정적인 운영이 비지니스 가치 및 신뢰와 직결되는 프로젝트라면 적합할 수도 있습니다. 뿐만 아니라 <strong>프로젝트 단위로 빠르게 개발 환경을 설정하고 개발을 진행</strong>한다면 멀티레포 방식을 선택하는게 더 나을 수도 있습니다.</p>
<h3 id="5-모노레포-도구">5. 모노레포 도구</h3>
<p>모노레포는 여러 프로젝트를 하나의 저장소에서 관리하며 코드 재사용성을 높이지만, <strong>규모가 커질수록 코드 공유 및 의존성 관리의 어려움</strong>이 발생합니다. 이러한 문제점을 해결하기 위해, 여러 모노레포 도구들이 등장했습니다.</p>
<h4 id="turborepo">Turborepo</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/97a2ecfd-5a76-496d-a723-25e7316692a5/image.png" alt="">
Turborepo는 <strong>빌드 성능 최적화라는 목표에 초점을 맞춘 도구</strong>입니다.</p>
<p>지능적인 빌드 캐싱을 지원합니다. 명령어를 실행할 때마다, 파일의 내용과 현재 상태를 기억해두고 만약 코드가 수정되지 않은 상태에서 다시 빌드 명령을 내리면 <strong>작업을 반복하는 것이 아니라 이전에 저장해 둔 결과물을 즉시 복원</strong>합니다. 이를 통해 <strong>빌드 시간을 획기적으로 단축</strong>할 수 있습니다.</p>
<p>뿐만 아니라 작업의 순서를 파악하여 <strong>병렬 실행을 지원</strong>합니다. 기존 방식이 작업을 하나씩 순서대로 처리하며 시간을 소모했다면, <strong>Turborepo는 서로 의존성이 없는 작업들을 동시에 병렬로 수행</strong>합니다. 결과적으로 프로젝트의 규모가 커지고 앱이 늘어나도 개발 환경의 속도가 느려지지 않도록 방지합니다.</p>
<p>Nx와 같은 모노레포 도구와 비교했을 때, 새로운 명령어를 배울 필요가 없고 설정 파일이 하나뿐이기에 러닝 커브가 높지 않습니다. 처음부터 완벽하게 세팅하고 시작하지 않아도, 추후에 필요 시 하나씩 기능을 붙여나가는 점진적 도입이 가능하다는 점도 장점입니다.</p>
<h4 id="pnpm-workspace">pnpm workspace</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/782f4571-396f-4c4a-b4cb-17019e3a633e/image.png" alt=""></p>
<p>pnpm workspace는 <strong>효율적인 의존성 관리에 초점을 맞춘 도구</strong>입니다. </p>
<p>의존성을 한번만 설치하면, 여러 프로젝트가 공유하기 때문에 디스크 공간을 절약하고 설치 속도가 빠릅니다. 또한 파일 하나로 간단하게 설정할 수 있기 때문에, 러닝 커브 또한 높지 않습니다.</p>
<h4 id="nx">Nx</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/33ae4def-b89c-47bf-8f1a-c3cc9765dcb8/image.png" alt=""></p>
<p>Nx는 <strong>빌드 속도 최적화를 넘어 엔터프라이즈급 모노레포 관리 도구를 지향</strong>합니다.</p>
<p>구글 Angular 팀 출신들이 개발한 도구이고, 다양한 기술 스택을 지원합니다. 핵심적인 기능은 <strong>지능형 캐싱과 병렬 빌드를 통한 강력한 성능 최적화이고 시각화된 의존성 그래프</strong>를 확인할 수 있습니다. 이를 통해 복잡한 의존 관계를 한눈에 파악하고, 일관된 개발 환경을 구축하는데 도움을 줍니다. </p>
<p>Nx는 학습해야 할 독자적인 개념이 많고, 추상화 수준이 높고 기능이 방대하므로 꽤 높은 러닝 커브를 가지고 있습니다. 도입 시, 현재 팀 상황에 필요하다면 신중히 도입하는 것이 좋습니다.</p>
<h3 id="6-우리-팀에-모노레포를-도입한다면">6. 우리 팀에 모노레포를 도입한다면?</h3>
<h4 id="모노레포-도입을-검토하게-된-이유">모노레포 도입을 검토하게 된 이유</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/3d66b110-577d-43fc-b88a-cfabe6ca075d/image.png" alt="">
현재 우리 팀의 서비스는 <strong>초반 온보딩 페이지는 공유하고, 그 이후에 주최사/관객으로 분리되는 Y자형 구조</strong>입니다.</p>
<p>주최사/관객 각각의 비지니스 로직은 다르지만, 공연 정보 등 서버에서 내려주는 데이터는 양쪽 모두 동일하게 공유해야 하고 공유하는 컴포넌트 또한 꽤 많이 존재합니다. <strong>추후 확장성을 고려하고, 프로젝트 전반에 걸쳐 디자인 시스템 관리를 용이하게 하기 위해 모노레포 도입을 고려</strong>하게 되었습니다.</p>
<p>이러한 상황에서 모노레포는 <strong>코드의 중복을 차단하고 유지보수 효율을 높일 수 있다고 판단</strong>했습니다.</p>
<h4 id="도구-선택---turborepo--pnpm">도구 선택 - Turborepo + pnpm</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/a5dc0716-3f79-4629-a363-1569d99b862f/image.png" alt="">
Turborepo를 선택한 첫 번째 이유는 앞서 언급한 것처럼 <strong>지능적인 빌드 캐싱</strong>입니다. 기존에는 하나의 앱만 집중 개발하고 있음에도, 불필요하게 코드 변경이 없는 다른 앱까지 재빌드됩니다. Turborepo는 빌드 성능 최적화에 초점을 맞추고, <strong>코드 수정 없이 발생한 빌드 명령에 대해서는 이전에 저장해 둔 결과물을 즉시 가져오므로 빌드 시간을 단축</strong>할 수 있습니다.</p>
<p>또한 현재 팀 내 모노레포에 대한 이해도가 높지 않은 상황에서, <strong>비교적 설정 복잡도가 낮고 러닝 커브가 높지 않다</strong>는 점도 선택 이유입니다. Turborepo는 <strong>비교적 단순한 설정으로 원하는 캐싱과 병렬 빌드 기능을 빠르게 적용</strong>할 수 있습니다.</p>
<p>pnpm은 <code>node_modules</code> 구조를 매우 엄격하게 만들고, <code>package.json</code> 에 명시하지 않은 라이브러리는 import 자체가 불가하므로 <strong>유령 의존성을 차단</strong>합니다. 뿐만 아니라 모노레포는 구조 상 <code>node_modules</code> 용량이 커질 가능성이 높은데, pnpm을 사용하면 <strong>디스크 용량을 절약하고 설치 속도를 향상</strong>할 수 있습니다.</p>
<h4 id="기본-폴더-구조-구상-추후-세부-폴더-구조-논의">기본 폴더 구조 구상 (추후 세부 폴더 구조 논의)</h4>
<pre><code>AMP-CLIENT/
├── package.json              
├── turbo.json                # Turborepo 파이프라인 설정
├── pnpm-workspace.yaml       # 워크스페이스 정의
│
├── apps/                     # 실제 실행되는 서비스 (Deploy targets)
│   ├── host/                 # 주최사 웹
│   │   ├── src/
│   │   │   ├── app/       
│   │   │   ├── pages/
│   │   │   ├── widgets/
│   │   │   ├── features/
│   │   │   ├── entities/
│   │   │   └── shared/
│   │   └── package.json      
│   │
│   └── audience/              # 관객 웹
│       ├── src/
│       │   ├── app/       
│       │   ├── pages/
│       │   ├── widgets/
│       │   ├── features/
│       │   ├── entities/
│       │   └── shared/
│       └── package.json      
│
└── packages/                 # 조립 부품 및 공통 기능 (Shared Code)
    ├── ads-ui/               # ADS (AMP DESIGN SYSTEM)
    │   └── src/
    │        ├── components/
    │        ├── styles/
    │        └── icons/ 
    │
    └── configs/               # 개발 환경 설정 (ESLint, TSConfig)
</code></pre><p>어디까지나 예시일 뿐이고, FSD 구조를 도입할 때에는 초반부터 과도하게 레이어를 분리하는 것은 지양하는 것을 권장하고 있기 때문에 추후 변경 가능성이 높습니다.</p>
<p>더 논의해볼 사항은 아래와 같습니다.</p>
<ul>
<li>FSD 구조의 <code>shared</code> 레이어와 모노레포 <code>packages</code>의 역할 충돌
  (<code>shared/ui</code>와 <code>packages/ads-ui</code>  등)<br/></li>
<li><code>apps/host</code>와 <code>apps/audience</code> 내부 FSD 구조에서 <code>widgets</code>, <code>features</code>, <code>entities</code> 중복 가능성</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[사용자의 마음을 읽는 UX 개발자 되기]]></title>
            <link>https://velog.io/@jin_wls/%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%EB%A7%88%EC%9D%8C%EC%9D%84-%EC%9D%BD%EB%8A%94-UX-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%90%98%EA%B8%B0</link>
            <guid>https://velog.io/@jin_wls/%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%EB%A7%88%EC%9D%8C%EC%9D%84-%EC%9D%BD%EB%8A%94-UX-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%90%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Dec 2025 07:42:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jin_wls/post/8ecc9e96-5abd-4e4e-ac13-db6e98db0f31/image.jpg" alt=""></p>
<h2 id="0-들어가며">0. 들어가며</h2>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/c384cf2c-dcf2-457e-b108-87f0fb3c47a1/image.png" alt=""></p>
<blockquote>
<p>기술은 어떻게 행동을 형성하는가?</p>
</blockquote>
<p>최근 우리 주변을 살펴보면, 정말 셀 수 없을 정도로 많은 기술들이 우리를 둘러싸고 있습니다. 그리고 우리는 <strong>그 모든 것들이 사용자 경험으로 이어지는 시대</strong>에 살고 있습니다. 이제 <strong>기술은 단순히 기능을 구현하는 도구가 아니라, 사용자의 행동을 형성</strong>하기 시작했습니다. 사용자와 서비스의 접점에 서있는 프론트엔드 개발자로서, 기술적인 부분은 당연하지만 사용자 경험에 관심을 가지는 것도 이제는 필수적이라고 생각합니다.</p>
<p>지금까지 그저 디자인 시스템대로 구현하며 관습적으로 사용해왔던 UI 패턴들, 하지만 기업들이 설계한 UI 패턴들은 모두 명확한 이유가 존재합니다. 그리고 그 &#39;이유&#39;를 아는 것과 모르는 것은 프로덕트의 품질을 결정짓는 차이가 된다고 생각합니다.</p>
<p>이번 글에서는 다룰 내용들은 직접적으로 코드를 다루는 기술적인 부분은 아닙니다. 그러나 최근 읽었던 책에서 얻은 인사이트를 기반으로, 어떻게 하면 <strong>사용자 경험을 고려한 프로덕트</strong>를 개발할 수 있을지와 관련된 내용들을 다뤄보려 합니다.</p>
<h2 id="1-심리학과-ux">1. 심리학과 UX?</h2>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/b88ce25d-df70-46e2-9025-af4b298807c4/image.png" alt=""></p>
<p>제목을 <strong>&#39;사용자의 마음을 읽는 UX 개발자 되기&#39;</strong> 라고 지었던 이유는, 다양한 심리학 원칙을 소개하고 이를 사용자 경험으로 치환할 수 있는 방법을 다루고자 하기 때문입니다. 사람들의 마음을 연구하는 심리학을 통해 사용자 경험을 새로운 관점에서 조명하고자 합니다.</p>
<p>그럼 지금부터, 우리가 의식하지 못했던 곳에 어떤 심리학 법칙이 사용되고 있었는지 하나씩 살펴보겠습니다.</p>
<h2 id="2-심리학을-통해-바라보는-ux">2. 심리학을 통해 바라보는 UX</h2>
<h3 id="21-밀러의-법칙-millers-law">2.1. 밀러의 법칙 (Miller’s Law)</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/c132990c-cd66-4743-86f3-83ff664b24d3/image.jpeg" alt=""></p>
<blockquote>
<p>보통 사람은 작업 기억에 7개 정도의 항목밖에 저장하지 못한다.</p>
</blockquote>
<p>1956년 하버드대학교 심리학과 교수였던 인지 심리학자 조지 밀러가 발표한 논문에서 기원한 법칙입니다. 추후 논문들에서 기억 범위는 일정치 않다는 주장도 있었지만, 작업 기억 용량에는 본질적으로 한계가 있다는 것을 인식한 초창기 연구였습니다. 우리가 한번쯤 들어본 <strong>&#39;인지 부하 이론&#39;</strong> 이라는 중요한 개념의 토대가 되기도 했습니다.</p>
<p>사실 우리가 중점적으로 봐야 할 부분은 &#39;7&#39; 이라는 숫자가 아니라, <strong>덩어리화(chunking)</strong>라는 개념입니다. <strong>인간은 덩어리화를 통해 정보를 암기</strong>하고, 이때 밀러는 <strong>덩어리의 크기는 중요하지 않다</strong>는 것을 알아냈습니다. 즉, 글자 7개를 외우든 단어 7개를 외우든 드는 노력은 비슷하다는 의미입니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/d68a7cf4-185f-4a8c-ace6-2fc3b478af26/image.png" alt=""></p>
<p>가장 간단한 예시로, 전화번호를 확인해볼까요?
왼쪽 번호를 보고 암기해보고, 오른쪽 번호를 보고 암기해보면 바로 느낄 수 있습니다. 오른쪽 번호가 형식에 맞게 덩어리화 되어 있기 때문에 우리는 더 쉽게 인지하고 암기할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/00a6971b-fcd8-4eb6-b7f1-107c3c56a62e/image.png" alt="">
실제로 대부분의 뉴스 웹사이트는 사용자 경험을 위해, 밀러의 법칙을 따라 한번에 많은 양의 정보를 띄우는 것이 아니라 중심이 되는 몇 개의 기사만 덩어리화 해서 보여줍니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/6c40cdd7-442b-453b-a4a0-3785d65a8c1c/image.jpeg" alt="">
우리가 흔히 사용하는 쇼핑몰 웹사이트도 제품 정보를 덩어리화 해서, 내비게이션 링크 항목의 수는 많지만 사용자가 빠르게 목록을 훑어볼 수 있도록 하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/9bbe34d9-04ba-4aa7-b14a-184835253c83/image.png" alt="">
마지막은 흔히 사용하는 네이버도  UI를 구현하는데 덩어리화를 적극적으로 채택하고 있습니다.</p>
<p>우리가 지금까지 의식하지는 못했지만 디자인된 모든 것들은 대부분 덩어리화 되어 있습니다. 우리의 눈과 뇌는 <strong>가까운 것에 연관성의 의미</strong>를 부여하고 그걸 무의식적으로 빠르게 인식하기 때문입니다. 단순히 화면을 보기 좋게 꾸미는 것이 아니라, <strong>사용자가 원하는 정보를 빠르게 찾고, 탐색에 소비하는 인지 부담을 최소화</strong> 하기 위해서는 이를 적극적으로 적용할 수 있어야 합니다.</p>
<div style="display: flex; flex-direction: column; align-items: center;">
  <img src="https://velog.velcdn.com/images/jin_wls/post/dac9aeaf-b1bd-495e-891f-17b67f556996/image.png" style="width: 100%; height: auto;">
  <img src="https://velog.velcdn.com/images/jin_wls/post/fff910f1-f1d1-438a-92ab-53b80165af36/image.png" style="width: 100%; height: auto;">
</div>



<p>이제 우리는 단순히 화면을 그리는 것을 넘어, <strong>데이터를 유저의 뇌가 처리하기 쉬운 단위로 덩어리화 하는 전처리 역할</strong>을 수행해야 합니다. 데이터 포맷팅을 통해 가독성을 높이고, &#39;점진적 정보 노출&#39; 전략에 따라** 퍼널 컴포넌트 설계, 아코디언 UI** 등을 활용하여 유저의 인지 부하를 줄임과 동시에 초기 렌더링 성능도 최적화 할 수 있습니다.</p>
<h3 id="22-도허티-임계-doherty-threshold">2.2. 도허티 임계 (Doherty Threshold)</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/213962c2-c1b4-494e-8009-793592aeac2e/image.jpeg" alt=""></p>
<blockquote>
<p>컴퓨터와 사용자가 서로를 기다리지 않아도 되는 속도(0.4초 이하)로 인터랙션하면 생산성은 급격히 높아진다.</p>
</blockquote>
<p>사용자 경험과 관련한 필수 요소 중 하나는 <strong>&#39;성능&#39;</strong>과 관련한 부분입니다. 사용자가 목표를 달성하는데 있어 처리 속도는 더디고, 그에 맞는 피드백도 없는 웹 사이트는 사용자 경험을 해치고 부정적인 인상으로 이어집니다.</p>
<p>과거에 비해 현대 웹페이지의 전체 크기는 과거에 비해 수년간 기하급수적으로 증가했습니다. 2010<del>2011년에는 PC 평균 <strong>633.7 KB</strong>, 모바일 평균 <strong>260.1 KB</strong> 였으나 2022</del>2023년에는 HTTP Archive 기준 각 <strong>2286.3 KB, 2006.6 KB</strong>까지 증가했습니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/e92442a1-9844-4f9e-ba44-5066cb80fd7f/image.png" alt=""></p>
<p>이러한 추세로 사용자의 대기 시간이 길어지고 있고, 모든 사용자들은 이를 원하지 않습니다. 시스템의 반응이 즉각적이라고 느끼려면 <strong>0.1초 이내에 응답해야 하고, 0.3초의 지연도 맨눈으로 감지할 수 있으며, 1초가 넘어가면 사용자의 집중력은 급격하게 떨어집니다.</strong> 이처럼 대기 시간이 길어지면 아예 작업을 포기하는 사용자가 늘어난다는 사실을 입증한 연구도 수없이 많습니다.</p>
<p><strong>우리는 물론 개발자로서 성능을 신경쓰며, 도허티 임계인 0.4초의 처리 시간을 지키고자 노력해야겠지만 정말 더 이상 개선할 방법이 없는 경우에는 어떻게 해야할까요?</strong></p>
<p><strong>- Skeleton UI</strong>
<img src="https://velog.velcdn.com/images/jin_wls/post/7667cb8f-2d19-4f5c-a380-8f57c5e56d71/image.png" alt="">
콘텐츠를 로딩하는 동안 뼈대 화면을 보여주는 방식입니다. 뼈대 화면이란 콘텐츠가 로딩되는 동안 콘텐츠 영역에 임시로 자리표시자 블록을 표시하는 것을 의미합니다. 사용자는 이를 통해 <strong>로딩 속도가 느려도 기다린다는 느낌이 덜하고 속도와 반응성이 더 낫다</strong>고 인지합니다.</p>
<p>스켈레톤 UI는 순수 HTML/CSS로 직접 스타일링하여 가볍게 구현할 수도 있고, <strong>react-loading-skeleton</strong> 같은 라이브러리를 사용해 생산성을 높일 수도 있습니다. 또한, React의 <strong>Suspense</strong>를 활용하면 데이터 로딩 상태를 선언적으로 관리하여 스켈레톤 UI를 적절한 타이밍에 노출시킬 수 있습니다.</p>
<p><strong>- Blur Up / LQIP(Low-Quality Image Placeholder)</strong>
<img src="https://velog.velcdn.com/images/jin_wls/post/559d9303-0325-4724-a312-f03bb6510731/image.png" alt="">
웹의 로딩 시간을 지연시키는  원인은 다양하겠지만, 이미지가 주범일 가능성이 있습니다. 실제로는 큰 이미지를 표시할 공간에 우선 <strong>작은 크기로 이미지를 로딩해서 확대</strong>합니다. 이때 픽셀 단위로 깨지는 노이즈는 <strong>가우시안 블러</strong>를 활용하여 가립니다. 그리고 추후 원본 이미지의 로딩이 완료되면, 기존 이미지를 감추고 실제 이미지를 표시합니다.</p>
<p>위처럼 간단한 UI 패턴이 효과적인 이유는 <strong>요청한 작업이 처리되고 있다는 사실을 사용자에게 분명히 알리고, 사용자가 기다리는 동안 볼거리를 제공</strong>하기 때문입니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/852ea564-046c-44d9-a3f0-85f153a0da7d/image.png" alt="">
이 외에도 <strong>로딩 시간을 보여주는 애니메이션을 활용한다거나, 낙관적 UI</strong>를 적용하는 방법 또한 존재합니다. 동작이 완료된 후에 피드백을 제공하는 것이 아니라, 미리 예측하여 낙관적으로 UI를 먼저 업데이트 하는 방식입니다. </p>
<p>실제로 인스타그램도 댓글을 게시할 때, 실제로 게시가 완료되기 전에 게시될 것으로 가정하고 즉각적인 시각적 피드백을 제공합니다. 백그라운드에서는 여전히 작업이 진행 중이지만, 사용자는 웹 성능이 좋다고 생각합니다.</p>
<h3 id="23-피크엔드-법칙peak-end-rule">2.3. 피크엔드 법칙(Peak-End Rule)</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/880902c9-0333-40de-bf04-6e1646ebc861/image.jpeg" alt=""></p>
<blockquote>
<p>인간은 경험 전체의 평균이나 합계가 아니라, 절정의 순간과 마지막 순간에 느낀 감정을 바탕으로 경험을 판단하는 경향이 있다.</p>
</blockquote>
<p>1993년 대니얼 카너먼을 비롯한 연구진이 발표한 논문에서 처음으로 증거가 제시된 법칙입니다.</p>
<p>인간은 과거 사건을 떠올릴 때, 경험 전반을 고려하기보다 <strong>감정적으로 절정에 이른 순간과 마지막 순간에 집중</strong>하는 흥미로운 경향이 있습니다. 마치 스냅사진처럼 특정 순간만을 기억한다고 생각하면 이해하기 쉽습니다. 이는 전반적인 경험을 어떻게 기억할지에 큰 영향을 미치고, 다시 경험할 의향이 있는지 또는 추천할 의향이 있는지 등을 결정합니다.</p>
<p>이를 통해 우리가 생각해봐야 할 점은 사용자가 전체 경험을 긍정적으로 평가하게 하려면 이렇게 중요한 순간에 주의를 기울여야 한다는 것입니다.</p>
<div style="display: flex; justify-content: center; gap: 0;">
  <img src="https://velog.velcdn.com/images/jin_wls/post/90b40b8f-134e-4700-a687-98323bb1ab7e/image.png" style="width: 50%; height: auto; display: block;">
  <img src="https://velog.velcdn.com/images/jin_wls/post/6a172983-087d-4863-b40a-d7407c54ae00/image.png" style="width: 50%; height: auto; display: block;">
</div>

<p>실제로 많은 기업들이 피크엔드 법칙을 사용하고 있습니다.</p>
<p>최근 연말이다 보니, <strong>Youtube Recap</strong>이나 <strong>Spotify Wrapped</strong> 처럼 지난 한 해동안 해당 서비스에서 활동한 기록(가장 좋아하는 아티스트, 가장 많이 들었던 노래) 등을 정리해서 사용자가 확인할 수 있도록 하고 있습니다.</p>
<p>이 두 기업은 개인화의 힘을 활용하여, <strong>한 해의 마지막에 사용자와 서비스가 특별하며 연결되어 있다는 느낌</strong>을 유도합니다. 이를 통해 1년 간 해당 서비스의 전반적인 사용자 경험을 <strong>긍정적 인상</strong>으로 만들어냅니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/4755eb72-4dd0-4741-af1e-8d8b5fee6360/image.png" alt="">
우리는 <strong>서비스의 핵심 가치를 달성하는 순간에, 기술적 디테일을 더해 성취감을 극대화</strong>해야 합니다. </p>
<p>좋은 예시로 토스를 들 수 있습니다. 토스는 &#39;송금&#39;이라는 서비스 핵심 기능을 긍정적 감정으로 바꾸기 위해 애니메이션을 활용합니다. JSON 기반 경량 애니메이션 파일 형식인 <strong>Lottie</strong> 등을 사용하여 고품질의 벡터 애니메이션을 렌더링합니다. 유저는 이 짧은 시각적 피드백을 통해 <strong>&quot;안전하고 빠르게 돈을 보내는데 성공했다&quot;</strong>라는 긍정적 감정을 느낄 수 있습니다. </p>
</br>
<div style="display: flex; justify-content: center; gap: 0;">
  <img src="https://velog.velcdn.com/images/jin_wls/post/1548d239-566b-4a62-99b9-fa07a9fee729/image.png" style="width: 50%; height: auto; display: block;">
  <img src="https://velog.velcdn.com/images/jin_wls/post/9da6b651-3e18-4421-afe3-7fac062e6d39/image.png" style="width: 50%; height: auto; display: block;">
</div>

<p>뿐만 아니라 인간은 부정적인 사건을 더 쉽게 기억에 남기는 <strong>&#39;부정 편향&#39;</strong>이 있으므로, 우리는 가능한 한 이러한 부정적인 절정을 완화해 이와 관련한 부정 편향을 피해야 합니다.</p>
<p>그 예시로 404 오류 페이지를 들 수 있습니다. 404 오류 페이지를 사용자가 마주하면, 원하는 페이지를 확인하지 못했으므로 부정적인 감정이 들기 쉽습니다. 이때 일부 기업들은 <strong>고객과 라포를 형성하고 브랜드의 개성을 드러내는 기회</strong>로 삼기도 합니다. 위 사진을 보면 그 예시를 바로 확인할 수 있습니다.</p>
<p>이처럼 피크엔드 법칙을 고려하면, <strong>유저의 감정이 고조되는 특정 시점에 인터랙션과 성공적인 피드백을 집중적으로 배치하고 부정적 경험은 최대한 완화</strong>하여 서비스의 인상을 긍정적으로 바꿀 수 있습니다.</p>
<h3 id="24-테슬러의-법칙-teslers-law">2.4. 테슬러의 법칙 (Tesler’s Law)</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/3f1d827d-82b5-4539-9f1f-a98780195ca4/image.jpeg" alt=""></p>
<blockquote>
<p>모든 시스템에는 더 이상 줄일 수 없는 복잡성이 존재한다.</p>
</blockquote>
<p>1980년대 중반 제록스 파크에서 컴퓨터과학자 래리 테슬러가 인터랙션 디자인 언어 개발 업무를 수행하던 시절에 탄생한 개념입니다. <strong>복잡성 보존의 법칙</strong>이라고 알려지기도 했습니다.</p>
<p>위에서 언급했듯, <strong>모든 프로세스에는 더 이상 처리할 수 없는 기본적인 복잡성이 존재</strong>합니다. 웹 사이트도 마찬가지이므로, <strong>사용자 또는 개발자 중 한 쪽이 반드시 감당</strong>해야 합니다. 우리는 개발자로서, 사용자가 감당할 복잡성을 우리가 조금이라도 더 감당하고 사용자에게 가중시켜서는 안됩니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/dee489ca-34a2-4dd1-9daa-c4eb41b5adc9/image.png" alt="">
실제 기업들은 어떨까요? 우리가 이메일을 작성한다고 생각해보면 이메일 전송 과정에 있어서 더 이상 줄일 수 없는 복잡성은 <strong>발신인, 수신인, 이메일 내용</strong> 정도입니다.</p>
<p>최신 이메일 클라이언트(Gmail 등)은 이러한 <strong>복잡성을 시스템이 감당</strong>하도록 하기 위해, 발신인은 로그인한 사용자의 이메일 주소로 자동 입력하고 수신인은 연락처 정보를 기반으로 자동 추천합니다. 최근에는 AI가 이메일 내용 작성 또한 도와주고 있습니다.</p>
<p>이러한 방법도 복잡성이 완전히 사라진 것은 아니지만, 추상화를 통해 사용자가 해야 하는 수고를 줄일 수 있습니다. 우리는 어떻게 하면 <strong>사용자의 수고를 추상화</strong>할지, 추상화를 잘하는 방법은 무엇일지를 반드시 고민해야 합니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/917177ac-de30-4faa-a215-9a894b590ab5/image.png" alt="">
애플페이나 삼성페이 같은 서비스도, 기존에 매우 번거롭고 복잡했던 결제 프로세스를 추상화를 통해 <strong>사용자가 겪는 복잡성을 혁신적으로 줄였습니다.</strong> 이제 우리는 결제 옵션을 선택하고 구매 정보만 확인하면 무엇이든 결제할 수 있습니다.</p>
<p>단순히 개발하는 것을 넘어, <strong>서비스에서 최소한의 복잡성을 명확히 인지하고 시스템이 감당하도록 하는 것</strong>은 별 것 아닌 것처럼 보이지만 매우 중요한 포인트라 생각합니다. 래리 테슬러의 말을 인용하며 이번 챕터를 마무리하겠습니다.</p>
<p><strong>&quot;만약 어떤 엔지니어가 소프트웨어를 다소 복잡하게 만드는 바람에 그가 일주일이면 충분히 제거할 복잡성을 처리하느라 100만명의 사용자가 매일 1분의 시간을 허비하고 있다면, 그건 엔지니어의 작업을 덜어낸 대가로 사용자에게 불이익을 주는 것과 다름없다.&quot;</strong></p>
<h3 id="25-포스텔의-법칙postels-law">2.5. 포스텔의 법칙(Postel’s Law)</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/f01e0f73-4d4b-4f62-8998-3ba709b22e1a/image.jpeg" alt=""></p>
<blockquote>
<p>자신이 행하는 것은 엄격하게, 남의 것을 받아들일 때는 너그럽게</p>
</blockquote>
<p>포스텔의 원칙은 원래 컴퓨터 네트워크 상 데이터 전송과 관련된 네트워크 엔지니어링 가이드라인으로 만들어졌습니다. 미국의 컴퓨터 과학자 존 포스텔은 <strong>TCP의 초기 모델을 구현</strong>한 사람으로, 위에서 언급한 &#39;견고함의 원칙&#39;을 근거로 도입한 장애 허용 시스템 덕에 <strong>초기 인터넷 노드 통신은 안정성</strong>을 얻었습니다.</p>
<p>이 원칙은 이제 컴퓨터 네트워크 뿐 아니라, 소프트웨어 아키텍쳐 등 그 외 분야에도 영향을 미치고 있습니다. 우리가 가장 많이 사용하는 HTML/CSS를 예로 들어보면 모두 <strong>오류를 느슨하게 다룹니다.</strong> 오류나 이해하지 못하는 부분이 있어도 느슨하게 넘어가는 유연성 덕분에, 인터넷 세계를 제패했습니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/1e8b3c42-f042-4d59-9cfa-ad7a4a4659de/image.png" alt="">
이제 포스텔의 원칙은 <strong>UX 분야에도 영향</strong>을 주고 있습니다. 사용자는 기계처럼 행동하지 않습니다. <strong>사용자는 일관성이 없고, 가끔 실수하며 때로는 우리의 예상 범위 밖에서 행동합니다. 동시에 사용자는 서비스가 자신을 이해하고 너그럽게 대해주기를 바랍니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/2c79bd19-0b83-43ea-b663-92722a97e388/image.jpeg" alt="">
대표적인 예시로 <strong>&#39;오류 수용&#39;</strong>이 있습니다. 대부분의 검색 엔진에서 정확한 단어를 검색하지 않아도, 비슷한 단어를 기반으로 자동 수정해줍니다. 우리는 항상 사용자의 오류를 대비해야 합니다. <strong>사용자의 오류는 너그럽게 받아들이고, 우리가 제공하는 서비스는 엄격하게 빈틈이 없어야 합니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/b1b690fe-01fa-48fb-8040-be72f1f52bda/image.png" alt="">
우리가 포스텔 법칙과 관련하여 알았으면 하는 부분은 <strong>&#39;점진적 향상 기법&#39;</strong>입니다.</p>
<p>더 좋지 않은 기기를 사용한다고 웹 사이트의 핵심 기능을 사용할 수 없다면 어떨까요? 아마 매우 불쾌한 사용자 경험을 가져올 것이라 생각합니다.</p>
<p>모든 사용자가 <strong>브라우저 기능 지원, 기기 기능이나 성능, 인터넷 연결 속도와 관계 없이 기본적인 콘텐츠 또는 기능에 접근</strong>할 수 있어야 합니다. 그리고 부가적인 스타일 또는 인터랙션은 성능을 탐지해 <strong>점진적으로 추가</strong>해야 합니다.</p>
<p>점진적 향상 기법은 다양한 사용자를 관대하게 수용하며 핵심 콘텐츠를 지키면서 향상된 추가 기능들은 보수적으로 접근하기 때문에 <strong>그 누구의 접근성도 저해하지 않습니다.</strong></p>
<p>우리는 사용자의 연결 상태가 불안정할 때 고용량 이미지나 화려한 애니메이션 로직을 생략하는 전략을 사용해볼 수도 있고, SSR을 사용해서 완성된 HTML 파일을 먼저 보내주고 그 후에 인터랙션을 입히는 Hydration 기법을 고려해볼 수도 있습니다.</p>
<h2 id="3-마치며">3. 마치며</h2>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/e5749bff-4b0d-412a-b9f4-67787dc2d722/image.png" alt=""></p>
<p>지금까지 단순히 기술적으로 코드를 작성하는 것을 넘어,** 심리학을 통해 사용자를 새롭게 바라보고 사용자 경험을 최적화하는 방법<strong>들을 알아봤습니다. 글을 작성하고 공부하며, 최근 AI의 발달로 인해 개발 효율이 높아진만큼, **조금은 인문학적인 내용을 기반으로 사용자에게 더욱 집중</strong>하는 것은 또 다른 경쟁력이 될 수 있겠다고 생각했습니다.</p>
<p>우리가 단순히 디자인을 화면에 그대로 복제하는 것에 그치지 않고, 어떻게 하면 사용자가 더 나은 사용자 경험을 가질 수 있을 지 고민할 때 더 나은 프로덕트를 만들 수 있다고 생각합니다.</p>
<p>기술은 계속해서 변화하고 지금은 존재하지 않는 새로운 기술들도 끊임없이 등장하겠지만, <strong>&#39;사람이 어떻게 생각하고 행동하는가&#39;</strong>와 관련한 심리학 본질은 쉽게 변하지 않으리라 생각합니다.</p>
<p>각 심리학 법칙을 통해 사용자를 이해하고 끊임없이 고민한 시간들이 모여 결국 <strong>&#39;사용자의 마음을 읽는 UX&#39;</strong>가 완성된다고 믿습니다. 우리 모두가 사용자의 마음을 읽고자 노력하는, 더 나아가 조금이나마 읽을 수 있는 개발자가 될 수 있었으면 좋겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[컴포넌트 설계란?]]></title>
            <link>https://velog.io/@jin_wls/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84%EB%9E%80</link>
            <guid>https://velog.io/@jin_wls/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84%EB%9E%80</guid>
            <pubDate>Fri, 14 Nov 2025 17:26:53 GMT</pubDate>
            <description><![CDATA[<h3 id="1-컴포넌트란">1. 컴포넌트란?</h3>
<p>우리가 개발을 하며 일상처럼 만나는 단어, 바로 &#39;컴포넌트&#39;입니다.</p>
<p>컴포넌트는 <strong>독립적이고 재사용 가능한 소프트웨어 모듈 또는 구성 요소</strong>를 의미합니다. 웹사이트 개발에서는 화면을 구현하는 기본적인 단위로 사용되며, 흔히 레고 블록에 비유하기도 합니다. 우리는 하나의 페이지를 다양한 컴포넌트를 조합하여 만들어냅니다.</p>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/03f29ddd-5d10-4756-9e28-30ef64ce3f10/image.webp" alt=""></p>
<h4 id="--왜-컴포넌트를-사용할까">- 왜 컴포넌트를 사용할까?</h4>
<p>하드웨어와 달리 소프트웨어는 독립적으로 개발되지 않은 경우가 많고, 독립적으로 개발되었다 하더라도 다른 모듈과의 호환을 생각하지 않고 개발할 수 없습니다. 이는 곧 소프트웨어의 재사용을 어렵게 만들고, 유지보수 비용이 크게 늘어나기 때문입니다. 이러한 특징이 컴포넌트를 개발에서 사용하게 된 배경으로 이어졌습니다.</p>
<p>어쩌면 컴포넌트 사용이 초기 설계에 더 많은 노력을 요구할 수 있지만, 그 대가로 <strong>재사용성, 유지보수성, 테스트 용이성 등 여러 이점</strong>을 가져올 수 있습니다. 장기적으로 보면 우리가 흔히 말하는 <strong>개발 비용을 절감하고, 지속적인 시스템 성장을 가능하게 하는 기반</strong>이 되기도 합니다.</p>
<h3 id="2-컴포넌트-설계의-필요성">2. 컴포넌트 설계의 필요성</h3>
<p>고백하자면 저는 리액트를 다루며 컴포넌트에 대해 알게 되었고, 처음에는 그저 리액트에서 컴포넌트를 생성하는 방식만 알고 개발했던 것 같습니다. 컴포넌트 설계에 대한 깊은 고민 없이 기능 구현에만 급급했던 결과, 컴포넌트가 점점 비대해지고 유지보수가 매우 어려워졌을 뿐 아니라 수정 시 여러 사이드 이펙트가 발생했습니다.</p>
<p>만약 혼자가 아니라 팀으로 협업하며 개발한다면 어떨까요? 아마 여러 명의 손을 거치며 개발한 코드가, 명확한 컴포넌트 설계 원칙 없이 진행된다면 생각보다 빠르게 유지보수가 불가능하거나 어려운 상태의 코드가 될 것이라 생각합니다.</p>
<p>이처럼 컴포넌트 설계는 최근 소프트웨어 개발의 핵심으로, 시스템의 복잡성을 관리하고 효율성을 높입니다. 비지니스 요구사항은 끊임없이 변화하며 우리는 이러한 변화에 대응해야 합니다. 컴포넌트 설계는 언젠가, 그리고 누군가가 수정해야 할 <strong>코드의 재사용성을 높이고 유지보수성을 높이며 프로젝트의 안정성을 높일 수 있습니다.</strong>
<img src="https://velog.velcdn.com/images/jin_wls/post/4cf31731-2edc-4425-b26c-aae04a8614bd/image.jpg" alt=""></p>
<h3 id="3-컴포넌트-설계를-위한-핵심-원칙">3. 컴포넌트 설계를 위한 핵심 원칙</h3>
<h4 id="--단일-책임-원칙-single-responsibility-principle-srp">- 단일 책임 원칙 (Single Responsibility Principle, SRP)</h4>
<p>각 컴포넌트는 <strong>하나의 명확한 역할을 수행</strong>해야 한다는 원칙입니다.</p>
<p>이는 기능 변경, 또는 수정이 일어났을 때 파급 효과를 최소화하기 위해 주로 사용합니다. 쉽게 말해 컴포넌트가 변경되는 이유가 한 가지여야 함을 의미합니다. </p>
<p>하나의 컴포넌트가 여러가지 책임을 가지고 있으면, 각기 다른 사유에 의해 코드를 변경해야 하고 이는 유지보수를 어렵게 하는 요인이 됩니다. 뿐만 아니라, 코드를 변경할 때 의도하지 않았던 다른 기능까지 변경되는 연쇄작용을 막을 수 있다는 점도 이점입니다.</p>
<h4 id="--개방-폐쇄-원칙open-closed-principle-ocp">- 개방-폐쇄 원칙(Open-Closed Principle, OCP)</h4>
<p><strong>기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계</strong>해야 한다는 원칙입니다.</p>
<p>즉, 확장에 대해서는 개방적(Open), 수정에 대해서는 폐쇄적(Close)이어야 함을 의미합니다. 추상화를 생각하면 되는데, 변하는 것과 변하지 않는 것을 분리하는 것입니다.</p>
<p>예를 들어보자면, 어떤 한 컴포넌트에 기능을 추가해야 할 때 기존 컴포넌트에 코드를 더 추가해서 기능을 구현하기보다는 두 컴포넌트를 아우르는 핵심 로직(변하지 않는 것)을 인터페이스로 구현합니다. 그리고 그 인터페이스를 활용해서 기능을 각각의 컴포넌트로 구현합니다.</p>
<p>이는 기존에 잘 작동하는 코드를 수정할 필요가 없을 뿐 아니라, 컴포넌트 간 의존 관계가 일방적이므로 변화가 미치는 영향을 최소화 할 수 있습니다.</p>
<h4 id="--의존성-역전-원칙-dependency-inversion-principle-dip">- 의존성 역전 원칙 (Dependency Inversion Principle, DIP)</h4>
<p>코드의 결합도를 낮추기 위해 사용하는 원칙입니다.</p>
<p><strong>고수준 모듈은 저수준 모듈에 의존해서는 안되며, 필요한 경우 양쪽 모두 추상화에 의존해야 한다는 원칙</strong>입니다. 즉 둘 다 추상화 계층(인터페이스)에 의존하도록 설계함으로 결합도를 낮춥니다.</p>
<p>사실 처음에는 이해가 잘 되지 않지만, 예를 들어 api를 연동하는 작업을 진행한다고 했을 때 고수준 모듈은 화면을 그리는 모듈입니다. 그리고 저수준 모듈은 axios 등을 활용하여 실제로 데이터를 서버에서 가져오는 모듈입니다. </p>
<p>이때 만약 고수준 모듈에서 바로 저수준 모듈을 가져와서 사용하면 결합도가 높아집니다. 화면 로직을 테스트 할 때 네트워크 통신이 발생하기 때문에 테스트의 어려움 뿐 아니라 만약 추후에 HTTP 클라이언트 라이브러리가 달라진다면, 모든 화면 컴포넌트도 같이 수정해야 합니다.</p>
<p>그런데 만약 사이에 추상화 파일을 만들어서, 사용자 목록을 가져오는 <code>getUser()</code>를 정의하여 중간에서 사용한다면 어떨까요? 결합도를 낮추고, 위에서 언급한 문제들 대부분을 해결할 수 있을 것입니다.</p>
<p>뿐만 아니라, 우리가 Props drilling을 막기 위해 사용했던 Context 객체도 DIP의 예시로 볼 수 있습니다.</p>
<h3 id="4-그렇다면-컴포넌트는-어떻게-분리해야-할까">4. 그렇다면 컴포넌트는 어떻게 분리해야 할까?</h3>
<p>여기서부터는 디자인 패턴과 연결되는 부분인데, 위 핵심 원칙들을 기반으로 어떻게 컴포넌트들을 효율적으로 분리할 수 있을지 2가지만 간단히 알아보겠습니다.</p>
<h4 id="--프레젠테이션-컨테이너-패턴presentation-container-pattern">- 프레젠테이션-컨테이너 패턴(presentation-container pattern)</h4>
<p><strong>UI를 렌더링하는 프레젠테이션 컴포넌트와 데이터 로직을 담은 컨테이너 컴포넌트를 분리</strong>하는 패턴입니다. 위에서 언급했던 단일 책임 원칙과 이어지는 부분이 많다고 생각합니다.</p>
<p>프레젠테이션 컴포넌트는 데이터를 관리하지 않고, 컨테이너 컴포넌트에게 전달 받아 UI를 구현하는 역할에만 집중합니다. 반대로 컨테이너 패턴은 화면에 아무것도 렌더링하지 않고, UI 구현에 필요한 로직을 수행하여 데이터를 전달해주는 역할에 집중합니다.<img src="https://velog.velcdn.com/images/jin_wls/post/e34ffbde-d51e-4fd9-bc2c-7412e36c8540/image.png" alt=""></p>
<h4 id="--아토믹-디자인-패턴atomic-design-pattern">- 아토믹 디자인 패턴(Atomic Design Pattern)</h4>
<p>이제 컴포넌트가 마치 부품이나 레고처럼 조립되어 페이지가 구현된다는 것과, 컴포넌트는 재사용을 위해 만든다는 사실은 알고 있습니다. 이제 이러한 컴포넌트들을 잘 정리해서 효율적으로 사용하기만 하면 되는데, 이때 활용할 수 있는 것이 &#39;아토믹 디자인 패턴&#39; 입니다.</p>
<p>컴포넌트도 계층이 분명 존재합니다. 아토믹 디자인 패턴은 <strong>가장 작은 컴포넌트 단위를 원자로 설정하고, 이를 바탕으로 상위 컴포넌트를 만들어 코드 재사용을 최대화하는 방법론</strong>입니다.</p>
<p>가장 작은 원자 컴포넌트는 레이블, 텍스트, 컨테이너, 버튼 등이 있고 이를 조립하여 분자 컴포넌트인 입력 폼을 만들고, 또 분자 컴포넌트를 조합하여 상위 컴포넌트를 만드는 방식입니다. </p>
<p>이를 통해 유지보수성을 향상시키고, 컴포넌트 재사용성 증대, UI 일관성 유지, 개발 과정에서의 유연성 등 다양한 이점을 가질 수 있습니다.
<img src="https://velog.velcdn.com/images/jin_wls/post/a987612d-7689-4ba4-b8f0-88edc43431fc/image.png" alt=""></p>
<h3 id="5-컴포넌트를-분리하는-기준과-방법-아티클-공유">5. 컴포넌트를 분리하는 기준과 방법 (아티클 공유)</h3>
<p><a href="https://medium.com/@junep/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A5%BC-%EB%B6%84%EB%A6%AC%ED%95%98%EB%8A%94-%EA%B8%B0%EC%A4%80%EA%B3%BC-%EB%B0%A9%EB%B2%95-e7cf16bb157a">프론트엔드 아키텍처: 컴포넌트를 분리하는 기준과 방법</a></p>
<p>이번 아티클을 작성하던 중, 컴포넌트를 분리하는 기준에 대해 잘 다뤄주신 아티클을 접하게 되었습니다. 컴포넌트를 언제 분리하면 좋을지 잘 정리해놓은 글이라서, 한번씩 읽어보시면 도움이 되실 것 같아 공유드리며 글을 마치겠습니다!</p>
<br/>
<br/>

<p><strong>[ 추가로 읽어보면 좋을 아티클 공유 ]</strong>
<a href="https://tech.kakaoent.com/front-end/2024/240116-common-component/">https://tech.kakaoent.com/front-end/2024/240116-common-component/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSS-in-JS란? (Emotion, Vanilla Extract)]]></title>
            <link>https://velog.io/@jin_wls/CSS-in-JS%EB%9E%80-Emotion-Vanilla-Extract</link>
            <guid>https://velog.io/@jin_wls/CSS-in-JS%EB%9E%80-Emotion-Vanilla-Extract</guid>
            <pubDate>Fri, 07 Nov 2025 12:17:30 GMT</pubDate>
            <description><![CDATA[<h3 id="1-기존-css의-단점과-css-in-js의-탄생-배경">1. 기존 CSS의 단점과 CSS-in-JS의 탄생 배경</h3>
<p>프론트엔드 개발에서 CSS를 다루는 방식은 끊임없이 달라지고 있습니다.
최근에는 React와 같은 컴포넌트 기반 아키텍처를 활용하여 개발하는 것이 주류가 되면서, 기존의 단순 CSS 방식은 아래 작성한 것처럼 여러 문제점과 한계를 나타내기 시작했습니다.</p>
<ol>
<li><p><strong>전역 네임스페이스 오염</strong>
 CSS의 모든 클래스 선택자는 기본적으로 전역 범위에 속하기 때문에, 서로 다른 컴포넌트에서 같은 클래스명을 사용하면 스타일이 충돌하게 됩니다. 이를 해결하기 위해 BEM 명명 규칙 등을 활용하여 중복을 피할 방법을 찾아야만 했습니다.</p>
</li>
<li><p><strong>코드 관리의 어려움</strong>
프로젝트 규모가 점점 커지면서 자연스럽게 <code>.css</code> 파일도 늘어났고, 관리 및 유지보수가 어려워지면서 파일 간 의존성이나 충돌 문제가 발생하기 쉬웠습니다.</p>
</li>
<li><p><strong>JavaScript와의 상태 공유 어려움 (동적 스타일링의 필요성)</strong>
UI를 상태에 따라 스타일을 동적으로 변하게 하는 경우, 기존 CSS는 정적이기 때문에 클래스 이름을 조건문으로 추가/삭제 하는 등 복잡하게 제어해야 했습니다. (스타일 관련 로직이 파편화됨)</p>
</li>
<li><p>** 예측 불가능한 스타일 적용 순서**
최근 많이 사용하는 SPA 환경에서는 성능 최적화를 위해 필요에 따라 CSS 파일을 비동기적으로 로드하는 경우가 존재합니다. 이 경우 어떤 파일이 먼저 도착할지 예상할 수 없으므로, 만약 하나의 요소에 동일한 명시도(specificity)를 가진 스타일 규칙이 적용된다면 최종적으로 적용되는 스타일을 예측할 수 없습니다.
<img src="https://velog.velcdn.com/images/jin_wls/post/eaa5d976-3ca9-491f-9946-9cb8f43fb8b0/image.png" alt=""></p>
</li>
</ol>
<h3 id="2-css-in-js란">2. CSS-in-JS란?</h3>
<p>이러한 문제점이 지속적으로 발생하자, 스타일링도 컴포넌트 단위로 관리하고자 하는 요구가 생겼고 CSS-in-JS가 등장하게 되었습니다. 이름 그대로 CSS-in-JS는 JavaScript 코드 내에서 CSS 스타일링을 작성하는 기법입니다.</p>
<p>컴포넌트 모듈 하나에서 스타일과 동작을 한번에 관리할 수 있다보니 유지보수가 용이하고, 컴포넌트 자체에 스타일을 지정하는 방식으로 스타일 충돌을 방지할 수 있다는 장점이 있습니다. 특히, JS 내부에서 CSS 코드를 작성할 수 있기 때문에 복잡한 동적 스타일링을 비교적 쉽게 작성할 수 있다는 점이 가장 큰 장점입니다.
<img src="https://velog.velcdn.com/images/jin_wls/post/be07e3e8-4cdf-4be3-abc0-d2f94ba422d7/image.png" alt=""></p>
<p>그렇다고 CSS-in-JS가 장점만 존재하는 것은 아닙니다. 스타일이 런타임에 계산되고 적용되므로 초기 로딩 속도가 느리거나, 브라우저에서 스타일을 적용하는데 더 오랜 시간이 걸리기도 합니다.</p>
<p>일부 사람들은 &#39;관심사 분리&#39;라는 측면에서, 부정적인 의견을 가지고 있기도 합니다.
지금까지 HTML은 구조, CSS는 표현, JS는 동작을 담당한다는 깔끔한 구분을 해치는 건 아닐지 우려하는 목소리도 있습니다. </p>
<h3 id="3-emotion-vanilla-extract">3. Emotion, Vanilla-extract</h3>
<p>CSS-in-JS 방식을 채택한 다양한 라이브러리가 있지만, 이 글에서는 그 중 Emotion과 Vanilla-extract 이렇게 2개의 라이브러리만 다룰 예정입니다.
<img src="https://velog.velcdn.com/images/jin_wls/post/93366aca-02ab-4426-98fc-4bd0f3caad4d/image.jpeg" alt="">
지금 크게 관련 있는 내용은 아니지만, 토스 프론트엔드 챕터 웹사이트에 접속하면 모바일 제품의 스타일에는 emotion을 사용하고 데스크탑 제품의 스타일은 vanilla-extract를 사용하고 있다는 점을 알 수 있습니다.</p>
<h3 id="4-emotion이란">4. Emotion이란?</h3>
<p>Emotion은 앞서 설명한 것처럼 JavaScript에 CSS 코드를 작성하기 위해 설계되었습니다. 현재 <code>styled Components</code>와 함께 가장 널리 사용되고 있는 CSS-in-JS 라이브러리입니다.</p>
<p>Emotion은 &#39;런타임 CSS-in-JS&#39; 방식으로 작동합니다. 애플리케이션이 브라우저에서 실행되는 동안 JavaScript가 스타일 코드를 생성하고, 고유한 클래스 이름을 생성하며, 이를 DOM에 주입합니다. 
<img src="https://velog.velcdn.com/images/jin_wls/post/7f1494c3-dc69-44b2-b151-c3f907384328/image.png" alt=""></p>
<h4 id="41-emotion의-스타일링-방식">4.1. Emotion의 스타일링 방식</h4>
<p>Emotion을 활용하여 스타일링을 하는 방식은 크게 2가지로 나눌 수 있습니다. 우리는 개인적인 취향이나 프로젝트 상황에 따라 그에 맞춰 자유롭게 선택하면 됩니다. 이러한 유연함이 Emotion의 장점이 되기도 합니다.</p>
<p>1.** <code>Styled</code> API (컴포넌트 기반 스타일링)**</p>
<p>스타일이 적용된 컴포넌트를 만들어서 사용하는 방식입니다. 스타일과 컴포넌트가 강력하게 결합되어 직관적이고, 재사용 가능한 UI 라이브러리를 구축할 때 유용합니다. 사실상 템플릿 리터럴을 활용하여 기존 CSS 문법을 그대로 사용하기 때문에 어렵지 않게 코드를 작성할 수 있습니다.</p>
<pre><code>import styled from &#39;@emotion/styled&#39;;

// 스타일이 적용된 컴포넌트 생성
const Button = styled.button`
  color: hotpink;
  background: ${props =&gt; props.primary ? &#39;blue&#39; : &#39;white&#39;}; // props로 동적 스타일링
`;

&lt;Button primary&gt;클릭&lt;/Button&gt;</code></pre><p>CSS-in-JS  방식의 장점이었던 동적 스타일링도 컴포넌트에 Props를 전달하여 쉽게 구현할 수 있습니다. <br/>
2. *<em><code>css</code> prop 또는 <code>css()</code> 함수  활용 *</em>
Emotion의 <code>css()</code> 함수를 활용하여 스타일링하는 방식입니다. <code>css()</code> 함수의 인자로 CSS 스타일 선언 내용을 넣어주면 됩니다.</p>
<pre><code>// 문자형 스타일
/** @jsxImportSource @emotion/react */
import { css } from &#39;@emotion/react&#39;

const color = &#39;white&#39;

render(
  &lt;div
    css={css`
      padding: 32px;
      background-color: hotpink;
      font-size: 24px;
      border-radius: 4px;
      &amp;:hover {
        color: ${color};
      }
    `}
  &gt;
    Hover to change color.
  &lt;/div&gt;
)</code></pre><pre><code>// 객체형 스타일
/** @jsxImportSource @emotion/react */
import { css } from &#39;@emotion/react&#39;

const color = &#39;white&#39;

render(
  &lt;div
    css={css({
      padding: &#39;32px&#39;,
      backgroundColor: &#39;hotpink&#39;,
      fontSize: &#39;24px&#39;,
      borderRadius: &#39;4px&#39;,
      cursor: &#39;pointer&#39;,
      &#39;&amp;:hover&#39;: {
        color: `${color}`,
      },
    })}
  &gt;
    Hover to change color.
  &lt;/div&gt;
)</code></pre><p>위 2개의 예제처럼 <code>css()</code> 코드에 문자형과 객체형을 넘길 수 있는데, 가급적 객체로 선언해서 넘기는 것을 권장하고 있습니다. 이 방법을 사용하면, <code>css()</code> 함수 호출을 생략하고 <code>css</code> prop에 바로 객체를 넘길 수 있으며, 특히 타입스크립트를 활용하면 타입 체킹을 통해 버그도 줄일 수 있습니다.</p>
<pre><code>// 함수 호출 생략 (객체 직접 전달)
&lt;div css={{ color: &#39;red&#39; }} /&gt;</code></pre><pre><code>/** @jsxImportSource @emotion/react */
import { CSSObject } from &#39;@emotion/react&#39;;

// 타입 명시
const buttonStyle: CSSObject = {
  padding: &#39;10px 20px&#39;,
  borderRadius: &#39;5px&#39;,
  border: &#39;none&#39;,
  cursor: &#39;pointer&#39;,
  backgroundColor: &#39;royalblue&#39;,
  color: &#39;white&#39;,
  &#39;&amp;:hover&#39;: {
    opacity: 0.9
  }
};</code></pre><pre><code>/** @jsxImportSource @emotion/react */
import { css } from &#39;@emotion/react&#39;;

// css 함수로 감싸면 타입 추론 자동 적용 (가장 간단함)
const buttonStyle = css({
  padding: &#39;10px 20px&#39;,
  borderRadius: &#39;8px&#39;,
  backgroundColor: &#39;hotpink&#39;,
  color: &#39;white&#39;,
  border: &#39;none&#39;,
  cursor: &#39;pointer&#39;,
  &#39;&amp;:hover&#39;: {
    opacity: 0.8,
  },
});

function App() {
  return &lt;button css={buttonStyle}&gt;클릭&lt;/button&gt;;
}</code></pre><p>마찬가지로 prop을 이용해서 동적 스타일링도 쉽게 구현할 수 있습니다.
아래 예시 코드는 <code>variant</code>라는 prop에 따라서 색이 다른 버튼을 구현하고 있습니다.</p>
<pre><code>const colors = {
  default: &#39;black&#39;,
  danger: &#39;red&#39;,
  outline: &#39;blue&#39;,
}

function VariableBtn({ children, variant }) {
  return (
    &lt;button
      css={{
        border: &#39;1px solid gray&#39;,
        borderRadius: &#39;6px&#39;,
        color: colors[variant],
        fontSize: &#39;14px&#39;,
        padding: &#39;10px 16px&#39;,
        cursor: &#39;pointer&#39;,
        appearance: &#39;none&#39;,
        userSelect: &#39;none&#39;,
      }}
    &gt;
      {children}
    &lt;/button&gt;
  )
}

export default VariableBtn</code></pre><pre><code>import VariableBtn from &#39;VariableBtn&#39;

function App() {
  return (
    &lt;&gt;
      &lt;VariableBtn variant=&quot;default&quot;&gt;default&lt;/VariableBtn&gt;
      &lt;VariableBtn variant=&quot;danger&quot;&gt;danger&lt;/VariableBtn&gt;
      &lt;VariableBtn variant=&quot;outline&quot;&gt;outline&lt;/VariableBtn&gt;
    &lt;/&gt;
  );
}</code></pre><p>만약 prop에 따라 바꾸고 싶은 스타일 요소가 여러 개라면 아래처럼 코드를 작성할 수도 있습니다.</p>
<pre><code>const colors = {
  default: &quot;rgb(36, 41, 47)&quot;,
  danger: &quot;rgb(207, 34, 46)&quot;,
  outline: &quot;rgb(9, 105, 218)&quot;,
};

const sizeStyles = {
  sm: {
    fontSize: &quot;12px&quot;,
    padding: &quot;3px 12px&quot;,
  },
  md: {
    fontSize: &quot;14px&quot;,
    padding: &quot;5px 16px&quot;,
  },
  lg: {
    fontSize: &quot;16px&quot;,
    padding: &quot;9px 20px&quot;,
  },
};

function Button({ children, size = &quot;md&quot;, variant = &quot;default&quot; }) {
  return (
    &lt;button
      css={{
        borderRadius: &quot;6px&quot;,
        border: &quot;1px solid rgba(27, 31, 36, 0.15)&quot;,
        backgroundColor: &quot;rgb(246, 248, 250)&quot;,
        color: colors[variant],
        fontFamily: &quot;-apple-system, BlinkMacSystemFont, sans-serif&quot;,
        fontWeight: &quot;600&quot;,
        lineHeight: &quot;20px&quot;,
        ...sizeStyles[size],
        textAlign: &quot;center&quot;,
        cursor: &quot;pointer&quot;,
        appearance: &quot;none&quot;,
        userSelect: &quot;none&quot;,
      }}
    &gt;
      {children}
    &lt;/button&gt;
  );
}

export default Button;</code></pre><h4 id="42-jsx-pragma">4.2 JSX Pragma</h4>
<pre><code>/** @jsxImportSource @emotion/react */</code></pre><p>리액트에서 css prop을 제대로 사용하기 위해서는 파일 상단에 JSX Pragma를 선언해야 합니다. 기본적으로 React에서 JSX는 <code>React.creteElement</code>로 변환되는데, Emotion은 자체적인 기능을 위해 그 대신 자신들만의 함수를 사용해야 합니다.
간단히 정리하자면, 컴파일러에게 &quot;이 파일의 JSX를 변환할 때, React의 <code>jsx()</code> 함수 말고 Emotion의 <code>jsx()</code> 함수를 사용해&quot; 라고 알려주는 역할을 합니다.</p>
<h4 id="43-emotion-장점과-단점">4.3. Emotion 장점과 단점</h4>
<p>Emotion은 JS와 CSS가 한 파일에 통합되어 있으므로, 코드의 유지보수성을 높일 수 있고 스타일이 컴포넌트에 캡슐화되어 스타일 충돌을 줄일 수 있다는 장점이 있습니다. 뿐만 아니라 CSS-in-JS 라이브러리 중에서도 빠른 런타임 성능과, 객체 스타일 문법을 지원하기 때문에 편리합니다.</p>
<p>다만, 스타일을 렌더링 시점에 생성하기 때문에 웹사이트의 복잡도에 따라 런타임 성능 저하 가능성과, 패키지 용량 증가로 인한 초기 로딩 속도 저하 등의 단점 또한 존재합니다. </p>
<h3 id="5-vanilla-extract">5. Vanilla Extract</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/84c63e83-50a1-4088-b8d8-019345b4d223/image.png" alt="">
Vanilla Extract와 앞서 알아본 Emotion의 가장 큰 차이점은 Vanilla Extract는 &#39;Zero-runtime CSS-in-JS&#39;라는 점입니다. 우리가 Emotion을 다루며 계속 언급했던 런타임에 스타일을 생성하고 주입함으로써 파생되는 문제점들을 해결하기 위해 등장했습니다.</p>
<p>Vanilla Extract는 스타일 과정을 JS 또는 TS에서 작성할 수 있게 해주면서도, 스타일이 런타임이 아닌 빌드 타임에 CSS 파일로 컴파일 됩니다. 따라서 런타임에 추가적인 작업 없이 순수 CSS 파일로 컴파일하는 방식을 제공하기 때문에, 성능 개선 및 초기 로드 시간 단축이 가능해집니다.</p>
<p>추가로 Vanilla Extract는 타입스트립트 기반으로 type-safe하게 CSS 스타일을 작성할 수 있습니다.</p>
<h4 id="51-cssts-">5.1. <code>.css.ts</code> ?</h4>
<p>Vanilla Extract의 가장 큰 특징은 <code>.css.ts</code> (<code>.css.js</code>) 라는 특이한 별도의 파일 확장자를 사용한다는 점입니다. 이 파일들을 빌드 과정에서 실행되어 정적인 CSS 파일로 변환되고, 실제 JS 번들에는 생성된 CSS 클래스 이름만 남게 됩니다.</p>
<h4 id="52-vanilla-extract의-스타일링-방식">5.2. Vanilla Extract의 스타일링 방식</h4>
<ol>
<li><strong>style 함수 사용</strong>
가장 기본적인 스타일링 방법입니다. 앞서 알아본 Emotion의 객체형 스타일 방식과 유사한 방식으로 스타일링을 할 수 있습니다. 한 가지 특징으로는 별도의 Extension 없이도 각 스타일 property에 대한 타입 추론이 가능하다는 점이 장점입니다.</li>
</ol>
<pre><code>// Button.css.ts
import { style } from &#39;@vanilla-extract/css&#39;;

// 빌드 시 고유한 해시 클래스 이름(예: Button_container__1hioa8b0)이 생성됩니다.
export const container = style({
  padding: &#39;12px 20px&#39;,
  borderRadius: &#39;8px&#39;,
  backgroundColor: &#39;hotpink&#39;,
  color: &#39;white&#39;,
  border: &#39;none&#39;,
  cursor: &#39;pointer&#39;,
  &#39;:hover&#39;: {
     opacity: 0.8
  }
});</code></pre><pre><code>// Button.tsx
import * as styles from &#39;./Button.css&#39;;

function Button() {
  return &lt;button className={styles.container}&gt;클릭&lt;/button&gt;;
}</code></pre><p>스타일 객체를 배열 형태로도 만들 수 있으므로, 재사용도 비교적 용이합니다.</p>
<pre><code>export const testStyle = style({
    display: &#39;flex&#39;,
    flexDirection: &#39;column&#39;
})

export const mergeStyle = style([
    testStyle, {
        justifyContent: &#39;space-around&#39;,
        gap: &#39;0px 8px&#39;
    }
])</code></pre><ol start="2">
<li><strong><code>styleVariants</code> (동적 스타일링)</strong>
Vanilla Extract는 기본적으로 빌드 타임에 스타일이 결정되어야 하므로, Emotion처럼 런타임 props에 따라 자유롭게 스타일을 바꾸기는 어렵습니다. 대신, 미리 스타일 조합을 만드는 <code>styleVariants</code>를 제공합니다. 
객체 형태로 여러 스타일을 정의하고, 그 중에서 선택해서 사용하는 방식입니다. 첫 번째 인자로는 스타일 변형의 이름을 키로, 해당 스타일에 대한 CSS 규칙을 value로 하는 객체를 전달합니다. <pre><code>// Button.css.ts
import { style, styleVariants } from &#39;@vanilla-extract/css&#39;;
</code></pre></li>
</ol>
<p>const base = style({
  padding: &#39;12px 20px&#39;,
  borderRadius: &#39;8px&#39;,
  border: &#39;none&#39;,
  cursor: &#39;pointer&#39;,
});</p>
<p>// variant에 따른 색상 조합을 미리 정의합니다.
export const variants = styleVariants({
  primary: [base, { backgroundColor: &#39;blue&#39;, color: &#39;white&#39; }],
  secondary: [base, { backgroundColor: &#39;gray&#39;, color: &#39;black&#39; }],
  danger: [base, { backgroundColor: &#39;red&#39;, color: &#39;white&#39; }],
});</p>
<pre><code></code></pre><p>// App.tsx
import { variants } from &#39;./Button.css&#39;;</p>
<p>function App() {
  return (
      &lt;&gt;
        <button className={variants.primary}>Primary</button>
        <button className={variants.danger}>Danger</button>
      &lt;/&gt;
  )
}</p>
<pre><code>
이렇게 동적 스타일링을 할 때, `Recipes` 라이브러리를 활용하면 여러개의 변형을 하나의 객체에서 정의하여 사용할 수 있습니다. 아래 예시를 보면, 나머지는 위 코드와 유사하지만 `compoundVariant`에서 `color`가 `danger`, `size`가 `large`일 때만 `border` 스타일링을 하고 있습니다.

이처럼 2가지 이상의 조건이 겹칠 때 특수한 스타일을 기존 `styleVariants`만 활용해서 구현하려면 복잡하지만, `recipe`을 사용하면 비교적 쉽게 구현할 수 있습니다. 또한 `recipe`에서는 기본값을 설정할 수 있어서, 아무런 옵션을 넘기지 않았을 때 기본 스타일도 관리할 수 있습니다.

</code></pre><p>import { recipe } from &#39;@vanilla-extract/recipes&#39;;</p>
<p>export const buttonRecipe = recipe({
  // 기본 스타일
  base: {
    border: &#39;none&#39;,
    borderRadius: &#39;8px&#39;,
  },</p>
<p> // 변형 가능한 스타일 조합
  variants: {
    color: {
      primary: { backgroundColor: &#39;blue&#39;, color: &#39;white&#39; },
      secondary: { backgroundColor: &#39;gray&#39;, color: &#39;black&#39; },
      danger: { backgroundColor: &#39;red&#39;, color: &#39;white&#39; }
    },
    size: {
      small: { padding: &#39;8px 12px&#39;, fontSize: &#39;14px&#39; },
      medium: { padding: &#39;12px 20px&#39;, fontSize: &#39;16px&#39; },
      large: { padding: &#39;16px 24px&#39;, fontSize: &#39;20px&#39; }
    }
  },</p>
<p>// 특정 조합에서만 적용되는 특수 스타일
  compoundVariants: [
    {
      variants: { color: &#39;danger&#39;, size: &#39;large&#39; },
      style: { border: &#39;2px solid darkred&#39; }
    }
  ],</p>
<p>  // 기본값 설정
  defaultVariants: {
    color: &#39;primary&#39;,
    size: &#39;medium&#39;
  }
});</p>
<pre><code>
3.** `createTheme` 활용 테마 생성**
`createTheme`을 활용하면 공유할 디자인 토큰을 CSS 변수 세트로 만들 수 있습니다. 
</code></pre><p>const [themeClass, vars] = createTheme({ ... });</p>
<pre><code>`createTheme`은 위와 같은 형태로 사용합니다.
첫 번째 반환 값인 `themeClass`는 정의한 테마 변수들의 범위를 지정하는 CSS 클래스 이름입니다. 이 클래스를 특정 태그에 적용하면, 그 하위 요소들은 정의된 CSS 변수 (vars)를 사용할 수 있습니다.
두 번째 반환 값인 `vars`는 실제 스타일 정의에서 사용하는 변수 객체입니다.</code></pre><p>// theme.css.ts</p>
<p>import { createTheme } from &#39;@vanilla-extract/css&#39;;</p>
<p>export const [themeClass, vars] = createTheme({
  color: {
    brand: &#39;blue&#39;,
    text: &#39;#333&#39;,
    background: &#39;#fff&#39;
  },
  space: {
    small: &#39;8px&#39;,
    medium: &#39;16px&#39;,
    large: &#39;24px&#39;
  }
});</p>
<pre><code></code></pre><p>// Box.css.ts</p>
<p>import { style } from &#39;@vanilla-extract/css&#39;;
import { vars } from &#39;./theme.css&#39;;</p>
<p>export const box = style({
  backgroundColor: vars.color.brand,
  padding: vars.space.medium,
});</p>
<pre><code></code></pre><p>// App.tsx
import { themeClass } from &#39;./theme.css&#39;;
import { box } from &#39;./Box.css&#39;;</p>
<p>function App() {
  return (
    <div className={themeClass}>
      <div className={box}>테마가 적용된 박스</div>
    </div>
  );
}</p>
<p>```</p>
<h4 id="53-vanilla-extract-장점과-단점">5.3. Vanilla Extract 장점과 단점</h4>
<p>Vanilla Extract는 빌드 시점에 CSS 파일로 컴파일되므로, 런타임에 스타일을 생성하는 기존 CSS-in-JS 라이브러리와 비교하여 성능 저하가 거의 없습니다. 뿐만 아니라 타입스크립트 기반이기 때문에 타입 안정성이 보장되어 컴파일 시점에 미리 오류를 잡아낼 수 있다는 장점을 가집니다.
그러나 런타임에 스타일이 동적으로 변경되는 동적 스타일링에는 일부 제약이 있을 수 있어 단점 또한 존재합니다. </p>
<h3 id="6-마치며">6. 마치며</h3>
<p>이번 글에서는 CSS-in-JS의 탄생 배경, 그리고 Runtime CSS-in-JS 라이브러리인 Emotion과 Zero-Runtime CSS-in-JS 라이브러리인 Vanilla Extract의 기본적인 개념과 기초 사용법을 다뤘습니다. </p>
<p>이 글에서 다루지 않은 내용도 많고 더 좋은 라이브러리에 대한 정답은 없기 때문에, 프로젝트의 규모와 요구사항, 개인의 선호도 등을 고려하여 가장 적합한 것을 선택하면 좋을 것 같습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 페이지 렌더링 과정(DOM, CSSOM, Render Tree)]]></title>
            <link>https://velog.io/@jin_wls/%EC%9B%B9-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95DOM-CSSOM-Render-Tree</link>
            <guid>https://velog.io/@jin_wls/%EC%9B%B9-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95DOM-CSSOM-Render-Tree</guid>
            <pubDate>Sun, 26 Oct 2025 14:38:43 GMT</pubDate>
            <description><![CDATA[<p>우리는 일상 속에서 정말 많은 웹사이트에 접속하며 살아가고 있다. 링크만 클릭하면, 어느 곳에서나 어떤 기기로든 접속이 가능하다. 사용자의 입장에서는 간단한 과정처럼 보이지만, 실제로 그 뒤에서는 어떠한 과정이 이루어지는지 이 글에서 간단하게나마 알아보고자 한다.</p>
<h3 id="1-웹-페이지-렌더링이란">1. 웹 페이지 렌더링이란?</h3>
<p>웹 브라우저가 HTML, CSS, JavaScript와 같은 소스코드를 파싱하여 사용자가 볼 수 있는 시각적 웹 페이지로 변환하는 과정을 의미한다. 브라우저는 소스코드를 해석하여 텍스트의 크기, 색상, 레이아웃 등 화면에 보이는 모든 것을 계산하여 그려낸다. 쉽게 말해, 웹 페이지가 화면에 표시될 수 있도록 하는 과정이다.</p>
<p>이러한 렌더링 과정은 브라우저의 성능을 좌우하며, 이를 이해하면 웹 사이트의 성능을 최적화하고 UX를 개선할 수 있다.</p>
<h3 id="2-전체-렌더링-과정">2. 전체 렌더링 과정</h3>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/cd27679c-18a8-4460-87d7-7b0333849730/image.png" alt=""></p>
<p>브라우저의 렌더링 과정을 간단하게 그림으로 표현하면 위와 같다.
만약 이 과정을 크게 나눈다면 대략적으로 두 단계로 나눌 수 있다. 첫 번째 단계는 문서를 파싱해서 Render Tree를 만드는 과정이고, 두 번째는 이 Render Tree를 기반으로 브라우저가 렌더링을 수행하는 과정이다.</p>
<h4 id="2-1-dom이란">2-1. DOM이란?</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/68b552de-78f3-4d70-82db-b2de1e54e5ca/image.png" alt=""></p>
<p>가장 먼저 브라우저는 HTML 파일을 읽고 DOM 트리를 생성한다.
DOM이란 &#39;Document Object Model&#39;의 약자로, 그대로 해석하면 &#39;문서 객체 모델&#39;이다. 이를 이해하기 위해서는 &#39;문서 구조&#39;를 이해하면 좋다. 우리가 작성하는 HTML 문서에는 <code>&lt;head&gt;</code>, <code>&lt;body&gt;</code>, <code>&lt;p&gt;</code> 등 여러 태그가 문서의 구조를 이루고 있다. 이러한 HTML 요소의 계층, 구조를 반영하여 만든 객체를 DOM이라고 한다.
<br/></p>
<p><strong>[DOM 구조]</strong>
  DOM을 제대로 이해하기 위해서는 &#39;Tree&#39;라는 자료구조를 이해하면 좋다 DOM의 객체 구조는 &#39;노드 트리&#39;로 표현되기 때문이다. 이는 하나의 부모 줄기가 여러 자식 가지로 갈라지는 트리 형태를 의미한다. 예를 들자면 <code>&lt;html&gt;</code>이라는 부모 줄기에 <code>&lt;head&gt;</code>, <code>&lt;body&gt;</code> 등 다양한 자식 가지가 뻗어나가는 형태를 생각하면 된다.
  <img src="https://velog.velcdn.com/images/jin_wls/post/d4f75bb7-5255-469e-b269-3faafb35a8cc/image.png" alt=""></p>
<p><strong>[DOM과 JavaScript]</strong>
DOM은 JavaSript와 같은 프로그래밍 언어와 HTML로 구성된 웹페이지를 연결시켜주는 역할을 한다. DOM API를 통해 HTML 요소를 찾고, 수정하고 삭제하며 이벤트에 반응하는 등 웹페이지와 상호작용하는 여러 기능들을 구현할 수 있다.</p>
<p>요약하면, 페이지 컨텐츠는 DOM에 저장되고 JavaScript를 통해 접근하거나 조작할 수 있다. </p>
<p><strong>[DOM이 생성되는 순서]</strong></p>
<p>지금까지 이야기한 것처럼, HTML 문서는 HTML 파서에 의해 DOM으로 변환된다.</p>
<p>만약 파서가 DOM 생성 중간에 <code>&lt;script&gt;</code> 태그를 만난다면, 파서는 DOM 생성을 중단하고 JavaScript 엔진이 스크립트에 정의된 파일과 코드를 실행한다. 이 작업이 모두 끝나야만 다시 DOM 생성을 시작한다.</p>
<p>즉, 브라우저는 HTML과 JavaScript를 동시에 처리하지 않고 &#39;동기적&#39;으로 처리한다. 이는 <code>&lt;script&gt;</code> 태그 위치에 따라 DOM 생성이 느려질 수 있다는 의미이다. 따라서 <code>&lt;script&gt;</code> 태그는 HTML 문서 가장 하단에 넣거나, async, defer와 같은 속성들을 활용해서 최적화하는 것이 바람직하다.</p>
<h4 id="2-2-cssom이란">2-2. CSSOM이란?</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/5d721115-4dfb-4201-8f6e-cc33a0477aca/image.png" alt=""></p>
<p>위에서 DOM을 먼저 알아봤기 때문에, CSSOM은 비교적 쉽게 이해할 수 있다.</p>
<p>HTML을 파싱해서 DOM을 생성했다면, 이번에는 CSS 파일을 파싱해서 CSSOM을 생성한다. CSSOM은 &#39;CSS Object Model&#39;의 약자로, CSS 스타일 정보를 트리 형태로 구조화한 것이다.</p>
<p>DOM을 사용해 JavaScript에서 문서의 구조와 내용을 읽고 수정할 수 있는 방식과 비슷하게, CSSOM을 활용하여 JavaScript에서 문서의 스타일을 읽고 수정할 수 있다.</p>
<h4 id="2-3-render-tree-생성">2-3. Render Tree 생성</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/d6c84663-5653-493b-aa83-9782b0457d4f/image.png" alt=""></p>
<p>DOM과 CSSOM을 생성했다면, 브라우저는 이제 DOM과 CSSOM을 결합해 Render Tree를 생성한다. Render Tree는 화면에 표시될 요소들만 포함하고 있으며, 각 요소에 적용될 스타일과 위치에 대한 정보를 담고 있다.</p>
<p>화면에 표시할 요소들만 표현한다는 의미는, 예를 들자면 <code>&lt;script&gt;</code>, <code>&lt;meta&gt;</code> 처럼 화면에 렌더링 될 요소가 아닌 것들은 생략한다. 또한 일부 노드들은 CSS를 사용하여 숨겨지며, 이러한 요소 또한 Render Tree에서 생략된다. (<code>display : none;</code>)</p>
<p>최종적으로 렌더링할 노드들을 선별했다면, 표시된 각 노드에 적절하게 일치하는 CSSOM 규칙을 찾아 적용한 후 내보낸다.</p>
<h4 id="2-4-layout-paint-composite">2-4. Layout, Paint, Composite</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/79b4482c-41ed-4a79-95f1-04af067401af/image.png" alt=""></p>
<p>이제 브라우저는 완성된 Render Tree를 기반으로 각 요소의 위치와 크기를 계산하는 Layout 단계에 들어간다. 이 과정에서 요소가 화면의 어디에 위치할지, 그리고 어떤 크기를 가질지 등을 결정한다.</p>
<p>Layout 단계 이후, paint 단계에서는 계산된 요소들의 위치와 크기를 참고하여 각 요소에 적용될 스타일 속성을 시각적으로 표현한다.</p>
<p>Composite 단계에서는, paint 과정에서 생성된 여러 개의 레이어들을 하나의 화면으로 결합한다. 특히 요소가 겹치는 경우에는 <code>z-index</code>, <code>position</code> 등의 속성을 고려해 겹침 순서를 처리한다.</p>
<h3 id="3-브라우저-렌더링-최적화">3. 브라우저 렌더링 최적화</h3>
<p>브라우저 렌더링 최적화 과정은 웹 사이트의 성능을 결정하는데 있어 핵심 요소이다. 만약 사용자가 웹 사이트에 접속했을 때, 렌더링 과정에서 발생하는 지연은 사용자 경험에 치명적이다. 뿐만 아니라 브라우저 렌더링 최적화를 통해 SEO도 긍정적인 영향을 줄 수 있다.
오늘은 전체적으로 어떤 방법이 있는지 알아보고, 다음 글에서 더 구체적으로 알아볼 것이다.</p>
<p><strong>1. HTML 구조 간소화</strong>
  불필요한 태그나 중첩된 구조를 줄여 브라우저가 DOM을 빠르게 처리할 수 있도록 하는 것이 좋다.</p>
<p><strong>2. 리플로우와 리페인트 줄이기</strong>
  리플로우와 리페인트를 최대한 줄이는 것은 브라우저 성능 최적화의 주요 대상이다. 이를 최소화하는 방법으로는 스타일 변경 최소화, 리플로우를 발생시키는 함수나 속성을 변수에 저장하여 여러번 호출 방지, DOM 접근 최소화 등이 있다.
아래 표는 리플로우, 리페인트를 발생시키는 원인을 정리한 것이다.</p>
<table>
<thead>
<tr>
<th><strong>원인</strong></th>
<th><strong>예시 코드</strong></th>
<th><strong>비고</strong></th>
</tr>
</thead>
<tbody><tr>
<td>DOM 구조 변경</td>
<td><code>element.appendChild()</code></td>
<td>새 노드 추가 시 전체 레이아웃 재계산</td>
</tr>
<tr>
<td>스타일 변경</td>
<td><code>element.style.width = &quot;100px&quot;</code></td>
<td>위치·크기 변경 시 Reflow 발생</td>
</tr>
<tr>
<td>요소 크기 측정</td>
<td><code>offsetWidth</code>, <code>clientHeight</code>, <code>getComputedStyle()</code></td>
<td>강제 Reflow(triggered layout)</td>
</tr>
<tr>
<td>윈도우 리사이즈</td>
<td><code>window.resize</code></td>
<td>전체 Layout 재계산</td>
</tr>
</tbody></table>
<p><strong>3. 이미지와 리소스 최적화</strong>
이미지는 파일 크기가 크기 때문에 로딩 속도에 큰 영향을 미친다. 사용자가 보지 않는 이미지를 미리 로드하면 불필요한 네트워크 요청과 자원 소비가 일어나므로, lazy loading을 적절히 사용하는 방법도 고려해보면 좋다.</p>
<p><strong>4. JS 로드 방식 최적화</strong>
스크립트 파일이 로드되고 실행되는 방식 또한 페이지 렌더링 성능에 중요한 역할을 미친다. 위에서 잠깐 언급한 것처럼 async, defer 속성을 적절히 사용하면 최적화에 큰 도움이 된다. async 속성은 DOM 조작이 필요 없거나, 다른 스크립트에 의존하지 않는 독립적인 스크립트에 사용하고 defer 속성은 반대로 DOM을 조작해야 하거나 스크립트 간의 실행 순서가 중요한 경우에 권장되는 방식이다.
<img src="https://velog.velcdn.com/images/jin_wls/post/5cb45d52-98f8-43b0-86f7-dc0f194d4ae5/image.png" alt=""></p>
<p><strong>참고</strong></p>
<p><a href="https://developer.mozilla.org/ko/docs/conflicting/Web/API/Document_Object_Model_a0b90593de4c5cb214690e823be115a18d605d4bc7719ba296e212da2abe18ef">https://developer.mozilla.org/ko/docs/conflicting/Web/API/Document_Object_Model_a0b90593de4c5cb214690e823be115a18d605d4bc7719ba296e212da2abe18ef</a>
<a href="http://docs.tosspayments.com/resources/glossary/dom">http://docs.tosspayments.com/resources/glossary/dom</a>
<a href="https://bitsofco.de/what-exactly-is-the-dom/?utm_source=CSS-Weekly&amp;utm_campaign=Issue-341&amp;utm_medium=email&amp;source=post_page-----7d72f85be01c---------------------------------------">https://bitsofco.de/what-exactly-is-the-dom/?utm_source=CSS-Weekly&amp;utm_campaign=Issue-341&amp;utm_medium=email&amp;source=post_page-----7d72f85be01c---------------------------------------</a>
<a href="https://web.dev/articles/critical-rendering-path/render-tree-construction?hl=ko">https://web.dev/articles/critical-rendering-path/render-tree-construction?hl=ko</a>
<a href="https://onlydev.tistory.com/9">https://onlydev.tistory.com/9</a>
<a href="https://binyard.me/javascript/basic/browser/js002">https://binyard.me/javascript/basic/browser/js002</a>
<a href="https://ko.javascript.info/script-async-defer">https://ko.javascript.info/script-async-defer</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Lazy_loading">https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Lazy_loading</a>
<a href="https://www.corewebvitals.io/pagespeed/async-vs-defer-javascript-and-core-web-vitals">https://www.corewebvitals.io/pagespeed/async-vs-defer-javascript-and-core-web-vitals</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP와 HTTPS 간단히 살펴보기]]></title>
            <link>https://velog.io/@jin_wls/HTTP%EC%99%80-HTTPS-%EA%B0%84%EB%8B%A8%ED%9E%88-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@jin_wls/HTTP%EC%99%80-HTTPS-%EA%B0%84%EB%8B%A8%ED%9E%88-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 15 Oct 2025 12:38:38 GMT</pubDate>
            <description><![CDATA[<p>최근 간단한 웹사이트를 배포하며, HTTP에 대해 알아보고 싶다는 생각이 들었다. 항상 웹사이트에 접속할 때마다 사용하면서도, 한번도 제대로 공부해본 적은 없어서 정확히 무슨 역할을 하는지 몰랐다. 
이 글에서는 HTTP가 무엇인지 간단하게나마 살펴보고 그 역할을 이해해보고자 한다.</p>
<h3 id="http란">HTTP란?</h3>
<p>HTTP란 Hyper Text Transfer Protocol의 약자로, <strong>서버와 클라이언트가 서로 데이터를 주고 받기 위해 사용하는 통신 규약</strong>을 의미한다. HTTP는 웹에서 이루어지는 모든 데이터 교환의 기초이자, &#39;클라이언트-서버 프로토콜&#39;이라고도 한다. 여기서 말하는 &#39;클라이언트-서버 프로토콜&#39;은 수신자(웹 브라우저) 측에 의해 요청이 초기화되는 프로토콜을 의미한다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/jin_wls/post/c21d8104-bbb8-4463-b089-b4fcbaa4e554/image.png" alt=""></th>
<th><img src="https://mdn.github.io/shared-assets/images/diagrams/http/overview/fetching-a-page.svg" alt=""></th>
</tr>
</thead>
</table>
<p>조금 더 쉽게 설명해보자면, 웹 브라우저가 먼저 서버에게 말을 걸어 무언가를 달라고 요청하는 것으로 통신이 시작된다. 이처럼 HTTP 통신은 클라이언트와 서버로 나뉜 구조로 되어있다. <strong>클라이언트가 요청(Request)하면 서버가 응답(Response) 하는 것이다.</strong></p>
<br/>

<h3 id="http의-특징은-어떤-것이-있을까">HTTP의 특징은 어떤 것이 있을까?</h3>
<p>이제 대표적인 HTTP의 특징 2가지를 알아볼 것이다.</p>
<h4 id="1--무상태성stateless">1 . 무상태성(Stateless)</h4>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/jin_wls/post/ebe5e691-a533-453b-92e3-e94139dcfedc/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/jin_wls/post/add1c43a-bfc8-4bd9-98c9-e65f487c487a/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>&#39;상태 유지&#39;를 한다는 것은 왼쪽 사진처럼 <strong>서버가 클라이언트의 상태를 보존하고 있는 것</strong>을 의미한다. 딱히 문제가 될 게 없어 보일 수 있지만, 만약 서버를 바꿔야 한다고 생각해보자. 그러면 그때마다 클라이언트의 내용을 기록해서 상태를 유지해야 하는데 쉽지 않은 일이 될 것이다.
오른쪽과 같은 무상태 환경에서는 <strong>클라이언트가 상태 정보를 갖고 있으므로,</strong> 서버에 의존하지 않는다. 그래서 서버와 통신할 때 클라이언트 측에서 실어 보내 인증하는 식이다. 서버에 의존하지 않는다는 것은 아무 서버나 호출해도 된다는 의미이고, 그 덕분의 <strong>서버의 수평확장</strong>에 유리하다. 
그렇다고 해서 무상태성이 장점만 있는 것은 아니다. 무상태 환경은 상태 유지 환경보다 <strong>데이터를 많이 사용한다.</strong></p>
<h4 id="2-http의-비연결성">2. HTTP의 비연결성</h4>
<p><img src="https://velog.velcdn.com/images/jin_wls/post/836e61b3-779f-4e77-b75d-da74aa4abc3e/image.png" alt=""></p>
<p>HTTP는 기본이 연결을 유지하지 않는 모델이므로, <strong>서버와 클라이언트의 연결을 지속하지 않는다.</strong> 만약 연결을 유지하면, 서버와 클라이언트는 서로의 네트워킹 요청이 없어도 계속해서 유지되어 자원이 계속 사용된다.
이와 반대로 연결을 유지하지 않고, <strong>필요할 때만 연결하여 사용하면 서버의 자원을 효율적으로 사용</strong>할 수 있다. 
다만 연결을 계속 끊으면, TCP/IP 연결을 매번 새롭게 맺어야 하고 이는 비효율로 이어질 수 있다. 이를 해결하기 위해 <strong>&#39;HTTP 지속 연결&#39;</strong> 등 여러 방법이 고안되고 최적화가 이루어지고 있다.
<img src="https://velog.velcdn.com/images/jin_wls/post/245fa59a-f850-4b48-918d-f0b1927d6ccf/image.png" alt="">
&#39;HTTP 지속 연결&#39;이란 하나의 파일을 받을 때마다 다시 TCP/IP 연결을 끊고 맺는 비연결성을 극복하기 위해, <strong>소켓 연결을 일정 시간동안 더 유지하면서 필요한 자원들을 모두 다운받을 때까지 연결이 종료되지 않고 요청 응답을 반복한 뒤 종료하는 것</strong>을 의미한다.</p>
<br/>

<h3 id="http-요청-메서드">HTTP 요청 메서드</h3>
<p>http 요청 메서드란 클라이언트가 서버에 요청을 보낼 때 사용되는 명령어이다.</p>
<p><strong>GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH, CONNECT</strong> 등 여러 메서드가 있지만 그 중 대표적인 4가지를 간단하게만 알아볼 것이다.</p>
<ol>
<li>GET : 주로 <strong>데이터를 조회하거나 검색</strong>할 때에 사용되는 메서드이다.</li>
<li>POST: 주로 <strong>새로운 리소스를 생성</strong>할 때 사용되는 HTTP 메서드다.</li>
<li>PUT: 주로 리소스를 <strong>전체적으로 업데이트</strong>하거나, 리소스가 없는 경우 <strong>새로 생성</strong>할 때 사용한다.</li>
<li>DELETE : 주로 저장된 <strong>리소스를 삭제</strong>하는데 사용된다.</li>
</ol>
<p>응답 상태 코드는 아래 이미지와 같다.
<img src="https://velog.velcdn.com/images/jin_wls/post/2cd02a8e-e53e-44bf-b83e-ace4fdfd98c9/image.png" alt=""></p>
<br/>

<h3 id="https-알아보기">HTTPS 알아보기</h3>
<p>[HTTP의 구조]
http는 Method, Path, Version, Headers, Body 등으로 구성된다.
<img src="https://velog.velcdn.com/images/jin_wls/post/73d05e25-ba3a-4a34-9fc4-1695b718147e/image.png" alt=""></p>
<p>하지만 HTTP는 암호화가 되지 않은 평문 데이터를 전송하는 프로토콜이었기 때문에, HTTP로 비밀번호나 주민번호 등을 주고 받으면 제 3자가 정보를 조회할 수 있었다. 이러한 문제를 해결하기 위해 HTTPS가 등장하게 되었다.
HyperText Transfer Protocol Secure의 약자인 HTTPS는 HTTP에 <strong>데이터 암호화 계층이 추가된 프로토콜이다</strong>.  HTTPS는 HTTP와 다르게 443번 포트를 사용하며, 네트워크 상에서 중간에 제 3자가 정보를 볼 수 없도록 암호화를 지원하고 있다.
노출이 되어도 괜찮은 단순한 정보만을 다룬다면 http도 괜찮겠지만, 만약 개인정보와 같은 민감한 데이터를 주고 받아야 한다면 https를 이용하는 것이 바람직하다.</p>
]]></description>
        </item>
    </channel>
</rss>