<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hello-yujin.log</title>
        <link>https://velog.io/</link>
        <description>매일매일 조금씩 성장하려 노력하는 프론트엔드 개발자입니다!</description>
        <lastBuildDate>Fri, 20 Feb 2026 04:54:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hello-yujin.log</title>
            <url>https://velog.velcdn.com/images/hello-yujin/profile/63762bea-9931-4261-b04d-0d65ffba4040/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hello-yujin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hello-yujin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[띵스룸 2025 하반기 운영 회고]]></title>
            <link>https://velog.io/@hello-yujin/%EB%9D%B5%EC%8A%A4%EB%A3%B8-2025-%ED%95%98%EB%B0%98%EA%B8%B0-%EC%9A%B4%EC%98%81-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hello-yujin/%EB%9D%B5%EC%8A%A4%EB%A3%B8-2025-%ED%95%98%EB%B0%98%EA%B8%B0-%EC%9A%B4%EC%98%81-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 20 Feb 2026 04:54:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>띵스룸 프론트엔드 레포지토리 링크: <a href="https://github.com/DdingSroom/dding-sroom-frontend">https://github.com/DdingSroom/dding-sroom-frontend</a></p>
</blockquote>
<p>띵스룸(Ddingsroom)은 명지대학교 학생지원팀의 승인 하에 명지대학교 학생회관 4층 5개의 스터디룸을 대상으로 운영되고 있는 명지대학교 공식 학생회관 스터디룸 예약 서비스이다.</p>
<p>이 글에서는 2025년 8월 중후반 약 2주간 진행된 시범 운영 및 2025년 9월부터 글을 쓰고 있는 현재 2026 2월까지 진행된 공식 운영 과정에서 있었던 일련의 일들을 회고글 형태로 작성해보려 한다.</p>
<p>개발을 본격적으로 시작하게 된건 2025년 7월 후반이었다. 본래의 학생회관 4층 스터디룸은 예약 서비스의 부재로 인해 공정하지 못한 이용 기회, 자리 무단 점유, 비효율적인 회전율 등의 여러 문제가 존재하고 있었다. 띵스룸 팀은(아래 &#39;우리&#39;) 이러한 문제를 내가 해결 할 수 있는 &#39;예약 서비스 개발&#39;로 해결할 수 있다 생각했고, 학생지원팀과의 면담 일정을 잡아 제안서를 가지고 학생지원팀 계장님께 제안을 드렸다.</p>
<blockquote>
<p>제출했던 띵스룸 제안서: <a href="https://certain-gallium-c59.notion.site/2421129efe5a80069a94f4e0c99cf3e4?source=copy_link">https://certain-gallium-c59.notion.site/2421129efe5a80069a94f4e0c99cf3e4?source=copy_link</a></p>
</blockquote>
<p>다행히 제안은 통과되었고 제안이 통과되고 난 후부터 바로 개발에 착수했다. 약 한달 간의 개발 기간을 통해 대략적인 서비스의 형태를 갖추게 되었고, 학생지원팀과 다시 면담 일자를 잡고 서비스 운영에 대한 허락을 받으러 갔다.</p>
<p>띵스룸은 학생들에게 제공 될 메인 예약 서비스 도메인 외에도 학생지원팀측에서 스터디룸 및 서비스에 가입한 학생들을 관리할 수 있도록 하는 관리자 도메인도 함께 구현되었기에, 학생지원팀측에서 주로 사용할 관리자 도메인을 위주로 서비스 이용 방법을 소개드렸다.</p>
<p>학생지원팀측에서 메인 예약 서비스 도메인 및 관리자 도메인에 대한 여러 번의 기술적인 테스트 및 검증을 통한 서비스에 대한 기술적인 문제가 없다는 판단 하에 운영을 허락해주셨다. </p>
<p>공식적인 운영에 앞서, 2025년 8월 17일 - 2025년 8월 31일 약 2주 간의 기간 동안 시범운영을 거쳐 문제가 없다면 2025년 9월 1일부터 공식 운영을 허락해주신다는 학교 측의 제안을 받아들여 5개의 스터디룸 앞에 시범운영 관련 공고문을 붙이고 시범운영을 시작했다.</p>
<p><img src="https://velog.velcdn.com/images/ddingsroom/post/65524a14-6915-4b2e-bc74-aeb2e4b3def9/image.jpeg" alt="">
스터디룸 앞에 붙여진 공고문 내용</p>
<hr>
<p><img src="https://velog.velcdn.com/images/ddingsroom/post/6d912460-1370-4581-8b05-f3310f1a757f/image.jpeg" alt="">
공고문을 붙이고 서비스가 운영되고 있는 사진</p>
<hr>
<p>시범 운영 기간 동안에는 학교 측의 요청 사항에 따라 서브 기능들(커뮤니티, 공지사항, 건의사항)을 제외한 메인 기능인 예약 관련 기능들(회원가입, 로그인, 예약, 마이페이지)만 운영을 진행했다.</p>
<p>그 결과 시범운영 기간 동안 아래 구글 애널리틱스 대시보드에 나와있는 것과 같이 약 4.8천 건의 조회수와 8천 건의 이벤트 수가 서비스 내에서 발생했다.
<img src="https://velog.velcdn.com/images/ddingsroom/post/d729d6ac-f45e-43c8-80ff-ab167c290457/image.png" alt=""></p>
<hr>
<p>또한 시범운영 기간 동안 교내 창의적 SW 경진대회가 있었는데 해당 대회에 서비스를 출품하고자 했다. 경진대회에 출품할 때는 서비스의 모든 기능을 다 보여주는게 맞다고 판단하여 실제 시범 운영이 진행되고 있는 Main 브랜치와 별도로 Production 브랜치를 생성해 배포 도메인을 다르게 하여 서비스를 출품했다.</p>
<blockquote>
<p>제출했던 SW 명세서: <a href="https://certain-gallium-c59.notion.site/SW-2551129efe5a80978256d62bbba81314?source=copy_link">https://certain-gallium-c59.notion.site/SW-2551129efe5a80978256d62bbba81314?source=copy_link</a></p>
</blockquote>
<blockquote>
<p>제출했던 전체 기능을 제공하는 버전 실행 방법을 설명하는 문서: <a href="https://certain-gallium-c59.notion.site/SW-2551129efe5a80978256d62bbba81314?source=copy_link">https://certain-gallium-c59.notion.site/SW-2551129efe5a80978256d62bbba81314?source=copy_link</a></p>
</blockquote>
<p>출품한 서비스는 교내 경진대회에서 서류 심사와 발표 심사를 거쳐 &#39;장려상(4위)&#39;라는 성과를 얻게 되었다.</p>
<blockquote>
<p>2025 명지대학교 SW 경진대회 입상작 관련 게시글: <a href="https://cs.mju.ac.kr/ince/5121/subview.do?enc=Zm5jdDF8QEB8JTJGYmJzJTJGaW5jZSUyRjY3OSUyRjIyNjAwMiUyRmFydGNsVmlldy5kbyUzRg%3D%3D">https://cs.mju.ac.kr/ince/5121/subview.do?enc=Zm5jdDF8QEB8JTJGYmJzJTJGaW5jZSUyRjY3OSUyRjIyNjAwMiUyRmFydGNsVmlldy5kbyUzRg%3D%3D</a></p>
</blockquote>
<p>시상식은 2025년 10월 17일 자연캠퍼스에서 진행되었다.</p>
<blockquote>
<p>시상식 관련 기사: <a href="https://www.newsis.com/view/NISX20251022_0003372670">https://www.newsis.com/view/NISX20251022_0003372670</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ddingsroom/post/1f0036cf-c567-4c41-9c4a-99bcaa05f2f3/image.png" alt=""></p>
<p>시상식에서 장려상을 수여받는 모습</p>
<hr>
<p>시범 운영 기간 동안 별도의 기술적인 오류나 결함, 학생지원팀으로의 문의 없이 무사히 운영을 마칠 수 있었고, 2025년 9월 1일 부터 학생지원팀의 승인 하에 공식 운영을 허락받게 되었다.</p>
<p>공식 운영부터는 시범 운영 때는 제공하지 않았던 서브 기능들(커뮤니티, 공지사항, 건의사항)까지 포함하여 운영을 진행하였다.</p>
<p>아래 구글애널리틱스 대시보드 결과와 같이 9월부터 12월까지 약 3달 간 3.1만 건의 조회수와 5.5만 건의 이벤트 수가 기록되었다.</p>
<p><img src="https://velog.velcdn.com/images/ddingsroom/post/3192ec99-88a4-4676-9712-d6d88687158d/image.png" alt=""></p>
<p>특히 중간고사나 기말고사 기간에는 트래픽이 평소보다 3배 이상 증가하였는데, 기술적인 오류나 결함없이 운영을 성공적으로 진행할 수 있었다.</p>
<p>아래와 같이 건의 내역으로 접수된 내용 중 기술적인 건의 내역은 최대한 빠르게 반영 및 개발하여 재배포를 진행하였고, 운영적인 건의 내역은 학생지원팀과의 논의를 통해 해결할 수 있는 내역은 최대한 빠르게 해결하였다.</p>
<p>학생지원팀도 지속적으로 관리자 페이지를 통해 예약 현황을 확인하며, 우리에게 수정 내용을 면담을 통해 요청하였고, 우리는 즉각적으로 피드백을 받아들여 서비스에 반영하였다.</p>
<p><img src="https://velog.velcdn.com/images/ddingsroom/post/9223ba5c-fd48-4ee5-8c34-d5fd3a73d1a9/image.gif" alt=""></p>
<hr>
<p>약 3개월 간의 공식 운영을 마치고 2025년 12월 23일부터 2026년 2월 28일까지 학생회관 4층 내부 공사가 진행되었다. 학교 측의 요청에 따라 해당 공사 기간 동안에는 서비스가 운영되지 않도록 막아두고 돌아오는 2026년 3월부터 다시 운영을 재개하기로 했다.</p>
<p><img src="https://velog.velcdn.com/images/ddingsroom/post/f9e04fe2-394c-482f-b86a-18d8dae5e6d4/image.png" alt="">
middleware로 서비스 진입을 막아놓은 화면</p>
<hr>
<p>또한 학교 홍보 기자단에서 &#39;띵스룸&#39; 서비스와 서비스를 개발한 &#39;띵스룸 팀&#39;에 대해 인터뷰 요청이 들어와 인터뷰를 진행하였고, 해당 인터뷰 내용은 학교 공식 인스타그램 계정에 매거진 형태로 게재되었다.</p>
<blockquote>
<p>게재된 매거진 링크: <a href="https://www.instagram.com/p/DRyMEddD7TA/?img_index=1">https://www.instagram.com/p/DRyMEddD7TA/?img_index=1</a></p>
</blockquote>
<hr>
<p>공사가 시작된 2025년 12월 23일 부터 현재 이 글을 쓰고 있는 2026년 2월 19일까지 아래와 같은 리팩토링 작업이 진행되었고, 3월부터는 서비스 게재와 함께 아래와 같은 사항들을 새로운 프론트엔드 팀원과 함께 리팩토링해나갈 계획에 있다.
<img src="https://velog.velcdn.com/images/ddingsroom/post/ae6643f4-1186-42e9-ad23-3854e34e7f47/image.png" alt=""></p>
<h4 id="2026년-상반기-리팩토링-예정-내역">2026년 상반기 리팩토링 예정 내역</h4>
<ul>
<li>Javascript -&gt; Typescript 언어 마이그레이션</li>
<li>운영하며 필요하다고 느낀 추가적인 초기세팅 재진행</li>
<li>Next.js 특징을 살려 서비스 고도화</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Comfit이 Storybook을 적용하기로 한 이유]]></title>
            <link>https://velog.io/@hello-yujin/Comfit%EC%9D%B4-Storybook%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0%EB%A1%9C-%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@hello-yujin/Comfit%EC%9D%B4-Storybook%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0%EB%A1%9C-%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Thu, 01 Jan 2026 11:40:12 GMT</pubDate>
            <description><![CDATA[<p>앱잼을 준비하면서 가장 걱정했던 건 “개발을 얼마나 빨리 하느냐”가 아니라, 팀으로 만들 때 생기는 소모였다.
기획/디자인/프론트/백엔드가 동시에 달리다 보면, 기능 하나 붙이는 데도 생각보다 시간이 많이 새는 구간이 있다.</p>
<ul>
<li>“이 버튼 hover는 어떻게 되는 거였지?”</li>
<li>“모달 padding이 왜 페이지마다 달라…?”</li>
<li>“이 컴포넌트, 여기선 되는데 저 페이지에서만 깨져요”</li>
<li>“디자이너가 말한 ‘카드’가 A카드인지 B카드인지부터 다시 맞추자…”</li>
</ul>
<p>나도 팀 프로젝트를 해보면서 느낀 건, 이런 문제들이 실력 부족 때문이 아니라 협업 구조가 불명확해서 터진다는 점이었다.
그래서 앱잼 전에 체계를 정리해놓고 싶었고, 그게 Storybook으로 이어졌다.</p>
<hr>
<h2 id="✔️-팀-프로젝트에서-제일-자주-터지는-문제-ui가-어디에-정의되어-있지">✔️ 팀 프로젝트에서 제일 자주 터지는 문제: “UI가 어디에 정의되어 있지?”</h2>
<p>개인 프로젝트와 다르게 팀 프로젝트는 화면이 늘어나면서 “비슷한 UI”가 여기저기 복제된다.
기존에는 보통 이런 흐름으로 개발을 진행했다.</p>
<ol>
<li>페이지 하나 생성</li>
<li>그 안에서 버튼/카드/모달을 바로 구현</li>
<li>“다른 데서도 쓰겠네?” 싶으면 그때 컴포넌트 분리</li>
</ol>
<p>결국 UI를 ‘코드’로 정의하고 관리하는 기준점이 필요했다.
그게 Comfit이 Storybook을 찾게 된 첫 번째 이유다.</p>
<p>Storybook을 쓰기 시작하면 앞으로 흐름이 아래와 같이 변경될 것이다.</p>
<ol>
<li>컴포넌트를 먼저 생성</li>
<li>Story에서 상태별로 확인</li>
<li>컴포넌트가 어느 정도 책임을 가져도 되는지 판단</li>
<li>그 다음 페이지에 조립</li>
</ol>
<p>특히 앱잼처럼 UI 요구사항이 자주 바뀌는 환경에서는 페이지 안에서 수정하는 것보다 컴포넌트 단위에서 검증하는 게 훨씬 빠르다고 생각했다.</p>
<hr>
<h2 id="✔️-storybook이-해결해준-핵심은-컴포넌트의-단일-출처single-source-of-truth">✔️ Storybook이 해결해준 핵심은 “컴포넌트의 단일 출처(Single Source of Truth)”</h2>
<p>Storybook은 한 줄로 정리하면 이거였다.</p>
<blockquote>
<p>UI 컴포넌트를 페이지랑 분리해서, 컴포넌트 자체를 전시하고 테스트하는 공간</p>
</blockquote>
<p>나는 Storybook을 도입하기 전에, 컴포넌트가 항상 페이지에 붙어 있는 형태로만 작업했다.
그러다 보니 컴포넌트를 고칠 때마다 이런 식의 비효율이 발생했다.</p>
<ul>
<li>페이지 실행 → 해당 컴포넌트 있는 화면 이동 → 상태 만들어서 확인 → 다시 수정 → 다시 확인</li>
<li>props 조합이 바뀌면 또 다른 페이지 가서 확인</li>
<li>빈 상태(empty), 로딩, 에러 상태 같은 건 페이지에서 만들기 번거로워서 “나중에…”가 됨</li>
</ul>
<p>Storybook은 이걸 반대로 만든다.</p>
<ul>
<li>컴포넌트만 단독으로 띄워서</li>
<li>props를 바꿔가며 케이스를 정리하고</li>
<li>“정상/로딩/빈상태/에러/긴텍스트/모바일” 같은 모든 상태를 의도적으로 만들어 확인한다</li>
</ul>
<p>이 흐름이 팀 프로젝트에서 특히 유용했던 이유는 명확했다.</p>
<h3 id="☑️-디자인-확인이-페이지가-아니라-컴포넌트-기준이-됨">☑️ “디자인 확인”이 페이지가 아니라 컴포넌트 기준이 됨</h3>
<p>페이지는 여러 요소가 섞여 있어서 디테일 체크가 어렵다.
근데 Storybook에서는 버튼 하나, 카드 하나를 정확히 그 컴포넌트만 보고 검증할 수 있다.</p>
<h3 id="☑️-qa가-기능-qa뿐-아니라-ui-케이스-qa까지-가능해짐">☑️ “QA”가 기능 QA뿐 아니라 UI 케이스 QA까지 가능해짐</h3>
<p>팀 프로젝트에서 자주 놓치는 케이스가 이거였다.</p>
<ul>
<li>텍스트가 길면 줄바꿈이 터지는지</li>
<li>이미지 없으면 레이아웃이 무너지는지</li>
<li>버튼 disabled 상태가 명확한지</li>
<li>에러 메시지가 두 줄 이상이면 UI가 깨지는지</li>
</ul>
<p>이런 걸 페이지에서 다 확인하려면 시간이 정말 많이 든다.
Storybook에서는 한 화면에서 “케이스”를 정리해두면 계속 재활용할 수 있다.</p>
<hr>
<h2 id="✔️-그래서-우리는-왜-지금-storybook을-붙이기로-결정했나">✔️ “그래서 우리는 왜 ‘지금’ Storybook을 붙이기로 결정했나”</h2>
<p>결론적으로 Storybook을 도입하기로 한 결정은 “좋아 보인다”가 아니라, 앱잼의 현실적인 제약 때문에 더 확실해졌다.</p>
<h3 id="☑️-앱잼은-시간이-짧고-ui-수정이-잦다">☑️ 앱잼은 시간이 짧고, UI 수정이 잦다</h3>
<p>짧은 기간에 MVP를 만들면, 기획과 디자인이 중간중간 계속 바뀐다.
이때 페이지에서만 UI를 관리하면, 변경이 생길 때마다 전체 화면을 다시 열어서 “어디가 영향 받는지” 추적해야 한다.</p>
<p>Storybook이 있으면 바뀐 컴포넌트의 영향 범위를 Stories 목록만 봐도 바로 알 수 있다.</p>
<h3 id="☑️-협업에서-가장-많이-깨지는-건-컴포넌트-재사용이다">☑️ 협업에서 가장 많이 깨지는 건 “컴포넌트 재사용”이다</h3>
<p>예를 들어 팀원 A는 <code>PrimaryButton</code>을 만들어 쓰는데
팀원 B는 같은 버튼을 페이지에 그냥 새로 만들 수도 있다. “어디에 뭐가 있는지” 안 보여서 생기는 문제라고 생각한다.</p>
<p>Storybook은 “컴포넌트 카탈로그” 역할을 해서 어떤 컴포넌트를 써야 하는지 한 번에 알 수 있게 한다.</p>
<h3 id="☑️-우리는-특히-상태-관리가-정리된-ui를-만들고-싶었다">☑️ 우리는 특히 ‘상태 관리’가 정리된 UI를 만들고 싶었다</h3>
<p>로딩, 에러, 빈 상태 처리가 흔히 “나중에”로 미뤄진다.
근데 앱잼은 데모까지 가야 해서, 이런 디테일이 결국 완성도를 가를 수 있다고 생각한다.</p>
<p>Storybook에서 상태를 강제로 정리해두면
개발 과정에서 “기본 상태만 있는 UI”가 되는 걸 막아줄 수 있다고 한다.</p>
<hr>
<h2 id="✔️-storybook-설치--세팅-react-기준--실습-기록">✔️ Storybook 설치 &amp; 세팅 (React 기준 / 실습 기록)</h2>
<p>나는 “일단 빨리 붙이고, 팀에서 쓰기 좋은 구조”를 목표로 했다.</p>
<h3 id="☑️-설치">☑️ 설치</h3>
<pre><code>npx storybook@latest init</code></pre><p>실행하면 알아서 프로젝트 환경에 맞게 설정해준다.
(React/Vite/Next 등 자동 감지)</p>
<h3 id="☑️-실행">☑️ 실행</h3>
<pre><code>npm run storybook</code></pre><p>기본 포트는 <code>6006</code></p>
<hr>
<h2 id="✔️내가-실제로-만든-예시-button-컴포넌트-스토리">✔️내가 실제로 만든 예시: Button 컴포넌트 스토리</h2>
<h3 id="☑️-button-컴포넌트">☑️ Button 컴포넌트</h3>
<pre><code class="language-tsx">// components/Button/Button.tsx
type ButtonProps = {
  label: string;
  variant?: &#39;primary&#39; | &#39;secondary&#39;;
  disabled?: boolean;
  onClick?: () =&gt; void;
};

export default function Button({
  label,
  variant = &#39;primary&#39;,
  disabled = false,
  onClick,
}: ButtonProps) {
  return (
    &lt;button
      disabled={disabled}
      onClick={onClick}
      className={`btn ${variant} ${disabled ? &#39;disabled&#39; : &#39;&#39;}`}
    &gt;
      {label}
    &lt;/button&gt;
  );
}</code></pre>
<h3 id="☑️-story-파일">☑️ Story 파일</h3>
<pre><code class="language-tsx">// components/Button/Button.stories.tsx
import type { Meta, StoryObj } from &#39;@storybook/react&#39;;
import Button from &#39;./Button&#39;;

const meta: Meta&lt;typeof Button&gt; = {
  title: &#39;components/Button&#39;,
  component: Button,
  args: {
    label: &#39;버튼&#39;,
    variant: &#39;primary&#39;,
    disabled: false,
  },
};

export default meta;
type Story = StoryObj&lt;typeof Button&gt;;

export const Primary: Story = {};
export const Secondary: Story = { args: { variant: &#39;secondary&#39; } };
export const Disabled: Story = { args: { disabled: true } };
export const LongText: Story = { args: { label: &#39;텍스트가 엄청 길어지면 어떻게 될까 확인하기&#39; } };</code></pre>
<p>이렇게 해두면 팀이 버튼을 쓸 때 Primary/Secondary/Disabled/긴텍스트 케이스를 “페이지 실행” 없이 바로 확인 가능하다.</p>
<hr>
<h2 id="✔️-args를-쓰기-시작하면서-상태-정의가-명확해짐">✔️ args를 쓰기 시작하면서 “상태 정의”가 명확해짐</h2>
<p>Storybook에서 가장 많이 쓰게 될 기능이 바로 <code>args</code>이다.</p>
<pre><code class="language-tsx">export const Disabled: Story = {
  args: {
    disabled: true,
  },
};</code></pre>
<p>이 한 줄이 의미하는 바는 아래와 같다.</p>
<ul>
<li>이 컴포넌트에는 <strong>disabled 상태가 존재</strong>한다</li>
<li>이 상태는 <strong>디자인/기획적으로도 고려된 상태</strong>다</li>
<li>페이지에서 “나중에 처리”할 수 없는 상태가 된다</li>
</ul>
<p>특히 아래 상태들을 Story로 만들어두는 게 좋을 것 같다.</p>
<ul>
<li>disabled</li>
<li>loading</li>
<li>empty (데이터 없음)</li>
<li>텍스트가 비정상적으로 긴 경우</li>
<li>모바일 뷰포트</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/097c25e5-706f-451c-bb27-5dfb3c942df8/image.png" alt="">
<img src="https://velog.velcdn.com/images/hello-yujin/post/51f2dbc9-1848-4982-9d4b-66d544d77853/image.png" alt=""></p>
<h2 id="✔️-viewport로-모바일-ui를-미리-검증하기">✔️ Viewport로 모바일 UI를 미리 검증하기</h2>
<p>앱잼 프로젝트 특성상 모바일 UI 대응은 거의 필수다.
Storybook에서는 애드온 하나로 이걸 바로 확인할 수 있다고 한다.</p>
<pre><code class="language-ts">// .storybook/preview.ts
import type { Preview } from &#39;@storybook/react&#39;;

const preview: Preview = {
  parameters: {
    viewport: {
      defaultViewport: &#39;mobile1&#39;,
    },
  },
};

export default preview;</code></pre>
<p>이렇게 설정해두면,</p>
<ul>
<li>모바일에서 버튼이 너무 커지지는 않는지</li>
<li>텍스트가 두 줄로 깨질 때 레이아웃이 무너지지 않는지</li>
<li>터치 영역이 너무 작지 않은지</li>
</ul>
<p>를 <strong>페이지 들어가지 않고도</strong> 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/2f50cc20-34b7-4184-92d0-8b93cc8bbcb7/image.png" alt="">
<img src="https://velog.velcdn.com/images/hello-yujin/post/2deafe0f-d162-437c-83e6-029056fe94a8/image.png" alt=""></p>
<h2 id="✔️-storybook을-붙이고-나서-실제로-체감될-변화">✔️ Storybook을 붙이고 나서, 실제로 체감될 변화</h2>
<h3 id="☑️-컴포넌트를-수정이-더욱-용이할-것이다">☑️ 컴포넌트를 수정이 더욱 용이할 것이다</h3>
<p>예전에는 “이 컴포넌트 고치면 어디 깨질지”가 무서웠다.
Storybook에서는 관련 story를 쭉 돌려보면서 영향 범위를 빠르게 확인할 수 있을 것이다.</p>
<h3 id="☑️-디자이너-커뮤니케이션이-쉬워질-것이다">☑️ 디자이너 커뮤니케이션이 쉬워질 것이다</h3>
<p>“이 카드에서 padding이 조금만 더…” 같은 피드백이 들어오면,
페이지 링크보다 Story 링크를 공유하는 게 훨씬 빨랐다.</p>
<ul>
<li>페이지: 다른 UI 요소 때문에 뭐가 문제인지 설명이 길어짐</li>
<li>Story: “이 컴포넌트 여기 부분만” 명확하게 잡힘</li>
</ul>
<h3 id="☑️-상태가-정리된-ui가-만들어질-것이다">☑️ ‘상태’가 정리된 UI가 만들어질 것이다</h3>
<p>로딩/빈상태/에러상태를 story로 만들어두니까
나중에 페이지 개발할 때 “이 상태 처리부터 해야지”라는 습관이 생길 것 같다.</p>
<hr>
<h2 id="✔️-우리는-storybook을-이렇게-운영하려고-한다-comfit-팀-규칙">✔️ 우리는 Storybook을 “이렇게” 운영하려고 한다 (Comfit 팀 규칙)</h2>
<p>앱잼에서 Storybook을 그냥 설치만 해두면, 한정된 시간 안에 정신 없이 결국 안 쓰게 될 수도 있다 생각한다.
그래서 나는 운영 규칙을 최소한으로 잡는 게 중요하다고 생각한다.</p>
<ul>
<li>공용 컴포넌트는 PR 올릴 때 story도 같이 작성</li>
<li>“상태 케이스”는 최소 3개(기본/disabled/긴텍스트 or 로딩)</li>
<li>컴포넌트 이름과 story title은 폴더 구조랑 일치</li>
<li>디자인 변경이 생기면 먼저 story에서 수정 후 페이지 적용</li>
</ul>
<h2 id="storybook-도입은-선택이-아니라-비용-절감일-것이다">Storybook 도입은 “선택”이 아니라 “비용 절감”일 것이다</h2>
<p>Storybook을 도입하기 전엔
“이거까지 하면 시간이 더 드는 거 아닌가?”라는 생각이 컸다.</p>
<p>하지만 직전 기수 앱잼 팀 프로젝트를 살펴보니 그 반대였다.</p>
<ul>
<li>페이지에서 확인하느라 새는 시간</li>
<li>수정 영향 범위 추적하느라 새는 시간 </li>
<li>QA에서 UI 케이스 다시 찾느라 새는 시간</li>
<li>디자인 커뮤니케이션 비용</li>
</ul>
<p>이런 것들을 합치면 Storybook은 초반 투자로 전체 비용을 줄이는 도구일 것이다.
앱잼처럼 짧고 빠르게 완성해야 하는 프로젝트에서는 특히 더 효과가 크다고 느껴질 것이라 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[앱잼 아티클 - Zustand에 대해]]></title>
            <link>https://velog.io/@hello-yujin/%EC%95%B1%EC%9E%BC-%EC%95%84%ED%8B%B0%ED%81%B4-Zustand%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@hello-yujin/%EC%95%B1%EC%9E%BC-%EC%95%84%ED%8B%B0%ED%81%B4-Zustand%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Mon, 29 Dec 2025 03:00:03 GMT</pubDate>
            <description><![CDATA[<h2 id="✔️-글을-쓰게-된-이유">✔️ 글을 쓰게 된 이유</h2>
<p>React로 프로젝트를 진행하다 보면 상태 관리가 점점 부담이 되는 순간이 온다. 처음에는 props로 충분하다고 생각했지만, 컴포넌트가 늘어나면서 props drilling이 깊어지고, Context API는 생각보다 구조가 복잡해졌다. Redux는 안정적이지만 작은 프로젝트에 적용하기에는 설정 비용이 꽤 크다는 느낌을 받았다.</p>
<p>그래서 이번 프로젝트에서는 <strong>가볍고 직관적인 전역 상태 관리 도구</strong>를 사용해보고 싶다는 생각을 하게 되었고 앱잼 팀원들과의 회의를 거쳐 Zustand를 최종적으로 선택하게 되었다. 이 글은 앱잼 프로젝트에 적용하기 전에 Zustand를 기술적으로 정리하고, 직접 실습해보기 위함이다.</p>
<hr>
<h2 id="✔️-zustand란-무엇인가">✔️ Zustand란 무엇인가</h2>
<p>Zustand는 Flux 패턴을 기반으로 한 경량 상태 관리 라이브러리로, React Hook 형태로 전역 상태를 관리할 수 있도록 도와준다. 가장 큰 특징은 store가 컴포넌트 트리 안에 존재하는 것이 아니라 <strong>외부에 독립적으로 존재</strong>한다는 점이다. 컴포넌트는 필요한 상태만 선택적으로 구독하고, 상태가 변경되었을 때 실제로 사용하는 값이 바뀐 컴포넌트만 리렌더링된다.</p>
<p>Context API처럼 Provider로 감쌀 필요도 없고, Redux처럼 액션과 리듀서를 분리할 필요도 없다. 상태를 정의하고 바로 사용하는 흐름이 굉장히 단순하다.</p>
<h4 id="개념-요약">개념 요약</h4>
<ul>
<li>store는 <strong>외부에 존재</strong></li>
<li>컴포넌트는 store를 <strong>구독(subscribe)</strong> 한다</li>
<li>selector를 통해 <strong>부분 구독</strong> 가능</li>
<li>상태 변경 시 <strong>구독 중인 컴포넌트만 리렌더링</strong><pre><code>[ Store ] → (subscribe) → [ Component ]</code></pre>👉 React Context와 달리 <strong>상태 변경 ≠ 전체 리렌더링</strong>
<img src="https://velog.velcdn.com/images/hello-yujin/post/19e83d02-b8ce-48e8-b19f-693787c96a81/image.jpg" alt=""></li>
</ul>
<hr>
<h2 id="✔️-store-생성-실습">✔️ Store 생성 실습</h2>
<p>구조를 이해하기 위해 가장 기본적인 <strong>counter store</strong>를 예제로 실습을 진행해보겠다.</p>
<p>Zustand에서 store는 <code>create</code> 함수로 생성한다. 이 함수 안에서는 <strong>상태(state)</strong>와 <strong>상태를 변경하는 함수(action)</strong>를 함께 정의한다. Redux처럼 액션과 리듀서를 분리하지 않고, 한 곳에서 상태 흐름을 모두 확인할 수 있다는 점이 특징이다.</p>
<p>먼저 counter 상태를 관리하는 store를 하나 만든다.</p>
<pre><code class="language-ts">// stores/useCounterStore.ts
import { create } from &#39;zustand&#39;;

interface CounterState {
  count: number;
  increase: () =&gt; void;
  decrease: () =&gt; void;
}

const useCounterStore = create&lt;CounterState&gt;((set) =&gt; ({
  count: 0,
  increase: () =&gt;
    set((state) =&gt; ({
      count: state.count + 1,
    })),
  decrease: () =&gt;
    set((state) =&gt; ({
      count: state.count - 1,
    })),
}));

export default useCounterStore;</code></pre>
<p>코드를 위에서부터 하나씩 보면 구조가 훨씬 명확해진다.
<code>CounterState</code> 인터페이스에서는 이 store가 어떤 상태와 함수를 가지는지 정의한다. 여기서는 숫자 형태의 <code>count</code> 상태와, 값을 증가·감소시키는 두 개의 함수가 전부다.</p>
<p><code>create</code> 함수의 인자로 전달되는 콜백 함수 안에서는 <code>set</code>을 사용할 수 있는데, 이 <code>set</code> 함수가 바로 상태를 변경하는 역할을 한다. <code>set</code> 안에서는 이전 상태를 받아 새로운 상태를 반환하는 방식으로 값을 업데이트한다. 이 방식은 React의 <code>setState</code>와 굉장히 유사해서 이해하기 어렵지 않다.</p>
<p>이렇게 만든 store의 가장 큰 특징은 컴포넌트 트리와 완전히 분리되어 있다는 점이다. Context API처럼 Provider로 감싸줄 필요가 없고, 단순히 훅을 import해서 호출하기만 하면 된다.</p>
<p>이제 이 store를 실제 컴포넌트에서 사용해보자.</p>
<pre><code class="language-tsx">import useCounterStore from &#39;@/stores/useCounterStore&#39;;

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

  return (
    &lt;div&gt;
      &lt;p&gt;{count}&lt;/p&gt;
      &lt;button onClick={increase}&gt;+&lt;/button&gt;
      &lt;button onClick={decrease}&gt;-&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>여기서 중요한 점은 store 전체를 한 번에 가져오지 않고, selector를 통해 필요한 값만 가져오고 있다는 것이다. count 값은 숫자가 바뀔 때만 리렌더링을 유발하고, 증가·감소 함수는 상태 변화와 무관하게 안정적으로 사용할 수 있다.</p>
<p>이 구조 덕분에 Zustand는 별도의 설정 없이도 자연스럽게 렌더링 최적화를 할 수 있다.</p>
<h2 id="✔️-zustand-내부-동작-구조">✔️ Zustand 내부 동작 구조</h2>
<p>Zustand의 <code>store</code>는 <code>create</code> 함수로 생성되며, 이 함수 안에서 상태와 상태를 변경하는 함수들을 함께 정의한다. 내부적으로는 <code>set</code>과 <code>get</code>을 통해 상태를 관리하며, 이 store는 하나의 싱글톤처럼 동작한다.</p>
<p>컴포넌트는 store 전체를 구독하는 것이 아니라 selector를 통해 필요한 상태만 가져오게 된다. 이 덕분에 상태가 변경되더라도 관련 없는 컴포넌트는 영향을 받지 않는다. 이런 구조 덕분에 Zustand는 별도의 복잡한 최적화 없이도 자연스럽게 렌더링 성능을 관리할 수 있다.</p>
<pre><code class="language-ts">create((set, get) =&gt; ({
  state,
  actions,
}))</code></pre>
<ul>
<li><code>set</code> : 상태 변경 함수</li>
<li><code>get</code> : 현재 상태 접근</li>
<li>store는 <code>singleton</code></li>
<li>React 외부에서도 접근 가능</li>
</ul>
<h4 id="상태-흐름">상태 흐름</h4>
<ol>
<li>컴포넌트가 selector로 상태를 구독</li>
<li><code>set()</code> 호출 → 상태 변경</li>
<li>변경된 상태를 사용하는 컴포넌트만 리렌더링</li>
</ol>
<hr>
<h2 id="✔️-selector-기반-구독의-중요성">✔️ Selector 기반 구독의 중요성</h2>
<p>Zustand를 사용할 때 가장 중요하다고 느낀 부분은 selector를 통한 상태 구독이다. store 전체를 한 번에 가져오면 상태가 조금만 바뀌어도 컴포넌트가 다시 렌더링된다. 반대로 selector를 사용하면 실제로 필요한 값이 변경될 때만 렌더링이 발생한다.</p>
<p>프로젝트 규모가 커질수록 이 차이는 꽤 크게 느껴질 수 있다. Zustand의 성능을 제대로 활용하려면 반드시 selector 기반 구독을 사용하는 습관을 들이는 게 좋다고 느꼈다.</p>
<h4 id="❌-전체-상태-구독-비추천">❌ 전체 상태 구독 (비추천)</h4>
<pre><code class="language-ts">const store = useCounterStore();</code></pre>
<ul>
<li>store의 <strong>어떤 값이 바뀌어도 리렌더링</strong></li>
</ul>
<h4 id="✅-부분-구독-권장">✅ 부분 구독 (권장)</h4>
<pre><code class="language-ts">const count = useCounterStore((state) =&gt; state.count);</code></pre>
<ul>
<li>count가 바뀔 때만 리렌더링</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/74ff90eb-9e60-40de-a81e-5456683fa7fb/image.png" alt=""></p>
<h2 id="✔️-zustand에서-비동기-처리하기--store로-api-로직-옮겨보기">✔️ Zustand에서 비동기 처리하기 — store로 API 로직 옮겨보기</h2>
<p>Zustand에서 비동기 처리가 자연스럽다고 느껴지는 이유는 Redux처럼 thunk나 saga 같은 별도의 middleware를 붙이지 않아도, store 안에서 그냥 <code>async</code> 함수를 정의하고 그 안에서 <code>set</code>으로 상태를 업데이트하면 끝난다. </p>
<p>이번 실습에서는 예시로 <strong>유저 목록을 가져오는 API 호출</strong>을 생각하고, 그 로직을 컴포넌트가 아니라 store 안으로 옮겨보겠다. 핵심은 <code>loading</code>, <code>error</code>, <code>data</code> 같은 상태를 store에서 같이 들고 가면서, 컴포넌트는 “불러오기 버튼”과 “화면 렌더링”만 담당하게 만드는 것이다.</p>
<p>먼저 <code>store</code>를 만든다. </p>
<pre><code class="language-ts">// stores/useUserStore.ts
import { create } from &#39;zustand&#39;;

type User = {
  id: number;
  name: string;
  email: string;
};

interface UserState {
  users: User[];
  isLoading: boolean;
  error: string | null;

  fetchUsers: () =&gt; Promise&lt;void&gt;;
  clearError: () =&gt; void;
}

const useUserStore = create&lt;UserState&gt;((set, get) =&gt; ({
  users: [],
  isLoading: false,
  error: null,

  clearError: () =&gt; set({ error: null }),

  fetchUsers: async () =&gt; {
    // 1) 요청 시작: 로딩 true, 에러 초기화
    set({ isLoading: true, error: null });

    try {
      // 2) API 호출 (예시)
      const res = await fetch(&#39;https://jsonplaceholder.typicode.com/users&#39;);

      // 3) 실패 케이스 처리
      if (!res.ok) {
        throw new Error(`요청 실패: ${res.status}`);
      }

      const data = await res.json();

      // 4) 성공 시: 데이터 저장 + 로딩 false
      set({
        users: data.map((u: any) =&gt; ({
          id: u.id,
          name: u.name,
          email: u.email,
        })),
        isLoading: false,
      });
    } catch (e: any) {
      // 5) 에러 시: 에러 메시지 저장 + 로딩 false
      set({
        error: e?.message ?? &#39;알 수 없는 에러가 발생했어요.&#39;,
        isLoading: false,
      });
    }
  },
}));

export default useUserStore;</code></pre>
<p>여기서 포인트는, <code>fetchUsers</code>라는 비동기 함수가 store 내부에 있다는 점이다. 이 함수는 요청 시작 시점에 로딩 상태를 켜고, 응답을 받으면 users를 업데이트하고, 실패하면 error를 업데이트한다. 즉 <strong>비동기 로직에 필요한 상태 흐름을 한 곳에서 통제</strong>하게 된다.</p>
<p>이제 컴포넌트에서는 훨씬 단순하게 사용할 수 있다. 컴포넌트는 “언제 fetch를 호출할지”와 “화면에 어떻게 보여줄지”만 신경 쓰면 된다.</p>
<pre><code class="language-tsx">import { useEffect } from &#39;react&#39;;
import useUserStore from &#39;@/stores/useUserStore&#39;;

export default function UserList() {
  const users = useUserStore((state) =&gt; state.users);
  const isLoading = useUserStore((state) =&gt; state.isLoading);
  const error = useUserStore((state) =&gt; state.error);
  const fetchUsers = useUserStore((state) =&gt; state.fetchUsers);
  const clearError = useUserStore((state) =&gt; state.clearError);

  // 페이지 들어오자마자 불러오고 싶으면 useEffect로 호출
  useEffect(() =&gt; {
    fetchUsers();
  }, [fetchUsers]);

  if (isLoading) return &lt;p&gt;불러오는 중...&lt;/p&gt;;

  if (error) {
    return (
      &lt;div&gt;
        &lt;p&gt;에러: {error}&lt;/p&gt;
        &lt;button
          onClick={() =&gt; {
            clearError();
            fetchUsers();
          }}
        &gt;
          다시 시도
        &lt;/button&gt;
      &lt;/div&gt;
    );
  }

  return (
    &lt;div&gt;
      &lt;button onClick={fetchUsers}&gt;유저 목록 새로 불러오기&lt;/button&gt;

      &lt;ul&gt;
        {users.map((u) =&gt; (
          &lt;li key={u.id}&gt;
            {u.name} ({u.email})
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>이렇게 되면 컴포넌트 안에서 <code>try/catch</code>, <code>setLoading</code>, <code>setError</code>, <code>setUsers</code> 같은 코드가 사라지고, UI는 UI답게 정리된다. 그리고 API 관련 상태 변화는 <code>store</code>에서 일관된 방식으로만 관리되기 때문에, 기능이 커져도 구조가 무너지지 않는다.</p>
<h2 id="✔️-persist-미들웨어-활용">✔️ persist 미들웨어 활용</h2>
<p>로그인 토큰이나 설정 값처럼 새로고침 이후에도 유지되어야 하는 상태는 persist 미들웨어를 사용하면 된다. localStorage나 sessionStorage를 선택할 수 있고, 설정도 단순하다.</p>
<p>실제로 로그인 상태 등을 관리할 때 굉장히 유용할 것 같았고, 프로젝트에서 바로 활용할 수 있겠다는 생각이 들었다.</p>
<pre><code class="language-ts">import { persist } from &#39;zustand/middleware&#39;;

const useAuthStore = create&lt;AuthState&gt;()(
  persist(
    (set) =&gt; ({
      accessToken: null,
      setToken: (token) =&gt; set({ accessToken: token }),
      logout: () =&gt; set({ accessToken: null }),
    }),
    {
      name: &#39;auth-storage&#39;,
      storage: sessionStorage,
    }
  )
);</code></pre>
<ul>
<li>새로고침 후에도 상태 유지</li>
<li>로그인/예약/설정 상태에 적합</li>
</ul>
<h2 id="✔️-zustand를-쓰는-기준">✔️ Zustand를 쓰는 기준</h2>
<p>Zustand는 전역 UI 상태나 사용자 흐름과 관련된 상태를 관리하는 데 적합하다. 모달 열림 여부, 로그인 정보, 예약 상태처럼 여러 컴포넌트에서 공통으로 사용되는 값들이 여기에 해당한다.</p>
<p>반면 서버에서 받아오는 데이터의 캐싱이나 refetch가 중요한 경우에는 React Query 같은 라이브러리를 함께 사용하는 것이 더 적절하다고 느꼈다.</p>
<h4 id="zustand에-적합한-상태">Zustand에 적합한 상태</h4>
<ul>
<li>로그인 정보</li>
<li>예약 정보</li>
<li>모달, 토스트, UI 상태</li>
<li>전역 필터 조건</li>
</ul>
<h4 id="zustand에-부적합한-상태">Zustand에 부적합한 상태</h4>
<ul>
<li>서버 캐시</li>
<li>pagination / refetch 중심 데이터</li>
</ul>
<p>👉 <strong>Zustand + React Query 조합 추천</strong></p>
<h2 id="✔️-store-분리-전략">✔️ Store 분리 전략</h2>
<p>Zustand를 사용할 때 store를 어떻게 나누느냐도 중요하다. 하나의 store에 모든 상태를 몰아넣기보다는, 인증, 사용자 정보, UI 상태처럼 도메인 단위로 분리하는 것이 관리하기 훨씬 수월하다.</p>
<p>이렇게 분리하면 상태의 책임이 명확해지고, 유지보수도 쉬워진다.</p>
<pre><code>stores/
 ├─ useAuthStore.ts
 ├─ useModalStore.ts
 ├─ useReservationStore.ts
 └─ useUserStore.ts</code></pre><ul>
<li>도메인 단위 분리</li>
<li>store 하나에 모든 상태 몰아넣지 않기</li>
</ul>
<h2 id="✔️-다른-상태-관리-방식과의-비교">✔️ 다른 상태 관리 방식과의 비교</h2>
<p>Context API는 간단한 상태 공유에는 적합하지만, 상태가 자주 바뀌는 경우 렌더링 최적화가 어렵다. Redux는 강력하지만 설정과 구조가 부담스럽다. Zustand는 이 둘 사이에서 비교적 가볍게 사용할 수 있는 대안이라는 느낌을 받았다.</p>
<p>프로젝트 성격과 규모에 따라 선택은 달라질 수 있지만, 이번 앱잼 React 프로젝트에서는 Zustand가 가장 적절한 선택이라고 판단했다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>Context</th>
<th>Redux</th>
<th>Zustand</th>
</tr>
</thead>
<tbody><tr>
<td>설정</td>
<td>쉬움</td>
<td>매우 복잡</td>
<td>매우 쉬움</td>
</tr>
<tr>
<td>성능 최적화</td>
<td>어려움</td>
<td>좋음</td>
<td>매우 좋음</td>
</tr>
<tr>
<td>코드량</td>
<td>적음</td>
<td>많음</td>
<td>적음</td>
</tr>
<tr>
<td>비동기</td>
<td>직접 처리</td>
<td>middleware 필요</td>
<td>기본 지원</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/98f3b02a-a205-49a3-b736-b6fe3c780f3c/image.png" alt=""></p>
<h2 id="✔️-마무리">✔️ 마무리</h2>
<p>Zustand는 전역 상태 관리를 복잡하게 만들지 않으면서도, 필요한 성능과 구조를 충분히 제공해주는 라이브러리라고 느꼈다. 설정 부담이 적고, 코드 흐름이 직관적이라 React 프로젝트에 빠르게 적용할 수 있다는 점이 가장 큰 장점이다.</p>
<p>이번 프로젝트에서는 UI 상태와 사용자 흐름 관리에 Zustand를 적극적으로 활용해볼 계획이다. 이후 실제로 사용하면서 느낀 장단점이나 패턴들도 따로 정리해보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 최적화: React.lazy, debounce, useCallback로 성능 끌어올리기]]></title>
            <link>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%B5%9C%EC%A0%81%ED%99%94-React.lazy-debounce-useCallback%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%B5%9C%EC%A0%81%ED%99%94-React.lazy-debounce-useCallback%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Sun, 21 Dec 2025 01:40:38 GMT</pubDate>
            <description><![CDATA[<p>리액트로 서비스를 만들다 보면 어느 순간부터 초기 로딩이 느려지고, 입력할 때 버벅이며, 컴포넌트가 필요 이상으로 리렌더링되는 문제를 겪게 된다.</p>
<p>이 글에서는 리액트 최적화 기법 3가지를 중심으로 “왜 필요한지 → 언제 쓰는지 → 어떻게 적용하는지”를 코드와 함께 정리해보겠다.</p>
<ul>
<li><strong>React.lazy</strong>: 초기 번들 크기를 줄이는 코드 스플리팅</li>
<li><strong>debounce</strong>: 입력/이벤트 폭주를 막는 제어 기법</li>
<li><strong>useCallback</strong>: 불필요한 리렌더링을 줄이는 함수 메모이제이션</li>
</ul>
<hr>
<h2 id="✔️-reactlazy-초기-로딩을-가볍게-만드는-코드-스플리팅">✔️ React.lazy: 초기 로딩을 가볍게 만드는 코드 스플리팅</h2>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/966216c6-02f1-4432-bdc3-6b66a56aeff4/image.png" alt=""></p>
<h3 id="왜-reactlazy가-필요할까">왜 React.lazy가 필요할까?</h3>
<p>리액트 앱은 기본적으로 <strong>한 번에 모든 JS 번들을 내려받아 실행</strong>한다.
페이지 수가 많아질수록, 또는 차트·에디터·지도 같은 무거운 컴포넌트가 많아질수록 <strong>첫 화면을 보기까지의 시간(LCP)</strong> 이 급격히 늘어난다.</p>
<p>하지만 모든 컴포넌트가 <strong>첫 화면에 꼭 필요하지는 않다.</strong> 예를 들어</p>
<ul>
<li>관리자 페이지</li>
<li>결제 페이지</li>
<li>무거운 모달</li>
<li>통계/차트 페이지</li>
</ul>
<p>이런 컴포넌트는 <strong>“필요해질 때 로드”</strong> 하는 것이 훨씬 효율적이다.</p>
<hr>
<h3 id="reactlazy-기본-사용법">React.lazy 기본 사용법</h3>
<pre><code class="language-jsx">import React, { Suspense } from &quot;react&quot;;

const HeavyChart = React.lazy(() =&gt; import(&quot;./HeavyChart&quot;));

export default function Dashboard() {
  return (
    &lt;Suspense fallback={&lt;div&gt;차트 로딩 중...&lt;/div&gt;}&gt;
      &lt;HeavyChart /&gt;
    &lt;/Suspense&gt;
  );
}</code></pre>
<ul>
<li><code>React.lazy</code> → 동적 import</li>
<li><code>Suspense</code> → 로딩 중 보여줄 UI 처리</li>
</ul>
<p>👉 초기 번들에는 <code>HeavyChart</code>가 포함되지 않고, 실제 렌더링 시점에 로드된다.</p>
<hr>
<h3 id="라우트-단위로-분리하면-효과가-가장-크다">라우트 단위로 분리하면 효과가 가장 크다</h3>
<pre><code class="language-jsx">import React, { Suspense } from &quot;react&quot;;
import { Routes, Route } from &quot;react-router-dom&quot;;

const Home = React.lazy(() =&gt; import(&quot;./pages/Home&quot;));
const Admin = React.lazy(() =&gt; import(&quot;./pages/Admin&quot;));
const Payment = React.lazy(() =&gt; import(&quot;./pages/Payment&quot;));

export default function App() {
  return (
    &lt;Suspense fallback={&lt;div&gt;페이지 로딩 중...&lt;/div&gt;}&gt;
      &lt;Routes&gt;
        &lt;Route path=&quot;/&quot; element={&lt;Home /&gt;} /&gt;
        &lt;Route path=&quot;/admin&quot; element={&lt;Admin /&gt;} /&gt;
        &lt;Route path=&quot;/payment&quot; element={&lt;Payment /&gt;} /&gt;
      &lt;/Routes&gt;
    &lt;/Suspense&gt;
  );
}</code></pre>
<h4 id="체감-할-수-있는-것">체감 할 수 있는 것</h4>
<ul>
<li>첫 화면 로딩 속도 개선</li>
<li>Network 탭에서 JS 파일이 여러 chunk로 나뉘는 것을 확인 가능</li>
</ul>
<hr>
<h3 id="모달도-lazy로-분리하면-ux가-좋아진다">모달도 lazy로 분리하면 UX가 좋아진다</h3>
<pre><code class="language-jsx">import React, { useState, Suspense } from &quot;react&quot;;

const ImageEditorModal = React.lazy(() =&gt; import(&quot;./ImageEditorModal&quot;));

export default function Page() {
  const [open, setOpen] = useState(false);

  return (
    &lt;&gt;
      &lt;button onClick={() =&gt; setOpen(true)}&gt;모달 열기&lt;/button&gt;

      {open &amp;&amp; (
        &lt;Suspense fallback={&lt;div&gt;모달 준비 중...&lt;/div&gt;}&gt;
          &lt;ImageEditorModal onClose={() =&gt; setOpen(false)} /&gt;
        &lt;/Suspense&gt;
      )}
    &lt;/&gt;
  );
}</code></pre>
<p>👉 <strong>모달을 열 때만 무거운 라이브러리를 로드</strong>하기 때문에 초기 진입 속도와 전체 UX 모두 개선된다.</p>
<hr>
<h2 id="✔️-debounce-입력-이벤트-폭주를-막는-방법">✔️ debounce: 입력 이벤트 폭주를 막는 방법</h2>
<h3 id="debounce가-필요한-이유">debounce가 필요한 이유</h3>
<p>검색창에서 키를 누를 때마다 API를 호출하면:</p>
<ul>
<li>타이핑 10번 → API 10번</li>
<li>네트워크 낭비</li>
<li>응답 순서 꼬임 가능성</li>
<li>UX 저하</li>
</ul>
<p>와 같은 현상이 일어날 수 있다.</p>
<p>👉 debounce는 <strong>“마지막 입력 후 일정 시간 동안 추가 입력이 없을 때만 실행”</strong> 되도록 만든다.</p>
<hr>
<h3 id="debounce-구현-예시">debounce 구현 예시</h3>
<pre><code class="language-jsx">import { useEffect, useState } from &quot;react&quot;;

export default function SearchBox() {
  const [keyword, setKeyword] = useState(&quot;&quot;);
  const [debouncedKeyword, setDebouncedKeyword] = useState(&quot;&quot;);

  useEffect(() =&gt; {
    const timer = setTimeout(() =&gt; {
      setDebouncedKeyword(keyword);
    }, 400);

    return () =&gt; clearTimeout(timer);
  }, [keyword]);

  useEffect(() =&gt; {
    if (!debouncedKeyword) return;
    console.log(&quot;API 호출:&quot;, debouncedKeyword);
  }, [debouncedKeyword]);

  return (
    &lt;input
      value={keyword}
      onChange={(e) =&gt; setKeyword(e.target.value)}
      placeholder=&quot;검색어 입력&quot;
    /&gt;
  );
}</code></pre>
<ul>
<li><code>keyword</code> → 즉시 반영</li>
<li><code>debouncedKeyword</code> → 입력 멈춘 뒤 확정</li>
<li>API 호출은 <strong>debounced 값 기준</strong></li>
</ul>
<hr>
<h3 id="커스텀-훅으로-재사용하기">커스텀 훅으로 재사용하기</h3>
<pre><code class="language-jsx">import { useEffect, useState } from &quot;react&quot;;

export function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() =&gt; {
    const timer = setTimeout(() =&gt; setDebounced(value), delay);
    return () =&gt; clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}</code></pre>
<pre><code class="language-jsx">const debouncedKeyword = useDebouncedValue(keyword, 400);</code></pre>
<p>👉 검색, 필터, 자동완성 등 거의 모든 입력 UX에 활용 가능하다.</p>
<hr>
<h2 id="✔️-usecallback-불필요한-리렌더링을-막는-함수-메모이제이션">✔️ useCallback: 불필요한 리렌더링을 막는 함수 메모이제이션</h2>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/4dbd2532-d2e0-4ac4-800d-d7805f7865ea/image.gif" alt=""></p>
<h3 id="문제-상황-함수는-매-렌더마다-새로-만들어진다">문제 상황: 함수는 매 렌더마다 새로 만들어진다</h3>
<pre><code class="language-jsx">const handleClick = () =&gt; {
  setCount(count + 1);
};</code></pre>
<p>이 함수는 <strong>컴포넌트가 렌더링될 때마다 새로 생성</strong>된다.
이걸 <code>props</code>로 내려주면, 자식 컴포넌트는 <strong>“props가 바뀌었다”고 인식하고 다시 렌더링</strong>된다.</p>
<hr>
<h3 id="usecallback-없이-발생하는-리렌더">useCallback 없이 발생하는 리렌더</h3>
<pre><code class="language-jsx">const Child = React.memo(({ onClick }) =&gt; {
  console.log(&quot;Child 렌더링&quot;);
  return &lt;button onClick={onClick}&gt;+&lt;/button&gt;;
});</code></pre>
<p>부모의 다른 state가 바뀌어도</p>
<ul>
<li>함수 참조 변경</li>
<li>Child 리렌더 발생</li>
</ul>
<p>이 일어난다.</p>
<hr>
<h3 id="usecallback으로-해결하기">useCallback으로 해결하기</h3>
<pre><code class="language-jsx">import { useCallback, useState } from &quot;react&quot;;

const Child = React.memo(({ onClick }) =&gt; {
  return &lt;button onClick={onClick}&gt;+&lt;/button&gt;;
});

export default function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState(&quot;&quot;);

  const handleClick = useCallback(() =&gt; {
    setCount((prev) =&gt; prev + 1);
  }, []);

  return (
    &lt;&gt;
      &lt;input value={text} onChange={(e) =&gt; setText(e.target.value)} /&gt;
      &lt;Child onClick={handleClick} /&gt;
    &lt;/&gt;
  );
}</code></pre>
<ul>
<li>함수 참조 유지</li>
<li><code>React.memo</code>와 함께 사용할 때 효과 극대화</li>
<li><code>prev</code> 패턴으로 의존성 배열 단순화</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[띵스룸 예약 화면 성능 최적화 기록 (Lighthouse / Performance 탭으로 TBT 원인 잡기)]]></title>
            <link>https://velog.io/@hello-yujin/%EB%9D%B5%EC%8A%A4%EB%A3%B8-%EC%98%88%EC%95%BD-%ED%99%94%EB%A9%B4-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B8%B0%EB%A1%9D-Lighthouse-Performance-%ED%83%AD%EC%9C%BC%EB%A1%9C-TBT-%EC%9B%90%EC%9D%B8-%EC%9E%A1%EA%B8%B0</link>
            <guid>https://velog.io/@hello-yujin/%EB%9D%B5%EC%8A%A4%EB%A3%B8-%EC%98%88%EC%95%BD-%ED%99%94%EB%A9%B4-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B8%B0%EB%A1%9D-Lighthouse-Performance-%ED%83%AD%EC%9C%BC%EB%A1%9C-TBT-%EC%9B%90%EC%9D%B8-%EC%9E%A1%EA%B8%B0</guid>
            <pubDate>Thu, 18 Dec 2025 05:13:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>목표: TBT(총 차단 시간) 줄이고, LCP 안정화해서 Performance 점수 개선하기</p>
</blockquote>
<h2 id="0-측정-전-환경-고정부터-하기-점수-출렁임-방지">0. 측정 전 “환경 고정”부터 하기 (점수 출렁임 방지)</h2>
<p>Lighthouse 결과에 아래 문구가 계속 떴다.</p>
<ul>
<li>Chrome extensions negatively affected this page&#39;s load performance…</li>
</ul>
<p>즉, <strong>확장 프로그램 영향으로 점수가 흔들릴 수 있음.</strong>
따라서 아래처럼 고정하고 측정했다.</p>
<ul>
<li>시크릿 모드(확장 OFF) 또는 확장 없는 프로필</li>
<li>DevTools Performance는 <strong>Record → 새로고침 → 아무것도 안 누르고 첫 화면 안정화 → Stop</strong></li>
<li>Next.js 환경이기 때문에 <code>next build &amp;&amp; next start</code>(프로덕션)에서 다시 한 번 측정</li>
</ul>
<hr>
<h2 id="1-문제-상황-performance-점수는-낮은데-무엇이-막고-있는지-모르겠음">1. 문제 상황: Performance 점수는 낮은데, 무엇이 막고 있는지 모르겠음</h2>
<p>처음에는 이런 상태였다.</p>
<ul>
<li>Performance 점수 낮음 (점수가 무려 40점대인 쓰레기 성능)</li>
<li>Metrics에서 특히 <strong>TBT가 매우 큼</strong></li>
<li>LCP도 비정상적으로 늦게 잡히는 케이스가 존재</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/4a4d3ed4-9aab-412b-995e-aef30e053df6/image.png" alt="">
<img src="https://velog.velcdn.com/images/hello-yujin/post/e6739e98-3b6d-42a0-b8fc-412aa095d9c4/image.png" alt=""></p>
<p>-- </p>
<h2 id="2-devtools-performance로-bottom-up에서-범인-찾기">2. DevTools Performance로 “Bottom-up”에서 범인 찾기</h2>
<p>Lighthouse는 “결과”만 보여주고, <strong>원인 함수/컴포넌트</strong>를 바로 집어주진 않는다.
그래서 DevTools <code>Performance</code> 탭에서:</p>
<ol>
<li>성능 측정</li>
<li>아래 탭 중 Bottom-up 탭 확인</li>
<li><code>Self time</code> / <code>Total time</code> 큰 항목이 실제 범인임을 확인</li>
</ol>
<p><strong>특정 함수/컴포넌트가 렌더링 중 반복 호출되며 메인 스레드를 오래 잡는 패턴</strong>이 보였다.
(예: <code>nowInKST()</code>처럼     toLocaleString(timeZone)     기반 계산이 타임슬롯 개수만큼 반복)
<img src="https://velog.velcdn.com/images/hello-yujin/post/91b2ebe8-2dc9-4478-9590-aa104f525279/image.png" alt=""></p>
<hr>
<h2 id="3-1차-개선-배너-마루-이미지-nextimage-전환--priority-적용">3. 1차 개선: 배너 마루 이미지 Next/Image 전환 + priority 적용</h2>
<h3 id="문제-상황-lcp-병목">문제 상황 (LCP 병목)</h3>
<p>Lighthouse에서 확인한 <strong>LCP 요소는 메인 배너 이미지</strong>였다.
해당 이미지는 다음 문제를 가지고 있었다.</p>
<ul>
<li>일반 <code>&lt;img&gt;</code> 태그 사용</li>
<li>기본 lazy loading</li>
<li>포맷 최적화(webp/avif) 미적용</li>
</ul>
<p>👉 그 결과, <strong>LCP가 10초 이상으로 측정</strong>되었다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/79665670-d8b3-4a4c-a8a3-e8708a9de3b5/image.png" alt=""></p>
<hr>
<h3 id="해결-방법">해결 방법</h3>
<p><strong>1. Next/Image로 전환</strong></p>
<ul>
<li>자동 리사이징</li>
<li>포맷 자동 변환(webp/avif)</li>
<li>네트워크 최적화</li>
</ul>
<p><strong>2. LCP 대상 이미지에만 <code>priority</code> 적용</strong></p>
<ul>
<li>페이지 로드 시 즉시 네트워크 요청</li>
<li>모든 이미지에 적용하지 않고</li>
<li><strong>상단 배너(LCP 후보)에만 제한적으로 적용</strong></li>
</ul>
<pre><code class="language-jsx">&lt;Image
  fill
  priority
  className=&quot;object-cover&quot;
  src={bannerImageUrl}
  alt=&quot;메인 배너 이미지&quot;
/&gt;</code></pre>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/9376dd35-cb26-4559-b811-7b37669acbd4/image.png" alt="">
<img src="https://velog.velcdn.com/images/hello-yujin/post/2eea7392-dfc2-4b09-9bde-9aff0ea96918/image.png" alt=""></p>
<ul>
<li><strong>LCP는 1.1s</strong>로, Core Web Vitals 기준을 충분히 만족하는 상태</li>
<li>초기 화면 체감 속도 대폭 개선</li>
<li>Performance 점수 상승</li>
</ul>
<p>👉 LCP는 “가장 큰 요소 하나”만 정확히 잡아도 효과가 크다.</p>
<hr>
<h2 id="2차-개선-초기-렌더-성능-개선을-위한-dynamic-import-적용">2차 개선: 초기 렌더 성능 개선을 위한 dynamic import 적용</h2>
<h3 id="문제-상황-초기-js-실행-비용-과다">문제 상황 (초기 JS 실행 비용 과다)</h3>
<p>초기 진입 시:</p>
<ul>
<li>당장 보이지 않는 페이지</li>
<li>사용자가 아직 접근하지 않는 컴포넌트까지</li>
</ul>
<p>모두 번들에 포함되어 있었다.</p>
<p>그 결과:</p>
<ul>
<li>JS 번들 크기 증가</li>
<li>메인 스레드 점유 증가</li>
<li><strong>TBT 상승</strong></li>
</ul>
<hr>
<h3 id="해결-방법-1">해결 방법</h3>
<p>Next.js권장방식대로 <code>React.lazy</code> 대신 <code>next/dynamic</code>을 활용해
<strong>초기 화면에 필요한 컴포넌트만 로드하도록 분리</strong>했다.</p>
<pre><code class="language-jsx">import dynamic from &#39;next/dynamic&#39;;

const ReservationSection = dynamic(
  () =&gt; import(&#39;@components/common/ReservationSection&#39;),
  {
    ssr: false,
    loading: () =&gt; (
      &lt;div className=&quot;w-full max-w-[95%] min-h-[240px] rounded-2xl bg-white shadow-sm border border-gray-50&quot; /&gt;
    ),
  },
);
</code></pre>
<hr>
<h2 id="3차-개선-reservationsection-탭-구조--렌더링-최적화">3차 개선: ReservationSection 탭 구조 &amp; 렌더링 최적화</h2>
<h3 id="문제-상황-숨겨진-렌더링">문제 상황 (숨겨진 렌더링)</h3>
<p>기존 ReservationSection 구조에서는:</p>
<ul>
<li>‘오늘 예약’, ‘내일 예약’ 컴포넌트가 <strong>모두 마운트</strong></li>
<li><code>display: none</code>으로만 제어</li>
</ul>
<p>👉 즉, <strong>안 보여도 렌더/계산/이펙트는 실행</strong>되고 있었다.</p>
<hr>
<h3 id="해결-방법-보이는-탭만-렌더링">해결 방법: “보이는 탭만 렌더링”</h3>
<p>최적화가 필요한 경우에는 <strong>보이는 탭만 렌더링(조건부 렌더링)</strong> 구조로 바꾸는 것이 효과적이다.</p>
<pre><code class="language-jsx">{currentTab === 0
  ? &lt;TodayReservationList /&gt;
  : &lt;TomorrowReservationList /&gt;}</code></pre>
<h3 id="결과-1">결과</h3>
<ul>
<li><strong>LCP, FCP, Speed Index, CLS → 모두 안정화 완료</strong></li>
<li>Performance 점수의 병목은 이제 <strong>TBT 하나로 명확히 좁혀진 상태</strong></li>
<li><strong>LCP 16.7s → 1.5s, TBT 14.8s → 3.3s</strong>로 개선하며 성능 병목을 렌더링 단계에서 JS 실행 단계로 축소</li>
<li>다음 단계의 최적화 목표는:<ul>
<li>초기 JS 실행량 추가 감소</li>
<li>불필요한 마운트/계산 제거</li>
<li>더 세분화된 코드 스플리팅</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/c333705a-838b-4079-aa23-a653bcabcb92/image.png" alt="">
<img src="https://velog.velcdn.com/images/hello-yujin/post/7636e486-7ef9-43f3-a5ad-f8015a0512fc/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 컴포넌트 패턴]]></title>
            <link>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%8C%A8%ED%84%B4-q2ojukf6</link>
            <guid>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%8C%A8%ED%84%B4-q2ojukf6</guid>
            <pubDate>Sun, 14 Dec 2025 01:54:09 GMT</pubDate>
            <description><![CDATA[<p>리액트에서 복잡한 UI를 만들다 보면 재사용성, 확장성, 그리고 명확한 책임 분리가 매우 중요해진다.
실제 자주 사용되는 세 가지 패턴을 정리해보았다.</p>
<p><strong>- Compound Component Pattern</strong>
<strong>- Higher-Order Component(HOC)</strong>
<strong>- React Portal</strong></p>
<hr>
<h2 id="✔️-compound-component-pattern">✔️ Compound Component Pattern</h2>
<p>컴파운드 패턴은 <strong>여러 개의 컴포넌트가 마치 하나의 컴포넌트처럼 함께 동작</strong>하도록 만드는 패턴이다.
React의 <strong>Context API</strong>와 주로 함께 사용된다.</p>
<h3 id="언제-쓰는가">언제 쓰는가?</h3>
<ul>
<li>드롭다운, 모달, 탭 UI처럼 “부모 안에 여러 하위 요소가 협력하는 구조”가 필요할 때</li>
<li><code>&lt;Select&gt;</code>, <code>&lt;Select.Option&gt;</code>처럼 개발자가 사용하기 쉽고 직관적인 API를 만들고 싶을 때</li>
</ul>
<hr>
<h3 id="예시-코드--tabs-component">예시 코드 – Tabs Component</h3>
<pre><code class="language-tsx">import { createContext, useContext, useState } from &quot;react&quot;;

const TabsContext = createContext&lt;any&gt;(null);

export function Tabs({ children, defaultValue }) {
  const [active, setActive] = useState(defaultValue);
  return (
    &lt;TabsContext.Provider value={{ active, setActive }}&gt;
      &lt;div&gt;{children}&lt;/div&gt;
    &lt;/TabsContext.Provider&gt;
  );
}

Tabs.List = function TabsList({ children }) {
  return &lt;div&gt;{children}&lt;/div&gt;;
};

Tabs.Trigger = function TabsTrigger({ value, children }) {
  const { active, setActive } = useContext(TabsContext);
  return (
    &lt;button
      onClick={() =&gt; setActive(value)}
      style={{
        fontWeight: active === value ? &quot;bold&quot; : &quot;normal&quot;,
      }}
    &gt;
      {children}
    &lt;/button&gt;
  );
};

Tabs.Content = function TabsContent({ value, children }) {
  const { active } = useContext(TabsContext);
  return active === value ? &lt;div&gt;{children}&lt;/div&gt; : null;
};</code></pre>
<h3 id="사용법">사용법</h3>
<pre><code class="language-tsx">&lt;Tabs defaultValue=&quot;tab1&quot;&gt;
  &lt;Tabs.List&gt;
    &lt;Tabs.Trigger value=&quot;tab1&quot;&gt;탭 1&lt;/Tabs.Trigger&gt;
    &lt;Tabs.Trigger value=&quot;tab2&quot;&gt;탭 2&lt;/Tabs.Trigger&gt;
  &lt;/Tabs.List&gt;

  &lt;Tabs.Content value=&quot;tab1&quot;&gt;내용 1&lt;/Tabs.Content&gt;
  &lt;Tabs.Content value=&quot;tab2&quot;&gt;내용 2&lt;/Tabs.Content&gt;
&lt;/Tabs&gt;</code></pre>
<h3 id="장점">장점</h3>
<ul>
<li>사용성이 직관적</li>
<li>UI 요소들을 마음대로 배치해도 내부적으로 잘 동작</li>
<li>Context를 통해 상태를 깔끔하게 공유</li>
</ul>
<hr>
<h2 id="✔️-higher-order-component-hoc">✔️ Higher-Order Component (HOC)</h2>
<p>HOC는 <strong>컴포넌트를 인자로 받아 새로운 기능을 덧붙인 컴포넌트를 반환하는 함수</strong>다.</p>
<h3 id="공식-정의">공식 정의</h3>
<blockquote>
<p>A higher-order component is a function that takes a component and returns a new component</p>
</blockquote>
<h3 id="언제-사용하는가">언제 사용하는가?</h3>
<ul>
<li>공통 로직을 여러 컴포넌트에서 재사용해야 할 때 (예: 인증 체크, 로깅, 공통 데이터 패칭, 권한 관리))</li>
</ul>
<hr>
<h3 id="예시-코드--인증-여부-검사-hoc">예시 코드 – 인증 여부 검사 HOC</h3>
<pre><code class="language-tsx">function withAuth(Component) {
  return function ProtectedComponent(props) {
    const isLogin = Boolean(localStorage.getItem(&quot;token&quot;));

    if (!isLogin) return &lt;div&gt;로그인이 필요합니다.&lt;/div&gt;;

    return &lt;Component {...props} /&gt;;
  };
}</code></pre>
<h3 id="사용법-1">사용법</h3>
<pre><code class="language-tsx">function MyPage() {
  return &lt;div&gt;마이페이지!&lt;/div&gt;;
}

export default withAuth(MyPage);</code></pre>
<h3 id="장점-1">장점</h3>
<ul>
<li>인증, 공통 로직을 깔끔하게 재사용 가능</li>
<li>코드 중복 감소</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>컴포넌트 트리가 복잡해지고 디버깅이 어려움</li>
<li>React 공식 문서에서는 HOC보다 <strong>Custom Hook</strong>을 더 권장하는 추세</li>
</ul>
<hr>
<h2 id="✔️-react-portal">✔️ React Portal</h2>
<p>Portal은 <strong>컴포넌트를 DOM 트리 외부로 렌더링</strong>하는 기능이다.</p>
<h3 id="대표적인-사용-사례">대표적인 사용 사례</h3>
<ul>
<li>모달</li>
<li>토스트 메시지</li>
<li>드롭다운 메뉴</li>
<li>오버레이 UI</li>
</ul>
<p>즉, 시각적으로는 화면 맨 위(z-index 최고)에 떠 있어야 하지만 컴포넌트 구조는 기존 트리 하위에 있을 때 매우 유용하다.</p>
<hr>
<h3 id="예시-코드--modal-구현">예시 코드 – Modal 구현</h3>
<pre><code class="language-tsx">import { createPortal } from &quot;react-dom&quot;;

export default function Modal({ children }) {
  const modalRoot = document.getElementById(&quot;modal-root&quot;);

  return createPortal(
    &lt;div className=&quot;overlay&quot;&gt;
      &lt;div className=&quot;modal&quot;&gt;{children}&lt;/div&gt;
    &lt;/div&gt;,
    modalRoot
  );
}</code></pre>
<h3 id="indexhtml에-portal-루트-추가">index.html에 Portal 루트 추가</h3>
<pre><code class="language-tsx">&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
&lt;div id=&quot;modal-root&quot;&gt;&lt;/div&gt;</code></pre>
<h3 id="사용법-2">사용법</h3>
<pre><code class="language-tsx">function App() {
  const [open, setOpen] = useState(false);

  return (
    &lt;&gt;
      &lt;button onClick={() =&gt; setOpen(true)}&gt;모달 열기&lt;/button&gt;

      {open &amp;&amp; (
        &lt;Modal&gt;
          &lt;h2&gt;모달 콘텐츠&lt;/h2&gt;
        &lt;/Modal&gt;
      )}
    &lt;/&gt;
  );
}</code></pre>
<hr>
<h3 id="portal을-쓰는-이유">Portal을 쓰는 이유?</h3>
<ul>
<li>모달이 부모의 <code>overflow: hidden</code>이나 <code>z-index</code> 영향을 받지 않도록 하기 위함</li>
<li>DOM 계층을 시각적 계층과 분리할 수 있음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JS 실행시간 최적화: 브라우저 파싱·컴파일·실행 시간을 줄이는 3가지 전략]]></title>
            <link>https://velog.io/@hello-yujin/JS-%EC%8B%A4%ED%96%89%EC%8B%9C%EA%B0%84-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%ED%8C%8C%EC%8B%B1%EC%BB%B4%ED%8C%8C%EC%9D%BC%EC%8B%A4%ED%96%89-%EC%8B%9C%EA%B0%84%EC%9D%84-%EC%A4%84%EC%9D%B4%EB%8A%94-3%EA%B0%80%EC%A7%80-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@hello-yujin/JS-%EC%8B%A4%ED%96%89%EC%8B%9C%EA%B0%84-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%ED%8C%8C%EC%8B%B1%EC%BB%B4%ED%8C%8C%EC%9D%BC%EC%8B%A4%ED%96%89-%EC%8B%9C%EA%B0%84%EC%9D%84-%EC%A4%84%EC%9D%B4%EB%8A%94-3%EA%B0%80%EC%A7%80-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Wed, 03 Dec 2025 19:46:23 GMT</pubDate>
            <description><![CDATA[<p>웹 서비스의 성능을 논할 때 가장 중요하게 다뤄야 할 지표 중 하나가 바로 <strong>JS 실행시간(JS execution time)</strong>이다.
브라우저는 우리가 작성한 JavaScript를 <strong>파싱 → 컴파일 → 실행</strong>하는 복잡한 단계를 거치는데, 이 과정이 길어질수록 사용자 경험은 느려지고 인터랙션은 끊기기 시작한다.</p>
<hr>
<h3 id="✍️-webpack-bundle-analyzer--번들을-시각적으로-분석하기">✍️ webpack-bundle-analyzer — 번들을 시각적으로 분석하기</h3>
<p>이 도구는 프로젝트의 JS 번들 크기를 시각화해주는 대표적인 분석 툴이다.
실제로 웹 서비스 성능 문제의 상당수는 “번들이 너무 크다”로부터 시작된다.
이를 해결하려면 <strong>무엇이 큰지 먼저 알아야 한다.</strong></p>
<h3 id="✔️-설치">✔️ 설치</h3>
<pre><code>npm install --save-dev webpack-bundle-analyzer</code></pre><h3 id="✔️-webpack-설정에-추가">✔️ Webpack 설정에 추가</h3>
<pre><code class="language-js">// webpack.config.js
const BundleAnalyzerPlugin =
  require(&quot;webpack-bundle-analyzer&quot;).BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: &quot;server&quot;,
      openAnalyzer: true,
    }),
  ],
};</code></pre>
<h3 id="✔️-번들-분석-화면-예시">✔️ 번들 분석 화면 예시</h3>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/38453b81-ff8f-4452-a4ca-a91079948c49/image.webp" alt="">
<img src="https://velog.velcdn.com/images/hello-yujin/post/55bdebd9-a6a8-4c90-b2bb-c93b0c8daf45/image.png" alt=""></p>
<p>이 화면을 통해 다음과 같은 의사결정을 할 수 있다.</p>
<ul>
<li>lodash 전체를 import하고 있어 tree-shaking이 안 되고 있네 → 부분 import로 변경</li>
<li>moment.js가 너무 크네 → dayjs로 교체</li>
<li>React-Query devtools가 production에도 포함되네 → 조건부 로딩 필요</li>
<li>initial chunk가 너무 커서 FCP(First Contentful Paint)에 악영향 → dynamic import 적용</li>
</ul>
<h3 id="✔️-dynamic-import-code-splitting-적용-예시">✔️ Dynamic Import (Code Splitting) 적용 예시</h3>
<pre><code class="language-jsx">// Before
import HeavyModal from &quot;./HeavyModal&quot;;

// After
const HeavyModal = React.lazy(() =&gt; import(&quot;./HeavyModal&quot;));</code></pre>
<p>코드 분할을 통해 최초 렌더링에 필요한 JS 크기를 큰 폭으로 줄일 수 있다.</p>
<hr>
<h2 id="✍️-css-최적화--렌더링-비용-줄이기">✍️ CSS 최적화 — 렌더링 비용 줄이기</h2>
<p>JS 실행시간 최적화라고 해서 JS만 줄이면 되는 것이 아니다.
<strong>CSS도 브라우저의 렌더링 성능에 큰 영향을 준다.</strong>
불필요한 CSS는 JS처럼 파싱하고 적용하는 데 시간이 소요되기 때문이다.</p>
<h3 id="✔️-1-사용되지-않는-css-제거-unused-css-purge">✔️ 1. 사용되지 않는 CSS 제거 (Unused CSS purge)</h3>
<p>Tailwind, Next.js, CRA 등 많은 프레임워크가 기본적으로 지원하지만 직접 PurgeCSS를 적용할 수도 있다.</p>
<pre><code>npm install @fullhuman/postcss-purgecss</code></pre><pre><code class="language-js">// postcss.config.js
module.exports = {
  plugins: [
    require(&quot;@fullhuman/postcss-purgecss&quot;)({
      content: [&quot;./src/**/*.jsx&quot;, &quot;./public/index.html&quot;],
      defaultExtractor: (content) =&gt; content.match(/[\w-/:]+(?&lt;!:)/g) || [],
    }),
  ],
};</code></pre>
<h3 id="✔️-2-critical-css-추출">✔️ 2. Critical CSS 추출</h3>
<p>Critical CSS는 <strong>“초기 렌더링에 필요한 최소한의 CSS만 inline으로 넣는 방법”</strong>이다.
Next.js는 이를 자동으로 처리해주지만, 직접 적용하려면:</p>
<pre><code class="language-html">&lt;style&gt;
  /* critical CSS inserted here */
&lt;/style&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;main.css&quot; /&gt;</code></pre>
<p>이렇게 하면 초기 렌더링 속도가 크게 향상된다.</p>
<h3 id="✔️-3-css-파일-분할-로딩">✔️ 3. CSS 파일 분할 로딩</h3>
<p>React + Webpack 환경에서 CSS도 JS처럼 Split 가능하다.</p>
<pre><code class="language-js">// webpack.config.js
{
  test: /\.css$/,
  use: [MiniCssExtractPlugin.loader, &quot;css-loader&quot;],
}</code></pre>
<p>파일이 분리되면 JS 실행시간도 줄고 렌더링 지연도 감소한다.</p>
<h3 id="✔️-css-최적화-결과-예시">✔️ CSS 최적화 결과 예시</h3>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/ed9c829e-828c-4274-86aa-ee57f9f4a796/image.png" alt="">
<img src="https://velog.velcdn.com/images/hello-yujin/post/593f341d-c0f9-4566-b972-1ec8e66dcd1c/image.jpg" alt="">
위처럼 CSS blocking time이 줄어들면 JS도 함께 빠르게 실행된다.</p>
<hr>
<h2 id="✍️-react-compiler--렌더링과-js-실행량을-획기적으로-줄여주는-최신-기술">✍️ React Compiler — 렌더링과 JS 실행량을 획기적으로 줄여주는 최신 기술</h2>
<p>React 팀이 공개한 <strong>React Compiler</strong>는 현재(2025년 기준) 가장 효율적인 렌더링 최적화 도구다.</p>
<h3 id="✔️-react-compiler가-하는-일">✔️ React Compiler가 하는 일</h3>
<ul>
<li>React.memo를 자동으로 적용</li>
<li>useCallback/useMemo 없이도 안정적인 렌더링</li>
<li>불필요한 리렌더링 제거</li>
<li>JS 실행량 자체를 감소시킴</li>
</ul>
<p>즉, 기존에는 <strong>개발자가 직접 최적화를 해줘야 했던 부분을 자동화</strong>해준다.</p>
<h3 id="✔️-react-compiler-적용-예시-nextjs-15-기준">✔️ React Compiler 적용 예시 (Next.js 15 기준)</h3>
<pre><code class="language-js">// next.config.js
module.exports = {
  reactCompiler: true,
};</code></pre>
<p>혹은 React 19 환경에서:</p>
<pre><code class="language-jsx">&quot;use client&quot;;
import { compiler } from &quot;react-compiler&quot;;</code></pre>
<h3 id="✔️-react-compiler-사용-전후-렌더링-비교">✔️ React Compiler 사용 전/후 렌더링 비교</h3>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/7c30483c-3d2e-4f2b-b0b4-16c46a16d77b/image.webp" alt="">
<img src="https://velog.velcdn.com/images/hello-yujin/post/1575c16d-a96d-4122-9fed-a33bbefbddf7/image.png" alt="">
React Compiler가 활성화되면 다음과 같은 효과가 있다:</p>
<ul>
<li>JS 실행 시간 감소</li>
<li>Virtual DOM 업데이트 비용 감소</li>
<li>컴포넌트 rerender 횟수 감소</li>
<li>이벤트 핸들러/콜백의 메모리 사용량 감소</li>
</ul>
<p>특히 많은 상태와 props가 오가는 대규모 SPA에서 체감 속도 차이가 크다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[지연시간 최적화: 사용자 요청 대기시간 최소화 및 리소스 캐싱 전략]]></title>
            <link>https://velog.io/@hello-yujin/%EC%A7%80%EC%97%B0%EC%8B%9C%EA%B0%84-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9A%94%EC%B2%AD-%EB%8C%80%EA%B8%B0%EC%8B%9C%EA%B0%84-%EC%B5%9C%EC%86%8C%ED%99%94-%EB%B0%8F-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%BA%90%EC%8B%B1-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@hello-yujin/%EC%A7%80%EC%97%B0%EC%8B%9C%EA%B0%84-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9A%94%EC%B2%AD-%EB%8C%80%EA%B8%B0%EC%8B%9C%EA%B0%84-%EC%B5%9C%EC%86%8C%ED%99%94-%EB%B0%8F-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%BA%90%EC%8B%B1-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Wed, 03 Dec 2025 19:29:32 GMT</pubDate>
            <description><![CDATA[<p>웹 서비스의 성능을 결정하는 가장 중요한 지표 중 하나는 바로 <strong>지연시간(latency)</strong>이다. 아무리 기능이 많고 UI가 예쁘더라도, 사용자가 클릭 후 응답을 받기까지의 시간이 길다면 서비스 품질은 급격히 떨어진다. 오늘은 실제 프론트엔드 개발에서 가장 많이 사용되는 세 가지 지연시간 최적화 전략인 Caching, Prefetching, CDN을 중심으로 이야기를 풀어보려고 한다.</p>
<hr>
<h2 id="✍️-caching-이미-가져온-건-또-가져오지-않는다">✍️ Caching: 이미 가져온 건 또 가져오지 않는다</h2>
<p>캐싱(Caching)은 가장 기본적인 지연시간 최적화 전략이다. 핵심은 <strong>“같은 요청을 반복하지 않는다”</strong>는 점이다. 요청한 리소스(HTML, JS, CSS, 이미지, API 응답 등)를 클라이언트나 서버 가까운 곳에 저장해 두었다가, 동일한 요청이 들어오면 캐시된 결과를 즉시 반환해서 네트워크 왕복 비용을 절약한다.</p>
<h3 id="✔️-브라우저-캐싱">✔️ 브라우저 캐싱</h3>
<p>브라우저는 정적 리소스를 캐싱해 두고, 캐시 정책(<code>Cache-Control</code>, <code>ETag</code>, <code>Last-Modified</code> 등)에 따라 재요청 여부를 판단한다.
예를 들어, 정적 이미지나 JS 번들에 <code>Cache-Control: max-age=31536000</code>이 걸려 있다면 브라우저는 거의 1년 동안 서버에 요청하지 않고 바로 로컬 캐시에서 리소스를 꺼내 쓴다.</p>
<h3 id="✔️-api-응답-캐싱">✔️ API 응답 캐싱</h3>
<p>React Query(TanStack Query) 같은 상태 관리 라이브러리도 내부적으로 캐싱을 적극적으로 활용한다.
5~10초 사이에 계속 같은 API를 호출하는 경우, 네트워크 요청을 줄이고 캐시 데이터를 즉시 반환해 UI 반응성을 크게 높여준다.</p>
<h4 id="간단한-코드-예시">간단한 코드 예시</h4>
<pre><code class="language-ts">const { data } = useQuery({
  queryKey: [&quot;posts&quot;],
  queryFn: fetchPosts,
  staleTime: 5000, // 5초 동안 refetch 방지
});</code></pre>
<p><strong>목록 → 상세 → 목록</strong>으로 이동해도 네트워크 요청 없이 즉시 화면이 렌더링된다.</p>
<h3 id="✔️-캐싱-동작-흐름">✔️ 캐싱 동작 흐름</h3>
<img src="https://velog.velcdn.com/images/hello-yujin/post/5c11e15a-9c8d-430f-9ad8-ffeddd676c5e/image.webp" width="80%" />

<img src="https://velog.velcdn.com/images/hello-yujin/post/a8bf21c5-5963-48c6-8c04-b75a1fef445a/image.png" width="80%" />




<h3 id="✔️-캐싱의-효과">✔️ 캐싱의 효과</h3>
<ul>
<li>네트워크 왕복 시간(RTT) 감소</li>
<li>서버 부하 감소</li>
<li>사용자 체감 속도 대폭 향상</li>
<li>반복되는 스크롤·탭 이동 상황에서 특히 효율적</li>
</ul>
<hr>
<h2 id="✍️-prefetching-사용자가-클릭하기-전에-미리-준비해두기">✍️ Prefetching: 사용자가 클릭하기 전에 미리 준비해두기</h2>
<p>Prefetching은 <strong>“사용자가 곧 접근할 것 같은 리소스를 미리 가져와 두는 전략”</strong>으로, 지연시간 최적화에서 가장 체감 효과가 큰 기술 중 하나다. 브라우저는 백그라운드에서 조용히 리소스를 다운로드해 놓고, 사용자가 실제 요청하는 순간 즉시 제공한다.</p>
<h3 id="✔️-페이지-prefetch">✔️ 페이지 Prefetch</h3>
<p>Next.js의 <code>next/link</code>는 사용자 viewport에 들어오면 자동으로 Link 타겟 페이지를 prefetch한다. 사용자가 링크를 클릭하는 순간 거의 즉시 페이지가 뜬다.</p>
<h3 id="✔️-데이터-prefetch">✔️ 데이터 Prefetch</h3>
<p>예를 들어, 쇼핑몰 사이트에서 <strong>상품 목록 → 상품 상세 페이지</strong>로 넘어갈 것이 예상된다면 React Query에서는 다음과 같이 prefetch할 수 있다:</p>
<pre><code class="language-ts">queryClient.prefetchQuery([&#39;product&#39;, id], () =&gt; fetchProduct(id));</code></pre>
<p>그 결과, 사용자가 상세 페이지 버튼을 누르는 순간 이미 데이터가 준비되어 있기 때문에 로딩 스피너도 거의 보이지 않는다.</p>
<h3 id="✔️-prefetch는-언제-사용할까">✔️ Prefetch는 언제 사용할까?</h3>
<ul>
<li>페이지 이동이 자주 일어나는 UI</li>
<li>사용자의 행동 패턴이 뚜렷한 사용자 플로우</li>
<li>모바일 환경처럼 네트워크 속도 편차가 큰 경우</li>
</ul>
<h3 id="✔️-주의점">✔️ 주의점</h3>
<p>과도하게 prefetch하면 오히려 불필요한 데이터 사용이 증가하여 성능을 해칠 수 있다.
따라서 <strong>사용자 행동 예측</strong>이 중요하다.</p>
<h3 id="✔️-페이지-prefetch-예시-nextjs">✔️ 페이지 Prefetch 예시 (Next.js)</h3>
<p>Next.js에서 <code>next/link</code>는 자동으로 prefetch 기능을 제공한다.</p>
<pre><code class="language-jsx">&lt;Link href=&quot;/products/1&quot; prefetch&gt;
  상품 상세보기
&lt;/Link&gt;</code></pre>
<p>viewport에 보이는 순간 Next.js가 내부적으로 페이지 데이터를 미리 가져오고, 사용자가 클릭하면 거의 새로고침 없이 즉시 이동된다.</p>
<h3 id="✔️-prefetch-동작-구조">✔️ Prefetch 동작 구조</h3>
<img src="https://velog.velcdn.com/images/hello-yujin/post/0845b626-107b-4478-9942-7370222308ed/image.png" width="80%" />

<img src="https://velog.velcdn.com/images/hello-yujin/post/551dd07d-9084-420e-818f-d8a2b3be038f/image.avif" width="80%" />




<hr>
<h2 id="✍️-cdn-지리적으로-가장-가까운-곳에서-제공하기">✍️ CDN: 지리적으로 가장 가까운 곳에서 제공하기</h2>
<p>CDN(Content Delivery Network)은 <strong>웹 리소스를 지리적으로 사용자와 가까운 서버에서 제공하는 시스템</strong>이다.
한국 사용자에게 미국 서버에서 직접 리소스를 제공한다면 RTT가 수십~수백 ms까지 올라갈 수 있지만, CDN 캐시 서버가 서울·도쿄 등에 배치되어 있다면 지연시간은 극적으로 줄어든다.</p>
<h3 id="✔️-왜-cdn이-중요한가">✔️ 왜 CDN이 중요한가?</h3>
<p>인터넷의 물리적 거리 문제를 해결하는 가장 현실적이고 강력한 기술이기 때문이다.
아무리 최적화를 해도 원본 서버가 멀면 기본적인 RTT는 절대 피할 수 없다.</p>
<h3 id="✔️-cdn의-장점">✔️ CDN의 장점</h3>
<ul>
<li>지리적으로 가까운 Edge 서버에서 리소스 제공</li>
<li>대규모 트래픽에도 안정적인 분산 처리</li>
<li>정적 리소스(JS, CSS, 이미지, 폰트) 제공 속도 극대화</li>
<li>브라우저 캐싱과 함께 사용하면 “더 이상 빨라질 데가 없는” 수준까지 최적화 가능</li>
</ul>
<h3 id="✔️-cdn-적용-예시html">✔️ CDN 적용 예시(HTML)</h3>
<pre><code class="language-html">&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.example.com/style.css&quot; /&gt;</code></pre>
<p>이렇게 CDN 도메인으로 불러오면, 사용자는 가장 가까운 Edge 서버에서 파일을 다운로드한다.</p>
<h3 id="cdn-요청-흐름-이미지">CDN 요청 흐름 이미지</h3>
<img src="https://velog.velcdn.com/images/hello-yujin/post/a3c7c80a-d555-4874-95f6-a54f24b4cb4d/image.png" width="80%" />






<h3 id="✔️-예시">✔️ 예시</h3>
<ul>
<li>Cloudflare CDN</li>
<li>AWS CloudFront</li>
<li>Akamai</li>
<li>Fastly</li>
</ul>
<p>👉 CDN은 단순히 빠르기만 한 것이 아니라,<strong>보안(WAF, SSL), 도커 이미지 배포, 캐시 무효화, 엣지 컴퓨팅</strong> 등 다양한 기능을 함께 제공해 modern web의 핵심 기반이 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 훅 깊게 살펴보기: useLayoutEffect, useContext, useCallback]]></title>
            <link>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%9B%85-%EA%B9%8A%EA%B2%8C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-useLayoutEffect-useContext-useCallback</link>
            <guid>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%9B%85-%EA%B9%8A%EA%B2%8C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-useLayoutEffect-useContext-useCallback</guid>
            <pubDate>Sat, 29 Nov 2025 22:04:05 GMT</pubDate>
            <description><![CDATA[<p>리액트를 사용하다 보면 성능 최적화나 상태 전파, DOM 조작과 같은 복잡한 시점 제어가 필요해지는 순간이 자주 찾아옵니다. 이번 글에서는 <strong>리액트 훅 중에서도 특히 ‘시점 제어’, ‘상태 공유’, ‘함수 메모이제이션’</strong>에 중요한 역할을 하는 세 가지 훅에 대해 정리해보겠습니다.</p>
<hr>
<h2 id="⚛️-uselayouteffect--dom-변경-시점까지-제어하고-싶을-때">⚛️ useLayoutEffect — DOM 변경 시점까지 제어하고 싶을 때</h2>
<p>리액트를 처음 배울 때 가장 많이 쓰는 훅은 useEffect입니다. 그런데 분명 코드가 맞는데 <strong>“화면 깜빡임”, “스타일이 늦게 반영됨”, &quot;스크롤 위치가 순간 튀는 현상&quot;</strong> 등의 문제가 있을 때가 있습니다.
이럴 때 필요한 훅이 바로 <strong>useLayoutEffect</strong>입니다.</p>
<hr>
<h3 id="✔️-useeffect-vs-uselayouteffect-실행-시점의-차이">✔️ useEffect vs useLayoutEffect: 실행 시점의 차이</h3>
<p>먼저 두 훅의 실행 흐름을 비교해볼게요.</p>
<p>React 렌더링 →
<strong>(1) DOM 업데이트 →</strong>
<strong>(2) useLayoutEffect 실행 →</strong>
브라우저가 화면에 그림 →
<strong>(3) useEffect 실행</strong></p>
<p>즉,</p>
<table>
<thead>
<tr>
<th>훅</th>
<th>실행 시점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>useLayoutEffect</strong></td>
<td>DOM이 “업데이트된 직후”, 화면에 그려지기 <em>이전</em></td>
</tr>
<tr>
<td><strong>useEffect</strong></td>
<td>브라우저가 화면을 그린 <em>이후</em></td>
</tr>
</tbody></table>
<h4 id="👉-중요한-포인트">👉 중요한 포인트</h4>
<p>useLayoutEffect는 <strong>렌더링을 블로킹</strong>합니다.
즉, 해당 콜백이 끝날 때까지 화면이 그려지지 않습니다.</p>
<p>그래서 잘 사용하면 깜빡임 없이 자연스러운 UI를 만들 수 있지만, 남용하면 성능이 나빠질 수 있습니다.</p>
<hr>
<h3 id="✔️-예제-실행-순서-비교">✔️ 예제: 실행 순서 비교</h3>
<pre><code class="language-jsx">useEffect(() =&gt; {
  console.log(&#39;useEffect&#39;, count)
}, [count])

useLayoutEffect(() =&gt; {
  console.log(&#39;useLayoutEffect&#39;, count)
}, [count])</code></pre>
<p>버튼을 클릭해 <code>count</code>가 변경되면 실행 순서는 항상 아래와 같습니다.</p>
<pre><code class="language-nginx">useLayoutEffect → useEffect</code></pre>
<hr>
<h3 id="✔️-언제-uselayouteffect를-써야-할까">✔️ 언제 useLayoutEffect를 써야 할까?</h3>
<p><strong>DOM은 계산됐지만 브라우저에 그려지기 전</strong>
“그리기 전에 꼭 해야 하는 작업”이 있을 때</p>
<p>예를 들어:</p>
<ul>
<li>DOM 크기를 재고 해당 값으로 스타일을 조정해야 할 때</li>
<li>스크롤 위치를 정확히 특정 지점으로 이동시켜야 할 때</li>
<li>애니메이션 초기 상태를 DOM 기반으로 세팅해야 할 때</li>
<li>측정 기반 UI(Layout Shift 방지)가 필요할 때</li>
</ul>
<p>즉, 화면에 깜빡임 없이 자연스럽게 초기 상태를 맞추고 싶을 때입니다.</p>
<hr>
<h2 id="⚛️-usecontext--props-drilling을-해결하는-리액트의-전역-전달자">⚛️ useContext — props drilling을 해결하는 리액트의 ‘전역 전달자’</h2>
<p>리액트 컴포넌트 트리 구조에서는 부모가 가진 데이터를 자식, 손자, 증손자에게 전달해야 하는 경우가 많습니다.이걸 전부 <code>props</code>로 내려보내면 아래처럼 지옥 같은 코드가 됩니다.</p>
<pre><code class="language-jsx">&lt;A props={value}&gt;
  &lt;B props={value}&gt;
    &lt;C props={value}&gt;
      &lt;D props={value} /&gt;
    &lt;/C&gt;
  &lt;/B&gt;
&lt;/A&gt;</code></pre>
<p>이걸 <strong>props drilling</strong>이라고 합니다.</p>
<hr>
<h3 id="✔️-usecontext란">✔️ useContext란?</h3>
<p>Context는 리액트에서 “전역 데이터”를 공유하는 메커니즘입니다.</p>
<ul>
<li>Provider가 값을 제공하고</li>
<li>하위의 모든 Consumer(useContext 사용 컴포넌트)가 값을 사용할 수 있음</li>
</ul>
<h3 id="✔️-context-사용-예제">✔️ Context 사용 예제</h3>
<pre><code class="language-jsx">const MyContext = createContext();

function Parent() {
  return (
    &lt;MyContext.Provider value={{ hello: &#39;react&#39; }}&gt;
      &lt;Child /&gt;
    &lt;/MyContext.Provider&gt;
  )
}

function Child() {
  const value = useContext(MyContext);
  return &lt;div&gt;{value.hello}&lt;/div&gt;
}
</code></pre>
<hr>
<h3 id="✔️-그러나-usecontext는-성능적으로-독이-될-수-있다">✔️ 그러나! useContext는 성능적으로 “독이 될 수 있다”</h3>
<h4 id="❗-provider-하위의-컴포넌트는-값이-변경되면-전부-리렌더링된다">❗ Provider 하위의 컴포넌트는 값이 변경되면 전부 리렌더링된다</h4>
<pre><code class="language-jsx">function ParentComponent() {
  const [text, setText] = useState(&#39;&#39;);

  return (
    &lt;ContextProvider text={text}&gt;
      &lt;input value={text} onChange={handleChange} /&gt;
      &lt;ChildComponent /&gt;
    &lt;/ContextProvider&gt;
  );
}</code></pre>
<p>이때 <code>text</code>가 바뀌면 화면에 출력하는 <code>ChildComponent</code>만 리렌더링될 것 같지만…</p>
<p><strong>👉 ContextProvider 하위 트리는 모두 리렌더링됩니다.</strong></p>
<p>콘솔 출력:</p>
<pre><code>렌더링 GrandChildComponent
렌더링 ChildComponent
렌더링 ParentComponent</code></pre><hr>
<h3 id="✔️-해결-memo--context-분리">✔️ 해결: memo + context 분리</h3>
<p>자식 컴포넌트를 memo로 감싸면 props가 변하지 않는 한 리렌더를 막을 수 있습니다.</p>
<pre><code class="language-jsx">const ChildComponent = memo(() =&gt; {
  return &lt;GrandChildComponent /&gt;;
});</code></pre>
<p>하지만 근본적인 해결책은:</p>
<ul>
<li><strong>Context를 너무 많은 데이터 저장소로 사용하지 말 것</strong></li>
<li><strong>Context를 역할별로 세분화해 분리할 것</strong></li>
<li><strong>Recoil/Zustand/Jotai 같은 상태 라이브러리 고려</strong></li>
</ul>
<hr>
<h2 id="⚛️-usecallback--함수를-재생성하지-않도록-메모이제이션하기">⚛️ useCallback — 함수를 재생성하지 않도록 메모이제이션하기</h2>
<p>React에서 함수는 매 렌더링마다 새로 만들어집니다.
이게 문제되는 이유는 <strong>자식 컴포넌트 메모이제이션(memo)</strong>과 연관되기 때문입니다.</p>
<hr>
<h3 id="✔️-문제-상황-memo를-썼는데도-자식이-계속-리렌더링됨">✔️ 문제 상황: memo를 썼는데도 자식이 계속 리렌더링됨</h3>
<pre><code class="language-jsx">const Child = memo(({ value, onChange }) =&gt; {
  useEffect(() =&gt; console.log(&#39;렌더링&#39;, value));
  return &lt;button onClick={onChange}&gt;toggle&lt;/button&gt;
});</code></pre>
<p>부모:</p>
<pre><code class="language-jsx">function App() {
  const [status1, setStatus1] = useState(false);
  const [status2, setStatus2] = useState(false);

  const toggle1 = () =&gt; setStatus1(!status1);
  const toggle2 = () =&gt; setStatus2(!status2);

  return (
    &lt;&gt;
      &lt;Child value={status1} onChange={toggle1} /&gt;
      &lt;Child value={status2} onChange={toggle2} /&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>버튼을 누를 때마다 두 Child 컴포넌트가 모두 리렌더링됩니다.</p>
<p>왜?
<strong>👉 onChange 함수가 매 렌더링마다 새로 생성되기 때문</strong></p>
<hr>
<h3 id="✔️-해결-usecallback-적용">✔️ 해결: useCallback 적용</h3>
<pre><code class="language-jsx">const toggle1 = useCallback(() =&gt; {
  setStatus1(!status1)
}, [status1])</code></pre>
<p>이제 함수는 <strong>의존 배열이 변경될 때만 새로 생성</strong>됩니다.</p>
<p>다시 렌더링을 확인하면:</p>
<ul>
<li>onChange가 변경된 컴포넌트만 리렌더링됨</li>
<li>memo + useCallback 조합으로 최적화 성공</li>
</ul>
<hr>
<h3 id="✔️-usecallback-vs-usememo-차이">✔️ useCallback vs useMemo 차이</h3>
<table>
<thead>
<tr>
<th>목적</th>
<th>사용 예</th>
</tr>
</thead>
<tbody><tr>
<td><strong>useMemo</strong></td>
<td>값을 메모이제이션</td>
</tr>
<tr>
<td><strong>useCallback</strong></td>
<td>함수를 메모이제이션</td>
</tr>
</tbody></table>
<p>둘은 사실 거의 똑같은 훅이며, useCallback은 다음과 같은 sugar syntax일 뿐입니다.</p>
<pre><code class="language-jsx">useCallback(fn, deps) === useMemo(() =&gt; fn, deps)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[✔️ 리액트 핵심 요소 깊게 살펴보기]]></title>
            <link>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EC%9A%94%EC%86%8C-%EA%B9%8A%EA%B2%8C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EC%9A%94%EC%86%8C-%EA%B9%8A%EA%B2%8C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 16 Nov 2025 00:39:57 GMT</pubDate>
            <description><![CDATA[<p>리액트를 사용하다 보면 자연스럽게 마주치게 되는 개념들이 있다.
<code>useMemo</code>, <code>useCallback</code>, <code>리렌더링</code>, <code>Virtual DOM</code>…
프로젝트를 하다 보면 이름은 계속 보이는데, 정작 “왜 필요한지, 내부에서 어떻게 동작하는지”는 흐릿하게 알고 넘어가는 경우가 많다.</p>
<p>하지만 이 개념들이 내부적으로 어떻게 동작하는지를 깊게 이해하고 나면, 성능 최적화나 구조 설계에서 “감으로 쓰는 코드”가 줄어들고, “어디를 어떻게 최적화해야 할지”를 훨씬 선명하게 판단할 수 있게 된다.</p>
<p>이번 글에서는 리액트의 핵심 개념 세 가지인 <strong>메모이제이션, 렌더링 구조, Virtual DOM + Fiber 아키텍처</strong>를 정리해보았다.</p>
<hr>
<h2 id="✔️-메모이제이션--불필요한-연산을-기억하는-기술">✔️ 메모이제이션 — “불필요한 연산을 기억하는 기술”</h2>
<p>리액트에서 메모이제이션은 대표적으로 다음 훅이 있다.</p>
<ul>
<li><p><strong>useMemo</strong></p>
</li>
<li><p><strong>useCallback</strong></p>
</li>
<li><p><strong>React.memo</strong></p>
</li>
</ul>
<p>이 세 가지는 형태는 다르지만 공통적으로 <strong>한 번 계산한 값, 한 번 만든 함수를 기억해뒀다가 재사용</strong>하는 역할을 한다.
즉, 매 렌더링마다 똑같은 일을 반복하지 않고, “이전 결과를 재활용할 수 있는 상황이라면 굳이 다시 계산하지 말자”라는 전략이다.</p>
<p>이 기술의 목적은 단 하나다.</p>
<blockquote>
<p>💡 불필요한 재연산 또는 재렌더링을 줄여 성능을 최적화한다.</p>
</blockquote>
<p>컴포넌트는 상태나 props가 조금만 변해도 다시 호출되기 때문에, 내부에 있는 연산이 비싸면 곧바로 렉이나 프레임 드랍으로 이어질 수 있다. 메모이제이션은 이런 “비싼 연산”이 꼭 필요할 때만 실행되도록 조건을 걸어 주는 장치라고 보면 된다.</p>
<hr>
<h3 id="usememo--값을-기억한다">useMemo — “값을 기억한다”</h3>
<p><code>useMemo</code>는 계산 비용이 큰 값을 기억한다. 말 그대로 “메모이제이션된 값”을 리턴하는 훅이다.</p>
<pre><code>const value = useMemo(() =&gt; heavyCalc(a, b), [a, b]);</code></pre><p><code>heavyCalc(a, b)</code>를 상당히 무거운 연산이라고 가정해보자.
<code>useMemo</code>를 사용하면 <strong><code>a</code> 또는 <code>b</code>가 바뀔 때만 <code>heavyCalc</code>가 재실행</strong>되고, 그 외의 렌더링에서는 <strong>이전에 계산해 둔 결과를 그대로 재사용</strong>한다.</p>
<p>즉, “입력이 변하지 않았다면 굳이 다시 계산하지 말고, 캐시해 둔 값을 그대로 돌려주자”는 방식이다. 대량의 데이터 필터링, 복잡한 통계 계산, 포맷팅 등 <strong>한 번 계산하는 데 시간이 꽤 드는 작업</strong>에 주로 사용될 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/6e726fcb-1e76-49ec-a2a5-5e34925ca454/image.png" alt=""></p>
<p><em>-&gt; 함수가 실행되면 먼저 입력값에 해당하는 결과가 캐시에 있는지 확인하고, 이미 계산된 값이  있다면 다시 계산하지 않고 캐시된 결과를 그대로 반환한다.
-&gt; 반대로 캐시에 없다면 함수가 실제로 실행되어 결과를 만든 뒤, 그 값을 캐시에 저장해 두었다가 다음 호출에서 재사용한다.</em></p>
<hr>
<h3 id="12-usecallback--함수의-주소값을-기억한다">1.2 useCallback — “함수의 주소값을 기억한다”</h3>
<p>리액트의 함수형 컴포넌트는 <strong>렌더링될 때마다 함수가 다시 실행</strong>된다.
당연히 그 안에서 정의한 <strong>이벤트 핸들러 같은 함수들도 매 렌더링마다 새로 생성</strong>된다.</p>
<p>보통은 큰 문제가 없지만, 이 함수를 자식 컴포넌트에 props로 전달할 경우에는 상황이 달라진다. 자식 입장에서는 “매번 다른 함수(참조)가 들어왔다”고 느끼기 때문에, 실제 로직은 바뀌지 않았더라도 불필요하게 다시 렌더링될 수 있다.</p>
<p>이때 <code>useCallback</code>을 사용한다.</p>
<pre><code>const handleClick = useCallback(() =&gt; {
  setCount(c =&gt; c + 1);
}, []);</code></pre><p><code>useCallback</code>은 말 그대로 <strong>“함수의 주소값(참조)을 기억해 두는 훅”</strong>이다. 의존성 배열에 있는 값이 바뀌지 않는 한, 렌더링이 여러 번 일어나더라도 <strong>동일한 함수 인스턴스를 재사용</strong>한다.</p>
<p>이 특징 때문에 <code>React.memo</code>와 함께 쓸 때 효과가 크다.
자식 컴포넌트가 <code>React.memo</code>로 감싸져 있고, props로 받은 함수가 <code>useCallback</code>으로 안정적으로 고정되어 있으면, 자식은 “props가 이전과 동일하다”고 판단해 렌더링을 건너뛸 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/bbde886d-b09c-4260-928f-3310e4d59162/image.png" alt=""></p>
<p><em>-&gt; 리액트가 전체를 다시 렌더링하지 않고, 바뀐 부분만 골라서 렌더링한다는 걸 보여준다.</em></p>
<ul>
<li><em>왼쪽의 빨간 노드(Dirty): state가 변경된 컴포넌트</em></li>
<li><em>오른쪽의 파란 노드: 실제로 다시 렌더링된 부분</em></li>
</ul>
<p><em>리액트는 이렇게 변경이 일어난 지점부터 아래로만 렌더링을 전파해 불필요한 계산을 줄이고 필요한 부분만 효율적으로 업데이트한다.</em></p>
<hr>
<h3 id="13-reactmemo--props가-같으면-렌더링하지-말자">1.3 React.memo — “props가 같으면 렌더링하지 말자”</h3>
<pre><code>export default React.memo(MyComponent);</code></pre><p><code>React.memo</code>는 <strong>컴포넌트 전체를 메모이제이션하는 고차 컴포넌트</strong>다.
이렇게 감싸주면 리액트는 이 컴포넌트에 전달된 <strong>props를 얕게(shallow) 비교</strong>한 뒤, 이전 렌더링 때와 값이 완전히 동일하다면 <strong>컴포넌트 자체의 렌더링을 생략</strong>한다.</p>
<p>즉, 같은 props로 또 호출될 필요가 없는, <strong>순수하게 UI만 그려주는 프리젠테이셔널 컴포넌트</strong>와 궁합이 좋다. 예를 들어 긴 리스트 아이템을 렌더링하는 컴포넌트라면, props가 변하지 않는 한 다시 렌더링할 이유가 없으니 <code>React.memo</code>로 감싸는 것만으로도 비용을 크게 줄일 수 있다.</p>
<p>✔️ 주의!</p>
<ul>
<li><p>객체/배열/함수는 <strong>참조가 바뀌면</strong> 다른 props로 판단됨</p>
</li>
<li><p>그래서 상위 컴포넌트가 매 렌더링마다 새 객체/함수를 만들어서 넘기면, <code>React.memo</code>는 매번 “다른 props”라고 생각해 버린다</p>
</li>
</ul>
<p>→ 따라서 <code>useMemo</code>/<code>useCallback</code>과 함께 사용해야 효과가 있음</p>
<p>예를 들어 <code>onClick={() =&gt; ...}</code>를 JSX 안에서 바로 만들기보다는, <code>useCallback</code>으로 감싸서 참조가 유지되도록 해주어야 <code>React.memo</code>의 장점을 온전히 누릴 수 있다.</p>
<hr>
<h3 id="14-정리">1.4 정리</h3>
<blockquote>
<p>📌 메모이제이션은 무조건 쓰는 도구가 아니다. 필요할 때만 사용해야 한다.</p>
</blockquote>
<p>잘못 사용하면 다음 문제가 발생할 수 있다.</p>
<ul>
<li><p><strong>메모리 사용량 증가</strong>
→ 한 번 계산한 값을 계속 들고 있어야 하기 때문에, 캐시가 많아질수록 메모리를 더 많이 점유한다.</p>
</li>
<li><p><strong>불필요한 비교 비용 발생</strong>
→ 매 렌더링마다 “이전 값과 같은지”를 비교하는 비용이 추가로 발생하고, 이 비용이 오히려 득보다 실이 커질 수도 있다.</p>
</li>
<li><p><strong>코드 복잡도 상승</strong></p>
</li>
<li><blockquote>
<p>컴포넌트 곳곳에 <code>useMemo</code>, <code>useCallback</code>, <code>React.memo</code>가 섞여 들어가면, 나중에 코드를 읽을 때 “왜 여기서 메모이제이션을 했는지” 맥락을 이해하기 어려워질 수 있다.</p>
</blockquote>
</li>
</ul>
<p>결국 “느리니까 일단 <code>useMemo</code>를 붙이자”가 아니라, <strong>“어떤 연산이 병목인지, 어느 지점이 진짜로 비용이 큰지”를 파악한 뒤 정밀하게 적용하는 것이 메모이제이션의 정석</strong>이다.</p>
<hr>
<h2 id="✔️-리액트-렌더링--렌더링이란-무엇인가">✔️ 리액트 렌더링 — “렌더링이란 무엇인가?”</h2>
<p>많은 개발자가 혼동하는 부분이 바로 <strong>렌더링이 일어나는 조건과 단계</strong>다.
보통은 “렌더링 = 화면이 다시 그려지는 것”이라고 막연히 생각하지만, 리액트는 이 과정을 훨씬 더 세분화해서 다룬다.</p>
<p>리액트는 단순히 “화면이 다시 그려지는 것”만을 렌더링이라고 말하지 않는다.
실제로는 다음과 같은 두 단계로 구성된 매우 복잡한 작업이다. 이 두 단계가 분리되어 있기 때문에, 리액트는 계산과 실제 DOM 변경을 따로 관리하면서 효율을 높일 수 있다.</p>
<hr>
<h3 id="리액트-렌더링의-두-단계">리액트 렌더링의 두 단계</h3>
<p><strong>① Render Phase (렌더 단계)</strong></p>
<ul>
<li><p>순수 함수처럼 실행되어 UI의 “스냅샷”을 생성</p>
</li>
<li><p>React Fiber 트리를 기반으로 각 컴포넌트의 결과를 계산</p>
</li>
<li><p>필요하다면 작업 도중에 <strong>비동기적으로 중단</strong> 가능</p>
</li>
<li><p>이 단계에서는 <strong>DOM에는 아직 아무 변화도 일어나지 않음</strong></p>
</li>
</ul>
<p>즉, Render Phase는 “만약 이런 상태라면 UI가 어떻게 생겼어야 하는지”를 머릿속(가상 트리)으로 그려보는 과정이다. 계산만 수행할 뿐, 실제로 화면에 적용되지는 않는다.② Commit Phase (커밋 단계)</p>
<p><strong>② Commit Phase (커밋 단계)</strong></p>
<ul>
<li><p>Render Phase에서 계산한 결과를 바탕으로 실제 DOM을 변경</p>
</li>
<li><p><code>useEffect</code>, <code>useLayoutEffect</code> 같은 부수효과도 이때 실행</p>
</li>
<li><p>이 단계는 <strong>중단 불가, 반드시 끝까지 완료되어야 함</strong></p>
</li>
</ul>
<p>Commit Phase가 끝나야 비로소 사용자 눈에 보이는 화면이 바뀐다.
즉, 우리가 보는 “UI 업데이트”는 Commit Phase의 결과물이다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/c53bf8f8-22c2-4faf-94e7-d86d1092da9a/image.png" alt="">
-&gt; <em><strong>Render Phase</strong>에서는 JSX를 기반으로 새로운 Fiber 트리를 만들고 이전 렌더 트리와 비교(diffing)을 수행한다.</em>
-&gt; <em><strong>Commit Phase</strong>에서는 diff 결과를 기반으로 실제 DOM을 최소한으로 업데이트한다.</em></p>
<hr>
<h3 id="21-렌더링-발생-조건">2.1 렌더링 발생 조건</h3>
<p>다음 중 하나만 일어나도 렌더링이 발생한다.</p>
<ul>
<li><p><strong>state 변경</strong></p>
</li>
<li><p><strong>props 변경</strong></p>
</li>
<li><p><strong>부모 컴포넌트가 렌더링됨</strong> (자식도 기본적으로 다시 렌더링)</p>
</li>
<li><p><strong>context 변경</strong></p>
</li>
</ul>
<p>리액트는 이런 변경 신호를 감지하면 해당 컴포넌트 및 자식들에 대해 Render Phase를 다시 수행한다.</p>
<p>그리고 여기서 중요한 점은…</p>
<blockquote>
<p><strong>❗ “렌더링이 일어났다고 해서 DOM이 무조건 변경되는 것은 아니다!”</strong></p>
</blockquote>
<ul>
<li><p><strong>렌더링(Render Phase)</strong>
: 가상 DOM(Fiber 트리) 상에서 “무엇이 어떻게 달라졌는지”를 계산하는 단계, 리액트 내부 계산 영역</p>
</li>
<li><p><strong>커밋(Commit Phase)</strong>
: 이 계산 결과를 실제 DOM에 반영하는 단계</p>
</li>
</ul>
<p>이렇게 두 단계를 분리해 두었기 때문에, 리액트는 렌더링(계산)은 자주 일어나도 괜찮게 만들고, DOM 변경은 정말 필요한 시점에 최소한으로만 수행하는 전략을 취할 수 있다.</p>
<hr>
<h2 id="✔️-virtual-dom--react-fiber--리액트-성능의-핵심">✔️ Virtual DOM &amp; React Fiber — “리액트 성능의 핵심”</h2>
<p>리액트의 가장 유명한 개념 중 하나가 바로 <strong>Virtual DOM(가상 DOM)</strong>이다.
평소 리액트를 공부하다 보면 거의 무조건 등장하는 개념이기 때문에 익숙한 용어이지만, 동시에 “Virtual DOM이라서 브라우저 DOM보다 무조건 빠르다”라는 식의 오해도 많은 것 같다.</p>
<p>실제로는 Virtual DOM 자체가 마법처럼 빠른 것이 아니라, <strong>DOM을 직접 다루는 횟수와 범위를 줄여서 전체적인 퍼포먼스를 안정적으로 유지하기 위한 전략</strong>에 가깝다.</p>
<p>즉, Virtual DOM의 핵심은 “절대적인 속도”가 아니라, <strong>다양한 UI 변경 상황에서 일관되게 ‘충분히 빠른’ 성능을 제공하는 구조</strong>에 있다.</p>
<hr>
<h3 id="31-virtual-dom이-필요한-이유">3.1 Virtual DOM이 필요한 이유</h3>
<p>브라우저 DOM 변경은 비용이 크다.</p>
<ul>
<li><p>레이아웃 계산 (Reflow)</p>
</li>
<li><p>페인트(Paint)</p>
</li>
<li><p>복잡한 트리 구조 변경</p>
</li>
</ul>
<p>DOM 요소를 자주 변경하면 브라우저는 레이아웃과 페인트를 반복해서 수행해야 하고, 화면이 복잡할수록 그 비용은 더 커진다.</p>
<p>리액트는 이러한 연산을 다음 순서로 최적화한다.</p>
<ol>
<li><p><strong>변경되었을 것 같은 부분만 Virtual DOM에서 먼저 계산</strong>한다.</p>
</li>
<li><p>이전 <strong>Virtual DOM과 비교(diff)</strong>해서 최소 변경 사항만 추려낸다.</p>
</li>
<li><p>그 최소 변경 사항만 <strong>실제 DOM에 반영</strong>한다.</p>
</li>
<li><p>필요 없는 렌더링은 <strong>Fiber의 스케줄링에 의해 중단하거나 연기</strong>할 수 있다.</p>
</li>
</ol>
<p>즉…</p>
<blockquote>
<p><strong>💡 Virtual DOM의 목적은 “최소 변경만 DOM에 반영하기 위한 사전 계산 구조”이다.</strong></p>
</blockquote>
<p>DOM을 직접 마구 건드리는 대신, 한 단계 위에서 “어디를 어떻게 바꿀지”를 먼저 계산하고, <strong>거기서 나온 diff만 실제 DOM에 적용하는 방식</strong>이라고 이해하면 된다.</p>
<hr>
<h3 id="32-react-fiber--리액트-아키텍처의-핵심">3.2 React Fiber — 리액트 아키텍처의 핵심</h3>
<p>Virtual DOM을 실제로 구현하는 기술이 바로 Fiber다.
단순히 “트리를 한 번에 재귀 순회”하는 수준이 아니라, 렌더링을 잘게 나누고 우선순위를 매기고, 중단·재개하는 데 최적화된** 리액트 전용 렌더링 엔진**이라고 보면 된다.</p>
<p>Fiber는 각 컴포넌트/DOM 노드를 나타내는 “작업 단위 객체”이며, 내부 구조는 다음과 같다.</p>
<ul>
<li><p><strong>tag</strong> (타입: 함수 컴포넌트, 호스트 컴포넌트, 텍스트 등)</p>
</li>
<li><p><strong>pendingProps</strong> : 아직 처리되지 않은 새 props</p>
</li>
<li><p><strong>memoizedProps</strong> : 이전 렌더링에서 사용된 props</p>
</li>
<li><p><strong>stateNode</strong> : 실제 DOM 노드나 인스턴스</p>
</li>
<li><p><strong>child</strong> : 첫 번째 자식 Fiber</p>
</li>
<li><p><strong>sibling</strong> : 같은 레벨의 다음 형제 Fiber</p>
</li>
<li><p><strong>return</strong> : 부모 Fiber</p>
</li>
<li><p><strong>alternate</strong> (더블 버퍼링 구현을 위한 “쌍” Fiber)</p>
</li>
</ul>
<p>이 정보들을 통해 리액트는 “현재 화면 상태”와 “새로 계산된 화면 상태”를 각각의 Fiber 트리로 관리하면서, 어떤 노드를 어떻게 업데이트해야 할지 세밀하게 제어할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/beac8558-c6f5-4687-81f4-3f0e33929f25/image.png" alt="">
-&gt; <em><code>React Fiber</code>가 컴포넌트 트리를 내부적으로 어떻게 연결하는지를 보여준다.
각 Fiber 노드는 세 가지 포인터로 연결된다.</em></p>
<ul>
<li><p><em><strong>child</strong>(빨간색): 첫 번째 자식 노드를 가리킴</em></p>
</li>
<li><p><em><strong>sibling</strong>(초록색): 형제 노드를 오른쪽으로 연결</em></p>
</li>
<li><p><em><strong>return</strong>(갈색): 부모 Fiber로 되돌아가는 링크</em></p>
</li>
</ul>
<p><em>JSX의 계층 구조가 실제로는 이렇게 <strong>child → sibling → return</strong> 형태의 연결 그래프로 재구성되기 때문에, 리액트는 트리를 유연하게 순회하고 작업을 중단·재개할 수 있다.</em></p>
<hr>
<h3 id="33-fiber의-강력한-특징">3.3 Fiber의 강력한 특징</h3>
<p><strong>① 작업을 쪼갤 수 있다 (scheduling)</strong></p>
<p>우선순위가 낮은 렌더링 작업은 나중에 수행하고, 사용자 입력 등 중요한 이벤트는 먼저 처리할 수 있도록 <strong>작업을 작은 단위로 쪼개고 스케줄링</strong>할 수 있다. 덕분에 큰 렌더링 작업이 있어도 UI가 완전히 멈춰버리는 상황을 줄일 수 있다.</p>
<p><strong>② 작업을 중단하고 재개할 수 있다</strong></p>
<p>SPA에서 많은 렌더링을 순차적으로 처리하면서 발생하던 버벅임을 없애기 위해, Fiber는 <strong>렌더링 도중에도 작업을 일시 중단</strong>할 수 있는 구조를 갖는다. 필요하다면 “여기까지 하고 잠깐 멈췄다가, 브라우저가 한숨 돌린 후에 다시 이어서 렌더링”하는 식으로 작업을 이어갈 수 있다.</p>
<p><strong>③ 더블 버퍼링</strong></p>
<p><strong>- current 트리(화면에 보이는 상태)</strong></p>
<p><strong>- workInProgress 트리(작업 중인 상태)</strong></p>
<p>리액트는 이 두 트리를 동시에 유지하다가, workInProgress 트리의 작업이 끝나면 <strong>포인터만 바꿔 current 트리로 승격</strong>시킨다. 이 방식 덕분에 DOM을 조금씩 바꾸다가 실패하는 일이 없고, 항상 “완성된 결과만 한 번에 커밋”해서 UI 일관성을 지킬 수 있다.
<img src="https://velog.velcdn.com/images/hello-yujin/post/25a837ec-8694-4afc-a203-b0909d7b286d/image.png" alt="">
-&gt; _오른쪽의 <strong>Virtual DOM</strong> 영역: 현재 화면을 나타내는 <strong>Current Fiber Tree</strong>와 새로 렌더링을 준비하는 <strong>Work-in-Progress Tree</strong>가 함께 존재한다. _
-&gt; _<strong>Render Phase</strong>: WIP 트리가 계산되며, Commit Phase가 되면 그 결과가 왼쪽의 <strong>실제 DOM</strong> 트리에 반영된다. _</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🌲Tree Shaking]]></title>
            <link>https://velog.io/@hello-yujin/Tree-Shaking</link>
            <guid>https://velog.io/@hello-yujin/Tree-Shaking</guid>
            <pubDate>Wed, 12 Nov 2025 08:51:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>“쓰지 않는 코드는 버려라.” — 번들 속 불필요한 잎사귀를 털어내는 기술</p>
</blockquote>
<hr>
<h2 id="🌲-들어가며">🌲 들어가며</h2>
<p>대부분의 웹앱은 수많은 라이브러리로 구성됩니다.
lodash, date-fns, moment, react-icons, three.js...
우리가 사용하는 코드보다 <strong>사용하지 않는 코드가 훨씬 많습니다.</strong></p>
<p>이제 브라우저가 그 모든 코드를 다운로드하고, 파싱하고, 실행해야 한다면?
필요하지 않은 코드도 사용자 경험에 직접적인 <strong>성능 비용</strong>을 유발하게 됩니다.</p>
<p>이 문제를 해결하는 것이 바로 <strong>Tree Shaking</strong>입니다.</p>
<blockquote>
<p>“빌드 단계에서 사용되지 않는(Dead) 코드를 제거해 번들을 더 작게 만드는 기술”</p>
</blockquote>
<hr>
<h2 id="tree-shaking-개념">Tree Shaking 개념</h2>
<p>Tree Shaking은 <strong>정적 분석(Static Analysis)</strong> 을 통해
사용되지 않는 코드를 “감지하고 제거”하는 번들 최적화 기술입니다.</p>
<p>말 그대로 <strong>트리에서 안 쓰이는 가지(코드)</strong>를 “털어내는(shake)” 과정이죠.</p>
<table>
<thead>
<tr>
<th>용어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>“Tree”</td>
<td>모듈 간의 import/export 의존 그래프</td>
</tr>
<tr>
<td>“Shake”</td>
<td>사용되지 않는(exported but unused) 코드 제거</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/b5175e8d-e192-4bb6-be26-7efe0f436a1f/image.png" alt="">
<strong>Tree Shaking 작동 원리 (사용되지 않는 코드 제거 과정)</strong></p>
<ul>
<li>상단의 <code>Input: { Used: { scope1 } }</code> 는 번들러(Webpack, Rollup 등)가 <strong>실제로 사용 중인 함수(scope1) 만 추적</strong>한다는 것을 의미합니다.</li>
<li>코드 내에서 <code>scope1</code>과 <code>scope2</code>는 실행되지만, <code>scope3</code>은 호출되지 않습니다.</li>
<li>따라서 <code>scope3</code>에서 사용하는 <code>isNumber()</code> 함수는 <strong>실제 빌드 결과에서 제거(Tree Shaking)</strong> 됩니다.
👉 Tree Shaking은 이렇게 “사용되지 않는 모듈(import)”을 <strong>정적 분석(Static Analysis)</strong> 을 통해 감지하고, 최종 번들에서 제외함으로써 <strong>파일 크기를 최소화하고 성능을 최적화</strong>합니다.</li>
</ul>
<hr>
<h2 id="🌲-왜-tree-shaking이-필요한가">🌲 왜 Tree Shaking이 필요한가?</h2>
<h3 id="문제-상황">문제 상황</h3>
<p>모듈 단위의 import/export 구조를 가지는 ES6(ESM) 환경에서는,
다음처럼 작은 기능 하나만 가져와도 사실상 <strong>전체 코드</strong>가 번들될 수 있습니다.</p>
<pre><code>// utils.js
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }
export function mul(a, b) { return a * b; }

// main.js
import { add } from &#39;./utils.js&#39;;
console.log(add(2, 3));</code></pre><p>위 코드의 결과는?</p>
<p>👉 기본적으로 <code>add</code>, <code>sub</code>, <code>mul</code> <strong>세 함수가 모두 번들에 포함됩니다.</strong>
왜냐하면 번들러가 “어떤 함수가 실제로 쓰이는지” 분석하지 않으면 전체 모듈을 가져오기 때문입니다.</p>
<p>Tree Shaking은 이 중 <strong>사용된 함수(<code>add</code>)만 남기고,</strong> 나머지(<code>sub</code>, <code>mul</code>)를 제거합니다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/65b5d8d7-163f-4b79-ab8b-5b94145a0d7a/image.png" alt="">
<strong>Tree Shaking 적용 전후 구조 비교</strong></p>
<ul>
<li><strong>왼쪽(Before Tree Shaking):</strong> <code>index.js</code>가 여러 모듈(<code>Module 1~3</code>, <code>file1~4.js</code>)과 함수들을 참조하고 있습니다. 하지만 이 중 다수의 모듈과 함수는 실제 실행되지 않음에도 번들에 포함되어 있습니다.</li>
<li><strong>오른쪽(After Tree Shaking):</strong> 번들러(Webpack/Rollup 등)가 <strong>정적 분석(Static Analysis)</strong> 을 통해 <strong>실제로 참조되는 함수(file3.js → function)</strong> 만 남기고, 사용되지 않는 코드(죽은 가지, Dead Code)를 모두 제거했습니다.
👉 Tree Shaking은 이렇게 <strong>“필요한 코드만 살아남게” 하여 번들 크기를 최소화</strong>하고, <strong>불필요한 로드 및 파싱 비용을 줄이는 핵심 최적화 기법</strong>입니다.</li>
</ul>
<hr>
<h2 id="🌲-tree-shaking의-작동-원리">🌲 Tree Shaking의 작동 원리</h2>
<p>Tree Shaking은 <strong>ES Modules(ESM)</strong> 구조와 <strong>정적 분석(Static Analysis)</strong> 을 기반으로 작동합니다.</p>
<h3 id="작동-단계">작동 단계</h3>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>① <strong>의존성 그래프 생성</strong></td>
<td>import/export 관계를 분석</td>
</tr>
<tr>
<td>② <strong>사용 경로 추적</strong></td>
<td>실제로 참조된 export만 “활성(active)” 표시</td>
</tr>
<tr>
<td>③ <strong>미사용 코드 제거</strong></td>
<td>활성되지 않은 export는 빌드 결과에서 제거</td>
</tr>
<tr>
<td>④ <strong>Dead Code Elimination (DCE)</strong></td>
<td>압축기(Uglify/Terser)가 남은 코드 중 부수효과 없는 부분 제거</td>
</tr>
</tbody></table>
<h3 id="예시로-이해하기">예시로 이해하기</h3>
<pre><code>// math.js
export const add = (a, b) =&gt; a + b;
export const minus = (a, b) =&gt; a - b;

// main.js
import { add } from &quot;./math.js&quot;;
console.log(add(2, 3));</code></pre><p>빌드 결과 (Before)</p>
<pre><code>const add=(a,b)=&gt;a+b;const minus=(a,b)=&gt;a-b;console.log(add(2,3));</code></pre><p>빌드 결과 (After Tree Shaking)</p>
<pre><code>console.log(((a,b)=&gt;a+b)(2,3));</code></pre><p>👉 <strong>minus()는 제거됨</strong></p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/77611f70-385b-406b-8e42-194c1ea84457/image.png" alt="">
<strong>ES Module의 Binding 구조</strong></p>
<ul>
<li>ES Module은 단순히 값을 복사하지 않고, <strong>“참조(reference)”로 연결된 바인딩(binding)</strong> 을 유지합니다.</li>
<li>즉, 한 모듈에서 <code>export</code>한 변수가 변경되면, 이를 <code>import</code>한 다른 모듈에서도 <strong>자동으로 최신 값이 반영</strong>됩니다.</li>
<li>하지만 반대로 <code>import</code>한 쪽에서 값을 변경하려는 시도는 불가능합니다 <strong>(읽기 전용 바인딩)</strong>.
👉 이러한 <strong>정적 구조적 연결성</strong> 덕분에 Tree Shaking 같은 정적 분석 기반 최적화가 가능해집니다.</li>
</ul>
<hr>
<h2 id="🌲-tree-shaking이-가능한-전제-조건">🌲 Tree Shaking이 가능한 전제 조건</h2>
<p>Tree Shaking이 작동하려면 몇 가지 <strong>필수 조건</strong>이 있습니다.</p>
<table>
<thead>
<tr>
<th>조건</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>✅ <strong>ES Module(ESM) 사용</strong></td>
<td><code>import/export</code> 구조만 정적 분석 가능. <code>require()</code>는 불가능</td>
</tr>
<tr>
<td>✅ <strong>부수효과(side effects)가 없어야 함</strong></td>
<td>모듈 로드시 실행되는 코드가 있으면 제거 불가</td>
</tr>
<tr>
<td>✅ <strong>번들러 최적화 옵션 활성화</strong></td>
<td>Webpack: <code>usedExports</code>, Rollup: <code>treeshake: true</code></td>
</tr>
<tr>
<td>✅ <strong>Production 모드 빌드</strong></td>
<td>Development에서는 최적화 미적용</td>
</tr>
</tbody></table>
<pre><code>
// webpack.config.js
module.exports = {
  mode: &quot;production&quot;,
  optimization: { usedExports: true },
};</code></pre><blockquote>
<p>🧭 주의:
CommonJS(<code>require</code>)는 <strong>동적 import 경로를 분석할 수 없어</strong> Tree Shaking이 불가능합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/5b830373-1596-4edf-b7a0-6ff15958ae08/image.jpg" alt="">
<strong>ESM vs CJS: Tree Shaking이 가능한 이유</strong></p>
<ul>
<li><strong>CJS(CommonJS)</strong> 는 <code>require()</code>를 런타임에 실행하므로, 어떤 모듈이 실제로 사용될지 <strong>정적 분석 시점에 알 수 없습니다.</strong></li>
<li><strong>ESM(ES Modules)</strong> 은 <code>import/export</code>가 <strong>파일 상단에서 정적으로 선언</strong>되기 때문에, 번들러(Webpack, Rollup 등)가 의존 그래프를 쉽게 분석하여 <strong>사용되지 않는 코드를 안전하게 제거(Tree Shaking)</strong> 할 수 있습니다.
👉 <strong>Tree Shaking은 ESM의 정적 구조 덕분에 가능한 최적화</strong>입니다.</li>
</ul>
<hr>
<h2 id="🌲-tree-shaking-vs-dead-code-elimination-dce">🌲 Tree Shaking vs Dead Code Elimination (DCE)</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Tree Shaking</th>
<th>Dead Code Elimination</th>
</tr>
</thead>
<tbody><tr>
<td>동작 시점</td>
<td>모듈 그래프 단계</td>
<td>코드 압축 단계</td>
</tr>
<tr>
<td>주체</td>
<td>번들러 (Webpack, Rollup 등)</td>
<td>압축기 (Terser, UglifyJS 등)</td>
</tr>
<tr>
<td>대상</td>
<td>미사용 export</td>
<td>실행되지 않는 코드 블록</td>
</tr>
<tr>
<td>핵심 원리</td>
<td>정적 의존성 분석</td>
<td>조건 분기 제거</td>
</tr>
<tr>
<td>예시</td>
<td>사용되지 않은 함수 제거</td>
<td>if (false) { ... } 블록 제거</td>
</tr>
</tbody></table>
<p>결국 <strong>Tree Shaking은 DCE의 전처리 단계</strong>라고 볼 수 있습니다.
→ 번들러가 “무의미한 export”를 제거하고,
→ 압축기가 “조건상 절대 실행되지 않는 코드”를 제거합니다.</p>
<hr>
<h2 id="🌲-tree-shaking의-한계와-실패-사례">🌲 Tree Shaking의 한계와 실패 사례</h2>
<table>
<thead>
<tr>
<th>원인</th>
<th>설명</th>
<th>해결책</th>
</tr>
</thead>
<tbody><tr>
<td>⚠️ <strong>CommonJS</strong></td>
<td><code>require()</code>는 정적 분석 불가</td>
<td>ESM(<code>import/export</code>)로 변경</td>
</tr>
<tr>
<td>⚠️ <strong>동적 import 경로</strong></td>
<td>문자열 조합으로 경로 결정 시 분석 불가</td>
<td>정적 문자열 사용</td>
</tr>
<tr>
<td>⚠️ <strong>부수효과(side effect)</strong></td>
<td>import 시 전역 변수 수정, 콘솔 출력 등</td>
<td>모듈을 “순수(pure)”하게 유지</td>
</tr>
<tr>
<td>⚠️ <strong>배럴 파일(index.js)</strong></td>
<td>모든 모듈을 재export 하면 모두 포함</td>
<td>직접 경로로 import</td>
</tr>
<tr>
<td>⚠️ <strong>ESLint / Babel 트랜스파일</strong></td>
<td><code>require</code> 기반 변환 시 Tree Shaking 깨짐</td>
<td><code>modules: false</code> 설정 유지</td>
</tr>
<tr>
<td>```</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>// babel.config.json
{
  &quot;presets&quot;: [[&quot;@babel/preset-env&quot;, { &quot;modules&quot;: false }]]
}</p>
<p>```
<img src="https://velog.velcdn.com/images/hello-yujin/post/18f69a13-5b81-416e-ad5e-128c38052bd9/image.jpeg" alt="">
<strong>배럴 파일 import 문제 도식</strong></p>
<ul>
<li><code>index.js</code>에서 모든 모듈을 한꺼번에 export하면, 사용되지 않는 함수까지 <strong>모두 번들 파일(bundle.js) 에 포함</strong>됩니다.</li>
<li>즉, Tree Shaking이 제대로 작동하지 않아 번들 크기가 커지는 원인이 됩니다.
👉 <strong>&quot;배럴 구조는 Tree Shaking의 적&quot;</strong>이라고 할 수 있습니다.</li>
</ul>
<hr>
<h2 id="🌲-마무리">🌲 마무리</h2>
<p>Tree Shaking은 “코드를 지운다”는 행위가 아닙니다.
<strong>“사용자에게 도달하지 않는 코드가 존재하지 않도록 한다”</strong>는 원칙입니다.</p>
<p>즉, 성능 최적화의 목적은 “코드를 덜 쓰는 것”이 아니라,
“사용하지 않는 코드를 아예 배포하지 않는 것”이죠.</p>
<p>이건 결국 “사용자의 다운로드 시간을 줄이는 UX 최적화”이기도 합니다.</p>
<blockquote>
<p>Tree Shaking은 가벼운 사용자 경험을 위한 사전 청소기다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[✂️ Code Splitting]]></title>
            <link>https://velog.io/@hello-yujin/Code-Splitting</link>
            <guid>https://velog.io/@hello-yujin/Code-Splitting</guid>
            <pubDate>Wed, 12 Nov 2025 08:39:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>한 번에 다 불러오지 말고, 필요한 시점에 나눠서 불러와라</p>
</blockquote>
<hr>
<h2 id="✂️-들어가며">✂️ 들어가며</h2>
<p>SPA(Single Page Application)는 한 번 로드되면 앱 전체를 빠르게 사용할 수 있다는 장점이 있습니다.
하지만 문제는 <strong>처음 로드되는 &quot;한 번&quot;이 너무 무겁다</strong>는 점이죠.</p>
<p>초기 번들이 너무 크면,</p>
<ul>
<li>페이지 진입 속도는 느려지고,</li>
<li>사용자는 첫 화면을 보기 전부터 지루해하며,</li>
<li>브라우저는 파싱·실행하느라 메인 스레드를 점유합니다.</li>
</ul>
<p>이때 필요한 전략이 바로 <strong>Code Splitting(코드 분할)</strong>입니다.</p>
<blockquote>
<p><strong>“앱 전체 코드를 한 덩어리로 불러오지 말고, 필요한 순간에 나눠서 불러온다.”</strong></p>
</blockquote>
<hr>
<h2 id="왜-코드-분할이-필요한가">왜 코드 분할이 필요한가?</h2>
<h3 id="문제-상황-번들-크기-폭발">문제 상황: 번들 크기 폭발</h3>
<p>현대 프론트엔드는 하나의 번들 파일(<code>bundle.js</code>)에 다음이 모두 들어갑니다:</p>
<ul>
<li>React / Vue / Lodash 같은 <strong>공통 라이브러리</strong></li>
<li>모든 페이지/컴포넌트의 코드</li>
<li>스타일, 폰트, 이미지 등 정적 리소스</li>
</ul>
<p>결과적으로 <strong>“첫 화면을 띄우기 위해 10만 줄의 코드”</strong>를 받게 됩니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>초기 번들 크기</td>
<td>약 2MB</td>
</tr>
<tr>
<td>JS 파싱 시간</td>
<td>600~900ms</td>
</tr>
<tr>
<td>첫 페인트 시점 (FCP)</td>
<td>3.8초</td>
</tr>
<tr>
<td>사용자 체감</td>
<td>“버벅인다”, “로딩이 너무 길다”</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/6e78b8be-3de7-4416-98a6-7b49dee86b57/image.png" alt="">
<strong>하나의 번들로 묶인 구조 (Webpack 번들링 개념도)</strong></p>
<ul>
<li><strong>왼쪽:</strong> 여러 개의 모듈(자바스크립트, Sass, 이미지 등)이 서로 의존 관계를 맺고 있습니다.</li>
<li><strong>가운데:</strong> Webpack이 이 모든 파일을 하나의 <strong>번들(bundle)</strong> 로 묶어주는 과정입니다.</li>
<li><strong>오른쪽:</strong> 번들링 결과물로, <code>.js</code>, <code>.css</code>, <code>.jpg</code>, <code>.png</code> 등의 <strong>정적 자산(Static Assets)</strong> 이 생성됩니다.
👉 이처럼 번들링은 프로젝트의 모든 자원을 하나로 압축하지만, <strong>모든 코드가 한 번에 로드되어 초기 성능 저하를 초래할 수 있습니다.</strong> 따라서 이후 단계에서 <strong>Code Splitting(코드 분할)</strong> 을 통해 필요한 부분만 나누어 불러오는 최적화가 이루어집니다.</li>
</ul>
<hr>
<h2 id="✂️-code-splitting-개념">✂️ Code Splitting 개념</h2>
<p>Code Splitting은 <strong>애플리케이션 코드를 여러 개의 작은 번들(Chunk)</strong>로 분리하여, 필요한 시점에만 네트워크로 로드하는 기법입니다.</p>
<p>즉, 앱을 기능 단위로 쪼개서 <strong>“사용자가 필요한 순간”</strong>에만 불러옵니다.</p>
<pre><code># 예시
main.js         (기본 UI)
chart.js        (통계 페이지용)
admin.js        (관리자 페이지용)</code></pre><p>이렇게 분할하면 초기 로딩 때 main.js만 다운로드하고, 관리자 페이지로 이동할 때 admin.js를 나중에 불러옵니다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/6243f1bc-f74f-41da-9b4e-c3738f2bf448/image.jpg" alt="">
<strong>Code Splitting 구조도</strong></p>
<ul>
<li><strong>상단:</strong> 기존에는 모든 코드가 하나의 <code>bundle.js</code> 안에 묶여 있어, 페이지를 처음 로드할 때 <strong>모든 스크립트가 한꺼번에 다운로드</strong>되었습니다.</li>
<li>하단: Code Splitting을 적용하면, 코드를 여러 개의** chunk 파일(<code>chunk.js</code>)** 로 나누어 <strong>사용자가 특정 페이지나 기능을 요청할 때만 해당 코드가 로드</strong>됩니다.
👉 초기 로딩 속도를 줄이고, <strong>사용자가 실제로 필요로 하는 기능만 즉시 로드</strong>할 수 있습니다.</li>
</ul>
<hr>
<h2 id="✂️-동작-원리-번들러가-코드를-나누는-방법">✂️ 동작 원리: 번들러가 코드를 나누는 방법</h2>
<h3 id="번들링의-기본">번들링의 기본</h3>
<p>Webpack, Vite, Rollup 같은 번들러는 모든 JS 파일의 의존 관계 그래프를 분석하여 하나의 파일로 묶습니다.</p>
<p>하지만 다음 조건을 만나면 <strong>자동으로 “청크(Chunk)”</strong>를 나눕니다.</p>
<p><strong>1. 동적 import 사용 (<code>import()</code>)</strong>
<strong>2. React.lazy()</strong> 등 lazy load 선언
<strong>3. Route-level Split</strong> (페이지 단위 분리)</p>
<pre><code>// before
import Chart from &#39;./Chart&#39;;

// after
const Chart = React.lazy(() =&gt; import(&#39;./Chart&#39;));</code></pre><p>👉 <code>import()</code>가 등장하는 순간,
Webpack은 해당 모듈을 별도 청크(<code>chart.chunk.js</code>)로 분리합니다.
브라우저는 실제로 그 코드가 필요해질 때 네트워크 요청을 보냅니다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/505f60b5-3eae-4302-a181-ea543cda89ed/image.webp" alt="">
<strong>Webpack 의존 그래프 구조도</strong></p>
<ul>
<li><strong>상단(<code>bootstrap.main.ts</code>)</strong> 은 애플리케이션의 <strong>Entry Point</strong> 로, Webpack이 번들링을 시작하는 진입점입니다.</li>
<li><strong>중앙(<code>app.component.js</code>)</strong> 에서 여러 모듈(<code>external.lib.js</code>, <code>some.component.ts</code>)로 <strong>의존 관계(Dependency)</strong> 가 확장됩니다.</li>
<li>각 모듈은 또 다른 파일(<code>.css</code>, <code>.sass</code>, <code>.dep.js</code>)을 불러오며,
Webpack은 이 관계들을 모두 연결해 <strong>의존 그래프</strong>를 형성합니다.
👉 번들러는 이 그래프를 기반으로 ** 코드 분할(Code Splitting)** 이 필요한 지점을 자동으로 감지하고, 필요할 때만 해당 모듈을 로드하도록 청크를 생성합니다.</li>
</ul>
<hr>
<h2 id="✂️-react에서-code-splitting-적용하기">✂️ React에서 Code Splitting 적용하기</h2>
<h3 id="1️⃣-라우트-기반-분할">1️⃣ 라우트 기반 분할</h3>
<p>페이지 전환 시 필요한 컴포넌트만 불러오는 방법입니다.</p>
<pre><code>// routes/Dashboard.jsx
export default function Dashboard() {
  return &lt;h1&gt;📊 Dashboard&lt;/h1&gt;;
}

// App.jsx
import { Suspense, lazy } from &#39;react&#39;;
const Dashboard = lazy(() =&gt; import(&#39;./routes/Dashboard&#39;));
const Settings = lazy(() =&gt; import(&#39;./routes/Settings&#39;));

export default function App() {
  return (
    &lt;Suspense fallback={&lt;div&gt;로딩 중...&lt;/div&gt;}&gt;
      &lt;Router&gt;
        &lt;Route path=&quot;/dashboard&quot; element={&lt;Dashboard /&gt;} /&gt;
        &lt;Route path=&quot;/settings&quot; element={&lt;Settings /&gt;} /&gt;
      &lt;/Router&gt;
    &lt;/Suspense&gt;
  );
}</code></pre><ul>
<li><code>lazy()</code>는 <strong>동적 import를 감싸는 React 도우미</strong></li>
<li><code>Suspense</code>는 <strong>로딩 중 fallback UI</strong>를 보여주는 역할</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/4767662b-275c-48db-9f43-58545d831235/image.png" alt="">
<strong>Code Splitting 적용 전: 모든 스크립트가 한 번에 로드되는 모습</strong></p>
<ul>
<li>Chrome DevTools의 <strong>Network 탭</strong>에서 확인한 결과,페이지 진입 시 <code>index.js</code>, <code>main.js</code>, <code>_app.js</code>, <code>webpack.js</code> 등</li>
<li><em>모든 스크립트 파일이 동시에 요청*</em>되고 있습니다.</li>
<li>이런 구조에서는 사용자가 방문하지 않는 페이지의 코드까지 미리 로드되기 때문에 <strong>초기 로딩 시간이 길어지고,</strong> <strong>TTI(Time To Interactive) 도 지연</strong>됩니다.
👉 Code Splitting을 적용하면, 이러한 스크립트 중 <strong>실제로 필요한 청크만 먼저 로드</strong>되어 <strong>초기 성능이 크게 개선</strong>됩니다.</li>
</ul>
<hr>
<h3 id="2️⃣-컴포넌트-단위-분할">2️⃣ 컴포넌트 단위 분할</h3>
<p>큰 라이브러리(예: Chart.js, Editor 등)는 초기 번들에서 제외하고 사용 시점에만 로드하도록 분할합니다.</p>
<pre><code>const Chart = lazy(() =&gt; import(&#39;./components/HeavyChart&#39;));
...
&lt;Suspense fallback={&lt;Spinner /&gt;}&gt;
  &lt;Chart /&gt;
&lt;/Suspense&gt;</code></pre><p>👉 사용자가 “차트 영역”을 보기 전까지 관련 라이브러리(JS, CSS)는 네트워크 요청조차 발생하지 않습니다.</p>
<h2 id="✂️-분할-전략의-종류">✂️ 분할 전략의 종류</h2>
<table>
<thead>
<tr>
<th>전략</th>
<th>설명</th>
<th>대표 예시</th>
<th>장점</th>
<th>주의점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>라우트 기반(Route-level)</strong></td>
<td>페이지별 코드 분할</td>
<td><code>/home</code>, <code>/settings</code></td>
<td>간단, 가장 흔함</td>
<td>초기 라우트만 작음</td>
</tr>
<tr>
<td><strong>영역 기반(View-level)</strong></td>
<td>Fold 아래 섹션/모달 등 분할</td>
<td>탭, 드로어, 모달</td>
<td>UX 매끄러움</td>
<td>관리 복잡</td>
</tr>
<tr>
<td><strong>벤더 분리(Vendor Split)</strong></td>
<td>공통 라이브러리를 별도 청크로</td>
<td>React, Lodash 등</td>
<td>캐시 재사용 ↑</td>
<td>초기 청크 관리 필요</td>
</tr>
<tr>
<td><strong>조건 기반(Conditional)</strong></td>
<td>조건문 내 동적 import</td>
<td>특정 기능 on/off</td>
<td>메모리 효율</td>
<td>런타임 분기 주의</td>
</tr>
</tbody></table>
<hr>
<h2 id="✂️-code-splitting의-부작용과-주의점">✂️ Code Splitting의 부작용과 주의점</h2>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
<th>해결책</th>
</tr>
</thead>
<tbody><tr>
<td>⚠️ <strong>과도한 분할</strong></td>
<td>청크가 너무 많으면 HTTP 요청 증가</td>
<td>청크 병합 정책 / HTTP2 병렬 요청 활용</td>
</tr>
<tr>
<td>⚠️ <strong>초기 로딩 딜레이</strong></td>
<td>필요한 코드가 늦게 로드되어 사용자 대기</td>
<td><code>preload</code> / <code>prefetch</code>로 사전 요청</td>
</tr>
<tr>
<td>⚠️ <strong>중복 의존성 포함</strong></td>
<td>동일 라이브러리가 여러 청크에 중복 포함</td>
<td>번들러 <code>splitChunks.cacheGroups</code>로 통합</td>
</tr>
<tr>
<td>⚠️ <strong>캐싱 무효화</strong></td>
<td>자주 변경되는 코드가 캐시를 깨뜨림</td>
<td>콘텐츠 해시 기반 파일명(<code>.abc123.js</code>) 사용</td>
</tr>
</tbody></table>
<hr>
<h2 id="✂️-code-splitting--prefetch--preload">✂️ Code Splitting + Prefetch / Preload</h2>
<p>Code Splitting을 했더라도, <strong>사용자가 자주 갈 페이지</strong>라면 미리 당겨받을 수도 있습니다.</p>
<pre><code>&lt;link rel=&quot;prefetch&quot; href=&quot;/settings.chunk.js&quot; /&gt;
&lt;link rel=&quot;preload&quot; href=&quot;/main.chunk.js&quot; as=&quot;script&quot; /&gt;</code></pre><table>
<thead>
<tr>
<th>힌트</th>
<th>동작 시점</th>
<th>목적</th>
</tr>
</thead>
<tbody><tr>
<td><strong>prefetch</strong></td>
<td>브라우저가 유휴 시간에 미리 요청</td>
<td>“곧 쓸 리소스” 준비</td>
</tr>
<tr>
<td><strong>preload</strong></td>
<td>지금 바로 높은 우선순위로 요청</td>
<td>“당장 필요한 리소스” 확보</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/b9994456-81b3-4a80-b32d-3669762411d2/image.png" alt="">
<strong>Preload 적용 전후 렌더링 비교 (Financial Times 실험 예시)</strong></p>
<ul>
<li><strong>상단(1: before preload):</strong> Preload를 적용하기 전, 초기 화면이 2.5초까지 거의 비어 있다가 3.0초 이후에야 주요 콘텐츠가 렌더링되기 시작합니다.</li>
<li><strong>하단(2: with preload):</strong> Preload를 적용한 뒤, 핵심 리소스가 사전에 요청되어 초기 렌더링 시점이 1초 이상 빨라졌습니다.
👉 <strong>Preload는 “지금 필요한 리소스”를 미리 불러와</strong> 사용자에게 <strong>더 빠른 첫 화면(FCP, LCP)을 제공</strong>하는 효과를 냅니다.</li>
</ul>
<hr>
<h2 id="✂️-성능-검증-방법">✂️ 성능 검증 방법</h2>
<table>
<thead>
<tr>
<th>도구</th>
<th>확인 포인트</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Chrome DevTools → Network</strong></td>
<td>청크 로드 시점 / 요청 순서 확인</td>
</tr>
<tr>
<td><strong>Lighthouse</strong></td>
<td>“Reduce unused JavaScript” 개선 여부</td>
</tr>
<tr>
<td><strong>Coverage 탭</strong></td>
<td>미사용 코드 비율 감소 확인</td>
</tr>
<tr>
<td><strong>Bundle Analyzer</strong></td>
<td>청크 간 중복 및 크기 분포 시각화</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/2dde1a3d-e278-4eb9-9b6e-a1fd88061bbe/image.png" alt="">
<strong>Code Splitting 후의 번들 크기 시각화 (Webpack Bundle Analyzer)</strong></p>
<ul>
<li>이 트리맵은 <strong>번들 파일(bundle.js)</strong> 내부의 구성 요소를 시각적으로 분석한 결과입니다.</li>
<li><strong>왼쪽(node_modules) 영역</strong>에는 외부 라이브러리(<code>apexcharts</code>, <code>react-dom</code>, <code>react-slick</code> 등)가, <strong>오른쪽(src) 영역</strong>에는 애플리케이션 자체 코드(<code>index.tsx</code>, <code>tailwind.css</code>)가 포함되어 있습니다.</li>
<li>각 블록의 크기는 해당 모듈이 번들 내에서 차지하는 <strong>파일 크기 비중</strong>을 의미합니다.
👉 이처럼 Code Splitting을 적용하면, <strong>필요한 라이브러리나 기능 단위로 번들을 나눠서 관리</strong>할 수 있고, 과도하게 큰 모듈(<code>apexcharts.common.js</code> 등)을 분리하거나 Lazy Loading 대상으로 최적화할 수 있습니다.</li>
</ul>
<hr>
<h2 id="✂️-마무리">✂️ 마무리</h2>
<p>코드 분할은 단순히 파일을 나누는 기술이 아닙니다.
<strong>“사용자 경험을 기준으로 로딩 순서를 설계하는 전략”</strong>입니다.</p>
<p>초기 로딩이 가벼워질수록, 사용자는 앱이 즉시 반응한다고 느낍니다.
이것이 “빠름”의 심리적 체감 속도입니다.</p>
<blockquote>
<p>필요한 시점에 필요한 코드만 로드하라.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[💤 Lazy Loading]]></title>
            <link>https://velog.io/@hello-yujin/Lazy-Loading</link>
            <guid>https://velog.io/@hello-yujin/Lazy-Loading</guid>
            <pubDate>Wed, 12 Nov 2025 08:26:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>“보이는 순간만 로드한다” </p>
</blockquote>
<hr>
<h2 id="💤-들어가며">💤 들어가며</h2>
<p>웹페이지가 느린 이유는 단순히 코드가 많아서가 아닙니다.
초기 로딩 시 <strong>필요하지 않은 리소스</strong>까지 모두 불러오기 때문입니다.</p>
<p>이미지, 동영상, 외부 스크립트, 차트 라이브러리…
사용자가 아직 스크롤하지도 않았는데 모두 다운로드된다면,
네트워크는 <strong>‘필요 없는 일’을 먼저 처리</strong>하고 있는 셈이죠.</p>
<p>이 문제를 해결하는 것이 바로 <strong>Lazy Loading (지연 로딩)</strong> 입니다.
즉, <strong>“사용자가 실제로 볼 때만 리소스를 불러오는 기술”</strong>입니다.</p>
<hr>
<h2 id="💤-lazy-loading-개념">💤 Lazy Loading 개념</h2>
<p>Lazy Loading은 브라우저의 기본 작동 방식을 “지연시켜” <strong>필요한 순간에만 네트워크 요청을 발생</strong>시키는 전략입니다.</p>
<h3 id="즉-로딩-타이밍을-이렇게-바꿉니다">즉, 로딩 타이밍을 이렇게 바꿉니다.</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>기존 방식</th>
<th>Lazy Loading 방식</th>
</tr>
</thead>
<tbody><tr>
<td>로딩 시점</td>
<td>페이지 초기 로딩 시</td>
<td>뷰포트(화면)에 들어올 때</td>
</tr>
<tr>
<td>다운로드</td>
<td>전부 동시에</td>
<td>필요한 리소스만 순차적으로</td>
</tr>
<tr>
<td>사용자 경험</td>
<td>초기에 느림</td>
<td>초기에 가볍고 빠름</td>
</tr>
<tr>
<td>네트워크 부하</td>
<td>높음</td>
<td>분산됨</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/cc47f172-0a58-4a29-a470-2f5f8d531718/image.png" alt="">
<strong>Before vs After Lazy Loading</strong></p>
<ul>
<li><strong>왼쪽 (Traditional Loading)</strong>: 페이지가 열리자마자 모든 콘텐츠를 한꺼번에 로드 → 초기 로딩 속도 저하</li>
<li><strong>오른쪽 (Lazy Loading)</strong>: 사용자가 스크롤할 때마다 화면에 보이는 콘텐츠만 로드 → 초기 렌더링 시간 단축
👉 Lazy Loading은 <strong>“보이는 순간에만 필요한 데이터를 로드한다”</strong>는 원리로 작동하며, 페이지 로드 성능을 향상시키고 불필요한 네트워크 요청을 줄이는 대표적인 <strong>렌더링 최적화 기법</strong>입니다.</li>
</ul>
<hr>
<h2 id="💤-lazy-loading의-동작-원리">💤 Lazy Loading의 동작 원리</h2>
<h3 id="1️⃣-intersection-observer-api">1️⃣ Intersection Observer API</h3>
<p>브라우저가 특정 요소가 <strong>뷰포트에 진입했는지 감시</strong>하는 기능입니다.
이때 감시 대상이 화면에 등장하면 <strong>이미지 로드 요청</strong>을 트리거합니다.</p>
<pre><code>useEffect(() =&gt; {
  const observer = new IntersectionObserver((entries) =&gt; {
    entries.forEach((entry) =&gt; {
      if (entry.isIntersecting) {
        entry.target.src = entry.target.dataset.src; // 진입 시 이미지 로드
        observer.unobserve(entry.target);
      }
    });
  });

  document.querySelectorAll(&quot;img[data-src]&quot;).forEach((img) =&gt; observer.observe(img));

  return () =&gt; observer.disconnect();
}, []);</code></pre><ul>
<li><code>isIntersecting</code>: 해당 요소가 뷰포트 안으로 들어왔는가</li>
<li><code>observer.unobserve</code>: 한 번 로드된 요소는 다시 감시하지 않음</li>
<li><code>data-src</code>: 아직 로드하지 않은 이미지 URL을 임시로 보관</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/b48ea96e-54e0-48ce-8847-9afd928f475f/image.png" alt="">
<strong>Intersection Observer 작동 원리</strong></p>
<ul>
<li>파란색 <code>root</code>는 브라우저의 Viewport이며, 감시 기준이 되는 영역입니다.</li>
<li><code>true</code>는 현재 Viewport 안에 있는 요소, <code>false</code>는 화면 밖에 있는 요소를 의미합니다.</li>
<li>스크롤 시 요소가 <code>root</code> 경계선에 도달하면 <strong>Intersection Observer</strong>가 이를 감지하여 Lazy Loading, Infinite Scroll, 애니메이션 실행 등의 로직을 자동으로 수행합니다.</li>
</ul>
<hr>
<h3 id="2️⃣-브라우저-기본-속성-loadinglazy">2️⃣ 브라우저 기본 속성 <code>loading=&quot;lazy&quot;</code></h3>
<p>최근 대부분의 브라우저(Chrome, Edge, Firefox)는 HTML 속성 하나로 기본 Lazy Loading을 지원합니다.</p>
<pre><code>&lt;img src=&quot;photo.jpg&quot; loading=&quot;lazy&quot; alt=&quot;sample image&quot; /&gt;</code></pre><ul>
<li><strong>지원 브라우저</strong>: Chrome 76+, Edge 79+, Firefox 75+</li>
<li><strong>비지원 브라우저(사파리 등)</strong>는 JS 폴리필(IntersectionObserver)을 함께 사용</li>
</ul>
<p>이 속성 하나로 이미지 로딩 타이밍을 자동으로 최적화할 수 있습니다.
Chrome은 기본적으로 뷰포트의 <strong>약 1250px 근처</strong>에 진입하면 요청을 시작합니다.</p>
<blockquote>
<p>🧭 Tip:
<code>loading=&quot;lazy&quot;</code>는 <code>&lt;iframe&gt;</code> 태그에도 적용됩니다.
→ YouTube, Map, 광고 등의 embed 콘텐츠를 늦게 로드할 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="rootmargin과-사전-로딩-거리"><code>rootMargin</code>과 사전 로딩 거리</h3>
<pre><code>const observer = new IntersectionObserver(callback, {
  rootMargin: &quot;200px&quot;,
});</code></pre><ul>
<li><code>rootMargin</code>은 화면 경계선 앞뒤 여유 거리입니다.</li>
<li><code>200px</code>로 설정하면, 이미지가 화면에 <strong>200px 전</strong>에 미리 로드됩니다.</li>
<li>지나치게 작으면 이미지가 깜빡이며 로드되고,너무 크면 Lazy Loading의 효과가 줄어듭니다.</li>
</ul>
<blockquote>
<p>🧭 추천 값: <code>200px ~ 400px</code> (사용자 스크롤 속도에 따라 조정)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/2d1e0783-b06d-4cfe-b1bb-878b2cfb9405/image.png" alt="">
<strong>Intersection Observer 속성 개념도</strong></p>
<ul>
<li><strong>왼쪽의 Root:</strong> 브라우저의 Viewport(감시 기준 영역)를 의미하며,</li>
<li><strong>가운데의 Root Margin:</strong> Viewport 바깥쪽 여유 영역으로, 미리 로드를 유도해 화면 깜빡임을 줄입니다.</li>
<li><strong>오른쪽의 Threshold:</strong> 요소가 화면에 얼마나 보여야 콜백이 실행될지를 결정합니다.
👉 적절한 <code>rootMargin</code>과 <code>threshold</code> 설정은 스크롤 시 더 부드러운 사용자 경험을 제공합니다.</li>
</ul>
<hr>
<h2 id="💤-lazy-loading-적용-예시">💤 Lazy Loading 적용 예시</h2>
<pre><code>import { useEffect, useRef, useState } from &quot;react&quot;;

function LazyImage({ src, alt }) {
  const [isVisible, setVisible] = useState(false);
  const imgRef = useRef(null);

  useEffect(() =&gt; {
    const io = new IntersectionObserver(([entry]) =&gt; {
      if (entry.isIntersecting) setVisible(true);
    }, { rootMargin: &quot;300px&quot; });

    if (imgRef.current) io.observe(imgRef.current);
    return () =&gt; io.disconnect();
  }, []);

  return (
    &lt;img
      ref={imgRef}
      src={isVisible ? src : &quot;/placeholder.jpg&quot;}
      alt={alt}
      loading=&quot;lazy&quot;
      style={{ width: &quot;100%&quot;, borderRadius: &quot;8px&quot; }}
    /&gt;
  );
}

export default function Gallery() {
  return (
    &lt;section&gt;
      &lt;h2&gt;Lazy Loaded Gallery&lt;/h2&gt;
      &lt;div className=&quot;grid&quot;&gt;
        {Array.from({ length: 12 }, (_, i) =&gt; (
          &lt;LazyImage
            key={i}
            src={`/images/photo-${i + 1}.jpg`}
            alt={`photo-${i}`}
          /&gt;
        ))}
      &lt;/div&gt;
    &lt;/section&gt;
  );
}</code></pre><p><img src="https://velog.velcdn.com/images/hello-yujin/post/e022e29e-4565-439a-9017-3f466d43e8cb/image.jpg" alt=""></p>
<p><strong>Lazy Loading 동작 예시</strong></p>
<ul>
<li><strong>왼쪽:</strong> 스크롤 전 상태로, 아직 실제 이미지가 로드되지 않아 저해상도 <strong>Placeholder(블러 처리 이미지)</strong>가 보입니다.</li>
<li><strong>오른쪽:</strong> 사용자가 스크롤해 해당 위치에 도달했을 때, 실제 고해상도 이미지가 로드되어 자연스럽게 전환된 모습입니다.
👉 Lazy Loading은 이런 방식으로 불필요한 초기 로딩을 줄이고
스크롤에 맞춰 콘텐츠를 점진적으로 표시합니다.</li>
</ul>
<hr>
<h2 id="💤-lazy-loading의-한계와-주의점">💤 Lazy Loading의 한계와 주의점</h2>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
<th>해결책</th>
</tr>
</thead>
<tbody><tr>
<td>⚠️ <strong>LCP 지연</strong></td>
<td>히어로 이미지(LCP 후보)를 Lazy 처리하면 화면 표시가 늦어짐</td>
<td>LCP 이미지는 <code>loading=&quot;eager&quot;</code> 또는 <code>fetchpriority=&quot;high&quot;</code></td>
</tr>
<tr>
<td>⚠️ <strong>SEO 문제</strong></td>
<td>구글봇이 JS를 실행하지 않으면 Lazy 이미지 인식 불가</td>
<td>SSR 또는 <code>&lt;noscript&gt;</code> 폴백 이미지 추가</td>
</tr>
<tr>
<td>⚠️ <strong>깜빡임</strong></td>
<td>rootMargin 너무 작을 경우</td>
<td>200~400px로 완화</td>
</tr>
<tr>
<td>⚠️ <strong>레이아웃 흔들림</strong></td>
<td>이미지 높이가 지정되지 않으면 CLS 발생</td>
<td><code>width</code>, <code>height</code>, 또는 <code>aspect-ratio</code> 지정</td>
</tr>
<tr>
<td>⚠️ <strong>저사양 기기</strong></td>
<td>스크롤 이벤트/관찰자 수가 많으면 CPU 점유</td>
<td>IntersectionObserver 재사용으로 감시 수 최소화</td>
</tr>
</tbody></table>
<hr>
<h2 id="💤-lazy-loading-적용-후-측정-방법">💤 Lazy Loading 적용 후 측정 방법</h2>
<table>
<thead>
<tr>
<th>도구</th>
<th>확인할 항목</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Chrome DevTools → Network</strong></td>
<td>이미지 요청이 스크롤 시점에 발생하는지</td>
</tr>
<tr>
<td><strong>Performance 탭</strong></td>
<td>스크롤 중 FPS 유지, Paint 최소화</td>
</tr>
<tr>
<td><strong>Lighthouse</strong></td>
<td>“Defer offscreen images” 개선 확인</td>
</tr>
<tr>
<td><strong>WebPageTest / GTMetrix</strong></td>
<td>Time to Interactive, Total Bytes 감소</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/1574808d-f888-4f6f-a1f1-6dc03c732024/image.png" alt="">
<strong>Lazy Loading 성능 비교 그래프</strong></p>
<ul>
<li><strong>그래프 설명:</strong>
WordPress 사이트를 기준으로, <strong>네이티브 이미지 Lazy Loading</strong>을 적용했을 때와 그렇지 않을 때의 <strong>LCP(Largest Contentful Paint, 핵심 콘텐츠가 렌더링되기까지의 시간)</strong>을 비교한 결과입니다.</li>
<li><strong>좌측(No):</strong> Lazy Loading 미적용 시, 브라우저가 모든 이미지를 한꺼번에 로드해 <strong>LCP가 더 길게 측정</strong>되었습니다.</li>
<li><strong>우측(Yes):</strong> Lazy Loading 적용 시, <strong>초기 렌더링에 필요한 이미지만 우선 로드</strong>되어 <strong>LCP가 단축</strong>되었습니다.
👉 <strong>Lazy Loading은 페이지 초기 로딩 성능을 향상시키고, 사용자 체감 속도를 개선하는 데 효과적</strong>입니다.</li>
</ul>
<hr>
<h2 id="💤-마치며">💤 마치며</h2>
<p>Lazy Loading은 “기술적으로 어렵지 않은데, 가장 효과가 큰 최적화”입니다.
이미지 한 장만 줄여도 LCP가 절반 가까이 개선되는 경우도 있습니다.</p>
<p>가장 중요한 원칙은 단 하나입니다.</p>
<blockquote>
<p><strong>“사용자가 실제로 보는 순간에만 로드하라.”</strong></p>
</blockquote>
<p>필요한 시점에 필요한 리소스를 보내면, 사용자 경험은 자연스럽게 부드럽고 빠른 서비스로 바뀝니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💭 데이터 리스트 가상화 (Virtualization)]]></title>
            <link>https://velog.io/@hello-yujin/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%83%81%ED%99%94-Virtualization</link>
            <guid>https://velog.io/@hello-yujin/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%83%81%ED%99%94-Virtualization</guid>
            <pubDate>Wed, 12 Nov 2025 08:12:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>수천 개의 데이터를 한 화면에 “한 번에” 렌더링하지 않는 이유</p>
</blockquote>
<hr>
<h2 id="💭-들어가며">💭 들어가며</h2>
<p>웹 서비스가 커질수록 “리스트”는 폭발적으로 늘어납니다.
상품 목록, 알림함, 채팅 메시지, 피드, 로그 — 모두 리스트 형태입니다.</p>
<p>그런데 리스트 아이템이 1,000개만 넘어도 브라우저는 숨이 찹니다.
<strong>스크롤이 버벅이고, 메모리 사용량이 치솟고, TTI(대화 가능 시점)</strong>은 지연됩니다.</p>
<p>이때 등장하는 것이 바로 <strong>데이터 리스트 가상화(Data List Virtualization)</strong>입니다.
한마디로 말하면 <strong>“보이는 만큼만 렌더링한다”</strong>는 기술이죠.</p>
<hr>
<h2 id="💭-렌더링은-보이는-것보다-더-많다">💭 렌더링은 ‘보이는 것보다 더 많다’</h2>
<p>일반적인 리스트 컴포넌트는 이렇게 동작합니다.</p>
<pre><code>function List({ items }) {
  return (
    &lt;div&gt;
      {items.map((item) =&gt; (
        &lt;div className=&quot;row&quot; key={item.id}&gt;
          {item.title}
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
}</code></pre><p>데이터가 1,000개라면 <code>&lt;div&gt;</code>가 1,000개 생성됩니다.
하지만 <strong>실제로 사용자가 한눈에 볼 수 있는 항목은 10~15개</strong>에 불과하죠.</p>
<p>이런 상황에서 브라우저는 다음과 같은 일을 합니다:</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>비용</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Layout</td>
<td>높이·폭 계산</td>
<td>모든 1,000개 DOM 요소의 레이아웃 계산</td>
</tr>
<tr>
<td>Paint</td>
<td>픽셀 그리기</td>
<td>보이지 않는 영역도 페인트 큐에 포함</td>
</tr>
<tr>
<td>Memory</td>
<td>유지비용</td>
<td>각 노드의 참조, 스타일, 이벤트 리스너 유지</td>
</tr>
<tr>
<td>JS Reflow</td>
<td>연쇄효과</td>
<td>스크롤 시 매번 스타일·레이아웃 재계산</td>
</tr>
</tbody></table>
<p>결과적으로 <strong>스크롤이 버벅</strong>이고,
모바일에서는 <strong>렌더링 중단(Frozen Frame)</strong>이 발생하기도 합니다.</p>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/e946954f-8fbe-4520-9c5f-19aece29abb9/image.png" alt=""></p>
<ul>
<li><strong>DOM</strong> 노드 증가에 따라 <strong>JS Heap</strong>과 <strong>FPS</strong>가 주기적으로 상승·하락</li>
<li>렌더링 부하가 누적되며 브라우저가 <strong>Reflow/Repaint</strong>를 반복하는 패턴 확인
👉 <strong>“DOM이 많아질수록 브라우저의 렌더링 비용이 급격히 증가한다”</strong></li>
</ul>
<hr>
<h2 id="💭-가상화virtualization란-무엇인가">💭 “가상화(Virtualization)”란 무엇인가?</h2>
<p>데이터 리스트 가상화는 다음 한 문장으로 요약됩니다.</p>
<blockquote>
<p><strong>“보이는 영역(뷰포트) + 주변 버퍼만 렌더링하고, 나머지는 가짜 높이로 대체한다.”</strong></p>
</blockquote>
<p>즉, 실제 DOM에는 <strong>일부만 존재</strong>하지만,
브라우저는 전체 스크롤 높이를 <strong>가상으로 계산</strong>해 마치 전부 있는 것처럼 보여줍니다.</p>
<h3 id="동작-개념">동작 개념</h3>
<table>
<thead>
<tr>
<th>스크롤 상태</th>
<th>실제 DOM 구성</th>
</tr>
</thead>
<tbody><tr>
<td>초기</td>
<td>index 0~15 항목만 렌더링</td>
</tr>
<tr>
<td>아래로 스크롤</td>
<td>index 10~25 항목만 DOM 유지, 나머지는 제거</td>
</tr>
<tr>
<td>끝까지 스크롤</td>
<td>index 985~1000 항목만 남음</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/42dee78e-6ec6-4b7e-afee-71ce7ff94153/image.png" alt=""></p>
<ul>
<li><strong>왼쪽(Regular Scrolling)</strong>: 리스트의 모든 아이템(item 998~item 1006)이 실제 DOM에 렌더링되어 있어, 항목이 많아질수록 레이아웃 계산과 페인트 비용이 급증합니다.</li>
<li><strong>오른쪽(Virtualized Scrolling)</strong>: 현재 화면(visible viewport) 안의 항목(item 1000~item 1005)만 실제 DOM에 존재하며, 위아래 항목은 존재하지 않거나 가상의 패딩 공간으로 대체됩니다.
👉 <strong>“보이는 부분만 렌더링”</strong>함으로써 브라우저의 렌더링 부하를 크게 줄이고 FPS를 안정적으로 유지할 수 있습니다.</li>
</ul>
<hr>
<h2 id="💭-브라우저는-이렇게-처리한다-작동-원리">💭 브라우저는 이렇게 처리한다 (작동 원리)</h2>
<p>리스트 가상화는 세 가지 핵심 원리로 작동합니다.</p>
<p><strong>① Viewport 계산</strong></p>
<ul>
<li>현재 스크롤 위치(<code>scrollTop</code>)와 화면 높이(<code>clientHeight</code>)를 이용해
“지금 보여줘야 할 항목의 인덱스 범위”를 계산합니다.</li>
<li>예: <code>scrollTop=500</code>이면 <code>startIndex=10</code>, <code>endIndex=25</code> 정도로 계산</li>
</ul>
<p><strong>② 가짜 컨테이너 높이 유지</strong></p>
<ul>
<li>실제 렌더링된 항목 외에도,
리스트 전체 높이를 유지하기 위해 상단/하단에 <strong>padding element</strong>(빈 div)를 둡니다.</li>
<li>이렇게 하면 <strong>스크롤바 길이</strong>는 실제 전체 데이터와 동일하게 보입니다.</li>
</ul>
<p><strong>③ 항목 재활용(Recycling)</strong></p>
<ul>
<li>스크롤할 때마다 DOM을 새로 만들지 않고, 기존 노드의 <strong>위치와 내용만 교체</strong>합니다.</li>
<li>즉, DOM 수는 일정하게 유지되며 스크롤만 가상적으로 “이동”합니다.
<img src="https://velog.velcdn.com/images/hello-yujin/post/69ede3bd-07a1-409f-8107-753d3e296552/image.jpeg" alt=""></li>
<li><em>브라우저 렌더링 파이프라인 (Critical Rendering Path)*</em></li>
<li><strong>Network 단계</strong>: HTML, CSS, JS 파일이 다운로드됨</li>
<li><strong>DOM &amp; CSSOM 생성</strong>: HTML → DOM, CSS → CSSOM 구조로 파싱</li>
<li><strong>Render Tree 결합</strong>: 브라우저가 어떤 요소를 어떤 스타일로 그릴지 결정</li>
<li><strong>Layout</strong>: 각 요소의 크기와 위치를 계산</li>
<li><strong>Paint</strong>: 픽셀 단위로 화면에 실제로 그려짐
👉 Virtualization은 이 중 <strong>Layout → Paint</strong> 구간에서의 작업량을 줄여, 브라우저가 모든 리스트 아이템을 매번 다시 계산하지 않도록 하여 <strong>스크롤 성능을 향상시키는 핵심 최적화 방식</strong>입니다.</li>
</ul>
<hr>
<h2 id="💭-구현-방식-비교">💭 구현 방식 비교</h2>
<table>
<thead>
<tr>
<th>방식</th>
<th>설명</th>
<th>대표 라이브러리</th>
<th>장점</th>
<th>주의사항</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Fixed Height</strong></td>
<td>항목의 높이가 일정</td>
<td><code>react-window</code></td>
<td>계산 단순, 빠름</td>
<td>동적 높이 불가</td>
</tr>
<tr>
<td><strong>Dynamic Height (Measured)</strong></td>
<td>높이를 측정/캐싱</td>
<td><code>react-virtualized</code></td>
<td>다양한 UI 지원</td>
<td>구현 복잡</td>
</tr>
<tr>
<td><strong>Infinite Scroll + Virtualization</strong></td>
<td>비동기 데이터 로딩 병합</td>
<td><code>react-virtualized</code> + fetch</td>
<td>UX 부드러움</td>
<td>경계 관리 필요</td>
</tr>
</tbody></table>
<hr>
<h2 id="💭-렌더링-병목-구간-예시">💭 렌더링 병목 구간 예시</h2>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/44aae9d2-7753-457e-b9f9-34fc214c57fa/image.png" alt="">
<strong>Virtualization 적용 전 렌더링 병목 구간 (DevTools Performance Timeline)</strong></p>
<ul>
<li><strong>노란색(Rendering)</strong>: Layout 및 스타일 재계산(Recalculate Style) 단계 — DOM이 많을수록 빈번히 발생</li>
<li><strong>빨간색(Painting)</strong>: 픽셀을 다시 그리는 과정(Repaint) — 스크롤이나 화면 갱신 시 자주 호출</li>
<li><strong>보라색(Scripting)</strong>: JavaScript 로직 실행
👉 Rendering과 Painting이 촘촘히 반복되는 것은 <strong>브라우저가 매 프레임마다 화면 전체를 다시 그리는 비효율적인 상태</strong>임을 의미합니다.
👉 Virtualization을 적용하면 이 과정 중 불필요한 Layout/Paint 호출이 제거되어, <strong>렌더링 부하가 줄고 FPS가 안정적으로 유지</strong>됩니다.</li>
</ul>
<hr>
<h2 id="💭-장단점">💭 장단점</h2>
<p>| 구분    |<br>| ----- | -------------------------------------------------------------------------------- 
| ✅ 장점  | - DOM 렌더링 최소화<br>- 메모리 사용량 감소<br>- 모바일 스크롤 프레임 유지<br>- 초기 로드 속도 향상               |    |
| ⚠️ 단점 | - SEO(검색엔진)가 전체 데이터 접근 불가<br>- 키보드 포커스 이동 시 항목 언마운트 문제<br>- 동적 높이 항목에서 위치 계산 어려움 |    |</p>
<blockquote>
<p>🧭 Tip:
가상화는 <strong>“렌더링 성능 최적화”</strong>이지, 데이터 요청 최적화는 아닙니다.
API 호출은 별도로 페이지네이션·캐싱과 함께 관리해야 합니다.</p>
</blockquote>
<hr>
<h2 id="💭-실제-적용-시-고려-사항">💭 실제 적용 시 고려 사항</h2>
<p><strong>1. 항목 높이</strong></p>
<ul>
<li>고정 높이면 <code>react-window</code></li>
<li>유동 높이면 <code>react-virtualized</code></li>
<li>가변 콘텐츠는 <code>CellMeasurer</code> 활용</li>
</ul>
<p><strong>2. 스크롤 컨테이너 구조</strong></p>
<ul>
<li>반드시 <code>overflow: auto</code> 또는 <code>scroll</code>이 지정된 부모 필요</li>
<li>상하 padding element로 스크롤 높이 보정</li>
</ul>
<p><strong>3. A11y (접근성)</strong></p>
<ul>
<li>보이지 않는 항목은 스크린리더 탐색 불가</li>
<li>가상화 시 <code>aria-live</code> 영역으로 시각적/논리적 순서 보정</li>
</ul>
<p><strong>4. SEO</strong></p>
<ul>
<li>검색엔진 크롤러는 JS 실행 없이 HTML만 읽을 수 있음 → SSR 또는 CSR 하이브리드 전략 필요</li>
</ul>
<p><strong>5. 테스트 포인트</strong></p>
<ul>
<li>스크롤 시 CPU 점유율</li>
<li>Reflow/Repaint 횟수</li>
<li>Long task 발생 여부</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/ab38aad1-46f6-4643-a096-0c0af7efd359/image.jpeg" alt="">
<strong>Lazy Loading 동작 시각화</strong></p>
<ul>
<li><strong>왼쪽:</strong> 여러 개의 <code>600×600</code> 이미지 블록이 나열되어 있으며, 화면 스크롤에 따라 새로운 이미지가 등장함</li>
<li><strong>오른쪽:</strong> Chrome DevTools의 Network 탭에서,
스크롤 시점마다 새 이미지 요청(<code>.jpg</code>, <code>.png</code>)이 발생하는 것을 확인할 수 있음
👉 Lazy Loading은 <strong>뷰포트 안에 들어온 이미지만</strong> 네트워크 요청을 보내는 방식으로, 페이지 초기 렌더링 속도를 개선하고 트래픽을 절감하는 대표적인 프론트엔드 최적화 기법입니다.</li>
</ul>
<hr>
<h2 id="💭-virtualization이-특히-효과적인-경우">💭 Virtualization이 특히 효과적인 경우</h2>
<table>
<thead>
<tr>
<th>케이스</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>쇼핑몰 상품 목록</td>
<td>이미지·텍스트 혼합, DOM 수 많음</td>
</tr>
<tr>
<td>SNS 피드</td>
<td>사용자 스크롤 중심 UI</td>
</tr>
<tr>
<td>데이터 테이블</td>
<td>고정 높이 행 구조</td>
</tr>
<tr>
<td>로그/리스트뷰</td>
<td>실시간 업데이트 발생</td>
</tr>
</tbody></table>
<hr>
<h2 id="💭-virtualization과-함께-쓰면-좋은-기술">💭 Virtualization과 함께 쓰면 좋은 기술</h2>
<table>
<thead>
<tr>
<th>기술</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Lazy Loading</strong></td>
<td>이미지/비디오 로드를 스크롤 진입 시점으로 지연</td>
</tr>
<tr>
<td><strong>Code Splitting</strong></td>
<td>스크롤 깊은 구간에 필요한 모듈을 나중에 로드</td>
</tr>
<tr>
<td><strong>Memoization</strong></td>
<td>스크롤 시 불필요한 재렌더링 방지</td>
</tr>
<tr>
<td><strong>IntersectionObserver</strong></td>
<td>뷰포트 진입 감지 (페이징 로드용)</td>
</tr>
</tbody></table>
<hr>
<h2 id="💭-마무리">💭 마무리</h2>
<p>데이터 리스트 가상화는 브라우저 성능의 “생명줄”입니다.
DOM이 1만 개든, 10만 개든, 사용자가 보는 건 한눈에 보이는 몇십 개뿐이죠.</p>
<p>Virtualization은 <strong>사용자가 체감하는 부드러움</strong>을 위해
렌더링 리소스를 “지금 필요한 부분”에만 집중시키는 기술입니다.</p>
<p>즉, 이것은 단순한 렌더링 트릭이 아니라
<strong>UX 중심 성능 설계(Performance-Driven UX)</strong>의 핵심입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 프로젝트에서 Hook을 어떻게 설계할까?]]></title>
            <link>https://velog.io/@hello-yujin/React-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Hook%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%84%A4%EA%B3%84%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@hello-yujin/React-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Hook%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%84%A4%EA%B3%84%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 11 Nov 2025 11:29:27 GMT</pubDate>
            <description><![CDATA[<p><strong>— 커스텀 훅을 기능이 아닌 ‘책임 단위’로 나누는 기준</strong>
React Hook은 단순히 코드를 줄이기 위한 문법이 아니다.
프로젝트가 커질수록 <strong>상태 관리, 로직 재사용, 데이터 흐름 제어</strong>의 중심이 된다.
이번 글에서는 “Hook을 언제, 어떤 기준으로 나누면 좋은가?”에 대해 공부한 내용을 정리해보았다.</p>
<hr>
<h2 id="왜-hook을-쓰는가--로직-재사용보다-관심사-분리">왜 Hook을 쓰는가 — 로직 재사용보다 “관심사 분리”</h2>
<p>처음엔 useState, useEffect로 충분하다.
하지만 컴포넌트가 커지면 이런 현상이 생긴다 👇</p>
<ul>
<li>UI 로직과 비즈니스 로직이 섞여 가독성이 떨어짐</li>
<li>상태 변화가 많아 디버깅이 어려워짐</li>
<li>비슷한 로직을 여러 컴포넌트에서 반복</li>
</ul>
<p>이때 <strong>커스텀 훅(Custom Hook)</strong> 은 “로직을 옮겨놓는 함수”가 아니라,
<strong>컴포넌트가 가져야 할 책임을 분리하고 독립시키는 도구</strong>로 보는 게 중요하다.</p>
<blockquote>
<p>“Hook은 코드를 분리하는 수단이 아니라, <strong>책임을 분리하는 수단</strong>이다.”</p>
</blockquote>
<hr>
<h2 id="usestate--useeffect로는-충분하지-않을-때">useState / useEffect로는 충분하지 않을 때</h2>
<p>React의 초반 단계에서는 대부분 이렇게 작성한다.</p>
<pre><code>function Example() {
  const [count, setCount] = useState(0);
  useEffect(() =&gt; {
    console.log(&quot;Count changed:&quot;, count);
  }, [count]);
}</code></pre><p>문제는, 기능이 늘어날수록 <code>useEffect</code>가 점점 복잡해지고 *<em>“의존성 지옥” *</em>이 시작된다는 것이다.</p>
<pre><code>useEffect(() =&gt; {
  fetchData();
  handleResize();
  handleAuthCheck();
}, [token, width, isLoggedIn]);</code></pre><p>각 로직이 서로 영향을 주고받으며 <strong>렌더링 타이밍, 비동기 처리, 상태 동기화 문제</strong>가 꼬이기 시작한다.
이 시점이 바로 커스텀 훅을 도입해야 할 타이밍이다.</p>
<hr>
<h2 id="커스텀-훅으로-분리하는-기준">커스텀 훅으로 분리하는 기준</h2>
<p>많은 개발자들이 처음엔 <code>useLogin</code>, <code>useFetch</code>, <code>useForm</code> 처럼“기능 단위”로 훅을 만든다.
하지만 진짜 좋은 분리는 <strong>‘하나의 책임(Responsibility)’에 집중된 훅</strong>이다.</p>
<table>
<thead>
<tr>
<th>잘못된 기준</th>
<th>올바른 기준</th>
</tr>
</thead>
<tbody><tr>
<td>비슷한 코드가 반복되니까 묶는다</td>
<td>하나의 논리적 책임을 가진다</td>
</tr>
<tr>
<td>여러 사이드이펙트를 한 곳에 몰아넣는다</td>
<td>각 훅은 오직 하나의 역할만 수행한다</td>
</tr>
<tr>
<td>훅 내부에서 또 다른 훅을 호출</td>
<td>상위 훅이 하위 훅을 조립해 데이터 흐름을 제어</td>
</tr>
</tbody></table>
<p>예를 들어 <code>useFetch</code>라는 훅은 <strong>“데이터 요청 + 에러 핸들링 + 로딩 상태”</strong>라는 하나의 책임에 집중되어 있다면 괜찮다.
하지만 여기에 “토큰 갱신”이나 “자동 재시도”까지 들어간다면 이미 역할이 넘친 상태다.</p>
<hr>
<h2 id="훅-간-의존성-관리--controller-패턴">훅 간 의존성 관리 — Controller 패턴</h2>
<p>커스텀 훅을 많이 만들다 보면 이런 문제가 생긴다.</p>
<blockquote>
<p>“A 훅 안에서 B 훅을 직접 호출하면 안 되나?”</p>
</blockquote>
<p>가능은 하지만, <strong>훅 간 결합도가 올라가고 재사용성이 떨어진다.</strong></p>
<p>이럴 때 효과적인 방법은 <strong>Controller 훅 패턴</strong>이다.
즉, 여러 하위 훅을 조립하고 상위 레벨에서 데이터 흐름을 제어하는 방식이다.</p>
<pre><code>function useController() {
  const { data, error, refetch } = useFetch(&quot;/api/data&quot;);
  const { isOpen, toggle } = useModal();
  const { value, onChange } = useFormField(&quot;&quot;);

  // 데이터 흐름 조립
  useEffect(() =&gt; {
    if (error) toggle(true); // 에러 시 모달 오픈
  }, [error]);

  return { data, error, isOpen, toggle, value, onChange, refetch };
}</code></pre><p>이렇게 하면 각각의 훅은 <strong>하나의 책임</strong>에 집중하고,
Controller 훅은 <strong>전체 로직을 조율하는 역할만</strong> 맡게 된다.</p>
<hr>
<h2 id="hook-구조-설계-패턴-3가지">Hook 구조 설계 패턴 3가지</h2>
<p>프로젝트 단위에서 커스텀 훅을 설계할 때는 <strong>3단계 구조</strong>로 나누면 깔끔하다.</p>
<h3 id="①-controller-hook">① Controller Hook</h3>
<p>여러 훅을 조립해 데이터 흐름을 제어</p>
<blockquote>
<p>예: <code>useDashboard</code>, <code>useAuthFlow</code></p>
</blockquote>
<ul>
<li>책임: 전체 로직 통합, 의존성 조율</li>
<li>장점: 상위 로직이 한눈에 들어옴</li>
<li>단점: 커지면 또 비대해질 수 있음</li>
</ul>
<h3 id="②-state-hook">② State Hook</h3>
<p>특정 도메인의 상태를 캡슐화</p>
<blockquote>
<p>예: <code>useUserState</code>, <code>useThemeState</code></p>
</blockquote>
<ul>
<li>책임: 상태와 업데이트 로직 관리</li>
<li>장점: 독립적인 상태 관리 가능</li>
<li>단점: 전역 남용 시 데이터 추적 어려움</li>
</ul>
<h3 id="③-utility-hook">③ Utility Hook</h3>
<p>순수 기능만 수행, 사이드이펙트 없음</p>
<blockquote>
<p>예: <code>useInterval</code>, <code>usePrevious</code>, <code>useMediaQuery</code></p>
</blockquote>
<ul>
<li>책임: 보조 로직 제공</li>
<li>장점: 재사용성 높음, 테스트 쉬움</li>
<li>단점: 너무 세분화하면 관리 복잡<blockquote>
<p>💡 패턴 정리:
“Controller → State / Utility” 구조로 나누면 데이터 흐름이 명확하고, 유지보수가 쉬워진다.</p>
</blockquote>
</li>
</ul>
<hr>
<h2 id="훅의-이름은-역할을-드러내야-한다">훅의 이름은 역할을 드러내야 한다</h2>
<p>좋은 훅은 이름만 봐도 무슨 일을 하는지 알 수 있다.
나쁜 훅은 “이 훅이 뭘 하는지 직접 열어봐야” 알 수 있다.</p>
<pre><code>// ❌ 나쁜 예시
useCommonLogic();
useSomething();

// ✅ 좋은 예시
useModalVisibility();
useAuthVerification();
useKeyboardShortcut();</code></pre><p>“무엇을 하는가?”가 아니라 “왜 존재하는가?”를 드러내는 네이밍이 좋다.</p>
<hr>
<h2 id="hook을-설계할-때-자주-하는-실수">Hook을 설계할 때 자주 하는 실수</h2>
<table>
<thead>
<tr>
<th>실수</th>
<th>문제점</th>
</tr>
</thead>
<tbody><tr>
<td>훅 안에서 상태를 너무 많이 관리</td>
<td>책임 불분명, 테스트 어려움</td>
</tr>
<tr>
<td>훅 안에서 훅을 직접 호출</td>
<td>결합도 증가, 순환참조 위험</td>
</tr>
<tr>
<td>의존성 배열을 무시하거나 남발</td>
<td>렌더링 타이밍 예측 불가</td>
</tr>
<tr>
<td>커스텀 훅 내부에서 DOM 조작</td>
<td>React의 선언형 철학과 충돌</td>
</tr>
</tbody></table>
<hr>
<h2 id="결론-훅은-로직-재사용보다-데이터-흐름을-명시화하는-도구">결론: 훅은 로직 재사용보다 데이터 흐름을 명시화하는 도구</h2>
<p>결국 커스텀 훅의 목적은 “로직을 옮기는 것”이 아니라,
<strong>상태 → 로직 → UI</strong>의 흐름을 명확히 보여주는 것이다.</p>
<blockquote>
<p>Hook은 기술이 아니라 <strong>사고방식</strong>이다.
컴포넌트를 “UI 중심”이 아니라 “데이터 흐름 중심”으로 바라보게 만들어준다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 심화 전 꼭 알아야 할 JS 개념 4가지]]></title>
            <link>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%8B%AC%ED%99%94-%EC%A0%84-%EA%BC%AD-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-JS-%EA%B0%9C%EB%85%90-4%EA%B0%80%EC%A7%80</link>
            <guid>https://velog.io/@hello-yujin/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%8B%AC%ED%99%94-%EC%A0%84-%EA%BC%AD-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-JS-%EA%B0%9C%EB%85%90-4%EA%B0%80%EC%A7%80</guid>
            <pubDate>Sat, 08 Nov 2025 00:13:38 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>클로저 · 싱글 스레드 · 이벤트 루프 · 렌더링 프로세스</p>
</blockquote>
<p>리액트 심화 개념(예: 렌더링 최적화, useCallback, useMemo, 비동기 상태 업데이트 등)을 이해하려면 <strong>자바스크립트의 실행 원리</strong>를 탄탄히 아는 것이 필수입니다.</p>
<p>이번 글에서는 리액트를 공부하기 전에 반드시 짚고 넘어가야 할 <strong>JS 핵심 4개념</strong>을 정리했습니다.</p>
<hr>
<h2 id="클로저-closure">클로저 (Closure)</h2>
<blockquote>
<p>정의: “함수가 <strong>선언될 때</strong>의 렉시컬 스코프(변수 환경)를 기억하여, <strong>실행 시점</strong>에도 그 변수들에 접근할 수 있는 현상(함수 + 환경의 조합).”</p>
</blockquote>
<hr>
<h3 id="1-메모리·실행-모델">1) 메모리·실행 모델</h3>
<ul>
<li>함수가 선언되면, 엔진은 그 함수에 대한 <strong>[[Environment]]</strong>(= 외부 렉시컬 환경 참조)를 저장합니다.</li>
<li>외부 함수가 종료되어도, <strong>내부 함수가 참조하는 변수들이 살아있다면</strong> GC 대상이 되지 않습니다. (“메모리 캡처”)</li>
<li>즉, <strong>스코프 체인</strong>은 “문법적(렉시컬) 위치”로 고정됩니다. 호출 위치가 아니라 <strong>선언 위치</strong>가 중요합니다.</li>
</ul>
<pre><code>function makeCounter(step = 1) {
  let value = 0;                 // ← 렉시컬 환경에 저장

  return function inc() {        // ← [[Environment]]로 value를 기억
    value += step;
    return value;
  };
}

const c1 = makeCounter();  // c1은 value=0을 기억
const c2 = makeCounter(2); // c2는 value=0, step=2를 기억

console.log(c1()); // 1
console.log(c1()); // 2
console.log(c2()); // 2
console.log(c2()); // 4</code></pre><hr>
<h4 id="흔한-함정-루프--var">흔한 함정: 루프 + <code>var</code></h4>
<pre><code>const btns = Array.from(document.querySelectorAll(&#39;button&#39;));
for (var i = 0; i &lt; btns.length; i++) {
  btns[i].addEventListener(&#39;click&#39;, () =&gt; console.log(i));
}
// 모든 버튼이 마지막 값만 출력</code></pre><ul>
<li><code>var</code>는 함수 스코프라 <strong>하나의 i</strong>를 공유합니다.
해결 👉 <code>let</code>(블록 스코프) 사용하거나 즉시실행함수(IIFE)로 캡쳐<pre><code>for (let i = 0; i &lt; btns.length; i++) {
btns[i].addEventListener(&#39;click&#39;, () =&gt; console.log(i));
}</code></pre></li>
</ul>
<hr>
<h3 id="2-리액트와의-연결">2) 리액트와의 연결</h3>
<ul>
<li><p><strong>Stale Closure(오래된 값 참조)</strong> 문제:</p>
<pre><code>function App() {
const [count, setCount] = useState(0);

// 이 핸들러는 &quot;정의된 렌더링 시점의 count&quot;를 기억
const onClick = () =&gt; {
  setTimeout(() =&gt; {
    // 여기서의 count는 과거 값일 수 있음
    setCount(count + 1);
  }, 1000);
};
return &lt;button onClick={onClick}&gt;{count}&lt;/button&gt;;
}</code></pre></li>
<li><p>해결 👉 <strong>업데이트 함수형(setter 콜백)</strong> 사용:</p>
<pre><code>setCount(prev =&gt; prev + 1); // 항상 최신 state 기반</code></pre></li>
<li><p><code>useCallback</code>/<code>useMemo</code>는 <strong>의존성 배열</strong>을 통해 어떤 값을 클로저로 “고정”할지 제어합니다.
의존성 누락 시 의도치 않은 낡은 값 참조가 발생합니다. (ESLint hooks 규칙으로 방지)</p>
</li>
</ul>
<hr>
<h3 id="3-알아두면-좋은-점">3) 알아두면 좋은 점</h3>
<ul>
<li><strong>이벤트 핸들러</strong>에서 state를 읽고 업데이트할 때는 가급적 <strong>함수형 업데이트</strong>로 안전하게 처리하기</li>
<li><strong>메모리 누수</strong>: 클로저가 거대한 객체를 잡고 있으면 GC가 못 합니다. 더 이상 필요없을 때 참조를 끊거나, effect 정리(clean-up)로 해제하기</li>
</ul>
<hr>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/46c1cfe5-35f9-4d75-b774-f3129791ece1/image.png" alt="">
<em>자바스크립트의 렉시컬 환경(Lexical Environment) 구조를 시각적으로 표현한 다이어그램</em></p>
<h2 id="싱글-스레드-single-thread">싱글 스레드 (Single Thread)</h2>
<blockquote>
<p>정의: 자바스크립트 엔진은 기본적으로 <strong>단 하나의 호출 스택(Call Stack)</strong> 에서 코드를 순차 실행합니다.</p>
</blockquote>
<hr>
<h3 id="1-왜-중요한가">1) 왜 중요한가</h3>
<ul>
<li>한 번에 하나의 작업만 <strong>메인 스레드</strong>에서 처리 → <strong>블로킹</strong>이 발생하면 <strong>UI 멈춤</strong></li>
<li>JS 자체는 싱글 스레드지만, 브라우저는 <strong>별도 스레드(타이머, 네트워크, I/O, 렌더링 등)</strong> 를 갖습니다. 비동기는 이들과의 협업으로 구현됩니다.<pre><code>console.log(&quot;A&quot;);
setTimeout(() =&gt; console.log(&quot;B&quot;), 0);
console.log(&quot;C&quot;);
// A -&gt; C -&gt; B</code></pre></li>
</ul>
<hr>
<h3 id="2-메인-스레드를-막는-코드-예시">2) 메인 스레드를 막는 코드 예시</h3>
<pre><code>// 200ms 동안 CPU 바쁜 작업(동기)
const block = (ms) =&gt; {
  const end = performance.now() + ms;
  while (performance.now() &lt; end) {}
};

console.log(&quot;start&quot;);
block(500);       // 이 동안 클릭, 스크롤, 렌더링 모두 지연
console.log(&quot;end&quot;);</code></pre><hr>
<h4 id="해결-아이디어">해결 아이디어</h4>
<ul>
<li>작업 쪼개기(Chunking): <code>setTimeout</code>, <code>queueMicrotask</code>, <code>requestIdleCallback</code>, <code>requestAnimationFrame</code> 등으로 틈 주기</li>
<li>Web Worker로 CPU 바운드 작업을 <strong>메인 스레드 밖</strong>에서 처리</li>
</ul>
<hr>
<h3 id="3-리액트와의-연결">3) 리액트와의 연결</h3>
<p>React 18의 <strong>Concurrent Rendering</strong>은 싱글 스레드 환경에서 “작업을 잘게 쪼개고, 우선순위를 둬서 중단/재개할 수 있도록” 만든 모델입니다.</p>
<ul>
<li>긴 렌더링 작업이 메인 스레드를 독점하지 않도록, <strong>사용자 입력/애니메이션</strong> 같은 급한 일에 먼저 시간을 양보합니다.</li>
</ul>
<hr>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/7a41a3d8-b7b6-4ae2-9735-71bcd0074ea6/image.png" alt="">
<em>자바스크립트 런타임 구조</em></p>
<h2 id="이벤트-루프-event-loop">이벤트 루프 (Event Loop)</h2>
<blockquote>
<p>정의: <strong>콜 스택이 비는 순간</strong>, 대기 중인 태스크(작업)를 <strong>정해진 우선순위</strong>에 따라 가져와 실행시키는 <strong>스케줄러/조정자</strong></p>
</blockquote>
<hr>
<h3 id="1-구성요소와-순서">1) 구성요소와 순서</h3>
<ul>
<li><strong>Call Stack</strong>: 현재 실행 중인 JS 프레임</li>
<li><strong>Web APIs</strong>: 타이머, DOM, fetch 등 브라우저/런타임 제공 기능</li>
<li><strong>Task Queue(Macro Task)</strong>: <code>setTimeout</code>, <code>setInterval</code>, <code>MessageChannel</code>, <code>script</code>…</li>
<li><strong>Microtask Queue</strong>: <code>Promise.then/catch/finally</code>, <code>queueMicrotask</code>, <code>MutationObserver</code></li>
<li><strong>Event Loop</strong>: 스택이 비면, <strong>먼저 Microtask 전부</strong> → 그 후 다음 Task 한 번 수행 → 다시 Microtask …<pre><code>console.log(&quot;start&quot;);
</code></pre></li>
</ul>
<p>setTimeout(() =&gt; console.log(&quot;timeout&quot;), 0);</p>
<p>Promise.resolve()
  .then(() =&gt; console.log(&quot;microtask-1&quot;))
  .then(() =&gt; console.log(&quot;microtask-2&quot;));</p>
<p>console.log(&quot;end&quot;);
// start -&gt; end -&gt; microtask-1 -&gt; microtask-2 -&gt; timeout</p>
<pre><code>---

#### `queueMicrotask` 타이밍</code></pre><p>console.log(&quot;A&quot;);
queueMicrotask(() =&gt; console.log(&quot;B (micro)&quot;));
console.log(&quot;C&quot;);
// A -&gt; C -&gt; B (micro)</p>
<pre><code>---

#### `requestAnimationFrame(rAF)` 타이밍
- **렌더 직전** 한 번 호출됨(프레임 동기화)</code></pre><p>requestAnimationFrame(() =&gt; {
  // 여기는 다음 페인트 직전: DOM 읽고/쓰고, 스타일/레이아웃 최소화 전략 가능
});</p>
<pre><code></code></pre><p>requestAnimationFrame(() =&gt; {
  // 여기는 다음 페인트 직전: DOM 읽고/쓰고, 스타일/레이아웃 최소화 전략 가능
});</p>
<pre><code>---

### 2) 리액트와의 연결
- `setState`는 보통 **동기처럼 보이지만 배치/스케줄링**되어 나중에 반영됩니다.
- `useEffect`는 **커밋 후(페인트 이후)** 비동기적으로 실행
- `useLayoutEffect`는 **DOM 커밋 직후(페인트 이전)** 실행되어 **레이아웃 측정**에 적합
- Transition(React 18): **긴 렌더링을 낮은 우선순위**로 보내 UI 응답성을 확보
---

### 알아두면 좋은 점
- Network 응답 후 DOM 업데이트 → `microtask` vs `task` **타이밍**에 따라 reflow 횟수가 달라질 수 있음
- **긴 루프/대량 DOM 조작**은 `rAF`/`IdleCallback`으로 분할, 또는 가상화·버퍼링 적용
---

![](https://velog.velcdn.com/images/hello-yujin/post/a36bd910-f950-4e7f-9d8e-52a2b34c7726/image.webp)
_이벤트 루프(Event Loop) 의 전체 흐름_

## 브라우저 렌더링 프로세스 (CRP: Critical Rendering Path)

&gt; 정의: 브라우저가 HTML/CSS/JS를 받아 **화면에 픽셀로 그릴 때까지**의 전체 파이프라인

---

### 1) 파이프라인 단계
1. **HTML 파싱 → DOM 생성**
2. **CSS 파싱 → CSSOM 생성**
3. **Render Tree = DOM + CSSOM 결합(보이는 노드만)**
4. **Layout(Reflow)**: 각 노드의 **기하(위치·크기)** 계산
5. **Paint**: 배경, 글자, 테두리 등 그리기
6. **Composite**: 여러 레이어를 GPU로 합성(Transform/Opacity는 합성 단계에서 처리 가능)
&gt; **Recalculate Style → Layout → Paint → Composite** 순으로 DevTools 타임라인에 보이는 이유가 이 흐름 때문
---

### 2) 비용이 큰 작업과 최적화 기준
- **Layout(=Reflow)**: 문서 흐름에 영향 → **비싸다 **
(예: `width/height/top/left`, 폰트 바뀜, DOM 삽입/삭제 등)
- **Paint**: 배경/텍스트/그라데이션/박스-쉐도우가 많으면 비용 상승
**Composite-only**: `transform`/`opacity` 변경은 **Layout/ Paint**를 건너뛸 수 있어 상대적으로 저렴</code></pre><p>/* 좋음: 합성 단계에서 처리됨 */
.box.animate {
  will-change: transform, opacity;
  transition: transform 300ms, opacity 300ms;
}</p>
<pre><code>---

### 3) JS와 렌더링의 상호작용
- JS가 **메인 스레드**를 오래 잡고 있으면, **렌더 스텝**(Layout/Paint/Composite)이 지연됩니다.
- 반대로, 스타일 계산이나 레이아웃을 **자주 강제**하면(예: 잦은 `offsetWidth` 읽기 → layout flush) 프레임이 끊깁니다.</code></pre><p>// layout thrashing (피해야 함)
for (const el of items) {
  const w = el.offsetWidth;  // 레이아웃 강제
  el.style.width = (w + 10) + &#39;px&#39;;
}</p>
<pre><code></code></pre><p>// layout thrashing (피해야 함)
for (const el of items) {
  const w = el.offsetWidth;  // 레이아웃 강제
  el.style.width = (w + 10) + &#39;px&#39;;
}</p>
<pre><code>---

#### 개선: 읽기→쓰기 배치</code></pre><p>// 1) 먼저 모두 읽고
const widths = items.map(el =&gt; el.offsetWidth);
// 2) 다음 프레임에 한번에 쓰기
requestAnimationFrame(() =&gt; {
  items.forEach((el, i) =&gt; { el.style.width = (widths[i] + 10) + &#39;px&#39;; });
});</p>
<pre><code>---

### 4) 리액트와의 연결
- 리액트는 **Virtual DOM**으로 실제 DOM 변경 최소화 → CRP 비용 절감
- 다만 **렌더 함수 자체가 무거우면**(큰 리스트, 복잡한 계산) **JS가 메인 스레드를 막아** 렌더링이 지연됩니다.
(해결: **컴포넌트 분할**, `React.memo`, `useMemo`/`useCallback`,** 리스트 가상화**, **서버 컴포넌트/SSR**, Concurrent 특성(transition) 활용)
- **측정 우선**: Chrome DevTools Performance/Profiler로 **Recalculate Style/Layout/Paint**가 어디서 발생하는지 확인

---

![](https://velog.velcdn.com/images/hello-yujin/post/0b83e057-939a-4c69-ad7f-da477ce2b55e/image.jpg)
_브라우저 렌더링 과정(Critical Rendering Path)_





</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[💻 개발자 도구 이해와 대역폭 최적화 - Ddingsroom 프로젝트에서의 분석 ]]></title>
            <link>https://velog.io/@hello-yujin/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%8F%84%EA%B5%AC-%EC%9D%B4%ED%95%B4%EC%99%80-%EB%8C%80%EC%97%AD%ED%8F%AD-%EC%B5%9C%EC%A0%81%ED%99%94-Ddingsroom-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@hello-yujin/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%8F%84%EA%B5%AC-%EC%9D%B4%ED%95%B4%EC%99%80-%EB%8C%80%EC%97%AD%ED%8F%AD-%EC%B5%9C%EC%A0%81%ED%99%94-Ddingsroom-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Wed, 05 Nov 2025 10:08:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>웹 성능을 개선하고 네트워크 사용량을 줄이는 가장 확실한 방법은 <strong>‘브라우저 개발자 도구(DevTools)’</strong>를 제대로 이해하는 것입니다.
이번 글에서는 <strong>제가 직접 운영 중인 스터디룸 예약 서비스 ‘Ddingsroom’</strong>을 예시로, 크롬 개발자 도구의 <strong>Network / Performance / Lighthouse</strong> 패널을 중심으로 <strong>실제 로딩 과정에서의 병목 지점을 찾고, 폰트 및 이미지 최적화로 대역폭을 줄이는 방법</strong>을 정리해보겠습니다.</p>
</blockquote>
<hr/>


<h2 id="1️⃣-network-패널-이해하기-ddingsroom-로딩-구조-분석">1️⃣ Network 패널 이해하기 (Ddingsroom 로딩 구조 분석)</h2>
<p>Network 패널은 웹 페이지가 요청하는 모든 리소스의 네트워크 흐름을 시각적으로 보여주는 도구입니다.
HTML, JS, CSS, 이미지, 폰트 등 어떤 리소스가 얼마만큼의 시간과 대역폭을 사용했는지를 분석할 수 있습니다.</p>
<p>Ddingsroom은 Next.js 기반으로 구성되어 있어,초기 렌더링 시 HTML → CSS → JS → API → 이미지 순으로 리소스가 순차적으로 요청됩니다.</p>
<hr/>

<h3 id="주요-항목">주요 항목</h3>
<ul>
<li><strong>Name</strong>: 요청된 파일 이름 (예: index.html, main.js)</li>
<li><strong>Status</strong>: HTTP 응답 코드 (200, 404, 304 등)</li>
<li><strong>Type</strong>: 리소스 타입 (document, script, stylesheet, image 등)</li>
<li><strong>Initiator</strong>: 어떤 코드가 해당 요청을 유발했는지 (예: JS, CSS import)</li>
<li><strong>Size</strong>: 다운로드된 실제 바이트 크기 (압축 전/후)</li>
<li><strong>Time</strong>: 요청~응답까지 걸린 총 시간</li>
<li><strong>Waterfall</strong>: 각 요청의 타이밍 시각화 (병렬 요청 구조 확인 가능)</li>
</ul>
<hr/>

<h3 id="ddingsroom에서의-활용-포인트">Ddingsroom에서의 활용 포인트</h3>
<p><strong>1. 불필요한 요청 찾기</strong>
예약 정보와 사용자 정보 API가 중복 호출되는지 점검
<strong>2. 304 Not Modified 확인</strong>
정적 리소스(<code>layout.css</code>, <code>main-app.js</code>)가 캐싱을 통해 효율적으로 재사용되는지 확인
<strong>3. Bundling 점검</strong>
<code>webpack.js</code> 파일의 크기가 큰 경우, 코드 스플리팅 필요성 검토
<strong>4. Lazy Loading 검증</strong>
예약 이미지(스터디룸 사진 등)가 초기 로딩 시 불필요하게 전부 불려오지 않는지 체크</p>
<hr/>

<h3 id="⚙️-ddingsroom-프로젝트-network-패널">⚙️ Ddingsroom 프로젝트 Network 패널</h3>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/d0caea36-8fea-4d0d-bf7e-f532b82a8099/image.png" alt=""></p>
<blockquote>
<p>실제 Ddingsroom 프로젝트의 네트워크 탭 화면 캡쳐본입니다.
상단의 <code>Throttling: 3G</code>와 <code>Disable cache</code>를 설정하여 모바일 네트워크 환경을 재현했습니다.
<code>layout.css</code>, <code>webpack.js</code>, <code>main-app.js</code> 등 주요 리소스의 요청 순서를 확인할 수 있고, DOMContentLoaded(5.38s)와 Load(38.86s) 시점을 기준으로 병목 구간을 분석했습니다.</p>
</blockquote>
<hr/>


<h2 id="2️⃣-performance-패널---렌더링-병목-분석">2️⃣ Performance 패널 - 렌더링 병목 분석</h2>
<p>Performance 패널은 <strong>페이지 로딩과 렌더링 과정 전체를 타임라인으로 시각화</strong>합니다. Network가 “전송 속도”를 본다면, Performance는 <strong>“CPU/렌더링 속도”</strong>를 분석하는 패널입니다.</p>
<p>Ddingsroom에서는 로그인 후 <strong>AfterLoginBanner</strong>나 <strong>예약 모달</strong> 렌더링 시 JS 연산량이 급증해 메인 스레드의 Task가 길어지는 경우가 있습니다.</p>
<hr/>

<h3 id="주요-항목-1">주요 항목</h3>
<ul>
<li><strong>Main Thread Activity</strong>: JS 실행, 렌더링, 리플로우, 페인트 등 브라우저 내부 작업</li>
<li><strong>FPS (Frame Per Second)</strong>: 초당 프레임 — 60fps 유지가 이상적</li>
<li><strong>Screenshots</strong>: 각 시점의 페이지 렌더링 미리보기</li>
<li><strong>CPU Usage</strong>: JavaScript 실행, 레이아웃, 스타일 계산 등의 CPU 부하율</li>
<li><strong>Bottom-up View</strong>: 함수별로 CPU 사용 시간 분석 가능</li>
</ul>
<hr/>

<h3 id="실제-적용-예시">실제 적용 예시</h3>
<p><strong>1. 스크롤 시 버벅임</strong> 
JS Event Listener가 너무 많거나 무거운 연산 확인
<strong>2. 리플로우 발생</strong>
CSS 속성 변경 시 Layout Trashing 여부 파악
<strong>3. LCP(Largest Contentful Paint)개선</strong>
이미지 로드 타이밍 및 lazy loading 확인</p>
<hr/>

<h3 id="⚙️-ddingsroom-프로젝트-performance-측정-결과">⚙️ Ddingsroom 프로젝트 Performance 측정 결과</h3>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/aea50746-9202-4d42-95df-a0730f1bda17/image.png" alt=""></p>
<blockquote>
<p>아래 결과는 <strong>Ddingsroom 로그인 페이지의 초기 로딩 성능</strong>을  Chrome DevTools Performance 패널에서 3G 네트워크 환경으로 측정한 수치입니다.</p>
</blockquote>
<hr/>

<table>
<thead>
<tr>
<th>항목</th>
<th>측정값</th>
<th>상태</th>
<th>해석</th>
</tr>
</thead>
<tbody><tr>
<td><strong>LCP (Largest Contentful Paint)</strong></td>
<td><strong>42.24초</strong></td>
<td>🔴 느림 (Poor)</td>
<td>메인 콘텐츠 표시까지 지나치게 긴 지연 발생</td>
</tr>
<tr>
<td><strong>CLS (Cumulative Layout Shift)</strong></td>
<td><strong>0.00</strong></td>
<td>🟢 매우 우수 (Good)</td>
<td>시각적 흔들림 없이 안정적으로 렌더링됨</td>
</tr>
<tr>
<td><strong>INP (Interaction to Next Paint)</strong></td>
<td><strong>48ms</strong></td>
<td>🟢 우수 (Good)</td>
<td>버튼 클릭 등 인터랙션 후 반응이 즉시 이루어짐</td>
</tr>
</tbody></table>
<hr>
<h4 id="분석">분석</h4>
<ul>
<li><p><strong>LCP 지연 원인</strong></p>
<ul>
<li>초기 로딩 시 <code>div.text-4xl.md:text-5xl.font-bold</code> 요소가 포함된 메인 헤더 영역이 <strong>렌더링 지연</strong> 발생</li>
<li>이미지, JS 번들, 또는 폰트 로딩이 <strong>렌더 블로킹(Render Blocking)</strong> 으로 작용했을 가능성 높음  </li>
<li>특히, <strong>폰트 요청이 비동기적으로 처리되지 않거나 캐시되지 않은 경우</strong> LCP가 악화될 수 있음</li>
</ul>
</li>
<li><p><strong>CLS 0.00 (Good)</strong>  </p>
<ul>
<li>모든 요소에 고정된 높이·비율이 적용되어 <strong>레이아웃 시프트 없이 안정적인 렌더링</strong>이 이루어짐  </li>
<li>이는 Tailwind의 <code>aspect-ratio</code>, 고정 <code>height</code>, <code>width</code> 지정 덕분으로 보임</li>
</ul>
</li>
<li><p><strong>INP 48ms (Good)</strong>  </p>
<ul>
<li>버튼 및 입력 요소의 이벤트 처리 루프가 가볍고,<br>JS 메인 스레드의 블로킹 구간이 거의 없음 → 사용자 반응 즉시 반영 가능</li>
</ul>
</li>
</ul>
<hr>
<h4 id="향후-개선-계획">향후 개선 계획</h4>
<blockquote>
<p>이번 측정에서 <strong>LCP(42.24s)</strong> 지표가 성능 병목으로 드러났습니다.<br>따라서 아래와 같은 단계별 최적화를 진행할 예정입니다.</p>
</blockquote>
<p><strong>1. 이미지 로딩 최적화</strong></p>
<ul>
<li><code>next/image</code> 컴포넌트로 모든 정적 이미지 교체  </li>
<li>hero·배너 이미지에 <code>priority</code> 속성 부여, 하단 콘텐츠에는 <code>loading=&quot;lazy&quot;</code> 적용  </li>
<li>JPEG → WebP / AVIF 변환 및 CDN 캐싱 도입으로 응답 속도 단축  </li>
</ul>
<p><strong>2. 폰트 로딩 개선</strong></p>
<ul>
<li><code>GmarketSans</code>를 <strong>Self-hosted WOFF2 폰트</strong>로 전환  </li>
<li><code>font-display: swap</code> 적용하여 폰트 로딩 중에도 텍스트를 즉시 표시  </li>
<li><code>&lt;link rel=&quot;preload&quot; as=&quot;font&quot;&gt;</code> 로 주요 폰트 사전 로드  </li>
</ul>
<p><strong>3. JS 번들 최적화</strong></p>
<ul>
<li>Next.js의 <code>dynamic import()</code>를 활용해 <strong>비필수 스크립트 지연 로딩</strong>  </li>
<li><code>React.memo</code> 및 <code>useMemo</code>로 렌더링 비용 최소화  </li>
<li>불필요한 콘솔 로그 및 외부 라이브러리 의존성 정리  </li>
</ul>
<p><strong>4. 캐싱 정책 고도화</strong></p>
<ul>
<li>정적 리소스(<code>/static/*</code>, <code>/fonts/*</code>, <code>/images/*</code>)에<br><code>Cache-Control: public, max-age=31536000, immutable</code> 헤더 적용  </li>
<li>이후 Cloudflare / Vercel CDN 연동을 통해 전역 엣지 캐시 활성화  </li>
</ul>
<hr>
<h2 id="3️⃣-lighthouse-패널---웹-성능-자동-진단">3️⃣ Lighthouse 패널 - 웹 성능 자동 진단</h2>
<p>Lighthouse는 <strong>페이지 품질을 종합적으로 분석해주는 자동화 툴</strong>입니다.</p>
<p>Performance, Accessibility, Best Practices, SEO, PWA 등 5가지 항목을 점수화해줍니다.</p>
<hr/>

<h3 id="lighthouse-주요-카테고리">Lighthouse 주요 카테고리</h3>
<ul>
<li><strong>Performance</strong>: 페이지 로딩 속도, LCP, CLS, TBT 등 주요 웹 성능 지표</li>
<li><strong>Accessibility</strong>: 대체 텍스트, ARIA 속성, 색 대비 등 접근성 검증</li>
<li><strong>Best Practices</strong>: HTTPS, JS 오류, 콘솔 경고 등 점검</li>
<li><strong>SEO</strong>: 검색엔진 최적화 관련 메타태그, robots.txt 여부</li>
<li><strong>PWA</strong>: Progressive Web App 지원 여부</li>
</ul>
<hr/>

<h3 id="lighthouse-점수-개선-방안">Lighthouse 점수 개선 방안</h3>
<ul>
<li>이미지 → WebP 형식으로 변환</li>
<li>폰트 → <code>font-display: swap</code> 사용</li>
<li>JS 번들 → Tree Shaking 적용</li>
<li>CSS → Critical CSS 인라인 삽입</li>
<li>캐시 정책 → <code>Cache-Control</code>, <code>ETag</code> 적극 활용</li>
</ul>
<hr/>

<p><img src="https://velog.velcdn.com/images/hello-yujin/post/5fbaebcb-1130-43bf-8273-412058a47035/image.png" alt=""></p>
<blockquote>
<p>이 화면은 <strong>Chrome DevTools의 Lighthouse 탭에서 성능 진단을 시작하기 전, 측정 조건을 설정하는 단계</strong>입니다.<br>분석할 페이지 유형, 디바이스 환경, 그리고 평가 항목(Category)을 지정한 뒤<br><strong>“Analyze page load” 버튼을 눌러 리포트를 생성</strong>합니다.</p>
</blockquote>
<hr/>

<h3 id="⚙️-ddingsroom-프로젝트-lighthouse-탭-성능-진단-결과">⚙️ Ddingsroom 프로젝트 Lighthouse 탭 성능 진단 결과</h3>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/57f9c0dc-725e-433a-a2a0-6d1c6a9362d9/image.png" alt=""></p>
<table>
<thead>
<tr>
<th align="center">항목</th>
<th align="center">점수</th>
<th align="center">상태</th>
<th align="left">해석</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>Performance</strong></td>
<td align="center">42</td>
<td align="center">🔻 <strong>낮음</strong></td>
<td align="left">초기 렌더링(LCP, TTI) 지연 / 이미지 비최적화 / JS 번들 큼</td>
</tr>
<tr>
<td align="center"><strong>Accessibility</strong></td>
<td align="center">95</td>
<td align="center">✅ <strong>우수</strong></td>
<td align="left">label, alt, 대비도 대부분 충족</td>
</tr>
<tr>
<td align="center"><strong>Best Practices</strong></td>
<td align="center">71</td>
<td align="center">⚠️ <strong>보통</strong></td>
<td align="left">이미지 크기, HTTPS 리소스 경고 등 일부 개선 필요</td>
</tr>
<tr>
<td align="center"><strong>SEO</strong></td>
<td align="center">100</td>
<td align="center">🟢 <strong>완벽</strong></td>
<td align="left">title / meta / robots 설정 양호</td>
</tr>
</tbody></table>
<blockquote>
<p>Lighthouse 분석 결과, Ddingsroom은 SEO와 접근성 측면에서 매우 우수한 구조를 보였지만 초기 렌더링 속도와 JS 번들 크기로 인해 Performance 점수가 낮게 측정되었습니다. </p>
<p>향후에는 이미지 WebP 변환, 코드 스플리팅, 폰트 최적화 등을 통해
성능 지표를 42 → 80 이상으로 향상시키는 것을 목표로 하고 있습니다.</p>
</blockquote>
<hr>
<h2 id="4️⃣-폰트-최적화font-optimization">4️⃣ 폰트 최적화(Font Optimization)</h2>
<p>폰트는 눈에 보이지 않지만, 웹 페이지 로딩을 느리게 만드는 주요 요인 중 하나입니다.</p>
<hr>
<h3 id="최적화-방법-5단계">최적화 방법 5단계</h3>
<p><strong>1. WOFF2 형식 사용</strong>
TTF/OTF보다 30~50% 용량 절감
<strong>2. 서브셋(Subsetting)</strong>
실제 사용하는 문자만 포함 (예: 영문 전용, 한글 일부)
<strong>3. <code>font-display: swap</code> 적용</strong>
폰트 로딩 중 FOUT(Flash of Unstyled Text) 방지</p>
<pre><code>@font-face {
  font-family: &#39;Pretendard&#39;;
  src: url(&#39;/fonts/pretendard.woff2&#39;) format(&#39;woff2&#39;);
  font-display: swap;
}</code></pre><p><strong>4. Preload / Preconnect 사용</strong></p>
<pre><code>&lt;link rel=&quot;preload&quot; href=&quot;/fonts/pretendard.woff2&quot; as=&quot;font&quot; type=&quot;font/woff2&quot; crossorigin&gt;</code></pre><p><strong>5. CDN 대신 Self-hosting 고려</strong>
Google Fonts보다 직접 호스팅이 빠를 수 있음</p>
<hr>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/5353fbf6-aec6-4f09-9e4f-636c6defd579/image.png" alt=""></p>
<blockquote>
<p>위 이미지는 <strong>브라우저 렌더링 과정에서 CSS와 폰트 로딩이 페이지 표시 속도에 어떤 영향을 미치는지</strong>를 보여줍니다.  </p>
</blockquote>
<ol>
<li><strong>T₀ ~ T₁ (Request → Build DOM)</strong>  </li>
</ol>
<ul>
<li>브라우저는 HTML 문서를 요청(<code>GET html</code>)하고 응답을 받아 <strong>DOM 트리</strong>를 생성합니다.  </li>
<li>이 단계에서 CSS 파일을 발견하면 <strong>추가 네트워크 요청(<code>GET css</code>)</strong>을 수행합니다.  </li>
<li>CSS는 렌더링을 차단하는 <strong>Render Blocking 리소스</strong>로, 응답이 오기 전까지 화면이 그려지지 않습니다.</li>
</ul>
<ol start="2">
<li><strong>T₁ ~ T₂ (Build CSSOM → Font 요청)</strong>  </li>
</ol>
<ul>
<li>CSS 응답이 오면 브라우저는 <strong>CSSOM(CSS Object Model)</strong>을 빌드합니다.  </li>
<li>CSS 내 <code>@font-face</code> 선언이 있으면 폰트를 새로 요청(<code>GET font</code>)하게 됩니다.  </li>
<li>폰트 응답이 늦을 경우 텍스트는 보이지 않고 <strong>FOUT(Flash of Unstyled Text)</strong> 또는 <strong>FOIT(Flash of Invisible Text)</strong> 현상이 발생합니다.  </li>
<li>이 과정이 <code>blocked text painting</code>으로 표시된 이유입니다.</li>
</ul>
<ol start="3">
<li><strong>T₂ ~ T₃ (First Paint → Paint Text)</strong>  </li>
</ol>
<ul>
<li>CSSOM과 DOM이 결합되어 <strong>렌더 트리(Render Tree)</strong>가 완성되면<br>브라우저가 최초로 화면을 그립니다(<strong>First Paint</strong>).  </li>
<li>그러나 폰트가 아직 로드되지 않았다면 텍스트는 그려지지 않고,<br>폰트 응답 후에야 실제 텍스트가 표시(<strong>Paint Text</strong>)됩니다.  </li>
<li>즉, 폰트 로딩이 늦을수록 <strong>LCP(Largest Contentful Paint)</strong> 지표가 나빠지고 사용자에게는 로딩이 느리게 느껴집니다.</li>
</ul>
<p><strong>핵심 요약</strong>  </p>
<blockquote>
<p>CSS와 폰트는 렌더링을 지연시키는 주요 원인이므로,<br><code>font-display: swap</code> 속성 적용이나 <code>preload</code>로 폰트를 미리 불러오면<br>First Paint 시점에 텍스트가 빠르게 표시되어 <strong>렌더 블로킹 시간을 단축</strong>할 수 있습니다.</p>
</blockquote>
<h3 id="⚙️-ddingsroom-프로젝트에의-적용-예시">⚙️ Ddingsroom 프로젝트에의 적용 예시</h3>
<blockquote>
<p>현재 Ddingsroom은 <code>GmarketSans</code> 시스템 폰트를 <strong>로컬 폰트 기반</strong>으로 사용하고 있습니다.<br>이 방식은 사용자 OS(macOS, Windows 등)에 해당 폰트가 설치되어 있을 경우<br><strong>네트워크 요청 없이 바로 렌더링</strong>된다는 장점이 있습니다.<br>하지만, 로컬에 폰트가 없는 환경에서는 <strong>대체 폰트가 적용되며 시각적 일관성이 깨질 수 있습니다.</strong></p>
</blockquote>
<blockquote>
<p>향후에는 <strong>Self-hosting + 최적화된 폰트 서빙 전략</strong>을 도입하여<br>렌더링 안정성과 성능 점수를 동시에 개선할 계획입니다.</p>
</blockquote>
<h2 id="5️⃣-이미지-최적화image-optimization">5️⃣ 이미지 최적화(Image Optimization)</h2>
<p>이미지는 전체 페이지 용량의 60~80%를 차지합니다.
따라서 <strong>적절한 포맷, 크기, 로딩 전략</strong>이 핵심입니다.</p>
<hr>
<h3 id="이미지-포맷-선택-가이드">이미지 포맷 선택 가이드</h3>
<table>
<thead>
<tr>
<th align="left">목적</th>
<th align="left">추천 포맷</th>
<th align="left">이유</th>
</tr>
</thead>
<tbody><tr>
<td align="left">일반 사진</td>
<td align="left"><strong>WebP / AVIF</strong></td>
<td align="left">용량 최대 70% 절감</td>
</tr>
<tr>
<td align="left">투명 배경</td>
<td align="left"><strong>PNG-8 / WebP</strong></td>
<td align="left">투명도 유지</td>
</tr>
<tr>
<td align="left">아이콘 / 로고</td>
<td align="left"><strong>SVG</strong></td>
<td align="left">벡터 기반, 반응형에 적합</td>
</tr>
<tr>
<td align="left">썸네일 / 프리뷰</td>
<td align="left"><strong>JPEG / WebP</strong></td>
<td align="left">빠른 로드</td>
</tr>
</tbody></table>
<hr>
<h3 id="실제-최적화-기법">실제 최적화 기법</h3>
<p><strong>1. Responsive Images</strong></p>
<pre><code>&lt;img src=&quot;hero-640.webp&quot;
     srcset=&quot;hero-1280.webp 1280w, hero-640.webp 640w&quot;
     sizes=&quot;(max-width: 768px) 640px, 1280px&quot;
     alt=&quot;메인 배너 이미지&quot;&gt;</code></pre><p><strong>2. Lazy Loading</strong></p>
<pre><code>&lt;img src=&quot;banner.webp&quot; loading=&quot;lazy&quot; alt=&quot;배너 이미지&quot;&gt;</code></pre><p><strong>3. Next-gen Formats (WebP, AVIF)</strong>
Lighthouse에서도 직접 추천됨</p>
<p><strong>4. CDN 이미지 압축 서비스 사용 (Cloudflare, Imgix 등)</strong></p>
<hr>
<p><img src="https://velog.velcdn.com/images/hello-yujin/post/794c12fe-2ebd-4340-b3ca-ae4c6faad871/image.png" alt=""></p>
<blockquote>
<p>위 이미지는 <strong>CDN(Content Delivery Network)</strong> 이 원본 서버(Origin Server)와 사용자(User) 사이에서 어떻게 <strong>이미지나 정적 리소스를 빠르고 안정적으로 전달하는지</strong>를 보여줍니다.</p>
</blockquote>
<hr>
<h4 id="작동-원리-요약">작동 원리 요약</h4>
<p><strong>1. 사용자 요청(Request)</strong>  </p>
<ul>
<li>사용자가 이미지를 요청하면 브라우저는 먼저 <strong>CDN 서버(엣지 서버)</strong> 로 요청을 보냅니다.  </li>
<li>CDN은 전 세계 여러 지역에 분산되어 있으며, 사용자의 물리적 위치와 가장 가까운 서버가 응답합니다.</li>
</ul>
<p><strong>2. CDN 캐시(Cache) 확인</strong>  </p>
<ul>
<li>CDN은 동일한 이미지 요청이 들어올 때마다 <strong>자신의 캐시 스토리지</strong>를 먼저 확인합니다.  </li>
<li>캐시에 파일이 있다면 <strong>원본 서버에 재요청하지 않고 즉시 응답(Response)</strong> 합니다.  </li>
<li>이렇게 하면 네트워크 지연(latency)이 줄고, 트래픽 비용도 절감됩니다.</li>
</ul>
<p>** 3. 원본 서버(Origin Server) 요청**  </p>
<ul>
<li>만약 CDN 캐시에 이미지가 없거나 만료되었다면,<br>CDN은 원본 서버에 직접 요청을 보냅니다.  </li>
<li>이후 받은 응답을 캐시에 저장해, <strong>다음 요청 시 빠르게 서빙할 수 있게</strong> 합니다.</li>
</ul>
<p><strong>4. 서버 장애 시의 이점</strong>  </p>
<ul>
<li>이미지처럼 변경이 드문 리소스는 CDN 캐시 내에 유지되기 때문에,<br>원본 서버가 일시적으로 다운되더라도 CDN이 <strong>캐시된 파일을 그대로 전달</strong>할 수 있습니다.  </li>
<li>즉, 사용자는 서비스 장애를 거의 느끼지 못합니다.</li>
</ul>
<hr>
<h3 id="⚙️-ddingsroom-프로젝트에의-적용-예시-1">⚙️ Ddingsroom 프로젝트에의 적용 예시</h3>
<blockquote>
<p>현재 Ddingsroom의 정적 이미지(<code>/public/static/images/*</code>)는 로컬에서 직접 서빙되고 있습니다.<br>향후에는 <strong>Cloudflare Images / AWS CloudFront / Vercel Image Optimization</strong> 같은 CDN을 활용하여 개선을 목표로 하고 있습니다.</p>
</blockquote>
<hr>
<h2 id="👀-마치며">👀 마치며</h2>
<p>솔직히, 지금까지는 “기능이 돌아가면 됐다”고 생각한 적이 많았습니다. 하지만 Ddingsroom은 실제로 학생들이 매일 접속하는 운영 서비스입니다. 1초의 지연, 한 번의 깜빡임도 사용성의 피로로 돌아올 수 있습니다. 
개발자 도구와 Lighthouse로 직접 분석해보니, 이미지·폰트·JS 번들이 성능에 미치는 영향을 구체적으로 확인할 수 있었습니다.</p>
<p>이번 점검을 통해 배운 건, <strong>성능 최적화는 ‘추가 작업’이 아니라 ‘사용자 경험의 기본’</strong>이라는 점입니다.
앞으로는 다음과 같은 방향으로 개선을 이어갈 계획입니다.</p>
<ul>
<li><p>이미지 → WebP 변환, lazy 로딩, CDN 캐싱</p>
</li>
<li><p>폰트 → woff2 self-hosting, preload, font-display: swap</p>
</li>
<li><p>코드 → dynamic import, Tree Shaking 적용</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[🏫 Ddingsroom의 세션 유지 구조 분석 ( + Local Storage, Session Storage, Cookie에 대해)]]></title>
            <link>https://velog.io/@hello-yujin/Ddingsroom%EC%9D%98-%EC%84%B8%EC%85%98-%EC%9C%A0%EC%A7%80-%EA%B5%AC%EC%A1%B0-%EB%B6%84%EC%84%9D-Local-Storage-Session-Storage-Cookie%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@hello-yujin/Ddingsroom%EC%9D%98-%EC%84%B8%EC%85%98-%EC%9C%A0%EC%A7%80-%EA%B5%AC%EC%A1%B0-%EB%B6%84%EC%84%9D-Local-Storage-Session-Storage-Cookie%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Thu, 30 Oct 2025 09:45:33 GMT</pubDate>
            <description><![CDATA[<p>웹 개발을 하다 보면 <strong>“로그인 상태를 어떻게 유지할까?”</strong> 라는 문제를 반드시 마주하게 됩니다. 이때 사용하는 것이 바로 <strong>브라우저 저장소 (Web Storage)</strong>입니다.</p>
<p>이번 글에서는 
1️⃣ <code>Local Storage</code>, <code>Session Storage</code>, <code>Cookie</code> 의 차이점
2️⃣ 그리고 실제로 제가 운영 중인 <strong>Ddingsroom 프로젝트</strong>에서 어떻게 세션을 관리하고 있는지
를 함께 살펴보겠습니다.</p>
<hr/>


<h3 id="📦-local-storage">📦 Local Storage</h3>
<p>Local Storage는 브라우저에 <strong>반영구적으로 데이터를 저장</strong>하는 저장소입니다.</p>
<ul>
<li><strong>유효 기간</strong>: 무기한 (사용자가 직접 삭제하지 않는 이상 남음)</li>
<li><strong>용량</strong>: 약 5MB (브라우저별로 다름)</li>
<li><strong>특징</strong>: 서버로 전송되지 않음, 클라이언트에서만 접근 가능</li>
</ul>
<h4 id="예시">예시</h4>
<pre><code>localStorage.setItem(&quot;theme&quot;, &quot;dark&quot;);
localStorage.getItem(&quot;theme&quot;); // &quot;dark&quot;
localStorage.removeItem(&quot;theme&quot;);
</code></pre><h4 id="주요-사용-예시">주요 사용 예시</h4>
<ul>
<li>다크 모드 / 라이트 모드 설정</li>
<li>최근 본 상품, 선호 카테고리 저장</li>
</ul>
<hr/>


<h3 id="🔐-session-storage">🔐 Session Storage</h3>
<p>Session Storage는 <strong>탭 단위로 유지되는 임시 저장소</strong>입니다.
브라우저 탭을 닫으면 데이터가 삭제됩니다.</p>
<ul>
<li><strong>유효 기간</strong>: 브라우저 탭을 닫을 때까지</li>
<li><strong>서버 전송</strong>: 자동 전송 안 됨</li>
<li><strong>용량</strong>: 약 5MB</li>
</ul>
<h4 id="예시-1">예시</h4>
<pre><code>sessionStorage.setItem(&quot;accessToken&quot;, &quot;abc123&quot;);
sessionStorage.getItem(&quot;accessToken&quot;); // &quot;abc123&quot;
sessionStorage.clear();
</code></pre><h4 id="주요-사용-예시-1">주요 사용 예시</h4>
<ul>
<li>로그인 세션 유지 (JWT 토큰 저장)</li>
<li>임시 입력 폼 데이터</li>
</ul>
<hr/>


<h3 id="🍪-cookie">🍪 Cookie</h3>
<p>Cookie는 서버와 클라이언트가 <strong>자동으로 주고받을 수 있는 데이터 조각</strong>입니다.</p>
<ul>
<li><strong>유효 기간</strong>: 설정 가능 (<code>max-age</code> / <code>expires</code>)</li>
<li><strong>용량</strong>: 약 4KB</li>
<li><strong>서버 전송</strong>: 자동 전송</li>
<li><strong>보안 옵션</strong>: <code>Secure</code>, <code>HttpOnly</code>, <code>SameSite</code> 등으로 강화 가능</li>
</ul>
<h4 id="예시-2">예시</h4>
<pre><code>document.cookie = &quot;user=Yujin; max-age=3600; path=/&quot;;
</code></pre><h4 id="주요-사용-예시-2">주요 사용 예시</h4>
<ul>
<li>자동 로그인, 세션 식별자 (<code>session_id</code>)</li>
<li>웹 분석, 광고 트래킹</li>
</ul>
<hr/>


<h3 id="세-저장소-비교">세 저장소 비교</h3>
<p align="center">
  <img src="https://velog.velcdn.com/images/hello-yujin/post/35912c4d-8af2-4c11-b2d7-f31bbd5c42de/image.webp" width="60%" height="50%" />
</p>

<h3 id="cookie--localstorage-관계-시각화">Cookie &amp; LocalStorage 관계 시각화</h3>
<p align="center">
  <img src="https://velog.velcdn.com/images/hello-yujin/post/aca78d8d-8dea-408a-b4b8-70c25d8cc007/image.png" width="60%" height="50%" />
</p>



<hr/>


<h2 id="🏫-ddingsroom-프로젝트의-세션-유지-구조">🏫 Ddingsroom 프로젝트의 세션 유지 구조</h2>
<p>간단하게 <code>Local Storage</code>, <code>Session Storage</code>, <code>Cookie</code>에 대해 알아봤으니 이제 실제 제가 운영중인 Ddingsroom 프로젝트에서 어떻게 세션을 유지하는지 살펴보도록 하겠습니다.
Ddingsroom은 <strong>JWT 기반 클라이언트 세션 관리</strong>를 사용하고 있습니다. 즉, 서버 세션이 아닌 <strong>토큰 기반 인증 방식</strong>입니다.</p>
<hr/>

<h4 id="로그인-이후의-흐름">로그인 이후의 흐름</h4>
<p>1️⃣ 로그인 성공 시 서버에서 <code>accessToken</code>, <code>refreshToken</code>, <code>userId</code>를 반환합니다.
2️⃣ 클라이언트에서 이를 <code>sessionStorage</code>에 저장합니다.
3️⃣ Axios 인스턴스가 모든 API 요청에 <code>Authorization</code> 헤더를 자동 추가합니다.</p>
<hr/>

<h4 id="📁-관련-파일-구조">📁 관련 파일 구조</h4>
<pre><code>libs/
 └── api/
      ├── instance.js     ← axios 인스턴스 + 인터셉터
      ├── getUserId.js    ← 토큰 기반 유저 식별
      └── admin.js        ← 관리자용 API
stores/
 ├── useTokenStore.js     ← Zustand 전역 상태로 토큰 관리
 ├── useReservationStore.js
 └── useCommunityStore.js
</code></pre><hr/>

<h4 id="1-axios-인스턴스-설정-libsapiinstancejs">1. axios 인스턴스 설정 (libs/api/instance.js)</h4>
<pre><code>import axios from &#39;axios&#39;;

const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
  timeout: 10000,
});

axiosInstance.interceptors.request.use((config) =&gt; {
  const accessToken = sessionStorage.getItem(&#39;accessToken&#39;);
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});
</code></pre><p><em>-&gt; 인증이 필요한 모든 요청에 자동으로 토큰이 붙습니다.</em></p>
<hr/>

<h4 id="2-응답-인터셉터에서-만료-토큰-처리">2. 응답 인터셉터에서 만료 토큰 처리</h4>
<pre><code>axiosInstance.interceptors.response.use(
  (response) =&gt; response,
  (error) =&gt; {
    const { response } = error;
    if (response &amp;&amp; (response.status === 401 || response.status === 403)) {
      sessionStorage.removeItem(&#39;accessToken&#39;);
    }
    return Promise.reject(error);
  }
);
</code></pre><p><em>-&gt; 토큰이 만료되면 자동으로 세션이 종료되어 재로그인을 유도합니다.</em></p>
<hr/>

<h4 id="3-zustand를-통한-전역-세션-관리-usetokenstorejs">3. Zustand를 통한 전역 세션 관리 (<code>useTokenStore.js</code>)</h4>
<pre><code>import { create } from &#39;zustand&#39;;

const useTokenStore = create((set) =&gt; ({
  accessToken: sessionStorage.getItem(&#39;accessToken&#39;) || &#39;&#39;,
  refreshToken: sessionStorage.getItem(&#39;refreshToken&#39;) || &#39;&#39;,
  userId: parseInt(sessionStorage.getItem(&#39;userId&#39;)) || null,

  setAccessToken: (token) =&gt; {
    set({ accessToken: token });
    sessionStorage.setItem(&#39;accessToken&#39;, token);
  },
  clearTokens: () =&gt; {
    set({ accessToken: &#39;&#39;, refreshToken: &#39;&#39;, userId: null });
    sessionStorage.clear();
  },
}));
</code></pre><p><em>-&gt; <code>Zustand</code>와 <code>sessionStorage</code>를 양방향 동기화하여, 새로고침해도 세션이 유지됩니다.</em></p>
<hr/>

<h4 id="왜-session-storage를-선택했을까">왜 Session Storage를 선택했을까?</h4>
<ul>
<li><strong>보안성</strong>: 토큰이 HTTP 요청마다 자동 전송되지 않아 XSS에만 주의하면 안전하다 판단했습니다.</li>
<li><strong>유지 기간</strong>: 브라우저 탭 단위로 로그인 시간이 유지되기에 공용 PC 등에서 자동 로그아웃이 가능합니다.</li>
<li><strong>UX 향상</strong>: 새로고침 시에도 <code>Zustand</code>의 <code>rehydrate()</code>로 세션 유지가 가능합니다.</li>
<li><strong>간결함</strong>: 복잡한 서버 세션 관리 대신 클라이언트 단에서 간단하게 구현이 가능합니다.</li>
</ul>
<hr/>

<h4 id="세션-구조-다이어그램">세션 구조 다이어그램</h4>
<pre><code>[서버] 
  ↓ (로그인 응답)
  accessToken, refreshToken, userId
  ↓
[클라이언트]
 ┌──────────────────────────┐
 │ sessionStorage            │
 │  ├ accessToken            │
 │  ├ refreshToken           │
 │  └ userId                 │
 └──────────────────────────┘
         │
         ▼
 [useTokenStore.js] ← Zustand 전역 상태
         │
         ▼
 [axiosInstance] → Authorization 헤더 자동 부착
</code></pre><hr/>


<h3 id="결론--실서비스에서-드러난-문제와-개선-방향">결론 — 실서비스에서 드러난 문제와 개선 방향</h3>
<p><strong>Ddingsroom</strong>은 <strong><code>JWT + Session Storage</code> 기반 세션 유지 구조</strong>를 사용합니다.</p>
<ul>
<li>서버 세션 대신 <strong>클라이언트 중심의 토큰 인증</strong></li>
<li><strong><code>Axios 인터셉터</code></strong>로 인증 헤더 자동 부착</li>
<li><strong><code>Zustand</code></strong>로 전역 상태 동기화
= 탭 단위의 <strong>보안 중심 UX</strong></li>
</ul>
<p>그러나 최근 실제 서비스 운영 중 다음과 같은 <strong>사용자 피드백</strong>이 접수되었습니다.</p>
<hr/>

<h4 id="🗣️-사용자-피드백-사례">🗣️ 사용자 피드백 사례</h4>
<blockquote>
<p>“자꾸 예약 내역이 사라집니다.
전날 예약했는데 다음날 들어와 보니 예약이 안 되어 있어서 너무 곤란했습니다.
오늘도 또 그러네요. 왜 그런 건가요?”</p>
</blockquote>
<p align="center">
  <img src="https://velog.velcdn.com/images/hello-yujin/post/f01aea83-4aaa-4109-a71b-7de8da226445/image.png" width="50%" height="30%" />
</p>
_실제 서비스에 접수된 건의 내역 화면 캡쳐본_

<p>이 피드백은 “예약이 정상적으로 완료되지 않은 것처럼 보인다”는 내용이었지만,
서버 로그를 분석해보니 <strong>예약 API 자체가 백엔드로 전송되지 않았거나, 인증 실패(401) 로 처리된 경우</strong>였습니다.</p>
<hr/>

<p><code>Access Token</code>이 만료된 상태에서 사용자가 예약 버튼을 누르면 요청이 서버로 전달되지 않거나 <code>401 Unauthorized</code>로 거절되지만, UI에는 별다른 오류 안내가 표시되지 않아 사용자는 예약이 된 줄 착각하게 됩니다.
결국 예약 데이터가 DB에 저장되지 않은 채 사라지는 현상이 발생한 것입니다.</p>
<hr/>

<p>이를 해결하기 위해 현재</p>
<ul>
<li><strong><code>Access Token</code> 사전 갱신 (<code>Preemptive Refresh</code>)</strong></li>
<li><strong>401 응답 시 자동 재발급 및 재시도 로직</strong></li>
<li><strong><code>HttpOnly</code> 쿠키 기반 <code>Refresh Token</code> 보관</strong></li>
<li><strong>멀티탭 동기화 구조 개선</strong>
등의 개선을 진행 중에 있습니다.</li>
</ul>
<p>이 개선이 적용되면, 사용자는 토큰 만료를 인식하지 않고도 서비스 내에서 로그인 상태가 끊김 없이 유지될 것 입니다.</p>
<blockquote>
<p>💡 다음 포스트에서는 이 <code>Refresh Token</code> 로직을 실제 코드 레벨에서 어떻게 구현했는지, <code>Axios 인터셉터</code>와 <code>Network Interceptor</code> 기반으로 자세히 다뤄보겠습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[🏫 SEO란 무엇인가? — DdingsRoom 프로젝트 적용 사례]]></title>
            <link>https://velog.io/@hello-yujin/SEO%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-DdingsRoom-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EC%A7%81%EC%A0%91-%EC%A0%81%EC%9A%A9%ED%95%9C-%EC%8B%A4%EC%A0%84-SEO-%EC%82%AC%EB%A1%80%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@hello-yujin/SEO%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-DdingsRoom-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EC%A7%81%EC%A0%91-%EC%A0%81%EC%9A%A9%ED%95%9C-%EC%8B%A4%EC%A0%84-SEO-%EC%82%AC%EB%A1%80%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 16 Oct 2025 08:33:31 GMT</pubDate>
            <description><![CDATA[<h3 id="1-들어가며">1. 들어가며</h3>
<p>서비스를 만들 때 대부분의 개발자는 “기능 구현”에 집중합니다.
하지만 배포 후 아무리 좋은 서비스를 만들어도,검색 결과에 내 서비스가 전혀 보이지 않는다면 아무 의미가 없을 것 입니다.</p>
<p>저는 <strong>명지대학교 학생회관 스터디룸 예약 서비스 &quot;DdingsRoom&quot;</strong>을 개발하면서 처음으로 <strong>SEO(Search Engine Optimization, 검색 엔진 최적화)</strong>의 중요성을 체감했습니다.
이번 글에서는 SEO의 기본 개념부터, 실제로 DdingsRoom 프로젝트에 적용한 코드를 중심으로 어떻게 검색 노출을 개선했는지 정리해보려 합니다.</p>
<br/>
<br/>
<br/>

<h3 id="2-seo란-무엇인가">2. SEO란 무엇인가?</h3>
<p><strong>SEO(Search Engine Optimization)</strong>는 Google이나 Naver 같은 <strong>검색 엔진이 내 웹사이트를 더 잘 이해하고 노출하도록 만드는 과정</strong>입니다.</p>
<p>검색 엔진은 아래 3단계를 통해 페이지를 노출시킵니다.</p>
<h4 id="1-크롤링crawling--검색봇이-웹을-돌아다니며-페이지를-수집">1. 크롤링(Crawling) – 검색봇이 웹을 돌아다니며 페이지를 수집</h4>
<h4 id="2-인덱싱indexing--수집한-페이지의-내용을-데이터베이스에-저장">2. 인덱싱(Indexing) – 수집한 페이지의 내용을 데이터베이스에 저장</h4>
<h4 id="3-랭킹ranking--검색어와-관련성-높은-페이지를-우선-노출">3. 랭킹(Ranking) – 검색어와 관련성 높은 페이지를 우선 노출</h4>
<p>즉, 개발자가 해야 할 일은
_“검색봇이 내 페이지를 쉽게 읽고, 정확히 이해하도록 구조를 만들어주는 것”_입니다.</p>
<br/>
<br/>
<br/>

<h3 id="3-ddingsroom-프로젝트에서-seo-적용하기">3. DdingsRoom 프로젝트에서 SEO 적용하기</h3>
<p>DdingsRoom은 Next.js 14 + Vercel 배포 환경으로 운영 중입니다.
Next.js는 서버 사이드 렌더링(SSR)과 정적 페이지 생성(SSG)을 지원하기 때문에 React 기반 서비스 중에서도 SEO 친화적인 프레임워크예요.</p>
<p>아래는 제가 RootLayout에서 적용한 SEO 설정 코드입니다.</p>
<pre><code>export const metadata = {
  metadataBase: new URL(&#39;https://ddingsroom.com&#39;),
  title: &#39;DdingsRoom | 명지대학교 학생회관 스터디룸 예약 서비스&#39;,
  description:
    &#39;명지대학교 인문캠퍼스 학생회관 스터디룸 예약부터 사용까지! 간편한 온라인 예약 시스템으로 언제든지 스터디룸을 예약하세요.&#39;,
  keywords: [
    &#39;명지대학교&#39;, &#39;스터디룸&#39;, &#39;예약&#39;, &#39;학생회관&#39;, &#39;인문캠퍼스&#39;, &#39;띵스룸&#39;, &#39;DdingsRoom&#39;,
  ],
  authors: [{ name: &#39;DdingsRoom Team&#39; }],
  creator: &#39;DdingsRoom&#39;,
  publisher: &#39;DdingsRoom&#39;,
  robots: &#39;index, follow&#39;,
  openGraph: { ... },
  twitter: { ... },
};</code></pre><p>이 설정을 통해 다음과 같은 SEO 요소들을 개선했습니다.</p>
<br/>

<h4 id="1-메타-정보-meta-information">1. 메타 정보 (Meta Information)</h4>
<pre><code>title: &#39;DdingsRoom | 명지대학교 학생회관 스터디룸 예약 서비스&#39;,
description: &#39;명지대학교 인문캠퍼스 학생회관 스터디룸 예약부터 사용까지!&#39;</code></pre><p>→ <strong>브라우저 탭 제목, 검색 엔진 미리보기, SNS 카드에 표시되는 정보</strong>입니다.
title과 description을 구체적으로 작성하면 Google 검색결과에 서비스가 깔끔하게 표시됩니다.</p>
<p align="center">
  <img src="https://velog.velcdn.com/images/hello-yujin/post/2a7e4014-3116-4aaf-a833-a4fcafc9ef44/image.png" width="80%" height="40" />
</p>
_Google 검색결과에 표시되는 서비스_

<br/>

<h4 id="2-키워드-keywords">2. 키워드 (Keywords)</h4>
<pre><code>keywords: [&#39;명지대학교&#39;, &#39;스터디룸&#39;, &#39;예약&#39;, &#39;학생회관&#39;, &#39;띵스룸&#39;]</code></pre><p>→ Google은 최근 직접적인 keywords는 중요하게 보지 않지만, 검색 노출의 문맥을 강화하는 데 도움이 되며,특히 국문 서비스나 지역 서비스(“명지대학교” 등)에서는 효과적이라고 합니다.</p>
<br/>

<h4 id="3-open-graph-og--twitter-card">3. Open Graph (OG) &amp; Twitter Card</h4>
<pre><code>openGraph: {
  title: &#39;DdingsRoom | 명지대학교 학생회관 스터디룸 예약 서비스&#39;,
  description: &#39;명지대학교 인문캠퍼스 학생회관 스터디룸 예약부터 사용까지!&#39;,
  images: [{ url: &#39;/static/images/ddingsroom.png&#39;, width: 1200, height: 630 }],
},
twitter: {
  card: &#39;summary_large_image&#39;,
  title: &#39;DdingsRoom | 명지대학교 학생회관 스터디룸 예약 서비스&#39;,
  description: &#39;명지대학교 인문캠퍼스 학생회관 스터디룸 예약부터 사용까지!&#39;,
  images: [&#39;/static/images/ddingsroom.png&#39;],
}</code></pre><p>→ <strong>SNS 공유 최적화(소셜 SEO) 부분</strong>이에요.
누군가가 서비스 링크를 카카오톡, 트위터, 인스타그램 등에 공유하면
이 <strong>메타 정보에 따라 미리보기 이미지, 제목, 설명이 자동으로 표시</strong>됩니다.</p>
<p>즉, 공유되는 순간부터 브랜딩 효과를 얻을 수 있습니다.</p>
<p align="center">
  <img src="https://velog.velcdn.com/images/hello-yujin/post/5516013d-8a1f-4106-a3e9-0e3b4f4f0199/image.png" width="50%" height="40" />
</p>
_카카오톡에 공유된 미리보기_

<br/>

<h4 id="4-robots--sitemap">4. Robots &amp; Sitemap</h4>
<pre><code>robots: &#39;index, follow&#39;</code></pre><p>→ 검색 엔진이 이 페이지를 수집(index) 하고, 내부 링크를 따라갈 수 있도록(follow) 허용하는 설정입니다.</p>
<p>추가로 /robots.txt와 /sitemap.xml을 생성하면 Google Search Console에서 인덱싱을 훨씬 빠르게 할 수 있다고 합니다.</p>
<br/>

<h4 id="5-viewport--theme-color">5. Viewport &amp; Theme Color</h4>
<pre><code>export const viewport = {
  width: &#39;device-width&#39;,
  initialScale: 1,
  maximumScale: 5,
  themeColor: &#39;#ffffff&#39;,
};</code></pre><p>→ <strong>모바일 친화도(Responsive SEO)를 위한 설정</strong>입니다.
모바일 페이지가 최적화되어야 Google의 모바일 퍼스트 인덱싱에서 좋은 점수를 받습니다.</p>
<br/>

<h4 id="6-google-analytics-ga4">6. Google Analytics (GA4)</h4>
<pre><code>&lt;Script
  src=&quot;https://www.googletagmanager.com/gtag/js?id=G-NEXY3X7HZG&quot;
  strategy=&quot;afterInteractive&quot;
/&gt;
&lt;Script id=&quot;google-analytics&quot; strategy=&quot;afterInteractive&quot;&gt;
  {`
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag(&#39;js&#39;, new Date());
    gtag(&#39;config&#39;, &#39;G-NEXY3X7HZG&#39;);
  `}
&lt;/Script&gt;</code></pre><p>→ <strong>Google Analytics를 통해 방문자 데이터를 추적</strong>합니다.
검색 유입 경로, 페이지별 체류시간 등을 분석해 SEO 전략의 효과를 정량적으로 확인할 수 있습니다.</p>
<p align="center">
  <img src="https://velog.velcdn.com/images/hello-yujin/post/3b1e61ba-5b8f-479d-9a90-14708c6cbf76/image.png" width="100%" height="60" />
</p>

<p>2025.10.21.화 기준 Google Analytics 연동 페이지 대시보드</p>
<br/>
<br/>
<br/>

<h3 id="4-적용-후-변화">4. 적용 후 변화</h3>
<p>SEO 적용 이전에는 Google에서 띵스룸이나 명지대학교 스터디룸을 검색해도
서비스가 노출되지 않았습니다.</p>
<p>그러나 메타데이터를 적용하고 Google Search Console에 sitemap을 등록한 후 약 2주 만에 서비스가 검색 결과에 노출되기 시작했습니다.</p>
<p>또한 Vercel에서 배포된 페이지의 Lighthouse SEO 점수는 70점대에서 98점으로 향상되었습니다.</p>
]]></description>
        </item>
    </channel>
</rss>