<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dyeon-dev.log</title>
        <link>https://velog.io/</link>
        <description>https://dyeon-dev.vercel.app/blog</description>
        <lastBuildDate>Mon, 02 Mar 2026 11:31:44 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dyeon-dev.log</title>
            <url>https://velog.velcdn.com/images/dyeon-dev/profile/fc32a22c-e203-477a-9f17-1aca0c792af0/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dyeon-dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dyeon-dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[네이버 부스트캠프 웹・모바일 10기 과정 수료 후기 🍀]]></title>
            <link>https://velog.io/@dyeon-dev/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-10%EA%B8%B0-%EA%B3%BC%EC%A0%95-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@dyeon-dev/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-10%EA%B8%B0-%EA%B3%BC%EC%A0%95-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 02 Mar 2026 11:31:44 GMT</pubDate>
            <description><![CDATA[<p>네부캠이 끝난지 거의 한 달이 되어간다. 그동안 이력서도 업데이트 하고, 피드백 특강도 받으면서 2월달 시간이 빠르게 흘렀다. 와중에 10기 캠퍼들의 포스트 캠프(네트워킹) 자리까지 마련되면서 많은 담소를 직접 나누기도 했다.!
<img src="https://velog.velcdn.com/images/dyeon-dev/post/3a9d1483-b7bf-4ea0-be65-d579d39ba876/image.jpg" alt=""></p>
<p>매주 네부캠 내에서 회고를 작성했지만, 무탈하게 끝난 기념으로 전체 회고를 작성해보려 한다.</p>
<h3 id="지원-동기"><strong>지원 동기?</strong></h3>
<p>우선, 지원하게 된 동기는 나는 프론트엔드 개발자를 희망했었다. 하지만 결국에 웹 개발을 잘 하기 위해서는 백엔드 역량도 어느정도 필요할 것이라는 생각이 들었다. 오롯이 나의 욕심에 의한 웹 풀스택 개발자로 나아가기 위한 첫 여정을 네부캠에서 하고 싶었다. 공식 홈페이지에 소개되어있던 지속가능한 개발자, 학습 철학 등이 나의 추구하는 방향과 아주 잘 맞아보였다.</p>
<h3 id="8개월간의-여정이-끝난-시점에서-나는-무엇을-얻었는가"><strong>8개월간의 여정이 끝난 시점에서 나는 무엇을 얻었는가?</strong></h3>
<blockquote>
<p>베이직(2주), 챌린지(4주), 멤버십(22주) 총 28주 </p>
</blockquote>
<p>프론트엔드에만 관심있던 내가 백엔드까지 사고를 확장하면서 <strong>소프트웨어 아키텍처의 설계 능력</strong>을 더욱 갖췄다고 할 수 있을 것 같다.
네부캠에서 많은 미션들을 겪으면서 모든 결정에 <strong>설계-구현 사고 흐름</strong>을 적용하게끔 만들었다.
그만큼 소프트웨어에는 &#39;정답&#39;이 없다는 것을 깨달았고, <strong>내가 생각하는 논리적인 구조만으로 상대방을 설득하고 프로젝트에 적용</strong>시켜야 한다.</p>
<p>또한, 8개월의 시간이 흐름에 따라 AI 도구들도 정말 비약적으로 발전했다. 그런 도구들을 매번 습득해나가면서 함께 성장했던 것 같다. 덕분에 <strong>AI 관련 활동</strong>들도 많이 해봤다. 아마 현업 수준으로 바이브 코딩도 많이 해봤을 거다.. 많은 인사이트를 얻게 해줬고 앞으로의 개발 경험에도 도움이 많이 될 거라고 생각한다~!</p>
<h3 id="4주간의-챌린지는-cs-기초를-다지는-시간들"><strong>4주간의 챌린지는 CS 기초를 다지는 시간들!</strong></h3>
<p>챌린지에서는 <strong>Low Level CS 지식에 밀착해서, 실제 시스템을 작은 단위로 구현하며 검증하는 미션들</strong> 위주였다. 
멤버십 과정으로 가기 위해서는 챌린지를 잘 해내야하는데, 내 기억으론 고3때보다 잠을 더 못잤던 기억이 있다..하하
나는 컴퓨터공학 유사 전공 계열(데이터사이언스)로 핵심적인 로우 레벨까지는 자세하게 배우지 못했어서 많이 힘들었던 기억이 있다. 그래도 이전에 정처기나 CS 공부를 조금이나마 했었기 때문에 깨어있는 시간들을 갈아넣어서 학습하고 미션을 해결하려고 많이 노력했다.
<img src="https://velog.velcdn.com/images/dyeon-dev/post/a61e3ba3-f09a-4267-82ac-3dab0f2bbadf/image.png" alt=""></p>
<p>가장 기억에 남는 활동은, “영상 업로드→처리→검증” 프로세스를 큐/매니저/이벤트 루프로 나눠 비동기 파이프라인을 시뮬레이션했던 것이다.
스레드 풀과 멀티 스레드 등 비동기 프로그래밍을 가능하게 하는 것들을 깊이있게 학습하면서 Node.js나 브라우저 내부 구조를 직접 탐구했다. 이런 시뮬레이터의 핵심 구조를 본보기 삼아서 영상 비동기 업로드 처리를 구현했던 기억이 있다. 
이처럼 <strong>동작 원리를 이해하기 위해, 추상 개념을 구현체(도구)로 내려서 확인하는 미션들을 통해 CS의 기초를 다질 수 있었다</strong>.</p>
<p>그래도 열심히 했던게 헛되지 않았고 🥹 감사하게도 멤버십 과정까지 참여하게 되었다!
<img src="https://velog.velcdn.com/images/dyeon-dev/post/ca2c3030-1d37-444d-a733-a53a9cffec02/image.png" alt=""></p>
<h3 id="다사다난-했던-멤버십-과정">다사다난 했던 멤버십 과정!</h3>
<blockquote>
<p>바닐라JS부터 리액트 프로젝트.. 그리고 그룹 프로젝트까지.. 총 6번의 굵직한 플젝들</p>
</blockquote>
<p>멤버십 때 경험했던 것처럼 미션을 해결해나가는 과정에는 정답이 없다. 그래서 매주 매칭된 팀원들과 함께 설계와 구현 과정들을 공유하고 <strong>서로 피드백도 주고 받으면서 성장</strong>해나갔다. </p>
<p>제일 처음 했던 프로젝트는 바닐라 자바스크립트 방식으로 EJS 템플릿을 사용해서 <strong>SSR</strong>으로 렌더링 처리하는 프로젝트였다. 
이후 바닐라 자바스크립트를 기반으로 한 <strong>SPA</strong> 패턴의 컴포넌트 렌더링 아키텍처를 구현하는 프로젝트를 진행하면서, Store 클래스의 <strong>Pub/Sub (Observer)</strong> 패턴 등을 직접 구현하며 향후 사용할 리액트 프레임워크의 <strong>동작원리의 기반</strong>을 다졌다.</p>
<p>웹 애플리케이션의 역사를 생각해봤을 때 초창기 전통적인 페이지 기반의 SSR 방식에서 필요한 부분만 부분적으로 업데이트하는 현대의 SPA 방식으로 넘어가는 그 전환점들을 직접 프로젝트로 경험해보며 웹의 역사를 직접적으로 알아갈 수 있었다. <a href="https://velog.io/@dyeon-dev/%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%B0%A9%EC%8B%9D-%EC%96%B4%EB%96%A4%EA%B1%B8-%EC%84%A0%ED%83%9D%ED%95%98%EB%8A%94%EA%B2%8C-%EC%A2%8B%EC%9D%84%EA%B9%8C-CSRSPA-SSRMPA">관련 포스팅</a></p>
<p>그리고 일련의 활동에서 <strong>배운 것들을 정리하고, 캠퍼들에게 공유하는 시간</strong>이 좋았다. 아무래도 같은 내용을 함께 학습하고 있다보니까 함께 소통하면서 얻어갈 수 있는 것들이 정말 많았다. 항상 스크럼 때 다양한 주제로 이야기를 나누게 되었고, 매주 동료 피드백에서 팀원들에게 전문성 기르기(PROFESSIONAL)와 함께 자라기(TOGETHER) 키워드를 가장 많은 피드백으로 받기도 했다.
<img src="https://velog.velcdn.com/images/dyeon-dev/post/ee02df14-f6c2-4af7-84f5-e210f9582390/image.png" alt=""></p>
<h3 id="팀-프로젝트-잘하는-법">팀 프로젝트 잘하는 법</h3>
<p>그리고 _그룹 스프린트_를 진행하면서 3주간 <strong>페어프로그래밍</strong>으로 구현하고, 2주간 다른 팀의 코드를 <strong>리팩토링</strong>하는 활동까지 했다. 이때 배운 것과 느낀게 굉장히 많았다.</p>
<p>본격적인 팀 프로젝트를 진행하니까 내 코드만 이해하는게 아니라 다른 사람의 코드까지 이해 해야됐다. 
그래서 우리팀은 모든 팀원의 <strong>코드 리뷰</strong>를 필수적으로 진행했다. 
그리고 <strong>페어프로그래밍</strong>을 통해 문제가 발생했던 상황을 유연하게 대처하기도 했다. 
또한 <strong>테스트 코드</strong>를 통해 결함없는 환경이 구축될 수 있도록 했다.</p>
<p>특히, 리팩토링 과정에서는 다른 팀이 설계하고 구현해둔 코드는 이해하기가 더더욱 쉽지 않았다. 이때 팀의 <strong>개발 일지</strong>와 <strong>설계 문서</strong>를 참고하니 많은 도움이 되었다. </p>
<p>이처럼 그룹 스프린트 동안에 겪은 문제 상황과 해결법을 몸소 느낀 뒤로 팀 프로젝트를 &#39;잘&#39;하기 위해 여러 방법을 체계화했다.
이렇게 체계화한 습관은 _그룹 프로젝트_에서도 그대로 이어졌다.</p>
<ul>
<li>PR 템플릿을 활용해서 문제 상황 해결 과정을 자세하게 남기기</li>
<li>개발일지 작성하기</li>
<li>모든 팀원이 꼼꼼하게 코드리뷰 남기기</li>
<li>팀원들과 설계 문서 필수로 작성하기</li>
<li>아키텍처 구조화를 이미지로 많이 생성하기 </li>
<li>의견 논의가 있을 때 discussions 사용하기</li>
</ul>
<p>프로젝트가 잘 나아가기 위해서는 팀원들과의 이해도를 잘 맞춰야 한다.
서로 이해한 수준이 다르면 코드는 산으로 갈 수 있기 때문이다.
따라서 나의 생각을 정확하게 전달하고 다른 사람의 의도를 정확하게 파악하는 소프트스킬을 팀 프로젝트를 통해서 많이 배울 수 있었다.</p>
<h3 id="tadak-팀-프로젝트">TADAK 팀 프로젝트</h3>
<p>열정적인 팀원들을 만나 최종 팀 프로젝트를 성공적으로 마칠 수 있었다. 
첫 일주일간 아이디어 기획 회의만 진행했었는데 우리들의 공통된 문제의식을 찾기 위해 노력했다. 아무래도 취준생 입장이니 학습하는데에 몰입을 해야되고 그곳에 재미를 느끼면 좋겠다는 생각을 했다. 그래서 지루한 고테 공부에 게이미피케이션을 적용하여 알고리즘 배틀 플랫폼을 만들었다. </p>
<p><a href="https://github.com/boostcampwm2025/web30-TADAK">깃허브 바로가기</a>
<img src="https://velog.velcdn.com/images/dyeon-dev/post/44118a7f-bbf4-479c-a07c-27b9427a16f1/image.png" alt=""></p>
<p><strong>프로젝트의 목표</strong>는 다음과 같았다. </p>
<ul>
<li>최소기능구현(MVP)으로 핵심 기능들을 먼저 만들고 추가적인 기능을 확장해나가기</li>
<li>사용자 피드백을 꾸준히 받으며 서비스 개선해나가기</li>
<li>실시간성, 공정성, 보안, 안정성, 사용자 경험을 모두 만족하는 서비스 만들기</li>
<li>확장성을 고려한 아키텍처 구성하기</li>
</ul>
<p>목표를 이루기까지 정말 많은 시행착오들이 있었는데, 매주 데모를 진행하면서 프로젝트의 버그나 개선점을 많이 찾아낼 수 있었다.
그 결과 매주 업데이트한 버전을 배포하고 <a href="https://github.com/boostcampwm2025/web30-TADAK/releases">릴리즈에 기록</a>했다. 그리고 매주 구글폼으로 사용자 피드백을 받아서 프로젝트의 목적에 달성했는지 평가했다.</p>
<p>최종 발표 당일에 예상치 못한 버그가 발생했다.. 다행히(?) 발표 순서 전에 발견을 했고 점심시간에 모여 팀원들과 의견을 나누며 한시간 이내로 문제를 해결했다. 아마 위에서 언급했던 모든 팀원이 이해도를 맞추던 습관 덕분에 어느 지점에서 문제가 발생했는지 빠르게 파악할 수 있었다. 팀원들과 함께 원인을 파악할 수 있었던 결과였기 때문에 다시 한 번 습관의 중요성을 그때 가장 크게 느꼈다.</p>
<hr>
<p>회고글을 적다보니 8개월간 정말 많은 것들을 배우고 성장했다는 생각이 든다. 
좋은 동료들을 만나 크게 성장한 것 같아서 뿌듯하고,
앞으로의 개발할 날들을 더욱 기대하면서 이만 마무리하겠다! 😉</p>
<p>부스트캠프 감사합니다~!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[공간데이터 타입과 R-tree 구조]]></title>
            <link>https://velog.io/@dyeon-dev/%EA%B3%B5%EA%B0%84%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85%EA%B3%BC-R-tree-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@dyeon-dev/%EA%B3%B5%EA%B0%84%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85%EA%B3%BC-R-tree-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Wed, 29 Oct 2025 02:57:16 GMT</pubDate>
            <description><![CDATA[<p><strong>지도 기능</strong>을 구현하기 위해서 데이터베이스에 <strong>위도, 경도 좌표</strong> 저장이 필요했다.
효율적으로 좌표를 저장하기 위해서 <strong>&quot;공간데이터&quot; 타입</strong>을 설정해줄 수 있다.</p>
<p>공간데이터를 적용하기 전에, 공간데이터 타입의 종류와 데이터베이스에 따른 차이점 등을 알아보자.
R-tree 자료구조 기반으로 공간인덱싱이 활용이 되는데, 데이터베이스에 따라 실제 R-tree 기반의 종류도 조금씩 다르다. 이에 대해 더 자세히 살펴보자.</p>
<h2 id="공간데이터-타입-종류">공간데이터 타입 종류</h2>
<p>MySQL에서 제공하는 공간 데이터의 종류는 총 7가지이다.
단일 타입으로는 <strong>Point, LineString, Polygon</strong> 세 가지이다.</p>
<ul>
<li><strong>Point</strong>: 좌표 공간의 한 지점</li>
<li><strong>LineString</strong>: 다수의 Point를 연결해주는 선분</li>
<li><strong>Polygon</strong>: 다수의 선분이 연결되어 닫혀있는 상태</li>
</ul>
<p>나머지 타입들은 이 세 가지 타입의 조합이다.</p>
<ul>
<li><strong>Geometry, Multipoint, Multilinestring, Multipolygon, Geometrycollection</strong></li>
</ul>
<p><a href="https://typeorm.io/docs/advanced-topics/indices/#spatial-indices">TypeORM 공식문서</a>를 참고해보면 MySQL, PostgreSQL 등 DB에서 공간데이터를 생성하는 방법으로 크게 두 가지가 나온다.
(먼저 공간 인덱스를 사용하는 열에 <code>spatial: true</code> 옵션을 사용하여 인덱스를 추가한다.)</p>
<h3 id="1-point-타입-mysql-point-타입">1. POINT 타입 (MySQL POINT 타입)</h3>
<pre><code class="language-ts">@Entity()
export class Thing {
    @Column(&quot;point&quot;)
    @Index({ spatial: true })
    point: string
}</code></pre>
<p><strong>⚙️ 내부 동작</strong></p>
<ul>
<li>내부적으로 <strong>2차원 좌표 (x, y)</strong> 를 저장</li>
<li>인덱스는 <strong>R-Tree 기반</strong>(MyISAM) 또는 <strong>B-Tree-like Spatial Index</strong>(InnoDB)로 구현</li>
<li><code>ST_Distance()</code>, <code>MBRContains()</code> 등 <strong>Bounding Box 기반 연산</strong>이 가능</li>
</ul>
<p><strong>특징</strong></p>
<ul>
<li>저장 형태: POINT(x, y) 또는 WKB(Binary)</li>
<li>표현 가능 객체: 단일 점(Point)만 가능</li>
<li>SRID 없음: 좌표계 정보 없음, 단순 2D 좌표</li>
</ul>
<h3 id="2-geometry-타입--srid-postgis-또는-mysql-geometry-타입">2. GEOMETRY 타입 + SRID (PostGIS 또는 MySQL Geometry 타입)</h3>
<pre><code class="language-ts">export interface Geometry {
    type: &quot;Point&quot;
    coordinates: [Number, Number]
}

@Entity()
export class Thing {
    @Column(&quot;geometry&quot;, {
    spatialFeatureType: &quot;Point&quot;,
    srid: 4326,
    })
    @Index({ spatial: true })
    point: Geometry
}</code></pre>
<p><strong>⚙️ 내부 동작</strong></p>
<ul>
<li><code>geometry</code>는 공간객체를 <strong>추상화된 타입으로 저장</strong></li>
<li><code>srid: 4326</code>을 지정하면, MySQL이 해당 좌표계를 기준으로 <strong>지구구면 연산</strong>(거리, 면적 등)을 수행</li>
<li>인덱스는 단순 <strong>bounding box</strong>가 아니라, <strong>좌표계 변환 + 공간 연산</strong>까지 고려</li>
</ul>
<p><strong>특징</strong></p>
<ul>
<li>공간 객체의 모든 타입을 <strong>포괄하는 슈퍼 타입</strong></li>
<li>저장 형태: WKT(<code>POINT(lon lat)</code>) 또는 WKB(Binary)</li>
<li>표현 가능 객체: <strong>Point, LineString, Polygon, MultiPoint</strong> 등 모든 공간 객체</li>
<li>인덱스: R-Tree 기반 <strong>Spatial Index (GiST/Spatial)</strong> 좌표계 기반 공간 인덱싱</li>
</ul>
<h3 id="point--geometry-타입-핵심-차이-정리">POINT / GEOMETRY 타입 핵심 차이 정리</h3>
<pre><code>GEOMETRY
 ├── POINT
 ├── LINESTRING
 ├── POLYGON
 └── MULTI* (복수형 버전)</code></pre><table>
<thead>
<tr>
<th>비교 항목</th>
<th><code>point</code></th>
<th><code>geometry</code></th>
</tr>
</thead>
<tbody><tr>
<td>데이터타입</td>
<td>단일 Point만 저장</td>
<td>Point, Line, Polygon 등 다양한 공간 객체</td>
</tr>
<tr>
<td>공간 인덱스</td>
<td>단순 R-Tree (Bounding Box)</td>
<td>고급 GiST / R-Tree 기반</td>
</tr>
<tr>
<td>공간 연산 함수 지원</td>
<td><code>MBRContains</code>, <code>MBRWithin</code> 등 단순 직사각형 기반, (<code>ST_Distance</code>, <code>ST_Within</code> 일부만)</td>
<td><code>ST_Contains</code>, <code>ST_Intersects</code>, <code>ST_Distance</code> 등 모든 <code>ST_*</code> 함수 완전 지원 <br>(정밀 공간 연산)</td>
</tr>
<tr>
<td>좌표계 변환</td>
<td>불가능 (단순 숫자좌표)</td>
<td>가능 (예: EPSG:3857 ↔ EPSG:4326)</td>
</tr>
<tr>
<td>사용 목적</td>
<td>내부 좌표 계산, 단순 위치정렬</td>
<td>지도/GIS용 실제 좌표계 기반 분석</td>
</tr>
</tbody></table>
<p>어떤 상황에 어떤 데이터 타입이 적합할까?</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>적합한 타입</th>
</tr>
</thead>
<tbody><tr>
<td>“유저가 마지막으로 클릭한 위치를 기록”</td>
<td><code>point</code></td>
</tr>
<tr>
<td>“서울시 내 카페를 반경 2km 이내 검색”</td>
<td><code>geometry</code> + <code>SRID=4326</code></td>
</tr>
<tr>
<td>“행정구역 폴리곤과 포함 여부 판정”</td>
<td><code>geometry</code></td>
</tr>
<tr>
<td>“지도 렌더링 / 거리 계산 / 좌표 변환”</td>
<td><code>geometry</code></td>
</tr>
</tbody></table>
<p>하지만 어떤 데이터베이스를 사용하느냐에 따라 같은 <code>point</code>, <code>geometry</code> 타입이어도 <strong>내부 동작이 다르다.</strong></p>
<ul>
<li><strong>MySQL</strong>에서는 <code>POINT + SRID</code>가 <strong>부분지원(메타데이터 수준)</strong>  </li>
<li><strong>PostGIS</strong>에서는 <code>POINT + SRID</code>가 <strong>완전한 공간객체 지원</strong></li>
</ul>
<table>
<thead>
<tr>
<th>항목</th>
<th>MySQL</th>
<th>PostGIS</th>
</tr>
</thead>
<tbody><tr>
<td><code>POINT</code> + SRID</td>
<td>SRID 저장만 가능, 좌표계 변환/지구거리 불가</td>
<td>완전한 geometry 연산 지원</td>
</tr>
<tr>
<td><code>GEOMETRY(Point)</code> + SRID</td>
<td>동일하게 SRID 적용, 모든 공간연산 가능</td>
<td>완전 동일 (내부적으로도 같은 구조)</td>
</tr>
<tr>
<td>권장 방식</td>
<td>GIS 분석 목적이면 <strong>geometry(Point, srid)</strong></td>
<td>둘 다 가능</td>
</tr>
<tr>
<td>정리하자면</td>
<td></td>
<td></td>
</tr>
<tr>
<td>- <code>POINT</code> + <code>SRID</code>는 <strong>단순히 좌표계 정보만 메타데이터로 포함</strong>한 버전</td>
<td></td>
<td></td>
</tr>
<tr>
<td>- <code>GEOMETRY(Point, SRID)</code>는 <strong>GIS 전용 연산, 좌표계 변환, 거리 계산 등 고급 기능까지 가능</strong>한 완전한 공간객체</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>👉 <strong>“지도 서비스 수준의 공간 연산”을 한다면 <code>geometry(Point, 4326)</code></strong><br>👉 반면 “그냥 위치좌표만 저장”하는 경우라면 <code>POINT SRID 4326</code>으로 충분하다.</p>
<h2 id="데이터베이스의-r-tree-구조">데이터베이스의 R-tree 구조</h2>
<p>두 가지 타입은 공간 인덱스를 만들기 위해 <strong>R-Tree</strong>를 기반으로 동작한다.
하지만 그 구현 방식과 제한사항도 <strong>DB 엔진(MyISAM, InnoDB, PostGIS)에 따라 다르다.</strong></p>
<table>
<thead>
<tr>
<th>DB 엔진</th>
<th>POINT 인덱스 구조</th>
<th>내부 알고리즘</th>
<th>한계</th>
</tr>
</thead>
<tbody><tr>
<td><strong>MyISAM (MySQL)</strong></td>
<td><strong>R-Tree</strong></td>
<td>전통적 R-Tree</td>
<td>SRID/좌표계, 트랜잭션 미지원, 단순 Bounding Box, InnoDB 대비 안정성 낮음</td>
</tr>
<tr>
<td><strong>InnoDB (MySQL 5.7+)</strong></td>
<td><strong>“R-Tree-like” (B-Tree + Spatial extension)</strong></td>
<td>B-Tree 기반으로 구현된 공간인덱스 구조 (R-Tree와 유사한 구조로 최적화)</td>
<td>MBR(최소경계사각형) 단위 비교, 진짜 R-Tree는 아님</td>
</tr>
<tr>
<td><strong>PostGIS (PostgreSQL)</strong></td>
<td><strong>GiST 기반 R-Tree variant</strong></td>
<td>R-Tree의 일반화 버전 (GiST)</td>
<td>완전한 공간 연산, SRID/좌표계 지원</td>
</tr>
</tbody></table>
<h3 id="myisam-mysql">MyISAM (MySQL)</h3>
<ul>
<li>MySQL의 <strong>초기 공간 인덱스 엔진</strong>으로, <strong>정통적인 R-Tree</strong> 구조를 사용한다.</li>
<li>인덱스 노드에는 각 포인트나 도형을 감싸는 <strong>MBR(Minimum Bounding Rectangle, 최소경계사각형)</strong> 이 키로 저장된다.</li>
<li>공간 연산(<code>MBRContains</code>, <code>MBRIntersects</code>, <code>MBRWithin</code> 등) 시 R-Tree 탐색 과정을 통해 <strong>MBR이 겹치는 후보군을 빠르게 필터링</strong>한다.</li>
<li>인덱스는 <strong>다차원 분할 로직</strong>을 사용하며, 삽입/삭제 시 <strong>노드 분할(splitting)</strong>, <strong>재삽입(reinsertion)</strong> 등의 전통적인 R-Tree 알고리즘을 수행한다.</li>
<li><strong>트랜잭션과 외래키 제약은 지원하지 않지만</strong>, 순수 공간 탐색 성능은 InnoDB보다 빠른 경우가 많다.</li>
</ul>
<p><strong>✏️ 내부 연산 과정</strong></p>
<ol>
<li>쿼리(예: <code>MBRContains()</code>) 시, R-Tree 인덱스가 각 객체의 MBR을 계층적으로 탐색</li>
<li>MBR이 겹치는 후보군만 빠르게 반환</li>
<li>필요 시 애플리케이션 레벨에서 추가적인 geometry 연산 수행</li>
</ol>
<p>➡️ <strong>즉, 진짜 R-Tree 구조를 구현한 MySQL의 고전적 공간 인덱스 엔진으로,<br>MBR 단위의 공간 필터링을 빠르게 수행하는 구조이다.</strong></p>
<h3 id="mysql-innodb의-spatial-index">MySQL InnoDB의 Spatial Index</h3>
<ul>
<li>이름은 “R-Tree 기반”이지만, <strong>진짜 R-Tree는 아니다.</strong></li>
<li>InnoDB의 페이지 구조는 기본적으로 <strong>B-Tree</strong> 입니다.<br>  그래서 공간 인덱스도 B-Tree 페이지를 확장해서 저장한다.</li>
<li>이때 키로 쓰이는 건 <strong>MBR (Minimum Bounding Rectangle)</strong> 값이다.</li>
<li>즉,
  <code>&quot;x_min, y_min, x_max, y_max&quot; → 1차원 key로 변환해서 B-Tree에 저장</code></li>
</ul>
<p>➡️ <strong>R-Tree의 개념(공간 분할, bounding box)</strong> 은 차용하지만,<br><strong>알고리즘과 트리 구조 자체는 B-Tree이다.</strong></p>
<p> <strong>✏️ 내부 연산 과정</strong></p>
<ol>
<li>쿼리(예: <code>ST_Within()</code>) 시, 인덱스가 각 포인트의 MBR을 조회</li>
<li>후보군을 MBR 기준으로 필터링</li>
<li>그 다음 “정확한 계산” 없이 결과 반환 (MBR 기반만 수행)</li>
</ol>
<p><strong>즉, “Bounding box overlap check”까지만 하는 1단계 필터이다.</strong></p>
<h3 id="postgis-gist-인덱스">PostGIS GiST 인덱스</h3>
<ul>
<li>GiST(Generalized Search Tree)는 <strong>PostgreSQL이 제공하는 인덱스 프레임워크</strong>로,<br>  R-Tree, B-Tree, kNN, HNSW 등 다양한 인덱스를 일반화할 수 있다.</li>
<li>PostGIS는 이 GiST 프레임워크 위에 <strong>R-Tree-like 알고리즘을 구현</strong>했다.</li>
</ul>
<p><strong>✏️ GiST의 구조</strong></p>
<ul>
<li>각 노드에 저장되는 값은 <code>Bounding Box</code> + <strong>Consistency Function</strong></li>
<li>Consistency Function이란:<br>  “이 인덱스 항목이 쿼리 조건에 부합하는가?”를 판단하는 함수.<br>  (예: <code>does overlap?</code>, <code>is within?</code>, <code>distance &lt; threshold?</code>)<h4 id="⚙️-두-단계-필터링">⚙️ 두 단계 필터링</h4>
</li>
</ul>
<ol>
<li><strong>Rough filter:</strong> Bounding box로 후보군을 찾음<br> → R-Tree처럼 빠름</li>
<li><strong>Exact filter:</strong> 실제 geometry 연산 수행 (<code>ST_Within</code>, <code>ST_Intersects</code> 등)</li>
</ol>
<p>➡️ <strong>즉, R-Tree의 공간 계층 구조 + 정확한 geometry 연산까지 포함된 진짜 공간 인덱스</strong></p>
<pre><code>MySQL InnoDB
 └── B-Tree Page → MBR(x_min, y_min, x_max, y_max)
      └── Record → WKB binary of geometry
      (정렬 기준: Bounding Box 좌표)

PostGIS GiST
 └── GiST Node → Bounding Box + Consistency Function
      ├── Leaf → geometry binary (w/ SRID)
      └── 연산 단계: Rough Filter → Exact Geometry Check</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[에어비엔비 지도 검색 분석 🔍]]></title>
            <link>https://velog.io/@dyeon-dev/%EC%97%90%EC%96%B4%EB%B9%84%EC%97%94%EB%B9%84-%EC%A7%80%EB%8F%84-%EA%B2%80%EC%83%89-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@dyeon-dev/%EC%97%90%EC%96%B4%EB%B9%84%EC%97%94%EB%B9%84-%EC%A7%80%EB%8F%84-%EA%B2%80%EC%83%89-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Wed, 29 Oct 2025 00:07:52 GMT</pubDate>
            <description><![CDATA[<h3 id="에어비엔비-지도-검색-분석">에어비엔비 지도 검색 분석</h3>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/64ce8673-f933-4bbf-95ba-6c98904859e4/image.png" alt=""></p>
<p>에어비엔비에서 아무런 검색값 없이 검색하게 되면 내 근처의 숙소 리스트를 보여줍니다.
그런데 저는 로그인을 하지 않았고, 위치 허용 여부를 선택한 적 없습니다.
그런데 어떻게 내 근처의 숙소 리스트를 보여줄까요?</p>
<p>에어비엔비의 경우 URL 파라미터의 값으로 상태관리를 주로 하는 것 같아보였습니다. 그래서 URL을 분석해보았습니다.
Airbnb는 검색 조건을 모두 <strong>GET Query</strong>로 노출시키는 방식이며,<br><strong>날짜/인원 선택에 따라 &quot;검색 모드, 좌표, 달력 사용 여부, 장기 숙박 옵션, 필터 UI 상태&quot; 같은 메타 정보까지 URL에 포함됩니다.</strong></p>
<h4 id="아무것도-선택-없이-검색한-경우">아무것도 선택 없이 검색한 경우</h4>
<pre><code>https://www.airbnb.co.kr/s/homes?
refinement_paths[]= /homes
&amp;location_search=NEARBY
&amp;source=structured_search_input_header
&amp;search_type=unknown</code></pre><ul>
<li>다른 건 거두절미 하고, <code>location_search=NEARBY</code> 라고 보입니다.</li>
<li>현재 사용자의 위치 기반으로 근처 지역 숙소 검색한다는 뜻이겠죠.</li>
<li>즉, 위치·날짜·인원 미선택 → 기본적으로 내 주변 기반 탐색</li>
</ul>
<h4 id="날짜-인원을-선택하고-검색할-경우-url">날짜, 인원을 선택하고 검색할 경우 URL</h4>
<pre><code>https://www.airbnb.co.kr/s/homes?
refinement_paths[]= /homes
&amp;location_search=NEARBY
&amp;source=structured_search_input_header
&amp;search_type=unknown
&amp;flexible_trip_lengths[]=one_week
&amp;center_lat=37.45
&amp;center_lng=126.73
&amp;monthly_start_date=2025-11-01
&amp;monthly_length=3
&amp;monthly_end_date=2026-02-01
&amp;search_mode=regular_search
&amp;price_filter_input_type=2
&amp;channel=EXPLORE
&amp;date_picker_type=calendar
&amp;checkin=2025-11-01
&amp;checkout=2025-11-04
&amp;adults=2
&amp;drawer_open=true</code></pre><ul>
<li><code>center_lat</code>, <code>center_lng</code> 값과 <code>location_search=NEARBY</code> 값이 보입니다.</li>
<li><code>location_search=NEARBY</code>: 위치 검색 옵션이 “내 주변” 모드라는 의미</li>
<li><code>center_lat</code> , <code>center_lng</code>: 실제 계산된 <strong>지도 중심 좌표</strong></li>
</ul>
<h4 id="지도-이동-기반으로-검색할-경우-url">지도 이동 기반으로 검색할 경우 URL</h4>
<pre><code>https://www.airbnb.co.kr/s/homes?
refinement_paths[]= /homes
&amp;location_search=NEARBY
&amp;source=structured_search_input_header
&amp;search_type=user_map_move
&amp;flexible_trip_lengths[]=one_week
&amp;monthly_start_date=2025-11-01
&amp;monthly_length=3
&amp;monthly_end_date=2026-02-01
&amp;search_mode=regular_search
&amp;price_filter_input_type=2
&amp;channel=EXPLORE
&amp;date_picker_type=calendar
&amp;checkin=2025-11-01
&amp;checkout=2025-11-04
&amp;adults=2
&amp;price_filter_num_nights=3
&amp;ne_lat=37.61692080810032
&amp;ne_lng=126.80809670588201
&amp;sw_lat=37.255288467379465
&amp;sw_lng=126.60219879834523
&amp;zoom=10.876819265101252
&amp;zoom_level=10.876819265101252
&amp;search_by_map=true</code></pre><ul>
<li>search_type이 unknown에서 user_map_move(지도 이동 기반 검색)으로 바뀌었습니다.</li>
<li>이 경우 <strong>지도 화면에 보이는 영역(뷰포트)</strong>을 기준으로 사용자가 지도를 움직이면 화면에 보이는 영역만 숙소를 다시 검색합니다.</li>
<li><code>ne_lat</code>, <code>ne_lng</code>, <code>sw_lat</code>, <code>sw_lng</code> 4개의 좌표도 추가되었습니다. 이건 현재 화면에 보이는 사각형 영역(Bounding Box)를 의미합니다. <ul>
<li>지도 화면에서 <strong>위쪽 오른쪽 끝의 좌표(ne)</strong>  </li>
<li>지도 화면에서 <strong>아래쪽 왼쪽 끝의 좌표(sw)</strong></li>
</ul>
</li>
<li><code>zoom</code> / <code>zoom_level</code>: 지도 확대 수준 (클수록 좁은 범위)</li>
<li>즉,  <strong>지도 이동 시 화면 좌표 계산 →  bounding box 생성 → 해당 범위 내 숙소만 다시 fetch</strong> 과정을 반복합니다.</li>
</ul>
<p>URL에서 확인했다시피, 저는 위치 정보를 허용한적이 없는데 지도 중심 좌표가 들어간 것을 확인하였습니다. 그리고 <strong>상황별로 위치 좌표를 사용하는 의미와 방식이 다릅니다</strong>.
정리해보자면, </p>
<h4 id="검색바-기반-검색의-경우-검색어가-없을-때">검색바 기반 검색의 경우 (검색어가 없을 때)</h4>
<ul>
<li>위치 기반으로 근처 지역 숙소 검색<h4 id="검색바-기반-검색의-경우-검색어가-있을-때">검색바 기반 검색의 경우 (검색어가 있을 때)</h4>
</li>
<li>중심 좌표(center_lat, center_lng) + 반경(radius) 방식<h4 id="지도-기반-검색의-경우">지도 기반 검색의 경우</h4>
</li>
<li>지도의 실제 화면 영역(bounding box)로 판단</li>
<li>화면이 가로/세로 비율을 가지므로 <strong>중심점 + 거리</strong>만으로는 표현 불가</li>
</ul>
<h3 id="왜-위치-허용을-하지-않았는데도-center_lat--center_lng가-보일까">왜 위치 허용을 하지 않았는데도 center_lat / center_lng가 보일까?</h3>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/54f119ec-53b2-456d-b8ee-f63c697fc120/image.png" alt=""></p>
<p>Airbnb에서 <strong>위치 권한을 허용하지 않았는데도 <code>center_lat</code> / <code>center_lng</code>같은 좌표들이 나타나는 이유는</strong> 실제로 내부에서 <strong>IP 기반 GeoIP 위치 값</strong>을 받아오기 때문입니다. 
즉, geolocation API가 아니어도 Airbnb는 사용자의 대략적인 지역 좌표를 추정할 수 있습니다.</p>
<p><strong>1) Airbnb는 먼저 <code>navigator.geolocation</code>을 시도하지 않는다.</strong></p>
<ul>
<li>Airbnb는 <strong>브라우저 권한 요청을 사용자에게 먼저 띄우지 않습니다.</strong>  </li>
<li>대부분의 사용자에게 &quot;위치 권한 허용&quot; 팝업이 뜬 적이 없을 것이다.</li>
<li>Airbnb는 geolocation API 없이도 사용자의 위치를 알고 싶어하기 때문에 서버에서 <strong>IP 기반으로 대략적인 위·경도를 계산</strong>해서 URL에 넣습니다.</li>
</ul>
<p><strong>2) IP 기반 GeoIP 정보는 위도/경도를 제공한다.</strong></p>
<ul>
<li>GeoIP DB는 단순히 “대한민국/서울” 수준이 아니라, <strong>대략적인 위도(latitude)·경도(longitude)까지 제공합니다.</strong></li>
<li>예) <code>IP → 서울시 관악구 주변   → lat=37.47, lng=126.95 같은 값 반환</code></li>
<li>그래서 위치 권한을 전혀 허용하지 않아도 center_lat, center_lng, 지도 중앙 좌표, 주변 숙소 선택 같은 것들이자동으로 가능합니다.</li>
</ul>
<p><strong>3) 검색 조건이 두 단계로 동작한다.</strong></p>
<ul>
<li><code>location_search=NEARBY</code> 위치 검색 옵션으로 “내 주변” 검색 모드로 동작하고,</li>
<li><code>center_lat</code> &amp; <code>center_lng</code> 등의 실제 계산된 지도 중심 좌표를 통해 검색합니다.</li>
</ul>
<p>이렇듯, geolocation을 허용하는 팝업이 뜨지 않고 Airbnb는 사용자 경험을 위해 <strong>좌표가 가능한 한 항상 존재하도록 설계</strong>했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React의 상태 관리 - useReducer, ContextAPI]]></title>
            <link>https://velog.io/@dyeon-dev/React%EC%9D%98-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@dyeon-dev/React%EC%9D%98-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Tue, 21 Oct 2025 00:47:59 GMT</pubDate>
            <description><![CDATA[<p>리액트에서의 상태는 시간이 지나면서 변할 수 있는 동적 데이터이며, 값이 변경될 때마다 컴포넌트의 렌더링 결과물에 영향을 준다. 
리액트 앱 내의 상태는 지역 상태 / 전역 상태 / 서버 상태로 구분할 수 있다.
리액트 내부 API만을 사용해서 상태를 관리할 수 있지만 성능 문제와 상태의 복잡성으로 인해 Redux, Recoil, Zustand와 같은 상태 라이브러리를 활용하기도 한다.</p>
<h1 id="상태state의-세-가지-구분">상태(state)의 세 가지 구분</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>저장 위치</th>
<th>예시</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>지역 상태 (Local State)</strong></td>
<td>특정 컴포넌트 내부 (<code>useState</code>, <code>useReducer</code>)</td>
<td>입력값, 모달 열림 여부 등</td>
<td>해당 컴포넌트 안에서만 접근 가능</td>
</tr>
<tr>
<td><strong>전역 상태 (Global State)</strong></td>
<td>여러 컴포넌트에서 공유 (<code>Context</code>, <code>Redux</code>, <code>Recoil</code> 등)</td>
<td>로그인 정보, 테마 모드 등</td>
<td>앱 전역에서 접근 가능</td>
</tr>
<tr>
<td><strong>서버 상태 (Server State)</strong></td>
<td>외부 서버(백엔드 API 등)에 저장</td>
<td>게시글 목록, 유저 정보, 상품 데이터 등</td>
<td>React 외부의 데이터 — 네트워크 요청으로 가져와야 함</td>
</tr>
</tbody></table>
<h2 id="지역-상태-local-state">지역 상태 (Local State)</h2>
<p>지역 상태는 <strong>컴포넌트 내부에서 사용되는 상태</strong>로, 예를 들어 체크박스의 폼의 입력값 등이 해당한다.
주로 <strong>useState 훅</strong>을 가장 많이 사용하며 때에 따라 <strong>useReducer</strong>와 같은 훅을 사용하기도 한다.</p>
<h2 id="전역-상태-global-state">전역 상태 (Global State)</h2>
<p>전역 상태는 <strong>앱 전체에서 공유되는 상태</strong>를 의미한다.
여러 개의 컴포넌트가 전역 상태를 사용할 수 있으며 <strong>상태가 변경되면 컴포넌트들도 업데이트</strong> 된다.
<strong>Prop drilling 문제를 피하고자</strong> 지역 상태를 해당 컴포넌트들 사이의 전역 상태로 공유할 수도 있다.</p>
<h2 id="서버-상태-server-state">서버 상태 (Server State)</h2>
<p>서버 상태는 사용자 정보, 글 목록 등 <strong>외부 서버에서 저장해야 하는 상태</strong>를 의미한다. 
<strong>UI 상태와 결합</strong>하여 관리하게 되며 <strong>로딩 여부</strong>나 <strong>에러 상태</strong> 등을 포함한다.</p>
<p>서버 상태도 결국 서버에서 가져온 데이터지만 React 내부에선 그냥 useState나 useReducer 같은 훅으로 관리되는 <strong>지역 상태</strong>이다.</p>
<h4 id="그런데-서버-상태는-일반-상태랑-다르다">그런데 서버 상태는 일반 상태랑 다르다</h4>
<p>비슷하게 관리할 수는 있지만, 서버 상태만의 특성이 있다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>지역/전역 상태</th>
<th>서버 상태</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 출처</td>
<td>클라이언트 내부에서 생성</td>
<td>서버(API)에서 가져옴</td>
</tr>
<tr>
<td>최신성</td>
<td>항상 최신 (즉시 수정 가능)</td>
<td>시간이 지나면 “오래됨”</td>
</tr>
<tr>
<td>갱신 방식</td>
<td><code>setState()</code>로 직접 수정</td>
<td>다시 <code>fetch</code>해야 함</td>
</tr>
<tr>
<td>비동기성</td>
<td>거의 없음</td>
<td>항상 비동기 (로딩/에러 필요)</td>
</tr>
<tr>
<td>동기화 필요성</td>
<td>없음</td>
<td>서버 데이터와 동기화 필요</td>
</tr>
</tbody></table>
<p>단순히 useState로 서버 데이터를 관리하면 로딩 상태 / 에러 처리 / 캐싱 / 리페치(refetch) / 동시성 관리 등을 직접 다 구현해야 해서 복잡해진다.</p>
<p>그래서 등장한게 <strong>react-query, SWR와 같은 서버 상태 관리 라이브러리</strong>이다. <strong>서버와의 동기화, 캐싱, 비동기 처리</strong> 등 때문에 최근에는 전용 라이브러리로 관리하는 추세다.</p>
<h1 id="상태로-정의하기-전에-고려해야-할-점">상태로 정의하기 전에 고려해야 할 점</h1>
<p>상태가 업데이트 될 때마다 리렌더링이 발생하기 때문에 유지보수 및 성능 관점에서 상태의 개수를 최소화하는 것이 바람직하다.
가능하다면 상태가 없는 Stateless 컴포넌트를 활용하는게 좋다.</p>
<p>어떤 값을 상태로 정의할 때는 다음 2가지 사항을 고려해야 한다.</p>
<h2 id="1-시간이-지나도-변하지-않는다면-상태가-아니다">1. 시간이 지나도 변하지 않는다면 상태가 아니다.</h2>
<p>상태는 <strong>시간에 따라 변할 수 있는 값</strong>을 관리하기 위한 것이다.
따라서 컴포넌트의 라이프사이클 동안 <strong>변하지 않는 값</strong>이라면 굳이 상태로 둘 필요가 없다. </p>
<h3 id="하지만-동일한-참조를-유지해야-한다면">하지만 동일한 참조를 유지해야 한다면</h3>
<p>상태로 둘 필요는 없지만, 그 값을 렌더링 과정에서는 <strong>&quot;하나의 동일한 객체로 유지&quot;</strong>해야 할 필요는 있다.</p>
<ul>
<li>렌더링될 때마다 새로 만들어지면 안되는 값이라면 <strong>그 참조를 일정하게 유지하는 방법</strong>이 필요한 것이다.</li>
<li><strong>객체 참조 동일성</strong>을 유지하는 방법을 고려해야 한다.</li>
</ul>
<p>예를 들어, &quot;시간이 지나도 변하지 않는 값&quot;으로 컴포넌트가 마운트될 때만 스토어 객체 인스턴스를 생성하고, 언마운트될 때까지 해당 참조가 변하지 않는다고 가정해보자.</p>
<pre><code class="language-ts">const store = new Store(); // 매번 새로운 인스턴스 생성</code></pre>
<p>이처럼 단순히 상수 변수에 저장하는 방법이 있을 것이다. 
하지만 이런 방식은 다음과 같은 문제점이 있다.</p>
<ul>
<li>함수형 컴포넌트는 렌더링될 때마다 함수가 다시 실행되므로 상수에 객체를 저장하면 매번 새로운 객체가 생성된다.</li>
<li>이 객체가 Context나 props 등으로 전달되면, 매번 참조가 달라져서 <strong>불필요한 리렌더링이 자주 발생</strong>할 수 있다.</li>
</ul>
<p>이 문제를 해결하려면, <strong>컴포넌트가 마운트될 때 한 번만 객체를 생성하고 이후 렌더링에서도 같은 참조를 유지</strong>하도록 만들어야 한다.</p>
<h3 id="동일한-객체-참조를-유지하는-3가지-방법">동일한 객체 참조를 유지하는 3가지 방법</h3>
<p>객체 참조 동일성을 유지하기 위해 사용되는 방법 중 하나는 메모이제이션이다. 
<strong>1. useMemo 사용 (권장되지 않음)</strong></p>
<pre><code class="language-ts">const store = useMemo(() =&gt; new Store(), []);</code></pre>
<ul>
<li>렌더링 간 동일한 객체를 유지할 수 있다.</li>
<li>하지만 useMemo는 <strong>성능 최적화를 위한 메모이제이션용 훅</strong>이며, 객체 참조 유지를 위한 용도로 사용하는 것은 권장되지 않는다.</li>
</ul>
<p><strong>2. useState의 지연 초기화(Lazy Initialization) 사용</strong></p>
<pre><code class="language-ts">const [store] = useState(() =&gt; new Store());</code></pre>
<ul>
<li>useState의 초기값 함수를 사용하면, 마운트 시점에 한 번만 실행된다.</li>
<li>이후 렌더링에서도 동일한 인스턴스가 유지된다.</li>
<li>기술적으로는 잘 동작하며, 실제로 동일한 객체 참조를 보장할 수 있다.</li>
</ul>
<blockquote>
<p>하지만 의미론적으로는 좋은 방법이 아니다.</p>
</blockquote>
<ul>
<li>useState는 본래 “시간이 지나면서 변하고, 렌더링 결과에 영향을 주는 값”을 관리하기 위해 설계된 훅이다.</li>
<li>그러나 위와 같은 경우의 목적은 <strong>“상태 변화”가 아닌 “객체 참조의 동일성 유지”</strong>이므로,
의미상으로는 useRef를 사용하는 것이 더 적합하다.</li>
</ul>
<p><strong>3. useRef 사용 (가장 권장되는 방식)</strong></p>
<pre><code class="language-ts">const store = useRef&lt;Store&gt;(null);

if (!store.current) {
  store.current = new Store();
}</code></pre>
<ul>
<li>useRef는 렌더링이 다시 일어나도 같은 객체를 계속 유지한다.</li>
<li>useRef.current는 React가 재렌더링 시에도 초기화하지 않기 때문에 <strong>“마운트 시 한 번 생성 → 언마운트 전까지 동일 참조 유지”</strong>에 가장 적합하다.</li>
<li>React 공식문서에서도 이 목적에는 useRef를 사용하는 것을 권장한다.</li>
</ul>
<h2 id="2-파생된-값은-상태가-아니다">2. 파생된 값은 상태가 아니다.</h2>
<h3 id="ssot-원칙">SSOT 원칙</h3>
<blockquote>
<p>SSOT (Single Source of Truth)
: 하나의 데이터는 단 하나의 출처(source) 에서만 생성·수정되어야 한다.</p>
</blockquote>
<p>React에서도 이 원칙은 그대로 적용된다.
다른 값에서 파생될 수 있는 데이터는 상태로 두지 말고, 단일 출처(SSOT) 원칙을 지켜야 한다.</p>
<p><strong>부모로부터 전달받은 props</strong>나 <strong>기존 상태에서 계산 가능한 값</strong>은 새로운 상태로 관리하면 안 된다.</p>
<h4 id="문제예시1-props를-상태로-복사한-경우">문제예시1. props를 상태로 복사한 경우</h4>
<pre><code class="language-ts">import { useState } from &quot;react&quot;;

type UserEmailProps = {
  initialEmail: string;
};

function UserEmail({ initialEmail }: UserEmailProps) {
  // ❌ props 값을 별도 상태로 복사
  const [email, setEmail] = useState(initialEmail);

  const onChangeEmail = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    setEmail(e.target.value);
  };

  return (
    &lt;div&gt;
      &lt;input type=&quot;text&quot; value={email} onChange={onChangeEmail} /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li>initialEmail이 바뀌어도 email 상태는 자동으로 갱신되지 않는다.</li>
<li>useEffect로 동기화를 시도하면, 사용자의 입력과 부모 데이터가 충돌할 수 있다.
(사용자가 수정한 값이 부모의 변경으로 덮어써질 위험)</li>
<li>즉, 두 개의 출처(initialEmail, email) 가 생겨서 SSOT가 깨진다.</li>
</ul>
<h4 id="해결-방법-상태-끌어올리기">해결 방법: 상태 끌어올리기</h4>
<ul>
<li>상태를 하위 컴포넌트에 두지 말고, <strong>상위 컴포넌트로 끌어올려(Lifting State Up) 단일 출처(props)로 관리</strong>한다.</li>
<li>자식은 부모의 setEmail을 호출해 부모 상태를 갱신한다.<pre><code class="language-ts">import { useState } from &quot;react&quot;;
</code></pre>
</li>
</ul>
<p>type UserEmailProps = {
  email: string;
  setEmail: React.Dispatch&lt;React.SetStateAction<string>&gt;;
};</p>
<p>// 상태를 부모로부터 props로 받기만 함 (출처는 부모 하나)
function UserEmail({ email, setEmail }: UserEmailProps) {
  const onChangeEmail = (e: React.ChangeEvent<HTMLInputElement>) =&gt; {
    setEmail(e.target.value);
  };</p>
<p>  return (
    <div>
      <input type="text" value={email} onChange={onChangeEmail} />
    </div>
  );
}</p>
<p>// 부모 컴포넌트: 상태의 &quot;단일 출처&quot;
function ParentComponent() {
  const [email, setEmail] = useState(&quot;<a href="mailto:example@email.com">example@email.com</a>&quot;);</p>
<p>  return (
    <div>
      <h2>사용자 이메일 수정</h2>
      <UserEmail email={email} setEmail={setEmail} />
      <p>현재 이메일: {email}</p>
    </div>
  );
}</p>
<p>export default ParentComponent;</p>
<pre><code>
#### 문제예시2. 내부 상태끼리 동기화한 경우
```ts
const [items, setItems] = useState&lt;Item[]&gt;([]);
const [selectedItems, setSelectedItems] = useState&lt;Item[]&gt;([]);

useEffect(() =&gt; {
  setSelectedItems(items.filter(item =&gt; item.isSelected));
}, [items]);</code></pre><ul>
<li><code>items</code>와 <code>selectedItems</code>가 <strong>서로 다른 출처</strong>로 존재해 동기화 누락·불일치·추적 어려움이 발생할 수 있다.</li>
<li>단순히 계산 가능한 값인데, 별도의 상태로 관리하면서 불필요한 렌더링이 추가된다.</li>
</ul>
<h4 id="해결-방법-계산된-값으로-처리">해결 방법: 계산된 값으로 처리</h4>
<ul>
<li><code>selectedItems</code>를 상태로 두지 말고, <strong>계산된 값으로 처리</strong>한다.</li>
<li>변수에 계산 결과를 담으면 리렌더링 횟수를 줄일 수 있다.<pre><code class="language-ts">const selectedItems = items.filter(item =&gt; item.isSelected);</code></pre>
</li>
<li>계산 비용이 크다면 성능 문제가 발생할 수도 있기 때문에 useMemo로 최적화한다.</li>
<li>items가 변경될 때만 계산을 수행하고 결과를 메모이제이션하여 성능을 개선할 수 있다.<pre><code class="language-ts">const selectedItems = useMemo(
() =&gt; veryExpensiveCalculation(items),
[items]
);</code></pre>
</li>
</ul>
<h1 id="usestate-vs-usereducer-어떤-것을-사용해야-할까">useState vs useReducer, 어떤 것을 사용해야 할까</h1>
<p>useState는 단순한 상태 관리에는 충분하지만, 상태구조가 복잡해지거나 여러 필드가 서로 연관될 땐 관리가 어려워진다.
이럴 땐 useReducer를 사용하는 것이 더 안전하고 명확하다.</p>
<p>useReducer 사용을 권장하는 경우는 크게 2가지가 있다.</p>
<ol>
<li><p>하위 필드가 많은 복잡한 상태 로직을 다룰 때
→ 여러 속성을 가진 객체 상태를 관리할 때</p>
</li>
<li><p>다음 상태가 이전 상태에 의존할 때
→ “size가 바뀌면 page를 0으로 초기화해야 한다”처럼 상태 간 규칙(비즈니스 로직)이 존재할 때</p>
</li>
</ol>
<h3 id="복잡한-검색-쿼리-상태-관리-상황">복잡한 검색 쿼리 상태 관리 상황</h3>
<h4 id="리뷰를-필터링하는-검색-기능을-만든다고-가정해보자">리뷰를 필터링하는 검색 기능을 만든다고 가정해보자.</h4>
<p>정보를 필터링해서 보여주기 위한 쿼리를 상태로 저장해야 할 것이다.
이러한 쿼리는 단순하지 않고 검색 날짜, 리뷰 점수, 키워드 등 많은 하위 필드를 가지게 된다.
페이지네이션까지 고려한다면 페이지, 사이즈 등의 필드도 추가될 수 있다.</p>
<pre><code class="language-ts">type ReviewRatingString = &#39;1&#39; | &#39;2&#39; | &#39;3&#39; | &#39;4&#39; | &#39;5&#39;;

interface SearchParams {
    startDate: Date;
    endDate: Date;
    rating: ReviewRatingString[];
    keywords: string[];

  // 이외 기타 필터링 검색 옵션
}

interface SearchState {
    filter: SearchParams;
    page: string;
    size: number;
}</code></pre>
<h4 id="문제-usestate-관리의-한계">문제: useState 관리의 한계</h4>
<p>이러한 데이터 구조를 useState로 다루면 상태를 업데이트할 때마다 잠재적인 오류 가능성이 증가한다.
예를 들어 page 값만 업데이트하고 싶어도 우선 전체 데이터를 가지고 온 다음 page 값을 덮어쓰게 되므로 size나 filter 같은 다른 필드가 수정될 수 있어 의도치 않은 오류가 발생할 수 있다.</p>
<p>또한, &#39;size 필드를 업데이트할 때는 page 필드를 0으로 설정해야 한다.&#39; 등의 특정한 업데이트 규칙이 있다면 useState만으로는 한계가 있다. 이럴때는 useReducer를 사용하는게 좋다.</p>
<p>useReducer는 &#39;무엇을 변경할지&#39;와 &#39;어떻게 변경할지&#39;를 분리하여 dispatch를 통해 어떤 작업을 할지를 액션으로 넘기고 reducer 함수 내에서 상태를 업데이트하는 방식을 정의한다.
이로써 복잡한 상태 로직을 숨기고 안정성을 높일 수 있다.</p>
<h4 id="해결-usereducer로-상태-변경-로직을-명확히-분리">해결: useReducer로 상태 변경 로직을 명확히 분리</h4>
<p><strong>1. Action(무엇을 할지) 정의</strong> </p>
<pre><code class="language-ts">type Action =
 | { type: &#39;filter&#39;; payload: SearchParams }
 | { type: &#39;navigate&#39;; payload: string }
 | { type: &#39;resize&#39;; payload: number }</code></pre>
<ul>
<li>&#39;filter&#39; → 새로운 검색 조건으로 필터링</li>
<li>&#39;navigate&#39; → 페이지 이동</li>
<li>&#39;resize&#39; → 페이지 크기 변경</li>
</ul>
<p><strong>2. Reducer(어떻게 할지) 정의</strong></p>
<pre><code class="language-ts">const reducer: React.Reducer&lt;SearchState, Action&gt; = (state, action) =&gt; {
    switch (action.type) {
        case &#39;filter&#39;:
        // 필터가 바뀌면 페이지를 0으로 초기화
            return { ...state, filter: action.payload, page: 0 };

       case &#39;navigate&#39;:
      // 페이지 이동
        return { ...state,  page: action.payload };

      case &#39;resize&#39;:
        // 페이지 크기 변경 시, page도 0으로 초기화
       return { ...state, page: 0, size: action.payload };

      default:
            return state;
    }
}</code></pre>
<p><strong>3. useReducer 사용</strong></p>
<pre><code class="language-ts">const [state, dispatch] = useReducer(reducer, getDefaultState());</code></pre>
<ul>
<li>state → 현재 상태 (SearchState)</li>
<li>dispatch(action) → 상태 변경을 요청하는 함수</li>
</ul>
<p><strong>4. dispatch로 상태 변경하기</strong> </p>
<pre><code class="language-ts">// dispatch 예시 
dispatch({ payload: filter, type: &quot;filter&quot; })
dispatch({ payload: 3, type: &quot;navigate&quot; })
dispatch({ payload: 20, type: &quot;resize&quot; })</code></pre>
<p>이처럼 무엇을 변경할지(action)만 전달하면, 어떻게 변경할지는 reducer내부에서 일관적으로 처리된다.
즉, 상태 변경 로직이 한 곳으로 모여 있어 안전하고 추적이 쉽다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>useState</th>
<th>useReducer</th>
</tr>
</thead>
<tbody><tr>
<td>상태 구조</td>
<td>단순 (원시값)</td>
<td>복잡한 객체 구조</td>
</tr>
<tr>
<td>상태 간 규칙</td>
<td>관리 어려움</td>
<td>reducer에서 일관 처리 가능</td>
</tr>
<tr>
<td>코드 중복</td>
<td>많음 (매번 스프레드 연산)</td>
<td>최소화 (action 기반)</td>
</tr>
<tr>
<td>유지보수성</td>
<td>낮음</td>
<td>높음 (로직 집중)</td>
</tr>
</tbody></table>
<h1 id="전역-상태-관리">전역 상태 관리</h1>
<p>어떤 상태를 컴포넌트 내부에서만 사용하는게 아니라 다른 컴포넌트와 공유할 수 있는 전역 상태로 사용하는 방법은 크게 <strong>리액트 Context API를 사용하는 방법</strong>과 <strong>외부 상태 관리 라이브러리를 사용하는 방법</strong>이 있다.</p>
<h3 id="context-api--usestate--usereducer">Context API + useState / useReducer</h3>
<p>Context API는 <strong>다른 컴포넌트들과 데이터를 쉽게 공유</strong>하기 위한 목적으로 제공되는 API이다.</p>
<ul>
<li>깊은 레벨의 컴포넌트 사이에 데이터를 전달하는 <strong>Prop Drilling</strong> 같은 문제를 해결하기 위한 도구로 활용된다.</li>
<li>Context API는 엄밀히 말하면 전역 상태 관리를 위한 솔루션보단, 여러 컴포넌트 간에 값을 공유하는 솔루션에 가깝다. </li>
<li>useState나 useReducer 같이 지역 상태를 관리하기 위한 API와 결합하여 상태를 공유하기 위한 방법으로 사용되기도 한다.</li>
</ul>
<blockquote>
<p>Context API 활용팁: 유틸리티 함수를 정의하여 더 간단한 코드로 컨텍스트와 훅을 생성</p>
</blockquote>
<ul>
<li><strong>자주 사용되는 Provider와 해당 컨텍스트를 사용하는 훅을 간편하게 생성하여 생산성</strong>을 높일 수 있다.</li>
<li>대규모 애플리케이션이나 성능이 중요한 애플리케이션에서 권장되지 않는 방법이다. 왜냐하면 <strong>Context Provider의 props로 주입된 값이나 참조가 변경될 때마다 해당 컨텍스트를 구독하고 있는 모든 컴포넌트가 리렌더링</strong>되기 때문이다.</li>
<li>물론 Context를 생성할 때 관심사를 잘 분리해서 구성하면 리렌더링 발생을 최소화할 수는 있겠지만, <strong>애플리케이션이 커지고 전역 상태가 많아질수록 불필요한 리렌더링과 상태의 복잡도가 증가</strong>한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[useState, useEffect, useRef 훅 잘쓰는법]]></title>
            <link>https://velog.io/@dyeon-dev/useState-useEffect-useRef-%ED%9B%85-%EC%9E%98%EC%93%B0%EB%8A%94%EB%B2%95</link>
            <guid>https://velog.io/@dyeon-dev/useState-useEffect-useRef-%ED%9B%85-%EC%9E%98%EC%93%B0%EB%8A%94%EB%B2%95</guid>
            <pubDate>Mon, 20 Oct 2025 23:15:28 GMT</pubDate>
            <description><![CDATA[<p>리액트에 훅이 추가(리액트 16.8 버전)되기 이전에는 클래스 컴포넌트에서만 상태를 가질 수 있었다. componentDidMount, componentDidUpdate와 같이 하나의 생명주기 함수에서만 상태 업데이트에 따른 로직을 실행시킬 수 있었다.</p>
<p>리액트 훅이 추가되면서 함수 컴포넌트에서도 클래스 컴포넌트와 같이 컴포넌트의 생명주기에 맞춰 로직을 실행할 수 있게 되었다.</p>
<ul>
<li>비즈니스 로직 재사용</li>
<li>작은 단위로 코드 분할 테스트 용이</li>
<li>사이드 이펙트와 상태를 관심사에 맞게 분리</li>
</ul>
<h1 id="usestate">useState</h1>
<p><strong>리액트 함수 컴포넌트에서 상태(status)를 관리</strong>하기 위해 useState 훅을 활용할 수 있다.
useState 훅을 사용하면 함수형 컴포넌트에서도 상태를 유지하고, 이 <strong>상태값이 업데이트 될 때마다 컴포넌트가 자동으로 리렌더링</strong> 된다.</p>
<h3 id="usestate는-무엇을-반환하나">useState는 무엇을 반환하나</h3>
<p>React의 useState는 다음 두 가지를 반환한다.</p>
<pre><code class="language-ts">const [state, setState] = useState(initialValue);</code></pre>
<ul>
<li>state: 현재 렌더링 시점의 상태값 (즉, 화면에 보이는 값)</li>
<li>setState: 상태를 바꾸도록 React에게 <strong>요청(scheduling)</strong> 하는 함수</li>
</ul>
<p>React는 함수형 컴포넌트가 한 번 렌더링될 때마다 고정된 state 스냅샷을 사용한다.
즉, setState()를 호출해도 그 자리에서 state가 즉시 상태를 바꾸지 않는다.</p>
<p>예시를 통해 이해해보자.</p>
<pre><code class="language-ts">const [count, setCount] = useState(0);

function handleClick() {
  setCount(count + 1);
  console.log(count); // 여전히 0, 바로 바뀌지 않음 
}
</code></pre>
<ul>
<li>setCount(count + 1)을 실행하면 React가 “이 컴포넌트를 다시 렌더링해야겠다” 하고 <strong>업데이트를 예약(scheduling)</strong> 한다.
→ 실제로 count가 변경된 상태는 “다음 렌더링 때” 반영된다. 그래서 <strong>React의 상태 업데이트는 비동기적(asynchronous) 으로 작동</strong>한다.</li>
</ul>
<p>이 비동기적 특성 때문에, 이전 상태를 기준으로 새 상태를 계산해야 하는 경우
<code>(prevState) =&gt; newState</code> 형태를 써야 정확한 결과를 얻는다.</p>
<pre><code class="language-ts">setCount(prev =&gt; prev + 1);
setCount(prev =&gt; prev + 1);
setCount(prev =&gt; prev + 1);
// 결과적으로 count가 +3이 됨 </code></pre>
<h3 id="실제-usestate의-튜플을-살펴보자">실제 useState의 튜플을 살펴보자</h3>
<pre><code class="language-ts">function useState&lt;S&gt;(
  initialState: S | (() =&gt; S)
): [S, Dispatch&lt;SetStateAction&lt;S&gt;&gt;];

type Dispatch&lt;A&gt; = (value: A) =&gt; void;
type SetStateAction&lt;S&gt; = S | ((prevState: S) =&gt; S);</code></pre>
<ul>
<li>useState가 반환하는 튜플의 첫번째 요소는 <strong>현재 상태값</strong>으로, 제네릭으로 지정한 S 타입이다. </li>
<li>두번째 요소는 <strong>컴포넌트에서 상태를 업데이트할 수 있는 Dispatch의 함수</strong>이다. </li>
<li>Dispatch 함수의 제네릭으로 지정한 <code>SetStateAction</code>에는 useState로 관리할 상태 타입인 S 또는 이전 상태 값을 받아 <strong>새로운 상태를 반환하는 함수</strong>인 <code>(prevState: S) =&gt; S)</code>가 들어갈 수 있다.</li>
</ul>
<p>이처럼 <strong>함수형 업데이트</strong>를 사용하면 실제로 비동기적으로 동작하는 상태 업데이트를 <strong>이전 상태값을 ‘동기적으로 참조’해서 계산</strong>할 수 있다.</p>
<blockquote>
<p>useState의 사용팁: 하나의 컴포넌트 안에 useState를 여러 개 쓸 수 있다. 그런데 useState가 많다는건 컴포넌트의 역할이 크다는 뜻이다. 즉, 의존되는 컴포넌트가 많은 것이기 때문에 수정할 필요가 있을 수 있다. </p>
</blockquote>
<h1 id="useeffect">useEffect</h1>
<p>useEffect는 <strong>렌더링 이후 리액트 함수 컴포넌트에 어떤 일을 수행</strong>해야 하는지 알려주기 위해 등록하는 함수이다. DependencyList라는 의존성 배열을 사용한다.</p>
<h3 id="useeffect-타입-정의를-살펴보자">useEffect 타입 정의를 살펴보자</h3>
<pre><code class="language-ts">function useEffect(effect: EffectCallback, deps?: DependencyList): void;

type DependencyList = readonly unknown[];
type EffectCallback = () =&gt; void | Destructor;</code></pre>
<h4 id="effectcallback">EffectCallback</h4>
<ul>
<li>첫번째 인자이자 effect의 타입인 <strong>EffectCallback</strong>은 <strong>Destructor를 반환하거나 아무것도 반환하지 않는 함수</strong>이다. </li>
<li>Promise 타입은 반환하지 않으므로 <strong>useEffect의 콜백함수에는 비동기 함수가 들어갈 수 없다</strong>. 비동기 함수를 호출한다면 경쟁 상태를 불러일으킬 수도 있다.</li>
</ul>
<h4 id="deps">deps</h4>
<ul>
<li><p>두번째 인자인 <strong>deps는 옵셔널하게 제공되고 effect가 수행되기 위한 조건을 나열</strong>한다.</p>
</li>
<li><p>deps가 변경되었는지를 <strong>얕은 비교</strong>로만 판단하기 때문에, 실제 객체 값이 바뀌지 않았더라도 객체의 참조 값이 변경되면 콜백 함수가 실행된다.</p>
<pre><code class="language-ts">type SomeObject = {
  name: string;
  id: string;
}
interface LabelProps {
  value: SomeObject;
}

const Label: React.FC&lt;LabelProps&gt; = ({ value }) =&gt; {
  useEffect(() =&gt; {
    // value.name과 value.id를 사용해서 작업한다
  }, [value]);
}</code></pre>
<p>→ 부모에서 받은 인자를 직접 deps로 작성한 경우, 원치 않는 렌더링이 발생할 수 있다. 이를 방지하기 위해 <strong>실제 사용되는 값을 useEffect의 deps로 사용</strong>해야 한다.</p>
<pre><code class="language-ts">const Label: React.FC&lt;LabelProps&gt; = ({ value }) =&gt; {
  useEffect(() =&gt; {
    // value.name과 value.id 대신 name, id를 직접 사용한다
  }, [value.name]);
}</code></pre>
</li>
</ul>
<p>이처럼 useEffect는 Destructor를 반환하는데 이것은 <strong>마운트가 해제될 때 실행하는 함수</strong>이다.</p>
<ul>
<li>deps가 빈배열일 때는 useEffect의 콜백 함수는 컴포넌트가 처음 렌더링될 때만 실행되고, Destructor(클린업 함수)는 컴포넌트가 마운트 해제될 때 실행된다.</li>
<li>deps 배열이 있다면, 배열 값이 변경될 때마다 Destructor가 실행된다.</li>
</ul>
<h3 id="비동기로-처리되는-useeffect">비동기로 처리되는 useEffect</h3>
<p>useEffect도 마찬가지로 <strong>비동기로 동작하기 때문에 선언을 먼저 해도 레이아웃 배치와 화면 렌더링이 모두 완료된 후에 실행</strong>된다.
그런데 만약 name을 지정하는 setName이 오랜 시간이 걸린 후에 실행된다면? 
→ 사용자는 빈 이름을 오랫동안 보고 있어야 할 것이다.</p>
<p>이런 문제를 해결하기 위해 useLayoutEffect 훅을 사용할 수 있다.</p>
<h3 id="동기적으로-처리되는-uselayouteffect">동기적으로 처리되는 useLayoutEffect</h3>
<p>useLayoutEffect는 <strong>화면이 렌더링 되기 전에 콜백 함수를 실행</strong>한다.
useEffect는 비동기로 동작해서 화면 업데이트를 방해하지 않지만, <strong>동기적인 방식</strong>이 필요하다면 <strong>useLayoutEffect</strong>를 사용하면 된다.</p>
<blockquote>
<p>useEffect의 사용팁: useEffect내의 deps를 올바르게 명시하지 않아서 발생하는 사이드 이펙을 해결하는 것이 더 많은 비용이 발생할 수 있다. 따라서 사용하는 방식을 올바르게 익혀두도록 하자. (useEffect 사용을 지양하는 방법도 있음)</p>
</blockquote>
<h1 id="useref">useRef</h1>
<p>useRef로 관리되는 <strong>변수는 값이 바뀌어도 값을 기억하고 있어 재렌더링이 발생하지 않는다</strong>. </p>
<pre><code class="language-ts">const ref = useRef(initialValue);</code></pre>
<ul>
<li>React는 ref를 렌더링 간에도 “<strong>같은 객체로 유지</strong>”시켜준다.</li>
<li>즉, <code>ref.current</code> 안의 값은 렌더링 사이에서도 사라지지 않고 유지된다.</li>
</ul>
<h2 id="usestate와-useref-차이">useState와 useRef 차이</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th><code>useState</code></th>
<th><code>useRef</code></th>
</tr>
</thead>
<tbody><tr>
<td>값 변경 시 렌더링?</td>
<td>다시 렌더링됨</td>
<td>렌더링 안 됨</td>
</tr>
<tr>
<td>언제 읽을 수 있나?</td>
<td>다음 렌더 후에</td>
<td>즉시 읽을 수 있음</td>
</tr>
<tr>
<td>주 사용 목적</td>
<td>화면 상태(UI 값)</td>
<td>렌더링과 무관한 값, DOM, 타이머 등</td>
</tr>
</tbody></table>
<h3 id="값을-보존하는-상자">값을 보존하는 상자</h3>
<p><code>useRef</code>는 <code>useState</code>와 다르게 <strong>렌더링과 무관한 값을 기억해야 할 때 사용</strong>된다.</p>
<p>ex)</p>
<ul>
<li><p><strong>이전 상태 기억하기</strong></p>
<pre><code class="language-ts">const prevValue = useRef&lt;number&gt;();
useEffect(() =&gt; {
prevValue.current = count; // 이전 값 저장
}, [count]);</code></pre>
</li>
<li><p><strong>타이머 ID 저장하기</strong></p>
<pre><code class="language-ts">const timeoutId = useRef&lt;number&gt;();
timeoutId.current = window.setTimeout(...);</code></pre>
</li>
<li><p><strong>렌더링 횟수 카운트</strong></p>
<pre><code class="language-ts">const renderCount = useRef(0);
renderCount.current += 1;
console.log(&quot;렌더 횟수:&quot;, renderCount.current);</code></pre>
</li>
</ul>
<h3 id="dom-요소에-접근할-때-사용">DOM 요소에 접근할 때 사용</h3>
<p>React는 기본적으로 “선언적 UI” 방식이다.
직접 DOM을 조작하는 대신, 상태(state)가 바뀌면 React가 알아서 DOM을 업데이트한다.
그런데 특정 상황에서는 React의 자동 업데이트보다 <strong>더 직접적인 제어</strong>가 필요할 때가 있다.
이럴 때 <strong>“실제 DOM 노드에 접근”</strong> 해야 한다.</p>
<p><code>&lt;input /&gt;</code> 요소에 <strong>foucs를 설정</strong>하거나 특정 컴포넌트의 위치로 스크롤 하는 등의 <strong>DOM요소에 직접 접근</strong>해야 할 때 사용할 수 있다.</p>
<pre><code class="language-ts">function SearchInput() {
  const inputRef = useRef&lt;HTMLInputElement&gt;(null);

  useEffect(() =&gt; {
    // 컴포넌트가 마운트되면 자동 포커스
    inputRef.current?.focus();
  }, []);

  return &lt;input ref={inputRef} placeholder=&quot;검색어 입력&quot; /&gt;;
}</code></pre>
<ul>
<li>React가 <code>&lt;input&gt;</code> 엘리먼트를 렌더링</li>
<li>해당 DOM 요소를 <code>inputRef.current</code>에 저장</li>
<li>이후 직접 <code>inputRef.current.focus()</code> 호출</li>
</ul>
<p>→ React가 대신 해줄 수 없는, “DOM API 호출” (focus, scroll, measure 등) 을 직접 수행할 수 있게 된다.</p>
<h3 id="dom-접근이-필요한-경우">DOM 접근이 필요한 경우</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>사용하는 이유</th>
</tr>
</thead>
<tbody><tr>
<td><strong>입력창 포커스</strong></td>
<td>자동 focus(), blur() 제어</td>
</tr>
<tr>
<td><strong>스크롤 제어</strong></td>
<td>특정 위치로 scrollIntoView()</td>
</tr>
<tr>
<td><strong>요소 크기/좌표 측정</strong></td>
<td><code>getBoundingClientRect()</code> 로 위치 계산</td>
</tr>
<tr>
<td><strong>캔버스/비디오 제어</strong></td>
<td><code>&lt;canvas&gt;</code>, <code>&lt;video&gt;</code> 의 내부 API 직접 제어</td>
</tr>
<tr>
<td><strong>외부 라이브러리 연동</strong></td>
<td>chart.js, mapbox 등 React 외부 DOM이 필요한 라이브러리 접근</td>
</tr>
</tbody></table>
<blockquote>
<p>일반적인 경우에는 DOM에 직접 접근하는 것은 권장하지 않는다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Debounce과 Throttle을 활용한 최적화]]></title>
            <link>https://velog.io/@dyeon-dev/Debounce%EA%B3%BC-Throttle</link>
            <guid>https://velog.io/@dyeon-dev/Debounce%EA%B3%BC-Throttle</guid>
            <pubDate>Thu, 02 Oct 2025 17:03:57 GMT</pubDate>
            <description><![CDATA[<h1 id="debounce">Debounce</h1>
<p>디바운스는 짧은 시간 동안 동일한 이벤트가 연속적으로 발생할 때, 마지막 이벤트만 실행되도록 하는 기법으로 성능 최적화에 자주 사용된다.
많은 이벤트가 발생하는 상황에서 불필요한 함수 호출을 줄이고, 최종 입력이나 동작만 반영하고 싶을 때 활용된다.</p>
<h2 id="활용-예시">활용 예시</h2>
<p><strong>1) 검색 자동완성</strong>
사용자가 키보드를 입력할 때마다 API 요청을 보내면 서버 부하가 크다.
디바운스를 적용하면 입력이 멈춘 뒤 일정 시간이 지난 후에만 검색 요청을 보낸다.
<strong>2) 입력 검증 (Form Validation)</strong>
이메일, 비밀번호 등 실시간 입력 검증 시 매 글자마다 검증하지 않고, 입력이 멈춘 후에만 검증 로직을 실행한다.
<strong>3) 마우스 Hover 기반 UI 처리</strong>
마우스 이동 시 mousemove 이벤트는 매우 빈번하게 발생한다.
예: 특정 UI에서 마우스를 올리면 다른 데이터가 노출될 때, 디바운스를 적용하여 불필요한 연속 호출을 막는다.
실제 사례: 과거 아마존은 마우스 움직임의 x, y 좌표 기울기를 계산해
좌우 45도 기울기 기준으로 스와이핑과 스크롤을 구분,
스크롤 동작일 경우 스와이프 기능을 preventDefault()로 막는 방식에 디바운스를 활용했다.
<strong>4) 윈도우 리사이즈 이벤트 처리</strong>
브라우저 창 크기를 변경하면 resize 이벤트가 매우 자주 발생한다.
디바운스를 적용해 최종적으로 크기 변경이 끝난 뒤 일정 시간 후에 한 번만 실행되도록 할 수 있다.
예: &quot;창 크기 변경 후 2초가 지나면 레이아웃 다시 계산&quot; 같은 방식.</p>
<h3 id="디바운싱-간단-예시-코드ts">디바운싱 간단 예시 코드(ts)</h3>
<pre><code class="language-ts">  titleInput?.addEventListener(&#39;input&#39;, debounce(function(e: Event) {
    console.log(e.target.value)
  }, 300))

  const debounce = &lt;E extends Event&gt;(fn: (e: E) =&gt; void, delay: number) =&gt; {
    let timer: number | undefined;
    return (e: E) =&gt; {
      if (timer) clearTimeout(timer);
      timer = window.setTimeout(() =&gt; fn(e), delay);
    };
  };
</code></pre>
<ol>
<li><strong>input 이벤트 등록</strong><br> 사용자가 입력 필드에 값을 입력할 때마다 <code>input</code> 이벤트가 발생하고, 이 이벤트를 <code>addEventListener</code>로 감지한다.</li>
<li><strong>debounce 래퍼 적용</strong><br>이벤트 핸들러로 바로 함수를 전달하는 대신, <code>debounce</code> 함수로 감싼 핸들러를 전달한다. 이렇게 하면 입력이 연속으로 빠르게 발생해도 최종 입력 후 일정 시간 동안 추가 입력이 없을 때만 실제 함수가 실행된다.</li>
<li><strong>타이머 관리</strong><br> <code>debounce</code> 내부에서는 <code>setTimeout</code>을 사용해 지정된 지연 시간(delay) 이후에 콜백을 실행한다. 새로운 이벤트가 발생하면 이전에 설정된 타이머를 <code>clearTimeout</code>으로 취소하고, 다시 타이머를 설정한다. 따라서 마지막 입력만 유효하게 처리된다.</li>
<li><strong>실제 콜백 실행</strong><br>지연 시간이 모두 지나면 <code>fn(e)</code>가 실행된다.<ul>
<li>여기서 <code>e</code>는 <code>input</code> 이벤트 객체이며, <code>e.target</code>은 이벤트가 발생한 <code>input</code> 요소를 가리킨다.</li>
<li>따라서 <code>e.target.value</code>를 통해 사용자가 최종적으로 입력한 텍스트 값을 얻을 수 있다.</li>
<li>이렇게 하면 사용자가 키보드를 빠르게 입력할 때마다 매번 실행되지 않고, <strong>최종적으로 멈춘 시점의 값</strong>만 처리된다.</li>
</ul>
</li>
</ol>
<h1 id="throttle">Throttle</h1>
<p>쓰로틀(throttle)은 짧은 시간 동안 동일한 이벤트가 연속적으로 발생할 때, 일정 주기마다 한 번씩만 실행되도록 하는 기법이다.
debounce와 비슷한 개념이지만, <strong>throttle은 일정 주기마다 함수를 실행</strong>하는 방식이라는 점에서 차이가 있다.
throttle의 설정 시간으로 100ms를 주게 되면, 해당 이벤트는 100ms 동안 최대 한 번만 발생하게 된다. 즉 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 한다.
디바운스와 달리 “마지막 이벤트만 실행”하는 것이 아니라, 일정 간격을 두고 주기적으로 실행하는 점이 특징이다.</p>
<h2 id="활용-예시-1">활용 예시</h2>
<p><strong>1) 스크롤 이벤트 처리</strong>
스크롤할 때 scroll 이벤트는 수십~수백 번씩 연속 발생한다.
스크롤 위치에 따라 헤더 숨김/노출, 무한 스크롤 데이터 로드 등을 처리할 때 쓰로틀을 적용하면 0.2초 간격마다 실행되도록 제한할 수 있다.
<strong>2) 마우스 이동(mousemove) 이벤트</strong>
드래그 앤 드롭 구현 시 마우스를 조금만 움직여도 이벤트가 과도하게 발생한다.
쓰로틀을 적용해 일정 간격마다 좌표를 갱신하면 부드럽게 UI를 업데이트할 수 있다.
<strong>3) 페이지네이션 (버튼 기반 로드)</strong>
사용자가 버튼을 너무 빨리 여러 번 클릭하는 경우, API가 중복 호출될 수 있다.
쓰로틀을 적용해 1초에 1번만 클릭 이벤트 처리되도록 제한하면 서버 요청을 보호할 수 있다.
<strong>4) 리사이즈 중간 값 반영</strong>
디바운스는 보통 리사이즈가 끝난 후 한 번 실행되지만,
쓰로틀은 리사이즈가 진행되는 동안에도 0.5초 간격으로 레이아웃 계산 같은 작업을 수행할 수 있다.
즉, 사용자에게 실시간에 가까운 피드백을 주면서도 성능 부담을 줄일 수 있다.</p>
<h4 id="쓰로틀링-간단-예시-코드ts">쓰로틀링 간단 예시 코드(ts)</h4>
<pre><code class="language-ts"> // throttle 유틸 함수
const throttle = &lt;E extends Event&gt;(fn: (e: E) =&gt; void, limit: number) =&gt; {
  let waiting = false;

  return (e: E) =&gt; {
    if (!waiting) {
      fn(e); // 즉시 실행
      waiting = true;
      setTimeout(() =&gt; {
        waiting = false; // 일정 시간 후 다시 실행 가능
      }, limit);
    }
  };
};

// 사용 예시: 스크롤 이벤트
const scrollHandler = throttle((e: Event) =&gt; {
  console.log(&quot;현재 스크롤 위치:&quot;, window.scrollY);
}, 500);

window.addEventListener(&quot;scroll&quot;, scrollHandler);</code></pre>
<ol>
<li><strong>이벤트 등록</strong><br> <code>scroll</code> 이벤트에 <code>throttle</code>로 감싼 함수를 등록한다.</li>
<li><strong>즉시 실행 &amp; 대기 상태 진입</strong><br> 이벤트가 발생했을 때 <code>waiting</code>이 <code>false</code>라면 <code>fn(e)</code>를 실행하고, <code>waiting</code>을 <code>true</code>로 바꾼다.</li>
<li><strong>지정된 시간 동안 무시</strong><br> <code>setTimeout</code>을 사용해 일정 시간(<code>limit</code>) 동안은 새로운 이벤트를 무시한다.</li>
<li><strong>재실행 가능 상태로 전환</strong><br> <code>limit</code> 시간이 지나면 <code>waiting</code>을 <code>false</code>로 되돌려 다시 실행할 수 있다.</li>
</ol>
<h3 id="debounce-vs-throttle-비교">debounce vs throttle 비교</h3>
<h4 id="debounce-1">debounce</h4>
<ul>
<li>스크롤이 내리다가 스크롤을 멈추고 특정 시간 후 이벤트가 하나 발생한다.<h4 id="throttle-1">throttle</h4>
</li>
<li>이벤트가 하나 발생한 뒤 특정 시간동안 이벤트가 발생하지 않지만 그 시간이 지나면 다시 이벤트가 동작한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/7b4494f2-4c79-49f1-aec9-27847e9af0e4/image.gif" alt=""></p>
<h3 id="leading-edge-vs-tradiling-edge">Leading edge vs Tradiling edge</h3>
<p>debounce는 연이어 발생한 이벤트를 하나의 그룹으로 묶어서 처리하는 방식으로, 주로 <strong>처음이나 마지막으로 실행된 함수를 처리</strong>하는 방식으로 사용된다.
debounce의 예시 코드는 마지막으로 실행된 함수를 처리하는 방식이고, 처음은 다루지 않았다.
debounce의 개념으로 <strong>이벤트의 처음이나 마지막을 처리하는 방식을 나눠서</strong> 살펴볼 수 있다.</p>
<h4 id="leading-edge">Leading edge</h4>
<ul>
<li>처음 실행하는 함수를 처리</li>
<li>처음에 실행한 함수를 실행하고 그 뒤의 이벤트들을 무시함</li>
<li>예시: 입력 필드에 &#39;안녕하세요&#39; 입력시 &#39;ㅇ&#39;만 요청이 보내짐</li>
</ul>
<h5 id="tradiling-edge">Tradiling edge</h5>
<ul>
<li>가장 마지막에 실행하는 함수를 처리</li>
<li>마지막에 실행한 함수를 실행하고 그 전의 이벤트들을 무시함</li>
</ul>
<h3 id="debounce-leading-edge-vs-trottle-차이">Debounce Leading edge vs Trottle 차이</h3>
<p>요청이 들어왔을 때 그 후 일정 시간 동안 모든 요청을 무시한다는 것에서 Leading edge과 Trottle 작동이 유사하기도 하다.
그렇다면 이 두 개는 어떤 차이가 있는걸까?</p>
<h4 id="debounce-leading-edge">Debounce Leading edge</h4>
<ul>
<li>설정한 타이머 시간 안에 요청이 지속적으로 들어올 경우 모든 요청을 무시하게 된다.</li>
</ul>
<h4 id="trottle">Trottle</h4>
<ul>
<li>반면에 Trottle은 지속적으로 요청이 들어올 경우 정해진 타이머 시간이 지나면 요청을 허용한다.</li>
</ul>
<h1 id="디바운싱-적용해보기">디바운싱 적용해보기</h1>
<p>디바운싱을 간단하게 Tradiling edge 방식으로 글자수 계산에 적용해보았다.
디바운싱을 0.3초로 설정해두어 매번 요청하는 부하를 줄일 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/95416731-b0de-4b91-8f8a-170a45764cad/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OSI 7 Layer의 역할을 파악하자]]></title>
            <link>https://velog.io/@dyeon-dev/OSI-7-Layer%EC%9D%98-%EC%A0%84%EC%86%A1</link>
            <guid>https://velog.io/@dyeon-dev/OSI-7-Layer%EC%9D%98-%EC%A0%84%EC%86%A1</guid>
            <pubDate>Thu, 02 Oct 2025 08:20:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>학습목표: OSI 7계층에서 각각 어떤 역할을 하는지 명확하게 파악한다.</p>
<ul>
<li>각 계층의 데이터 전송 단위 / 목적지 식별 방법 / 물리적 하드웨어에 대해 이해한다.</li>
</ul>
</blockquote>
<p>이전까지 OSI 7 계층 중에서 응용계층을 중점적으로 학습하고 구현했었다.
웹 개발을 하다보면 사용자와 밀접하게 닫는 부분이기도 하고 여러 네트워크 서비스를 제공하는 계층이기 때문에 응용 계층을 가장 많이 접할 수 밖에 없다.
그런데 개발자는 다양한 상황의 오류를 마주할 수 있기 때문에 네트워크 통신에 대한 내용도 알아야 한다.</p>
<p>따라서 <strong>네트워크 통신이 일어나는 과정을 7단계로 나눈 네트워크 참조모델인 OSI 7 계층</strong>에 대해 알아보자.</p>
<h2 id="🌐-osi-7-계층-정리">🌐 OSI 7 계층 정리</h2>
<p>네트워크 참조모델에서 각각의 계층이 하는 일은 명확하게 정해져있다. 따라서 계층별 목적에 맞는 프로토콜과 장비를 구성하면 <strong>네트워크의 구성과 설계, 문제 진단과 해결</strong>이 용이해진다.</p>
<h3 id="1-물리-계층physical-layer">1. 물리 계층(Physical Layer)</h3>
<ul>
<li>역할: 비트 신호를 유무선 통신 매체를 통해 운반하는 계층</li>
<li>데이터 단위: 비트(0, 1로 이루어진 신호로 구성)</li>
<li>목적지 식별: 없음(단순 신호 전달)</li>
<li>관련 하드웨어: <strong>허브</strong>, 리피터, 케이블, 커넥터, NIC(물리적 부분)</li>
</ul>
<h3 id="2-데이터-링크-계층-data-link-layer">2. 데이터 링크 계층 (Data Link Layer)</h3>
<ul>
<li>역할: <strong>같은 LAN</strong>에 속한 호스트끼리 올바르게 정보를 주고받기 위한 계층</li>
<li>데이터 단위: 프레임 (Frame)</li>
<li>목적지 식별: <strong>MAC 주소</strong> (물리적 주소, 48비트)</li>
<li>관련 하드웨어: <strong>스위치</strong>, 브리지, NIC(논리적 부분)</li>
</ul>
<p><strong>같은 네트워크에 속한 호스트를 식별할 수 있는 주소(MAC 주소)를 사용</strong>하고, 물리계층을 통해 주고받는 정보에 오류를 식별한다.</p>
<h3 id="3-네트워크-계층-network-layer">3. 네트워크 계층 (Network Layer)</h3>
<ul>
<li>역할: LAN을 넘어 <strong>다른 네트워크</strong>와 통신을 주고받기 위해 필요한 계층</li>
<li>데이터 단위: 패킷 (Packet)</li>
<li>목적지 식별: <strong>IP 주소</strong></li>
<li>관련 하드웨어: <strong>라우터</strong>, L3 스위치</li>
</ul>
<p>네트워크 통신 과정에서 <strong>호스트를 식별할 수 있는 주소(IP 주소)</strong>가 필요하다.</p>
<hr>
<blockquote>
<p>3계층까지는 물리적 계층이었기 때문에 관련 하드웨어가 존재했고, 4계층부터는 소프트웨어적 계층이기 때문에 소프트웨어 프로토콜과 관련이 있다.</p>
</blockquote>
<h3 id="4-전송계층-transport-layer">4. 전송계층 (Transport Layer)</h3>
<ul>
<li>역할: 네트워크를 통해 송수신되는 패킷은 전송 도중에 유실되거나 순서가 뒤바뀔 때도 있는데, 전송 계층은 이러한 상황에 대비해 <strong>신뢰성 있는 전송</strong>을 가능하게 하는 계층</li>
<li>데이터 단위: 세그먼트 (Segment, TCP) / 데이터그램 (Datagram, UDP)</li>
<li>목적지 식별: <strong>포트(Port) 번호</strong></li>
<li>대표 프로토콜: <strong>TCP, UDP</strong></li>
<li>특징: 방화벽, 로드밸런서와 밀접</li>
</ul>
<p><strong>포트(Port) 정보</strong>를 통해 특정 <strong>응용 프로그램과의 연결</strong> 다리 역할을 수행하는 계층이다.</p>
<h3 id="5-세션-계층-session-layer">5. 세션 계층 (Session Layer)</h3>
<ul>
<li>역할: <strong>응용 프로그램 간의 연결 상태를 의미하는 세션(Session)</strong>을 관리하기 위한 계층</li>
<li>데이터 단위: 데이터 (Data)</li>
<li>목적지 식별: 세션 ID, 소켓 (IP + Port 조합)</li>
<li>대표 프로토콜: RPC, NetBIOS</li>
</ul>
<p>프로그램 간의 연결 상태를 유지하거나 새롭게 생성하거나 연결을 끊는 역할을 수행한다.</p>
<h3 id="6-표현-계층-presentation-layer">6. 표현 계층 (Presentation Layer)</h3>
<ul>
<li>역할: 인코딩, 압축, 암호화와 같은 작업을 수행하며 번역가와 같은 역할을 하는 계층 </li>
<li>데이터 단위 (PDU): 데이터 (Data)</li>
<li>목적지 식별: 없음 (데이터 변환만 담당)</li>
<li>특징: JPEG, MP3, SSL/TLS 등의 데이터의 표현 방식 통일</li>
</ul>
<h3 id="7-응용-계층-application-layer">7. 응용 계층 (Application Layer)</h3>
<ul>
<li>역할: 사용자와 네트워크 서비스 간 인터페이스 제공</li>
<li>데이터 단위: 데이터 (Data, Message)</li>
<li>목적지 식별: 애플리케이션 식별자 (URL, 메일 주소 등)</li>
<li>특징:  HTTP, FTP, SMTP 등</li>
</ul>
<table>
<thead>
<tr>
<th>계층</th>
<th>데이터 단위</th>
<th>목적지 식별</th>
<th>주요 역할</th>
<th>하드웨어 예시</th>
</tr>
</thead>
<tbody><tr>
<td>7 응용</td>
<td>데이터</td>
<td>URL, 메일주소</td>
<td>사용자 서비스</td>
<td>-</td>
</tr>
<tr>
<td>6 표현</td>
<td>데이터</td>
<td>없음</td>
<td>암호화/압축</td>
<td>-</td>
</tr>
<tr>
<td>5 세션</td>
<td>데이터</td>
<td>세션 ID</td>
<td>연결 관리</td>
<td>-</td>
</tr>
<tr>
<td>4 전송</td>
<td>세그먼트/데이터그램</td>
<td>포트 번호</td>
<td>신뢰성, 흐름 제어</td>
<td>방화벽, LB</td>
</tr>
<tr>
<td>3 네트워크</td>
<td>패킷</td>
<td>IP 주소</td>
<td>라우팅, 경로 선택</td>
<td>라우터</td>
</tr>
<tr>
<td>2 데이터링크</td>
<td>프레임</td>
<td>MAC 주소</td>
<td>오류 검출, 프레임 전송</td>
<td>스위치, 브리지</td>
</tr>
<tr>
<td>1 물리</td>
<td>비트</td>
<td>없음</td>
<td>신호 전송</td>
<td>허브, 케이블</td>
</tr>
</tbody></table>
<hr>
<p>각 계층에 대해 살펴보았으니, 각 계층에 특징이 될만한 것들을 살펴보자!</p>
<h2 id="물리계층-데이터링크-계층의-통신-매체와-네트워크-장비">물리계층, 데이터링크 계층의 통신 매체와 네트워크 장비</h2>
<ul>
<li>통신 매체는 의외로 모든 성능의 기본이 되는 경우가 많다.</li>
<li>연결 매체의 성능이 뒷받침되지 않으면 호스트의 빠른 속도는 아무런 효용이 없다.<ul>
<li>예를 들어, 호스트가 1초에 100GB를 송신할 수 있어도, 1초에 1GB씩 송수신 가능한 통신 매체를 사용하면 송수신이 불가능하다.</li>
</ul>
</li>
</ul>
<h3 id="물리계층과-데이터링크-계층에-속한-다양한-네트워크-하드웨어">물리계층과 데이터링크 계층에 속한 다양한 네트워크 하드웨어</h3>
<h3 id="1-유선-lan---이더넷">1. 유선 LAN - 이더넷</h3>
<p>물리계층과 데이터링크 계층에는 LAN 내의 호스트들이 올바르게 정보를 주고받을 수 있게 해주는 <strong>이더넷</strong>이 있다.</p>
<ul>
<li>이더넷은 <strong>IEEE 802.3 이름으로 국제 표준화된 기술</strong>이다.</li>
<li>이더넷 프레임은 프리앰블, 수신지 MAC 주소, 타입/길이, 데이터, FCS 등의 정보를 포함하고 있다.</li>
</ul>
<blockquote>
<p>개발자 입장에서 특정 이더넷 표준을 자세히 들여다봐야하는 상황은 많지 않지만, 다음 2가지 사항은 기억해두는 것이 좋다.</p>
<p>1) 오늘날의 (유선) LAN 대부분이 이더넷 표준을 따르기 때문에 대다수의 LAN 장비들이 특정 이더넷 표준을 따른다.
2) 이더넷 표준이 달라지면 통신 매체의 종류를 비롯한 신호 송수신 방법, 나아가 최대 지원 속도도 달라진다.</p>
</blockquote>
<p><strong>유선 통신 매체 - 트위스티드 페어(twisted pair cable) 케이블</strong></p>
<ul>
<li>구리선을 통해 전기적으로 신호를 주고 받는 통신 매체</li>
<li>두가닥(pair)씩 꼬아져있는(twisted) 구리선</li>
<li>성능은 카테고리를 통해 알 수 있다.</li>
</ul>
<p>이더넷 LAN 장비로 허브와 스위치가 있다. 허브와 스위치는 물리 계층과 데이터링크 계층의 중간 노드이다.</p>
<h4 id="허브">허브</h4>
<ul>
<li>물리 계층에 존재</li>
<li>여러 대의 호스트를 브로드캐스트로 연결한다.</li>
<li>허브는 전달받은 신호를 모든 포트로 내보낸다.</li>
<li>오늘날에는 허브 대신 스위치를 사용하는 경우가 많다.</li>
</ul>
<h4 id="스위치">스위치</h4>
<ul>
<li>스위치는 허브의 한계를 보완하기 위한 데이터링크 계층의 네트워크 장비이다.</li>
<li>스위치가 전달받은 신호를 원하는 포트에만 내보낼 수 있다.</li>
<li>MAC 주소 테이블을 만들어 참조를 통해 가능하다.</li>
<li>VLAN 기능을 통해 같은 스위치에 연결된 모든 호스트를 하나의 네트워크로 간주하고 싶지 않을 때, 여러 논리적인 네트워크로 나누고 싶을 때 사용한다.</li>
</ul>
<h3 id="2-무선-lan---wifi">2. 무선 LAN - WiFi</h3>
<p><strong>무선 통신 매체 - 전파와 WiFi</strong></p>
<ul>
<li>전파는 약 3kHz부터 3THz 사이의 진동수를 갖는 <strong>전자기파</strong>를 의미한다.</li>
<li>진동수 <strong>2.4GHz, 5GHz</strong> 정도는 알아두는 것이 좋다.</li>
<li>주로 와이파이를 사용할 때 활용된다.</li>
</ul>
<h4 id="와이파이">와이파이</h4>
<ul>
<li><strong>IEEE 802.11 표준</strong>을 따르는 무선 LAN 기술이다.</li>
<li>같은 지역 내에 2.4GHz / 5GHz 대역을 사용하는 무선 네트워크가 여러개 존재할 수 있다.</li>
</ul>
<h2 id="네트워크-계층---ip">네트워크 계층 - IP</h2>
<p>LAN을 넘어서 다른 네트워크와 통신을 주고받으려면 네트워크 계층 이상의 기술이 필요하다.</p>
<p>여기서 중요하게 사용되는 기술이 네트워크 계층의 핵심 프로토콜인 IP이다.</p>
<h3 id="ip의-목적과-특징">IP의 목적과 특징</h3>
<p>목적은 크게 2가지이다.</p>
<ol>
<li><strong>주소 지정</strong>: <strong>네트워크 간의 통신 과정에서 호스트를 특정</strong>하는 역할을 수행한다.</li>
</ol>
<ul>
<li>패킷을 올바르게 전송하기 위해 송신지와 수신지의 <strong>IP 주소</strong>와 <strong>MAC 주소</strong>가 모두 필요하다.</li>
<li>MAC 주소보다는 <strong>IP 주소가 우선적으로 활용</strong>된다.</li>
<li>서로 다른 네트워크에 속한 두 호스트가 네트워크 간 통신을 수행할 때 <strong>IP 주소를 바탕으로 패킷을 전달</strong>한다. </li>
<li>이때 <strong>라우터 장비</strong>가 사용된다. 최적의 경로를 결정하고 해당 경로로 패킷을 내보낸다. (라우팅 과정) </li>
</ul>
<ol start="2">
<li><strong>단편화</strong>: 데이터를 여러 <strong>IP 패킷으로 쪼개어 보내는 역할</strong>을 수행한다.</li>
</ol>
<ul>
<li>MTU(Maximum Transmission Unit) 단위에 따라 패킷을 쪼애서 전송한다. - 최대 1500 바이트</li>
<li>쪼개서 전송된 패킷들은 수신지에서 재조합된다.</li>
<li>IP 패킷 헤더: 식별자 / 플래그 / 단편화 오프셋</li>
</ul>
<p>특징</p>
<ul>
<li><strong>신뢰할 수 없는 통신이자, 비연결형 통신</strong></li>
<li>전송 계층의 TCP, UDP의 존재 목적과 직결된다.</li>
<li>비연결형 프로토콜 - 상대 호스트의 수신 가능 여부는 고려하지 않고, 수신지를 향해 그저 패킷을 전송할 뿐</li>
</ul>
<blockquote>
<p>스위치와 라우터의 역할은 비슷하지만, 라우터는 ip자체로 제한(차단)을 둘수있다. 따라서 외부 ip주소를 관리가능하다. 현대에는 스위치도 발전을 해서 라우터의 역할까지 수행할 수 있다.</p>
</blockquote>
<h2 id="전송-계층---tcp와-udp">전송 계층 - TCP와 UDP</h2>
<h3 id="ip의-한계">IP의 한계</h3>
<p>IP는 네트워크 계층의 프로토콜로, <strong>&quot;주소 지정&quot;과 &quot;패킷 전달/단편화&quot;</strong> 까지만 책임진다.
하지만 다음은 책임지지 않는다.</p>
<ul>
<li>패킷이 도착했는지 보장하지 않음 (손실 가능)</li>
<li>패킷이 순서대로 도착하는지 보장하지 않음 (라우팅 경로가 다르면 순서가 바뀔 수 있음)</li>
<li>패킷이 중복될 수도 있음</li>
<li>패킷의 무결성을 보장하지 않음 (손상될 수도 있음)
즉, IP만으로는 &quot;데이터를 확실하게 전달한다&quot;는 보장이 전혀 없다.</li>
</ul>
<p>전송 계층의 TCP와 UDP가 존재하는 이유는, <strong>IP 혼자서는 통신 품질을 보장하지 못하기 때문</strong>이다.</p>
<h3 id="tcp-transmission-control-protocol">TCP (Transmission Control Protocol)</h3>
<p>목적: <strong>IP 위에서 신뢰성 있는 연결 제공</strong>
기능:</p>
<ul>
<li>연결 지향적 (3-way handshake로 세션 수립)</li>
<li>데이터 전송 순서 보장 (시퀀스 번호)</li>
<li>데이터 무결성 확인 (체크섬, 재전송)</li>
<li>흐름 제어, 혼잡 제어</li>
</ul>
<p>활용 예시: 웹(HTTP/HTTPS), 이메일(SMTP), 파일 전송(FTP)</p>
<p>즉, “IP가 불안정하니 내가 대신 신뢰성을 책임지겠다.”</p>
<h3 id="udp-user-datagram-protocol">UDP (User Datagram Protocol)</h3>
<p>목적: <strong>IP의 단순, 빠른 성격을 그대로 활용</strong>
기능:</p>
<ul>
<li>비연결형, 확인/재전송 없음</li>
<li>순서 보장 없음</li>
<li>단순히 IP 주소 + 포트만 붙여서 보냄</li>
</ul>
<p>활용 예시: 스트리밍, 게임, DNS</p>
<p>즉, “IP의 불안정성을 그대로 쓰지만, 대신 속도와 단순성을 확보하겠다.”</p>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/a457a9a7-0df1-4eb3-a5b1-269e27159ad3/image.png" alt=""></p>
<p>네트워크 상에서 호스트가 실행하는 프로세스를 어떻게 식별할 수 있을까?
포트 번호를 통해 식별할 수 있다.
네트워크 패킷을 주고받는 프로세스에는 포트 번호가 할당된다.
즉, IP주소 + 포트 번호를 통해 &#39;특정 호스트가 실행하는 특정 프로세스&#39;를 식별할 수 있다.
이러한 식별은 전송 계층(TCP, UDP)의 주된 목적이다.
<code>192.168.0.15:8000</code> 같은 포트번호를 헤더에서 명시하는 필드가 존재한다. </p>
<h3 id="tcp의-연결-수립">TCP의 연결 수립</h3>
<p>TCP는 UDP와 달리 송수신 이전에 연결을 수립하고, 송수신 이후에는 연결을 종료한다. TCP는 패킷을 주고받기 전에 연결 수립 과정을 거친다.
TCP의 연결 수립는 쓰리 웨이 핸드셰이크(Three-way-handshake)를 통해 이뤄진다. </p>
<ol>
<li>[송수신 방향 A -&gt; B] SYN 세그먼트 전송</li>
<li>[송수신 방향 B -&gt; A] SYN+ACK 세그먼트 전송</li>
<li>[송수신 방향 A -&gt; B] ACK 세그먼트 전송</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/e9b5769a-e9a8-45c8-8240-7145e04f9e7b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[민감 정보를 담은 쿠키를 안전하게 보호하자!]]></title>
            <link>https://velog.io/@dyeon-dev/%EB%AF%BC%EA%B0%90-%EC%A0%95%EB%B3%B4%EB%A5%BC-%EB%8B%B4%EC%9D%80-%EC%BF%A0%ED%82%A4%EB%A5%BC-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%B3%B4%ED%98%B8%ED%95%98%EC%9E%90</link>
            <guid>https://velog.io/@dyeon-dev/%EB%AF%BC%EA%B0%90-%EC%A0%95%EB%B3%B4%EB%A5%BC-%EB%8B%B4%EC%9D%80-%EC%BF%A0%ED%82%A4%EB%A5%BC-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%B3%B4%ED%98%B8%ED%95%98%EC%9E%90</guid>
            <pubDate>Fri, 26 Sep 2025 02:51:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>학습 목표: 민감한 정보를 담은 쿠키를 안전하게 보호하기 위해 브라우저 레벨부터 서버 속성까지 다양한 방법을 학습한다.</p>
</blockquote>
<h2 id="웹-브라우저에-저장되는-쿠키">웹 브라우저에 저장되는 쿠키</h2>
<p>로그인을 진행하면서 세션-쿠키 방식으로 구현했다.
이때 세션ID를 브라우저의 쿠키에 저장하게 되고, 브라우저와 서버는 쿠키 정보를 주고받는다.</p>
<p>쿠키를 저장하는 공간을 전통적으로 쿠키 통(COOKIE_JAR)라고 부른다.
브라우저는 헤더로 Set-Cookie를 받으면 COOKIE_JAR를 업데이트 해야한다.</p>
<h3 id="쿠키의-특징">쿠키의 특징</h3>
<ul>
<li>쿠키는 브라우저의 신원으로, 같은 도메인에서 동일한 쿠키가 사용된다. 그래서 동일한 쿠키를 사용하면 동일한 사용자라고 인식한다. 이는 보안 측면에서 문제가 될 수 있다. </li>
<li>같은 도메인 내에서는 <strong>모든 API 요청에 자동으로 세션 쿠키가 포함</strong>된다.</li>
<li>이는 개인 프라이빗 데이터가 유출되는 것이다.</li>
<li>공격자가 서버나 브라우저를 이용해 쿠키를 탈취할 가능성이 있다.</li>
</ul>
<h3 id="쿠키의-한계">쿠키의 한계</h3>
<p>이렇게 <strong>보안 기능이 따로 없는 쿠키를 위해 실무적으로 방어</strong>를 해줘야한다.</p>
<ul>
<li>브라우저 레벨: SOP(기본), CORS 정책 적절 설정</li>
<li>쿠키 옵션: <code>HttpOnly</code>, <code>Secure</code>, <code>SameSite</code></li>
<li>서버 쪽: CSRF 토큰, Origin/Referer 검사, 출력 이스케이프, CSP로 XSS 차단</li>
</ul>
<h2 id="동일-출처-정책sop">동일 출처 정책(SOP)</h2>
<p>일단 감사하게도, 브라우저 레벨에서 동일 출처 정책(SOP) 기능이 장착되어있다.
서버가 Set-Cookie로 세션을 내려주면 브라우저가 동일 출처로 자동 저장·전송한다.
동일 출처란 같은 origin, 같은 호스트명, 같은 포트를 가진 웹페이지에만 전송을 허용하는 것이다.</p>
<h3 id="왜-sop가-중요하냐면">왜 SOP가 중요하냐면</h3>
<ul>
<li>SOP는 <strong>다른 출처(교차 출처)의 스크립트가 해당 응답을 읽지 못하게</strong> 막는다.</li>
<li>즉, 공격자 페이지에서 요청을 <em>보내는 것</em> 자체는 (브라우저가 허용하면) 가능하더라도, <strong>응답을 읽어 처리하는 단계</strong>를 차단함으로써 데이터 유출을 막는다.
이 방법으로, 웹사이트의 프라이빗 데이터가 그 웹사이트에만 유지되고 다른 서버의 공격자에게 유출되지 않을 수 있다.</li>
</ul>
<h3 id="sop가-못막는-것">SOP가 못막는 것</h3>
<p>SOP는 교차 출처 간의 호출을 막지만, 이런 SOP가 막지 못하는 게 있다. 폼 전송과 같은 교차 사이트 요청 위조(CSRF) 공격을 가능하게 한다. 즉, 공격자가 사용자의 브라우저로 요청을 보내게 해서 쿠키는 자동으로 포함될 수 있는 것이다.</p>
<pre><code class="language-html">&lt;!-- 공격자가 만든 페이지 --&gt;
&lt;form action=&quot;https://bank.example/add&quot; method=&quot;POST&quot;&gt;
  &lt;input name=&quot;amount&quot;&gt;
  &lt;button&gt;OK&lt;/button&gt;
&lt;/form&gt;</code></pre>
<h2 id="csrf-방어">CSRF 방어</h2>
<p>SOP의 한계로 인해 CSRF 방어(토큰, SameSite 등)가 필요하다.
폼 전송을 위한 안전한 솔루션은 <strong>SameSite</strong> 쿠키이다. 
만약 서버가 자신의 쿠키를 SameSite로 설정해두면, 브라우저는 폼 전송이 교차 차이트일 때 그 쿠키를 전송하지 않는다.</p>
<ul>
<li>Set-Cookie: SameSite=Lax</li>
<li>SameSite 속성은 Lax, Strict, None 3가지이다.</li>
</ul>
<table>
<thead>
<tr>
<th>속성</th>
<th>교차 사이트 GET 요청 (링크 클릭 등)</th>
<th>교차 사이트 POST / AJAX / iframe 등</th>
<th>동일 사이트 GET/POST 요청</th>
<th>특징 / 주의점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Lax</strong></td>
<td>✅ 쿠키 전송 (탑레벨 내비게이션의 안전한 GET은 허용)</td>
<td>❌ 쿠키 전송 안 함</td>
<td>✅ 쿠키 전송</td>
<td>기본값. 일반적인 사용성(링크 통한 로그인 유지)과 보안을 균형 있게 지원</td>
</tr>
<tr>
<td><strong>Strict</strong></td>
<td>❌ 쿠키 전송 안 함</td>
<td>❌ 쿠키 전송 안 함</td>
<td>✅ 쿠키 전송</td>
<td>가장 보안 강력. 교차 사이트에서 들어오는 모든 요청에 쿠키 차단 → 외부 리디렉션(OAuth, 결제) 깨질 수 있음</td>
</tr>
<tr>
<td><strong>None</strong></td>
<td>✅ 쿠키 전송</td>
<td>✅ 쿠키 전송</td>
<td>✅ 쿠키 전송</td>
<td>교차 사이트 요청에도 항상 쿠키 전송. 반드시 <code>Secure</code>와 함께 사용해야 하고 CSRF 방어(토큰, Origin 검증 등) 필요. <br>외부 연동(소셜 로그인, 결제 콜백 등) 시 사용</td>
</tr>
</tbody></table>
<h2 id="xss-공격의-방어막">XSS 공격의 방어막</h2>
<ul>
<li><code>HttpOnly</code><ul>
<li><strong>브라우저 내부에서만 사용</strong> (요청 시 자동 포함)</li>
<li><strong>JS 코드에서는 접근 불가</strong></li>
</ul>
</li>
<li><code>Content-Security-Policy(CSP)</code>: 콘텐츠 보안 정책</li>
</ul>
<h3 id="콘텐츠-보안-정책">콘텐츠 보안 정책</h3>
<p>XSS 공격의 방어막 중 하나는 Content-Security-Policy(CSP) 헤더이다.
이 헤더의 전체 사양은 복잡하지만, 가장 단순한 예시는 default-src 키워드 뒤에 공백으로 구분된 서버 목록을 설정하는 것이다.</p>
<ul>
<li><code>Content-Security-Policy: default-src &#39;self&#39;</code></li>
</ul>
<p>브라우저에게 리스트된 출처를 제외한 CSS, JS, 이미지 등 어떤 리소스도 로드하지 말라고 요청한다. 
만약 공격자가 페이지에 <code>&lt;script&gt;</code>를 추가했더라도 브라우저는 그 스크립트를 로드하거나 실행하지 않는다.</p>
<p>[로그인 요청 시 세션-쿠키 설정]
<img src="https://velog.velcdn.com/images/dyeon-dev/post/a303f606-fec7-4931-a15c-5c917f472aed/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WS + WAS 서버 Nginx로 배포하기(ft. 배포 스크립트)]]></title>
            <link>https://velog.io/@dyeon-dev/WS-WAS-Nginx-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@dyeon-dev/WS-WAS-Nginx-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Thu, 25 Sep 2025 21:39:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>학습 목적: WS 와 WAS 를 명확하게 책임/역할 별로 구분하는 것</p>
</blockquote>
<p>학습 목적을 달성하기 위해 <strong>정적 컨텐츠는 Web Server, 동적 컨텐츠는 WAS</strong>가 담당하도록 했습니다. 쉽게 말해, WAS 앞단에 Web Server를 하나 둬서 효율적으로 서버 트래픽을 관리하는 것입니다.</p>
<p>브라우저에서 정적 컨텐츠를 만나면 <strong>Web Server는 WAS로 서빙하는 역할</strong>을 해주어야 합니다. 이러한 역할을 해주는 고성능 웹서버 프로그램인 <strong>Nginx</strong>를 사용했습니다.</p>
<p>배포를 위해 <strong>1) Nginx</strong>와 <strong>2) pm2</strong>를 설정해주고, <strong>3) 배포 스크립트</strong>를 작성했어요. 🛠️</p>
<h3 id="배포-환경">배포 환경</h3>
<ul>
<li>VMware Fusion</li>
<li>Ubuntu 24.04</li>
<li>서버 스택<ul>
<li>git</li>
<li>npm</li>
<li>pm2: 앱은 PM2로 127.0.0.1:3000에서 동작</li>
<li>Nginx: Node 3000 + Nginx 80</li>
</ul>
</li>
</ul>
<p>우분투 서버로 배포를 진행했고, 아래와 같은 로직으로 구성했습니다.
<img src="https://velog.velcdn.com/images/dyeon-dev/post/ec2cf93b-2896-4546-851f-39fb0da50405/image.png" alt="">
빌드 → Nginx 설정 적용 → 정적, 동적 컨텐츠 리버스 프록시 → pm2 프로세스로 WAS 실행</p>
<blockquote>
<p>이 글에서 VMware 우분투 환경 설정 과정은 생략합니다.</p>
</blockquote>
<h2 id="1-nginx-설정">1) Nginx 설정</h2>
<p>우분투 서버에 Nginx 설치 및 시작</p>
<ul>
<li>Ubuntu에서 Nginx를 설치하면 자동으로 systemd 서비스(nginx.service)에 등록됩니다.</li>
<li>서버가 부팅될 때 systemd가 nginx를 기동합니다.
→ 즉, 기본적으로 OS 레벨에서 항상 실행 중이에요.<pre><code>sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx    # 부팅 시 자동 시작
sudo systemctl start nginx
sudo systemctl status nginx --no-pager # active (running) 상태여야 정상.</code></pre></li>
</ul>
<p>방화벽(ufw) 개방</p>
<pre><code>sudo ufw allow &#39;Nginx Full&#39;   # 80/443 허용
sudo ufw status</code></pre><p>테스트</p>
<pre><code class="language-sudo">→ 설정 OK 나와야 함.
외부 브라우저에서 확인
http://172.30.1.86/
→ 기본 Nginx 웰컴 페이지가 보여야 정상.</code></pre>
<p>동작 확인 (서버 → 외부 순서)</p>
<pre><code># Nginx가 80 포트 리슨 중인지
sudo ss -lntp &#39;sport = :80&#39;

# Nginx 경유 헬스체크(프록시 OK 확인)
curl -i http://127.0.0.1/health

# 정적 파일 핑(있을 경우)
curl -I http://127.0.0.1/assets/</code></pre><p>외부(맥)에서</p>
<pre><code>http://172.30.1.86/health
http://172.30.1.86/</code></pre><p>Nginx에 .conf 파일 만들기</p>
<pre><code>sudo mkdir -p /etc/nginx/conf.d
sudo nano /etc/nginx/conf.d/codestargram.conf</code></pre><details>
<summary>.conf에서 리버스 프록시 설정</summary>
<div markdown="1">

<pre><code class="language-conf"># /etc/nginx/conf.d/codestargram.conf

# 앱(백엔드) 업스트림: PM2가 띄운 Node 서버
upstream codestargram_app { # 백엔드 서버(3000)를 논리적 그룹으로 묶는 곳
    server 127.0.0.1:3000;
    keepalive 64; # Nginx ↔ WAS 사이 연결 재사용. 요청마다 TCP를 새로 열지 않고, 성능/지연에 이점.
}

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;  # 어떤 Host로 오든(도메인/IP) 수신

    # 정적 빌드 자산 (Vite dist/assets)
    location /assets/ {
        alias /var/www/codestargram/repo/web-p3-codestargram2/dist/assets/;
        access_log off;
        expires 1y;
        # expires/Cache-Control로 강한 캐시 적용해 성능↑ (Vite의 해시 파일명과 궁합 좋음)
        add_header Cache-Control &quot;public, max-age=31536000, immutable&quot;;
        try_files $uri =404;
    }

    # 헬스체크(앱으로 프록시) - 모니터링/로드밸런서가 이 경로를 주기적으로 호출해 상태 확인
    location = /health {
        proxy_pass http://codestargram_app;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection        &quot;&quot;;
    }

    # 나머지 모든 경로는 Node 앱으로 프록시
    location / {
        proxy_pass http://codestargram_app;
        proxy_http_version 1.1;

        # 표준 프록시 헤더
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection        &quot;&quot;;
    }
}
</code></pre>
</div>
</details>


<p><strong>📌 conf 흐름</strong>
클라이언트 요청 → Nginx :80 수신</p>
<ul>
<li>/assets/* → Nginx가 직접 <code>/var/www/codestargram/.../dist/assets/</code>에서 파일 제공(고속·캐시 가능)</li>
<li>/health → Nginx가 PM2에서 띄운 Node.js WAS(127.0.0.1:3000)로 프록시</li>
<li>나머지 /* (/login, /register, /api/...,그 외 모든 경로) → 전부 WAS(127.0.0.1:3000)로 프록시</li>
</ul>
<p><strong>Nginx 적용 시 장점</strong></p>
<ul>
<li>Nginx가 빠르게 캐시/서빙 → WAS 부하 감소</li>
<li>동적/API는 프록시로 WAS에 전달 → 보안/구조 분리</li>
<li>프록시 헤더로 진짜 IP/프로토콜을 WAS가 정확히 알 수 있음</li>
<li>upstream으로 확장성(멀티 WAS, 로드밸런싱) 확보</li>
</ul>
<h2 id="2-pm2-설정">2) pm2 설정</h2>
<h3 id="pm2를-왜-사용할까">pm2를 왜 사용할까?</h3>
<p>PM2는 Process Manager의 약자로 NodeJS 프로세서를 관리하는 <strong>원활한 서버 운영을 위한 패키지</strong>입니다. 개발할 때 nodemon을 쓴다면, 배포할 때는 pm2를 씁니다.</p>
<ul>
<li>서버를 백그라운드에서 실행시켜 터미널을 닫아도 꺼지지 않음.</li>
<li>서비스를 제공하고 있는 도중 서버가 다운되어도 서버를 다시 켜줌.</li>
<li>Node.js는 싱글 스레드 기반이지만, 멀티 코어 혹은 하이퍼 스레딩을 사용할수 있게 해줌.</li>
<li>클라이언트로부터 요청이 올 때 알아서 요청을 여러 노드 프로세스에 고르게 분배함. (로드 밸런싱)</li>
</ul>
<h3 id="pm2-설정-파일-만들기-ecosystemconfigjs">pm2 설정 파일 만들기 (ecosystem.config.js)</h3>
<p>pm2를 보다 수월하게 관리하기 위해 ecosystem.config.js 라는 파일을 만들어 설정할 수 있습니다. 프로젝트 루트에 ecosystem.config.js을 생성합니다.</p>
<pre><code>pm2 ecosystem # ecosystem.config.js 파일이 생성</code></pre><p>저는 ESM을 사용중이었기 때문에 ecosystem.config.cjs 로 변경했습니다.</p>
<pre><code class="language-js">module.exports = {
  /* apps 항목은 pm2에 사용할 옵션을 기재 */
  apps : [{
    name: &#39;codestargram2&#39;, // app 이름 
    script: &#39;server/app.js&#39;, // 실행할 스크립트 파일
    cwd: __dirname,          // 레포 루트 기준 실행 (deploy.sh가 cd 후 실행)
    // 워칭(운영에서는 비활성)
    watch: false,
    // watch: [&quot;server&quot;, &quot;src&quot;],       // 개발용: 필요 시 경로 지정
    // ignore_watch: [&quot;node_modules&quot;, &quot;logs&quot;, &quot;dist&quot;],

    // 로그
    merge_logs: true,
    time: true,
    out_file: &quot;/var/www/codestargram/logs/out.log&quot;,
    error_file: &quot;/var/www/codestargram/logs/error.log&quot;,

    env: {
      NODE_ENV: &quot;production&quot;,
      PORT: 3000                      // Nginx upstream과 일치
    },
    env_development: {
      NODE_ENV: &quot;development&quot;,
      PORT: 3000
    }
  }],
};</code></pre>
<h2 id="3-배포-스크립트-작성-과정">3) 배포 스크립트 작성 과정</h2>
<h3 id="1-빌드">1. 빌드</h3>
<p>정적 컨텐츠를 Nginx를 통해 처리한다고 했습니다. 정적 컨텐츠 산출물을 위해 <strong>Vite의 빌드 과정</strong>이 필요합니다.
Vite는 public/ 하위 파일을 그대로 dist/로 복사하고, 루트에 있는 index.html을 도입점으로 하여 <strong>dist/에 AST 기반 최적화를 통해 빌드 산출물을 생성</strong>합니다.
이걸 서버에서 사용할 수 있게끔 해야하기 때문에 코드를 서버로 전달해야합니다. </p>
<h3 id="2-코드-배포-방식">2. 코드 배포 방식</h3>
<p>처음에는 코드 배포를 어떻게 할지부터 고민했습니다.
맥에 있는 프로젝트를 우분투 서버에서 배포 스크립트(<code>deploy.sh</code>)로 돌려야 했기 때문에 아래의 두 가지 방식 중에 고민했습니다.</p>
<p><strong>옵션 A) Git 기반 (반복 배포용)</strong></p>
<ul>
<li>서버가 Git clone으로 코드를 가져오는 구조</li>
<li>로컬에서 푸시 → 서버에서 deploy.sh 실행만 하면 됨. (또는 GitHub Actions로 자동 실행)</li>
</ul>
<p><strong>옵션 B) rsync로 맥 → 서버 직접 동기화(즉시 적용용)</strong></p>
<ul>
<li>맥의 폴더를 서버로 복사하는 구조</li>
<li><code>deploy.sh</code>의 “git 변경 감지” 부분은 맞지 않으므로 건너뛰거나 비활성화해야 함.</li>
</ul>
<h4 id="옵션-a-선택">옵션 A 선택</h4>
<p>배포 스크립트가  <code>git fetch / rev-parse</code>를 쓰기 때문에, Git을 사용하는 구조가 깔끔합니다. 
또한, 운영 환경에서는 항상 Git 저장소와 동기화된 코드로 빌드하는 게 가장 안전하다고 판단했습니다.
그래서 “<strong>로컬에서 코드 수정 → GitHub에 push → 서버에서 pull 후 빌드</strong>” 흐름을 기본 원칙으로 잡았습니다.</p>
<h3 id="3-배포-스크립트-작성">3. 배포 스크립트 작성</h3>
<p>매번 수동으로 git pull, npm install, npm run build, pm2 restart 하는 건 번거롭고, 실수하기 쉽습니다.
그래서 <strong>이 과정을 하나로 묶은 자동화 스크립트(deploy.sh)</strong>를 만들기로 했습니다.</p>
<p>정리하면, “<strong>Git → 빌드 → 배포 → PM2 재시작 → 헬스체크 → 롤백 → Nginx 리로드</strong>” 전체 과정을 자동화한 파일입니다.</p>
<p>서버에서 직접 스크립트 파일을 만들어서 작성해도 되지만, 개발 편의성을 위해 프로젝트에서 스크립트를 작성하고 맥(로컬) 환경에 있는 배포 스크립트를 서버로 업로드 해주었습니다.</p>
<details>
<summary>deploy.sh</summary>
<div markdown="1">

<pre><code>
#!/usr/bin/env bash
set -euo pipefail

# ==========================
# 고정 설정
# ==========================
APP_NAME=&quot;codestargram2&quot;
REPO_DIR=&quot;/var/www/codestargram/repo/web-p3-codestargram2&quot;   # Git 작업 디렉토리
STATIC_DST=&quot;/var/www/codestargram/repo/web-p3-codestargram2/dist&quot;    # Nginx 정적 자산 경로
HEALTH_URL=&quot;http://127.0.0.1:3000/health&quot;                    # 앱 헬스체크 엔드포인트
RELEASES_DIR=&quot;/var/www/codestargram/releases&quot;                # 빌드 산출물 보관
KEEP_RELEASES=5                                              # 보관 개수
NODE_ENV=&quot;production&quot;

# 배포 브랜치
BRANCH=&quot;deploy&quot;

# ==========================
# 유틸 함수
# ==========================
log() { printf &quot;\033[1;36m[deploy]\033[0m %s\n&quot; &quot;$*&quot;; }
err() { printf &quot;\033[1;31m[error]\033[0m %s\n&quot; &quot;$*&quot; &gt;&amp;2; }
die() { err &quot;$*&quot;; exit 1; }
need() { command -v &quot;$1&quot; &gt;/dev/null 2&gt;&amp;1 || die &quot;필요 명령어가 없습니다: $1&quot;; }

# ==========================
# 전제 체크
# ==========================
need git; need npm; need pm2; need curl; need rsync
mkdir -p &quot;$STATIC_DST&quot; &quot;$RELEASES_DIR&quot;

[ -d &quot;$REPO_DIR/.git&quot; ] || die &quot;REPO_DIR 경로가 Git repo가 아닙니다: $REPO_DIR&quot;

log &quot;USING REPO_DIR=$REPO_DIR, BRANCH=$BRANCH&quot;
cd &quot;$REPO_DIR&quot;

# ==========================
# Git 동기화
# ==========================
log &quot;원격 브랜치 존재 확인: origin/$BRANCH&quot;
git ls-remote --exit-code --heads origin &quot;$BRANCH&quot; &gt;/dev/null \
  || die &quot;원격에 브랜치가 없습니다: origin/$BRANCH&quot;

log &quot;git fetch origin $BRANCH&quot;
git fetch origin &quot;$BRANCH&quot;

LOCAL=$(git rev-parse HEAD || echo &quot;UNKNOWN&quot;)
REMOTE=$(git rev-parse &quot;origin/$BRANCH&quot;)

if [ &quot;$LOCAL&quot; = &quot;$REMOTE&quot; ]; then
  log &quot;변경 사항 없음 → 종료&quot;
  exit 0
fi

log &quot;변경 사항 감지됨 (local=$LOCAL, remote=$REMOTE) → 원격 상태로 맞춤&quot;
git reset --hard &quot;origin/$BRANCH&quot;

# ==========================
# 빌드
# ==========================
log &quot;의존성 설치 (빌드용: dev 포함)&quot;
if [ -f package-lock.json ]; then
  npm ci
else
  npm install
fi

log &quot;빌드 실행&quot;
export NODE_ENV=&quot;$NODE_ENV&quot;
npm run build

# (선택) 런타임 최적화: dev 제거
log &quot;런타임 최적화: devDependencies 제거&quot;
npm prune --omit=dev


[ -d &quot;dist&quot; ] || die &quot;dist 디렉토리가 없습니다. 빌드 스크립트를 확인하세요.&quot;

# ==========================
# 릴리스 보관 + 정적 자산 배포
# ==========================
TS=$(date +&quot;%Y%m%d-%H%M%S&quot;)
RELEASE_DIR=&quot;$RELEASES_DIR/$TS&quot;
log &quot;릴리스 보관 디렉토리: $RELEASE_DIR&quot;
mkdir -p &quot;$RELEASE_DIR&quot;
cp -R dist/* &quot;$RELEASE_DIR/&quot;

log &quot;현재 정적 자산 백업 및 교체&quot;
mkdir -p &quot;$STATIC_DST/.backup&quot;
if [ -n &quot;$(ls -A &quot;$STATIC_DST&quot; 2&gt;/dev/null || true)&quot; ]; then
  rsync -a --delete &quot;$STATIC_DST/&quot; &quot;$STATIC_DST/.backup/$TS/&quot;
fi
rsync -a --delete &quot;$RELEASE_DIR/&quot; &quot;$STATIC_DST/&quot;

# 오래된 릴리스 정리
log &quot;오래된 릴리스 정리(최신 ${KEEP_RELEASES}개만 유지)&quot;
ls -1t &quot;$RELEASES_DIR&quot; | tail -n +$((KEEP_RELEASES+1)) | while read -r old; do
  rm -rf &quot;$RELEASES_DIR/$old&quot;
done || true

# ==========================
# PM2 무중단 재시작
# ==========================
log &quot;PM2 startOrReload (ecosystem.config.js)&quot;
pm2 startOrReload ecosystem.config.cjs --update-env || {
  err &quot;ecosystem.config.cjs 실행 실패 → 단일 스크립트로 재시도&quot;
  pm2 restart &quot;$APP_NAME&quot; || pm2 start server/app.js --name &quot;$APP_NAME&quot;
}

# ==========================
# 헬스체크
# ==========================
log &quot;헬스체크: $HEALTH_URL&quot;
RETRY=20
SLEEP=1
ok=false
for i in $(seq 1 $RETRY); do
  if curl -fsS &quot;$HEALTH_URL&quot; &gt;/dev/null 2&gt;&amp;1; then
    ok=true; break
  fi
  sleep &quot;$SLEEP&quot;
done

if [ &quot;$ok&quot; != true ]; then
  err &quot;헬스체크 실패! 롤백 수행&quot;
  if [ -d &quot;$STATIC_DST/.backup/$TS&quot; ]; then
    rsync -a --delete &quot;$STATIC_DST/.backup/$TS/&quot; &quot;$STATIC_DST/&quot;
  fi
  pm2 reload &quot;$APP_NAME&quot; || true
  die &quot;배포 실패(헬스체크 불통). 롤백 완료.&quot;
fi

# ==========================
# Nginx 리로드(선택)
# ==========================
if command -v nginx &gt;/dev/null 2&gt;&amp;1; then
  log &quot;Nginx 설정 테스트&quot;
  if nginx -t; then
    log &quot;Nginx 리로드&quot;
    nginx -s reload || systemctl reload nginx || true
  else
    err &quot;Nginx 설정 테스트 실패(무시하고 진행)&quot;
  fi
fi

log &quot;✅ 배포 완료: $TS&quot;</code></pre></div>
</details>

<h4 id="📌-deploysh-스크립트-요약">📌 deploy.sh 스크립트 요약</h4>
<p><strong>1. 배포 스크립트를 진행할 브랜치를 deploy로 설정했어요.</strong>
따로 배포 전용 브랜치를 만들어서 하는 것을 추천합니다.</p>
<p><strong>2. Git 동기화 부분을 넣었어요.</strong>
스크립트 초반에는 항상 Git 원격 브랜치와 맞추는 절차를 넣었습니다.</p>
<ul>
<li><code>git fetch</code> 로 원격 최신 상태 가져오기</li>
<li><code>git rev-parse</code> 로 로컬/원격 해시 비교</li>
<li>해시 값이 다르면 <code>git reset --hard origin/&lt;branch&gt;</code> 로 맞췄습니다.<ul>
<li><code>reset --hard</code> 말고 <code>git merge</code>로 해도 되지만, 서버에서 실수로 수정 후 커밋된게 있으면 충돌이 발생할 수 있고, 수동 머지를 해야합니다. 결과적으로 배포 자동화가 깨지고, 사람이 개입해야 합니다.</li>
<li><code>reset --hard</code>을 사용하면 현재 작업 트리를 원격 브랜치와 100% 동일하게 만들기 때문에 <strong>항상 깔끔한 상태에서 빌드를 보장</strong>할 수가 있습니다. </li>
</ul>
</li>
</ul>
<p><strong>3. 빌드 &amp; 배포 과정을 자동화했어요.</strong>
Git 동기화가 끝나면 바로 의존성 설치 후 빌드를 진행합니다.</p>
<ul>
<li><code>npm ci</code> 또는 <code>npm install</code> 로 의존성 설치</li>
<li><code>npm run build</code> 로 빌드</li>
<li>산출물(dist/)을 <code>/var/www/codestargram/releases</code> 에 타임스탬프별로 보관</li>
<li>최신 결과물을 <code>/var/www/codestargram/repo/web-p3-codestargram2/dist</code> 로 덮어쓰기</li>
</ul>
<p>그리고 오래된 릴리스는 자동으로 정리해서, 서버 공간을 아끼도록 했습니다.</p>
<p><strong>4. 서버 프로세스 재시작을 PM2로 처리했어요.</strong>
빌드된 코드가 준비되면 배포 스크립트와 pm2 연동을 진행합니다.</p>
<ul>
<li><code>pm2 startOrReload ecosystem.config.cjs</code> 실행<ul>
<li>최초 실행 시 → PM2가 새 앱을 띄우고</li>
<li>이후 실행 시 → 무중단 reload가 됨</li>
</ul>
</li>
<li>만약 설정 파일이 없다면, <code>server/app.js</code>를 직접 PM2로 띄우도록 예외 처리</li>
<li>이렇게 하면 <strong>Node.js 앱이 항상 살아 있고, 서비스가 끊기지 않습니다</strong>.</li>
</ul>
<p><strong>5. 헬스체크와 롤백을 넣었어요.</strong>
단순히 재시작만 하면 끝이 아니라, 정말 서버가 잘 떴는지 확인해야 합니다.</p>
<ul>
<li>curl 로 <code>http://127.0.0.1:3000/health</code> 요청</li>
<li>실패하면 → 직전에 백업해둔 정적 자산을 다시 복원</li>
<li>그리고 PM2를 이전 상태로 돌려서, 사용자가 다운타임을 겪지 않게 했습니다.</li>
</ul>
<p><strong>6. 마지막으로 Nginx 설정 리로드까지</strong>
정적 자산을 <code>/var/www/codestargram/repo/web-p3-codestargram2/dist</code> 에 새로 배치했으니, 필요할 때 Nginx도 리로드하게 했습니다.</p>
<ul>
<li><code>nginx -t</code> 로 설정 확인 후 <code>nginx -s reload</code></li>
</ul>
<h3 id="4-서버에-배포-스크립트-설정">4. 서버에 배포 스크립트 설정</h3>
<p><strong>1. 배포 스크립트를 설정을 위한 서버 준비를 1회 진행해줍니다.</strong></p>
<pre><code class="language-bash">ssh kimdayeon@172.30.1.86

# 디렉터리 준비
sudo mkdir -p /var/www/codestargram/{repo,releases,scripts,logs}
sudo chown -R $USER:$USER /var/www/codestargram

# 코드 클론 (GitHub)
cd /var/www/codestargram/repo
git clone https://github.com/&lt;YOU&gt;/web-p3-codestargram2 .   # 또는 SSH URL
git checkout deploy  # 배포 브랜치

# PM2 설정 파일을 repo 루트에 둠 (ecosystem.config.js)
# deploy.sh는 /var/www/codestargram/scripts/deploy.sh 에 둘 것</code></pre>
<p>서버에서 혼란이 없도록, 디렉토리 구조와 경로를 역할에 따라 생성했습니다.</p>
<ul>
<li>repo: 서버에서 git clone한 결과물<ul>
<li><code>/repo/ecosystem.config.cjs</code>으로 pm2 설정</li>
<li><code>/repo/web-p3-codestargram2/dist</code>에서 Nginx로 정적 파일 서빙</li>
</ul>
</li>
<li>releases: 릴리즈 보관용</li>
<li>scripts: 배포 스크립트</li>
<li>logs: 로그 기록 </li>
</ul>
<p>이렇게 정리하면, 파일이 어디에 있는지 바로 파악이 가능합니다.</p>
<p><strong>2. 맥(로컬) 환경에 있는 배포 스크립트를 서버로 업로드해주고, 실행권한 설정을 해줍니다.</strong></p>
<pre><code class="language-bash"># 맥에서
scp /경로/deploy.sh kimdayeon@172.30.1.86:/var/www/codestargram/scripts/deploy.sh
ssh kimdayeon@172.30.1.86 &quot;chmod +x /var/www/codestargram/scripts/deploy.sh \&quot;</code></pre>
<p><strong>3. 서버에 패키지 설치가 안되어있다면 해줍니다.</strong></p>
<pre><code class="language-bash"># 패키지 업데이트
sudo apt update

# curl 설치 (없다면)
sudo apt install -y curl

# Node.js 20.x 설치 (LTS 권장)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# 설치 확인
node -v
npm -v

# pm2 설치 및 확인
sudo npm install -g pm2
pm2 -v</code></pre>
<p><strong>✅ 주요 체크 포인트</strong></p>
<ul>
<li><code>/var/www/codestargram/repo/web-p3-codestargram2</code> 에 실제 레포가 클론되어 있어야 합니다. (.git 존재)</li>
<li><code>ecosystem.config.cjs</code> 는 레포 루트에 위치해야 합니다.(없으면 PM2가 server/app.js로 fallback)</li>
<li>관련 패키지가 서버에 설치되어있어야 합니다.</li>
</ul>
<h3 id="5-배포-스크립트-실행-및-결과">5. 배포 스크립트 실행 및 결과</h3>
<p>체크 포인트를 확인했다면 스크립트를 실행합니다.</p>
<pre><code class="language-bash">/var/www/codestargram/scripts/deploy.sh</code></pre>
<p><strong>변경사항이 있을 시:</strong>
deploy로 PR 생성/머지 후 스크립트 실행 
<img src="https://velog.velcdn.com/images/dyeon-dev/post/8b14fa51-2d4b-4669-b280-d78b9403d4fd/image.png" alt=""></p>
<p><strong>변경사항이 없을 시:</strong>
변경사항 없음 -&gt; 종료
<img src="https://velog.velcdn.com/images/dyeon-dev/post/f9b672ec-c23b-4ed5-81be-679610387a67/image.png" alt=""></p>
<h4 id="배포-결과물">배포 결과물</h4>
<p>서버 주소에서 배포된 코드가 실행된 걸 확인할 수 있었습니다.
<img src="https://velog.velcdn.com/images/dyeon-dev/post/2547ae32-392f-48b6-bf40-f3a7bc5cfaa3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 요청 방식 비교 분석 Form vs Fetch API - 네트워크 계층과 브라우저 처리 관점의 차이]]></title>
            <link>https://velog.io/@dyeon-dev/%EC%9B%B9-%EC%9A%94%EC%B2%AD-%EB%B0%A9%EC%8B%9D-%EB%B9%84%EA%B5%90-%EB%B6%84%EC%84%9D-Form-vs-Fetch-API-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B3%84%EC%B8%B5%EA%B3%BC-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%B2%98%EB%A6%AC-%EA%B4%80%EC%A0%90%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@dyeon-dev/%EC%9B%B9-%EC%9A%94%EC%B2%AD-%EB%B0%A9%EC%8B%9D-%EB%B9%84%EA%B5%90-%EB%B6%84%EC%84%9D-Form-vs-Fetch-API-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B3%84%EC%B8%B5%EA%B3%BC-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%B2%98%EB%A6%AC-%EA%B4%80%EC%A0%90%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Fri, 19 Sep 2025 03:06:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>목차</strong></p>
</blockquote>
<ol>
<li>개요 - 무엇이 다른가?</li>
<li>요청(Request) 생성 방식</li>
<li>응답(Response) 처리 방식</li>
<li>브라우저 동작과 UX</li>
<li>네트워크 계층 관점 요약</li>
<li>결론</li>
</ol>
<h2 id="1-개요---무엇이-다른가">1. 개요 - 무엇이 다른가?</h2>
<p>단순 HTML <code>&lt;form&gt;</code> 전송 방식과 fetch API 전송 방식은 기능적으로는 &quot;서버로 HTTP 요청을 보낸다&quot;는 점에서 같지만, 네트워크 계층 동작 방식과 브라우저 처리 관점에서 중요한 차이가 있다.</p>
<p><strong>핵심 차이점</strong>
**    ◦ 요청 생성 주체: 브라우저 자동 생성 vs 개발자 직접 제어
    ◦ 응답 처리 방식: 브라우저의 페이지 전환 vs 자바스크립트 코드 처리
    ◦ 사용자 경험(UX): 전체 페이지 새로고침 vs 부분적 동적 업데이트**</p>
<p>이 두 방식의 근본적인 차이점을 가지고 자세히 알아보겠다.</p>
<h2 id="2-요청request-생성-방식">2. 요청(Request) 생성 방식</h2>
<p>브라우저와 서버가 통신하기 위해서는 HTTP 스펙에 맞는 요청과 응답을 해야한다.
먼저 브라우저에서는 표준 헤더가 담긴 요청을 서버에게 보낸다. 
이때 요청 방식에 따라 브라우저에서 작동하는 방식과 직접 제어하는 방식으로 나뉜다.</p>
<h3 id="html-form">HTML Form</h3>
<ul>
<li>브라우저가 자동으로 HTTP 요청 메시지를 구성한다.<ul>
<li>method=&quot;GET&quot;이면 Query String </li>
<li>method=&quot;POST&quot;이면 application/x-www-form-urlencoded 기본 인코딩</li>
</ul>
</li>
<li>기본적으로 브라우저가 표준 헤더(Content-Type, Accept) 등을 자동 세팅한다.</li>
<li>개발자가 세밀하게 헤더나 요청 바디를 통제하기 어렵다.</li>
</ul>
<h3 id="fetch-api">fetch API</h3>
<ul>
<li>개발자가 요청을 직접 제어한다.</li>
<li>HTTP 메서드, 헤더, 바디(JSON, FormData, Blob 등)를 자유롭게 지정 가능하다.</li>
<li>쿠키/인증 정보를 포함할지(credentials 옵션) 등 네트워크 계층의 동작을 선택적으로 제어할 수 있다.</li>
</ul>
<h2 id="3-응답response-처리-방식">3. 응답(Response) 처리 방식</h2>
<p>서버는 브라우저에게 받은 요청에 따른 응답을 해준다. 
이때 브라우저의 응답은 서버에서 페이지를 직접 던져주면서 새로 로드되거나, 
비동기 객체를 사용하여 페이지가 새로 로드되지 않고 자바스크립트에서 세부적인 컨트롤이 가능한 방식으로 나뉜다.</p>
<h3 id="html-form-1">HTML Form</h3>
<ul>
<li>서버 응답(HTML, Redirect 등)은 브라우저가 새 페이지로 로드하거나 리다이렉트 처리한다.</li>
<li>즉, 브라우저 렌더링 파이프라인에 직접 연결 → 응답은 자동으로 화면 전환으로 이어진다.</li>
<li>응답을 자바스크립트 코드로 가공할 수는 없다.</li>
</ul>
<h3 id="fetch-api-1">fetch API</h3>
<ul>
<li>응답이 <code>Promise&lt;Response&gt;</code> 객체로 전달된다.</li>
<li>응답 본문을 <code>response.json()</code>, <code>response.text()</code> 등으로 직접 파싱해 사용자가 원하는 로직으로 처리할 수 있다.</li>
<li>화면 전환은 자동으로 일어나지 않으며, SPA 방식에서 필요한 UI만 갱신이 가능하다.</li>
</ul>
<h3 id="html-form-방식이-자바스크립트-개입이-어려운-이유">HTML Form 방식이 자바스크립트 개입이 어려운 이유</h3>
<p><code>&lt;form&gt;</code>의 전송 결과는 브라우저 렌더링 파이프라인으로 바로 들어간다.
(응답 본문 = 새 페이지로 렌더링)
응답 데이터를 JS 코드에서 직접 가공하거나 조건에 따라 분기 처리하는 것은 불가능하다.
따라서 <strong>&quot;조건에 따라 다른 페이지로 보낸다&quot; 같은 로직은 서버에서 결정</strong>해야 한다.</p>
<h4 id="html-form-전송--redirect-처리-흐름">HTML Form 전송 + Redirect 처리 흐름</h4>
<p>Form 전송 후 서버가 3xx 상태 코드(302 Found, 303 See Other 등)를 응답하면,
브라우저는 자동으로 Location 헤더에 지정된 URL로 이동한다.
이 과정에서 개발자가 중간에 자바스크립트로 개입할 수 없다.
즉, 리다이렉트를 서버가 제어해야 한다.</p>
<pre><code>HTTP/1.1 302 Found
Location: /home  → 브라우저는 /home 자동 이동된다!</code></pre><p><img src="https://velog.velcdn.com/images/dyeon-dev/post/b38c4b7c-4df1-4ab9-a18d-2115121f6969/image.png" alt=""></p>
<ul>
<li>메서드 변환: <strong>302/303이면 보통 GET으로 전환</strong></li>
<li>전형적 <strong>PRG(Post/Redirect/Get) 패턴</strong>에 적합</li>
</ul>
<h4 id="fetch-redirect-처리-브라우저-기준">fetch Redirect 처리 (브라우저 기준)</h4>
<p>fetch는 응답이 JS로 전달되고, 서버가 3xx 응답을 보내면 브라우저 fetch가 내부적으로 그 리다이렉트를 따라가서 최종 응답까지 자동으로 받아온다.</p>
<pre><code class="language-js">const res = await fetch(&quot;/login&quot;, { method: &quot;POST&quot; }); // follow(기본)
if (res.redirected) {
  // 필요하면 여기서 직접 네비게이션
  window.location.href = res.url;
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/d9af0c8c-1704-4eb5-a485-4496275d7316/image.png" alt=""></p>
<ul>
<li>기본값 <code>redirect: &quot;follow&quot;</code>으로 자동 추적하고, 중간의 3xx 응답은 JS에 노출되지 않고, Promise는 최종 응답으로 resolve 된다.</li>
<li>브라우저에선 보안 때문에 헤더(Location)를 노출하지 않는다. 바디도 접근 불가하다.</li>
<li>페이지 네비게이션은 일어나지 않는다. 단지 네트워크 레벨에서 추가 요청을 수행해 최종 리소스를 받아올 뿐이고, 화면 전환은 location.href 등으로 직접한다.</li>
</ul>
<h2 id="4-브라우저-동작과-ux">4. 브라우저 동작과 UX</h2>
<p>위의 응답에 따라 브라우저의 동작 과정이 달라지고 사용자 경험까지 달라지게 된다.</p>
<h3 id="html-form-2">HTML Form</h3>
<ul>
<li>기본 동작: 요청을 보내고 응답에 따라 페이지 리로드(Full reload)</li>
<li>사용자는 새로고침(깜빡임) 경험을 겪는다.</li>
<li>History API와 결합하지 않으면 SPA 같은 UX를 만들기 어렵다.</li>
</ul>
<h3 id="fetch-api-2">fetch API</h3>
<ul>
<li>요청-응답이 백그라운드 비동기 처리.</li>
<li>페이지 전환이 일어나지 않음 → 기존 화면 상태 유지 가능.</li>
<li>부분 렌더링이나 동적 업데이트에 유리.</li>
</ul>
<h2 id="5-네트워크-계층-관점-요약">5. 네트워크 계층 관점 요약</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>HTML Form</th>
<th>fetch API</th>
</tr>
</thead>
<tbody><tr>
<td>요청 생성</td>
<td>브라우저가 자동 조립 (제한적 제어)</td>
<td>개발자가 완전 제어 가능</td>
</tr>
<tr>
<td>응답 처리</td>
<td>브라우저가 페이지로 렌더링</td>
<td>JS 코드에서 직접 가공</td>
</tr>
<tr>
<td>네트워크 연결</td>
<td>쿠키/세션 자동 포함</td>
<td><code>credentials</code> 옵션으로 통제</td>
</tr>
<tr>
<td>UX 영향</td>
<td>전체 페이지 리로드</td>
<td>페이지 유지 + 부분 업데이트</td>
</tr>
</tbody></table>
<h2 id="6-결론">6. 결론</h2>
<ul>
<li>HTML Form: 전통적인 MPA (Multi-Page Application) 패턴에 적합하며, 서버가 렌더링을 주도하고 브라우저가 자동으로 처리하는 방식.</li>
<li>Fetch API: 현대적인 SPA (Single-Page Application) 패턴의 핵심으로, 클라이언트(개발자 코드)가 요청과 응답을 세밀하게 제어하는 방식.</li>
</ul>
<p>두 방식은 동일한 HTTP 계층을 사용하지만, <strong>&#39;응답 처리 주체&#39;</strong>와 <strong>&#39;화면 갱신 방식&#39;</strong>에서 본질적인 차이를 보인다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node.js의 net 모듈로 웹 서버 만들기]]></title>
            <link>https://velog.io/@dyeon-dev/Node.js%EC%9D%98-net-%EB%AA%A8%EB%93%88%EB%A1%9C-%EC%9B%B9-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dyeon-dev/Node.js%EC%9D%98-net-%EB%AA%A8%EB%93%88%EB%A1%9C-%EC%9B%B9-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 17 Sep 2025 00:10:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>목차</strong></p>
</blockquote>
<ol>
<li>Node.js는 서버가 아니다. 그럼 어떻게 서버를 만들 수 있는걸까?</li>
<li>net 모듈 - 웹 서버 만드는 도구</li>
<li>서버 객체 생성하기</li>
<li>HTTP 응답 형식 직접 작성해보기</li>
<li>HTTP 요청 구조에서 method, url 추출</li>
<li>결론 및 향후 목표</li>
</ol>
<h3 id="1-nodejs는-서버가-아니다-그럼-어떻게-서버를-만들-수-있는걸까">1. Node.js는 서버가 아니다. 그럼 어떻게 서버를 만들 수 있는걸까?</h3>
<p>Node.js 자체는 웹 서버가 아니다.
Node.js는 구글의 V8 엔진으로 자바스크립트 런타임 환경을 제공하는 프로그램일 뿐이다.
웹 브라우저말고도 자바스크립트를 실행할 수 있도록 해준다.
서버는 특정 포트를 열고 클라이언트의 요청을 기다리는 역할을 한다.
Node.js는 이러한 서버 기능을 구현할 수 있는 API를 제공하기 때문에, 직접 코드를 작성해서 서버를 만들 수 있다.</p>
<p>node.js의 웹 서버 프로그래밍에는 2가지 방식이 있다.</p>
<ul>
<li>node.js에서 기본으로 제공해 주는 모듈을 이용<ul>
<li>net 모듈 사용</li>
<li>http 모듈 사용</li>
</ul>
</li>
<li>Express 등의 웹 프레임워크 사용</li>
</ul>
<p><strong>웹 서버 방식의 이해도를 높이기 위해 net 모듈을 사용해서 웹 서버를 만들어보자.</strong></p>
<h3 id="2-net-모듈---웹-서버-만드는-도구">2. net 모듈 - 웹 서버 만드는 도구</h3>
<p>Node.js의 net 모듈은 <strong>TCP 통신</strong>을 위한 기본적인 도구이다.
이 모듈을 사용하여 소켓 통신을 구현하고, 서버와 클라이언트 간의 <strong>데이터 스트림</strong>을 다룰 수 있다. 
net 모듈로 특정 포트에서 연결을 기다리고, 연결이 들어오면 데이터를 주고받는 Socket 객체를 생성한다.
이 Socket 객체를 통해 들어오는 요청을 처리한다.</p>
<h3 id="3-서버-객체-생성하기">3. 서버 객체 생성하기</h3>
<p>net 모듈을 불러오고 <strong>createServer()</strong> 메서드를 통해 <strong>TCP 서버 객체를 생성</strong>한다.
서버 객체를 생성한 후 특정 포트번호로 <strong>listen()</strong> 메서드를 호출하면 해당 <strong>포트로 웹 서버 통신이 시작</strong>된다.</p>
<pre><code class="language-js">import * as net from &quot;node:net&quot;;
const PORT = process.env.PORT || 3000;

const server = net.createServer((socket)=&gt; {
    // 클라이언트로부터 데이터를 수신할 때 
    socket.on(&quot;data&quot;, (data) =&gt; {
        console.log(data.toString());
        // 클라이언트에게 데이터 반환
        socket.write(`서버가 보낸 응답: ${data}`); 
    }
})

server.listen(PORT, () =&gt; {
    console.log(`TCP server (net) listening at http://localhost:${PORT}/index.html`);
});</code></pre>
<ul>
<li>브라우저에 <a href="http://localhost:3000/">http://localhost:3000/</a> 을 입력했을 때 <strong>socket.on()</strong> 메서드를 통해 <strong>클라이언트로부터 온 데이터를 수신</strong>한다.</li>
<li>data 정보는 <strong>Buffer 형식</strong>으로 들어오고, 이를 문자열로 바꿔서 확인해보면 다음과 같은 정보가 담겨있다.<pre><code>GET / HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: &quot;Not)A;Brand&quot;;v=&quot;8&quot;, &quot;Chromium&quot;;v=&quot;138&quot;, &quot;Google Chrome&quot;;v=&quot;138&quot;
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: &quot;macOS&quot;
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: x_auth=eyJhbGciOiJIUzI1NiJ9.NjZjOTZkOTQ3NDllOGQ4ZTUyZWM3ZDQ3.PCwoG67FlL4g66sRfkvwJVgjS-NLIY9q-yzb9qq5tgY</code></pre></li>
</ul>
<blockquote>
<p>Buffer란? </p>
</blockquote>
<ul>
<li>바이너리 데이터를 다루는 객체이다. </li>
<li>Node.js는 데이터를 주고받을 때 Buffer 객체 형태로 처리한다. </li>
<li>Buffer는 메모리에 직접 할당된 고정 크기 버퍼로, I/O 작업(파일 읽기, 네트워크 통신 등)의 효율을 높여준다. </li>
<li>toString() 메서드를 사용하여 Buffer의 내용을 문자열로 변환할 수 있다.</li>
</ul>
<h3 id="4-http-응답-형식-직접-작성해보기">4. HTTP 응답 형식 직접 작성해보기</h3>
<p>브라우저에 <a href="http://localhost:3000/">http://localhost:3000/</a> 을 입력했을 때, 위의 코드에서 <strong>socket.write()가 작동하지 않고, 값이 없거나 오류가 출력</strong>된다.</p>
<p>그 이유는 다음과 같다.
net 서버는 단순히 <strong>TCP Socket을 통한 원시 데이터(Raw Data)</strong>만 주고받을 수 있다.
<strong>브라우저는 HTTP 프로토콜만 사용</strong>하기 때문에 net 서버가 브라우저의 HTTP 요청을 처리할 수 없는 것이다. 
이를 해결하기 위해 <strong>브라우저 규약에 맞게 HTTP 응답 형식</strong>을 직접 작성해서 클라이언트가 정상 파싱하도록 해야 한다.</p>
<ul>
<li>공식문서: <a href="https://nodejs.org/api/http.html#responsewritechunk-encoding-callback%EC%9D%98">Node.js HTTP</a> response.writeHead(), response.write() 섹션 참고.</li>
</ul>
<p>그럼 <strong>HTTP 응답 형식</strong>부터 자세히 알아보자! (이게 핵심)
<strong>HTTP(Hypertext Transfer Protocol)</strong>는 텍스트 기반 프로토콜로, 규칙에 맞춰 개발해서 서로 정보를 교환할 수 있도록 한다. 
클라이언트와 서버는 <strong>상태 줄, header, 빈 줄(CRLFCRLF), body</strong> 구조 순서로 통신한다.</p>
<h4 id="header의-역할메타데이터">header의 역할(메타데이터)</h4>
<ul>
<li><strong>Content-Type</strong>: body를 어떤 형식으로 해석할지 지정</li>
<li><strong>Content-Length</strong> / Transfer-Encoding: 응답 body 길이나 전송 방식(예: chunked)을 알려, 클라이언트가 어디까지를 body로 읽을지 판단하게 함. 둘 중 하나는 필수. 없으면 클라이언트는 응답 경계를 알 수 없음.</li>
<li><strong>Connection</strong>: keep-alive/close 여부를 지정</li>
<li>그 외 캐시, 쿠키, CORS 등 클라이언트 동작을 결정하는 다양한 정책 전달.<h4 id="body의-역할실제-데이터">body의 역할(실제 데이터)</h4>
</li>
<li>HTML, JSON, 이미지 등 클라이언트가 최종적으로 사용하는 페이로드</li>
<li>일부 상태코드(예: 204, 304)나 요청 메서드(HEAD)에서는 응답에 body가 포함되지 않는다.</li>
</ul>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>204 No Content</strong></th>
<th><strong>304 Not Modified</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>목적</strong></td>
<td><strong>요청에 대한 응답으로 보낼 새로운 콘텐츠가 없을 때 사용한다.</strong></td>
<td><strong>클라이언트의 캐시된 리소스가 최신 상태임을 알린다.</strong></td>
</tr>
<tr>
<td><strong>사용 시점</strong></td>
<td><strong><code>PUT</code>, <code>DELETE</code>, <code>POST</code> 등 서버의 리소스를 변경하는 요청이 성공했을 때 주로 사용한다.</strong></td>
<td><strong>캐시된 리소스를 확인하기 위한 조건부 <code>GET</code>, <code>HEAD</code> 요청에 대한 응답으로 사용한다.</strong></td>
</tr>
<tr>
<td><strong>캐시</strong></td>
<td><strong>기본적으로 캐시될 수 있다. <code>ETag</code> 헤더가 포함될 수 있다.</strong></td>
<td><strong>캐시를 활용하기 위한 상태 코드다.</strong></td>
</tr>
<tr>
<td><strong>클라이언트 동작</strong></td>
<td><strong>클라이언트는 현재 페이지를 유지하거나, 서버의 성공 응답을 바탕으로 다음 작업을 진행한다.</strong></td>
<td><strong>클라이언트는 가지고 있는 캐시된 복사본을 그대로 사용한다.</strong></td>
</tr>
</tbody></table>
<h4 id="빈-줄crlfcrlf">빈 줄(CRLFCRLF)</h4>
<ul>
<li>헤더들은 <code>\r\n</code>으로 줄바꿈하며, 헤더 종료 후 반드시 빈 줄 <code>\r\n</code>을 넣는다.</li>
<li>그 다음이 body. 이 경계가 없으면 클라이언트는 파싱할 수 없다.</li>
</ul>
<p>그럼 HTTP 응답 형식을 직접 작성해보자.</p>
<pre><code class="language-js">const server = net.createServer((socket)=&gt; {
  socket.on(&quot;data&quot;, (data) =&gt; {
    try {
      const body = `&lt;h1&gt;HTTP 응답 바디&lt;/h1&gt;`;
      const header = 
      `HTTP/1.1 200 OK\r\n` +
      `Content-Type: text/html; charset=UTF-8\r\n` +
      `Content-Length: ${Buffer.byteLength(body)}\r\n` + 
      `\r\n`;
      const response = header + body;
      socket.write(response);
      socket.end();
    } catch (error) {
        console.error();
    } 
  })

  socket.on(&quot;error&quot;, (err) =&gt; {
    console.error(&quot;Socket error:&quot;, err.message);
    try { socket.destroy(); } catch {}
  });
})
</code></pre>
<p>브라우저에 접속하면 <strong>body로 선언한 부분이 표시</strong>된다.
<strong>Response Header</strong>에도 적용한 부분이 적용되어 있다.
<img src="https://velog.velcdn.com/images/dyeon-dev/post/af087107-9924-4701-97b4-cba0d81de35b/image.png" alt=""></p>
<h3 id="5-http-요청-구조에서-method-url-추출">5. HTTP 요청 구조에서 method, url 추출</h3>
<p>HTTP 요청은 GET / HTTP/1.1과 같이 시작하는 <strong>Request Line</strong>과 헤더로 구성된다. 
이 Request Line에서 URI를 추출하고 <strong>각 URI에 맞는 HTML 페이지를 응답하도록 분기처리</strong>를 해준다.</p>
<pre><code class="language-js">// [&quot;GET&quot;, &quot;/&quot;, &quot;HTTP/1.1&quot;]
    const [method, url] = data.toString().split(&quot; &quot;);

    const serverSideRendering = (url) =&gt; {
      switch (url) {
        case &quot;/&quot;:
          return fs.readFileSync(&quot;./views/index.html&quot;);
        case &quot;/login&quot;:
          return fs.readFileSync(&quot;./views/login.html&quot;);
        case &quot;/register&quot;:
          return fs.readFileSync(&quot;./views/register.html&quot;);
        default:
          return `&lt;h1&gt;not found page&lt;/h1&gt;`;
      }
    }</code></pre>
<p>위 부분을 <strong>body</strong> 부분에 넣어주면 서버사이드렌더링이 적용된다!</p>
<pre><code class="language-js">const body = serverSideRendering(url);</code></pre>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/9d4fb236-3539-4455-88b6-5c7e132304c7/image.png" alt="">
<img src="https://velog.velcdn.com/images/dyeon-dev/post/e969cda5-7fbc-4295-8e59-ead0e07ddbcc/image.png" alt=""></p>
<h3 id="6-결론-및-향후-목표">6. 결론 및 향후 목표</h3>
<p>이렇게 <strong>net 모듈</strong>을 사용해서 <strong>Node.js 서버의 기초</strong>를 직접 만들어보았다.
HTTP 요청과 응답 형식을 알아보고 <strong>HTTP/1.1 헤더</strong>를 수동으로 작성해보았다.
Node.js 서버의 기초이기 때문에 Express를 활용하기 위한 기본기를 다지는 경험이었다. 
또한, 서버가 정적 HTML로 응답해주는 방식으로 <strong>MPA 구조와 SSR 방식</strong>의 기초를 다졌다.</p>
<p>하지만 지금까지 구현한 소스 코드는 stylesheet 와 파비콘 등을 지원하지 못하고 있다. 
이후 더 <strong>다양한 컨텐츠 타입을 지원하도록 개선</strong>해 볼 것이다. MIME 타입을 적용하고 서버 코드를 확장하는 방식으로 진행할 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ESLint, Preitter 제대로 알고 사용하자]]></title>
            <link>https://velog.io/@dyeon-dev/ESLint-Preitter-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90</link>
            <guid>https://velog.io/@dyeon-dev/ESLint-Preitter-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90</guid>
            <pubDate>Wed, 03 Sep 2025 18:56:37 GMT</pubDate>
            <description><![CDATA[<p>코드의 품질을 위해 ESLint와 Preitter를 보통 혼합해서 쓴다.
왜 그럴까? 하는 역할은 비슷해보이는데 어떤 게 달라서 둘 다 같이 써야하는지 알아보고 각각 어떤 설정이 어떤 역할을 하는건지 파악해보자.</p>
<p>VSCode가 코드의 Lint Error를 잡는 것부터, 저장 시 자동 수정하는 설정까지 복잡한 조건 설정 과정을 적용해보자!</p>
<h1 id="eslint">ESLint</h1>
<p>ESLint는 코드 품질을 개선하고 런타임에 버그가 발생하지 않도록 사전에 버그를 수정하는 데 도움이 되는 linter이다.</p>
<p>&#39;Lint&#39;는 실제로 보풀이라는 뜻이다. ESLint는 거슬리는 보풀 같은 코드를 깔끔하게 만들어준다는 뜻이라고 보면 된다.ㅎ</p>
<h3 id="eslint를-사용하는-이유">ESLint를 사용하는 이유</h3>
<ul>
<li>코드 일관성 유지: 코드 스타일 가이드를 준수하여 일관성 있는 코드 작성 가능</li>
<li>버그 및 오류 방지: 잠재적인 버그나 오류를 사전에 찾아내고 수정할 수 있다.</li>
<li>팀 협업 강화: 팀원 간에 코드 품질을 통일시켜 협열 효율을 높일 수 있다.</li>
</ul>
<h3 id="v9x-버전-마이그레이션-사항">v9.x 버전 마이그레이션 사항</h3>
<ul>
<li>eslintrc는 더 이상 사용되지 않는다. eslintrc 구성 형식을 사용해야 하는 경우 <code>ESLINT_USE_FLAT_CONFIG</code> 환경 변수를 <code>false</code>으로 설정해야 한다.</li>
</ul>
<p>이 글에서는 v9.x 버전으로 다룬다.</p>
<h3 id="플러그인-제공">플러그인 제공</h3>
<p>ESLint 에서 많이 사용하는 plugin 으로는 기본인 <code>plugin:@typescript-eslint/recommended</code> 가 있다.<br>그리고 Airbnb, Google 등에서 본인들의 ESLint style 을 작성하여 npm 모듈로 제공하고 있다.</p>
<ul>
<li>ESLint Rules Reference : <a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbFp5ZnI5RnhGQ0ptQkluUVRHdDVVbmp2cndpd3xBQ3Jtc0ttX3lOc1BnMVUwRkQtY2tnTVhmTF8yQXVLMk1RNGRJV01zdmtPUTJNYVRmUlY1TXBnOFZlYzI1b0otVG85T3h2bFBqMEZCeWFYaFFSay1nU1RBemhtS0xmbXZiLXdzZF9waW9FcWdqemh5OC1kVzd6QQ&amp;q=https%3A%2F%2Feslint.org%2Fdocs%2Flatest%2Frules&amp;v=M6NVLZx2m5Y">https://eslint.org/docs/latest/rules</a> </li>
<li>Airbnb React/JSX Style Guide : <a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqa3kxRlRJLVZOaHN2SUtmUFNLYTNEdy1vQUVNUXxBQ3Jtc0tuY0RkbkxiOWxKRDFZVV9nS1duckdHdFhuTjZUNTRWdUtTbTI2VDNuU1hXNk5CYU8zTGg0cDVKS05KNGdSVzllSFlxNnR0bXpqSnFsUVJFNFhCYkQyU0MwS3UzVGN6X05jb3Y5UXAxMFJ1azFOamJxZw&amp;q=https%3A%2F%2Fgithub.com%2Fapple77y%2Fjavascript%2Ftree%2Fmaster%2Freact&amp;v=M6NVLZx2m5Y">https://github.com/apple77y/javascrip...</a> </li>
<li>eslint-config-standard-with-typescript : <a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbi15SnhiLUdiQ0N5dXJfLTRlQXlzcG1rZm5Md3xBQ3Jtc0ttZGRYTVJHOFJqQ1B0U2M5QThjYVZSLWJaYk95SGw4SkM4Vjdpb1lURWdnQW1GblJLc1VsQmxvbnhITjBZM25icUtyMUtoTlY3TE1ja2ZmeHNKVksxNU8tVDhYR0g4aF9iX0E5R1llc0luZ1hsNUpUdw&amp;q=https%3A%2F%2Fgithub.com%2Fstandard%2Feslint-config-standard-with-typescript&amp;v=M6NVLZx2m5Y">https://github.com/standard/eslint-co...</a> </li>
<li>Next.js ESLint : <a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbHNkaVkxajNPUS1GdVlUdk1ua0VWTXYySENud3xBQ3Jtc0ttblN5WE9YQ3NQSmpzTmtjMVdsMDY1QWV2bzdCV29fMnVfZGNlVm9QbUM4QUtiOF85RVpEZVEzSXdZRGVqMXhrcGdNSENJVTJ0TGJIWDI3Q0pvRnl1QzVvUEF5dEtHVFkzT2otR2V4blVlNjZLUnZkOA&amp;q=https%3A%2F%2Fnextjs.org%2Fdocs%2Fapp%2Fbuilding-your-application%2Fconfiguring%2Feslint%23eslint-config&amp;v=M6NVLZx2m5Y">https://nextjs.org/docs/app/building-...</a></li>
</ul>
<h3 id="규칙">규칙</h3>
<p>공식 문서의 <a href="https://eslint.org/docs/latest/rules/">Rules Reference</a>를 확인해보면, 각 규칙에는 있는 이모지 표시로 <strong>규칙 활성화</strong>와 <strong>자동 수정 여부</strong>를 확인할 수가 있다.</p>
<ul>
<li>✅ <code>recommended</code>config 설정 시 활성화 되는 규칙</li>
<li>🔧  --fix 명령어로 자동으로 수정할 수 있는 규칙</li>
</ul>
<p>세 가지 오류 수준을 통해 ESLint가 규칙을 적용하는 방식을 세부적으로 제어할 수 있다.</p>
<ol>
<li><code>&quot;off&quot; or &quot;0&quot;</code></li>
</ol>
<ul>
<li>규칙을 해제, 해당 규칙을 사용하지 않음</li>
</ul>
<ol start="2">
<li><code>&quot;warn&quot; or &quot;1&quot;</code></li>
</ol>
<ul>
<li>규칙을 경고로 설정, 규칙을 강제하지 않고 경고만 해줌</li>
</ul>
<ol start="3">
<li><code>&quot;error&quot; or &quot;2&quot;</code></li>
</ol>
<ul>
<li>규칙을 에러로 설정, 통합 테스트, build, PR등의 경우에 에러 발생</li>
</ul>
<h3 id="설치">설치</h3>
<pre><code>npm init @eslint/config@latest</code></pre><blockquote>
<ol>
<li>How would you like to use ESLint? // eslint를 어떤 방식으로 쓸거에요?</li>
</ol>
</blockquote>
<ul>
<li>To check syntax only  // 문법적 오류만 잡을거에요</li>
<li>To check syntax and find problems  // 문법이랑 에러도 잡을거에요</li>
</ul>
<blockquote>
<ol start="2">
<li>What type of modules does your project use? // 프로젝트에선 어떤 모듈화를 사용하나요?</li>
</ol>
</blockquote>
<ul>
<li>JavaScript modules (import/export)</li>
<li>CommonJS (require/exports)</li>
<li>None of these</li>
</ul>
<blockquote>
<ol start="3">
<li>Which framework does your project use?  // 어떤 프레임워크를 사용하나요?</li>
</ol>
</blockquote>
<ul>
<li>React</li>
<li>Vue.js</li>
<li>None of these // 안써요</li>
</ul>
<blockquote>
<ol start="4">
<li>Does your project use TypeScript?  // 프로젝트는 typescript를 사용하나요?  </li>
</ol>
</blockquote>
<ul>
<li>y/s로 대답</li>
</ul>
<blockquote>
<ol start="5">
<li>Where does your code run?  // 당신의 코드는 어디서 돌리나요?</li>
</ol>
</blockquote>
<ul>
<li>browser</li>
<li>Node</li>
</ul>
<blockquote>
<ol start="6">
<li>Which package manager do you want to use?  // 패키지 매니저 선택</li>
</ol>
</blockquote>
<ul>
<li>npm</li>
<li>yarn</li>
<li>pnpm</li>
</ul>
<p>설치를 완료하면 <strong>eslint.config.mjs</strong> 파일이 생성된다. 생성된 파일에 설정들을 추가해보자.</p>
<pre><code class="language-js">export default defineConfig([

    { files: [&quot;app.js&quot;, &quot;server/**/*.js&quot;], languageOptions: { globals: globals.node } },
    { files: [&quot;features/**/*.js&quot;], languageOptions: { globals: globals.browser }},
    js.configs.recommended,
    {
        rules: {
            &quot;no-unused-vars&quot;: &quot;warn&quot;,
            // &quot;no-undef&quot;: &quot;warn&quot;,
        },
    },

]);</code></pre>
<ul>
<li><code>&quot;eslint:recommended&quot;</code>: <strong>ESLint가 권장하는 규칙</strong>. 위에서 본 ✅ 표시 적용</li>
<li><code>rules</code>: <strong>ESLint는 규칙이 있어야 그걸 보고 코드를 검사</strong>한다. 새 객체를 정의하여 규칙을 개별적으로 구성할 수 있다.<ul>
<li><code>no-undef</code>: 코드에서 선언되지 않은 식별자를 사용하면 오류를 표시하는 규칙이다. 하지만 globals 설정을 해주면 이런 오류를 잡아주기 때문에 따로 처리하지 않았다. 또한, TypeScript 계획이 있다면 no-undef는 TS가 잡아주므로 끄는 편(off)이 일반적이다.</li>
</ul>
</li>
<li><strong><code>browser</code> 코드와 <code>node</code> 코드를 files 패턴으로 분리해 각각 <code>globals</code>를 적용해야 한다. <code>globals.browser/globals.node</code>를 <code>languageOptions.globals</code>에 넣어야 <code>no-undef</code> 오탐이 생기지 않는다.</strong><blockquote>
<p><strong>왜 globals 설정이 no-undef 오탐을 줄이나요?</strong></p>
<pre><code> - 문제: 브라우저/노드 환경에는 미리 제공되는 전역 식별자들이 있는데, ESLint가 이를 모르면 no-undef로 오탐한다.
 - 브라우저 전역 예시: `window`, `document`, `navigator`, `location` 등
 - 노드 전역 예시: `process`, `dirname`, `Buffer`, `global` 등
 - 해결: `globals` 패키지의 `globals.browser`, `globals.node`를 ESLint 설정의 `languageOptions.globals`에 주입하면, 해당 식별자들을 “이미 전역에 존재하는 읽기전용 변수”로 인식해서 no-undef 오탐이 사라진다.</code></pre><p><img src="https://velog.velcdn.com/images/dyeon-dev/post/f3a3ad0f-cc1c-4ac7-8214-c58b1d7c6e83/image.png" alt="">
<img src="https://velog.velcdn.com/images/dyeon-dev/post/de190cf9-9a0f-471f-a39f-81d15e839985/image.png" alt=""></p>
</blockquote>
</li>
</ul>
<h1 id="preitter">Preitter</h1>
<p>Preitter는 <strong>포맷팅</strong> 역할을 해서 <strong>코드를 일관성</strong> 있게 만들어준다.
ESLint와 달리 <strong>규칙이 미리 세팅</strong>되어 있기 때문에 설정 없이도 <code>npx prettier . --write</code> 명령어로 바로 사용해볼 수 있는 간편함이 있다.</p>
<h3 id="preitter-설치">Preitter 설치</h3>
<pre><code>npm install --save-dev --save-exact prettier</code></pre><h3 id="prettierrc-파일-추가">.prettierrc 파일 추가</h3>
<p>.prettierrc 파일을 추가해서 직접 <strong>커스텀</strong>을 해도 된다.</p>
<pre><code>{
  ------------------------
  기본 포맷팅 설정
  ------------------------
  // 한 줄의 최대 길이를 설정 (기본값: 80)
  // 이 길이를 초과하면 자동으로 줄바꿈이 발생
  &quot;printWidth&quot;: 80,

  // 들여쓰기 시 사용할 공백 문자 수 (기본값: 2)
  // useTabs가 false일 때만 적용됨
  &quot;tabWidth&quot;: 2,

  // 들여쓰기에 탭 문자 사용 여부 (기본값: false)
  // true: 탭 문자 사용, false: 공백 문자 사용
  &quot;useTabs&quot;: false,

  // 문장 끝 세미콜론 사용 여부 (기본값: true)
  // true: 모든 문장 끝에 세미콜론 추가
  // false: 필요한 경우에만 세미콜론 추가
  &quot;semi&quot;: true,

  ------------------------
  따옴표 관련 설정
  ------------------------
  // 문자열에 작은따옴표 사용 여부 (기본값: false)
  // true: &#39;string&#39;, false: &quot;string&quot;
  &quot;singleQuote&quot;: true,

  // 객체 속성에 따옴표 추가 방식 (기본값: &quot;as-needed&quot;)
  &quot;quoteProps&quot;: &quot;as-needed&quot;,
  // - &quot;as-needed&quot;: 필요한 경우에만 따옴표 추가
  // - &quot;consistent&quot;: 하나라도 따옴표가 필요하면 모든 속성에 따옴표 추가
  // - &quot;preserve&quot;: 입력된 따옴표 스타일 유지

  // JSX에서 작은따옴표 사용 여부 (기본값: false)
  // singleQuote 설정과 독립적으로 동작
  &quot;jsxSingleQuote&quot;: true,

  ------------------------
  쉼표 및 괄호 설정
  ------------------------
  // 객체, 배열 등의 후행 쉼표 설정 (기본값: &quot;es5&quot;)
  &quot;trailingComma&quot;: &quot;es5&quot;,
  // - &quot;all&quot;: 모든 구문에서 후행 쉼표 사용 (함수 인자 포함)
  // - &quot;es5&quot;: ES5에서 유효한 위치에만 후행 쉼표 추가
  // - &quot;none&quot;: 후행 쉼표 사용 안 함

  // 객체 리터럴의 중괄호 주위 공백 추가 (기본값: true)
  // true: { foo: bar }, false: {foo: bar}
  &quot;bracketSpacing&quot;: true,

  // JSX 요소의 &gt; 위치 설정 (기본값: false)
  // true: 마지막 줄에 &gt; 위치, false: 다음 줄에 &gt; 위치
  &quot;bracketSameLine&quot;: false,

  // 화살표 함수 매개변수 괄호 사용 방식 (기본값: &quot;always&quot;)
  &quot;arrowParens&quot;: &quot;always&quot;,
  // - &quot;always&quot;: (x) =&gt; x
  // - &quot;avoid&quot;: x =&gt; x (매개변수가 하나일 때)

  ------------------------
  특수 포맷팅 설정
  ------------------------
  // 줄 끝 문자 설정 (기본값: &quot;lf&quot;)
  &quot;endOfLine&quot;: &quot;lf&quot;,
  // - &quot;lf&quot;: \n (Unix)
  // - &quot;crlf&quot;: \r\n (Windows)
  // - &quot;cr&quot;: \r (Mac OS)
  // - &quot;auto&quot;: 첫 줄 끝 문자 유지

  // 마크다운 텍스트의 줄바꿈 방식 (기본값: &quot;preserve&quot;)
  &quot;proseWrap&quot;: &quot;always&quot;,
  // - &quot;always&quot;: 항상 printWidth에 따라 줄바꿈
  // - &quot;never&quot;: 줄바꿈 하지 않음
  // - &quot;preserve&quot;: 원본 텍스트 줄바꿈 유지

  // HTML 공백 처리 방식 (기본값: &quot;css&quot;)
  &quot;htmlWhitespaceSensitivity&quot;: &quot;strict&quot;,
  // - &quot;css&quot;: CSS display 속성 기준으로 처리
  // - &quot;strict&quot;: 모든 공백을 유지
  // - &quot;ignore&quot;: 모든 공백을 무시
}</code></pre><p>하지만 <strong>Preitter는 단순히 코드를 예쁘게 만들어주는 역할</strong>만 하고, <strong>코드 품질과 관련된 검사는 ESLint의 몫</strong>이다.
그래서 <strong>Preitter는 ESLint와 통합하는 방법을 제공</strong>한다.
<code>eslint-plugin-prettier</code>를 설치하면 된다.
<a href="https://prettier.io/docs/comparison">공식문서</a>에 설명된 것처럼 코드 포맷팅 관련 문제에는 Prettier를, 코드 품질 관련 문제에는 ESLint를 사용해보자. </p>
<pre><code class="language-js">&quot;devDependencies&quot;: {
    &quot;@eslint/js&quot;: &quot;^9.34.0&quot;,
    &quot;eslint&quot;: &quot;^9.34.0&quot;,
    &quot;eslint-config-prettier&quot;: &quot;^10.1.8&quot;,
    &quot;globals&quot;: &quot;^16.3.0&quot;,
    &quot;prettier&quot;: &quot;3.6.2&quot;
}</code></pre>
<p>지금까지 설치한 각 패키지들의 핵심 기능을 설명하면 다음과 같다.</p>
<p><strong>핵심 린팅 도구</strong><br>• <code>eslint</code>: JavaScript/TypeScript 코드의 문제점을 검사하는 린터<br>• <code>@eslint/js</code>: ESLint의 JavaScript 관련 기본 설정</p>
<p><strong>코드 스타일링</strong><br>• <code>prettier</code>: 코드 포맷터
• <code>eslint-config-prettier</code>: 서로 충돌하는 옵션이 있으면 Preitter 규칙을 사용하여 충돌 방지 </p>
<h3 id="eslint-config에서-preitter-설정">ESLint config에서 Preitter 설정</h3>
<p>ESLint와 Preitter의 충돌을 설정해주기 위해 <code>eslint.config.mjs</code>에서 설정을 해줘야 한다.</p>
<pre><code class="language-js">import prettierConfig from &quot;eslint-config-prettier&quot;;

export default defineConfig([
    {
        files: [&quot;app.js&quot;, &quot;server/**/*.js&quot;],
        languageOptions: { globals: globals.node },
    },
    {
        files: [&quot;features/**/*.js&quot;],
        languageOptions: { globals: globals.browser }.
    },
    js.configs.recommended,
    prettierConfig, // Prettier와 충돌하는 규칙 비활성화
]);</code></pre>
<p><strong>🐛<code>eslint-plugin-prettier</code> 설정은 뺐어요.</strong>
이렇게 통합하는 과정에서 여러 블로그 글을 참고하다보면 <code>eslint-config-prettier</code>뿐만 아니라 <strong><code>eslint-plugin-prettier</code> 플러그인을 추가로 사용하는 경우</strong>도 있다. 
이건 Prettier가 ESLint 규칙으로 통합돼서 실행할 수 있도록 해주는 기능이다.</p>
<p>그런데,, 나도 처음에 <code>eslint-plugin-prettier</code> 플러그인으로 다음과 같이 통합해주는 과정을 거쳤는데 그렇게 하니까 몇가지 부분에서 통합이 제대로 이뤄지지 않는 문제상황을 겪었다.</p>
<pre><code class="language-js">import prettier from &#39;eslint-plugin-prettier&#39;;

...

{
        plugins: { prettier },
        rules: {
            &quot;prettier/prettier&quot;: &quot;error&quot;, // Prettier 규칙 위반 시 오류 표시
        },
    },</code></pre>
<p>.prettierrc에서 분명 싱글 따옴표(&#39;) 규칙으로 설정을 했는데 ESLint로 통합해서 적용할 때 더블 따옴표(&quot;) 규칙으로 적용되면서 Prettier 규칙을 위반했다고 빨간줄이 생기는 오류가 발생했다.</p>
<p>이 부분에 대해서 왜 그런지 찾아보다가.. 처음에는 뭔말인지 잘 모르고 넘어갔던 <a href="https://prettier.io/docs/integrating-with-linters">공식문서</a>에서 언급한 부분을 다시 한 번 보게 되었다. 
이 플러그인은 <strong>특정 상황에서는 유용할 수 있지만 일반적으로 권장되지는 않는 방식</strong>이라고 하는데, 이렇게 통합이 잘 되지 않는 문제가 발생하기도 해서 권장되지 않는다고 설명한게 아닐까 생각이 든다..!</p>
<h2 id="자동화-설정">자동화 설정</h2>
<p>매번 <code>npm run lint</code>를 돌리거나 빨간줄을 직접 수정하기에는 상당히 귀찮다. 이걸 해결해주기 위해 코드를 저장할 때 자동으로 코드포맷팅을 해주는 자동화 설정을 해주자.</p>
<p>폴더 상위 디렉토리에 <code>.vscode/settings.json</code>를 추가한다.</p>
<pre><code class="language-json">{
    // ESLint 확장 프로그램 활성화
    &quot;eslint.enable&quot;: true,


    // 기본 코드 포맷터를 Prettier로 설정
    &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;,


    // 파일 저장 시 자동으로 코드를 포맷팅
    &quot;editor.formatOnSave&quot;: true,
    &quot;editor.codeActionsOnSave&quot;: {
        &quot;source.fixAll.eslint&quot;: &quot;explicit&quot;, // ESLint 오류 자동 수정
        &quot;source.organizeImports&quot;: &quot;explicit&quot; // import 문 자동 정리
    },

    // ESLint가 검사할 파일 타입들을 지정
    &quot;eslint.validate&quot;: [
        &quot;javascript&quot;,
        &quot;javascriptreact&quot;,
        &quot;typescript&quot;,
        &quot;typescriptreact&quot;
    ],

    &quot;prettier.requireConfig&quot;: true, // .prettierrc 파일이 있을 때만 Prettier를 사용 (프로젝트별 설정 강제)

    // .mjs 파일을 JavaScript로 인식하도록 설정
    &quot;files.associations&quot;: {
        &quot;*.js&quot;: &quot;javascript&quot;,
        &quot;*.mjs&quot;: &quot;javascript&quot;
    },


    // 탭 크기를 2칸으로 설정하고, 탭 대신 스페이스 사용
    &quot;editor.tabSize&quot;: 2,
    &quot;editor.insertSpaces&quot;: true,


    // 파일 저장 시 빈 줄 제거
    &quot;files.trimTrailingWhitespace&quot;: true,
    &quot;files.insertFinalNewline&quot;: true
}</code></pre>
<h4 id="참고자료">참고자료</h4>
<ul>
<li><a href="https://eslint.org/docs/latest/rules/">ESLint Rules</a></li>
<li><a href="https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options">Migration Guide: Configuring Language Options</a></li>
<li><a href="https://prettier.io/docs/install">Preitter install</a></li>
<li><a href="https://helloinyong.tistory.com/325">ESLint, Prettier Setting, 헤매지 말고 정확히 알고 설정하자</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ER 다이어그램을 단계별로 설계하자]]></title>
            <link>https://velog.io/@dyeon-dev/ER-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8%EC%9D%84-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%84%A4%EA%B3%84%ED%95%98%EC%9E%90</link>
            <guid>https://velog.io/@dyeon-dev/ER-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8%EC%9D%84-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%84%A4%EA%B3%84%ED%95%98%EC%9E%90</guid>
            <pubDate>Tue, 02 Sep 2025 14:19:35 GMT</pubDate>
            <description><![CDATA[<h1 id="erdentity-relationship-diagram란">ERD(Entity Relationship Diagram)란?</h1>
<p>ER 다이어그램은 실세계에 존재하는 물건을 여러 가지 속성을 가진 엔티티로 파악하고 <strong>엔티티(entity) 사이의 상호 관계를 관계(relationship)로 표현</strong>한 그림이다. 
ER 다이어그램의 속성은 데이터이므로 ER 다이어그램을 통해 시스템에서 사용하는 각 데이터의 상호 관계를 읽을 수가 있다.</p>
<p>ER 다이어그램은 <strong>엔티티, 속성, 관계</strong> 3가지로 구성된다.</p>
<h3 id="엔티티">엔티티</h3>
<p>실세계에 존재하는 물건에 해당</p>
<ul>
<li>독립 엔티티<ul>
<li>다른 엔티티에 의존하지 않고 존재하는 것</li>
</ul>
</li>
<li>종속 엔티티<ul>
<li>다른 엔티티에 의존하여 존재하는 것</li>
</ul>
</li>
</ul>
<h3 id="속성">속성</h3>
<p>엔티티에 포함된 데이터가 속성이다.</p>
<h3 id="관계">관계</h3>
<p>엔티티 간의 연관을 나타내는 것으로 의존 정도에 따라 <strong>점선</strong> 또는 <strong>직선</strong>으로 표현한다.</p>
<ul>
<li><strong>실선 관계</strong><ul>
<li>자식 엔티티의 기본키(PK) 가 부모 엔티티의 기본키를 포함할 때</li>
<li>즉, 부모가 없으면 자식이 존재할 수 없음</li>
</ul>
</li>
<li><strong>점선 관계</strong><ul>
<li>자식 엔티티가 부모 엔티티의 기본키를 포함하지 않고, 대신 부모의 키를 외래키(FK) 로만 가질 때</li>
<li>자식이 부모의 존재에 “의존”은 하지만, PK 차원에서는 독립적이기 때문에 점선 관계가 됨</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/e1c1d5e3-a571-418f-a344-7028c5d5e5b7/image.png" alt=""></p>
<p>그림에서 &#39;주문&#39;은 다른 엔티티에 의존하지 않기 때문에 <strong>독립 엔티티</strong>이며, &#39;주문 상세&#39;은 &#39;주문&#39; 없이 존재하지 않기 때문에 <strong>종속 엔티티</strong>이다.
또한, &#39;주문&#39;과 &#39;주문 상세&#39;는 종속 관계(식별 관계)에 있어 실선으로 이어지고 하나의 &#39;주문&#39;은 하나 이상의 &#39;주문 내역&#39;과 <strong>1:N</strong>으로 연관된다.</p>
<h2 id="마스터-자료와-트랜잭션-자료">마스터 자료와 트랜잭션 자료</h2>
<ul>
<li>리소스 엔티티<ul>
<li>DB 설계에서 &#39;마스터&#39;가 될 수 있는 엔티티</li>
<li>ex) 거래처, 고객, 상품 등 기본적으로 필요한 데이터</li>
</ul>
</li>
<li>이벤트 엔티티 <ul>
<li>DB 설계에서 트랜잭션이 될 수 있는 엔티티</li>
<li>ex) 주문, 발주 등 어떤 사건이 일어났을 때 발생하는 데이터
<img src="https://velog.velcdn.com/images/dyeon-dev/post/ab2a613c-a65d-48c2-a87d-393ba1ce790e/image.png" alt=""></li>
</ul>
</li>
</ul>
<p>&#39;고객&#39;은 <strong>리소스 엔터티</strong>이므로 마스터가 될 수 있다. 
하지만 &#39;주문&#39;은 <strong>트랜잭션</strong>이 되기 때문에 <strong>이벤트 엔티티</strong>이다.</p>
<h3 id="step-1-리소스-엔티티-추출-및-정의">Step 1. 리소스 엔티티 추출 및 정의</h3>
<p>시스템 전체를 나타내는 <strong>요구 사항 분석 명세서 등으로부터 리소스 엔티티를 추출하고 영역을 정의</strong>한다.</p>
<p>설명, 제약, 규칙, 범위가 될 수 있는 것은 빠짐없이 찾아내서 엔티티의 영역을 정의한다. 이는 데이터 모델을 만들 때 필요한 것이다.</p>
<p>모든 엔티티는 충분히 의미가 있는 것이어야 한다.
즉, 각 엔티티가 &#39;무엇을 위해 존재하는가?&#39;, &#39;누가 관리를 할 것인가?&#39;를 중심으로 체크한다.</p>
<h3 id="step2-이벤트-엔티티-추출-및-정의">Step2. 이벤트 엔티티 추출 및 정의</h3>
<p>리소스와 마찬가지로 현재 파악 가능한 이벤트 엔티티를 찾아내고 각 영역을 정의한다.</p>
<p>관계에 대한 의미를 정의하는 문장은 &#39;~한다&#39;는 동사 구문을 갖게 된다. 
화살표 방향에 따라 &#39;부모 엔티티는 자식 엔티티를 ~한다.&#39;로 통일하면 알기 쉬울 것이다.</p>
<p>이 단계에서 <strong>📌 ERD 주요 엔티티</strong>를 정의해보자! 정리가 길어서 아래에 작성해두었다.</p>
<h3 id="step3-추출한-엔티티들-사이에-관계를-표현">Step3. 추출한 엔티티들 사이에 관계를 표현</h3>
<p>엔티티 사이의 관계에 대한 의미를 파악하고 정의한다.
모든 관계의 선은 방향성에 대한 이유가 있어야 한다.</p>
<p>이 단계에서 ☁️ <strong><a href="https://www.erdcloud.com/d/FuxP3XfkfPpyMamQi">ERD cloud</a>로 ER 다이어그램을 만들어보자!</strong>
<img src="https://velog.velcdn.com/images/dyeon-dev/post/78255b94-60fc-4aea-9d2d-4658e5e3c544/image.png" alt=""></p>
<ul>
<li>리소스 엔티티는 보라색으로 지정해두었다.</li>
<li>N:M 관계를 1:N 관계로 중간테이블로 분리한 엔티티는 주황색으로 지정해두었다.</li>
</ul>
<h3 id="step4-er-다이어그램-체크리스트">Step4. ER 다이어그램 체크리스트</h3>
<p>마지막으로 체크리스트로 제대로 ERD를 만들었는지 점검해보자!</p>
<p><strong>엔티티</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><strong>관계</strong></p>
<table>
<thead>
<tr>
<th>체크항목</th>
<th>질문</th>
</tr>
</thead>
<tbody><tr>
<td>다중도</td>
<td>1:1, 1:n 적합한가? 올바른 방향인가?</td>
</tr>
<tr>
<td>중복</td>
<td>중복되는 관계는 없는가?</td>
</tr>
<tr>
<td>이름</td>
<td>이름이 올바로 붙여 있는가? 관계 이름이 중복된 것은 없는가?</td>
</tr>
</tbody></table>
<p><strong>완전성</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>
<h2 id="📌-erd-주요-엔티티-정리">📌 ERD 주요 엔티티 정리</h2>
<p><strong>1. 고객 (Customer) 엔티티</strong></p>
<p>역할: 쇼핑몰을 이용하는 사용자
주요 속성: 고객ID(PK), 이름, 이메일, 주소, 연락처 등
관계:</p>
<ul>
<li>고객 → 주문 (1:N, 고객은 여러 주문을 할 수 있음)</li>
<li>고객 → 장바구니 (1:1 또는 1:N).</li>
<li>고객 → 리뷰 (1:N, 여러 리뷰 작성 가능)</li>
</ul>
<p><strong>2. 상품 카테고리 (Category) 엔티티</strong>
역할: 상품을 분류하는 체계
주요 속성: 카테고리ID(PK), 카테고리명, 상위 카테고리ID(계층적 구조 가능)
관계:</p>
<ul>
<li>카테고리 → 상품 (1:N, 한 카테고리에 여러 상품 소속)</li>
</ul>
<p><strong>3. 상품 (Product) 엔티티</strong>
역할: 판매되는 개별 상품
주요 속성: 상품ID(PK), 상품명, 가격, 재고수량, 설명 등
관계:</p>
<ul>
<li>상품 → 주문상세 (1:N, 상품은 여러 주문에 포함될 수 있음)</li>
<li>상품 → 장바구니아이템 (1:N)</li>
<li>상품 → 리뷰 (1:N)</li>
<li>상품 → 카테고리 (N:1)</li>
</ul>
<p><strong>4. 주문 (Order)</strong>
역할: 고객이 상품을 구매하기 위해 생성하는 거래 단위
주요 속성: 주문ID(PK), 고객ID(FK), 주문일자, 배송주소, 주문상태
관계:</p>
<ul>
<li>주문 → 주문상세 (1:N, 한 주문은 여러 상품을 가짐)</li>
<li>주문 → 트랜잭션 (1:N, 주문에 대해 여러 결제/환불 내역이 있을 수 있음)</li>
</ul>
<p><strong>5. 주문상세 (OrderDetail)</strong>
역할: 주문 안에 포함된 개별 상품 항목
주요 속성: (주문ID + 상품ID) = PK (복합키), 수량, 단가.
관계:</p>
<ul>
<li>주문상세 → 주문 (N:1)</li>
<li>주문상세 → 상품 (N:1)</li>
</ul>
<p>특징: 부모의 PK를 포함하므로 Identifying 관계(실선)</p>
<p><strong>6. 장바구니 (Cart / CartItem)</strong>
역할: 고객이 결제 전 담아둔 상품 목록
주요 속성: 카트ID(PK), 고객ID(FK), 상품ID(FK), 수량
관계:</p>
<ul>
<li>장바구니 → 고객 (N:1)</li>
<li>장바구니 → 상품 (N:1)</li>
</ul>
<p><strong>7. 상품리뷰 (Review)</strong>
역할: 고객이 구매한 상품에 대해 남기는 평가/코멘트
주요 속성: 리뷰ID(PK), 고객ID(FK), 상품ID(FK), 평점, 내용, 작성일
관계:</p>
<ul>
<li>리뷰 → 고객 (N:1)</li>
<li>리뷰 → 상품 (N:1)</li>
</ul>
<p><strong>8. 트랜잭션 (Transaction)</strong>
역할: 주문에 대해 발생한 금전적 거래 내역 (결제, 취소, 환불 등)
주요 속성: 트랜잭션ID(PK), 주문ID(FK), 결제일자, 결제수단, 금액, 상태
관계:</p>
<ul>
<li>트랜잭션 → 주문 (N:1)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[이벤트 핸들러(Event Handler) 정리]]></title>
            <link>https://velog.io/@dyeon-dev/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%95%B8%EB%93%A4%EB%9F%ACEvent-Handler</link>
            <guid>https://velog.io/@dyeon-dev/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%95%B8%EB%93%A4%EB%9F%ACEvent-Handler</guid>
            <pubDate>Thu, 28 Aug 2025 17:01:25 GMT</pubDate>
            <description><![CDATA[<h1 id="이벤트와-핸들러의-기본-개념">이벤트와 핸들러의 기본 개념</h1>
<p>&quot;<strong>이벤트</strong>&quot;는 우리가 웹 페이지에서 마우스를 클릭하거나 키보드를 누르는 것과 같은 사용자 행동, 또는 인풋 창에 포커스가 맞춰지거나 문서 로드가 완료되는 것과 같이 특정 시점에 발생하는 모든 행동을 의미한다.
이러한 이벤트가 발생했을 때 특정 함수를 실행하도록 할당하는 것을 &quot;<strong>핸들러</strong>&quot;라고 한다.
이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을 <strong>이벤트 핸들러 등록</strong>이라 한다.</p>
<p>이처럼 이벤트와 그에 대응하는 함수(이벤트 핸들러)를 통해 사용자와 애플리케이션은 상호작용을 할 수 있다. 이와 같이 이벤트 중심으로 제어하는 방식을 <strong>이벤트 드리븐 프로그래밍(event-driven programming)</strong>이라 한다.</p>
<h2 id="🚀-이벤트-핸들러-할당-방식">🚀 이벤트 핸들러 할당 방식</h2>
<p>사용자가 버튼을 &quot;클릭&quot;했을 때 메시지를 처리하고 싶다면,
개발자가 명시적으로 함수를 호출하는 것이 아니라 브라우저에게 함수 호출을 위임하는 것이다.
아래의 코드를 살펴보면 알겠지만 onclick 프로퍼티에 함수를 할당했다.</p>
<h3 id="html-속성-방식-어트리뷰트-방식">HTML 속성 방식 (어트리뷰트 방식)</h3>
<p>html과 js를 분리하지 않는 방식이다.
<code>onclick</code> 어트리뷰트로 하게되면 함수 호출문을 직접 할당하는 방식이라 잘 쓰진 않는다.</p>
<p>하지만 CBD 방식의 Angular/React/Svelte/Vue.js 같은 프레임워크에서는 이벤트 핸들러 어트리뷰트 방식으로 이벤트를 처리한다. CBD에서는 HTML, CSS, JS가 다른 개별 요소가 아닌, 뷰를 구성하기 위한 요소로 보기 때문에 관심사가 다르다고 생각하지 않는다.</p>
<h4 id="1-인라인-코드로-작성">1. <strong>인라인 코드로 작성</strong></h4>
<pre><code class="language-html">&lt;button onclick=&quot;alert(&#39;click&#39;)&quot;&gt;클릭&lt;/button&gt;</code></pre>
<h4 id="2-onclick-속성에-직접-함수명-지정">2. onclick 속성에 <strong>직접 함수명 지정</strong></h4>
<ul>
<li>이벤트 핸들러 안에서 직접 실행할 코드를 문자열로 작성하는 방식이기 때문에 <code>()</code>를 붙여야 실행 된다.<pre><code class="language-js">&lt;button onclick=&quot;sayHello()&quot;&gt;클릭&lt;/button&gt;
&lt;script&gt;
function sayHello() {
    alert(&quot;Hello&quot;);
}
&lt;/script&gt;</code></pre>
</li>
<li><code>onclick=&quot;sayHello()&quot;</code> 어트리뷰트는 파싱되어 다음과 같은 함수를 암묵적으로 생성하고, 이벤트 핸들러 어트리뷰트 이름과 동일한 키 onlick 이벤트 핸들러 프로퍼티에 할당한다.<pre><code class="language-js">function onclick(event) {
sayHello();
}</code></pre>
</li>
<li>이처럼 동작하는 이유는 이벤트 핸들러에 인수를 전달하기 위해서다. 만약 이벤트 핸들러 어트리뷰트 값으로 함수 참조를 할당해야한다면 이벤트 핸들러에 인수를 전달하기는 곤란하다.<pre><code class="language-html">&lt;!-- 이벤트 핸들러에 인수를 전달하기는 x --&gt;
&lt;button onclick=&quot;sayHello&quot;&gt;클릭&lt;/button&gt;</code></pre>
</li>
</ul>
<h3 id="js에서-할당-방식-이벤트-핸들러-프로퍼티-방식">JS에서 할당 방식 (이벤트 핸들러 프로퍼티 방식)</h3>
<p>html과 js를 분리하는 방식이다.
DOM 노드 객체는 이벤트에 대응하는 이벤트 핸들러 프로퍼티를 가지고 있다.
이벤트 핸들러 프로퍼티 키는 어트리뷰트와 마찬가지로 on+이벤트 종류를 나타내는 이벤트 타입으로 이뤄져있다. 
이벤트 핸들러 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록된다.</p>
<h4 id="3-dom-요소에-id를-부여한-후-자바스크립트에서-객체핸들러--핸들러함수-할당">3. <strong>DOM 요소에 ID를 부여한 후 자바스크립트에서 <code>객체.핸들러 = 핸들러(함수)</code> 할당</strong></h4>
<ul>
<li>이때 핸들러 할당 시 <strong>함수명 뒤에 괄호를 붙이지 않아야 함수 객체 자체를 직접 할당</strong>할 수 있다.</li>
<li>()가 있으면, sayHello()가 즉시 실행된 결과값(여기서는 undefined)이 onclick에 들어가 버려서 크롬창 열자마자 alert 창이 떠버린다.<pre><code class="language-js">&lt;button id=&quot;btn&quot;&gt;클릭&lt;/button&gt;
&lt;script&gt;
const el = document.getElementById(&#39;btn&#39;)
el.onclick = sayHello;
&lt;/script&gt;</code></pre>
</li>
</ul>
<h4 id="4-addeventlistener">4. <strong><code>addEventListener</code></strong></h4>
<ul>
<li><strong>하나의 이벤트에 여러 핸들러를 등록할 수 있기 때문에 가장 권장되는 방식이다.</strong></li>
<li>addEventListener도 이벤트 핸들러를 등록하는 방식과 삭제하는 방식이 다양하다.</li>
</ul>
<p>4-1. <strong><code>addEventListener</code>(이벤트, 함수핸들러)</strong></p>
<pre><code class="language-js">&lt;button id=&quot;btn2&quot;&gt;클릭&lt;/button&gt;
&lt;script&gt;
  function sayHello() {
    alert(&quot;Hello&quot;);
  }

  const el = document.getElementById(&#39;btn2&#39;)
  el.addEventListener(&quot;click&quot;, sayHello);
&lt;/script&gt;</code></pre>
<p>4-2. *<em><code>addEventListener</code>(이벤트, 함수 직접 작성) - 익명함수로 등록 *</em></p>
<pre><code class="language-js">&lt;button id=&quot;btn2&quot;&gt;클릭&lt;/button&gt;
&lt;script&gt;
  const el = document.getElementById(&#39;btn2&#39;)
  el.addEventListener(&quot;click&quot;, () =&gt; {
      alert(&quot;Hello&quot;);
  });
&lt;/script&gt;</code></pre>
<p>4-3. <strong><code>addEventListener</code> 삭제</strong></p>
<ul>
<li><code>removeEventListener</code>를 사용하여 할당된 핸들러를 삭제</li>
</ul>
<blockquote>
<p>💡 이벤트 리스너 삭제할 때 주요포인트</p>
</blockquote>
<ul>
<li>익명 함수(() =&gt; { ... })로 등록한건 제거가 불가능하다.<pre><code class="language-js">&lt;button id=&quot;btn2&quot;&gt;클릭&lt;/button&gt;
&lt;script&gt;
const el = document.getElementById(&#39;btn2&#39;);
// 익명 함수로 등록
el.addEventListener(&quot;click&quot;, () =&gt; {
  alert(&quot;Hello&quot;);
});
// 아래 코드는 제거 불가능 (참조가 다르기 때문)
el.removeEventListener(&quot;click&quot;, () =&gt; {
  alert(&quot;Hello&quot;);
});
&lt;/script&gt;
</code></pre>
</li>
</ul>
<pre><code>- `removeEventListener`는 반드시 `addEventListener`에서 등록한 **동일한 함수 객체 참조**를 써야 작동하기 때문이다. JS 엔진은 겉보기엔 코드가 같아도, &quot;완전히 별개 함수&quot;로 인식하기 때문에 나중에 제거할 가능성이 있으면 별도 **함수 선언/변수에 담아서 등록**해야 한다.
```js
&lt;button id=&quot;btn2&quot;&gt;클릭&lt;/button&gt;
&lt;script&gt;
  function sayHello() {
    alert(&quot;Hello&quot;);
  }

  const el = document.getElementById(&#39;btn2&#39;);

  // 이벤트 등록
  el.addEventListener(&quot;click&quot;, sayHello);

  // 3초 후에 이벤트 제거
  setTimeout(() =&gt; {
    // 함수 참조로 등록 → 제거 가능
    el.removeEventListener(&quot;click&quot;, sayHello);
    alert(&quot;이제 클릭해도 반응 안 함!&quot;);
  }, 3000);
&lt;/script&gt;</code></pre><p>4-4. <strong><code>DOMContentLoaded</code>처럼 문서 로드 완료 시 발생하는 이벤트</strong>의 경우, <strong>반드시 <code>addEventListener</code>를 사용</strong>해야 한다.</p>
<ul>
<li>HTML에 직접 할당하면 동작하지 않는다. </li>
<li>DOMContentLoaded 전용 핸들러 속성(onDOMContentLoaded)은 없다. <code>document.onDOMContentLoaded = ...</code> 이런 건 동작 X</li>
<li>반면, <code>load</code>, <code>beforeunload</code> 같은 이벤트는 <code>window.onload</code>, <code>window.onbeforeunload</code> 같은 핸들러 속성도 제공된다.</li>
</ul>
<blockquote>
<p>💡 팁 : 가급적 addEventListener로 통일하는 것이 좋다.</p>
</blockquote>
<p>아래에서 DOMContentLoaded와 관련된 문서 로딩 시점의 이벤트제어를 정리했다.</p>
<hr>
<h2 id="📃-문서-로딩-시점의-이벤트-제어domcontentloaded-load-beforeunload-unload">📃 문서 로딩 시점의 이벤트 제어(DOMContentLoaded, load, beforeunload, unload)</h2>
<p>공식 문서에 따르면 HTML 문서의 생명주기엔 다음과 같은 3가지 주요 이벤트가 관여한다고 한다. </p>
<ul>
<li><strong>DOMContentLoaded</strong> : <strong>브라우저가 HTML을 전부 읽고 DOM 트리를 완성하는 즉시 발생</strong>한다. 이미지 파일(<code>&lt;img&gt;</code>)이나 스타일시트 등의 기타 자원은 기다리지 않는다.</li>
<li><strong>onload</strong> : HTML로 DOM 트리를 만드는 게 완성되었을 뿐만 아니라 <strong>모든 콘텐츠(images, script, css, etc)가 모두 불러오는 것이 끝났을 때 발생</strong>한다.</li>
<li><strong>beforeunload/unload</strong> : <strong>사용자가 페이지를 떠날 때 발생</strong>한다.</li>
</ul>
<p>따라서 위의 순서대로 생명주기가 실행된다. 각각의 시점 이벤트이기 때문에 이벤트를 주입해서 사용해주면 된다.</p>
<pre><code class="language-js">// only document
window.addEventListener(&quot;DOMContentLoaded&quot;, (event) =&gt; {
    console.log(&quot;DOMContentLoaded&quot;);
});

// after resources (css, images)
window.addEventListener(&quot;onload&quot;, (event) =&gt; {
    console.log(&quot;load&quot;);
});

// before unload
window.addEventListener(&quot;beforeunload&quot;, (event) =&gt; {
    console.log(&quot;beforeunload&quot;);
});

// resource is being unloaded
window.addEventListener(&quot;unload&quot;, (event) =&gt; {
    console.log(&quot;unload&quot;);
});</code></pre>
<p>주로 어떤 상황에서 활용될까?</p>
<h3 id="domcontentloaded">DOMContentLoaded</h3>
<p><strong>DOM이 준비된 것을 확인한 후 원하는 DOM 노드를 찾아 핸들러를 등록해 인터페이스를 초기화할 때 사용</strong>된다. 
앞에서 언급했던 것처럼 이 이벤트를 다루려면 <code>addEventListener</code>를 사용해야 한다.</p>
<pre><code class="language-js">&lt;script&gt;
  function ready() {
    alert(&#39;DOM이 준비되었습니다!&#39;);

    // 이미지가 로드되지 않은 상태이기 때문에 사이즈는 0x0입니다.
    alert(`이미지 사이즈: ${img.offsetWidth}x${img.offsetHeight}`);
  }

  document.addEventListener(&quot;DOMContentLoaded&quot;, ready);
&lt;/script&gt;

&lt;img id=&quot;img&quot; src=&quot;https://en.js.cx/clipart/train.gif?speed=1&amp;cache=0&quot;&gt;</code></pre>
<p>위 예시에서 DOMContentLoaded은 DOM이 로드되었을 때 실행된다. 
따라서 밑에 위치한 img뿐만 아니라 모든 요소에 접근할 수 있다. 하지만 이미지 파일의 로딩은 기다리지 않기 때문에 alert 창에는 이미지 사이즈가 0으로 뜬다.</p>
<p>이와 같은 특징을 브라우저 렌더링 과정에 대입해서 살펴보자.
<img src="https://velog.velcdn.com/images/dyeon-dev/post/c6e6e2a2-d038-43bb-bcbe-f42c5f0c5afa/image.png" alt=""></p>
<blockquote>
<p>💡 &quot;<strong>렌더링 과정 중 DOM 트리가 완성되면 DOMContentLoaded 이벤트가 발생한다</strong>&quot;</p>
</blockquote>
<p>여기서 말하는 <strong>DOM 트리</strong>가 완성되는 단계를 브라우저가 <strong>동기적</strong>으로, 즉 위에서 아래로 순차적으로 실행되는 관점에서 좀 더 자세히 살펴보자. 
이 과정에서 <strong>HTML을 파싱하면서 자바스크립트와 CSS를 만났을 때 DOMContentLoaded 이벤트처리</strong>는 어떻게 되는가?</p>
<p><strong>1. HTML 문서를 처리하는 도중에 <code>&lt;script&gt;</code> 태그를 만났을 때</strong></p>
<ul>
<li>DOM 트리 구성을 중단하고 <code>&lt;script&gt;</code>를 실행한다. <code>&lt;script&gt;</code>가 끝나면 다시 HTML 문서 처리를 재개한다.</li>
<li>따라서 <strong>DOMContentLoaded 이벤트 역시 <code>&lt;script&gt;</code> 안에 있는 스크립트가 처리되고 난 후에 발생</strong>한다.<pre><code class="language-js">&lt;script&gt;
document.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {
  alert(&quot;DOM이 준비되었습니다!&quot;);
});
&lt;/script&gt;
</code></pre>
</li>
</ul>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>

<script>
  alert("라이브러리 로딩이 끝나고 인라인 스크립트가 실행되었습니다.");
</script>
<pre><code>- 위 코드의 결과를 보면, 스크립트가 모두 실행되고 나서야 DOMContentLoaded 이벤트가 발생한다.
- 따라서 &quot;라이브러리 로딩이 끝나고…&quot;가 먼저 보인 후 &quot;DOM이 준비되었습니다!&quot;가 출력된다.

**2. HTML 문서를 처리하는 도중에 외부 스타일시트를 만났을 때**
- HTML 파싱 중에 `&lt;link rel=&quot;stylesheet&quot;&gt;`를 만나면 CSS 파싱은 비동기적으로 진행될 수 있다. 즉, **외부 스타일시트는 DOM에 영향을 주지 않기 때문에 DOMContentLoaded는 외부 스타일시트가 로드되기를 기다리지 않는다.**
- 따라서 DOMContentLoaded 이벤트가 발생할 때 CSSOM이 모두 준비됐다는 보장은 없다.
- 보통은 **CSS 파일이 아직 다운로드 중이어도 DOMContentLoaded가 발생**할 수 있다.

- 💥 그런데 한가지 예외가 있다. **스타일시트를 불러오는 태그 바로 다음에 스크립트가 위치하면 이 스크립트는 스타일시트가 로드되기 전까지 실행되지 않는다.**
```js
&lt;link type=&quot;text/css&quot; rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&gt;
&lt;script&gt;
  // 이 스크립트는 위 스타일시트가 로드될 때까지 실행되지 않습니다.
  // 스크립트에서 요소의 좌표 정보를 사용하고 있다.
  alert(getComputedStyle(document.body).marginTop);
&lt;/script&gt;</code></pre><ul>
<li><code>&lt;link&gt;</code>와 <code>&lt;script&gt;</code> 사이에 다른 태그가 끼면, 스크립트 실행이 그 CSSOM을 보장해서 기다리지는 않는다.</li>
<li>왜냐하면 그 시점에서는 해당 스크립트가 “바로 직전에 선언된 스타일시트와 직접 연관 있을 가능성”이 낮다고 보기 때문이다.</li>
</ul>
<h3 id="load">load</h3>
<p>DOMContentLoaded 이벤트는 DOM만 보장했다면,
<strong>load 이벤트</strong>는 <strong>DOM + CSSOM + 이미지/폰트 같은 모든 리소스까지 준비 완료</strong>되었을 때 실행된다.</p>
<pre><code class="language-js">&lt;script&gt;
  window.onload = function() { // window.addEventListener(&#39;load&#39;, (event) =&gt; {와 동일합니다.
    alert(&#39;페이지 전체가 로드되었습니다.&#39;);

    // 이번엔 이미지가 제대로 불러와 진 후에 실행됩니다.
    alert(`이미지 사이즈: ${img.offsetWidth}x${img.offsetHeight}`);
  };
&lt;/script&gt;

&lt;img id=&quot;img&quot; src=&quot;https://en.js.cx/clipart/train.gif?speed=1&amp;cache=0&quot;&gt;</code></pre>
<ul>
<li>위 예시에서 window.onload는 이미지가 모두 로드되고 난 후 실행되기 때문에 이미지 사이즈가 제대로 출력된다.</li>
</ul>
<h3 id="beforeunload">beforeunload</h3>
<p><strong>사용자가 페이지를 떠날 때</strong> 추가 확인을 요청할 수 있는데, 이는 <strong>beforeunload</strong>를 통해 제어해주면 된다.</p>
<pre><code class="language-js">// use addEventListener beforeunload
window.addEventListener(&quot;beforeunload&quot;, (event) =&gt; {
  // 표준에 따라 기본 동작 방지
  event.preventDefault();
  // Chrome에서는 returnValue 설정이 필요함
  event.returnValue = &quot;&quot;;
});</code></pre>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/d0af996e-eab6-4807-a3a3-8dfa38e02ed1/image.png" alt=""></p>
<ul>
<li>페이지를 벗어날때 나오던 위와 같은 알림 창이 바로 이런 상황에서 처리를 해준 것이다. 이벤트를 막아서 &quot;정말 떠날 건지 확인&quot;하는 경고창을 띄울 수 있다.</li>
<li>표준(HTML Living Standard) 에서는 <strong>event.preventDefault()</strong> 호출만으로도 충분하다고 정의돼 있다. 하지만 크롬, 사파리, 엣지, 파이어폭스 등 주요 브라우저에서는 호환성을 위해 <strong>event.returnValue</strong>에 무언가를 할당해야 대화상자가 뜨도록 바뀌었다.</li>
</ul>
<h3 id="unload">unload</h3>
<p><strong>unload</strong> 페이지가 <strong>완전히 종료</strong>될 때 마지막으로 할 수 있는 작업 이벤트이다. 
페이지 종료 시 마지막 정리 작업이나 <strong>통계, 로그 전송</strong> 같은 목적에만 쓸 수 있다.
만약, 사용자와 상호작용(경고창 등)을 하려면 반드시 <strong>beforeunload</strong>를 써야 한다.</p>
<p>unload 이벤트는 사용자가 페이지를 떠날 때 발생하므로 <strong>unload 이벤트에서 분석 정보를 서버로 보낼 수도 있을 것이다.</strong></p>
<p><strong>세션 종료 로그 보내기</strong></p>
<pre><code class="language-js">window.addEventListener(&quot;unload&quot;, () =&gt; {
  // 사용자가 페이지를 떠날 때 서버로 로그 전송
  navigator.sendBeacon(&quot;/log&quot;, JSON.stringify({ action: &quot;exit&quot;, time: Date.now() }));
});</code></pre>
<p><strong>Analytics / 페이지 사용 통계 기록</strong></p>
<pre><code class="language-js">window.addEventListener(&quot;unload&quot;, () =&gt; {
  // 페이지를 떠날 때 사용자가 머문 시간을 서버에 기록
  const timeSpent = Date.now() - performance.timing.navigationStart;
  navigator.sendBeacon(&quot;/analytics&quot;, JSON.stringify({ timeSpent }));
});</code></pre>
<ul>
<li><code>navigator.sendBeacon()</code>를 쓰면 비동기 HTTP 요청을 안전하게 보낼 수 있다.</li>
<li>일반 fetch나 XMLHttpRequest는 unload 시점에서는 대부분 취소된다.</li>
</ul>
<hr>
<h2 id="⌨️-자주-사용되는-이벤트event">⌨️ 자주 사용되는 이벤트(Event)</h2>
<p>다양한 이벤트 타입이 있지만, 자주 사용되는 몇 가지를 중심으로 살펴보자. </p>
<h3 id="마우스-클릭-이벤트">마우스 클릭 이벤트</h3>
<ul>
<li><strong><code>click</code></strong> : 요소를 클릭할 때 발생</li>
<li><strong><code>dblclick</code></strong> : 더블 클릭할 떄 발생</li>
</ul>
<h3 id="키보드-이벤트">키보드 이벤트</h3>
<p>이벤트 객체를 인수로 받아, <strong>어떤 키가 눌렸는지(<code>event.key</code>), 현재 이벤트 타입(<code>event.type</code>)</strong> 등 다양한 정보를 활용할 수 있다. </p>
<ul>
<li><strong><code>keyup</code></strong>: 누른 키에서 손을 뗄 때 실행</li>
<li><strong><code>keydown</code></strong>: 키보드를 누를 때 실행. 키를 누르고 있을 때 단 한번만 실행</li>
<li><strong><code>keypress</code></strong>(deprecated): 키보드를 누를 때 실행. 키를 누르고 있을 때 계속 실행 </li>
</ul>
<h4 id="1️⃣-keyup">1️⃣ <code>keyup</code></h4>
<ul>
<li><p><strong>언제?</strong> 키에서 손을 뗐을 때 실행</p>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li>입력이 끝났을 때를 감지할 수 있음</li>
<li>입력 중에는 동작 안 하고, 손을 뗐을 때만 발생</li>
</ul>
</li>
<li><p><strong>사용 예시</strong></p>
<ul>
<li>검색창 자동완성 (keyup 시점에 서버 요청 보내기)</li>
<li>입력 완료 후 유효성 검사 (비밀번호 길이, 이메일 포맷 체크 등)</li>
<li>키 누르고 있다가 뗄 때 어떤 동작을 멈추는 상황 (게임 캐릭터 이동 중지)</li>
</ul>
</li>
<li><p><strong>사용법</strong></p>
<ul>
<li><p>핸들러는 이벤트객체 (event) 를 인수로 받음.</p>
<pre><code class="language-js">&lt;body&gt;
&lt;input id=&quot;text&quot; type=&quot;text&quot; /&gt;

&lt;script&gt;
const input = document.getElementById(&quot;text&quot;);
input.addEventListener(&quot;keyup&quot;, (event) =&gt; {
  console.log(&quot;현재 입력값:&quot;, event, event.key);
});
&lt;/script&gt;
&lt;/body&gt;</code></pre>
</li>
</ul>
</li>
<li><p>결과값</p>
<pre><code>- `&quot;텍스트&quot;` : 입력한 특정 텍스트가 콘솔창에 출력됨
- `event` : 해당 키보드 이벤트 관련된 모든 정보가 콘솔창에 출력됨
- `event.key` : 오직 키보드 값만 콘솔창에 출력됨</code></pre></li>
</ul>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/b597b891-bb47-471b-8e3a-2e125f4db2ff/image.png" alt=""></p>
<h4 id="2️⃣-keydown">2️⃣ <code>keydown</code></h4>
<ul>
<li><strong>언제?</strong> 키를 &quot;누르는 순간&quot; 단 한 번 실행</li>
<li><strong>특징</strong><ul>
<li>어떤 키든(문자, 화살표, F1~F12, ESC 등) 전부 인식 가능</li>
<li>반복 입력(꾹 누르고 있을 때도 계속 발생)</li>
</ul>
</li>
<li><strong>사용 예시</strong><ul>
<li>단축키 구현 (Ctrl + S, ESC 눌렀을 때 동작 등)</li>
<li>방향키로 캐릭터 움직이기</li>
<li>입력 도중 특정 키 막기 (예: 숫자만 입력 허용)<pre><code class="language-js">document.addEventListener(&quot;keydown&quot;, (e) =&gt; {
if (e.key === &quot;Escape&quot;) {
console.log(&quot;ESC 눌렀다!&quot;);
}
});</code></pre>
</li>
</ul>
</li>
</ul>
<p>3️⃣ <code>keypress</code> (⚠️ 현재는 표준에서 deprecated, 대부분 keydown으로 대체)</p>
<ul>
<li>언제? 키를 눌러서 실제 &quot;문자&quot;가 입력될 때 실행</li>
<li>특징<ul>
<li>문자를 표현할 수 있는 키만 인식 (예: a, 1, ; 등)</li>
<li>화살표키, F1 같은 제어 키는 감지 못함</li>
<li>한글/이모지 같은 조합형 문자는 제대로 인식되지 않음</li>
</ul>
</li>
<li>사용 예시<ul>
<li>과거에는 &quot;입력되는 문자 자체&quot; 확인할 때 사용했음 (예: 텍스트 입력 감시)</li>
<li>지금은 input 이벤트나 keydown을 대신 사용 권장</li>
</ul>
</li>
</ul>
<h4 id="key-이벤트-발생-순서">key 이벤트 발생 순서</h4>
<p>키를 누르면 keydown 이벤트가 발생, 이어서 바로 keypress 이벤트가 발생. 그런 다음 키가 해제되면 keyup 이벤트가 생성된다.</p>
<blockquote>
<p>💡 keydown &gt; keypress &gt; keyup 순으로 이벤트 진행</p>
</blockquote>
<blockquote>
<p>직접 코드로 확인: <a href="https://jsbin.com/vigimenaji/edit?html,output">https://jsbin.com/vigimenaji/edit?html,output</a></p>
</blockquote>
<h3 id="포커스-이벤트input-창">포커스 이벤트(Input 창)</h3>
<ul>
<li><p><strong><code>focus</code></strong> : 포커스가 맞춰질 때</p>
</li>
<li><p><strong><code>blur</code></strong> : 포커스를 잃을 때</p>
</li>
<li><p>사용자가 폼에 입력할 때 UI를 동적으로 변경할 수 있다. </p>
<pre><code class="language-js">&lt;body&gt;
  &lt;input id=&quot;text&quot; type=&quot;text&quot; /&gt;

  &lt;script&gt;
    const input = document.getElementById(&quot;text&quot;);
    input.addEventListener(&quot;focus&quot;, () =&gt; {
      input.style.backgroundColor = &quot;lightblue&quot;;
    });

    input.addEventListener(&quot;blur&quot;, () =&gt; {
      input.style.backgroundColor = null;
    });
  &lt;/script&gt;
&lt;/body&gt;</code></pre>
</li>
</ul>
<h3 id="마우스-이벤트">마우스 이벤트</h3>
<ul>
<li><strong><code>mousemove</code></strong> : 마우스를 움직일 때마다 발생한다. <ul>
<li><code>event.clientX</code>와 <code>event.clientY</code>를 사용하여 <strong>마우스 포인터의 현재 위치를 파악</strong>할 수 있다. </li>
<li>이를 활용하여 웹 페이지에서 마우스 커서를 따라 움직이는 요소를 만들 수 있다. </li>
</ul>
</li>
</ul>
<pre><code class="language-js"> &lt;body&gt;
    &lt;div
      id=&quot;box&quot;
      style=&quot;
        position: relative;
        width: 100px;
        height: 100px;
        border: 2px solid lightblue;
      &quot;
    &gt;&lt;/div&gt;

    &lt;div
      id=&quot;circle&quot;
      style=&quot;
        position: absolute;
        width: 10px;
        height: 10px;
        background-color: lightpink;
        border-radius: 50%;
      &quot;
    &gt;&lt;/div&gt;

    &lt;script&gt;
      const box = document.getElementById(&quot;box&quot;);
      const circle = document.getElementById(&quot;circle&quot;);

      box.addEventListener(&quot;mousemove&quot;, (event) =&gt; {
        circle.style.top = `${event.clientY}px`;
        circle.style.left = `${event.clientX}px`;
      });
    &lt;/script&gt;
  &lt;/body&gt;</code></pre>
<h3 id="윈도우-이벤트">윈도우 이벤트</h3>
<ul>
<li><strong><code>resize</code></strong> : 윈도우 창의 크기가 변경될 때 발생한다.<ul>
<li>오직 window 객체에서만 발생한다.</li>
<li>이를 통해 창의 너비와 높이 값을 업데이트하여 <strong>반응형 웹 디자인</strong>에 활용할 수 있다.<pre><code class="language-js">&lt;script&gt;
window.addEventListener(&quot;resize&quot;, () =&gt; {
  document.body.innerText = `현재 창 크기는 ${window.innerWidth} x ${window.innerHeight}`;
});
&lt;/script&gt;</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="값-변경-이벤트">값 변경 이벤트</h3>
<ul>
<li><p><strong><code>input</code></strong>: 입력 필드의 값이 어떤 방식으로든 변경되었을 때마다 발생</p>
<ul>
<li>사용자가 키보드로 입력하는 것뿐만 아니라, 마우스로 붙여넣기, 음성 인식, 드래그 슬라이더 조작 등 다양한 방법으로 입력 필드(input, textarea, checkbox, radio, select 등)의 값이 달라졌을 때 트리거된다. </li>
<li>따라서 <strong>실시간으로 입력 필드 값의 변화를 감지하여 즉각적으로 반응</strong>해야 하는 경우 유용하게 사용된다.<ul>
<li>실시간 유효성 검사: 사용자가 입력할 때마다 입력값의 유효성을 검사하여 오류 메시지를 즉시 보여줄 수 있다. </li>
<li>값 변화 시 처리: 입력 필드에 텍스트를 입력하면 해당 텍스트를 다른 요소에 즉시 표시하거나, 특정 단어를 다른 것으로 자동으로 바꾸는 등의 처리를 할 수 있다. <pre><code class="language-html">&lt;input type=&quot;text&quot; id=&quot;input&quot;&gt; oninput: &lt;span id=&quot;result&quot;&gt;&lt;/span&gt;
&lt;script&gt;
input.oninput = function() {
result.innerHTML = input.value;
};
&lt;/script&gt;</code></pre>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong><code>change</code></strong>: 입력 필드에서 포커스가 해제된 후에 값이 변경되었을 때 발생</p>
<ul>
<li>change 이벤트는 input 이벤트와는 달리 HTML 요소가 포커스를 잃었을 때 사용자 입력이 종료되었다고 인식하여 발생한다. <pre><code class="language-html">&lt;input type=&quot;text&quot; onchange=&quot;alert(this.value)&quot;&gt;
&lt;input type=&quot;button&quot; value=&quot;버튼&quot;&gt;</code></pre>
</li>
</ul>
</li>
</ul>
<p>즉, 사용자가 입력을 하고 있을 때는 input 이벤트가 발생하고, 사용자 입력이 종료되어 값이 변경되면 change 이벤트가 발생한다.</p>
<h4 id="참고자료">참고자료</h4>
<ul>
<li><a href="https://www.youtube.com/watch?v=J5EJija5-Sw&amp;t=157s">이벤트 핸들러 예제 및 사용법</a></li>
<li><a href="https://www.w3schools.com/jsref/jsref_event.asp">HTML DOM Events
</a></li>
<li><a href="https://ko.javascript.info/onload-ondomcontentloaded">문서 로딩 시점의 이벤트 제어</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Events#Event_handler_properties">이벤트란</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event">key 이벤트 핸들러</a></li>
<li><a href="https://ko.javascript.info/events-change-input#:~:text=%ED%82%A4%EB%B3%B4%EB%93%9C%20%EC%9D%B4%EB%B2%A4%ED%8A%B8%EC%99%80%20%EB%8B%AC%EB%A6%AC%20input%20%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%8A%94%20%EC%96%B4%EB%96%A4%20%EB%B0%A9%EB%B2%95%EC%9C%BC%EB%A1%9C%EB%93%A0,%EA%B8%80%EC%9E%90%EB%A5%BC%20%EB%B6%99%EC%97%AC%20%EB%84%A3%EA%B1%B0%EB%82%98%20%EC%9D%8C%EC%84%B1%EC%9D%B8%EC%8B%9D%20%EA%B8%B0%EB%8A%A5%EC%9D%84%20%EC%82%AC%EC%9A%A9%ED%95%B4%20%EA%B8%80%EC%9E%90">값 변경 이벤트 핸들러</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그인 방식에는 어떤 것들이 있을까? Express에 적용해보자 (세션, 쿠키)]]></title>
            <link>https://velog.io/@dyeon-dev/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B0%A9%EC%8B%9D%EC%97%90%EB%8A%94-%EC%96%B4%EB%96%A4-%EA%B2%83%EB%93%A4%EC%9D%B4-%EC%9E%88%EC%9D%84%EA%B9%8C-Express%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90-%EC%84%B8%EC%85%98-%EC%BF%A0%ED%82%A4</link>
            <guid>https://velog.io/@dyeon-dev/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B0%A9%EC%8B%9D%EC%97%90%EB%8A%94-%EC%96%B4%EB%96%A4-%EA%B2%83%EB%93%A4%EC%9D%B4-%EC%9E%88%EC%9D%84%EA%B9%8C-Express%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90-%EC%84%B8%EC%85%98-%EC%BF%A0%ED%82%A4</guid>
            <pubDate>Tue, 26 Aug 2025 13:38:54 GMT</pubDate>
            <description><![CDATA[<p>우리가 사용하는 브라우저는 HTTP 통신을 통해 클라이언트와 서버가 통신을 한다. 그런데 이 HTTP 규약은 stateless하다는 특징이 있다. 그럼 사용자가 로그인을 할 때 새 탭에서 로그인을 할 경우 매번 로그인을 시도해야 된다. 그래서 로그인을 할 때 정보를 어딘가에 저장해두면 문제를 쉽게 해결할 수가 있다. </p>
<p>그 때 사용하는 것이 바로 <strong>브라우저의 스토리지</strong>이다. <strong>쿠키</strong> 같은 것을 사용해서 해당 로그인 정보를 브라우저에 저장해서 로그인을 편리하게 구현할 수 있다.</p>
<h2 id="쿠키란">쿠키란?</h2>
<ul>
<li>쿠키는 서버가 클라이언트에게 전송하는 작은 데이터 파일이다. </li>
<li>쿠키는 클라이언트에 저장되고 key와 value로 구성된다. </li>
<li>서버에게 받은 쿠키는 정보들을 웹 브라우저에 저장하고 </li>
<li>브라우저가 자동으로 요청 시마다 쿠키를 서버로 전송한다.</li>
<li>용량 제한이 있다. (도메인별 약 4KB 정도)</li>
</ul>
<h3 id="쿠키의-수명과-브라우저-동작">쿠키의 수명과 브라우저 동작</h3>
<p>쿠키는 만료 시간(Expire/Max-Age)에 따라 <strong>세션 쿠키(브라우저 종료 시 삭제)</strong> / <strong>영속 쿠키(만료일까지 유지)</strong>로 나뉜다.</p>
<ul>
<li><strong>세션 쿠키 (Session Cookie)</strong>: Expires나 Max-Age를 설정하지 않은 경우 기본값<ul>
<li>브라우저 종료 시 자동 삭제</li>
<li>로그인 유지가 되지 않음</li>
</ul>
</li>
<li><strong>영속 쿠키 (Persistent Cookie)</strong>: Expires(특정 날짜/시간)나 Max-Age(초 단위 유효시간)를 지정한 경우<ul>
<li>지정한 시간이 될 때까지 브라우저를 껐다 켜도 그대로 남아있음</li>
<li>자동 로그인 유지 가능</li>
</ul>
</li>
</ul>
<p><code>{ Expires, Max-Age }</code> 옵션을 지정하지 않으면 원래는 세션 쿠키가 되어야 한다. 즉, 브라우저를 닫으면 삭제되는 게 정상이다. 그런데 브라우저마다 동작이 좀 다르다!</p>
<ul>
<li><strong>Chrome, Edge, 일부 최신 브라우저</strong> → 세션 복원(Session Restore) 기능이 켜져 있으면, 세션 쿠키도 다시 복원해버린다. 
그래서 브라우저를 종료했다 다시 열어도 쿠키가 남아있는 것처럼 보인다.</li>
<li><strong>Firefox, Safari</strong> → 옵션을 껐으면 세션 쿠키는 닫을 때 지워진다.</li>
</ul>
<h3 id="🔑-쿠키-로그인-방식">🔑 쿠키 로그인 방식</h3>
<ol>
<li>클라이언트가 로그인을 시도한다.</li>
<li>로그인을 성공했을 경우, 서버는 토큰을 생성해서 클라이언트에 전달한다.</li>
<li>클라이언트는 쿠키에 토큰을 저장한다.</li>
<li>해당 웹사이트를 방문할 때마다 쿠키는 서버에 보내지게 된다.</li>
<li>서버에서는 토큰이 유효한지 판단을 하며 클라이언트와 통신한다.</li>
</ol>
<p>이 과정에서 중요한 부분은 클라이언트에 저장이 된다는 점이다. 이 사실은 여러 상황으로 직결될 수 있다.</p>
<ul>
<li><strong>모든 브라우저에서 지원</strong>한다.</li>
<li>서버에서 따로 저장을 하지 않기 때문에 <strong>서버 과부화가 일어나지 않는다</strong>.</li>
<li>누군가가 쿠키에 담긴 <strong>나의 토큰 정보를 빼앗긴다면</strong> 나인척 할 수 있다. (보안 취약)
 
보안적으로 봤을 때, <strong>토큰이 탈취 될 가능성</strong>이 있어서 쿠키만 사용한다는 것은 좋은 생각이 아니다.</li>
</ul>
<p>그래서 로그인을 유지시키기 위해서는 <strong>쿠키 방법에 세션 방법을 추가해서 중요한 정보는 서버에 저장하여 보안을 관리</strong>한다.</p>
<h2 id="세션이란">세션이란?</h2>
<ul>
<li>세션은 브라우저 저장소가 아니다.</li>
<li>세션은 <strong>서버 메모리/DB 같은 서버 측에 저장</strong>되는 정보이다.</li>
<li>세션은 “로그인한 사용자&quot; 정보를 쿠키에 저장하지 않고 서버에 저장하며, 대신 <strong>쿠키에는 이를 식별할 수 있는 <code>세션 ID</code>를 저장</strong>한다. </li>
<li>이처럼 세션도 쿠키를 이용하지만 추정 불가능한 <code>세션 ID</code>를 주고받기 때문에 <strong>보안상 안전</strong>하다. </li>
<li>따라서 노출되면 안 되는 중요한 정보는 세션을 이용하여 저장한다.</li>
<li>세션은 서버에 저장되다 보니까, 너무 많이 담기면 서버 과부화가 일어날 가능성이 생긴다.</li>
<li>서버에서 사용되다 보니까, 쿠키보다는 탈취되는 과정이 어렵다.</li>
</ul>
<p>⚠️ 주의) 여기서 말하는 세션은 세션 스토리지랑은 다른거다! </p>
<p> </p>
<h3 id="🔑-전통적인-세션-로그인-방식">🔑 전통적인 세션 로그인 방식</h3>
<ol>
<li>클라이언트가 로그인을 시도한다.</li>
<li>로그인에 성공했을 경우, <strong>서버는 클라이언트 고유 세션 ID를 생성하고 서버에 저장</strong>한다.</li>
<li>서버는 클라이언트에게 <strong>세션 ID를 전달하고 쿠키에 저장</strong>한다.</li>
<li>클라이언트는 세션 ID를 쿠키에서 가져오면서 서버와 통신한다.</li>
<li>서버는 세션 ID가 유효한지 확인한다.
 
세션 방식에서는 세션 ID의 반쪽은 사용자 브라우저에 쿠키로 저장되고, 나머지 반쪽은 서버에 저장된다고 생각하면 된다. </li>
</ol>
<p>이처럼 이 세션 ID를 사용해서 어떤 사용자가 서버에 로그인 되었음이 지속되는 이 상태를 &#39;세션&#39;이라고 한다.</p>
<h4 id="정리">정리</h4>
<ul>
<li>쿠키 = 브라우저 저장소 (데이터 자체가 클라이언트에 있음)</li>
<li>세션 = 서버 저장소 (브라우저에는 &quot;세션ID&quot;만 저장, 본 데이터는 서버에 있음)</li>
</ul>
<h1 id="express-환경에서-session--cookie-적용해보기">Express 환경에서 session + cookie 적용해보기</h1>
<h4 id="🔧-서버-개발-환경">🔧 서버 개발 환경</h4>
<pre><code>Node.js: v22 (LTS)
Express: v5.1.0
템플릿 엔진 EJS: v3.1.10
low DB: NeDB </code></pre><h3 id="🤔-고민과-선택-과정">🤔 고민과 선택 과정</h3>
<p>세션+쿠키 방식의 로그인을 적용하기 전에, <strong><code>express-session</code> + <code>nedb-session-store</code> 모듈을 사용</strong>할지 <strong>직접 세션을 커스텀</strong>할지 고민하였다. 왜냐하면 세션ID를 만들어서 세션DB까지 구현하는 요구사항이라면, 굳이 모듈에 대한 의존성 없이도 커스텀 방식으로 구현은 가능하기 때문이다. 
하지만 아래와 같은 이유로 <strong>각각의 특징과 트레이드오프를 생각해서 모듈 방식으로 선택</strong>했다.</p>
<h4 id="커스텀-세션-방식-nedbsid">커스텀 세션 방식 (NeDB+sid)</h4>
<ul>
<li>현재 NeDB를 사용중이라 동일한 스토리지로 <strong>간단하게 확장</strong>이 가능하다. 따로 세션 저장소 구현체를 설치하지 않아도 된다.</li>
<li>커스텀 로직(세션 스키마, 만료 정책 등)을 <strong>세밀하게 제어</strong>하기에 용이하다.</li>
<li>의존성 최소화하기 위해 굳이 <strong>모듈이 없어도 요구사항을 충족</strong>할 수 있다.</li>
<li><strong>쿠키와 세션 옵션을 따로 설정</strong>해줘야 한다.<pre><code class="language-js">// 세션 생성 및 쿠키 설정
const sessionId = randomUUID()
const now = Date.now()
const maxAgeMs = 1000 * 60 * 60 * 24 * 7 // 7일
await createSession({
sessionId,
username: user.username,
nickname: user.nickname,
createdAt: new Date(now).toISOString(),
expiresAt: new Date(now + maxAgeMs),
})
res.cookie(&#39;sid&#39;, sessionId, {
httpOnly: true,
sameSite: &#39;lax&#39;,
maxAge: maxAgeMs,
})</code></pre>
</li>
</ul>
<h4 id="모듈-방식-express-session-nedb-session-store">모듈 방식 (express-session, nedb-session-store)</h4>
<ul>
<li>express-session를 사용하면 세션 ID를 저장하기 위한 호환되는 <strong>세션 스토어</strong> 사용을 권장한다.</li>
<li>이후 Redis나 MongoDB와 같은 <strong>스토어와 교체 용이성</strong>이 좋다.</li>
<li>세션 스토어를 사용하면 Passport, Flash, CSRF 등 express-session에 기대는 미들웨어를 함께 쓸 때 유용하다.</li>
<li><strong>쿠키와 세션 옵션이 일원화</strong>되어 있어 편리하게 로직을 관리할 수 있다.<pre><code class="language-js">app.use(session({
  secret: &#39;your-secret&#39;,      
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    sameSite: &#39;lax&#39;,
  }
}))</code></pre>
</li>
</ul>
<p><strong>요구사항 충족</strong>과 더불어 <strong>확장성</strong>을 고려해서 모듈 방식을 선택했다. lowDB로 NeDB를 선택했던 것도 mongoDB와 유사성 및 교체 용이성을 바라보고 결정했기 때문에 이번에도 스토어와의 <strong>교체 용이성</strong>을 위해 <code>nedb-session-store</code>를 사용했다. 그리고 모듈의 <strong>쿠키+세션 옵션</strong>이 간편한 것도 좋았다.</p>
<h3 id="구현-과정">구현 과정</h3>
<ol>
<li><strong>session과 cookie-parser, nedb-session-store 모듈 설치</strong>
<code>npm i express-session cookie-parser nedb-session-store</code></li>
</ol>
<ul>
<li><code>express-session</code>: <strong>세션 관리용 미들웨어</strong>로 세션 관리 시 클라이언트에 세션 쿠키를 보낸다.</li>
<li><code>cookie-parser</code>: 요청과 함께 들어온 쿠키를 해석하여 곧바로 req.cookies객체로 만든다.</li>
<li><code>nedb-session-store</code>: 세션 데이터를 NeDB 파일(DB)로 저장하게 해주는 어댑터로 express-session의 세션 저장소(store) 구현체 중 하나이다.</li>
</ul>
<ol start="2">
<li><strong>app.js 설정</strong></li>
</ol>
<ul>
<li><strong>세션은 사용자별로 req.session 객체 안에 유지</strong>된다.</li>
<li>안전하게 쿠키를 전송하려면 <strong>쿠키에 서명을 추가</strong>해야하고, <strong>쿠키를 서명할 때 secret</strong> 값이 필요하다.<ul>
<li><code>cookie-parser</code>의 secret과 같게 설정하는 것이 좋다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">const cookieParser = require(&#39;cookie-parser&#39;)
const session = require(&#39;express-session&#39;)
const NedbStore = require(&#39;nedb-session-store&#39;)(session)

// cookieParser에 비밀키를 넣어 요청온 쿠키값이 내가 서명한 쿠키인지 파악한다.
// 암호 키를 작성하는 것에는 크게 규격이 없으며 개발자의 자유이다. 단 쉽게 유추할 수 있는 값은 사용하지 말자.
app.use(cookieParser(process.env.COOKIE_SECRET);

app.use(session({
  secret: process.env.COOKIE_SECRET, // 암호화하는 데 쓰일 키
  resave: false, // 세션을 언제나 저장할지 설정함
  saveUninitialized: true, // 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정
  store: new NedbStore({ filename: &#39;./data/sessions.db&#39; }), // 세션 객체에 세션스토어를 적용 
  cookie: {    //세션 쿠키 설정 (세션 관리 시 클라이언트에 보내는 쿠키)
    maxAge: 1000 * 60 * 60 * 24 * 7,
    httpOnly: true, // 자바스크립트를 통해 세션 쿠키를 사용할 수 없도록 함
    sameSite: &#39;lax&#39;, // CSRF 위험을 줄임, 완전 차단은 아니고 “안전한” 탐색에서만 허용
    // Secure: true // https 환경에서만 session 정보를 주고받도록처리
  },
  // name: &#39;session-cookie&#39; // 세션 쿠키명 디폴트값은 connect.sid이지만 다른 이름을 줄수도 있다.
}));

app.get(&#39;/&#39;, (req, res, next) =&gt; {
 // 세션에 데이터를 설정하면, 모든 세션이 설정되는게아니라, 요청 받은 고유의 세션 사용자의 값만 설정 된다.
 // 즉, 개인의 저장 공간이 생긴 것과 같다.
 req.session.id = &quot;hello&quot;; 
}
</code></pre>
<h4 id="✍️-인증서명된-쿠키">✍️ 인증(서명)된 쿠키</h4>
<p><code>cookieParser(process.env.COOKIE_SECRET)</code>
cookieParser의 첫번째 인수로 <span style="color: red">비밀 키</span>를 넣어줄 수 있다.
서명된 쿠키가 있는 경우, 제공한 비밀 키를 통해 해당 쿠키가 내가 만든 쿠키임을 검증할 수 있다.
쿠키는 클라이언트에서 위조하기 쉬우므로, 비밀 키를 통해 만들어낸 서명을 쿠키 값 뒤에 붙이는 것이다!
이렇게 작성하면 서명된 쿠키를 생성하고 활용할 때 서버와 클라이언트 PC만 알아볼 수 있도록 통신하게 된다.</p>
<h4 id="💾-세션-스토어">💾 세션 스토어</h4>
<p>세션 스토어는 <strong>세션이 데이터를 저장하는 곳</strong>을 말한다.
대표적으로 <code>Memory Store, File Store, Mongo Store</code> 가 있다.
default 값은 Memory Store인데, 메모리는 서버나 클라이언트를 껐다 키면 사라지는 <strong>휘발성</strong>이다. 
그래서 세션을 저장할 <strong>고유 저장소</strong>를 따로 지정해야 한다.</p>
<ul>
<li>어떤 DB를 사용하는지 따라서 store를 이 <a href="https://www.npmjs.com/package/express-session#compatible-session-stores">API 문서</a>를 보고 선택하면 된다.</li>
</ul>
<p>필자는 인메모리 방식인 <code>nedb-session-store</code>를 설치해서 lowDB로 저장 관리했다.</p>
<p>이렇게 하고 나서 처음 서버를 올리면 sessions.db 가 생긴다.
사용자가 로그인에 성공해서 서버에 접속할 때 이 sessions.db에 유저 정보의 세션 ID가 생긴다.</p>
<pre><code class="language-json">{
   &quot;_id&quot;:&quot;RIERELq-eIFQ-Uw9ubtI1mL749qAhu_K&quot;,
   &quot;session&quot;:{
      &quot;cookie&quot;:{
         &quot;originalMaxAge&quot;:604800000,
         &quot;expires&quot;:{
            &quot;$$date&quot;:1756807508766
         },
         &quot;httpOnly&quot;:true,
         &quot;path&quot;:&quot;/&quot;,
         &quot;sameSite&quot;:&quot;lax&quot;
      },
      &quot;user&quot;:{
         &quot;username&quot;:&quot;ekdus&quot;,
         &quot;nickname&quot;:&quot;dayeonkim&quot;
      }
   },
   &quot;expiresAt&quot;:{
      &quot;$$date&quot;:1756807578016
   },
   &quot;createdAt&quot;:{
      &quot;$$date&quot;:1756202708767
   },
   &quot;updatedAt&quot;:{
      &quot;$$date&quot;:1756202778017
   }
}</code></pre>
<p><code>_id</code> 가 세션 ID로 생성되었다.</p>
<p><strong>브라우저에서 세션 쿠키를 확인</strong>해보면 </p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/3be56fff-bc31-458b-9cee-1bd2674e2c38/image.png" alt=""> connect.sid:
s:<strong>RIERELq-eIFQ-Uw9ubtI1mL749qAhu_K</strong>.C5F3R%2FIMTdZkfQVa%2FiBld1Z6zn0CwyQ3ifjwjDVejQo</p>
</blockquote>
<p>connect.sid로 같은 <code>RIERELq-eIFQ-Uw9ubtI1mL749qAhu_K</code> 값으로 생성된걸 확인할 수 있고, <code>.</code>뒤에 <code>C5F3R%2FIMTdZkfQVa%2FiBld1Z6zn0CwyQ3ifjwjDVejQo</code> 값은 COOKIE_SECRET으로 서명(signature)된 값이다. 이는 암호화(encryption)와는 다른 값이다. <code>s</code>는 서명(signed) 쿠키라는 표시이다.</p>
<ol start="3">
<li><strong>loginController 설정</strong><pre><code class="language-js">// POST /login
const postLogin = async (req, res) =&gt; {
 try {
     const { username, password } = req.body
     const user = await findUserByUsername(username)
     // 세션에 사용자 정보 저장
     req.session.user = { username: user.username, nickname: user.nickname }
     return res.status(200).json({ success: true, message: &#39;로그인 성공&#39; })
 } catch (err) {
     console.error(&#39;postLogin error:&#39;, err)
     return res.status(500).json({ success: false, message: &#39;서버 오류&#39; })
 }
}</code></pre>
<code>req.session.user</code>에 사용자 정보(이름, 닉네임)를 저장해서 세션 스토어에 값이 같이 저장되도록 했다. 이렇게 하면 로그인 이후 요청들에서 미들웨어가 <code>connect.sid</code>로 세션을 찾아 <code>req.session.user</code>를 다시 채워준다. 그래서 매번 DB 조회할 필요없이 어디서든 <code>req.session.user</code>로 로그인 상태/사용자 식별이 가능하다.</li>
</ol>
<h3 id="세션의-한계와-보안-이슈">세션의 한계와 보안 이슈</h3>
<p>세션은 쿠키보다 안전해 보이지만, 단점도 존재한다.</p>
<ol>
<li><p><strong>서버 자원 부담</strong><br>세션은 서버 메모리/DB에 저장되므로, 동시 접속자가 많을수록 서버에 부하가 생길 수 있다.<br>(해결책: Redis 같은 외부 세션 저장소를 사용)</p>
</li>
<li><p><strong>확장성 문제</strong><br>서버를 여러 대 운영할 때 세션을 공유하지 않으면 사용자마다 로그인 상태가 달라지는 문제가 생긴다.<br>(해결책: 공용 세션 저장소 사용)</p>
</li>
<li><p><strong>세션 하이재킹 위험</strong><br>세션 ID가 탈취되면, 아이디/비밀번호를 몰라도 그대로 로그인 상태가 된다.<br>(예: XSS, 네트워크 스니핑, 세션 고정 공격 등)</p>
</li>
</ol>
<h4 id="방어-방법">방어 방법</h4>
<ol>
<li>쿠키 보안 속성</li>
</ol>
<ul>
<li>HttpOnly: true → JS에서 접근 차단(XSS 방어)</li>
<li>Secure: true → HTTPS에서만 전송</li>
<li>SameSite: strict/lax → CSRF 공격 방어</li>
</ul>
<ol start="2">
<li>세션 관리</li>
</ol>
<ul>
<li>로그인 성공 시 세션 ID를 새로 발급(Session Regeneration)</li>
<li>세션에 IP, User-Agent 등을 기록해서 매 요청마다 검증</li>
<li>일정 시간 활동 없으면 세션 만료 (Idle Timeout)</li>
</ul>
<ol start="3">
<li>전송 보안</li>
</ol>
<ul>
<li>HTTPS 필수 → 네트워크에서 세션 ID 탈취 방지
확장성</li>
<li>Redis 같은 외부 세션 스토어 사용 → 서버 여러 대 운영 시에도 안정적</li>
</ul>
<p>이런 방법들을 조합해야 안전한 세션 관리가 가능하다.</p>
<h4 id="참고자료">참고자료</h4>
<p>cookie-parser, sign</p>
<ul>
<li><a href="https://expressjs.com/en/resources/middleware/cookie-parser.html">https://expressjs.com/en/resources/middleware/cookie-parser.html</a></li>
<li><a href="https://expressjs.com/en/api.html#res.cookie">https://expressjs.com/en/api.html#res.cookie</a></li>
<li><a href="https://inpa.tistory.com/entry/EXPRESS-%F0%9F%93%9A-bodyParser-cookieParser-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4#express_-_cookie-parser">https://inpa.tistory.com/entry/EXPRESS-%F0%9F%93%9A-bodyParser-cookieParser-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4#express_-_cookie-parser</a></li>
</ul>
<p>session 미들웨어</p>
<ul>
<li><a href="https://inpa.tistory.com/entry/EXPRESS-%F0%9F%93%9A-express-session-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4">https://inpa.tistory.com/entry/EXPRESS-%F0%9F%93%9A-express-session-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[렌더링 방식 어떤걸 선택하는게 좋을까? CSR(SPA), SSR(MPA)]]></title>
            <link>https://velog.io/@dyeon-dev/%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%B0%A9%EC%8B%9D-%EC%96%B4%EB%96%A4%EA%B1%B8-%EC%84%A0%ED%83%9D%ED%95%98%EB%8A%94%EA%B2%8C-%EC%A2%8B%EC%9D%84%EA%B9%8C-CSRSPA-SSRMPA</link>
            <guid>https://velog.io/@dyeon-dev/%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%B0%A9%EC%8B%9D-%EC%96%B4%EB%96%A4%EA%B1%B8-%EC%84%A0%ED%83%9D%ED%95%98%EB%8A%94%EA%B2%8C-%EC%A2%8B%EC%9D%84%EA%B9%8C-CSRSPA-SSRMPA</guid>
            <pubDate>Mon, 25 Aug 2025 13:31:18 GMT</pubDate>
            <description><![CDATA[<h1 id="초창기-웹-애플리케이션-mpa-ssr">초창기 웹 애플리케이션 (MPA, SSR)</h1>
<h3 id="📌-mpa-multi-page-application">📌 MPA (Multi-Page Application)</h3>
<ul>
<li>다중 페이지로 이뤄져있어 변경사항이 있을 때마다 서버로 페이지를 요청해서 새로 렌더링 한다. </li>
</ul>
<p>웹 애플리케이션의 역사를 보자면, 초창기 웹은 텍스트 중심의 단순 문서였다. 따라서 <strong>MPA(Multi-Page Application)</strong> 방식으로 구현되었다. MPA는 <strong>페이지 변경시마다 서버에 페이지 요청</strong>을 하여 새로고침이 발생하고 깜빡임이 있었다. 웹의 발전과 함께 복잡도가 증가하면서 MPA 방식의 성능 이슈가 발생했다.</p>
<p>이후 2004년 웹 시장에서는 굉장히 획기적인 일이 발생한다. 바로 제시 제임스(Jesse James)라는 사람이 Ajax 기술 사양을 발표하여 JavaScript를 통해 서버로부터 비동기적으로 데이터를 처리하는 획기적인 기술을 제시했다. <strong>Ajax 기술의 등장으로 필요한 부분만 리로드할 수 있게 되어 성능 문제가 해결</strong>되었다.</p>
<h3 id="📌-전통적인-page기반-ssr-server-side-rendering">📌 전통적인 Page기반 SSR (Server-Side Rendering)</h3>
<p>이런식으로 <strong>서버에서 html로 페이지를 다 만들어서 클라이언트에게 주는 것</strong>을 SSR이라고 한다.</p>
<h4 id="ssr-동작-흐름">SSR 동작 흐름</h4>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/0bd8e7d7-28fc-42d9-a26d-06ee71273b86/image.png" alt=""></p>
<ol>
<li>사용자가 웹 접속 후 브라우저가 서버에 리소스를 요청한다.</li>
<li>서버는 즉시 렌더링 가능한 <strong>초기 콘텐츠가 로딩된 html 파일</strong>을 만든다.
(리소스 체크, 컴파일 후 완성된 HTML 컨텐츠로 만든다.)</li>
<li><strong>초기 렌더링</strong>: 브라우저에 전달되는 순간, 이미 렌더링 준비가 되어있기 때문에 <strong>HTML은 즉시 렌더링</strong> 된다. <strong>그러나 사이트 자체는 조작 불가능하다. (Javascript가 읽히기 전이다.)</strong></li>
<li><strong>JS 처리</strong>: 브라우저가 <strong>자바스크립트를 다운</strong>받는다. 다운 받아지고 있는 사이에 유저는 콘텐츠는 볼 수 있지만 사이트를 조작할 수는 없다. <strong>이때의 사용자 조작을 기억하고 있는다.</strong></li>
<li>JS가 서버에서 전달된 HTML에 <strong>이벤트리스너 등을 할당해서 인터랙티브한 페이지를 제공한다.</strong>
JS까지 성공적으로 컴파일 되었기 때문에 기억하고 있던 사용자 조작이 실행되고 이제 웹 페이지는 상호작용 가능해진다.</li>
</ol>
<h4 id="ssr-특징---웹-성능-지표-관점">SSR 특징 - 웹 성능 지표 관점</h4>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/cc04c9b3-20a5-4a36-9e5c-2635b304e3fc/image.png" alt=""></p>
<ul>
<li>👍 <strong>FCP</strong>가 빠르다.<ul>
<li>FCP(First Contentful Paint): 사용자가 화면에서 콘텐츠를 볼 수 있는 페이지 로드 타임라인의 첫 번째 지점</li>
</ul>
</li>
<li>👎 <strong>TBT</strong>가 발생한다.<ul>
<li>Total Blocking Time(TBT) : FCP로부터 TTI(사용자가 페이지에서 상호작용이 가능한 시점)까지의 시간</li>
</ul>
</li>
</ul>
<h1 id="더욱-늘어난-니즈-spa-csr">더욱 늘어난 니즈 (SPA, CSR)</h1>
<h3 id="📌-spa-single-page-application">📌 SPA (Single-Page Application)</h3>
<p>하지만 갈수록 더욱 복잡해지는 애플리케이션은 니즈가 늘어났다. <strong>하나의 페이지 안에서 데이터를 받아 와서 필요한 부분만 부분적으로 업데이트</strong>하도록 하는 <strong>SPA(Single-Page Application)</strong>가 등장했다.</p>
<h3 id="📌-csr-client-side-rendering">📌 CSR (Client-Side Rendering)</h3>
<p>CSR은 <strong>모든 렌더링 작업을 브라우저에서 수행하는 방식</strong>이다. 서버로부터 <strong>최소한의 HTML과 JS 파일</strong>을 받아 브라우저가 이를 기반으로 UI를 동적으로 생성한다. 
CSR은 빠르게 SPA의 표준으로 자리잡았으며 페이지 전환 시 전체 페이지를 다시 로드하지 않고 <strong>필요한 부분만 업데이트함으로써 사용자 경험을 크게 향상</strong>했다.</p>
<h4 id="csr-동작-흐름">CSR 동작 흐름</h4>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/d5681d70-1bce-4a29-a4e4-ac1c3c0ad8fd/image.png" alt=""></p>
<ol>
<li>사용자가 웹 접속 후 브라우저가 서버에 리소스 요청한다.</li>
<li>서버는 <strong>간단한 HTML 파일과 JS 파일을 브라우저로 전송</strong>한다. 주로 <code>&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;</code>와 같은 최소한의 구조만 포함된다. (TTFB)</li>
<li><strong>초기 렌더링</strong>: 브라우저가 <strong>HTML과 CSS를 다운로드하고 렌더링해서 초기 페이지 구조를 만든다.</strong> (<strong>이때 SSR과 달리 사용자는 빈 화면을 보게된다.</strong>)</li>
<li><strong>JS 처리</strong>: 브라우저는 HTML에 포함된 script 태그를 통해 JS를 다운로드하고 실행한다. </li>
<li>JS가 서버에서 전달된 HTML에 기능을 추가해서 페이지가 인터렉티브해진다.</li>
<li><strong>fetching data</strong>: 데이터를 위한 <strong>API가 호출</strong>되고, 서버가 API로부터의 요청에 응답한다. (이때 사용자는 placeholder를 보게 된다.)</li>
<li>API로부터 받아온 data를 placeholder 자리에 넣어준다. </li>
<li><strong>UI 완성</strong>: 이 데이터를 기반으로 UI가 완성되고, 이때 LCP(Largest Contentful Paint)가 측정된다.</li>
</ol>
<h4 id="csr-특징---웹-성능-지표-관점">CSR 특징 - 웹 성능 지표 관점</h4>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/e6f99545-62bb-4b55-95d0-7674a54b9da2/image.png" alt=""></p>
<ul>
<li>👍 <strong>TTFB</strong>가 빠르다.<ul>
<li>TTFB(Time to First Byte): 요청을 보내고 응답의 첫번째 바이트가 도착하기까지의 시간</li>
<li>서버가 복잡한 HTML을 렌더링하지 않고, 빈 HTML+JS 번들 링크만 보내주면 되기 때문에 작업이 단순하여 TTFB가 빠른 경향이 있다.</li>
</ul>
</li>
<li>👎 <strong>FCP / LCP</strong>가 안좋다.<ul>
<li>FCP(First Contentful Paint): 브라우저가 요청을 보낸 시점부터 DOM의 첫 번째 콘텐츠가 화면에 렌더링되는 시점</li>
<li>LCP(Largest Contentful Paint): 브라우저가 요청을 보낸 시점부터 가장 큰 콘텐츠 요소가 화면에 렌더링되는 시점</li>
<li>빈 HTML+JS 파일 링크를 줘도, 사용자가 실제로 콘텐츠를 보려면 JS 실행 후에야 보이기 때문에 늦어진다.</li>
<li>fetch로 json 형식으로 받아서 렌더링하는 서버랑 통신하는 과정까지 마쳐야하기 때문에 첫페이지가 느리다.</li>
</ul>
</li>
</ul>
<h4 id="csr의-문제점">CSR의 문제점</h4>
<p><strong>1. 초기 로딩 속도</strong>
CSR에서는 HTML, CSS 렌더링 후 JS 파일을 다운로드, 파싱, 실행해야지만 인터랙티브한 페이지를 볼 수 있다.
이 모든 과정이 순차적으로 이루어져야하기 때문에 FCP, LCP를 포함한 초기 로딩 성능을 저하한다. 
이는 사용자가 사용하는 디바이스의 성능이 안좋거나 네트워크 연결 속도가 느릴수록 로딩 시간이 길어지기 때문에 저사양 디바이스에서 사용자 경험이 크게 떨어질 수 있다.  </p>
<p><strong>2. 사용자 경험</strong>
서버에서 최소한의 HTML만 제공되기 때문에 JS 실행 전까지는 빈 화면 또는 로딩 스피너만 보여주게 된다. 또한 대량의 데이터를 가져오는 애플리케이션에서는 data fetching 및 렌더링 과정이 길어져 사용자가 주요 콘텐츠를 보는 시간이 길다. </p>
<p><strong>3. JS bundle size</strong>
JS는 클라이언트에서 다운받는다. 기능이 많아질수록 JS bundle size도 늘어나 브라우저가 이를 처리하는데 많은 시간이 소요된다. </p>
<p><strong>4. SEO</strong>
검색 엔진 크롤러는 주로 서버에서 렌더링된 HTML 콘텐츠를 인덱싱한다. 하지만 CSR에서는 하나의 div 태그가 포함된 HTML만 제공하기 때문에 검색 순위가 낮아질 수 있다. </p>
<p><strong>5. 성능 최적화</strong>
초기 렌더링 시 HTML 구조가 거의 없기 때문에 브라우저에서 효율적인 HTML 캐싱이 어렵다. 또한 data fetching, 상태관리, 컴포넌트 렌더링 등의 모든 작업을 클라이언트에서 처리하기 때문에 브라우저의 부하가 증가한다. </p>
<h3 id="📌-csr의-문제점-해결을-위한-현대적인-ssr-방식">📌 CSR의 문제점 해결을 위한 현대적인 SSR 방식</h3>
<p>CSR은 위와 같은 한계가 있기 때문에, 이를 보완하기 위해 현대적인 SSR 방식이 발전했다.
전통적인 SSR은 서버에서 완성된 HTML을 내려주기 때문에 초기 렌더링은 빠르지만, 매 요청마다 서버 부하가 커지고 동적인 상호작용 구현이 제한적이라는 단점이 있었다.</p>
<p>이를 해결하기 위해 <strong>Hydration 기반 SSR</strong>이 등장했다. 서버는 미리 렌더링된 <strong>HTML을 클라이언트</strong>로 보내 즉시 콘텐츠를 보여주고(FCP 개선), <strong>이후 브라우저에서 자바스크립트가 실행</strong>되면서 해당 HTML과 매칭되어 이벤트 바인딩과 상호작용이 가능해진다. 
이렇게 하면 <strong>SSR의 빠른 초기 로딩</strong>과 <strong>CSR의 동적 인터랙션</strong>을 모두 누릴 수 있다. Next.js, Nuxt.js 같은 프레임워크들이 대표적이다.
또한, SSR의 성능 문제를 개선하기 위해 *<em>SSG(Static Site Generation), ISR(Incremental Static Regeneration) *</em>같은 방식도 도입되었다.</p>
<ul>
<li><strong>SSG</strong>는 빌드 시점에 HTML을 생성해 두어 요청 시 즉시 반환할 수 있어 TTFB와 FCP가 모두 빠르다. 다만, 데이터가 자주 바뀌는 페이지에는 적합하지 않다.</li>
<li><strong>ISR</strong>은 특정 주기로 정적 페이지를 다시 생성하는 방식으로, 최신성을 유지하면서도 정적 사이트의 속도를 유지할 수 있다.</li>
</ul>
<p>현대 웹은 이러한 전략들을 혼합해 사용한다. 예를 들어, 블로그 글 목록은 SSG로, 실시간 데이터가 필요한 대시보드는 SSR로, 로그인이나 마이페이지 같은 일부 영역은 CSR로 구현하는 Hybrid Rendering 방식이 일반적이다.</p>
<p>💡 따라서 오늘날의 SSR은 단순히 “서버에서 렌더링한다”에 그치지 않고, <strong>CSR의 장점을 흡수해 초기 로딩 성능과 SEO, 상호작용성을 모두 고려하는 진화된 렌더링 방식</strong>으로 이해할 수 있다.</p>
<h3 id="ssr-vs-csr">SSR vs CSR</h3>
<table>
<thead>
<tr>
<th></th>
<th>SSR</th>
<th>CSR</th>
</tr>
</thead>
<tbody><tr>
<td>장점</td>
<td>초기 로딩 속도가 빠름, SEO가 쉬움</td>
<td>화면 깜빡임이 없어 좋은 사용자 경험 제공, 초기 로딩 이후 구동 속도 빠름</td>
</tr>
<tr>
<td>단점</td>
<td>페이지 이동 시 화면 깜빡임으로 사용자 경험 저하, 매번 요청을 보내기 때문에 서버 과부하 발생 가능</td>
<td>초기 로딩 속도가 느림, SEO가 어려움</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node.js에서 EJS 이미지 파일 업로드 및 삭제 구현]]></title>
            <link>https://velog.io/@dyeon-dev/Node.js%EC%97%90%EC%84%9C-EJS-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B0%8F-%EC%82%AD%EC%A0%9C-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@dyeon-dev/Node.js%EC%97%90%EC%84%9C-EJS-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B0%8F-%EC%82%AD%EC%A0%9C-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Wed, 20 Aug 2025 11:40:45 GMT</pubDate>
            <description><![CDATA[<h3 id="클라이언트에서-이미지-업로드하기">클라이언트에서 이미지 업로드하기</h3>
<p>html에서 이미지를 업로드하기 위해서는 input 태그의 type=&quot;file&quot;을 사용하면 된다.
그리고 addEventListener로 이벤트 변경이 일어나는걸 감지해서 img 태그의 profileImage.src에 해당 파일을 삽입한다.</p>
<pre><code class="language-js">&lt;form id=&quot;mypageForm&quot; class=&quot;mypage-form&quot;&gt;
  &lt;div class=&quot;form-image&quot;&gt;
    &lt;img src=&quot;/images/profile.jpg&quot; alt=&quot;프로필 이미지&quot; id=&quot;profileImage&quot; name=&quot;profileImage&quot;&gt;
    &lt;input type=&quot;file&quot; id=&quot;input-file&quot; accept=&quot;image/*&quot;&gt;
    &lt;label for=&quot;input-file&quot;&gt;수정&lt;/label&gt;
  &lt;/div&gt;
&lt;/form&gt;

&lt;script&gt;
  let profileImage = document.getElementById(&#39;profileImage&#39;);
  let inputFile = document.getElementById(&#39;input-file&#39;);

  inputFile.addEventListener(&#39;change&#39;, function(e) {
      const file = e.target.files[0];
      if (file) {
          profileImage.src = URL.createObjectURL(file);
      }
  })
&lt;/script&gt;</code></pre>
<p>그럼 이미지 파일을 선택하고 업로드가 된다. 그런데 이 파일을 서버에 POST 요청을 하면 문제가 생기고 DB에 저장할 수도 없다! 파일을 무작정 body에 담아서 POST 요청을 보내면 서버는 파일을 가져오지 못한다.</p>
<h3 id="해결방법-multer-미들웨어-사용">해결방법: multer 미들웨어 사용</h3>
<p>일반적으로 클라이언트에서 서버로 폼 데이터(form data)가 전송될 때 해당 데이터는 인코딩되어 전송된다. 이때 파일을 전송하려면 form 태그의 enctype 속성을 &quot;multipart/form-data&quot;로 바꿔줘야 한다. enctype 속성은 입력된 데이터가 서버에 전송될 때 인코딩되는 방식을 결정한다.
디폴트는 application/x-www-form-urlencoded인데, 이 경우 모든 문자를 인코딩하여 전송하게 되며, 위에서 언급한 multipart/form-data는 모든 문자를 인코딩하지 않고 전송한다. 
<strong>mutler가 바로 이 multipart/form-data를 다루기 위한 node.js의 미들웨어다.</strong></p>
<ol>
<li><p>뷰 폼을 multipart/form-data로 전송하도록 수정</p>
<pre><code class="language-js">&lt;form id=&quot;mypageForm&quot; class=&quot;login-form&quot; enctype=&quot;multipart/form-data&quot;&gt;</code></pre>
</li>
<li><p>multer 모듈 설치
`npm i multer&#39;</p>
</li>
<li><p>라우터에서 multer 미들웨어를 추가해 파일을 public/uploads에 저장</p>
</li>
</ol>
<ul>
<li>가장 기본적인 형태의 multer 사용이다. destination으로 경로를 지정해주었고, filename으로 파일명을 지정해주었다.</li>
<li>파일명 생성 로직을 UUID 기반의 안전한 규칙으로 구현했다.</li>
<li>이때 mime type 설정을 해주면 원하는 타입만 필터링이 가능하다. image 파일만 업로드가 가능하도록 필터를 추가하여 다른 파일은 거부되도록 했다.</li>
<li>파일사이즈는 최대 5MB로 제한되게 하여 비용/저장 공간을 관리했다.</li>
<li>이로인해 전송받은 파일은 node.js 내부의 uploads 디렉터리에 저장될 것이다.<pre><code class="language-js">const express = require(&#39;express&#39;)
const router = express.Router()
const multer = require(&#39;multer&#39;)
const path = require(&#39;path&#39;)
const fs = require(&#39;fs&#39;)
</code></pre>
</li>
</ul>
<p>// 업로드 스토리지 설정 (public/uploads)
const storage = multer.diskStorage({
    destination: (req, file, cb) =&gt; {
        const uploadPath = path.join(__dirname, &#39;..&#39;, &#39;public&#39;, &#39;uploads&#39;)
        fs.mkdir(uploadPath, { recursive: true }, (err) =&gt; cb(err, uploadPath))
    },
    filename: (req, file, cb) =&gt; {
        const ext = path.extname(file.originalname).toLowerCase()
        const unique = (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString(&#39;hex&#39;))
        cb(null, <code>${unique}${ext}</code>)
    },
})</p>
<p>// 이미지 파일만 허용
const fileFilter = (req, file, cb) =&gt; {
    if (file.mimetype &amp;&amp; file.mimetype.startsWith(&#39;image/&#39;)) return cb(null, true)
    cb(new Error(&#39;이미지 파일만 업로드할 수 있습니다&#39;))
}
const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 } })</p>
<pre><code>
4. 컨트롤러에서 req.file을 읽어 profileImage 경로(/uploads/파일명)를 DB에 저장
```js
// POST /mypage
const postMypage = async (req, res) =&gt; {
    try {
        const { nickname, password } = req.body
        const username = res.locals.user.username

        const fieldsToUpdate = {}
        // 파일 업로드가 있는 경우 public 상대 경로로 저장
        if (req.file) {
            const relativePath = path.posix.join(&#39;/uploads&#39;, req.file.filename)
            fieldsToUpdate.profileImage = relativePath
        }
    }
}</code></pre><h2 id="이미지-삭제하기">이미지 삭제하기</h2>
<ul>
<li>삭제 버튼을 만들고 클릭하면 profileImage.src에 기본 이미지를 할당하고 <code>inputFile.value = &#39;&#39;</code>처리를 해준다.</li>
<li>이렇게까지만 하면 서버로 삭제의사가 전달되지 않아 DB에 profileImage가 그대로 남아있다.</li>
</ul>
<h3 id="해결방법-삭제-플래그-설정">해결방법: 삭제 플래그 설정</h3>
<p>이미지를 제거하면 그 결과를 서버에게 알려줘야한다. <strong>그런데 직접적으로 알릴 방법이 없다.</strong> 그래서 <strong>삭제 버튼을 누르면 변경되는 값(0,1)을 통해 서버가 감지</strong>해서 기존 파일을 삭제하고 DB에서 삭제가 이뤄지도록 구현했다.</p>
<ul>
<li>뷰에 <strong>숨은 필드 <code>removeProfileImage</code>를 추가</strong>해, 삭제 버튼 클릭 시 서버로 값이 <strong>1로 전송</strong>되도록 한다. </li>
<li>컨트롤러에서 <code>removeProfileImage === &#39;1&#39;</code>이면 <strong>기존 파일을 삭제하고(<code>fs.unlink</code>)</strong>, <strong>DB의 profileImage를 <code>null</code>로 업데이트</strong>한다.</li>
<li><strong>새 파일이 업로드되면 그 경로가 우선 적용</strong>된다.</li>
<li>이렇게 하면 프로필 삭제 후 저장 → 다시 들어가면 기본 이미지로 보인다.</li>
</ul>
<pre><code class="language-js">&lt;div class=&quot;form-image-buttons&quot;&gt;
  &lt;label for=&quot;input-file&quot;&gt;수정&lt;/label&gt;
  &lt;label id=&quot;delete-image&quot;&gt;삭제&lt;/label&gt;
&lt;/div&gt;

&lt;input type=&quot;hidden&quot; id=&quot;removeProfileImage&quot; name=&quot;removeProfileImage&quot; value=&quot;0&quot;&gt;


&lt;script&gt;
  let profileImage = document.getElementById(&#39;profileImage&#39;);
  let inputFile = document.getElementById(&#39;input-file&#39;);

  inputFile.addEventListener(&#39;change&#39;, function(e) {
      const file = e.target.files[0];
      if (file) {
          profileImage.src = URL.createObjectURL(file);
          // 파일을 새로 선택하면 removeProfileImage를 0으로 되돌린다.
          document.getElementById(&#39;removeProfileImage&#39;).value = &#39;0&#39;
      }
  })

  let deleteImage = document.getElementById(&#39;delete-image&#39;);
  deleteImage.addEventListener(&#39;click&#39;, function(e) {
    profileImage.src = &#39;/images/profile.jpg&#39;;
    inputFile.value = &#39;&#39;
    document.getElementById(&#39;removeProfileImage&#39;).value = &#39;1&#39;
  })
&lt;/script&gt;</code></pre>
<pre><code class="language-js">// POST /mypage
const postMypage = async (req, res) =&gt; {
    try {
        const { nickname, password, removeProfileImage } = req.body
        const username = res.locals.user.username

        const fieldsToUpdate = {}
        // 삭제 플래그가 있으면 이미지 제거
        if (removeProfileImage === &#39;1&#39;) {
            // 기존 파일 삭제 시도
            const current = await findUserByUsername(username)
            if (current &amp;&amp; current.profileImage) {
                const absolute = path.join(__dirname, &#39;..&#39;, &#39;public&#39;, current.profileImage)
                fs.unlink(absolute, () =&gt; {})
            }
            fieldsToUpdate.profileImage = null
        }
        // 새 파일 업로드가 있는 경우 설정 (삭제 플래그보다 우선)
        if (req.file) {
            const relativePath = path.posix.join(&#39;/uploads&#39;, req.file.filename)
            fieldsToUpdate.profileImage = relativePath
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/10cd81ab-b235-42af-b326-ae07b9505dc9/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express+EJS 템플릿으로 HTML 렌더링하기]]></title>
            <link>https://velog.io/@dyeon-dev/ExpressEJS-%ED%85%9C%ED%94%8C%EB%A6%BF%EC%9C%BC%EB%A1%9C-HTML-%EB%A0%8C%EB%8D%94%EB%A7%81%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dyeon-dev/ExpressEJS-%ED%85%9C%ED%94%8C%EB%A6%BF%EC%9C%BC%EB%A1%9C-HTML-%EB%A0%8C%EB%8D%94%EB%A7%81%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 19 Aug 2025 14:29:42 GMT</pubDate>
            <description><![CDATA[<h1 id="ejs란">EJS란?</h1>
<p>EJS는 <strong>html의 태그처럼 자바스크립트 내용을 삽입</strong>할 수 있다.
일반 html 파일은 무조건 <code>&lt;script&gt;</code> 태그를 사용해서 분리시켜야하지만, EJS는 지정된 태그를 통해 스트립트 내용을 하나의 요소처럼 사용될 수 있게 한다.
또한, <strong>서버에서 보낸 변수를 가져와 사용할 수 있다!</strong>
기본 세팅은 Node.js의 Express를 사용한다.</p>
<h2 id="ejs-템플릿">EJS 템플릿</h2>
<p><strong>템플릿 엔진</strong>: 문법과 설정에 따라 파일을 html 형식으로 변환시키는 모듈
Embedded JavaScript의 약자로 자바스크립트가 내장되어 있는 html 파일</p>
<h3 id="ejs-설치">ejs 설치</h3>
<p><code>npm install ejs</code></p>
<h3 id="템플릿-엔진-설정하기">템플릿 엔진 설정하기</h3>
<p><code>app.set(&quot;view engine&quot;, &quot;ejs&quot;)</code></p>
<p>EJS 엔진에서는 기본적으로 views 폴더에 템플릿 파일 저장
<code>app.set(&quot;views&quot;, &quot;./views&quot;)</code></p>
<p>템플릿 파일에서 사용하는 정적 파일 제공 (CSS, JS, 이미지 등)
<code>app.use(express.static(&#39;public&#39;))</code></p>
<h2 id="ejs-문법">EJS 문법</h2>
<h3 id="코드를-내장시키는--태그">코드를 내장시키는 &lt;%%&gt; 태그</h3>
<p>ejs에는 자바스크립트를 내장시킬 수 있는 2가지 태그가 있다. 
가장 기본은 &lt;%%&gt;이고, 이 사이에 자바스크립트 내용을 넣으면 된다.</p>
<h3 id="파일-불러오는--include">파일 불러오는 &lt;%-include()%&gt;</h3>
<p><code>&lt;%-include(&#39;view의 파일&#39;)%&gt;</code></p>
<ul>
<li>다른 view 파일을 불러올 때 사용한다.</li>
<li>예를 들어, 공통 컴포넌트를 빼고 불러올 때 유용하게 사용할 수 있다.<ul>
<li>navbar 같은 공통 컴포넌트 로직을 따로 navbar.ejs 파일로 만들어서 main.ejs 파일에서 <code>&lt;%-include(&#39;navbar&#39;) %&gt;</code> 으로 불러오면 된다.</li>
</ul>
</li>
</ul>
<h3 id="변수의-값을-출력하는--">변수의 값을 출력하는 &lt;%= %&gt;</h3>
<p><code>&lt;%= 변수 %&gt;</code></p>
<ul>
<li>EJS는 Node(Express)에서 <strong><code>res.render(view, data)</code>로 넘긴 값을 템플릿에서 바로 사용</strong>할 수 있다. </li>
<li>방식은 심플하고 <strong>서버사이드 렌더링(SSR)</strong>에 잘 맞는다.</li>
</ul>
<h4 id="사용법">사용법</h4>
<ul>
<li><strong>컨트롤러(라우트)에서 데이터 전달</strong><pre><code class="language-js">// 예: 컨트롤러
app.get(&#39;/profile&#39;, (req, res) =&gt; {
res.render(&#39;profile&#39;, {
  title: &#39;프로필&#39;,
  user: { username: &#39;dyeon&#39;, nickname: &#39;다연&#39; },
  posts: [{ id: 1, text: &#39;hi&#39; }, { id: 2, text: &#39;hello&#39; }]
})
})</code></pre>
</li>
<li><strong>EJS에서 사용</strong><pre><code class="language-html">&lt;!-- profile.ejs --&gt;
&lt;h1&gt;&lt;%= title %&gt;&lt;/h1&gt;
&lt;p&gt;닉네임: &lt;%= user.nickname %&gt;&lt;/p&gt;
</code></pre>
</li>
</ul>
<ul>
  <% posts.forEach(post => { %>
    <li><%= post.text %></li>
  <% }) %>
</ul>
```                            

<h2 id="ejs로-로그인-화면-구현-및-처리하기">EJS로 로그인 화면 구현 및 처리하기</h2>
<h3 id="ejs-파일-생성-및-구현">EJS 파일 생성 및 구현</h3>
<ul>
<li>기존 html를 거의 그대로 사용한다. </li>
<li>컴포넌트를 추가해야되면 &lt;%-include() %&gt;를 사용해서 원하는 부분에 추가한다.<pre><code class="language-js">// views/login.ejs
&lt;%-include(&#39;navbar&#39;) %&gt;
</code></pre>
</li>
</ul>
<main>
    <section class="login-section">
        <div class="login-container">
            <h1>로그인</h1>
            <form action="/login">
                <div class="form-group">
                    <label for="username">아이디</label>
                    <input type="text" id="username" name="username" required>
                </div>
                <div class="form-group">
                    <label for="password">비밀번호</label>
                    <input type="password" id="password" name="password" required>
                </div>
                <button type="submit" class="login-submit-btn">로그인</button>
            </form>
        </div>
    </section>
</main>
```

<h3 id="ejs-파일을-불러오기-위한로그인-화면-불러오기-컨트롤러-및-라우터-구현">EJS 파일을 불러오기 위한(로그인 화면 불러오기) 컨트롤러 및 라우터 구현</h3>
<p>html 코드를 적었으니까 이걸 보여줘야한다.
/login 루트 경로 접속 시 위의 코드인 <code>login.ejs</code>가 로그인 화면으로 나타나도록 한다.</p>
<ol>
<li>*<em>컨트롤러 코드 작성 *</em></li>
</ol>
<ul>
<li>로그인 관련 컨트롤러 함수를 작성할 <code>loginController.js</code> 파일을 생성</li>
<li>login.ejs 파일을 렌더링하는 컨트롤러 <code>getLogin</code> 함수를 구현하고 모듈로 내보냄.</li>
<li>/login 경로로 GET 요청 시 실행됨.<pre><code class="language-js">// controllers/loginController.js
const getLogin = (req, res) =&gt; {
  res.render(&#39;login&#39;)
}
</code></pre>
</li>
</ul>
<p>module.exports = {
    getLogin
}</p>
<pre><code>
2. **로그인 관련 라우트 코드 작성**
- 로그인 관련 라우트 코드를 작성할 `loginRoutes.js` 파일을 생성함.
- /login 경로로 GET 요청 시 `getLogin` 함수가 실행되도록 라우팅을 설정하고 라우터를 내보냄.
```js
// routes/loginRoutes.js
const express = require(&#39;express&#39;)
const router = express.Router()
const loginController = require(&#39;../controllers/loginController&#39;)

router.get(&#39;/login&#39;, loginController.getLogin)

module.exports = router</code></pre><ol start="3">
<li><strong><code>app.js</code>에서 로그인 라우트가 실행되도록 추가</strong><pre><code class="language-js">app.use(&#39;/&#39;, require(&#39;./routes/loginRoutes&#39;))</code></pre>
</li>
</ol>
<h3 id="로그인-처리-post-요청-구현">로그인 처리 (POST 요청) 구현</h3>
<p>사용자가 아이디와 비밀번호 입력 후 로그인 클릭 시 POST 방식으로 서버에 정보를 보내도록 한다.</p>
<ol>
<li><p><strong><code>login.ejs</code> 폼 태그의 <code>method</code>를 <code>post</code>로 설정하고 <code>action</code>을 /login 경로로 지정</strong></p>
<pre><code class="language-js">&lt;form action=&quot;/login&quot; method=&quot;post&quot; class=&quot;login-form&quot;&gt;</code></pre>
</li>
<li><p><strong>/login 경로로 POST 요청이 들어왔을 때 처리할 함수를 <code>loginController.js</code>에 추가</strong></p>
</li>
</ol>
<ul>
<li>POST 요청을 처리하는 <code>postLogin</code> 함수를 작성하고, 요청 본문에서 사용자 아이디와 비밀번호를 가져옴</li>
<li>임시로 아이디 &#39;admin&#39;과 비밀번호 &#39;1234&#39;가 일치하면 로그인 성공, 아니면 실패 메시지를 반환하도록 구현함 (아직 컨트롤러가 잘 작동하는지만 확인, 향후 DB로 연결 예정)<pre><code class="language-js">// POST /login
const postLogin = (req, res) =&gt; {
  const { username, password } = req.body
  if(username === &#39;admin&#39; &amp;&amp; password === &#39;1234&#39;) {
      res.redirect(&#39;/&#39;)
  } else {
      res.send(&#39;로그인 실패&#39;)
  }
}</code></pre>
</li>
</ul>
<ol start="3">
<li><strong>로그인 라우트에 <code>postLogin</code> 함수를 가져와 POST 방식 /login 요청 시 실행되도록 설정함.</strong><pre><code class="language-js">const { getLogin, postLogin } = require(&#39;../controllers/loginController&#39;)
</code></pre>
</li>
</ol>
<p>router.route(&#39;/login&#39;).get(getLogin).post(postLogin)</p>
<pre><code>
## EJS로 데이터 넘기기(ft.쿠키로 로그인 관리)
사용자의 로그인 정보를 저장하고 navbar나 페이지에서 **로그인 여부에 따라 분기처리**를 해주어야 한다.
로그인 상태 관리를 위해 세션없이 **쿠키로 로그인 상태를 관리**하고, **미들웨어에서 로그인 유저를 `res.locals.user`에 넣으면 모든 EJS에서 사용이 가능하다.**

1. **로그인 성공 시 쿠키로 사용자 정보 저장**
- username, nickname 데이터 쿠키 심기

```js
// controllers/loginController.js (성공 시)
res.cookie(&#39;user&#39;, JSON.stringify({ username: user.username, nickname: user.nickname }), { httpOnly: false })</code></pre><ol start="2">
<li><strong>서버에서 locals로 로그인 상태 전역 주입</strong></li>
</ol>
<ul>
<li>매 요청마다 쿠키에 user가 정보가 있으면 로그인 상태이기 때문에 res.locals.user로 주입</li>
</ul>
<pre><code class="language-js">// app.js
const cookieParser = require(&#39;cookie-parser&#39;)
app.use(cookieParser())

app.use((req, res, next) =&gt; {
  res.locals.user = req.cookies.user ? JSON.parse(req.cookies.user) : null
  next()
})</code></pre>
<ol start="3">
<li><strong>EJS에서 분기 렌더링</strong></li>
</ol>
<ul>
<li>응답 데이터에 <strong>locals.user가 있다면</strong></li>
<li><strong>변수 문법을 사용하여 EJS 템플릿에 데이터를 넣어준다.</strong></li>
<li>이렇게 구현하면 로그인 성공 후 navbar에 닉네임/글쓰기/로그아웃이 표시되고, 비로그인 시 로그인/회원가입이 표시된다.</li>
</ul>
<pre><code class="language-js">&lt;% if (locals.user) { %&gt;
  &lt;span class=&quot;user-name&quot;&gt;안녕하세요, &lt;%= user.nickname %&gt;님!&lt;/span&gt;
  &lt;button class=&quot;write-btn&quot; href=&quot;#&quot;&gt;글쓰기&lt;/a&gt;
  &lt;form action=&quot;/logout&quot; method=&quot;post&quot;&gt;
    &lt;button class=&quot;logout-btn&quot; type=&quot;submit&quot;&gt;로그아웃&lt;/button&gt;
  &lt;/form&gt;
&lt;% } else { %&gt;
  &lt;form action=&quot;/login&quot; method=&quot;get&quot;&gt;
    &lt;button class=&quot;login-btn&quot; type=&quot;submit&quot;&gt;로그인&lt;/button&gt;
  &lt;/form&gt;
  &lt;form action=&quot;/signup&quot; method=&quot;get&quot;&gt;
    &lt;button class=&quot;signup-btn&quot; type=&quot;submit&quot;&gt;회원가입&lt;/button&gt;
  &lt;/form&gt;
&lt;% } %&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSS Layout]]></title>
            <link>https://velog.io/@dyeon-dev/CSS-Layout</link>
            <guid>https://velog.io/@dyeon-dev/CSS-Layout</guid>
            <pubDate>Tue, 19 Aug 2025 00:01:53 GMT</pubDate>
            <description><![CDATA[<h2 id="elements가-배치layout되는-방식">elements가 배치(Layout)되는 방식</h2>
<p>엘리먼트를 화면에 배치하는 것을 레이아웃 작업이라고도 하며, 렌더링 과정이라고도 한다.
엘리먼트는 위에서 아래로 순서대로 블록을 이루며 배치되는 것이 기본이다.
하지만 웹사이트의 배치는 다양하게 표현 가능해야 하기 때문에, 이를 다양한 방식으로 배치할 수 있도록 CSS에는 추가적인 속성을 제공한다.</p>
<p>아래의 속성들을 중심으로 엘리먼트의 배치를 이해하는 것이 중요하다.</p>
<ul>
<li><strong>display(block, inline, inline-block)</strong></li>
<li><strong>position(static, absolute, relative, fixed)</strong></li>
<li><strong>float(left, right)</strong></li>
</ul>
<h2 id="display-속성">display 속성</h2>
<h3 id="위에서-아래로-쌓이는-블록-요소-displayblock">위에서 아래로 쌓이는 블록 요소 (display:block)</h3>
<p>display 속성이 block이거나 inline-block인 경우 그 엘리먼트는 벽돌을 쌓는 블록을 가지고 쌓인다.
높이값을 더 주면 더 높은 크기로 엘리먼트가 쌓인다.
<strong>inline-block</strong>은 블록 요소 특성 일부를 갖지만, inline처럼 흐름에 따라 좌우로 배치 가능하다. inline 엘리먼트처럼 전후 줄바꿈 없이 한 줄에 다른 엘리먼트들과 나란히 배치되지만, block 엘리먼트처럼 width와 height 속성 지정 및 margin과 padding 속성의 상하 간격 지정이 가능하다. </p>
<pre><code class="language-html">&lt;div&gt;block1&lt;/div&gt;
&lt;p&gt;block2&lt;/p&gt;
&lt;div&gt;block3&lt;/div&gt;</code></pre>
<h3 id="좌우로-흐르는-인라인-요소-displayinline">좌우로 흐르는 인라인 요소 (display:inline)</h3>
<p>display속성이 inline인 경우는 우측으로, 그리고 아래쪽으로 빈자리를 차지하며 흐른다.
참고로, inline속성의 엘리먼트는 높이와 넓이를 지정해도 반영이 되지 않는다.</p>
<pre><code class="language-html">&lt;div&gt;
  &lt;span&gt;나는 어떻게 배치되나요?&lt;/span&gt;
  &lt;span&gt;좌우로 배치되는군요&lt;/span&gt;
  &lt;a&gt;링크는요?&lt;/a&gt;
  &lt;strong&gt;링크도 강조도 모두 좌우로 흐르는군요&lt;/strong&gt;
&lt;/div&gt;</code></pre>
<h2 id="position-속성">position 속성</h2>
<h3 id="좀-다르게-배치시키기-position속성">좀 다르게 배치시키기 (position속성)</h3>
<p>엘리먼트 배치가 순서대로만 위아래로 또는 좌우로 흐르면서 쌓이기만 하면, 다양한 배치를 하기 어렵다.
position속성을 사용면 상대적/절대적으로 어떤 위치에 엘리먼트를 배치하는 것이 수월하다.</p>
<ol>
<li><p>기본 <strong>static</strong>: 그냥 순서대로 배치</p>
</li>
<li><p><strong>absolute</strong>: 기준점에 따라 특별한 위치에 위치</p>
<ul>
<li><strong>배치 흐름(normal flow)에서 제거</strong>하고 <code>top/left/right/bottom</code> 값에 따라 특정 <strong>기준점</strong>(containing block) 기준으로 위치시킴</li>
<li>기준점을 찾는 규칙은 조상 요소 중에서 position 속성이 <code>static</code>이 아닌 요소를 기준점으로 삼음.</li>
<li>즉, <code>relative / absolute / fixed / sticky</code> 이 네 가지 중 하나가 설정된 <strong>조상이 있으면, 그 요소의 padding box가 기준</strong>이 됨.</li>
<li>만약 조상들 중에 전부 static만 있다면? 
결국 <strong>브라우저 뷰포트(<code>&lt;html&gt;</code> 요소)</strong>가 기준점이 됨.</li>
</ul>
</li>
<li><p><strong>relative</strong>: 원래 자신이 위치해야할 곳을 기준으로 이동</p>
<ul>
<li>*<em>배치 흐름(normal flow)에서 계속 포함 *</em>하기 때문에 원래 있어야할 자리 (normal flow 배치 자리)를 차지한 채로, 시각적으로만 이동</li>
<li>따라서 <strong>아래에 오는 다른 요소가 그 빈자리를 차지하지 못함</strong></li>
<li>겹쳐 보이더라도 실제 배치 흐름은 깨지지 않음</li>
<li><code>top/left/right/bottom</code> 값만큼 보이는 박스만 움직임</li>
</ul>
</li>
<li><p><strong>fixed</strong>: viewport(전체화면)좌측,맨위를 기준으로 동작</p>
</li>
</ol>
<h4 id="relative-vs-absolute">relative vs absolute</h4>
<ul>
<li>relative = 자리를 유지한 채 이동 (다른 요소가 그 자리 못 씀)</li>
<li>absolute = 흐름에서 빠져나와 자유롭게 배치 (다른 요소가 그 자리 차지 가능)<blockquote>
<p>code &gt; <a href="https://jsbin.com/vegixihamu/edit?html,css,output">https://jsbin.com/vegixihamu/edit?html,css,output</a>
<img src="https://velog.velcdn.com/images/dyeon-dev/post/e0f06f28-d3fc-4cbe-bcba-85df43c799aa/image.png" alt=""></p>
</blockquote>
</li>
</ul>
<h3 id="기본-배치에서-벗어나서-떠있기-floatleft--right">기본 배치에서 벗어나서 떠있기 (float:left / right)</h3>
<p>float 속성으로 원래 flow에서 벗어날 수 있고 둥둥 떠다닐 수 있다.
일반적인 배치에 따라서 배치된 상태에서 float는 벗어난 형태로 특별히 배치된다. 
따라서 뒤에 block엘리먼트가 float된 엘리먼트를 의식하지 못하고 중첩되어서 배치된다.
float의 속성은 이런 독특함 때문에 웹사이트 레이아웃 배치에서 유용하게 활용되기도 하는데, flex, grid 같은 최신 레이아웃 시스템이 등장하기 전까지는 굉장히 많이 쓰였다.
float는 사실상 텍스트 감싸기나 간단한 좌우 배치 정도에서 유용하다.
레이아웃 전체를 짜는 용도로 쓰기엔 한계가 커서 지금은 flex/grid로 대체되었다.</p>
<blockquote>
<p>code &gt; <a href="http://jsbin.com/cutivij/2/edit?html,css,output">http://jsbin.com/cutivij/2/edit?html,css,output</a></p>
</blockquote>
<h4 id="float를-사용한-레이아웃-예시-한계">float를 사용한 레이아웃 예시 (한계)</h4>
<pre><code>&lt;div class=&quot;container&quot;&gt;
  &lt;div class=&quot;box&quot;&gt;1&lt;/div&gt;
  &lt;div class=&quot;box&quot;&gt;2&lt;/div&gt;
  &lt;div class=&quot;box&quot;&gt;3&lt;/div&gt;
&lt;/div&gt;
</code></pre><pre><code class="language-css">.container {
  width: 300px;
  border: 1px solid #333;
  overflow: hidden; /* clearfix 용도로 사용 */
}

.box {
  float: left;
  width: 100px;
  height: 100px;
  line-height: 100px;
  text-align: center;
  background-color: lightblue;
  margin-right: 10px;
}
</code></pre>
<ul>
<li>박스 간격 조절이 어려움 (margin으로만)</li>
<li>수직 정렬 불가 (line-height 트릭 사용)</li>
<li>컨테이너 높이 자동 조절이 안 됨 (clearfix 필요)</li>
</ul>
<h4 id="flex로-바꾼-예시-해결">flex로 바꾼 예시 (해결)</h4>
<pre><code class="language-css">.container {
  display: flex;
  justify-content: space-between; /* 수평 간격 조절 */
  align-items: center;            /* 수직 정렬 */
  width: 300px;
  border: 1px solid #333;
}

.box {
  width: 100px;
  height: 100px;
  background-color: lightgreen;
  text-align: center;
  line-height: 100px; /* 수직 정렬은 align-items로도 가능 */
}</code></pre>
<ul>
<li>justify-content로 박스 간격 자유롭게 조절 가능</li>
<li>align-items로 수직 중앙 정렬 가능</li>
<li>컨테이너 높이 자동 조절 (clearfix 필요 없음)</li>
<li>순서 변경, 감싸기(wrap) 등도 간단하게 처리 가능 (flex-wrap: wrap;)</li>
</ul>
<h3 id="유연한-박스-모델을-제공-flexbox">유연한 박스 모델을 제공 (flexbox)</h3>
<p>Flexbox는 CSS의 레이아웃 모듈 중 하나로, 유연한 박스 모델을 제공하여 엘리먼트를 쉽게 배치할 수 있도록 도와준다. 
Flexbox는 주로 수평 정렬, 수직 정렬, 아이템 간 간격 설정 등을 다루는 데 사용된다.</p>
<ol>
<li><strong>주 축 (Main Axis)과 교차 축 (Cross Axis)</strong></li>
</ol>
<ul>
<li>Flex 컨테이너는 주 축과 교차 축을 가지며, 이 축들을 기준으로 아이템들이 배치</li>
<li>기본적으로 주 축은 수평 방향이고, 교차 축은 수직 방향</li>
<li>주 축과 교차 축은 Flex 컨테이너의 속성에 따라 결정 </li>
</ul>
<ol start="2">
<li><strong>Flex Container (부모 요소)</strong></li>
</ol>
<ul>
<li>Flexbox를 적용하는 컨테이너</li>
<li>display: flex 또는 display: inline-flex를 사용하여 컨테이너를 정의</li>
</ul>
<table>
<thead>
<tr>
<th><strong>Flex 컨테이너 속성</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>flex-direction</code></strong></td>
<td><strong>아이템들이 배치되는 주 축의 방향을 설정</strong></td>
</tr>
<tr>
<td><strong><code>justify-content</code></strong></td>
<td><strong>주 축에서 아이템들을 정렬하는 방법을 설정.</strong></td>
</tr>
<tr>
<td><strong><code>align-items</code></strong></td>
<td><strong>교차 축에서 아이템들을 정렬하는 방법을 설정.</strong></td>
</tr>
<tr>
<td><strong><code>flex-wrap</code></strong></td>
<td><strong>아이템들이 한 줄에 모두 배치되지 않을 경우 줄 바꿈 여부를 설정.</strong></td>
</tr>
<tr>
<td><strong><code>align-content</code></strong></td>
<td><strong>교차 축에서 여러 줄이 있을 때 줄 전체 묶음을 정렬</strong></td>
</tr>
</tbody></table>
<ol start="3">
<li><strong>Flex Items (자식 요소)</strong></li>
</ol>
<ul>
<li>Flex 컨테이너 내부에 있는 엘리먼트들을 Flex 아이템이라고 함</li>
</ul>
<table>
<thead>
<tr>
<th><strong>Flex 아이템 속성</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>flex-basis</code></strong></td>
<td><strong>아이템의 기본 크기를 설정</strong></td>
</tr>
<tr>
<td><strong><code>flex-grow</code></strong></td>
<td><strong>아이템의 남는 공간 활용에 대한 증가 비율을 설정.</strong></td>
</tr>
<tr>
<td><strong><code>flex-shrink</code></strong></td>
<td><strong>아이템의 감소 비율을 설정.</strong></td>
</tr>
<tr>
<td><strong><code>align-self</code></strong></td>
<td><strong>개별 아이템의 교차 축 정렬 방법을 설정. (Flex 컨테이너의 <code>align-items</code>를 재정의할 수 있음.)</strong></td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/dyeon-dev/post/656ca250-4e21-4de8-98d5-53c6556b748a/image.png" alt=""></p>
<h3 id="정리">정리</h3>
<p>레이아웃의 방법은 여러가지가 있다. 어떤 방법을 선택할지 고민하는 습관을 들이자.</p>
<table>
<thead>
<tr>
<th>display 속성</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>block</td>
<td>전체 너비 차지, 위에서 아래로 쌓임, 높이/너비 적용 가능</td>
</tr>
<tr>
<td>inline</td>
<td>좌우로 흐름, 높이/너비 적용 불가</td>
</tr>
<tr>
<td>inline-block</td>
<td>inline처럼 좌우로 흐르면서, block처럼 높이/너비 적용 가능</td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>position 속성</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>static</td>
<td>기본, normal flow</td>
</tr>
<tr>
<td>relative</td>
<td>원래 자리 기준으로 이동, flow 유지</td>
</tr>
<tr>
<td>absolute</td>
<td>flow에서 제거, 조상 요소 중 <code>position</code>이 static이 아닌 요소 기준 배치</td>
</tr>
<tr>
<td>fixed</td>
<td>viewport 기준으로 고정 배치</td>
</tr>
<tr>
<td>sticky</td>
<td>스크롤 위치에 따라 relative → fixed 전환</td>
</tr>
</tbody></table>
<p>float는 과거 레이아웃(2~3단 컬럼)에서 사용했으나, flex/grid 등장으로 레이아웃 용도는 감소했다.
최신 웹에서는 flex / grid 활용하여 유지보수가 용이하고 직관적이게 구현하자.</p>
<ul>
<li>특별한 위치에 배치 → position: absolute / relative 활용</li>
<li>단순 좌우 배치 → inline-block이나 float 가능</li>
<li>텍스트/요소 간 간격 → margin과 padding 활용</li>
<li>브라우저 호환성 확인 필수</li>
</ul>
<h4 id="참고자료">참고자료</h4>
<ul>
<li><a href="https://css-tricks.com/snippets/css/a-guide-to-flexbox/">CSS Flexbox Layout Guide</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTML 배치를 위한 태그 (ft. 웹페이지 구조로 살펴보기)]]></title>
            <link>https://velog.io/@dyeon-dev/HTML-%EB%B0%B0%EC%B9%98%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%83%9C%EA%B7%B8-ft.-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B5%AC%EC%A1%B0%EB%A1%9C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dyeon-dev/HTML-%EB%B0%B0%EC%B9%98%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%83%9C%EA%B7%B8-ft.-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B5%AC%EC%A1%B0%EB%A1%9C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 18 Aug 2025 13:07:41 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣-html-문서의-기본-구조">1️⃣ HTML 문서의 기본 구조</h2>
<p>HTML 문서의 기본 구조는 다음과 같다. 이렇게 작성하는 건 &#39;웹 콘텐츠를 작성할 준비가 되어있는 상태&#39;로 만드는 작업이다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;title&gt;여기에는 문서의 제목을 입력해주세요&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    여기에 웹페이지에 표시할 콘텐츠(태그)를 입력해주세요
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>HTML 코드를 보면 <strong>계층적</strong>이다.
브라우저는 HTML 정보를 <strong>tree 구조</strong>로 보관하고 있다.
HTML은 적절한 <strong>tag</strong>를 사용해서 구조를 만든다.</p>
<h4 id="head-태그"><code>&lt;head&gt;</code> 태그</h4>
<ul>
<li>html 문서의 <strong>meta 정보</strong>이다. </li>
<li><code>&lt;head&gt;</code>의 안에 작성되는 태그들은 웹 브라우저 화면에 표시될 콘텐츠를 나타내는 것은 아니지만, 웹페이지의 품질에 영향을 주는 중요한 정보들이다.</li>
<li>다양한 설정 태그를 추가 입력할 수 있다.</li>
</ul>
<h4 id="body-태그"><code>&lt;body&gt;</code> 태그</h4>
<ul>
<li>화면에 보여지기 위한 정보이다.</li>
<li>텍스트나 이미지, 미디어 요소 등 다양한 콘텐츠들을 포함하여 웹페이지를 풍성하게 꾸밀 수 있다.</li>
</ul>
<h2 id="2️⃣-화면의-배치-layout-를-위한-태그">2️⃣ 화면의 배치 (layout) 를 위한 태그</h2>
<p>실제 웹 페이지에서 이 태그들이 실제 사용된 예시를 알아보자.
개발자도구로 elements를 확인해보면 <code>&lt;body&gt;</code> 안에 여러 태그들이 쓰이고 있다.</p>
<blockquote>
<p>화면에서 레이아웃 구조를 나타내기 위해서 어떤 태그를 사용할까?</p>
</blockquote>
<p>대표적으로 <strong>header, section, aside, nav, footer</strong>가 있다.</p>
<p>이는 Top-down방식으로 HTML구조를 만들때 이용한다.</p>
<h3 id="header-태그"><code>&lt;header&gt;</code> 태그</h3>
<p><code>&lt;header&gt;</code>는 <strong>웹 페이지의 맨 위에 있는 글로벌 헤더나,
글이나 콘텐츠의 서두[서론/서문/도입부]를 나타낼 때 사용</strong>한다.
여기에는 웹 페이지의 <strong>제목, 로고, 주요 네비게이션 메뉴</strong> 등이 포함될 수 있다.</p>
<ul>
<li><p><a href="http://m.naver.com">네이버 메인페이지</a>
<img src="https://velog.velcdn.com/images/dyeon-dev/post/b38e63de-3fb2-43b8-948c-77d2c4efaeeb/image.png" alt=""> - 네이버 HTML의 태그를 확인해보면, <code>&lt;header&gt;</code>안에 <strong>검색 영역</strong>인 <code>&lt;section&gt;</code>과 <strong>메뉴 영역</strong>인 <code>&lt;nav&gt;</code>가 있다.</p>
</li>
<li><p><a href="https://www.apple.com/kr/store?afid=p240%7Cgo~cmp-200859801~adg-16348496721~ad-746150935850_aud-2374950006609%3Akwd-905396899162~dev-c~ext-~prd-~mca-~nt-search&amp;cid=aos-kr-kwgo-Brand--">애플 공식홈페이지</a> 
<img src="https://velog.velcdn.com/images/dyeon-dev/post/cab767db-eb33-4aa1-8af1-ffe413137d18/image.png" alt=""></p>
<ul>
<li>애플 웹 페이지에서는 <strong>헤더 영역이<code>&lt;div id=&quot;globalheader&quot;&gt;</code>으로 쓰이고 있다.</strong> 
이 부분에 궁금증이 들어서 왜 헤더 태그가 아닌 div 태그를 쓴 건지, 시맨틱 태그가 고려된게 맞는지 gpt에게 물어봤다. 이유는 <strong>애플의 프론트엔드 철학과 호환성 때문이라고 한다.</strong> 
간단하게 말하자면, 애플은 디자인 가이드라인과 일관성을 최우선시하기 때문에 많은 페이지와 컴포넌트를 공유하는데, CSS/JS에서 <code>#globalheader</code>라는 고정 ID를 사용하는 게 <strong>유지보수 측면에서 훨씬 단순</strong>하여 구현 방식에서 <code>&lt;header&gt;</code>를 쓰면 의미상 더 명확하지만, <code>&lt;div&gt;</code>를 택한 것이다. 
즉, <strong>시맨틱 태그 대신 호환성과 레거시 유지보수를 우선한 선택이라고 보면 된다.</strong> (시맨틱 태그를 고려하지 않은 게 아님)</li>
<li>헤더 영역 안에 <strong>보조 콘텐츠 영역</strong>인 <code>&lt;aside&gt;</code>와 <strong>메뉴 영역</strong>인 <code>&lt;nav&gt;</code>가 있다.</li>
</ul>
</li>
</ul>
<h4 id="💡랜드마크-rolebanner-속성을-사용하자--aria-role-속성">💡랜드마크 role=&quot;banner&quot; 속성을 사용하자 ! (ARIA role 속성)</h4>
<p>HTML에서 &quot;<strong>랜드마크(landmark)</strong>&quot;란 웹 페이지의 구조를 설명하고 웹 접근성을 향상시키는 데 사용되는 특별한 역할을 하는 요소이다.</p>
<p>header의 HTML 요소(Element)의 확장 개념으로 좀 더 명확한 HTML 요소의 구조와 의미를 부여하는 역할을 하는 role 속성의 값은 <code>banner</code>이다. 이는 스크린 리더를 사용하는 사용자를 위한 웹 접근성을 고려하기 위해 사용된다.
<code>&lt;header&gt;</code>가 <code>&lt;body&gt;</code> 바로 아래에 있지 않고 <code>&lt;article&gt;</code>, <code>&lt;section&gt;</code>, <code>&lt;nav&gt;</code> 등 다른 페이지 영역 내부에 포함될 경우, 접근성 트리에서는 일반 section 역할로 인식될 수 있다. 이때는 명시적으로 role=&quot;banner&quot;를 지정해주는 것이 유용하다.
<code>&lt;header&gt;</code> 태그에 명시적으로 role 속성의 값이 banner로 지정되어 있지 않으면 웹 접근성을 위한 스크린 리더는 HTML 문서의 맥락상 global header인지 판단한다.</p>
<p>global header임을 명시적으로 나타내기 위해서는 <code>role=&quot;banner&quot;</code>를 사용하는 것이 좋다.</p>
<pre><code>&lt;header role=&quot;banner&quot;&gt;
    &lt;h1&gt;웹사이트 제목&lt;h1&gt;
    .... 로고, 네비게이션, 검색....
&lt;/header&gt;</code></pre><h3 id="section-태그"><code>&lt;section&gt;</code> 태그</h3>
<p><code>&lt;section&gt;</code> 태그는 문서, 애플리케이션의 일반적인 섹션을 나타낸다.
더 적합한 의미를 가진 요소가 없을 때 <strong>논리적인 측면에서 주제별 콘텐츠 그룹을 나타내는데 사용한다.</strong>
<code>&lt;section&gt;</code> 태그를 일반 컨테이너로 사용하면 안된다. 단순 스타일링이 목적이라면 <code>&lt;div&gt;</code> 태그를 사용해야 한다.</p>
<blockquote>
<p><code>&lt;section&gt;</code>과 <code>&lt;article&gt;</code> 태그의 차이점</p>
</blockquote>
<ul>
<li><code>&lt;section&gt;</code> 태그는 문서나 애플리케이션의 논리적인 측면에서 주제별 콘텐츠 그룹을 나타내는데 사용된다.</li>
<li><code>&lt;article&gt;</code> 태그는 문서, 페이지, 애플리케이션, 또는 사이트에서 독립적으로 배포 가능하거나 재사용할 수 있는 콘텐츠를 묶는 영역으로 사용된다. 예를 들어, 게시판 글, 뉴스 기사, 블로그 글, 댓글 등이 있다. <code>&lt;article&gt;</code> 태그 내에서 <code>&lt;section&gt;</code> 태그를 사용하는 것은 가능하다.</li>
</ul>
<h3 id="aside-태그"><code>&lt;aside&gt;</code> 태그</h3>
<p><code>&lt;aside&gt;</code> 태그는 페이지의 <strong>주요 콘텐츠 영역의 보조 콘텐츠 영역</strong>을 나타낸다. 주요 콘텐츠와 상호작용하지 않고 보조적인 역할을 하는 내용이 포함될 수 있다.
주로 문서의 주요 내용과 관련된 별도로 분리된 추가 정보를 제공하는 데 사용해야 하며, 문서의 주요 내용에 포함되는 내용을 나타내는 데는 사용해서는 안된다.</p>
<p>사용 예제로는 <strong>사이드바</strong>에서 많이 사용된다.</p>
<blockquote>
<p>블로그 사이드바</p>
</blockquote>
<pre><code class="language-html">&lt;article&gt;
    &lt;h1&gt;블로그 포스트 제목&lt;/h1&gt;
    &lt;p&gt;블로그 내용...&lt;/p&gt;
&lt;/article&gt;
&lt;aside&gt;
    &lt;!-- 블로그 포스트와 관련된 추가 정보 --&gt;
    &lt;h2&gt;관련 포스트&lt;/h2&gt;
    &lt;ul&gt;
        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;포스트 1&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;포스트 2&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;포스트 3&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
&lt;/aside&gt;</code></pre>
<blockquote>
<p>광고 배너</p>
</blockquote>
<pre><code class="language-html">&lt;main&gt;
    &lt;h1&gt;제품 소개&lt;/h1&gt;
    &lt;p&gt;제품에 대한 자세한 정보...&lt;/p&gt;
&lt;/main&gt;
&lt;aside&gt;
    &lt;ins&gt;
        &lt;!-- 제품 관련 광고 배너 --&gt;
        &lt;img src=&quot;광고이미지.jpg&quot; alt=&quot;제품 광고&quot;&gt;        
    &lt;/ins&gt;
&lt;/aside&gt;</code></pre>
<blockquote>
<p>인용구</p>
</blockquote>
<pre><code class="language-html">&lt;article&gt;
    &lt;h1&gt;역사적 이벤트&lt;/h1&gt;
    &lt;p&gt;이벤트에 대한 설명...&lt;/p&gt;
&lt;/article&gt;
&lt;aside&gt;
    &lt;!-- 역사적 이벤트와 관련된 인용구 --&gt;
    &lt;blockquote&gt;
        &lt;p&gt;역사는 어떤 상황에서든 경험된 것이 아니라, 해석된 것이다.&lt;/p&gt;
        &lt;span&gt;칼 유워스&lt;/span&gt;
    &lt;/blockquote&gt;
&lt;/aside&gt;</code></pre>
<h3 id="nav-태그"><code>&lt;nav&gt;</code> 태그</h3>
<p><code>&lt;nav&gt;</code> 태그는 <strong>다른 웹 페이지로 연결하거나, 현재 웹 페이지의 콘텐츠 내부로 연결되는 탐색(navigation)을 위한 링크(links)가 있는 영역</strong>을 나타낸다.
일반적인 예로는 <strong>메뉴, 목차, 색인</strong> 등이 있다.</p>
<blockquote>
<p>주요 네비게이션 메뉴</p>
</blockquote>
<pre><code class="language-html">&lt;nav&gt;
    &lt;ol&gt;
        &lt;li&gt;&lt;a href=&quot;/&quot;&gt;홈&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;/products&quot;&gt;제품&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;/products/category1&quot;&gt;카테고리 1&lt;/a&gt;&lt;/li&gt;
        &lt;!-- Breadcrumbs를 사용하여 사용자는 현재 페이지가 &quot;홈 &gt; 제품 &gt; 카테고리 1 &gt; 현재 페이지&quot;임을 이해할 수 있고, 각 부분을 클릭하여 해당 페이지로 이동할 수 있다. --&gt;
        &lt;li aria-current=&quot;page&quot;&gt;현재 페이지&lt;/li&gt;
    &lt;/ol&gt;
&lt;/nav&gt;</code></pre>
<h3 id="footer-태그"><code>&lt;footer&gt;</code> 태그</h3>
<p>웹 페이지의 맨 하단에 있는 global footer를 나타내거나, 글이나 콘텐츠 영역에 대한 작성자, 저작권 정보, 관련 문서에 대한 링크 등의 내용을 나타낸다.</p>
<h4 id="💡랜드마크-rolecontentinfo-속성을-사용하자--aria-role-속성">💡랜드마크 role=&quot;contentinfo&quot; 속성을 사용하자 ! (ARIA role 속성)</h4>
<p>footer의 HTML 요소(Element)의 확장 개념으로 좀 더 명확한 HTML 요소의 구조와 의미를 부여하는 역할을 하는 role 속성의 값은 <code>contentinfo</code>이다. 이는 스크린 리더를 사용하는 사용자를 위한 웹 접근성을 고려하기 위해 사용된다.</p>
<p>대부분의 웹 접근성을 위한 스크린리더에서는 <code>&lt;footer&gt;</code> 태그가 HTML 문서의 맥락상 global footer인지 판단한다. 그래서, <code>&lt;footer&gt;</code> 태그에 role=&quot;contentinfo&quot;를 추가하지 않아도 되지만, 일부 스크린리더에서 global footer인지 판단하지 못하는 문제가 있다. 웹 접근성을 위해 이 문제를 해결하려면 <code>&lt;footer&gt;</code> 태그에 role=&quot;contentinfo&quot;를 명시적으로 추가해야 한다.</p>
<pre><code>&lt;footer role=&quot;contentinfo&quot;&gt;
    &lt;address&gt;
        © 2025 웹 페이지 제작자
        &lt;a href=&quot;mailto:contact@example.com&quot;&gt;contact@example.com&lt;/a&gt;
    &lt;/address&gt;
&lt;/footer&gt;</code></pre><p>대표적으로 3개의 웹 페이지에서 확인한 결과, 애플 공식홈페이지에서만 footer에서 <code>role=&quot;contentinfo&quot;</code>을 사용중이었다.
<img src="https://velog.velcdn.com/images/dyeon-dev/post/ccacf855-ca6c-4e8f-a060-2cb1c8ef9316/image.png" alt=""></p>
<h3 id="👀-웹-페이지-대충-비교-분석해보기">👀 웹 페이지 대충 비교 분석해보기</h3>
<ul>
<li><a href="https://www.apple.com/kr/store?afid=p240%7Cgo~cmp-200859801~adg-16348496721~ad-746150935850_aud-2374950006609%3Akwd-905396899162~dev-c~ext-~prd-~mca-~nt-search&amp;cid=aos-kr-kwgo-Brand--">애플 공식홈페이지</a> <ul>
<li>애플 웹 페이지에서는 *<em><code>&lt;section&gt;</code> 태그를 사용한 영역은 없다. *</em></li>
<li><strong>대신 <code>&lt;div&gt;</code> 태그에 고유 id로 시맨틱 태그를 적용</strong>하였다.</li>
<li>총 8개의 모든 영역을 <code>&lt;div id=&quot;1_section&quot;&gt;</code>, <code>&lt;div id=&quot;2_section&quot;&gt;..</code> 식으로 나누었다.</li>
<li>전체적으로 <code>&lt;div&gt;</code> 태그를 많이 썼다. *<em>그럼에도 불구하고 프론트엔드 설계랑 철학이 엿보이는 것 같다. *</em></li>
</ul>
</li>
<li><a href="http://m.naver.com">네이버 메인페이지</a><ul>
<li>네이버 웹 페이지에서는 *<em><code>&lt;section&gt;</code> 태그를 검색 영역에서만 사용했다. *</em></li>
<li>*<em>네이버는 특징적으로 게시판 글, 뉴스 기사를 주로 다루기 때문에 <code>&lt;article&gt;</code> 태그가 있을 줄 알았는데 없었다. *</em>
비슷한 의미로 <code>&lt;section&gt;</code> 영역도 콘텐츠 부분에 사용하지 않은 점이 궁금하다.</li>
</ul>
</li>
<li><a href="https://nol.yanolja.com/?trackcode=mkt_google_sa&amp;utm_source=google_sa&amp;utm_medium=cpc&amp;utm_campaign=20738115572&amp;utm_content=160897187931&amp;utm_term=kwd-298391364620&amp;gad_source=1&amp;gad_campaignid=20738115572&amp;gbraid=0AAAAAoeYBbkyrSnPc4YPlwUQT32FGCJub&amp;gclid=Cj0KCQjwnovFBhDnARIsAO4V7mD0J-Cfn-kUAYtsvs-8sypoTDvMdZfCxQu5_HAY7pzUFNRsknsHjpAaArIqEALw_wcB&amp;headerState=FOLD">NOL 야놀자 메인페이지</a><ul>
<li>야놀자 웹 페이지에서는 <strong>총 12개 영역 중에 2개의 영역에서만 <code>&lt;section&gt;</code> 태그를 사용</strong>했다. </li>
<li>특가 호텔 슬라이더와 기획전 모음 영역. 두 곳에서만 사용되고,</li>
<li><em>나머지 영역들에서는 <code>&lt;div&gt;</code> 태그로 사용*</em>됐는데, 왜 통일성 없이 이렇게 했는지 궁금하다.
통일성을 못찾아서 어떤 기준으로 한 건지 분석 실패..</li>
</ul>
</li>
</ul>
<h2 id="3️⃣-정리">3️⃣ 정리</h2>
<p><strong>태그를 사용할 때는 웹 페이지의 내용을 시맨틱하게 구조화하여 이해하기 쉽게 작성하는 것이 기본</strong>이다.</p>
<p>시맨틱 태그는 <strong>SEO, 접근성, 협업 효율성</strong> 측면에서 장점이 크다. </p>
<p>다만, 실무에서는 프레임워크나 레거시 코드, 유지보수 편의성 때문에 <code>&lt;div&gt;</code> 같은 비시맨틱 태그를 전략적으로 활용하는 경우도 많다. 따라서 “시맨틱 태그 vs 비시맨틱 태그”의 이분법보다는 <strong>상황에 맞는 균형 잡힌 선택</strong>이 필요하다.</p>
<p>또한, <strong>웹 접근성을 높이기 위해 landmark role</strong>(<code>role=&quot;banner&quot;</code>, <code>role=&quot;contentinfo&quot;</code> 등)을 명시적으로 병행하는 습관을 가지는 것이 좋다.</p>
<h4 id="참고자료">참고자료</h4>
<ul>
<li><a href="https://codingeverybody.kr/category/html/html-tag/">HTML 태그 소개</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>