<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>swallowed_o0.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 15 Aug 2025 04:15:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>swallowed_o0.log</title>
            <url>https://velog.velcdn.com/images/swallowed_o0/profile/73902ed7-5c67-431b-9e3e-704502a89c28/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. swallowed_o0.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/swallowed_o0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[💬 Q&A : 성능 최적화]]></title>
            <link>https://velog.io/@swallowed_o0/QA-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@swallowed_o0/QA-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Fri, 15 Aug 2025 04:15:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>성능 최적화를 한 번도 제대로 알아본 적이 없는 것 같아, 면접을 준비하면서 어떠한 방법들과 개념들이 있는지 알아보았다.</p>
</blockquote>
<h2 id="✅-전송-최적화---네트워크-구간">✅ 전송 최적화 - 네트워크 구간</h2>
<blockquote>
<p>브라우저가 페이지를 표시하기 위해 서버에서 <code>HTML</code> , <code>CSS</code> , <code>JS</code> , 이미지 등 여러 파일을 받아오는데, 이 <strong>다운로드 과정</strong>을 줄이거나 <strong>빠르게</strong> 만드는 방법이다.</p>
</blockquote>
<h3 id="📍-코드-분할--code-splitting">📍 코드 분할 : Code Splitting</h3>
<ul>
<li>처음 페이지를 열 때, JS 파일이 너무 크면 다운로드, 해석, 실행이 오래 걸린다.</li>
<li>그래서 코드를 잘게 쪼개 <strong>필요한 순간에만 불러오면 초기 로딩이 빨라진다.</strong></li>
<li><code>React.lazy</code> , <code>import()</code> 를 사용하여 해당 기능이 필요할 때만 가져오게 할 수 있다.</li>
<li>다만 지나치게 쪼개면 네트워크 요청이 많아져서 오히려 느려질 수 있다.</li>
</ul>
<h3 id="📍-트리-셰이킹--tree-shaking">📍 트리 셰이킹 : Tree Shaking</h3>
<ul>
<li>번들에 사용하지 않는 코드까지 포함되면 파일이 불필요하게 커진다.</li>
<li><strong>사용되는 코드만 남기고 나머지를 제거</strong> 하면 전송 용량이 줄어든다</li>
<li><code>ES Module</code> 방식 (<code>import/export</code>) 을 사용하고, 라이브러리는 필요한 함수만 가져오면 된다.</li>
</ul>
<h3 id="📍-압축--gzipbrotli">📍 압축 : gzip/brotli</h3>
<ul>
<li><code>JS</code> , <code>CSS</code> , <code>HTML</code> 은 텍스트이므로 압축하면 용량을 20~70% 줄일 수 있다.</li>
<li>서버나 CDN에서 <code>Content-Encoding: br</code> 이나 <code>gzip</code> 을 설정한다.</li>
<li>이미 압축된 이미지/폰트는 다시 압축해도 효과가 없다.</li>
</ul>
<h3 id="📍-이미지-최적화--webpavif-lazy-loading">📍 이미지 최적화 : WebP/AVIF, lazy-loading</h3>
<ul>
<li>이미지가 페이지 로딩 속도를 가장 많이 잡아먹는 경우가 많다.</li>
<li><code>JPG/PNG</code> 대신 용량이 적은 <code>WebP/AVIF</code> 포맷을 사용한다.</li>
<li>또는 <code>loading=&quot;lazy&quot;</code> 로 화면에 보이기 직전까지 다운로드를 지연시킨다.</li>
</ul>
<h3 id="📍-cdn--캐시--cache-control-etag">📍 CDN &amp; 캐시 : Cache-Control, ETag</h3>
<ul>
<li>같은 파일을 재방문 때 다시 다운로드하지 않게 하면 빠르다.</li>
<li>파일명에 버전 해시를 붙이고, <code>CDN</code> 에서 장기 캐시를 설정한다.</li>
</ul>
<br/>

<hr>
<h2 id="✅-렌더링-최적화">✅ 렌더링 최적화</h2>
<blockquote>
<p>서버에서 파일을 받아온 후, 브라우저가 빠르게 렌더링할 수 있도록 하는 방법</p>
</blockquote>
<h3 id="📍-ssrssg">📍 SSR/SSG</h3>
<ul>
<li>서버에서 <code>HTML</code> 을 만들어 보내면, 브라우저가 바로 표시 가능하다.</li>
<li>검색 노출이 중요하거나 첫 홤녀 표시 속도가 중요한 경우 이를 사용하면 좋다.</li>
<li>하이드레이션(서버 렌더 HTML에 JS 이벤트 연결) 비용이 너무 크면 상호작용이 느려질 수 있다.</li>
</ul>
<h3 id="📍-csr-지연-로딩">📍 CSR 지연 로딩</h3>
<ul>
<li>한 번에 많은 <code>JS</code> 를 실행하면 첫 화면 표시가 늦어진다.</li>
<li>화면 아래쪾의 컴포넌트는 <code>InterSection Observer</code>로 실제 보일 때 로딩하게 한다.</li>
</ul>
<h3 id="📍-critical-css--폰트-최적화">📍 Critical CSS / 폰트 최적화</h3>
<ul>
<li>화면 표시를 막는 CSS/폰트 로딩을 줄이면 LCP가 개선된다.</li>
<li>필요한 CSS만 HTML에 인라인, 나머지는 지연로딩</li>
<li>폰트는 <code>rel=preconnect</code> +<code>font-display:swap</code> 을 사용한다.</li>
<li>CSS의 <code>@import</code> 는 로딩 차단이 커서 피해야 한다.</li>
</ul>
<h3 id="📍-레이아웃-스래싱-방지">📍 레이아웃 스래싱 방지</h3>
<ul>
<li>DOM의 레이아웃을 읽고 쓰는 동작이 섞이면 브라우저가 계쏙 다시 계산해야 해서 프레임 드랍이 난다.</li>
<li>읽기 작업 후 쓰기 작업으로 묶고, 위치 변경은 <code>transform</code> 을 사용한다.</li>
</ul>
<br/>

<hr>
<h2 id="✅-런타임-최적화---실행-중">✅ 런타임 최적화 - 실행 중</h2>
<blockquote>
<p>이미 로드된 페이지가 사용자의 입력/스크롤 등에 빠르게 반응하도록 실행 중 동작을 최적화하는 방법이다.</p>
</blockquote>
<h3 id="📍-메모이제이션">📍 메모이제이션</h3>
<ul>
<li>같은 계산을 반복하지 않게 캐싱해 CPU 낭비를 줄인다.</li>
<li>React의 <code>useMemo</code> , <code>useCallback</code> , <code>React.memo</code> 등을 사용하면 된다.</li>
<li>다만, 계산이 빠른 작업에 남용하면 메모리 낭비가 될 수도 있다.</li>
</ul>
<h3 id="📍-가상-스크롤">📍 가상 스크롤</h3>
<ul>
<li>수천 개의 리스트를 한 번에 그리면 느려진다.</li>
<li><strong>화면에 보이는 범위만 DOM에 렌더링, 나머지는 가상으로 관리</strong>한다.</li>
</ul>
<h3 id="📍-이벤트-위임">📍 이벤트 위임</h3>
<ul>
<li>수많은 자식 요소마다 이벤트를 달면 메모리와 성능이 낭비된다.</li>
<li><strong>부모 요소에 이벤트를 한 번만 달고</strong> <code>event.target</code> 으로 분기 처리를 해준다.</li>
</ul>
<h3 id="📍-debouncethrottle">📍 Debounce/Throttle</h3>
<ul>
<li>입력/스크롤 같은 이벤트가 너무 자주 발생하면 렌더링이 느려진다.</li>
<li><strong>Debounce</strong> : 마지막 이벤트 후 일정 시간만 실행</li>
<li><strong>Throttle</strong> : 일정 주기마다 실행</li>
</ul>
<blockquote>
<h3 id="🔗-핵심-지표">🔗 핵심 지표</h3>
</blockquote>
<ul>
<li><strong>LCP</strong> : 가장 큰 컨텐츠 표시까지 걸린 시간 (목표 ≤ 2.5s)</li>
<li><strong>CLS</strong> : 화면 요소의 예기치 않은 움직임 정도 (목표 ≤ 0.1)</li>
<li><strong>INP</strong> : 사용자 입력 후 반응 시간 (목표 ≤ 200ms)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[💬 Q&A : 프로젝트 초기 세팅]]></title>
            <link>https://velog.io/@swallowed_o0/QA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@swallowed_o0/QA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Sun, 22 Jun 2025 12:05:18 GMT</pubDate>
            <description><![CDATA[<h2 id="🧐-nodejs는-프론트엔드-개발자에게-왜-필요할까">🧐 Node.js는 프론트엔드 개발자에게 왜 필요할까?</h2>
<ul>
<li>자바스크립트는 원래 브라우저 안에서만 실행됨</li>
<li><code>Node.js</code>는 JS를 브라우저 밖에서도 실행할 수 있게 해주는 환경</li>
<li>우리가 사용하는 개발 도구들(Vite, ESLint, Tailwind 등)은 모두 <code>Node.js</code> 기반</li>
</ul>
<br>

<h2 id="🧐-패키지-매니저란-무엇이고-어떤-역할을-할까">🧐 패키지 매니저란 무엇이고, 어떤 역할을 할까?</h2>
<ul>
<li>다른 사람이 만든 라이브러리를 설치하고 관리해주는 도구</li>
<li>대표적으로 <code>npm</code>, <code>yarn</code>, <code>pnpm</code> 이 있음</li>
<li>패키지를 설치하면 <code>node_modules/</code>에 실제 파일이 설치되고
<code>package.json</code>과 <code>lock</code> 파일에 기록됨</li>
</ul>
<br>

<h2 id="🧐-npm-yarn-pnpm-중-무엇을-써야-할까">🧐 npm, yarn, pnpm 중 무엇을 써야 할까?</h2>
<blockquote>
<p>🚨 주의 : 혼용 금지! <code>lock</code> 파일 충돌 + 구조 꼬임 발생</p>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th><strong>npm</strong></th>
<th><strong>yarn</strong></th>
<th><strong>pnpm</strong></th>
</tr>
</thead>
<tbody><tr>
<td>🌱 등장 시기</td>
<td>가장 먼저 등장 (기본)</td>
<td>Facebook이 성능 개선 위해 제작</td>
<td>가장 최신 성능 지향</td>
</tr>
<tr>
<td>🚀 속도</td>
<td>느림</td>
<td>보통</td>
<td><strong>가장 빠름</strong></td>
</tr>
<tr>
<td>📦 설치 방식</td>
<td>패키지를 <strong>복사</strong>해서 node_modules에 저장</td>
<td>npm과 유사</td>
<td><strong>하드링크</strong>로 연결, 저장 공간 절약</td>
</tr>
<tr>
<td>💾 디스크 용량 효율</td>
<td>낮음 (중복 저장)</td>
<td>보통</td>
<td><strong>높음 (중복 없음)</strong></td>
</tr>
<tr>
<td>🔐 lock 파일</td>
<td><code>package-lock.json</code></td>
<td><code>yarn.lock</code></td>
<td><code>pnpm-lock.yaml</code></td>
</tr>
<tr>
<td>📚 생태계/호환성</td>
<td>모든 프로젝트에서 사용 가능</td>
<td>많은 레거시 프로젝트에서 사용</td>
<td>빠르게 성장 중, Next.js 등 채택</td>
</tr>
<tr>
<td>📁 node_modules 구조</td>
<td>평평 (flat), 충돌 발생 가능</td>
<td>flat 구조</td>
<td>격리된 <code>.pnpm</code> 폴더로 관리 안정적</td>
</tr>
<tr>
<td>🔁 캐시 전략</td>
<td>단순 디렉토리 캐시</td>
<td>파일 시스템 기반</td>
<td><strong>전용 저장소 캐시, 더 효율적</strong></td>
</tr>
<tr>
<td>📜 명령어 스타일</td>
<td><code>npm install</code></td>
<td><code>yarn add</code></td>
<td><code>pnpm add</code></td>
</tr>
<tr>
<td>🔀 병렬 설치</td>
<td>기본</td>
<td>빠름</td>
<td><strong>최적화된 병렬 설치</strong></td>
</tr>
</tbody></table>
<br>

<h2 id="🧐-패키지-매니저들이-빠르다디스크-적게-쓴다는-건">🧐 패키지 매니저들이 빠르다/디스크 적게 쓴다는 건?</h2>
<ul>
<li><code>캐시</code> : 한 번 받은 패키지를 저장해두고 다음에 재사용</li>
<li><code>디스크 용량</code> : 같은 파일을 여러 번 복사하느냐(→ 용량 낭비)</li>
</ul>
<blockquote>
<p><code>pnpm</code> 은 <strong>하드링크를 사용해서 파일을 한 번만 설치</strong>하고 각 프로젝트가 링크만 걸어 쓰기 때문에 <strong>빠르고 가볍다.</strong></p>
</blockquote>
<blockquote>
<h3 id="🔎-하드링크란">🔎 하드링크란?</h3>
</blockquote>
<ul>
<li>복사본이 아니라 이름표를 하나 더 만드는 방식</li>
<li>파일을 여러 경로에서 공유해서 접근하지만, 디스크에선 실제 파일은 하나만 존재</li>
</ul>
<br>

<h2 id="🧐-번들러란">🧐 번들러란?</h2>
<ul>
<li>여러 파일(JS/CSS/이미지)을 <strong>하나로 묶어주는 도구</strong></li>
<li>브라우저는 파일을 한 번에 불러올 수 없기 때문에, 번들러가 파일을 묶고 압축해서 효율적으로 전달해줌</li>
</ul>
<br>

<h2 id="🧐-webpack과-vite는-어떤-차이가">🧐 Webpack과 Vite는 어떤 차이가?</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Webpack</th>
<th>Vite</th>
</tr>
</thead>
<tbody><tr>
<td>개발 서버 방식</td>
<td>전체 번들 후 실행</td>
<td>ESM 기반 즉시 실행</td>
</tr>
<tr>
<td>속도</td>
<td>느림</td>
<td>매우 빠름</td>
</tr>
<tr>
<td>설정 난이도</td>
<td>복잡</td>
<td>간단</td>
</tr>
<tr>
<td>핫 리로드(HMR)</td>
<td>느림</td>
<td>빠름</td>
</tr>
</tbody></table>
<br>

<h2 id="🧐-hmr">🧐 HMR?</h2>
<blockquote>
<p><strong>Hot Module Replacement</strong></p>
</blockquote>
<ul>
<li>파일을 수정했을 때, 전체 페이지 <strong>새로고침 없이, 바뀐 모듈만 교체</strong>해서 적용되는 기능</li>
<li><code>상태/스크롤/입력값</code>을 유지하면서 개발할 수 있어서 개발 속도가 엄청 빨라짐</li>
</ul>
<br>

<h2 id="🧐-swc">🧐 SWC?</h2>
<blockquote>
<p><strong>Speedy Web Compiler</strong></p>
</blockquote>
<ul>
<li><code>Babel</code>보다 훨씬 빠르게 <code>JS/TS/JSX</code>를 변환해주는 컴파일러</li>
<li><code>Rust</code>로 만들어져서 속도가 빠르고, 최신 프레임워크에서 채택 중</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚 HTTP 완벽가이드 - 6장. 프락시]]></title>
            <link>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-6%EC%9E%A5.-%ED%94%84%EB%9D%BD%EC%8B%9C</link>
            <guid>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-6%EC%9E%A5.-%ED%94%84%EB%9D%BD%EC%8B%9C</guid>
            <pubDate>Thu, 08 May 2025 09:56:31 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-핵심-개념-요약">📍 핵심 개념 요약</h2>
<ul>
<li>프락시는 <strong>클라이언트와 서버 사이에서 중간자 역할</strong>을 하는 서버</li>
<li>클라이언트는 프락시를 통해 요청을 보내고, 프락시는 실제 서버 대신 요청 후 응답을 클라이언트에 전달</li>
<li>개발 중 <strong>CORS 문제 회피</strong>를 위해 프락시 서버를 설정하기도 한다.</li>
<li>요청이 프락시를 거치면, 실제 서버 주소가 숨겨지고 <strong>보안 또는 속도 개선</strong>이 가능하다.</li>
</ul>
<blockquote>
<h3 id="✨-포인트">✨ 포인트</h3>
</blockquote>
<ul>
<li>캐시 프락시 : 자주 요청되는 리소스를 저장해 응답 속도를 높이고 트래픽 감소</li>
<li>콘텐츠 필터링 : 회사/학교 등에서 특정 사이트 차단할 때 사용</li>
<li>로깅 및 감시 : 요청 기록을 분석하거나 로그를 저장할 수 있다.</li>
</ul>
<hr>
<h2 id="✅ㅤ웹-중개자">✅ㅤ웹 중개자</h2>
<ul>
<li>HTTP 프락시 서버는 웹 서버이기도 하고, 웹 클라이언트이기도 하다.</li>
<li>프락시 서버는 하나의 클라이언트가 독점적으로 사용할 수도 있고 여러 클라이언트가 공유할 수도 있다.</li>
<li>대부분의 프락시는 공용이며, 공유된 프락시다. </li>
<li>중앙 집중형 프락시를 관리하는 게 더 비용효율이 높고 쉽다.</li>
</ul>
<br>

<h2 id="✅ㅤ프락시-vs-게이트웨이">✅ㅤ프락시 VS 게이트웨이</h2>
<blockquote>
<p>실질적으로 프락시와 게이트웨이의 차이점은 모호하다. 프락시가 때떄로 약간의 프로토콜 변환을 하기도 하기 떄문</p>
</blockquote>
<ul>
<li>프락시 : 같은 프로토콜을 사용하는 둘 이상의 애플리케이션을 연결</li>
<li>게이트웨이 : 서로 다른 프로토콜로 말하더라도 서로 간의 트랜잭션을 완료할 수 있도록 해주는 프로토콜 변환기처럼 동작</li>
</ul>
<br>

<h2 id="✅ㅤ왜-프락시를-사용하는가">✅ㅤ왜 프락시를 사용하는가?</h2>
<ul>
<li><p>서버는 각 리소스에 대해 <strong>올바른 MIME 타입을 설정</strong>해야 한다 ➡️ <code>Content-Type</code> 헤더 </p>
</li>
<li><p>실용적이고 유용한 것이라면 무슨 일이든 한다.</p>
</li>
<li><p>보안을 개선하고 성능을 높여주며, 비용을 절약한다.</p>
</li>
<li><p>모든 HTTP 트래픽을 들여다보고 건드릴 수 있기 때문에, 프락시는 부가적인 가치를 주는 여러 유용한 웹 서비스를 구현하기 위해 트래픽을 감시하고 수정할 수 있다.</p>
</li>
<li><p>예 ) 어린이 필터, 문서 접근 제어자, 보안 방화벽, 웹 캐시, 대리 프락시(=서버 가속기), 콘텐츠 라우터, 트랜스코더, 익명화 프락시</p>
</li>
</ul>
<blockquote>
<h3 id="🔎ㅤ트래픽">🔎ㅤ트래픽</h3>
</blockquote>
<ul>
<li>네트워크 상에서 <strong>데이터가 오가는 양</strong>을 의미한다.</li>
<li>주로 클라이언트 ↔️ 서버 간에 오고 가는 요청과 응답을 모두 포함한다.</li>
</ul>
<br>

<h2 id="✅ㅤ프락시-서버-배치">✅ㅤ프락시 서버 배치</h2>
<blockquote>
<p>어떻게 사용할지에 따라 프락시는 어디에든 배치할 수 있다.</p>
</blockquote>
<h3 id="📍-클라이언트-근처-포워드-프록시">📍 클라이언트 근처: 포워드 프록시</h3>
<ul>
<li>위치    : 클라이언트와 인터넷 사이                            </li>
<li>예시   : 사내 네트워크에서 외부 접속 제한할 때                    </li>
<li>기능  : IP 숨김, 보안 필터링, 내부 사용자 제어                 </li>
<li>활용 : CORS 우회 프록시로 활용, 외부 API에 서버 없이 프록시 요청 가능 </li>
</ul>
<br>

<h3 id="📍-서버-근처-리버스-프록시">📍 서버 근처: 리버스 프록시</h3>
<ul>
<li>위치 : 서버 앞단 (외부에서 보면 서버처럼 보임)            </li>
<li>예시 : Nginx, Apache HTTPD, AWS ALB       </li>
<li>기능 : 로드 밸런싱, 캐싱, SSL 종료, 보안             </li>
<li>활용 : 프론트 요청을 리버스 프록시가 받아서 내부 API 서버에 전달 </li>
</ul>
<br>

<h3 id="📍-네트워크-중간-게이트웨이">📍 네트워크 중간: 게이트웨이</h3>
<ul>
<li>위치    : 시스템 간 경계 지점 (API Gateway, 통신 변환)</li>
<li>기능    : 프로토콜 변환 (HTTP ↔ gRPC 등), 인증, 요청 라우팅</li>
<li>예시    : AWS API Gateway, Kong</li>
<li>활용 : 다양한 API를 단일 엔드포인트로 관리할 때 사용 <code>(예: /api → 내부 마이크로서비스로 분기)</code></li>
</ul>
<br>

<h3 id="📍-보안-터널링-목적-터널-프록시">📍 보안 터널링 목적: 터널 프록시</h3>
<ul>
<li>위치 : HTTPS 요청 시 중간에서 암호화된 트래픽 전달</li>
<li>기능 : CONNECT 메서드로 TLS 연결만 열어줌</li>
<li>활용 : HTTPS 프록시 사용 시 인증서 문제 디버깅 시 이해 필요</li>
</ul>
<br>

<h2 id="✅ㅤ어떻게-프락시가-트래픽을-처리하는가">✅ㅤ어떻게 프락시가 트래픽을 처리하는가</h2>
<blockquote>
<p>트랙픽은 클라이언트가 서버에 보내는 <strong>요청</strong> + 서버가 클라이언트에 보내는 응답이고, 프락시는 이 요청/응답을 <strong>가로채서 중계하거나 조작</strong>할 수 있다.</p>
</blockquote>
<ul>
<li>프락시는 <strong>요청 헤더, 응답 헤더, 본문</strong>을 모두 수정할 수 있다.</li>
<li>응답 본문에서 <strong>금칙어를 필터링</strong>하거나, <strong>이미지 포맷을 변경</strong>하는 것도 가능하다.</li>
</ul>
<br>

<h2 id="✅ㅤ클라이언트-프락시-설정">✅ㅤ클라이언트 프락시 설정</h2>
<h3 id="📍ㅤ브라우저-설정">📍ㅤ브라우저 설정</h3>
<ul>
<li>사용자가 <strong>수동으로 브라우저에 프락시 주소를 입력</strong></li>
<li><strong>자동 프락시 설정 URL (PAC 파일)</strong>를 사용</li>
</ul>
<h3 id="📍-운영체제-차원에서-설정">📍 운영체제 차원에서 설정</h3>
<ul>
<li>window, macOS 등에서는 시스템 전체에 적용할 <strong>전역 프락시 설정 가능</strong></li>
<li>이 경우 브라우저뿐 아니라 <code>curl</code> , <code>npm</code>, <code>git</code>, 터미널 요청 등에도 적용된다.</li>
</ul>
<h3 id="📍-환경-변수-이용">📍 환경 변수 이용</h3>
<ul>
<li><strong>CLI 환경</strong>이나 <strong>서버 스크립트에서 자주 사용</strong></li>
</ul>
<br>

<h2 id="✅ㅤ프락시-요청의-미묘한-특징">✅ㅤ프락시 요청의 미묘한 특징</h2>
<h3 id="1️⃣ㅤuri-변경은-매우-민감하다">1️⃣ㅤURI 변경은 매우 민감하다</h3>
<blockquote>
<p>프락시 서버는 클라이언트 요청을 다른 서버로 전달할 때 절대 URI를 수정하지 않아야 한다.</p>
</blockquote>
<ul>
<li><code>http://www.example.com:80/index.html</code>처럼 포트 번호를 명시적으로 추가하거나, <code>%7E</code> 같은 이스케이프 문자로 인코딩된 URL을 디코딩하거나 재인코딩하는 일은 요청이 실패하거나 의도치 않은 응답을 받게 만들 수 있다.</li>
</ul>
<blockquote>
<p>HTTP 명세: 프락시는 <strong>절대 경로를 변경해서는 안 되며</strong>, 유일한 허용은 빈 경로를 <code>/</code>로 바꾸는 것뿐이다.</p>
</blockquote>
<h3 id="2️⃣ㅤ브라우저의-호스트명-자동-완성-차이">2️⃣ㅤ브라우저의 호스트명 자동 완성 차이</h3>
<blockquote>
<p>사용자가 주소창에 <code>yahoo</code> 라고 입력하면, 프락시의 존재 여부에 따라 브라우저의 동작이 달라진다.</p>
</blockquote>
<p>📍 프락시가 없을 때</p>
<ul>
<li>브라우저는 다양한 자동 확장을 시도한다.<pre><code>www. 접두사 + .com 접미사 → www.yahoo.com</code></pre></li>
<li>로컬 DNS 서픽스 자동 검색 → <code>yahoo.company.com</code> 또는 <code>host7.oreilly.com</code> 등</li>
</ul>
<p>📍 프락시가 있을 때</p>
<ul>
<li>자동 확장이 중지된다.</li>
<li>브라우저는 그대로의 호스트명(예: <code>yahoo</code>)을 프락시에 전달한다.</li>
<li>따라서, 프락시가 직접 DNS 조회하거나 자체적으로 호스트명을 보완해야 한다.</li>
</ul>
<br>

<h2 id="✅ㅤ프락시-캐시">✅ㅤ프락시 캐시</h2>
<blockquote>
<p>클라이언트와 서버 사이에서 <strong>캐시 역할</strong>을 수행하여 동일한 요청에 대해 서버에 재요청하지 않고 저장된 응답을 제공할 수 있다.</p>
</blockquote>
<h3 id="📍-캐시의-작동-방식">📍 캐시의 작동 방식</h3>
<ul>
<li>응답 저장 : 서버로부터 받은 응답을 저장하여, 동일한 요청이 들어오면 저장된 응답을 반환</li>
<li>조건부 요청 : 저장된 응답이 최신인지 확인하기 위해 서버에 조건부 요청을 보낼 수 있다.</li>
</ul>
<h3 id="📍-캐시-제어">📍 캐시 제어</h3>
<blockquote>
<p>프락시 캐시의 동작을 제어하기 위해 다양한 HTTP 헤더를 사용할 수 있다.</p>
</blockquote>
<ul>
<li><p><code>Cache-Control</code> : 캐시 동작을 세부적으로 제어할 수 있다.</p>
<ul>
<li><code>no-cache</code> : 응답을 캐시하지 않는다.</li>
<li><code>max-age</code> : 응답의 최대 유효 시간을 지정한다.</li>
</ul>
</li>
<li><p><code>Pragma</code> : HTTP/1.0에서 사용되던 캐시 제어 헤더로, <code>no-cache</code> 값을 사용하여 캐시를 방지한다.</p>
</li>
</ul>
<blockquote>
<h3 id="✨-포인트-1">✨ 포인트</h3>
</blockquote>
<ul>
<li><strong>실시간 데이터가 필요한 API 응답</strong>에는 <code>Cache-Control: no-cache</code>를 설정하여 캐시를 방지할 수 있다.</li>
<li><strong>변경이 적은 정적 리소스</strong>에는 적절한 <code>max-age</code> 값을 설정하여 캐시를 활용할 수 있다.</li>
</ul>
<h3 id="📍-프락시-인증-방식">📍 프락시 인증 방식</h3>
<ul>
<li><code>Proxy-Authenticate</code> 헤더 : <strong>프락시 서버가 클라이언트에게</strong> 인증을 요구할 때 사용</li>
<li><code>Proxy-Authorization</code> 헤더 : <strong>클라이언트가 프락시 서버에 인증</strong> 정보를 제공할 때 사용함</li>
</ul>
<h3 id="📍-프락시-로깅">📍 프락시 로깅</h3>
<blockquote>
<p>프락시 서버는 요청 및 응답에 대한 로그를 기록하여 <strong>트래픽 분석 및 문제 해결</strong>에 활용할 수 있다.</p>
</blockquote>
<ul>
<li>요청 정보 : 클라이언트 IP, 요청 메서드, 요청 URL, 요청 시간 등</li>
<li>응답 정보 : 응답 상태 코드, 응답 시간, 응답 크기 등</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚 HTTP 완벽가이드 - 5장. 웹 서버]]></title>
            <link>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-5%EC%9E%A5.-%EC%9B%B9-%EC%84%9C%EB%B2%84</link>
            <guid>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-5%EC%9E%A5.-%EC%9B%B9-%EC%84%9C%EB%B2%84</guid>
            <pubDate>Wed, 30 Apr 2025 04:56:14 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-핵심-개념-요약">📍 핵심 개념 요약</h2>
<ul>
<li>웹 서버는 HTTP 요청을 받아 응답을 돌려주는 프로그램이다.</li>
<li>주된 역할은 리소스 제공, 요청 라우팅, 애플리케이션과 연동 등이다.</li>
<li>정적 컨텐츠 제공(HTML, CSS, JS, 이미지)과 동적 컨텐츠 처리(API, 서버 로직 실행) 모두 가능하다.</li>
<li>보안, 인증, 로깅, 캐싱, 압축 등을 통해 웹 통신을 최적화하고 관리한다.</li>
</ul>
<blockquote>
<h3 id="✨-포인트">✨ 포인트</h3>
</blockquote>
<ul>
<li>정적 리소스 제공 : 프론트엔드 빌드 결과물을 서빙하는 기본 역할</li>
<li>MIME 타입 관리 : 서버가 올바른 <code>Content-Type</code> 을 세팅해야 브라우저가 제대로 해석 가능</li>
<li>캐싱 제어 : <code>Cache-Control</code>, <code>ETag</code> 를 통해 정적 파일 업데이트 관리</li>
<li>압축 : 서버가 리소스를 압축해 전달하면 페이지 로딩 속도 형상</li>
<li>CORS 처리 : 프론트와 백엔드 분리된 구조에서는 CORS 설정 필수 고려</li>
<li>리버스 프록시 사용 : <code>Nginx</code> 등 프록시 서버를 통해 API 서버와 프론트엔드 앱을 연결</li>
<li>보안 : 모든 통신은 기본적으로 HTTPS 적용이 필수</li>
<li>서버 오류 핸들링 : <code>404</code> ,<code>500</code> 에러 처리 페이지 커스터마이징 필요</li>
</ul>
<hr>
<h2 id="✅ㅤ웹-서버의-기본-동작">✅ㅤ웹 서버의 기본 동작</h2>
<ul>
<li><strong>HTTP 요청 수신 ➡️ 요청 분석 ➡️ 적절한 응답 생성 ➡️ 전송 흐름</strong>으로 동작</li>
<li><strong>리소스를 반환</strong>하거나, 동적 애플리케이션 서버에 <strong>요청을 넘겨 처리 결과를 받아 전달</strong>한다.</li>
</ul>
<br>

<h2 id="✅ㅤ정적-컨텐츠와-동적-컨텐츠">✅ㅤ정적 컨텐츠와 동적 컨텐츠</h2>
<blockquote>
<p>빌드된 <code>React/Vue</code> 앱은 정적 파일로 서버가 서빙하지만, <strong>API 요청은 동적으로 처리</strong>된다.</p>
</blockquote>
<ul>
<li>정적 컨텐츠 : <strong>HTML, CSS, JS, 이미지</strong> 파일처럼 <strong>변하지 않는 리소스</strong></li>
<li>동적 컨텐츠 : <strong>서버 측 애플리케이션이 요청</strong>에 따라 <strong>결과를 만들어내는 리소스</strong></li>
</ul>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>웹 서버 (Web Server)</strong></th>
<th><strong>WAS (Web Application Server)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>주요 역할</strong></td>
<td>정적인 콘텐츠 제공 (HTML, CSS, JS, 이미지 등)</td>
<td>동적인 로직 처리 (비즈니스 로직, DB 연동 등)</td>
</tr>
<tr>
<td><strong>예시</strong></td>
<td>Nginx, Apache HTTP Server</td>
<td>Tomcat, JBoss, WebLogic, Jetty</td>
</tr>
<tr>
<td><strong>처리 대상</strong></td>
<td>브라우저 요청 → 정적 파일 응답</td>
<td>브라우저 요청 → 내부 로직 실행 → 응답 생성</td>
</tr>
<tr>
<td><strong>실행 방식</strong></td>
<td>요청된 파일을 그대로 전달</td>
<td>자바 서블릿, JSP, 스프링 같은 로직을 실행</td>
</tr>
<tr>
<td><strong>요청 흐름</strong></td>
<td>브라우저 → 웹 서버 → 정적 응답</td>
<td>브라우저 → 웹 서버 → WAS → 동적 응답</td>
</tr>
<tr>
<td><strong>언어 의존성</strong></td>
<td>언어 비의존적 (파일만 서빙)</td>
<td>주로 Java 기반 (Spring, JSP 등)</td>
</tr>
<tr>
<td><strong>보통의 구성</strong></td>
<td>웹 서버 + WAS 조합으로 사용 (성능, 보안, 분리 목적)</td>
<td></td>
</tr>
</tbody></table>
<blockquote>
<ol>
<li>사용자가 <code>/index.html</code> 요청</li>
</ol>
</blockquote>
<ul>
<li>웹 서버가 정적 파일을 바로 전달</li>
</ul>
<ol start="2">
<li>사용자가 <code>/user?id=1</code> 요청</li>
</ol>
<ul>
<li>웹 서버가 <strong>동적 요청임을 인식</strong></li>
<li>WAS(Spring 등)에게 요청을 전달</li>
<li>WAS가 <strong>DB에서 유저 정보를 조회</strong></li>
<li>WAS가 <strong>HTML을 생성해 웹 서버에 전달</strong></li>
<li>웹 서버가 <strong>최종 응답을 클라이언트에 전달</strong></li>
</ul>
<br>

<h2 id="✅ㅤmime-타입">✅ㅤMIME 타입</h2>
<ul>
<li>서버는 각 리소스에 대해 <strong>올바른 MIME 타입을 설정</strong>해야 한다 ➡️ <code>Content-Type</code> 헤더 </li>
</ul>
<table>
<thead>
<tr>
<th align="center">예시</th>
<th align="center">MIME 타입</th>
</tr>
</thead>
<tbody><tr>
<td align="center">HTML</td>
<td align="center"><code>text/html</code></td>
</tr>
<tr>
<td align="center">CSS</td>
<td align="center"><code>text/css</code></td>
</tr>
<tr>
<td align="center">JavaScript</td>
<td align="center"><code>application/javascript</code></td>
</tr>
<tr>
<td align="center">PNG</td>
<td align="center"><code>image/png</code></td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤ웹-서버의-추가-기능">✅ㅤ웹 서버의 추가 기능</h2>
<ul>
<li>캐싱 제어 : <code>Cache-Control</code> , <code>ETag</code> 헤더 설정</li>
<li>압축 : <code>gzip</code> , <code>Brotli</code> 압축을 통해 파일 크기 줄이기</li>
<li>보안 : <code>HTTPS</code> 적용, <code>HSTS</code> 설정</li>
<li>인증 : <code>HTTP</code> 기본 인증, 토큰 기반 인증 연동</li>
<li>로깅 및 모니터링 : 요청/응답 기록, 오류 추적</li>
</ul>
<blockquote>
<p><code>Cache-Control: no-cache</code> , <code>Cache-Control: max-age=~</code> 같은 설정으로 정적 파일 갱신 주기 조절</p>
</blockquote>
<br>

<h2 id="✅ㅤ리버스-프록시-서버">✅ㅤ리버스 프록시 서버</h2>
<blockquote>
<p>프론트 서버를 <code>/</code>에, API 서버를 <code>/api</code>로 연결하는 설정이 기본 패턴이다.</p>
</blockquote>
<ul>
<li>클라이언트 요청을 받아 실제 서버로 전달하는 중간 서버</li>
<li><code>Nginx</code> , <code>Apache</code> 가 많이 사용된다.</li>
<li><strong>로드 밸런싱, HTTPS 종단, 정적 파일 서빙, API 서버/프론트 서버 분리 연결</strong></li>
</ul>
<br>

<h2 id="✅ㅤ가상-호스팅">✅ㅤ가상 호스팅</h2>
<blockquote>
<p>스테이징 서버와 운영 서버를 같은 서버에서 운영할 때 유용</p>
</blockquote>
<ul>
<li><strong>하나의 물리적 서버에서 여러 도메인을 서비스</strong>하는 기술</li>
<li><strong>IP 주소 공유 방식</strong>으로 동작</li>
</ul>
<br>

<h2 id="✅ㅤ서버-오류-처리">✅ㅤ서버 오류 처리</h2>
<ul>
<li><code>4xx</code>, <code>5xx</code> 에러를 서버가 감지하고 적절한 에러 페이지를 응답한다.</li>
<li>커스텀 에러 페이지를 설정할 수 있다.</li>
</ul>
<blockquote>
<p>브라우저에서 커스텀 404 페이지를 띄우거나 SPA 앱에서는 <code>history fallback</code> 처리가 필요</p>
</blockquote>
<h3 id="🔎ㅤhistory-fallback">🔎ㅤHistory Fallback</h3>
<blockquote>
<p>SPA에서는 주소창에 어떤 경로를 입력해도 서버가 <strong>항상 <code>index.html</code> 을 응답</strong>하여, 클라이언트 라우터가 화면을 알아서 전환하도록 만드는 설정</p>
</blockquote>
<ul>
<li>SPA는 하나의 <code>index.html</code> 로 모든 페이지를 처리한다.</li>
<li>실제로 <code>/about</code> ,<code>/profile</code> 같은 경로에 별도 파일이 없다.</li>
<li>브라우저 주소창에 직접 경로를 입력하면 서버가 해당 파일을 찾으려고 한다 ➡️ <strong>파일 ❌ = 404 에러</strong></li>
<li>이를 막기 위해 서버가 항상 <code>index.html</code> 을 반환하도록 설정한다.</li>
<li>클라이언트 라우터가 URL을 해석해 알맞은 화면을 렌더링한다.</li>
</ul>
<br>

<h2 id="✅ㅤ리다이렉션">✅ㅤ리다이렉션</h2>
<ul>
<li>서버가 클라이언트에게 <strong>&quot;다른 URL로 가라&quot;</strong>고 알려주는 것</li>
<li>서버 응답 코드 <code>3xx</code> 로 동작한다.</li>
<li>리다이렉션 <strong>설정이 잘못되면 무한루프, SEO 문제</strong>를 일으킬 수 있다.</li>
</ul>
<table>
<thead>
<tr>
<th align="center">대표적인 예시</th>
<th align="center">후</th>
</tr>
</thead>
<tbody><tr>
<td align="center">HTTP</td>
<td align="center">HTTPS 자동 전환</td>
</tr>
<tr>
<td align="center">example.com</td>
<td align="center"><a href="http://www.example.com">www.example.com</a></td>
</tr>
<tr>
<td align="center">/old-path</td>
<td align="center">/new-paath</td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤ보안-ssltls">✅ㅤ보안 (SSL/TLS)</h2>
<ul>
<li>HTTPS 연결을 위해 서버에 인증서를 설치한다. ➡️ <strong>SSL/TLS</strong></li>
<li>클라이언트-서버 간 <strong>데이터 암호화</strong>를 보장한다.</li>
</ul>
<br>

<h2 id="✅ㅤ부하-분산-로드-밸런싱">✅ㅤ부하 분산 (로드 밸런싱)</h2>
<ul>
<li>여러 대의 서버에 트래픽을 분산시켜 성능과 안정성을 높인다.</li>
</ul>
<blockquote>
<p>프론트에서 가끔 <strong>&quot;한 번 로그인했는데, 다른 서버에서는 로그아웃 상태&quot;</strong> 와 같은 문제가 생길 수 있다. 이때, 세션 스토리지 방법이 중요하다.</p>
</blockquote>
<br>

<h2 id="✅ㅤ로깅">✅ㅤ로깅</h2>
<ul>
<li>서버는 <strong>요청과 응답에 대한 로그</strong>를 남긴다.</li>
<li>요청 시간, 요청 IP주소, 요청 URL, 응답 상태 코드, 응답 시간</li>
<li>로깅은 <strong>문제 분석, 해킹 탐지, 사용량 분석 등에 필수적</strong>이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚 HTTP 완벽가이드 - 4장. 커넥션 관리]]></title>
            <link>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-4%EC%9E%A5.-%EC%BB%A4%EB%84%A5%EC%85%98-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-4%EC%9E%A5.-%EC%BB%A4%EB%84%A5%EC%85%98-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Mon, 28 Apr 2025 08:26:05 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-핵심-개념-요약">📍 핵심 개념 요약</h2>
<ul>
<li><code>HTTP</code> 통신은 <strong>TCP 커넥션 위</strong>에서 이루어진다.</li>
<li>커넥션 관리가 제대로 안 되면 <strong>느린 속도, 리소스 낭비, 서버 과부하</strong>가 발생한다.</li>
<li><code>HTTP/1.0</code> : <strong>요청마다 새로운 커넥션을 생성 (비지속 연결)</strong></li>
<li><code>HTTP/1.1</code> : 기본적으로 <strong>커넥션을 유지 (지속 연결 - <code>keep-alive</code>)</strong></li>
<li>성능 최적화를 위해 <strong>커넥션 재사용, 커넥션 풀, 타임아웃 관리</strong>가 필요하다.</li>
<li>프록시와 게이트웨이는 클라이언트-서버 사이에서 <strong>각각의 커넥션을 따로 관리</strong>한다.</li>
<li><code>HTTP/2</code>, <code>HTTP/3</code>에서는 <strong>하나의 연결로 여러 요청을 동시에 처리</strong>한다.</li>
</ul>
<blockquote>
<h3 id="✨-포인트">✨ 포인트</h3>
</blockquote>
<ul>
<li><strong>TCP 커넥션은 비용이 크다</strong> → <strong>가능하면 재사용</strong> 해야 한다.</li>
<li><strong>서버당 병렬 커넥션 수 제한</strong> → 리소스 고갈(포트, CPU) 방지 (브라우저는 1도메인 당 6개 제한)</li>
<li>커넥션 타임아웃 설정 → <strong>유휴 커넥션을 방치하면 메모리 누수/자원 낭비 위험</strong></li>
<li>오래된 <code>Keep-alive</code> 커넥션을 주기로 정리 → 서버 부하 최소화</li>
<li><code>HTTP/1.1</code> → <code>Keep-alive</code> 주의, 파이프라이닝 거의 사용 ❌</li>
<li><code>HTTP/2~</code> → <strong>커넥션 1개로 다수 요청, 멀티플렉싱 적극 활용</strong></li>
</ul>
<hr>
<h2 id="✅ㅤ커넥션-관리">✅ㅤ커넥션 관리</h2>
<blockquote>
<p>API 서버 만들 때, 커넥션 풀 설정 ( <code>keep-alive timeout</code>, <code>idle timeout</code> ) 반드시 해야 한다.</p>
</blockquote>
<ul>
<li>HTTP 자체는 상태를 유지하지 않지만, 실제 통신은 TCP 커넥션에 의존한다.</li>
<li>커넥션을 열고 닫는 작업은 네트워크, 서버, 클라이언트 <strong>모두에게 큰 비용을 발생</strong>시킨다.</li>
<li>따라서 <strong>&quot;커넥션을 어떻게 효율적으로 유지하고, 끊고, 재사용할지&quot;</strong>를 설계하는 것이 중요하다.</li>
</ul>
<br>

<h2 id="✅ㅤ비지속-연결-http10">✅ㅤ비지속 연결 (HTTP/1.0)</h2>
<blockquote>
<p>기본은 요청 1개 → 응답 1개 → 커넥션 종료</p>
</blockquote>
<ul>
<li>매번 <code>TCP 핸드셰이크(3-way handshake)</code> 비용 발생</li>
<li><strong>많은 리소스 다운로드 시 매우 느리다.</strong></li>
<li><code>Connection: keep-alive</code> 헤더를 명시적으로 추가하면 여러 요청을 하나의 커넥션에서 처리할 수 있지만, 완벽 지원이 되지 않는 서버가 많았다.</li>
</ul>
<br>

<h2 id="✅ㅤ지속-연결-http11">✅ㅤ지속 연결 (HTTP/1.1)</h2>
<ul>
<li>기본값이 <code>keep-alive</code> 로 변경</li>
<li>커넥션을 끊지 않고 다음 요청까지 유지 → <strong>핸드셰이크 오버헤드 감소</strong></li>
<li>일정 시간 동안 추가 요청이 없으면 서버가 커넥션을 닫는다.</li>
<li>필요시 명시적으로 <code>Connection: close</code> 를 보내서 끊을 수 있다.</li>
</ul>
<blockquote>
<p>서버에서 <code>keep-alive</code> 타임아웃을 짧게 <code>5~15초</code> 정도로 잡아 서버 리소스를 절약</p>
</blockquote>
<br>

<h2 id="✅ㅤ파이프라이닝-http11">✅ㅤ파이프라이닝 (HTTP/1.1)</h2>
<ul>
<li>여러 요청을 순서대로 넣고 응답도 순서대로 받는다.</li>
<li>첫 번째 요청이 느리면, 뒤에 <strong>대기 중인 모든 응답이 지연</strong>된다 → <code>HOLB</code></li>
<li>거의 쓰이지 않았다. (브라우저도 기본으로 비활성화)</li>
</ul>
<blockquote>
<p><code>HTTP/2</code> 멀티플렉싱으로 대체되었다.</p>
</blockquote>
<br>

<h2 id="✅ㅤ병렬-커넥션의-사용">✅ㅤ병렬 커넥션의 사용</h2>
<ul>
<li>브라우저는 하나의 서버(도메인)당 <strong>최대 6개 커넥션</strong> 정도를 <strong>동시</strong>에 연다.</li>
<li>병목을 줄이고, <strong>리소스 로딩 속도를 개선</strong>하기 위해</li>
<li>과거에는 <strong>도메인 샤딩</strong> 기법으로 커넥션 수를 늘렸다.</li>
<li><code>HTTP/2</code> 이상 사용 시 <strong>도메인 샤딩 금지 ❌</strong> → 오히려 커넥션이 분산돼 성능 저하</li>
</ul>
<blockquote>
<h3 id="🔎ㅤ도메인-샤딩">🔎ㅤ도메인 샤딩</h3>
</blockquote>
<ul>
<li>리소스를 <strong>여러 도메인에 분산해서 요청</strong>하는 기술이다.</li>
<li>여러 서브도메인에 리소스를 나눠 배포해 병렬로 더 많은 리소스를 동시에 다운받을 수 있다.</li>
<li>장점 ) 페이지 로딩 속도 개선, 서버 부하 분산 가능</li>
<li>단점 ) 추가 커넥션 생성 비용 발생, 오버헤드 발생, 복잡한 리소스 관리</li>
</ul>
<br>

<h2 id="✅ㅤ커넥션-관리-전략">✅ㅤ커넥션 관리 전략</h2>
<ul>
<li>지속 커넥션 사용 <code>Keep-alive</code> 활성화</li>
<li>유후 커넥션에 <strong>적절한 타임아웃 설정</strong></li>
<li>커넥션 풀로 <strong>연결 수 제어 및 관리</strong></li>
<li>부하가 심할 경우, <strong>커넥션 수 제한과 L7 로드밸런싱</strong> 병행</li>
<li>서버 리소스를 보호하기 위해, 클라이언트 <code>Keep-alive</code> 를 <strong>무한히 허용하지 않는다.</strong></li>
</ul>
<br>

<h2 id="✅ㅤ프록시와-커넥션-관리">✅ㅤ프록시와 커넥션 관리</h2>
<ul>
<li>프록시는 클라이언트와 서버 양쪽에 <strong>각각 커넥션을 맺는다.</strong></li>
<li>프록시와 서버 간 커넥션을 계속 열어두고 <strong>다수 클라이언트 요청을 처리</strong>할 수 있다.</li>
<li>프록시가 <strong>커넥션 풀을 적극적으로 관리해 성능 최적화</strong>를 한다.</li>
</ul>
<br>

<h2 id="✅ㅤhttp2--http3의-등장">✅ㅤHTTP/2 &amp; HTTP/3의 등장</h2>
<h3 id="📍-http2">📍 HTTP/2</h3>
<ul>
<li>하나의 TCP 커넥션에서 <strong>여러 요청/응답을 동시에 처리</strong></li>
<li>헤더 압축으로 네트워크 최적화</li>
</ul>
<h3 id="📍-http3">📍 HTTP/3</h3>
<ul>
<li>TCP 대신 <strong>UDP 기반</strong> <code>QUIC 프로토콜</code> 사용</li>
<li><strong>핸드셰이크 비용 감소, 리커넥트 속도 대폭 개선</strong></li>
</ul>
<blockquote>
<p>HTTP 커넥션 관리는 <strong>서비스 성능과 서버 안정성의 핵심</strong>이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚 HTTP 완벽가이드 - 3장. HTTP 메시지]]></title>
            <link>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-3%EC%9E%A5.-HTTP-%EB%A9%94%EC%8B%9C%EC%A7%80</link>
            <guid>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-3%EC%9E%A5.-HTTP-%EB%A9%94%EC%8B%9C%EC%A7%80</guid>
            <pubDate>Wed, 16 Apr 2025 02:00:19 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-핵심-개념-요약">📍 핵심 개념 요약</h2>
<ul>
<li>HTTP 메시지 구조 : 시작줄 → 헤더 → 본문 순서의 데이터 블록</li>
<li>요청 메시지 : <code>&lt;메서드&gt; &lt;요청 URL&gt; &lt;버전&gt;</code> 으로 시작</li>
<li>응답 메시지 : <code>&lt;버전&gt; &lt;상태 코드&gt; &lt;사유 구절&gt;</code> 로 시작</li>
<li>헤더 : 요청/응답에 대한 부가정보 전달 (key:value 형태)</li>
<li>상태 코드 : 응답 결과를 1xx~5xx로 분류하여 나타냄</li>
<li>CORS 관련 헤더 : 출처 제한, 인증 처리 등을 제어하는 보안 관련 필드</li>
</ul>
<blockquote>
<h3 id="✨-포인트">✨ 포인트</h3>
</blockquote>
<ul>
<li><code>PUT</code> : 전체 수정 / <code>PATCH</code> : 일부 수정</li>
<li>로그인 상태 유지 → 토큰/JWT/쿠키 + Authorization 헤더 조합 필요</li>
<li><code>Content-Type: application/json</code> → JSON 형식 명시</li>
<li><code>Authorization: Bearer &lt;token&gt;</code> → 인증 정보 포함</li>
<li><code>Accept</code> → 받고 싶은 응답 포맷 설정</li>
</ul>
<hr>
<h2 id="✅ㅤ메시지의-흐름">✅ㅤ메시지의 흐름</h2>
<blockquote>
<p>HTTP가 인터넷의 배달원이라면, HTTP 메시지는 소포와 같다.
데이터를 담은 메시지는 클라이언트 ↔️ 프락시 ↔️ 서버 사이를 흐르며 정보를 교환한다.</p>
</blockquote>
<ul>
<li><strong>시작줄 → 헤더 → 본문</strong> 순으로 구성</li>
<li>인바운드 : 서버를 향함</li>
<li>아웃바운드 : 사용자 에이전트로 돌아옴</li>
<li>업스트림/다운스트림 : 네트워크의 방향을 나타냄</li>
</ul>
<br>

<h2 id="✅ㅤ메시지-문법">✅ㅤ메시지 문법</h2>
<h3 id="📌-요청-메시지-구조">📌 요청 메시지 구조</h3>
<pre><code class="language-php-template">&lt;메서드&gt; &lt;요청 URL&gt; &lt;버전&gt;
&lt;헤더&gt;
&lt;본문&gt;</code></pre>
<h3 id="📌-응답-메시지-구조">📌 응답 메시지 구조</h3>
<pre><code class="language-php-template">&lt;버전&gt; &lt;상태 코드&gt; &lt;사유 구절&gt;
&lt;헤더&gt;
&lt;본문&gt;</code></pre>
<blockquote>
<p>본문이 없더라도, 헤더 블록은 반드시 <strong>CRLF(빈 줄)</strong>로 끝나야 한다.</p>
</blockquote>
<br>

<h2 id="✅ㅤ메서드">✅ㅤ메서드</h2>
<table>
<thead>
<tr>
<th align="center">메서드</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">GET</td>
<td align="center">문서 요청</td>
</tr>
<tr>
<td align="center">HEAD</td>
<td align="center">헤더만 요청</td>
</tr>
<tr>
<td align="center">POST</td>
<td align="center">데이터 전송</td>
</tr>
<tr>
<td align="center">PUT</td>
<td align="center">리소스 생성/교체</td>
</tr>
<tr>
<td align="center">DELETE</td>
<td align="center">리소스 삭제</td>
</tr>
<tr>
<td align="center">OPTIONS</td>
<td align="center">지원 메서드 조회</td>
</tr>
<tr>
<td align="center">TRACE</td>
<td align="center">요청 경로 추적</td>
</tr>
<tr>
<td align="center">...</td>
<td align="center">확장 메서드 (LOCK, COPY 등)</td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤ헤더">✅ㅤ헤더</h2>
<table>
<thead>
<tr>
<th align="center">헤더</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Content-Type</td>
<td align="center">본문 데이터 형식</td>
</tr>
<tr>
<td align="center">Accept</td>
<td align="center">클라이언트가 원하는 응답 형식</td>
</tr>
<tr>
<td align="center">Authorization</td>
<td align="center">인증 정보 (토큰 등)</td>
</tr>
<tr>
<td align="center">Cookie / Set-Cookie</td>
<td align="center">세션 유지</td>
</tr>
<tr>
<td align="center">Connection</td>
<td align="center">연결 유지 (keep-alive, close)</td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤ본문">✅ㅤ본문</h2>
<ul>
<li>전송할 실제 데이터</li>
<li><code>POST</code> , <code>PUT</code> 등에서 주로 사용</li>
<li>프론트엔드에서는 보통 <code>JSON.stringify()</code> 로 감싼 JSON 데이터를 전송<pre><code class="language-js">body: JSON.stringify({ name: &#39;Bo00oa&#39; })</code></pre>
</li>
</ul>
<br>

<h2 id="✅ㅤ상태코드">✅ㅤ상태코드</h2>
<table>
<thead>
<tr>
<th align="center">범위</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">1xx</td>
<td align="center">정보 (Processing 중)</td>
</tr>
<tr>
<td align="center">2xx</td>
<td align="center">성공</td>
</tr>
<tr>
<td align="center">3xx</td>
<td align="center">리다이렉션</td>
</tr>
<tr>
<td align="center">4xx</td>
<td align="center">클라이언트 오류</td>
</tr>
<tr>
<td align="center">5xx</td>
<td align="center">서버 오류</td>
</tr>
</tbody></table>
<h3 id="📌-주요-상태-코드">📌 주요 상태 코드</h3>
<table>
<thead>
<tr>
<th align="center">코드</th>
<th align="center">의미</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">200</td>
<td align="center">OK</td>
<td align="center">요청 성공</td>
</tr>
<tr>
<td align="center">201</td>
<td align="center">Created</td>
<td align="center">리소스 생성됨</td>
</tr>
<tr>
<td align="center">204</td>
<td align="center">No Content</td>
<td align="center">본문 없음</td>
</tr>
<tr>
<td align="center">301</td>
<td align="center">Moved Permanently</td>
<td align="center">영구 이동</td>
</tr>
<tr>
<td align="center">400</td>
<td align="center">Bad Request</td>
<td align="center">잘못된 요청</td>
</tr>
<tr>
<td align="center">401</td>
<td align="center">Unauthorized</td>
<td align="center">인증 필요</td>
</tr>
<tr>
<td align="center">403</td>
<td align="center">Forbidden</td>
<td align="center">접근 거부</td>
</tr>
<tr>
<td align="center">404</td>
<td align="center">Not Found</td>
<td align="center">리소스 없음</td>
</tr>
<tr>
<td align="center">405</td>
<td align="center">Method Not Allowed</td>
<td align="center">허용되지 않은 메서드</td>
</tr>
<tr>
<td align="center">500</td>
<td align="center">Internal Server Error</td>
<td align="center">서버 오류</td>
</tr>
<tr>
<td align="center">502</td>
<td align="center">Bad Gateway</td>
<td align="center">게이트웨이 오류</td>
</tr>
<tr>
<td align="center">503</td>
<td align="center">Service Unavailable</td>
<td align="center">서비스 불가</td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤcors-관련-헤더">✅ㅤCORS 관련 헤더</h2>
<table>
<thead>
<tr>
<th align="center">헤더</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Access-Control-Allow-Origin</td>
<td align="center">허용된 출처</td>
</tr>
<tr>
<td align="center">Access-Control-Allow-Methods</td>
<td align="center">허용된 HTTP 메서드</td>
</tr>
<tr>
<td align="center">Access-Control-Allow-Headers</td>
<td align="center">허용된 요청 헤더</td>
</tr>
<tr>
<td align="center">Access-Control-Allow-Credentials</td>
<td align="center">쿠키 포함 여부 (true)</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚 HTTP 완벽가이드 - 2장. URL과 리소스]]></title>
            <link>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-2%EC%9E%A5.-URL%EA%B3%BC-%EB%A6%AC%EC%86%8C%EC%8A%A4</link>
            <guid>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-2%EC%9E%A5.-URL%EA%B3%BC-%EB%A6%AC%EC%86%8C%EC%8A%A4</guid>
            <pubDate>Wed, 16 Apr 2025 01:11:15 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-핵심-개념-요약">📍 핵심 개념 요약</h2>
<ul>
<li><code>URL</code> : Uniform Resource Locator - 웹 리소스의 주소</li>
<li><code>URI</code> : Uniform Resource Identifier - URL을 포함하는 상위 개념(이름 또는 위치로 식별)</li>
<li><strong>스킴</strong> : <code>http://</code> 와 같은 통신 프로토콜 지정</li>
<li>호스트 : 도메인 또는 IP 주소</li>
<li>경로 : 리소스의 위치</li>
<li>쿼리 스트링 : <code>?key=value</code> 형식의 추가 파라미터</li>
<li>프래그먼트 : <code>#section1</code> - 브라우저 내 특정 위치 이동 (서버에는 전송 ❌)</li>
</ul>
<pre><code>&lt;스킴&gt;://&lt;사용자 이름&gt;:&lt;비밀번호&gt;@&lt;호스트&gt;:&lt;포트&gt;/&lt;경로&gt;;&lt;파라미터&gt;?&lt;질의&gt;#&lt;프래그먼트&gt;</code></pre><blockquote>
<h3 id="✨-포인트">✨ 포인트</h3>
</blockquote>
<ul>
<li>쿼리 파라미터는 검색, 필터링 등에 자주 활용</li>
<li>브라우저 라우팅(SPA)에서 <code>#</code> 사용 여부는 <strong>해시 라우터</strong> VS <strong>히스토리 라우터</strong> 기준</li>
</ul>
<hr>
<h2 id="✅ㅤurl은-단순한-위치가-아닌-식별자">✅ㅤURL은 단순한 위치가 아닌 식별자</h2>
<ul>
<li>자원은 이름을 통해 식별된다.</li>
<li><code>URL</code> 은 리소스의 주소이자 이름이며, 웹에서 리소스를 식별하기 위한 표준적인 방법</li>
<li><code>URL</code> 은 단순한 <strong>위치</strong>가 아니라 자원의 <strong>고유한 식별자</strong></li>
<li>클라이언트와 서버가 리소스를 주고받기 위해 사용하는 공통 언어</li>
</ul>
<br>

<h2 id="✅ㅤurluriurn의-관계">✅ㅤURL/URI/URN의 관계</h2>
<blockquote>
<p>URL은 URI의 하위 개념, URI는 다시 두 가지로 나뉜다</p>
</blockquote>
<blockquote>
<p><code>URL (Locator)</code>: 리소스의 위치를 명시 → <code>https://example.com/index.html</code>
<code>URN (Name)</code>: 리소스의 고유 이름을 명시 → <code>urn:isbn:0451450523</code></p>
</blockquote>
<table>
<thead>
<tr>
<th align="center">구분</th>
<th align="center">설명</th>
<th align="center">예시</th>
</tr>
</thead>
<tbody><tr>
<td align="center">URI (Identifier)</td>
<td align="center">리소스를 식별하는 모든 것</td>
<td align="center"><a href="https://example.com/index.html">https://example.com/index.html</a>, urn:isbn:0451450523</td>
</tr>
<tr>
<td align="center">URL (Locator)</td>
<td align="center">리소스의 위치를 나타냄</td>
<td align="center"><a href="https://example.com/index.html">https://example.com/index.html</a></td>
</tr>
<tr>
<td align="center">URN (Name)</td>
<td align="center">리소스의 고유 이름을 나타냄</td>
<td align="center">urn:isbn:0451450523</td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤ호스트와-포트">✅ㅤ호스트와 포트</h2>
<h3 id="1️⃣-호스트">1️⃣ 호스트</h3>
<ul>
<li>리소스를 호스팅하는 인터넷 상의 장비를 의미</li>
<li>일반적으로 도메인 이름 또는 IP 주소로 표현</li>
<li>클라이언트는 이 정보를 통해 어떤 서버에 요청을 보낼지 판단</li>
</ul>
<h3 id="2️⃣-포트">2️⃣ 포트</h3>
<ul>
<li>서버 내부에서 리소스를 제공하는 프로그램이 사용하는 네트워크 포트</li>
<li>HTTP의 기본 포트는 <code>80</code> , HTTPS는 <code>443</code></li>
<li>포트는 명시하지 않으면 기본값을 사용하지만, 필요 시 명시할 수 있다.</li>
</ul>
<br>

<h2 id="✅ㅤ사용자-이름과-비밀번호">✅ㅤ사용자 이름과 비밀번호</h2>
<ul>
<li>일부 URL 스킴(특히 FTP 등)은 <strong>인증 정보를 포함</strong>할 수 있다.</li>
<li>URL에 포함된 형식 : <code>ftp://username:password@host/path</code></li>
<li>생략 시, 기본 사용자 이름과 브라우저의 기본 비밀번호를 사용하는 경우도 있다.</li>
<li>보안 문제로 인해 <strong>현대 웹에서는 거의 사용되지 않으며, HTTPS에서는 권장되지 않는다.</strong><br>

</li>
</ul>
<h2 id="✅ㅤ경로-path">✅ㅤ경로 (path)</h2>
<ul>
<li>서버 내에서 리소스가 어디에 위치하는지를 나타냄</li>
<li><code>/</code> 를 기준으로 여러 개의 경로 조각으로 나뉜다.</li>
<li>각 경로 조각은 개별적인 파라미터 컴포넌트를 가질 수 있다. (하지만 자주 사용되진 않음)</li>
</ul>
<br>

<h2 id="✅ㅤ파라미터">✅ㅤ파라미터</h2>
<ul>
<li>일부 스킴에서는 단순한 경로만으로는 충분하지 않다.</li>
<li>프로토콜이나 서버 동작에 필요한 <strong>추가 정보(파라미터)</strong>를 포함한다.</li>
<li>보통 각 경로 조각에 <code>;</code>을 붙여 사용한다. <code>/books;type=ebook;lang=en</code></li>
</ul>
<blockquote>
<p>하지만 실무에서는 RESTful URL 설계나 라우팅에서 거의 사용되지 않고 과거 프로토콜(FTP 등)에서 더 많이 쓰였다.</p>
</blockquote>
<br>

<h2 id="✅ㅤ쿼리-스트링">✅ㅤ쿼리 스트링</h2>
<ul>
<li>URL의 <code>?</code> 다음에 위치하며, 서버에 추가 정보나 옵션을 전달할 때 사용</li>
<li><code>이름=값</code> 쌍의 리스트 형태로, <code>&amp;</code> 로 구분</li>
<li>주로 <strong>게이트웨이나 웹 애플리케이션 서버에서 정보를 전달받을 떄 사용</strong><br>

</li>
</ul>
<h2 id="✅ㅤ프래그먼트">✅ㅤ프래그먼트</h2>
<ul>
<li><code>#</code> 뒤에 위치하며, 리소스 내부의 특정 위치를 가리킨다.</li>
<li>서버에는 전달되지 않아서 서버는 항상 전체 리소스를 응답한다.</li>
<li>클라이언트가 리소스를 받은 후, <strong>프래그먼트를 기준으로 해당 위치로 스크롤하거나 표시</strong> 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚 HTTP 완벽 가이드 - 1장.  웹의 기초]]></title>
            <link>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-1%EC%9E%A5.-%EC%9B%B9%EC%9D%98-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@swallowed_o0/HTTP-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-1%EC%9E%A5.-%EC%9B%B9%EC%9D%98-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Wed, 16 Apr 2025 00:31:40 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-핵심-개념-요약">📍 핵심 개념 요약</h2>
<ul>
<li><code>HTTP</code> : HyperText Transfer Protocol - 웹의 요청/응답 통신 규칙</li>
<li>리소스 : URI로 식별 가능한 웹 상의 모든 대상 (HTML, 이미지, JSON 등)</li>
<li>요청/응답 : <code>HTTP</code> 는 요청-응답 기반의 통신 구조</li>
</ul>
<blockquote>
<h3 id="✨-포인트">✨ 포인트</h3>
</blockquote>
<ul>
<li><code>HTTP</code> 는 비연결성 : 요청 간 상태를 유지하지 않는다. </li>
<li>로그인은 <code>토큰, 세션, 쿠키</code> 로 해결</li>
<li>프론트 코드에서 API 요청 → <code>HTTP</code> 요청의 시작</li>
<li><code>fetch</code> , <code>axios</code> 등은 결국 <code>HTTP</code> 요청을 보내는 도구!</li>
</ul>
<hr>
<h2 id="✅ㅤhttp의-정의">✅ㅤHTTP의 정의</h2>
<blockquote>
<p>웹에서 HTML 문서를 주고받기 위해 만들어진 애플리케이션 계층의 통신 규약</p>
</blockquote>
<h3 id="🔎ㅤhttp의-구조">🔎ㅤHTTP의 구조</h3>
<ul>
<li>애플리케이션 계층 : OSI 7계층  중 가장 위에서 동작, TCP/IP 위에서 동작하며 사용자는 직접 전함</li>
</ul>
<h3 id="🔎ㅤhttp-메시지-구조">🔎ㅤHTTP 메시지 구조</h3>
<h3 id="📤-요청-request">📤 요청 (request)</h3>
<pre><code class="language-http">GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Chrome/124.0</code></pre>
<ul>
<li>시작줄 : 메서드 / 경로 / 프로토콜</li>
<li>헤더 : 요청에 대한 부가 정보</li>
<li>본문 : <code>POST</code> / <code>PUT</code> 등에서 전송할 데이터 포함</li>
</ul>
<h3 id="📥-응답-response">📥 응답 (response)</h3>
<pre><code class="language-http">HTTP/1.1 200 OK
Content-Type: text/html

&lt;html&gt;...&lt;/html&gt;</code></pre>
<ul>
<li>상태줄 : HTTP 버전 + 상태코드 + 메시지</li>
<li>헤더 : 응답 관련 정보</li>
<li>본문 : 실제 클라이언트에게 보여줄 내용</li>
</ul>
<br>

<h2 id="✅ㅤhttp와-tcpip">✅ㅤHTTP와 TCP/IP</h2>
<blockquote>
<p><code>HTTP</code> 는 TCP 위에서 동작하며, 데이터를 안전하게 전송하기 위해 3-way 핸드세이크를 이용</p>
</blockquote>
<h3 id="포트-번호">포트 번호</h3>
<ul>
<li><code>HTTP</code> → <strong>80</strong>번 포트</li>
<li><code>HTTPS</code> → <strong>443</strong>번 포트</li>
</ul>
<br>

<h2 id="✅ㅤurl과-uri의-차이">✅ㅤURL과 URI의 차이</h2>
<table>
<thead>
<tr>
<th align="center">용어</th>
<th align="center">의미</th>
<th align="center">예시</th>
</tr>
</thead>
<tbody><tr>
<td align="center">URI</td>
<td align="center">자원의 위치/이름을 식별하는 모든 방식</td>
<td align="center">mailto:<a href="mailto:someone@example.com">someone@example.com</a></td>
</tr>
<tr>
<td align="center">URL</td>
<td align="center">자원의 위치 + 접근 방법 포함</td>
<td align="center"><a href="https://example.com/index.html">https://example.com/index.html</a></td>
</tr>
<tr>
<td align="center">URN</td>
<td align="center">자원의 이름만 명시, 위치는 없음</td>
<td align="center">urn:isbn:0451450523</td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤhttp-요청-메서드">✅ㅤHTTP 요청 메서드</h2>
<table>
<thead>
<tr>
<th align="center">메서드</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">GET</td>
<td align="center">데이터 조회</td>
</tr>
<tr>
<td align="center">POST</td>
<td align="center">데이터 생성</td>
</tr>
<tr>
<td align="center">PUT</td>
<td align="center">데이터 전체 수정</td>
</tr>
<tr>
<td align="center">PATCH</td>
<td align="center">데이터 일부 수정</td>
</tr>
<tr>
<td align="center">DELETE</td>
<td align="center">데이터 삭제</td>
</tr>
<tr>
<td align="center">HEAD</td>
<td align="center">본문 제외하고 헤더만 요청</td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤhttp-상태-코드">✅ㅤHTTP 상태 코드</h2>
<table>
<thead>
<tr>
<th align="center">코드</th>
<th align="center">의미</th>
</tr>
</thead>
<tbody><tr>
<td align="center">1xx</td>
<td align="center">정보 (처리 중)</td>
</tr>
<tr>
<td align="center">2xx</td>
<td align="center">성공 (ex. 200 OK, 201 Created)</td>
</tr>
<tr>
<td align="center">3xx</td>
<td align="center">리다이렉션 (ex. 301 Moved Permanently, 302 Found)</td>
</tr>
<tr>
<td align="center">4xx</td>
<td align="center">클라이언트 오류 (ex. 400 Bad Request, 404 Not Found)</td>
</tr>
<tr>
<td align="center">5xx</td>
<td align="center">서버 오류 (ex. 500 Internal Server Error)</td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤmime-타입-content-type">✅ㅤMIME 타입 (Content-Type)</h2>
<blockquote>
<p>클라이언트와 서버가 전송하는 데이터의 종류를 명시</p>
</blockquote>
<table>
<thead>
<tr>
<th align="center">MIME 타입</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">text/html</td>
<td align="center">HTML 문서</td>
</tr>
<tr>
<td align="center">application/json</td>
<td align="center">JSON 데이터</td>
</tr>
<tr>
<td align="center">text/css</td>
<td align="center">CSS 스타일</td>
</tr>
<tr>
<td align="center">image/png</td>
<td align="center">PNG 이미지</td>
</tr>
<tr>
<td align="center">video/mp4</td>
<td align="center">MP4 영상</td>
</tr>
</tbody></table>
<br>

<h2 id="✅ㅤ상태-비저장성">✅ㅤ상태 비저장성</h2>
<ul>
<li><code>HTTP</code> 는 기본적으로 상태 정보를 유지하지 않는다.</li>
<li>요청 간 연결된 상태를 유지하지 않기 때문에, 매 요청마다 모든 정보 포함 필요</li>
<li>쿠키, 세션, 토큰 등을 통해 상태 유지를 보완한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ RoomE 아카이브 ] #4. 나의 Dev-Log : 트러블슈팅_헥사곤 그리드 배치]]></title>
            <link>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-4.-%EB%82%98%EC%9D%98-Dev-Log-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%ED%97%A5%EC%82%AC%EA%B3%A4-%EA%B7%B8%EB%A6%AC%EB%93%9C-%EB%B0%B0%EC%B9%98</link>
            <guid>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-4.-%EB%82%98%EC%9D%98-Dev-Log-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%ED%97%A5%EC%82%AC%EA%B3%A4-%EA%B7%B8%EB%A6%AC%EB%93%9C-%EB%B0%B0%EC%B9%98</guid>
            <pubDate>Mon, 31 Mar 2025 08:30:25 GMT</pubDate>
            <description><![CDATA[<h2 id="📌ㅤ헥사곤-그리드-배치">📌ㅤ헥사곤 그리드 배치</h2>
<blockquote>
<h3 id="📍ㅤ제약-및-구현-조건">📍ㅤ제약 및 구현 조건</h3>
</blockquote>
<ul>
<li>방들이 <strong>겹치거나 중간에 빈 공간이 생기지 않아야 한다.</strong></li>
<li><strong>규칙성 있게 확장</strong>되어야 하며, <strong>나의 방은 항상 중앙</strong>에 위치해야 한다.</li>
<li><strong>헥사곤 좌표계를 이용</strong>하여 배치한다.</li>
</ul>
<br>



<h2 id="🚨-10번째-방부터-중복-배치되는-현상">🚨 10번째 방부터 중복 배치되는 현상</h2>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/1b50ba7a-befb-423f-aaa2-9123b2018baf/image.png" width=70%>ㅤ</p>
<blockquote>
<h3 id="📍ㅤ문제-정의">📍ㅤ문제 정의</h3>
<p>헥사곤 그리드를 사용하여 방을 배치하는 과정에서 <strong>10번째 방 이후부터 중복 배치되는 현상</strong>이 발생했다. <strong>육각형 그리드의 기하학적 특성과 좌표계의 일관성을 보장하지 못한 좌표 배치 로직에서 발생</strong>했다.</p>
</blockquote>
<blockquote>
<h3 id="📎ㅤ헥사곤-그리드의-좌표-시스템">📎ㅤ헥사곤 그리드의 좌표 시스템</h3>
<p>헥사곤 그리드를 코드로 구현하려면, <strong>좌표 시스템이 필요</strong>하다. 여러가지 좌표 시스템이 있지만, <strong>큐브 좌표계가 직관적이고 계산이 쉬워서 이를 활용</strong>했다.</p>
</blockquote>
<h3 id="✅ㅤ큐브-좌표계">✅ㅤ큐브 좌표계</h3>
<ul>
<li><p>형식 : <code>[q, r, s]</code></p>
</li>
<li><p>항상 <code>q + r + s = 0</code> 조건을 만족해야 한다.</p>
</li>
<li><p>3개의 축(q, r, s)을 사용하며, 각 축은 6방향 중 3개의 방향을 표현</p>
</li>
<li><p>장점 : 방향 계산이 직관적이며, 인접 셀 계산이 쉬움.</p>
<p>  중앙: [0, 0, 0] | 왼쪽: [-1, 0, 1] | 오른쪽: [1, 0, -1]</p>
</li>
</ul>
 <br>

<blockquote>
<h3 id="🧐ㅤ문제-분석">🧐ㅤ문제 분석</h3>
</blockquote>
<ul>
<li><code>q + r + s = 0</code> 조건을 만족하지 않는 좌표가 생성</li>
<li><code>visited</code> Set을 활용했으나, 잘못된 위치에서도 중복 방이 배치</li>
</ul>
<pre><code class="language-jsx">const directions = [
  [1, -1, 0],    // 6. 위 오른쪽 대각선 상단
  [0, -1, 1],   // 5. 위 왼쪽 대각선 상단
  [0, 1, 1],    // 4. 아래 왼쪽 대각선 하단
  [-1, 1, 1],    // 3. 아래 오른쪽 대각선 하단
  [-1, 0, 1],    // 2. 왼쪽
  [1, 0, -1],    // 1. 오른쪽
];</code></pre>
<pre><code class="language-jsx">for (let step = 0; step &lt; steps &amp;&amp; result.length &lt; roomCount; step++) {
  const posKey = `${cubeX},${cubeY},${cubeZ}`;

  if (!visited.has(posKey)) {  // ✅ 방문 여부만 확인
    visited.add(posKey);
    result.push({ position: [cubeX, cubeY, cubeZ], room: rooms[roomIndex] });
    roomIndex++;
  }

  cubeX += directions[side][0];
  cubeY += directions[side][1];
  cubeZ += directions[side][2];
}</code></pre>
<ul>
<li>일부 <code>dx, dy, dz</code> 값이 <code>q + r + s = 0</code> 조건을 만족하지 않음</li>
<li>그로인해 <strong>헥사곤 그리드 내에서 유효한 위치로 간주되지 않음ㅤ➡️ㅤ중복 체크가 제대로 되지 않음</strong></li>
<li><code>visited.has(posKey)</code> 만으로는 <strong>잘못된 위치의 중복 배치를 방지할 수 없음</strong></li>
</ul>
<blockquote>
<h3 id="✅ㅤ해결">✅ㅤ해결</h3>
<ul>
<li>큐브 좌표계의 <strong>조건을 유지하는 방향 벡터로 수정</strong></li>
</ul>
</blockquote>
<ul>
<li><strong>중복 방지를 강화</strong></li>
<li><strong>배치 순서를 수정</strong>하여 순환 배치를 개선</li>
</ul>
<pre><code class="language-jsx">const directions = [
  [-1, 0, 1],    // 1. 왼쪽
  [1, 0, -1],    // 2. 오른쪽
  [0, -1, 1],   // 3. 위 왼쪽 대각선 상단
  [1, -1, 0],    // 4. 위 오른쪽 대각선 상단
  [-1, 1, 0],    // 5. 아래 왼쪽 대각선 하단
  [0, 1, -1],    // 6. 아래 오른쪽 대각선 하단
];</code></pre>
<pre><code class="language-jsx">for (let step = 0; step &lt; (ring - 1) &amp;&amp; placedInRing &lt; positionsInRing; step++) {
  const [x, y, z] = currentPos;
  const posKey = `${x},${y},${z}`;

  if (!visited.has(posKey) &amp;&amp; (x + y + z) === 0) {
    visited.add(posKey);
    result.push({ position: [x, y, z], room: rooms[roomIndex] });
    roomIndex++;
    placedInRing++;
  }

  currentPos = [x + dx, y + dy, z + dz];
}
</code></pre>
<br>

<h2 id="🚨-18번째-방부터-아래쪽에서만-배치되는-현상">🚨 18번째 방부터 아래쪽에서만 배치되는 현상</h2>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/1c15ab02-4b20-4e3d-83fc-de47aad8b10e/image.png" width=70%>ㅤ</p>
<blockquote>
<h3 id="📍ㅤ문제-정의-1">📍ㅤ문제 정의</h3>
<p>헥사곤 그리드를 확장하면서 <strong>18번째 방 이후부터 배치 방향이 한쪽으로만 치우치는 현상</strong>이 발생했다.</p>
</blockquote>
<blockquote>
<h3 id="🧐ㅤ문제-분석-1">🧐ㅤ문제 분석</h3>
</blockquote>
<ul>
<li><code>baseIndex</code> 가 <strong>단일 위치로 고정</strong>되어 있음</li>
<li>새로운 위치를 찾을 때, <strong>이전 링의 첫 번째 요소만 기준으로 사용</strong>하여 의도대로 안 이루어짐</li>
<li>그렇게 <strong>일부 방향에서 배치가 누락되면서 아래쪽으로만 방이 추가</strong></li>
</ul>
<pre><code class="language-jsx">const baseIndex = previousRing[directionIndex % previousRing.length];
const basePos = result[baseIndex].position;
const [dx, dy, dz] = dir;
const newPos: Position = [basePos[0] + dx, basePos[1] + dy, basePos[2] + dz];
const posKey = `${newPos[0]},${newPos[1]},${newPos[2]}`;

if (!visited.has(posKey) &amp;&amp; newPos[0] + newPos[1] + newPos[2] === 0) {
  visited.add(posKey);
  result.push({ position: newPos, room: rooms[roomIndex] });
  return { roomIndex: roomIndex + 1, placedInRing: placedInRing + 1 };
}</code></pre>
<blockquote>
<h3 id="✅ㅤ해결-1">✅ㅤ해결</h3>
<ul>
<li><code>baseIndex</code> 를 단일 위치가 아닌, 이전 링의 모든 위치를 순회하며 유효한 새 위치를 찾도록 수정</li>
</ul>
</blockquote>
<ul>
<li><code>(directionIndex + i) % previousRing.length</code> 로 시작점을 조정하여 방형별 배치가 누락되지 않도록 개선</li>
</ul>
<pre><code class="language-jsx">for (let i = 0; i &lt; previousRing.length; i++) {
  const baseIndex = previousRing[(directionIndex + i) % previousRing.length];
  const basePos = result[baseIndex].position;
  const [dx, dy, dz] = dir;
  const newPos: Position = [basePos[0] + dx, basePos[1] + dy, basePos[2] + dz];
  const posKey = `${newPos[0]},${newPos[1]},${newPos[2]}`;

  if (!visited.has(posKey) &amp;&amp; newPos[0] + newPos[1] + newPos[2] === 0) {
    visited.add(posKey);
    result.push({ position: newPos, room: rooms[roomIndex] });
    return { roomIndex: roomIndex + 1, placedInRing: placedInRing + 1 };
  }
}
return { roomIndex, placedInRing };</code></pre>
<br>

<hr>
<h2 id="💬ㅤ비하인드">💬ㅤ비하인드</h2>
<blockquote>
<p>어지러운 <code>x,y,z</code> 좌표계.... 최상의 배치를 맞추기 위한 노력...</p>
</blockquote>
<table>
<thead>
<tr>
<th>Front View</th>
<th>Top View</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/9556073c-b5f8-4559-9259-a73b4d40fc7b/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/e43057ea-9807-43cb-bfde-2a4f270eb2c7/image.png" alt=""></td>
</tr>
</tbody></table>
<br>

<blockquote>
<p>첫 번째 링을 완성하기 까지의 여정...</p>
</blockquote>
<table>
<thead>
<tr>
<th>문어발 스타일</th>
<th>아래는 왜...</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/12703916-70cc-4f24-b0e8-4eb9fd474eff/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/6f49ed0f-0252-407d-ad53-c90d49defdc6/image.png" alt=""></td>
</tr>
</tbody></table>
<br>

<blockquote>
<p>첫 번째 링 이후의 방황하는 방들... </p>
</blockquote>
<table>
<thead>
<tr>
<th>#1</th>
<th>#2</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/82985715-fb47-4529-aae6-dbc92292baeb/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/40ae4b56-a022-4a52-9332-a5bcc32c8ad4/image.png" alt=""></td>
</tr>
</tbody></table>
<br>

<blockquote>
<p>여러가지 확장 방식을 시도해보면서 겪은 다양한 형태</p>
</blockquote>
<table>
<thead>
<tr>
<th>문어발 스타일</th>
<th>아래는 왜...</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/f73bcdd4-d585-435e-afa7-452b94fdd5bd/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/1e0bb7e5-0c53-47aa-a3d6-b9ecabe16f51/image.png" alt=""></td>
</tr>
</tbody></table>
<br>

]]></description>
        </item>
        <item>
            <title><![CDATA[[ RoomE 아카이브 ] #2. 나의 Dev-Log : Three.js를 활용한 3D 모델 렌더링 및 구현]]></title>
            <link>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-2.-%EB%82%98%EC%9D%98-Dev-Log-Three.js%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-3D-%EB%AA%A8%EB%8D%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%B0%8F-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-2.-%EB%82%98%EC%9D%98-Dev-Log-Three.js%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-3D-%EB%AA%A8%EB%8D%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%B0%8F-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 31 Mar 2025 08:24:34 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-threejs-란">📍 Three.js 란?</h2>
<blockquote>
<p><code>WebGL</code> 을 기반으로 한 JavaScript 라이브러리로, 웹 브라우저에서 3D 그래픽을 쉽게 렌더링할 수 있도록 도와준다. 기본적인 원리로는 <code>WebGL</code>을 직접 다루는 복잡한 과정을 추상화하여 사용자가 간단한 코드로도 강력한 3D 환경을 구현할 수 있게 해준다.</p>
</blockquote>
<blockquote>
<h3 id="📎-threejs의-핵심-개념">📎 Three.js의 핵심 개념</h3>
</blockquote>
<ul>
<li><code>Scene</code> : 모든 3D 객체가 배치되는 공간</li>
<li><code>Camera</code> : 장면을 어떤 위치와 구조로 담아낼지 결정</li>
<li><code>Renderer</code> : 3D 장면을 캔버스에 그리는 역할</li>
<li><code>Light</code> : 3D 객체에 빛을 추가</li>
<li><code>Mesh</code> : 3D 모델을 구성하는 기본 단위로, Geometry와 Material로 구성</li>
<li><code>Controls</code> : 사용자 입력으로 카메라를 조작</li>
</ul>
<br>

<hr>
<h2 id="📌-프로젝트에서-threejs-활용">📌 프로젝트에서 Three.js 활용</h2>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/f5613b2f-91c1-4e1d-a0da-52a64ae86e44/image.png" alt=""></p>
<h3 id="01-3d-모델-불러오기">01. 3D 모델 불러오기</h3>
<blockquote>
<p><code>@react-three.drei</code> 의 <code>useGLTF</code> 훅을 활용했다.</p>
</blockquote>
<blockquote>
<h3 id="📎ㅤreact-threedrei-란">📎ㅤ@react-three/drei 란?</h3>
<p><code>Three.js</code>를 <code>React</code>에서 더 쉽게 사용할 수 있도록 도와주는 유틸리티 라이브러리이다. <code>useGLTF</code> 뿐만 아니라 카메라 컨트롤, 환경 설정 등을 손쉽게 구현할 수 있다.</p>
</blockquote>
<pre><code class="language-jsx">const { scene } = useGLTF(modelPath) as GLTFResult;
&lt;primitive object={scene} scale={0.68} rotation={[0, -Math.PI / 4, 0]} /&gt;</code></pre>
<ul>
<li><code>modelPath</code> 경로의 GLTF 모델을 로드하여 <code>scene</code> 으로 저장한다.</li>
<li><code>primitive</code> 를 사용하여 GLTF 모델을 <code>Three.js</code> 의 <code>object</code> 형태로 변환 후 렌더링</li>
</ul>
<blockquote>
<h3 id="📎ㅤgltf-란">📎ㅤ<strong>GLTF</strong> 란?</h3>
<p>: 3D 모델을 압축하여 웹에서 빠르게 로드할 수 있도록 도와주는 포맷이다.</p>
</blockquote>
<br>

<h3 id="02-위치-및-그림자-설정">02. 위치 및 그림자 설정</h3>
<pre><code class="language-jsx">useEffect(() =&gt; {
  if (scene) {
    scene.position.set(0, 0, 0);
    scene.traverse((object) =&gt; {
      if (object.isMesh) {
        object.castShadow = true;
        object.receiveShadow = true;
      }
    });
  }
}, [scene]);</code></pre>
<ul>
<li>모델의 위치를 설정, <code>scene.traverse</code> 를 이용하여 모델 내의 모든 Mesh 요소에 그림자 설정을 적용</li>
</ul>
<blockquote>
<h3 id="📎ㅤtraverse-란">📎ㅤtraverse 란?</h3>
<p>: <code>scene</code> 내부의 <strong>모든 객체를 순회하면서 특정 동작을 수행</strong>하도록 해준다.</p>
</blockquote>
<blockquote>
<h3 id="📎ㅤcastshadow와-receiveshadow-란">📎ㅤcastShadow와 receiveShadow 란?</h3>
</blockquote>
<ul>
<li><code>castShadow</code> : 해당 객체가 빛을 받아 그림자를 만들도록 설정</li>
<li><code>receiveShadow</code> : 해당 객체가 다른 객체의 그림자를 받을 수 있도록 설정</li>
<li>이 두 속성을 <strong>모두 활성화해야 현실적인 그림자 표현이 가능</strong>하다</li>
</ul>
<br>

<h3 id="03-모델-렌더링">03. 모델 렌더링</h3>
<pre><code class="language-jsx">&lt;Canvas shadows camera={CAMERA_CONFIG}&gt;
  &lt;RoomLighting /&gt;
  &lt;Suspense fallback={null}&gt;
    &lt;Center&gt;
      &lt;primitive object={scene} scale={0.68} rotation={[0, -Math.PI / 4, 0]} /&gt;
    &lt;/Center&gt;
  &lt;/Suspense&gt;
  &lt;OrbitControls enableRotate={false} enablePan={false} minDistance={5} maxDistance={12} /&gt;
&lt;/Canvas&gt;</code></pre>
<ul>
<li><code>Canvas</code> 컴포넌트를 사용하여 렌더링</li>
<li><code>primitive</code> 를 통해 Three.js의 <code>scene</code> 객체를 직접 렌더링</li>
<li><code>OrbitControls</code> 를 추가하여 카메라 이동 범위를 제한</li>
</ul>
<blockquote>
<h3 id="📎ㅤorbitcontrols-란">📎ㅤOrbitControls 란?</h3>
<p>: <code>Three.js</code> 에서 제공하는 컨트롤러로, 사용자가 마우스로 카메라를 회전, 이동, 줌할 수 있도록 도와주며 다양한 속성들로 설정하거나 막을 수 있다.</p>
</blockquote>
<ul>
<li><code>enableRotate={false}</code> : 회전 기능 비활성화</li>
<li><code>enablePan={false}</code> : 카메라 이동 비활성화</li>
<li><code>minDistance</code> 및 <code>maxDistance</code> : 줌 범위 조절</li>
</ul>
<br>

<h3 id="04-조명-및-카메라-설정">04. 조명 및 카메라 설정</h3>
<blockquote>
<p>재사용성과 관리의 용이성을 고려해서 별도의 <code>constants</code> 파일로 분리해서 사용하였다.</p>
</blockquote>
<h3 id="카메라-설정">카메라 설정</h3>
<pre><code class="language-js">export const CAMERA_CONFIG = {
  position: new Vector3(0, 4, 10),
  fov: 30,
};</code></pre>
<blockquote>
<h3 id="📎ㅤfov-란">📎ㅤFov 란?</h3>
<p>: Field of View로 즉, 카메라가 한 번에 볼 수 있는 시야각을 의미한다. 값이 클수록 넓게 보이고, 작을수록 좁게 보인다. 이를 통해 원근감을 조절할 수 있다.</p>
</blockquote>
<h3 id="조명-설정">조명 설정</h3>
<blockquote>
<p>렌더링에서 가장 중요한 건 <strong>조명</strong>이다. 왜냐하면 조명의 유/무의 차이로 퀄리티가 달라보이기 때문에, 조명 설정이 가장 중요했다. </p>
</blockquote>
<pre><code class="language-jsx">export function RoomLighting() {
  const { mainLight, ambient, fill } = LIGHT_CONFIG;

  return (
    &lt;&gt;
      &lt;ambientLight intensity={ambient.intensity} color={ambient.color} /&gt;
      &lt;directionalLight
        position={[mainLight.position[0], mainLight.position[1], mainLight.position[2]]}
        intensity={mainLight.intensity}
        color={mainLight.color}
        castShadow
        shadow-mapSize-width={mainLight.shadowConfig.mapSize[0]}
        shadow-mapSize-height={mainLight.shadowConfig.mapSize[1]}
        shadow-camera-near={mainLight.shadowConfig.camera.near}
        shadow-camera-far={mainLight.shadowConfig.camera.far}
        shadow-bias={mainLight.shadowConfig.bias}
      /&gt;
      &lt;pointLight
        position={[fill.position[0], fill.position[1], fill.position[2]]} 
        intensity={fill.intensity}
        color={fill.color}
      /&gt;
    &lt;/&gt;
  );
}</code></pre>
<blockquote>
<h3 id="📎ㅤlight-종류">📎ㅤLight 종류</h3>
</blockquote>
<ul>
<li><code>ambientLight</code> : 전체적인 기본 조명으로, 모든 방향에서 부드럽게 빛을 퍼뜨린다.</li>
<li><code>directionalLighy</code> : 태양빛과 유사한 조명으로, 특정 방향에서 평행하게 빛을 비춘다.</li>
<li><code>pointLight</code> : 특정 지점에서 모든 방향으로 빛을 방출하는 조명</li>
</ul>
<br>

<h3 id="05-가구-모델링-분리-및-관리">05. 가구 모델링 분리 및 관리</h3>
<blockquote>
<p>방 내부에 배치되는 가구(책장, CD플레이어, 저금통, 방명록)은 <code>useRoomItems</code> 훅으로 따로 만들어 관리하고 있다.</p>
</blockquote>
<blockquote>
<h3 id="🛋️ㅤ가구의-종류">🛋️ㅤ가구의 종류</h3>
</blockquote>
<ul>
<li><strong>저금통</strong>, <strong>방명록</strong> : 사용자가 <strong>편집할 수 없는</strong> 가구 ❌</li>
<li><strong>CD 플레이어</strong>, <strong>책장</strong> : 사용자가 <strong>편집할 수 있는</strong> 가구 ⭕️</li>
</ul>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/796aadfa-29f7-4fb0-8aae-1ee77084b234/image.gif" alt=""></p>
<p>➡️ㅤ사용자는 <strong>가구 토글 기능</strong>을 이용해 <strong>CD 플레이어</strong>, <strong>책장</strong>를 배치하거나 삭제할 수 있다.</p>
<pre><code class="language-jsx">const { items } = useRoomItems({ roomId, furnitures });</code></pre>
<ul>
<li>방마다 배치된 가구의 종류가 다를 수 있기 때문에 <code>roomId</code> 에 따라 동적으로 구성</li>
</ul>
<pre><code class="language-jsx">{items.map((item) =&gt; (
  &lt;Suspense key={item.id}&gt;
    &lt;Furnitures item={item} onInteract={handleInteraction} onHover={handleHover} /&gt;
  &lt;/Suspense&gt;
))}</code></pre>
<ul>
<li><code>items</code> 배열을 순회하여 <code>Furnitures</code> 컴포넌트를 렌더링</li>
<li><code>onInteract</code> 와 <code>onHover</code> 이벤트 핸들러를 통해 가구와의 상호작용을 구현</li>
</ul>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/bca883d7-6431-4539-89f4-4e57d5475c62/image.gif" alt=""></p>
<blockquote>
<p>사용자가 원하는 <strong>테마로 변경 가능</strong>하다</p>
</blockquote>
<br>

<h3 id="06-상호작용-구현--가구-클릭-및-호버-이벤트-">06. 상호작용 구현 ( 가구 클릭 및 호버 이벤트 )</h3>
<table>
<thead>
<tr>
<th>방명록</th>
<th>책장(도서)</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/9018ed84-4d96-4404-8113-4e0925f2c362/image.gif" alt=""></td>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/d9c3923b-75e9-4be8-8a2b-ff498d16ba5c/image.gif" alt=""></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>CD플레이어(음악)</th>
<th>저금통(포인트)</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/9836a281-e058-4673-a445-883c190b8eef/image.gif" alt=""></td>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/146cb155-5456-42b2-8636-3d3cb780c2b7/image.gif" alt=""></td>
</tr>
</tbody></table>
<pre><code class="language-jsx">const handlePointerOver = (e: MouseEvent) =&gt; {
  e.stopPropagation();
  scene.traverse((object) =&gt; {
    if (object.isMesh) {
      object.material.emissive = object.material.color;
      object.material.emissiveIntensity = 0.5;
    }
  });
};

const handlePointerOut = (e: MouseEvent) =&gt; {
  e.stopPropagation();
  scene.traverse((object) =&gt; {
    if (object.isMesh) {
      object.material.emissiveIntensity = 0;
    }
  });
};</code></pre>
<ul>
<li>호버하면(onPointerOver), emissive 값을 변경하여 하이라이트 효과 적용</li>
<li>호버 해제 시(onPointerOut), <code>emissiveIntensity</code>를 <code>0</code>으로 설정하여 원래 상태로 복구</li>
</ul>
<blockquote>
<h3 id="📎ㅤemissive-란">📎ㅤEmissive 란?</h3>
</blockquote>
<ul>
<li>모델이 <strong>자체적으로 빛을 내는 효과</strong>를 줄 수 있도록 하는 속성</li>
<li>호버 시, <code>emissiveIntensity</code> 값을 높여 <strong>하이라이트 효과를 부여</strong></li>
</ul>
<br>

<hr>
<h2 id="💬ㅤ비하인드">💬ㅤ비하인드</h2>
<blockquote>
<p>처음 사이트에 모델링을 렌더링 했을 때는 보이지도 않아서 뭐가 잘못되었는지... 카메라 세팅 끝에 방을 정중앙에 불러올 수 있었다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/d30b0ea8-1d9f-470c-8ea8-138d83694561/image.png" alt=""></p>
<blockquote>
<p>이제 가구들은 먼지처럼 그저 흩어져 있었다. 사실 이 부분은 노가다로 위치를 찾고 회전을 시키는 것 밖에는 없는 것 같아서 일일히 맞추었다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/f6873f34-4d61-4725-a24c-5a7da6ee3a8b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ RoomE 아카이브 ] #3. 나의 Dev-Log : 메인 프리뷰 기능 구현]]></title>
            <link>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-3.-%EB%82%98%EC%9D%98-Dev-Log-%EB%A9%94%EC%9D%B8-%ED%94%84%EB%A6%AC%EB%B7%B0-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-3.-%EB%82%98%EC%9D%98-Dev-Log-%EB%A9%94%EC%9D%B8-%ED%94%84%EB%A6%AC%EB%B7%B0-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 31 Mar 2025 07:35:09 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-방-프리뷰">📍 방 프리뷰</h2>
<blockquote>
<p>메인 페이지의 핵심 기능인 방 프리뷰는 사용자의 방을 중심으로 하우스메이트(팔로잉)의 방을 시각적으로 보여주는 기능이다. 3D 모델링된 방을 벌집 구조로 배치하여 한눈에 확인할 수 있도록 구현해야 한다.</p>
</blockquote>
<blockquote>
<h3 id="📌ㅤ제약-및-구현-조건">📌ㅤ제약 및 구현 조건</h3>
</blockquote>
<ul>
<li>방들이 <strong>겹치거나 중간에 빈 공간이 생기지 않아야 한다.</strong></li>
<li><strong>규칙성 있게 확장</strong>되어야 하며, <strong>나의 방은 항상 중앙</strong>에 위치해야 한다.</li>
<li><strong>헥사곤 좌표계를 이용</strong>하여 배치한다.</li>
</ul>
<hr>
<h2 id="🛠️-구현-로직-흐름">🛠️ 구현 로직 흐름</h2>
<h3 id="1ㅤ방-정보-조회">(1)ㅤ방 정보 조회</h3>
<ul>
<li><strong>팔로잉한 유저 목록</strong> 가져오기ㅤ➡️ㅤ<code>getFollowing()</code> 호출</li>
<li>각 <strong>유저의 방 정보</strong> 가져오기ㅤ➡️ㅤ<code>getRoomById(user.id)</code> 호출</li>
<li>가져온 <strong>방 정보를 <code>rooms</code> 상태로 저장</strong></li>
</ul>
<pre><code class="language-jsx">useEffect(() =&gt; {
  const fetchData = async () =&gt; {
    try {
      const followingUsers = await getFollowing();
      const roomData = await Promise.all(
        followingUsers.map((user) =&gt; getRoomById(user.id))
      );
      setRooms(roomData);
    } catch (error) {
      console.error(&#39;방 정보 로딩 실패&#39;, error);
    }
  };
  fetchData();
}, []);</code></pre>
<br>

<h3 id="2-헥사곤-배치-규칙">(2) 헥사곤 배치 규칙</h3>
<ul>
<li><strong>중심(0,0,0) 좌표</strong>에 <strong>나의 방 배치</strong></li>
<li><strong>6가지 방향</strong>으로 <strong>첫 번째 링</strong>을 배치</li>
<li>이후 기존 배치를 기준으로 <strong>왼쪽</strong>, <strong>오른쪽</strong> 방향으로 확장</li>
<li><strong>중복 좌표 체크</strong>하여 중복 배치 방지</li>
<li><strong>헥사곤 좌표계</strong>를 사용하여 <code>(q,r,s)</code> 좌표를 2D <code>(x,y,z)</code> 좌표로 변환</li>
</ul>
<pre><code class="language-jsx">const getHexPosition = (index: number) =&gt; {
  const row = Math.floor(index / 3);
  const col = index % 3;
  const x = col * 1.5; // 간격 조정
  const z = row * 1.3;
  return [x, 0, z];
};</code></pre>
<br>

<h3 id="3-방-배치">(3) 방 배치</h3>
<ul>
<li><code>getHexPosition(index)</code> 함수를 활용해 위치 계산</li>
<li><code>Room</code> 컴포넌트를 활용해 방을 배치<pre><code class="language-jsx">&lt;Canvas&gt;
{rooms.map((room, index) =&gt; (
  &lt;Room
    key={room.id}
    modelPath={room.modelPath}
    userId={room.userId}
    position={getHexPosition(index)}
    scale={0.5} // 메인에서는 작게
    onClick={() =&gt; navigate(`/room/${room.userId}`)}
  /&gt;
))}
&lt;/Canvas&gt;</code></pre>
</li>
</ul>
<br>

<hr>
<h2 id="📁-로직-분리-및-구현">📁 로직 분리 및 구현</h2>
<h3 id="1-userooms---방-데이터-불러오기">1. useRooms - 방 데이터 불러오기</h3>
<blockquote>
<h3 id="📍-역할">📍 역할</h3>
</blockquote>
<ul>
<li>현재 로그인한 사용자의 방 정보와 팔로잉한 사용자의 방 정보를 불러와 <code>rooms</code> 상태에 저장</li>
<li>방 테마 정보를 매핑하여 <code>modelPath</code> 추가</li>
</ul>
<pre><code class="language-jsx">export default function useRooms(limit = 30, myUserId: number) {
  const [rooms, setRooms] = useState&lt;Room[]&gt;([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState&lt;Error | null&gt;(null);

  useEffect(() =&gt; {
    const fetchData = async () =&gt; {
      try {
        const houseMateUsers = await housemateAPI.getFollowing(0, limit);
        const roomData = await Promise.all(
          houseMateUsers?.housemates?.map((mate: Housemate) =&gt;
            roomAPI.getRoomById(mate.userId)
          )
        );

        const modelRooms = roomData.map((room) =&gt; {
          const themeKey = mapThemeKeyToFullThemeKey(room.theme);
          return { ...room, modelPath: FullThemeData[themeKey].modelPath };
        });

        setRooms(modelRooms);
      } catch (error) {
       ...에러처리 코드
    };
    fetchData();
  }, [limit, myUserId]);

  return { rooms, loading, error };
}</code></pre>
<h3 id="✅ㅤ로직의-흐름">✅ㅤ로직의 흐름</h3>
<p><strong>(1) 로그인한 사용자의 방 정보 가져오기</strong></p>
<ul>
<li><code>roomAPI.getRoomById(myUserId)</code> 호출</li>
<li><code>mapThemeKeyToFullThemeKey</code> 를 이용해 테마 키를 변환 후, <code>modelPath</code> 추가</li>
</ul>
<p><strong>(2) 팔로잉한 유저들의 방 정보 가져오기</strong></p>
<ul>
<li><code>housemateAPI.getFollowing(0, limit)</code> 호출</li>
<li>각 유저의 방 정보를 비동기적으로 가져옴</li>
</ul>
<p><strong>(3) 데이터 저장</strong></p>
<ul>
<li>나의 방 정보와 팔로잉한 유저들의 방을 하나의 배열로 합쳐 <code>rooms</code> 상태로 업데이트</li>
</ul>
<br>

<hr>
<h3 id="2-usehexagongrid---방을-헥사곤-그리드-형태로-배치">2. useHexagonGrid - 방을 헥사곤 그리드 형태로 배치</h3>
<blockquote>
<h3 id="📍-역할-1">📍 역할</h3>
</blockquote>
<ul>
<li><code>rooms</code> 배열을 헥사곤 구조로 배치</li>
<li>중앙을 기준으로 점진적으로 확장하며 배치</li>
</ul>
<h3 id="⚙️ㅤ이동-방향-벡터-좌표-시스템">⚙️ㅤ이동 방향 벡터 (좌표 시스템)</h3>
<pre><code class="language-jsx">const directions: Position[] = [
  [-1, 0, 1], [1, 0, -1], [0, -1, 1],
  [1, -1, 0], [-1, 1, 0], [0, 1, -1],
];</code></pre>
<h3 id="⚙️ㅤ링-확장-규칙을-정의한-배열">⚙️ㅤ링 확장 규칙을 정의한 배열</h3>
<pre><code class="language-jsx">const expansions: Expansion[] = [
  { directionIndex: 2, dir: [-1, 0, 1], side: &#39;left&#39; },
  { directionIndex: 0, dir: [-1, 0, 1], side: &#39;left&#39; },
  { directionIndex: 4, dir: [-1, 0, 1], side: &#39;left&#39; },
  { directionIndex: 3, dir: [1, 0, -1], side: &#39;right&#39; },
  { directionIndex: 1, dir: [1, 0, -1], side: &#39;right&#39; },
  { directionIndex: 5, dir: [1, 0, -1], side: &#39;right&#39; },
];</code></pre>
<h3 id="⚙️ㅤexpandring-함수">⚙️ㅤexpandRing 함수</h3>
<blockquote>
<p>헥사곤 그리드의 새로운 링을 확장할 때 사용하며, <strong>기존 링을 기준으로 새로운 헥사곤 방을 배치</strong>하는 역할을 한다. 원래는 <code>useHexagonGrid</code> 에서 코드 분리가 안되어 있었지만, 로직이 복잡해지는 관계로 따로 분리하여 사용하고 있다.</p>
</blockquote>
<pre><code class="language-jsx">function expandRing(
  expansion: Expansion, // 확장 규칙
  previousRing: number[], // 이전 링에 속한 방의 인덱스 배열
  result: RoomPosition[], // 전체 배치된 방들의 목록
  visited: Set&lt;string&gt;, // 방문한 방 좌표를 기록한 Set
  rooms: Room[], // 배치할 전체 방 목록
  roomIndex: number, // 현재 배치 중인 방의 인덱스
  placedInRing: number, // 현재 링에 배치된 방 개수
  positionsInRing: number // 현재 링에 배치할 수 있는 최대 방 개수
)</code></pre>
<pre><code class="language-jsx">// 헥사곤 그리드 확장 함수
function expandRing(...): { roomIndex: number; placedInRing: number } {
  const { directionIndex, dir } = expansion;
  if (placedInRing &gt;= positionsInRing || roomIndex &gt;= rooms.length)
    return { roomIndex, placedInRing };

  for (let i = 0; i &lt; previousRing.length; i++) {
    const baseIndex = previousRing[(directionIndex + i) % previousRing.length];
    const basePos = result[baseIndex].position;
    const [dx, dy, dz] = dir;
    const newPos: Position = [basePos[0] + dx, basePos[1] + dy, basePos[2] + dz];
    const posKey = `${newPos[0]},${newPos[1]},${newPos[2]}`;

    if (!visited.has(posKey) &amp;&amp; newPos[0] + newPos[1] + newPos[2] === 0) {
      visited.add(posKey);
      result.push({ position: newPos, room: rooms[roomIndex] });
      return { roomIndex: roomIndex + 1, placedInRing: placedInRing + 1 };
    }
  }
  return { roomIndex, placedInRing };
}</code></pre>
<h3 id="✅ㅤ로직의-흐름-1">✅ㅤ로직의 흐름</h3>
<p><strong>(1) 배치 종료 조건 체크</strong></p>
<ul>
<li>현재 링에 배치된 방이 최대 배치 가능 개수를 초과하면 종료</li>
<li>남은 방이 없으면 종료</li>
</ul>
<p><strong>(2) 이전 링을 기준으로 새로운 방 배치</strong></p>
<p><strong>(3) 새로운 위치 계산</strong></p>
<ul>
<li>이전 링의 특정 위치를 기준으로 새 방의 좌표를 계산ㅤ➡️ㅤ<code>newPos</code></li>
<li>새로운 위치를 <code>posKey</code> 문자열로 변환해 중복 체크에 사용</li>
</ul>
<p><strong>(4) 중복 체크 및 방 배치</strong></p>
<ul>
<li>이미 배치된 방이면 스킵</li>
<li>헥사곤 좌표계의 특성인 <code>x + y + z = 0</code> 을 만족하는 경우만 추가</li>
<li>방을 추가한 후 <code>roomIndex</code> 와 <code>placedInRing</code> 값을 증가</li>
</ul>
<br>

<h3 id="📌ㅤ주요-로직---헥사곤-그리드-배치">📌ㅤ주요 로직 :  헥사곤 그리드 배치</h3>
<pre><code class="language-jsx">const result: RoomPosition[] = [];
const visited = new Set&lt;string&gt;();
const ringRooms: number[][] = [[]];</code></pre>
<ul>
<li><code>result</code> : 배치된 방들의 목록</li>
<li><code>visited</code> : 방문한 좌표를 저장하여 중복 방지</li>
<li><code>ringRooms</code> : 링별로 방 인덱스를 저장하는 배열</li>
</ul>
<h3 id="✅ㅤ로직의-흐름-2">✅ㅤ로직의 흐름</h3>
<pre><code class="language-jsx">// 중앙에 첫 번째 방 배치
result.push({ position: [0, 0, 0], room: rooms[0] });
visited.add(&#39;0,0,0&#39;);
ringRooms[0] = [0];
let roomIndex = 1;

// 첫 번째 링 배치 (최대 6개)
if (roomIndex &lt; rooms.length) {
  const firstRingIndices: number[] = [];
  for (let dir = 0; dir &lt; 6 &amp;&amp; roomIndex &lt; rooms.length; dir++) {
    const [dx, dy, dz] = directions[dir];
    const posKey = `${dx},${dy},${dz}`;

    if (!visited.has(posKey)) {
      visited.add(posKey);
      result.push({ position: [dx, dy, dz], room: rooms[roomIndex] });
      firstRingIndices.push(roomIndex);
      roomIndex++;
    }
  }
  ringRooms.push(firstRingIndices);
}</code></pre>
<p><strong>(1) 초기 방 배치</strong></p>
<ul>
<li>첫 번째 방(나의 방)을 <code>(0,0,0)</code> 에 배치</li>
<li>이후 6가지 방향을 돌며 <strong>첫 번째 링 완성</strong></li>
</ul>
<br>

<pre><code class="language-jsx">// 이후 링 확장
let ring = 1;
while (roomIndex &lt; rooms.length) {
  const positionsInRing = Math.min(rooms.length - roomIndex, 6);
  let placedInRing = 0;
  const previousRing = ringRooms[ring];

  if (!previousRing) break;

  for (const expansion of expansions) {
    const updated = expandRing(
      ...매개변수
    );
    roomIndex = updated.roomIndex;
    placedInRing = updated.placedInRing;
  }

  if (placedInRing &gt; 0) {
    ringRooms.push(result.slice(roomIndex - placedInRing, roomIndex).map((_, i) =&gt; roomIndex - placedInRing + i));
    ring++;
  } 
  ...</code></pre>
<p><strong>(2) 추가 방 배치</strong></p>
<ul>
<li>방이 많아지면 링을 확장하여 배치</li>
<li><code>expandRing()</code> 을 사용하여 각 방향에 배치 가능 여부 확인 후 추가</li>
<li>새 링 추가 시, <code>ringRooms</code> 에 추가</li>
</ul>
<br>

<pre><code class="language-jsx">// 2D 좌표 변환 (q, r -&gt; x, y 변환)
return result.map(({ position: [q, r], room }) =&gt; {
  const x = centerX + width * (q + r / 2);
  const y = centerY - height * (2.98 / 4.2) * r;
  return { room, position: [x, y, r * 0.7] };
});
}, [rooms, centerX, centerY]);</code></pre>
<p><strong>(3) 위치 변환</strong></p>
<ul>
<li><code>q, r, s</code> 좌표계를 화면 <code>x, y</code> 좌표로 변환ㅤ➡️ㅤ헥사곤 좌표계를 2D 좌표로 변환</li>
</ul>
<br>

<hr>
<h3 id="3-hiverooms---방을-3d-씬에-렌더링">3. HiveRooms - 방을 3D 씬에 렌더링</h3>
<blockquote>
<h3 id="📍-역할-2">📍 역할</h3>
</blockquote>
<ul>
<li><code>useRooms</code> 에서 가져온 방 데이터를 <code>useHexagonGrid</code> 로 배치 후, Three.js로 렌더링</li>
<li><code>Canvas</code> 를 활용하여 방 모델을 배치하고 마우스 이벤트를 처리</li>
</ul>
<pre><code class="language-jsx">export default function HiveRooms({ myUserId }) {
  const { rooms } = useRooms(30, myUserId);
  const positionedRooms = useHexagonGrid(rooms, 0, 0);
  const [hoveredRoom, setHoveredRoom] = useState&lt;number | null&gt;(null);

  return (
    &lt;div className=&#39;w-full h-screen relative&#39;&gt;
      &lt;Canvas camera={{ position: [0, 4, 10], fov: 25 }} shadows&gt;
        &lt;RoomLighting /&gt;
        {positionedRooms.map(({ room, position }, index) =&gt; (
          &lt;group
            key={index}
            position={position}
            onPointerOver={() =&gt; setHoveredRoom(index)}
            onPointerOut={() =&gt; setHoveredRoom(null)}&gt;
            &lt;HiveRoomModel room={room} /&gt;
          &lt;/group&gt;
        ))}
      &lt;/Canvas&gt;
      {hoveredRoom !== null &amp;&amp; (
        &lt;div className=&#39;absolute bottom-22 left-1/2 transform -translate-x-1/2&#39;&gt;
          ✊🏻 똑똑! {rooms[hoveredRoom]?.nickname}의 방에 들어가실래요?
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="✅ㅤ로직의-흐름-3">✅ㅤ로직의 흐름</h3>
<p><strong>(1) 방 데이터 가져오기</strong></p>
<p><strong>(2) 헥사곤 그리드 위치 계산</strong></p>
<p><strong>(3) Three.js로 방 모델 렌더링</strong></p>
<ul>
<li><code>Canvas</code> 내부에서 <code>HiveRoomModel</code> 을 반복 렌더링</li>
<li><code>onPointerOver</code>, <code>onPointerOut</code>을 통해 호버 효과 추가</li>
</ul>
<p><strong>(4) 마우스 이벤트 처리</strong></p>
<ul>
<li><code>mousedown</code>, <code>mousemove</code>, <code>mouseup</code> 이벤트를 활용해 드래그/클릭 감지</li>
<li>클릭한 방으로 이동하도록 구현</li>
</ul>
<br>

<hr>
<h2 id="✨-사용자-경험-개선">✨ 사용자 경험 개선</h2>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/bdab6c44-2745-4f18-993b-17822e73a556/image.gif" alt=""></p>
<blockquote>
<p>👉 <strong>UX 개선 포인트</strong>
✅ㅤ프리뷰 기능의 <strong>직관적인 사용</strong>을 돕기 위해 <strong>가이드 애니메이션</strong> 추가</p>
</blockquote>
<ul>
<li>마우스 왼쪽 클릭으로 드래그 가능</li>
<li>휠 스크롤을 사용해 줌 인/아웃 조작 가능</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ RoomE 아카이브 ] #1. 나의 Dev-Log : Dock Menu 컴포넌트 제작기]]></title>
            <link>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-1.-%EB%82%98%EC%9D%98-Dev-Log-Dock-Menu-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%A0%9C%EC%9E%91%EA%B8%B0</link>
            <guid>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-1.-%EB%82%98%EC%9D%98-Dev-Log-Dock-Menu-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%A0%9C%EC%9E%91%EA%B8%B0</guid>
            <pubDate>Mon, 31 Mar 2025 07:22:58 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-dock-menu-구현">📌 Dock Menu 구현</h2>
<blockquote>
<p>이번에 처음으로 복잡한 css 처리를 포함한 <code>Dock-Menu</code>를 만들어 보았다. 이 메뉴는 [테마 설정], [취향 설정]을 위한 도구들이 모여 있으며,디자인에서는 자연스러운 애니메이션을 통해 선택된 아이콘이 바뀌고, 선택된 상태가 유지되도록 설계 했다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/a0765d70-869c-46f4-abad-bd354c4867ba/image.png" alt=""></p>
<ul>
<li>기본적으로 메뉴가 <strong>호버되면 툴팁이 나타나고 선택된 메뉴는 아이콘이 변경</strong>되며, 나머지 메뉴들은 <code>#ffffff</code> 으로 바뀐다. <strong>메뉴가 선택되면 메뉴는 자동으로 닫히고 관련된 컴포넌트가 렌더링</strong>된다.</li>
<li>이번에 가장 신경 쓴 부분은 <strong>UX와 부드러운 전환</strong>이다.</li>
</ul>
<blockquote>
<h3 id="🔎ㅤ새로-알-게-된-것">🔎ㅤ새로 알 게 된 것</h3>
</blockquote>
<ul>
<li><code>button</code> : 기본적으로 키보드 접근성이 제공돼서 Tab 키로 이동 가능하다.</li>
<li><code>span</code> : 텍스트나 이미지를 감싸기 위해 사용하며, 스타일링을 위해 주로 쓰이는 인라인 요소로 적합하다.</li>
<li><code>aria-label</code> : <strong>웹 접근성</strong>을 위한 속성으로, 스크린 리더 같은 보조 기술이 요소를 읽을 때 제공할 텍스트 설명을 설정하는 역할을 한다.</li>
</ul>
<hr>
<h2 id="01-배치-및-스타일링">01. 배치 및 스타일링</h2>
<blockquote>
<h3 id="구상">구상</h3>
</blockquote>
<ul>
<li>기본 상태 : <strong>기본 도구 메뉴 아이콘만</strong> 보인다.</li>
<li>호버 : 높이가 커지고, <strong>2개의 추가 아이콘(취향 설정, 테마 설정)</strong>이 위로 슬라이드 되어 나타난다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/7f588c4d-b754-4892-a4fa-22873a1232a2/image.png" alt=""></p>
<blockquote>
<p>메뉴의 초기 사이즈는 <code>76px</code> 였으나, 실제 화면에서 테스트해본 결과 크기가 너무 커 보였다. 그래서 일반적인 Dock-Menu는 어떤 사이즈일까 찾아보았다.</p>
</blockquote>
<ul>
<li><p>모바일 : <code>48~64px</code></p>
</li>
<li><p>태블릿/PC : <code>56~72px</code></p>
</li>
<li><p>macOs Dock : <code>48~64px</code></p>
</li>
<li><p>결론적으로는 <code>64px</code> 로 변경하였다.</p>
</li>
</ul>
<h3 id="🧐ㅤ고정시킬-때-보통-어떤-걸-사용할까--sticky-vs-fixed">🧐ㅤ고정시킬 때 보통 어떤 걸 사용할까?  Sticky VS Fixed</h3>
<blockquote>
<p><code>fixed</code></p>
</blockquote>
<ul>
<li>화면을 스크롤해도 <strong>항상 같은 위치에 고정</strong>된다.</li>
<li>하단에 고정된 Dock-Menu/floating Btn을 만들 때 사용된다.</li>
</ul>
<blockquote>
<p><code>sticky</code></p>
</blockquote>
<ul>
<li><strong>부모 요소의 스크롤에 따라 위치가 변동</strong>된다.</li>
<li>예를 들어, 특정 섹션을 스크롤할 때 해당 섹션 안에서만 고정되는 요소를 만들 때 사용된다.</li>
</ul>
<p>➡️ <code>Dock-Menu</code> 는 화면 하단에 고정되므로 <code>fixed</code> 사용</p>
<h3 id="📍-dock-menu-구조">📍 Dock-Menu 구조</h3>
<pre><code class="language-jsx">export default function DockMenu({ activeSettings, onSettingsChange }) {
  const [isOpen, setIsOpen] = useState(false);
  const menuRef = useRef(null);

  const getMainButtonBackground = () =&gt; {
    if (!hasSelectedSetting) {
      return &#39;bg-white&#39;;
    }
    if (!isOpen &amp;&amp; activeSettings) {
      return &#39;bg-white&#39;;
    }
    return &#39;bg-transparent hover:bg-white/50&#39;;
  };

  return (
    &lt;div
      ref={menuRef}
      className={`bottom-menu ... ${isOpen ? &#39;h-[202px]&#39; : &#39;h-16&#39;}`}
    &gt;
      &lt;div className=&quot;...&quot;&gt;
        &lt;button
          className={`bottom-menu-icon group absolute ${getMainButtonBackground()}`}
          onClick={() =&gt; setIsOpen(!isOpen)}
          aria-label=&quot;도구 메뉴 열기&quot;
        &gt;
          &lt;img src={isOpen ? dockMenuNoselectIcon : dockMenuIcon} /&gt;
        &lt;/button&gt;
        &lt;div className={`bottom-menu-content transition-all duration-100 ${isOpen ? &#39;opacity-100&#39; : &#39;opacity-0 pointer-events-none&#39;}`}&gt;
          {[&#39;preference&#39;, &#39;theme&#39;].map((setting, index) =&gt; (
            &lt;button
              key={setting}
              onClick={() =&gt; onSettingsChange(setting)}
              className={`bottom-menu-icon group absolute bottom-[${68 + index * 70}px] ${activeSettings === setting ? &#39;bg-white&#39; : &#39;bg-transparent hover:bg-white/50&#39;}`}
            &gt;
              &lt;img src={activeSettings === setting ? selectedIcons[setting] : unselectedIcons[setting]} /&gt;
              &lt;span className=&quot;... opacity-0 group-hover:opacity-100 transition-opacity&quot;&gt;
                {setting === &#39;preference&#39; ? &#39;취향 설정하기&#39; : &#39;테마 설정하기&#39;}
              &lt;/span&gt;
            ...
}</code></pre>
<h3 id="1ㅤ메뉴의-동작-및-상태에-따른-스타일링">(1)ㅤ메뉴의 동작 및 상태에 따른 스타일링</h3>
<ul>
<li>메뉴가 열리면 <code>bottom-menu</code> 의 높이가 확장되며, 닫히면 기본 상태로 돌아간다.</li>
<li>Dock-Menu 버튼 클릭시 <code>setIsOpen(true)</code> 로 메뉴를 열고 다시 클릭 시 <code>setIsOpen(false)</code> 로 닫는다.</li>
<li><code>pointer-events-none</code> : 메뉴가 닫혀있을 때 사용자가 메뉴 항목을 클릭할 수 없도록 설정</li>
<li><code>getMainButtonBackground</code> : 사용자가 특정 메뉴를 활성화하면 그 메뉴의 배경이 <code>bg-white</code> 로 변경되고 다른 메뉴는 <code>bg-transparent</code> 로 설정하여 어떤 메뉴가 활성화되어있는지 쉽게 알 수 있게 했다.</li>
</ul>
<h3 id="2ㅤ하위-메뉴-아이템">(2)ㅤ하위 메뉴 아이템</h3>
<ul>
<li>각 설정 버튼 <code>preference</code> 와 <code>theme</code> 는 <code>absolute</code> 로 위치가 고정되어 있고 메뉴가 열리면 버튼들이 각 위로 배치헸다.</li>
<li><code>transition-opacity</code> 와 <code>group-hover:opacity-100</code> 을 사용하여 <code>hover</code> 시 이미지의 투명도가 변경되도록 했다. 이로 인해 호버 시 좀 더 직관적인 구분이 가능하도록 했다.</li>
<li><code>hover</code> 하면 해당 메뉴의 툴팁이 나타나도록 <code>group-hover:opacity-100 transition-opacity</code> 효과를 추가</li>
</ul>
<br>

<h2 id="02-상태-관리-및-로직-구현">02. 상태 관리 및 로직 구현</h2>
<blockquote>
<p>Dock-Menu의 열림 상태 및 선택된 설정을 관리하기 위해 <code>zustand</code> 활용</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/fbbf9693-96ac-4a67-b2bf-6a82b252d9bf/image.gif" alt=""></p>
<pre><code class="language-jsx">const [isOpen, setIsOpen] = useState&lt;boolean&gt;(false);
const [hasSelectedSetting, setHasSelectedSetting] = useState&lt;boolean&gt;(false);
</code></pre>
<pre><code class="language-jsx">const handleSettingClick = (setting: string) =&gt; {
  if (setting === activeSettings) {
    setIsOpen(false);
    setHasSelectedSetting(false);
    onSettingsChange(null);
  } else {
    setIsOpen(false);
    setHasSelectedSetting(true);
    onSettingsChange(setting);
  }
};

const handleMainButtonClick = () =&gt; {
  if (isOpen) {
    setIsOpen(false);
    onSettingsChange(null);
    setHasSelectedSetting(false);
  } else {
    setIsOpen(true);
  }
};</code></pre>
<ul>
<li><code>isOpen</code> : Dock-Menu의 열림/닫힘 상태를 관리</li>
<li><code>activeSettings</code> : 부모 컴포넌트에서 관리하는 상태로, <code>preference</code> 또는 <code>theme</code> 설정이 선택됨을 관리</li>
<li><code>hasSelectedSetting</code> : 선택 여부 관리</li>
<li><code>handleSettingClick</code> : 이미 선택된 설정을 다시 클릭하면 해당 설정을 비활성화, 다른 설정을 클릭하면 새로운 설정을 활성화</li>
<li><code>handleMainButtonClick</code> : Dock-Menu가 열려있을 때, 클릭하면 닫고 설정을 초기화하고 닫혀있을 때 클릭하면 열리게 설정</li>
</ul>
<h2 id="03-사용자-경험-개선">03. 사용자 경험 개선</h2>
<h3 id="1ㅤ자동-닫힘-및-아이콘-변경">(1)ㅤ자동 닫힘 및 아이콘 변경</h3>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/5d8c720a-d577-41ef-b607-ccea162c2543/image.gif" alt=""></p>
<blockquote>
<p>👉 <strong>UX 개선 포인트</strong>
✅ㅤ<strong>불필요한 클릭 최소화</strong> → 선택 후 자동 닫힘으로 사용자 조작 간소화
✅ㅤ<strong>시각적 피드백 제공</strong> → 아이콘 변경을 통해 현재 설정 상태를 명확하게 표현</p>
</blockquote>
<pre><code class="language-jsx">const handleSettingClick = (setting: string) =&gt; {
  //...
  setIsOpen(false);
};

const getCurrentIcon = () =&gt; {
  if (!hasSelectedSetting) return dockMenuIcon;
  if (isOpen) return dockMenuNoselectIcon;
  if (activeSettings === &#39;preference&#39; &amp;&amp; !isOpen) return preferenceIcon;
  if (activeSettings === &#39;theme&#39; &amp;&amp; !isOpen) return themeIcon;
  return dockMenuIcon;
};</code></pre>
<ul>
<li>사용자가 <code>preference</code> 또는 <code>theme</code> 을 선택하면 Dock-Menu가 닫히도록 구현</li>
<li><code>handleSettingClick</code> 함수에서 <code>setIsOpen(false)</code> 호출하여 선택후 자동으로 닫히도록 처리</li>
<li>닫힌 후 기본 도구 메뉴 아이콘에서 <code>getCurrentIcon</code> 함수로 선택한 설정 메뉴 아이콘으로 변경되도록 설정</li>
</ul>
<h3 id="2ㅤdock-menu-바깥-클릭-시-메뉴-닫기">(2)ㅤDock-Menu 바깥 클릭 시 메뉴 닫기</h3>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/0e3a8562-0422-4ba4-9c9f-a1e86f8b5658/image.gif" alt=""></p>
<blockquote>
<p>👉 <strong>UX 개선 포인트</strong>
✅ㅤ<strong>사용자 편의성 향상</strong> → 사용자가 직접 메뉴를 닫을 필요 없이 자연스럽게 닫힘
✅ㅤ<strong>자연스러운 인터랙션</strong> → 바깥 클릭 시 닫히는 동작은 일반적인 UI 패턴과 일치하여 직관적인 경험 제공</p>
</blockquote>
<pre><code class="language-jsx">const menuRef = useRef&lt;HTMLDivElement&gt;(null);

useEffect(() =&gt; {
  const handleClickOutside = (event: MouseEvent) =&gt; {
    if (menuRef.current &amp;&amp; !menuRef.current.contains(event.target as Node)) {
      setIsOpen(false);
    }
  };
  document.addEventListener(&quot;mousedown&quot;, handleClickOutside);
  return () =&gt; document.removeEventListener(&quot;mousedown&quot;, handleClickOutside);
}, []);</code></pre>
<ul>
<li><code>useRef</code> 를 활용해 Dock-Menu 컨테이너를 참조</li>
<li><code>mousedown</code> 이벤트 리스너를 추가하여 바깥을 클릭하면 자동으로 닫히도록 구현</li>
<li><code>menuRef.current.contains(event.target as Node)</code> 를 검사하여 바깥 클릭 감지 후 <code>setIsOpen(false)</code> 수행</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ RoomE 아카이브 ]  #0. 프로젝트 회고 : Background]]></title>
            <link>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-0.-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D-%EB%B0%8F-%ED%9A%8C%EA%B3%A0-Background</link>
            <guid>https://velog.io/@swallowed_o0/RoomE-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-0.-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D-%EB%B0%8F-%ED%9A%8C%EA%B3%A0-Background</guid>
            <pubDate>Fri, 28 Mar 2025 03:08:01 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/96434307-1517-42c6-8b9b-48c8ebc5354f/image.jpg" alt=""></p>
<h2 id="💬ㅤroome-의-탄생-배경">💬ㅤRoomE 의 탄생 배경</h2>
<blockquote>
<p>처음엔 멋진 프로젝트를 만들고 싶다는 마음에 신박한 아이디어를 찾으려 했다. 하지만 그런 아이디어일수록 이미 존재하는 서비스가 많았고 고민할수록 점점 무겁고 어려운 주제만 남아 막막해졌다. 그런 주제들은 구현 과정에서 부딪힐 문제들도 많아 보였고 아이디어 자체에만 집착하다 보니 결국 제자리걸음을 하고 있었다.</p>
<p>그래서 방향을 바꾸기로 했다. <strong>우리 모두가 공감할 수 있고 개발하는 동안 진짜 재미있고 몰입할 수 있는 주제는 무엇일까?</strong> 이 질문을 시작으로, 자연스럽게 우리가 가진 고민들을 나누게 되었다. 우리는 모두 취업을 준비하고 있었고 저마다의 걱정이 있었지만, 본질적인 고민은 같았다. <strong>&quot;나 자신을 제대로 알지 못하는데, 어떻게 날 잘 표현할 수 있을까?&quot;</strong> 
이 고민을 해결하려면, <strong>먼저 나를 깊이 이해하는 과정이 필요</strong>하다는 걸 깨달았다. 하지만 바쁜 일상 속에서 우리는 점점 자신을 돌아볼 기회를 잃어가고 있었다. 그래서 우리는 <strong>자아 탐색과 심리적 안정을 위한 공간</strong>을 만들기로 했다.</p>
<p>그렇게 탄생한 RoomE는 <strong>사용자가 스스로를 더 잘 이해하고, 자신의 생각과 감정을 정리</strong>하며 <strong>성장할 수 있는 경험을 제공</strong>하는 프로젝트다. 우리는 이를 통해 각자가 자연스럽게 자신을 돌아보고 스스로의 방식으로 고민을 정리하며 앞으로 나아갈 수 있도록 돕고 싶었다.</p>
</blockquote>
<br>

<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/33ce2760-c909-4f05-b1a9-ee7821786c5f/image.png" alt="아이디어 스케치"></p>
<h2 id="💬ㅤ구현-가능성">💬ㅤ구현 가능성</h2>
<blockquote>
<p><strong>우리의 기획은 기대되었지만, 현실적인 고민도 컸다.</strong> 
처음 구상한 콘셉트는 싸이월드와 본디에서 영감을 받아, <strong>사용자가 개성 있는 가상의 방을 만들고, 이웃과 연결되며 상호작용하는 시스템</strong>이었다. <strong>방들이 연결되면서 하나의 네트워크를 이루는 구조</strong>를 생각했으며, 이는 <strong>시각적으로 더 매력적이고 인터랙션한 요소</strong>가 많았기에 다른 프로젝트들과 차별화 될 수 있는 강점이 많았다.</p>
<p>하지만 이를 구현하려면 <strong>3D 환경을 조성하고 방들이 어떻게 연결될지를 계산하는 복잡한 알고리즘</strong>이 필요했다. <code>three.js</code>를 사용하면 가능하겠지만, 팀원 중 누구도 3D 개발 경험이 없었고 주어진 개발 기간은 단 2~3주밖에 되지 않았다. 그렇기에 <strong>&quot;이걸 현실적으로 해낼 수 있을까?</strong> 라는 고민이 깊어졌다.</p>
<p>그럼에도 불구하고, 이 프로젝트에서 3D는 핵심 요소였다. 기획과 디자인, 서비스의 퀄리티를 살리기 위해서는 3D 구현을 포기할 수 없다고 판단했다. <strong>결국, 팀원 중 3D를 조금이라도 접해본 내가 직접 배워가며 구현</strong>해보기로 결심했다. 실패할 가능성도 고려해, 백업 플랜을 준비하고 팀원들과 함께 방향을 조율했다.</p>
<p>기획 단계에서 <code>three.js</code> 의 기본 개념과 사용법을 익히기 위해 강의를 듣기 시작했다.그러나 가장 큰 문제는 3D 모델링이었다. AI 툴을 사용해보기도 했지만, 그 결과물이 서비스의 스타일과 맞지 않았고 원하는 퀄리티를 얻기에는 한계가 있었다. 그래서 <strong>유튜브 강의로 블렌더를 배워 직접 모델링 작업에 도전</strong>하기로 했다.</p>
<p>처음 해보는 작업이라 막막하고 <strong>완성할 수 있을지 확신할 수 없었지만,  지레 겁먹고 주저하기보다 도전해보는 것이 더 많은 것을 배울 수 있는 기회라 생각했다.</strong></p>
</blockquote>
<br>

<h2 id="💬ㅤ최종적인-회고">💬ㅤ최종적인 회고</h2>
<blockquote>
<p>이번 프로젝트는 가장 힘들었지만, 가장 애정이 가는 프로젝트였다.
무엇보다도 <strong>끊임없는 도전과 팀워크의 진정한 가치를 몸소 체험할 수 있었기 때문</strong>이다.</p>
<p>가장 큰 난관은 <strong>타이트한 일정</strong>이었다. 디자인부터 3D 구현까지 혼자 맡다 보니 <strong>시간을 최대한 효율적으로 활용</strong>해야 했다. 하지만 다행히도 팀원들이 각자 잘할 수 있는 분야에서 적극적으로 도와주었고 내가 그 부분에 온전히 집중할 수 있도록 도와주었다. 이런 어려운 순간마다 서로 역할을 나누고 그 역할을 완수하기 위해 협력하는 과정은 정말 값진 경험이었다.</p>
</blockquote>
<p><strong>모든 팀원이 각자의 자리에서 최선을 다했기에, 제한된 시간 속에서도 우리가 낼 수 있는 최고의 퀄리티를 만들어낼 수 있었다고 생각</strong>한다. 개발을 시작한 이후 처음으로 <strong>&#39;이게 진짜 팀워크구나&#39;</strong> 하고 실감할 수 있었다. 이 과정은 정말 힘들었지만, 동시에 즐거웠다. <strong>각자가 맡은 일에 완전히 몰입하며, 하나의 목표를 향해 나아가는 경험이 너무 좋았고, 이런 협업을 다시 한 번 경험</strong>하고 싶다는 생각이 들었다. </p>
<blockquote>
<p>이 프로젝트를 무사히 끝마친 내 자신이 자랑스럽다. 중간에 어려운 순간들이 많았지만, 타협하지 않고 끝까지 도전한 경험이 가장 값진 결과라고 생각한다. <strong>이 경험을 통해 배운 것들은 단순한 기술을 넘어, 앞으로 나아가는데 큰 자산이 될 거라고 생각</strong>한다.</p>
</blockquote>
<br>

<h2 id="🏆ㅤ결과">🏆ㅤ결과</h2>
<table>
<thead>
<tr>
<th>🎖️우수</th>
<th>🎖️인기</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/04756abd-28db-4524-830a-0a9210a7665f/image.jpeg" alt=""></td>
<td><img src="https://velog.velcdn.com/images/swallowed_o0/post/16e941bf-be24-4f15-8fa9-8658e73e28e7/image.jpeg" alt=""></td>
</tr>
</tbody></table>
<blockquote>
<p>이 프로젝트의 끝이 눈에 보이는 성과로 이어져서 너무 기쁘다. 특히, <strong>우수상은 프로젝트의 완성도와 기술적인 평가를 바탕으로 주어진 상</strong>이라 의미가 깊고, <strong>인기상은 수료생들이 가장 마음에 들었던 프로젝트를 뽑는 상</strong>이라 더 특별한 의미가 있다.</p>
<p>완성도를 인정받은 것도 중요하지만, 사람들의 마음에 와닿았다는 점이 더 큰 기쁨이었다. 우리가 열심히 고민하고 노력한 결과가 진정성 있게 전달되어, <strong>사람들이 그 매력을 느꼈다는 사실이 정말 뿌듯하다. 앞으로도 더 매력적이고 많은 사람들의 마음을 사로잡을 수 있는 프로젝트를 만들어 나가고 싶다.</strong></p>
</blockquote>
<br>

<h2 id="💬ㅤ그-이후">💬ㅤ그 이후..</h2>
<blockquote>
<p>지금은 디벨롭하여 실제로 서비스를 운영해보고자 기획하고 있다. 시간이 부족하여 신경쓰지 못했던 부분들과 더 추가하고 싶었던 기능들을 보완하여 출시해볼 생각이다.</p>
</blockquote>
<br>


<h2 id="💬ㅤ비하인드">💬ㅤ비하인드</h2>
<h3 id="📍-팀-결성">📍 팀 결성</h3>
<blockquote>
<p>이번 프로젝트는 FE 팀을 구성하고 주제 및 기획을 잡고 BE 팀을 구인하여 프로젝트를 진행해나가는 방식이었다. 그래서 부랴부랴 PR_PPT 디자인 작업도 했다. 생각보다 잘 나와서 여기에도 올려본다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/d3cf6959-46d2-43e5-80d4-ae2a698efa3d/image.jpg" alt=""></p>
<br>

<h3 id="📍-3d-모델링의-역경">📍 3D 모델링의 역경</h3>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/0896cce8-09b3-4423-9c0d-dd231bf14ac5/image.png" alt=""></p>
<blockquote>
<p>다행히 유튜브 강의가 잘 되어 있어서, 여러 가지 강의들을 보면서 하나씩 채워갔다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/87ccc352-d25a-4de0-bd07-6fe336ce7ae3/image.png" alt=""></p>
<blockquote>
<p>특히 테마별로 방을 만드는 작업은 생각보다 재밌었다. 블렌더 툴에 점점 익숙해져서 시간도 절약할 수 있었다. 이런 프로그램들을 다룰 때마다 느끼는 점은, 어도비 툴이나 피그마처럼 한 가지 프로그램을 익혀놓으면 비슷한 유형의 프로그램을 처음 접하더라도 훨씬 수월하게 다룰 수 있다는 것이다. 나 역시 이번 경험에서 큰 도움이 되었다.</p>
</blockquote>
<br>

<h3 id="📍-조각-디자인-모음">📍 조각 디자인 모음</h3>
<blockquote>
<h3 id="roome-로고-디자인">RoomE 로고 디자인</h3>
</blockquote>
<ul>
<li>귀엽고 포근한 느낌으로 작업했다. </li>
<li>사이트에 실제로 올려보니 컬러가 있는게 오히려 마이너스 인 것 같아 심플한 버전으로 채택 </li>
</ul>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/229c3df2-1414-4401-94ea-2a32984bcdd5/image.png" width=40%/>ㅤ</p>
<blockquote>
<h3 id="roome-메달-디자인">RoomE 메달 디자인</h3>
</blockquote>
<ul>
<li>랭킹에 사용할 메달 디자인으로 로고의 캐릭터를 활용해서 작업했다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/f312b2d5-8c25-42fd-adaa-a39408334f65/image.png" alt=""></p>
<blockquote>
<h3 id="roome-cd-플레이어-디자인">RoomE CD 플레이어 디자인</h3>
</blockquote>
<ul>
<li>음악이 재생될 때, CD 플레이어의 CD가 돌아가는 애니메이션을 주기 위해 따로 제작하였다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/5d0c02fd-a015-4820-baf3-b05db2087015/image.png" alt=""></p>
<blockquote>
<h3 id="roome-버튼-디자인">RoomE 버튼 디자인</h3>
</blockquote>
<ul>
<li>클릭할 때, 좀 더 역동감을 주고자 오락실 버튼에서 착안하여 제작하였다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/8e89219d-1f59-48db-8cb4-544d471d68b5/image.png" alt=""></p>
<blockquote>
<h3 id="roome-커스텀-디자인">RoomE 커스텀 디자인</h3>
</blockquote>
<ul>
<li>서비스와 어울리는 토스트/모달창을 디자인에 맞춰 제작하였다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/d327ddc9-a4da-4690-9259-dc2d10ed970e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/e0b2ba58-96af-46c8-a1ec-5cefb399f206/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ COMMA 아카이브 ] #4. 팀의 Dev-Log : 트러블슈팅_Hz에 따른 게임 속도 차이]]></title>
            <link>https://velog.io/@swallowed_o0/COMMA-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-3.-%ED%8C%80%EC%9D%98-Dev-Log-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%EB%AA%A8%EB%8B%88%ED%84%B0-%EC%A3%BC%EC%82%AC%EC%9C%A8%EC%97%90-%EB%94%B0%EB%A5%B8-%EA%B2%8C%EC%9E%84-%EC%86%8D%EB%8F%84-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@swallowed_o0/COMMA-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-3.-%ED%8C%80%EC%9D%98-Dev-Log-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%EB%AA%A8%EB%8B%88%ED%84%B0-%EC%A3%BC%EC%82%AC%EC%9C%A8%EC%97%90-%EB%94%B0%EB%A5%B8-%EA%B2%8C%EC%9E%84-%EC%86%8D%EB%8F%84-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Thu, 27 Mar 2025 04:30:38 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-delta-time-적용">📌 Delta Time 적용</h2>
<h3 id="🚨-문제점">🚨 문제점</h3>
<blockquote>
<p>최종 QA를 진행 중, <strong>팀원들 간 게임 속도가 상이</strong>하다는 것을 발견하였다. 해당 문제에 대해서 알아본 결과, <strong>모니터 주사율(Hz)에 따라 게임 속도가 다르다는 것을 알게되었다.</strong> 기존에 구현했던 게임들은 <strong>프레임 기반 업데이트를 사용</strong>하였기에 주사율이 높은 모니터에서는 게임이 더 빠르게 실행되고 있었다. 이럴 경우, 사용자가 가진 모니터별로 난이도가 달라져 <strong>공평한 밸런스로 게임을 즐길 수 없는 문제가 발생된다.</strong></p>
</blockquote>
<h3 id="💡-문제-분석">💡 문제 분석</h3>
<blockquote>
<ul>
<li>게임의 오브젝트 이동이 초당 프레임 수에 따라 결정되고 있다.</li>
</ul>
</blockquote>
<ul>
<li><code>60 Hz</code> 모니터의 경우 초당 60번 업데이트가 발생하지만, <code>144 Hz</code>의 경우 초당 144번 업데이트가 발생하여 게임 속도가 증가한다.</li>
<li>즉, 프레임수에 비례하여 속도가 증가하는 구조</li>
</ul>
<br>

<hr>
<h2 id="🔎ㅤ관련-개념과-용어들">🔎ㅤ관련 개념과 용어들</h2>
<blockquote>
<h3 id="📍-주사율---화면이-1초에-몇-번-새로고침-되는가">📍 주사율  = 화면이 1초에 몇 번 새로고침 되는가?</h3>
<p>모니터의 성능을 나타내는 단위 중 하나로, <strong>1초에 화면을 몇 번 갱신하는지를 의미</strong>한다.</p>
</blockquote>
<ul>
<li>60Hz 모니터 → 1초에 60번</li>
<li>144Hz 모니터 → 1초에 144번</li>
<li>240Hz 모니터 → 1초에 240번<blockquote>
</blockquote>
➡️ㅤ<strong>주사율이 높을수록 부드러운 화면을 볼 수 있다.</strong></li>
</ul>
<blockquote>
<h3 id="📍-프레임과-fps-란">📍 프레임과 FPS 란?</h3>
</blockquote>
<ul>
<li>프레임 = 한장의 화면<ul>
<li>게임에서는 프레임을 빠르게 변경하여 움직임을 표현한다.<blockquote>
 <br>  </blockquote>
</li>
</ul>
</li>
<li>FPS = 초당 몇 개의 프레임을 보여주는가?<ul>
<li>1초 동안 화면이 갱신되는 횟수를 의미한다.</li>
<li>60FPS → 1초에 60번</li>
<li>144FPS → 1초에 144번<blockquote>
</blockquote>
➡️ㅤ<strong>FPS가 높을수록 부드러운 움직임을 볼 수 있다.</strong></li>
</ul>
</li>
</ul>
<h3 id="🤔-그런데-왜-게임-속도까지-달라질까">🤔 그런데 왜 게임 속도까지 달라질까?</h3>
<ul>
<li>게임 로직은 프레임을 기준으로 실행되는데, <strong>모니터 주사율이 높을수록 프레임 수도 증가하기 때문에 게임이 더 빨리 실행되는 문제가 발생</strong></li>
</ul>
<pre><code class="language-js">function update() {
  character.x += 5; // 1프레임마다 5px 이동
}</code></pre>
<ul>
<li><p>60FPS (60Hz 모니터)</p>
<ul>
<li>1초 = 60프레임 → 5px * 60 = <code>300px</code> 이동</li>
</ul>
</li>
<li><p>144FPS (144Hz 모니터)</p>
<ul>
<li>1초 = 144프레임 → 5px * 144 = <code>720px</code> 이동</li>
</ul>
</li>
</ul>
<p>➡️ㅤ<strong>같은 시간 동안 더 멀리 이동 → 게임 난이도 증가</strong></p>
<blockquote>
<p>이렇게 되면, 같은 게임이더라도 <strong>주사율이 높은 사람은 더 빠르게 진행 되고, 주사율이 낮은 사람은 느리게 진행되는 문제가 발생</strong>한다.</p>
</blockquote>
<br>

<hr>
<h2 id="✅ㅤ해결-방법--delta-time-적용">✅ㅤ해결 방법 : Delta Time 적용</h2>
<blockquote>
</blockquote>
<p>이 문제를 해결하기 위해 <code>Delta Time</code> 을 적용하여 시간 단위로 이동 속도를 조정하였다.</p>
<h3 id="📍ㅤdelta-time-이란">📍ㅤDelta Time 이란?</h3>
<ul>
<li>각 프레임 사이의 시간 차이를 의미한다.</li>
<li>즉, 각 프레임이 실행될 때 걸린 시간을 계산하여, 속도를 보정 해주는 방법이다.</li>
<li><code>현재 프레임 시간</code> - <code>이전 프레임 시간</code> 을 계산하여 프레임 간격을 구한다.</li>
<li>오브젝트의 이동 속도를 <code>speed * deltaTime</code> 형태로 적용하여 일정한 속도로 움직이도록 조정한다.</li>
</ul>
<h3 id="💬ㅤdelta-time-적용-시">💬ㅤDelta Time 적용 시</h3>
<pre><code class="language-js">let lastFrameTime = 0;
function update(timestamp) {
  if (!lastFrameTime) lastFrameTime = timestamp;
  const deltaTime = (timestamp - lastFrameTime) / 1000; // 초 단위로 변환
  lastFrameTime = timestamp;
  character.x += 100 * deltaTime; // 1초에 100px 이동하도록 보정
}</code></pre>
<ul>
<li><p>60FPS (60Hz 모니터)</p>
<ul>
<li><code>deltaTime ≈ 1/60 = 0.016초</code></li>
<li>이동 거리 = <code>100 * 0.016 = 1.6px</code> (프레임당 이동)</li>
<li><code>1.6px * 60 = 100px</code> (1초 이동)</li>
</ul>
</li>
<li><p>144FPS (144Hz 모니터)</p>
<ul>
<li><code>deltaTime ≈ 1/144 = 0.007초</code></li>
<li>이동 거리 = <code>100 * 0.007 = 0.7px</code> (프레임당 이동)</li>
<li><code>0.7px * 144 = 100px</code> (1초 이동)</li>
</ul>
</li>
</ul>
<p>➡️ㅤ<strong>주사율이 다르더라도 1초에 같은 거리(100px)만큼 이동</strong></p>
<br>

<hr>
<h2 id="🛠-적용한-코드">🛠 적용한 코드</h2>
<blockquote>
<p><code>requestAnimationFrame</code> 의 <code>timestamp</code> 값을 활용하여 Delta Time을 계산</p>
</blockquote>
<pre><code class="language-js">let lastFrameTime = 0; // 이전 프레임 시간 저장

function main(timestamp) {
  if (!lastFrameTime) lastFrameTime = timestamp;
  const deltaTime = (timestamp - lastFrameTime) / 1000; // 밀리초 → 초 변환
  lastFrameTime = timestamp;

  if (!Enemy.isGameOver) {
    update(deltaTime);
    render();
    requestId.value = requestAnimationFrame(main);
  } else {
    stop();
    stopAllMusic();
    emits(&quot;open-game-over&quot;, score.value, currentTime.value);
    cancelAnimationFrame(requestId.value);
  }
}</code></pre>
<ul>
<li><code>deltaTime = (현재시간 - 이전시간) / 1000</code> 으로 프레임 간격을 계산</li>
<li>이동 속도에 <code>speed * deltaTime</code> 을 곱해 초당 일정한 속도를 유지</li>
<li>게임 오버 시 <code>requestAnimationFrame</code> 을 중단하여 불필요한 연산 방지</li>
</ul>
<br>

<hr>
<h2 id="🔗ㅤkaplay를-사용한-적용-방식">🔗ㅤKaplay를 사용한 적용 방식</h2>
<blockquote>
<p>Kaplay에서는 <code>requestAnimationFrame</code> 을 직접 사용하지 않고 엔진의 <code>onUpdate()</code> 메서드를 활용하여 Delta Time을 적용</p>
</blockquote>
<pre><code class="language-js">let lastUpdateTime = performance.now();

k.onUpdate(() =&gt; {
  if (!isGameStarted.value) return;

  const currentTime = performance.now();
  const deltaTime = (currentTime - lastUpdateTime) / 1000;
  lastUpdateTime = currentTime;

  for (let i = 0; i &lt; obstaclesLayer.parts.length; i++) {
    const currentPart = obstaclesLayer.parts[i];
    const nextPart = obstaclesLayer.parts[(i + 1) % obstaclesLayer.parts.length];

    if (currentPart.pos.x &lt; -IMAGE_WIDTH) {
      currentPart.pos.x = nextPart.pos.x + IMAGE_WIDTH;
    }

    currentPart.move(obstaclesLayer.speed * deltaTime * 60, 0);
  }

  obstaclesLayer.speed -= 5 * deltaTime; // 속도 변화도 deltaTime을 반영
});</code></pre>
<ul>
<li><code>performance.now()</code> 를 사용해 밀리초 단위의 프레임 간격을 계산</li>
<li><code>obstaclesLayer.speed -= 5 * deltaTime;</code> 로 속도 감소도 Delta Time 기반으로 조정</li>
</ul>
<blockquote>
<h3 id="🎯-결과">🎯 결과</h3>
</blockquote>
<ul>
<li>주사율(Hz)에 상관없이 장애물이 일정한 속도로 이동 ➡️ 프레임 속도 차이에 따른 게임 속도 문제 해결</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ COMMA 아카이브 ] #3. 나의 Dev-Log : 플래피 부 게임 구현]]></title>
            <link>https://velog.io/@swallowed_o0/COMMA-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-3.-%EB%82%98%EC%9D%98-Dev-Log-%ED%94%8C%EB%9E%98%ED%94%BC-%EB%B6%80-%EA%B2%8C%EC%9E%84-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@swallowed_o0/COMMA-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-3.-%EB%82%98%EC%9D%98-Dev-Log-%ED%94%8C%EB%9E%98%ED%94%BC-%EB%B6%80-%EA%B2%8C%EC%9E%84-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Wed, 26 Mar 2025 14:19:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/f9808d77-0cd8-4873-b2d3-220d204436f8/image.png" alt=""></p>
<blockquote>
<h3 id="🕹️-flappy-boo--플래피-부">🕹️ Flappy-Boo : 플래피 부</h3>
<p>유령 캐릭터 &quot;Boo&quot;가 장애물을 피하며 날아가는 단순하면서도 중독성 있는 게임이다. <code>space</code>  키를 조작하여 유령을 점프시키며 장애물에 부딪히지 않고 최대한 오래 비행하는 것이 목표이다.</p>
</blockquote>
<h2 id="📍-kaplay-라이브러리-사용">📍 Kaplay 라이브러리 사용</h2>
<blockquote>
<ul>
<li>이 게임은 <code>Kaplay</code> 라이브러리를 활용하여 구현하였다. <code>Kaplay</code> 는 복잡한 게임 로직을 간결하게 구현할 수 있도록 도와주는 라이브러리이다.</li>
</ul>
</blockquote>
<ul>
<li><code>객체 관리와 충돌 감지, 애니메이션 처리</code> 등 플래피 부 게임을 개발하는 데 있어 필수적인 요소들을 간단한 코드로 구현할 수 있게 도와주었다.</li>
<li>이를 통해 <code>반복적인 코드를 줄이고 직관적인 메서드를 사용하여 빠르게 게임을 개발</code>할 수 있었다.</li>
</ul>
<hr>
<h2 id="🎬-kaplay-초기화-설정">🎬 Kaplay 초기화 설정</h2>
<pre><code class="language-js">k = kaplay({
  width: 1300,
  height: 750,
  letterbox: true,
  global: true,
  canvas: canvas,
});</code></pre>
<ul>
<li><code>kaplay()</code> : 게임 엔진을 초기화하고 캔버스를 설정</li>
<li><code>letterbox</code> : 화면 비율 유지를 위해 레터박스를 추가</li>
<li><code>global</code> : 전역으로 접근할 수 있도록 설정</li>
<li><code>canvas</code> : 사용할 HTML 캔버스를 지정</li>
</ul>
<br>

<h2 id="🎬-scene-분리--startgame">🎬 Scene 분리 — Start/Game</h2>
<blockquote>
<ul>
<li>게임이 로딩되는 <code>게임 시작 화면</code> 과 <code>실제 게임이 진행되는 게임 화면</code> 을 구분하여 구현하였다.</li>
</ul>
</blockquote>
<ul>
<li>게임 시작 화면에서 <code>play</code> 버튼을 누르거나 <code>space</code> 를 누르면 게임 화면으로 넘어가도록 하였다.</li>
</ul>
<h3 id="✔ㅤ구현-방식">✔ㅤ구현 방식</h3>
<ul>
<li>kaplay의 <code>scene</code> 기능을 사용하여 <code>Start</code>,<code>Game</code> 씬을 나누고 <code>go()</code>로 전환할 수 있게 구현하였다.</li>
<li><code>k.scene(&quot;이름&quot;, 콜백함수)</code> / <code>k.go(&quot;이름&quot;)</code></li>
</ul>
<pre><code class="language-js">// 🏁 Start 씬: 시작 화면
k.scene(&quot;start&quot;, async () =&gt; {
  makeBackground(k);

  const playBtn = k.add([
    k.sprite(&quot;playBtn&quot;),
    k.scale(0.35),
    k.area(),
    k.anchor(&quot;center&quot;),
    k.pos(k.center().x + 20, k.center().y + 40),
  ]);

  playBtn.onClick(goToGame);  // 버튼 클릭 시 main 씬으로 이동
});


// 🎮 Game 씬: 실제 게임 플레이 화면
k.scene(&quot;main&quot;, async () =&gt; {
  let score = 0;
  makeBackground(k);

  // 플레이어 생성 및 컨트롤 설정
  const player = makePlayer(k);
  player.setControls();

  // 충돌 처리
  player.onCollide(&quot;obstacle&quot;, async () =&gt; {
    if (player.isDead) return;
    player.isDead = true;
    isGameStarted.value = false;
    emit(&quot;open-game-over&quot;, score, currentTime.value);
    reset();
  });

  k.onKeyPress(&quot;space&quot;, () =&gt; {
    if (!isGameStarted.value) {
      startGame();
    } else if (!player.isDead) {
      player.jump(400);
    }
  });
});</code></pre>
<br>

<h2 id="⚙️-게임-요소-관리">⚙️ 게임 요소 관리</h2>
<h3 id="1-게임-요소-로드">1. 게임 요소 로드</h3>
<pre><code class="language-js">k.loadSprite(&quot;boo&quot;, &quot;/assets/images/game/flappy/Boo.png&quot;);</code></pre>
<ul>
<li><code>k.loadSprite(&quot;불러올 이름&quot;, &quot;파일 경로&quot;)</code></li>
<li><code>k.sprite(&quot;불러올 이름&quot;)</code> 으로 나중에 사용하도록 준비</li>
<li>즉, 사용할 이미지를 미리 불러오는 역할</li>
</ul>
<pre><code class="language-js">k.loadSound(&quot;jump&quot;, &quot;/assets/images/game/flappy/jump.wav&quot;);
k.play(&quot;jump&quot;, { volume: 0.02 });</code></pre>
<ul>
<li><code>k.loadSound(&quot;불러올 이름&quot;, &quot;파일 경로&quot;)</code> : 사운드를 로드하는 메서드로 게임의 사운드를 </li>
<li><code>k.play(&quot;불러올 이름&quot; {볼륨})</code> : 사운드를 재생</li>
</ul>
<h3 id="2-객체-추가">2. 객체 추가</h3>
<pre><code class="language-js">const player = k.add([k.sprite(&quot;boo&quot;), k.pos(100, 100), k.area()]);</code></pre>
<ul>
<li><code>k.add()</code> : 게임 내 새로운 오브젝트(플레이어, 장애물, 배경 등)를 추가할 때 사용</li>
<li><code>k.pos()</code> : 위치 지정</li>
<li><code>k.area()</code> : 충돌 감지를 위한 <code>hitbox</code> 충돌 영역 지정</li>
<li><code>k.sprite()</code> : 오브젝트에 사용할 이미지 지정</li>
<li><code>k.body()</code> : 물리 속성<pre><code class="language-js">k.setGravity(2500);</code></pre>
</li>
<li>게임의 중력 값 설정</li>
</ul>
<h3 id="3-업데이트-처리">3. 업데이트 처리</h3>
<pre><code class="language-js">k.onUpdate(() =&gt; {
  if (isGameStarted.value) {
    clouds.forEach((cloud) =&gt; {
      cloud.move(cloud.speed, 0);
      if (cloud.pos.x &gt; canvas.width) {
        cloud.pos.x = -cloud.width;}})
}});</code></pre>
<ul>
<li><code>k.onUpdate(callback)</code> : 매 프레임마다 <code>callback</code> 실행</li>
<li>무한 스크롤 구현에 활용 : 화면에 벗어나면 오른쪽으로 다시 재설정</li>
</ul>
<h3 id="4-특정-키-처리">4. 특정 키 처리</h3>
<pre><code class="language-js">k.onKeyPress(&quot;space&quot;, () =&gt; {
  if (!isGameStarted.value) {
    startGame();
  } else if (!player.isDead) {
    player.jump(400);
    if (audioEnabled.value) k.play(&quot;jump&quot;, { volume: 0.02 });
  }
});</code></pre>
<ul>
<li><code>k.onkeyPress(key, callback)</code> : 특정 키가 눌렸을 경우, 1번만 <code>callback</code> 실행</li>
<li>게임의 시작과 플레이어의 점프 이벤트 구현에 활용</li>
</ul>
<h3 id="5-점수-관리">5. 점수 관리</h3>
<pre><code class="language-js">k.loop(1, () =&gt; {
  if (isGameStarted.value &amp;&amp; !player.isDead) {
    score += 50;
    scoreLabel.updateScore(score);}
});</code></pre>
<ul>
<li>주어진 시간 간격마다 콜백함수를 실행</li>
<li><code>k.loop(interval, callback)</code></li>
<li>게임 내 점수를 주기적으로 증가 시키는 로직에 활용</li>
</ul>
<h3 id="6-충돌-감지">6. 충돌 감지</h3>
<pre><code class="language-js">player.onCollide(&quot;obstacle&quot;, async () =&gt; {
  if (player.isDead) return;

  if (audioEnabled.value) k.play(&quot;hurt&quot;);
  player.isDead = true;
  player.disableControls();
  isGameStarted.value = false;
  obstaclesLayer.speed = 0;
  map.speed = 0;

  stop();
  emit(&quot;open-game-over&quot;, score, currentTime.value);
  reset();
});</code></pre>
<ul>
<li><code>player.onCollide(tag, callback)</code> : 특정 태그가 부딪혔을 때, 1번만 <code>callback</code> 실행</li>
<li>플레이어가 장애물에 부딪혔을 경우 충돌 감지에 활용</li>
</ul>
<h3 id="7-카메라-세팅">7. 카메라 세팅</h3>
<pre><code class="language-js">k.setCamScale(k.vec2(1.2));
player.onUpdate(() =&gt; {
  if (isGameStarted.value &amp;&amp; !player.isDead) {
    k.setCamPos(player.pos.x + 100, 400);}
});</code></pre>
<ul>
<li>카메라의 확대/축소 비율과 위치 조정</li>
<li>게임 내 카메라의 시야를 조정하여 플레이어의 움직임에 따라 카메라가 따라가도록 설정</li>
</ul>
<br>

<hr>
<h2 id="💡-화면-크기-대응">💡 화면 크기 대응</h2>
<blockquote>
<h3 id="🚨-문제점">🚨 문제점</h3>
<p>모니터의 해상도가 다를 때, 게임 캔버스 크기가 고정되어 있으면 너무 작거나 크게 보이는 문제가 발생</p>
</blockquote>
<blockquote>
<h3 id="✅-해결">✅ 해결</h3>
<p><code>window.addEventListener(&quot;resize&quot;, setCanvasSize)</code>를 사용하여 화면 크기가 변경될 때마다 캔버스 크기를 조절하도록 구현</p>
</blockquote>
<pre><code class="language-js">// 캔버스 크기 조정 함수
function setCanvasSize() {
  const width = window.innerWidth;
  const height = window.innerHeight;

  game.setSize(width, height);
}

// 초기 캔버스 설정 및 리사이즈 이벤트 연결
setCanvasSize();
window.addEventListener(&quot;resize&quot;, setCanvasSize);</code></pre>
<br/>


<h2 id="💡-장애물-생성-및-무한-스크롤-구현">💡 장애물 생성 및 무한 스크롤 구현</h2>
<blockquote>
<h3 id="🚨-문제점-1">🚨 문제점</h3>
<p>장애물이 1번이 아닌 플레이어가 게임 오버 될 때까지 무한히 생성되어야 하며, 장애물을 전부 그릴 수 없기 때문에 1개의 장애물 파일로 계속 재활용해야 하는 상황</p>
</blockquote>
<blockquote>
<h3 id="✅-해결-1">✅ 해결</h3>
<p><code>onUpdate</code> 메서드를 사용하여 장애물을 주기적으로 생성하고 화면 밖으로 나간 장애물을 다시 오른쪽 끝으로 재배치하는 로직을 구현</p>
</blockquote>
<pre><code class="language-js">// 장애물 배열 및 속도 설정
const obstaclesLayer = {
  speed: -100,
  parts: [
    k.add([
      k.sprite(&quot;obstacles&quot;),
      k.pos(0, 0),
      k.area(),
      k.scale(SCALE_FACTOR),
    ]),
    k.add([
      k.sprite(&quot;obstacles&quot;),
      k.pos(IMAGE_WIDTH, 0),
      k.area(),
      k.scale(SCALE_FACTOR),
    ]),
  ],
};

// 장애물 무한 스크롤 처리
k.onUpdate(() =&gt; {
  const currentTime = performance.now();
  const deltaTime = (currentTime - lastUpdateTime) / 1000;
  lastUpdateTime = currentTime;

  if (isGameStarted.value) {
    for (let i = 0; i &lt; obstaclesLayer.parts.length; i++) {
      const currentPart = obstaclesLayer.parts[i];
      const nextPart =
        obstaclesLayer.parts[(i + 1) % obstaclesLayer.parts.length];

      if (currentPart.pos.x &lt; -IMAGE_WIDTH) {
        currentPart.pos.x = nextPart.pos.x + IMAGE_WIDTH;
      }
      currentPart.move(obstaclesLayer.speed * deltaTime * 60, 0);
    }
  }

  if (isGameStarted.value) {
    obstaclesLayer.speed -= 5 * deltaTime;
  }
});
</code></pre>
<ul>
<li><code>obstaclesLayer</code> 로 2개의 장애물 파트를 배열로 관리하고 각각의 장애물이 화면 밖으로 나가면 다른 장애물 뒤로 재배치</li>
<li><code>k.onUpdate</code> 를 사용하여 모든 장애물을 매 프레임마다 왼쪽으로 이동시키고 화면 밖으로 나가면 다시 오른쪽 끝으로 보내는 방식으로 무한 스크롤을 구현</li>
</ul>
<blockquote>
<h3 id="📎-참고">📎 참고</h3>
</blockquote>
<ul>
<li><a href="https://jslegenddev.substack.com/p/how-to-implement-infinite-parallax">무한 스크롤 구현 참고 아티클</a></li>
</ul>
<br/>

<h2 id="💡-난이도-상승">💡 난이도 상승</h2>
<blockquote>
<h3 id="🚨-문제점-2">🚨 문제점</h3>
<p>똑같은 장애물이 반복되기 때문에 사용자들이 재미를 느끼려면 점차 난이도가 상승되는 포인트가 필요</p>
</blockquote>
<blockquote>
<h3 id="✅-해결-2">✅ 해결</h3>
<p>게임이 진행됨에 따라 장애물의 속도를 점차 증가시켜 난이도를 증가시키는 로직 추가</p>
</blockquote>
<pre><code class="language-js">if (isGameStarted.value) {
  obstaclesLayer.speed -= 5 * deltaTime;
}</code></pre>
<ul>
<li>초기에 장애물의 기본 속도는 <code>-100</code> 으로 설정 ( 왼쪽 방향으로 이동 )</li>
<li>매 프레임마다 <code>5*deltaTime</code> 만큼 감소</li>
</ul>
<br/>

<h2 id="💡-충돌-감지-및-게임-오버-처리">💡 충돌 감지 및 게임 오버 처리</h2>
<blockquote>
<h3 id="🚨-문제점-3">🚨 문제점</h3>
<p>플레이어와 장애물이 부딪혔을 시, 게임 오버 처리를 위해 충돌 감지 로직이 필요</p>
</blockquote>
<blockquote>
<h3 id="✅-해결-3">✅ 해결</h3>
<p><code>onCollide</code>를 사용하여 플레이어와 장애물 충돌을 감지하고 충돌 시 게임 오버 화면으로 전환하도록 구현</p>
</blockquote>
<pre><code class="language-js">// 플레이어 충돌 감지 및 게임 오버 처리
player.onCollide(&quot;obstacle&quot;, async () =&gt; {
  if (player.isDead) return;

  if (audioEnabled.value) k.play(&quot;hurt&quot;);
  player.isDead = true;
  player.disableControls();
  isGameStarted.value = false;
  obstaclesLayer.speed = 0;
  map.speed = 0;

  stop();

  // 게임 오버 화면 호출
  emit(&quot;open-game-over&quot;, score, currentTime.value);
  reset();
});</code></pre>
<ul>
<li>플레이어가 장애물과 충돌할 때 게임 오버가 발생하도록 처리</li>
<li>충돌 후에는 <code>player.isDead=true</code> 로 설정하고 장애물과의 상호작용을 비활성화</li>
</ul>
<br/>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ COMMA 아카이브 ] #2. 나의 Dev-Log : 트러블슈팅_한글 파일명 업로드 오류]]></title>
            <link>https://velog.io/@swallowed_o0/Plaist-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-2.-%EB%82%98%EC%9D%98-Dev-Log-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%ED%95%9C%EA%B8%80-%ED%8C%8C%EC%9D%BC%EB%AA%85-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@swallowed_o0/Plaist-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-2.-%EB%82%98%EC%9D%98-Dev-Log-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%ED%95%9C%EA%B8%80-%ED%8C%8C%EC%9D%BC%EB%AA%85-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Wed, 26 Mar 2025 12:02:25 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-파일명-ascii-변환">📌 파일명 ASCII 변환</h2>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/aaeff7c8-1f41-4478-9b36-e5550d8c7f59/image.png" alt=""></p>
<blockquote>
<h3 id="🚨-문제점">🚨 문제점</h3>
<p>사용자가 이미지를 업로드할 때 이미지의 파일명이 한글로 된 경우 업로드 되지 않는 오류가 발생했다.</p>
</blockquote>
<blockquote>
<h3 id="💡-문제-분석">💡 문제 분석</h3>
</blockquote>
<ul>
<li><code>한글 파일 이름 인코딩 문제</code> : 파일명에 한글이 포함된 경우, 서버에서 이를 처리하는 데 문제가 발생할 수 있다. 이는 URL 인코딩이나 파일 시스템에서 한글을 처리할 수 없는 경우로 인해 업로드 실패가 발생</li>
<li><code>브라우저의 파일 전송 문제</code> : 같은 이유로 브라우저가 한글 파일명을 URL로 올바르게 인코딩하지 못하는 경우 서버가 이를 처리하지 못하고 오류를 발생시킬 수 있다.</li>
</ul>
<h3 id="✅-올바르게-인코딩-할-수-있도록-수정">✅ 올바르게 인코딩 할 수 있도록 수정</h3>
<ul>
<li>파일명을 <code>ASCII</code>로 변환 : 한글 파일명을 ASCII로 변환하여 서버에서 인식할 수 있게 만든다. 이를 위해 <code>normalize</code> 메서드를 사용하여 유니코드를 정규화하고, 특수 문자를 제거하여 <code>ASCII</code> 문자만 남도록 처리한다.</li>
</ul>
<pre><code class="language-js">const sanitizeFileName = (name) =&gt; {
  const extension = name.split(&#39;.&#39;).pop();
  const baseName = name.replace(/\.[^/.]+$/, &quot;&quot;);
  const sanitizedBaseName = baseName
    .normalize(&quot;NFD&quot;) // 유니코드 정규화
    .replace(/[\u0300-\u036f]/g, &quot;&quot;) // 발음 구별 기호 제거
    .replace(/[^a-zA-Z0-9-_]/g, &quot;&quot;); // 알파벳, 숫자, -, _ 만 허용
  return `${sanitizedBaseName}-${Date.now()}.${extension}`;
};</code></pre>
<ul>
<li>파일 객체 유효성 확인 : 업로드할 파일이 브라우저에서 기본 제공하는 <code>File</code> 객체인지 확인하고 유효한 형식인지 검사하는 로직 추가<pre><code class="language-js">if (!(file instanceof File)) {
throw new Error(&quot;유효하지 않은 파일 형식입니다.&quot;);
}</code></pre>
<img src="https://velog.velcdn.com/images/swallowed_o0/post/7c55479b-500a-4ff5-8674-5e1bc5d5a3a3/image.png" alt=""></li>
</ul>
<blockquote>
<p>수정 후 정상적으로 업로드 되는 것을 확인!</p>
</blockquote>
<hr>
<h2 id="🔗-ascii-파일명-변환-방식의-장점">🔗 ASCII 파일명 변환 방식의 장점</h2>
<h3 id="✔ㅤ서버-호환성-향상">✔ㅤ서버 호환성 향상</h3>
<ul>
<li>ASCII 변환을 통해 한글, 특수문자, 공백 등이 포함된 파일 이름을 안전한 형식으로 변경, 이로 인해 서버나 파일 시스템에서 파일 이름 인코딩 문제를 방지</li>
</ul>
<h3 id="✔ㅤurl-안정성-확보">✔ㅤURL 안정성 확보</h3>
<ul>
<li>파일명이 URL 인코딩 없이도 안전하게 사용할 수 있다. URL에서 잘못 처리될 수 있는 문자를 제거하므로 업로드된 파일의 접근 URL이 더 간결하고 안정적이다.</li>
</ul>
<h3 id="✔ㅤ간단한-구현">✔ㅤ간단한 구현</h3>
<ul>
<li>파일명을 ASCII로 변환하는 로직은 비교적 간단하고 대부분 환경에서 호환된다.</li>
</ul>
<hr>
<h2 id="🔗-normalize">🔗 normalize()</h2>
<blockquote>
<p>문자열을 유니코드 정규화 형태로 변환하는 메서드이다. 유니코드 정규화란? 유니코드 문자들을 표준화된 형태로 변환하여 같은 의미의 문자가 여러 가지 방식으로 표현되지 않도록 하는 방법이다.</p>
</blockquote>
<h3 id="✔ㅤ주요-용도">✔ㅤ주요 용도</h3>
<ul>
<li>발음 구별 기호 제거</li>
<li>문자 결합 및 분리</li>
</ul>
<h3 id="✔ㅤ사용법">✔ㅤ사용법</h3>
<blockquote>
<p>문자열을 특정 형태로 변환하는데, 가장 일반적으로 사용되는 것은 <code>NFC</code>, <code>NFD</code>, <code>NFKC</code>, <code>NFKD</code> </p>
</blockquote>
<ul>
<li><p><code>NFC</code> : 문자를 합성된 형태로 변환</p>
</li>
<li><p><code>NFD</code> : 문자를 분리된 형태로 변환</p>
</li>
<li><p><code>NFKC</code> : 문자들의 호환성을 고려하여 변환</p>
</li>
<li><p><code>NFKD</code> : <code>NFKC</code> 와 유사하지만, 분리된 형태로 변환</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ COMMA 아카이브 ] #1. 나의 Dev-Log : 다중 이미지 업로드 구현]]></title>
            <link>https://velog.io/@swallowed_o0/COMMA-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-1.-%EB%82%98%EC%9D%98-Dev-Log-%EB%8B%A4%EC%A4%91-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@swallowed_o0/COMMA-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-1.-%EB%82%98%EC%9D%98-Dev-Log-%EB%8B%A4%EC%A4%91-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Wed, 26 Mar 2025 10:45:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/00cdef07-d3cb-4683-aee3-9594ad72a656/image.jpg" alt=""></p>
<blockquote>
<p>구성 흐름 : <code>파일 선택 → 미리보기 생성 → 상위 컴포넌트로 데이터 전달 → 서버 업로드 준비 → 업로드</code></p>
</blockquote>
<h2 id="🔎-컴포넌트-구조">🔎 컴포넌트 구조</h2>
<h3 id="📍-posteditimg">📍 <code>PostEditImg</code></h3>
<pre><code>: 다중 이미지 업로드 UI와 로직을 담당하는 부모 컴포넌트</code></pre><ul>
<li>최대 4개의 이미지 슬롯</li>
<li>이미지 미리보기와 업로드 트리거 관리</li>
</ul>
<h3 id="📍-posteditimgcard">📍 <code>PostEditImgCard</code></h3>
<pre><code>: 개별 이미지 카드를 렌더링하는 자식 컴포넌트</code></pre><ul>
<li>이미지가 있으면 미리보기를 표시하고, 없으면 업로드 버튼(+)을 표시</li>
<li>크기와 투명도를 props를 받아 동적으로 스타일링</li>
</ul>
<blockquote>
<h3 id="🎨-이미지-업로드-ui">🎨 이미지 업로드 UI</h3>
</blockquote>
<ul>
<li><code>&lt;input type=&quot;file&quot; multiple&gt;</code>를 4개 생성하여 각 슬롯에 연결</li>
<li><code>v-for</code>를 사용해 인덱스 별로 파일 입력을 동적으로 생성</li>
<li><code>hidden</code> 클래스로 input을 숨기고, <code>PostEditImgCard</code> 클릭 시 <code>triggerFileSelect</code>로 파일 선택을 트리거</li>
<li>이미지 업로드 후 미리보기는 <code>URL.createObjectURl(file)</code> 로 생성</li>
</ul>
<hr>
<h2 id="🔗-임시-url-생성">🔗 임시 URL 생성</h2>
<ul>
<li><code>URL.createObjectURL(file)</code> 은 파일 객체를 가리키는 임시 URL을 생성해준다. 이 URLdmf <code>&lt;img&gt;</code> 태그의 <code>src</code> 속성에 설정하면 사용자가 업로드한 이미지를 화면에 미리보기로 보여줄 수 있다.</li>
</ul>
<h3 id="✔ㅤ메모리-관리">✔ㅤ메모리 관리</h3>
<ul>
<li><code>createObjectURL</code> 로 생성된 URL은 브라우저가 자동으로 해제하지 않기 때문에, 불필요한 메모리 사용을 방지하려면 사용이 끝난 후 <code>URL.revokeObjectURL(url)</code>을 호출하여 해제해야 한다.</li>
</ul>
<h3 id="-기존-코드-">[ 기존 코드 ]</h3>
<pre><code class="language-js">handleFileChange(event, index) {
  const files = event.target.files;
  if (!files || files.length === 0) return;

  const newImages = Array.from(files).map((file) =&gt; ({
    file,
    preview: URL.createObjectURL(file),
  }));

  this.$emit(&quot;addImage&quot;, { index, images: newImages });
  event.target.value = &quot;&quot;;
},</code></pre>
<h3 id="-수정-코드-">[ 수정 코드 ]</h3>
<pre><code class="language-js">handleFileChange(event, index) {
  const files = event.target.files;
  if (!files || files.length === 0) return;

  if (this.images[index] &amp;&amp; this.images[index].preview) {
    URL.revokeObjectURL(this.images[index].preview);
  }

  const newImages = Array.from(files).map((file) =&gt; ({
    file,
    preview: URL.createObjectURL(file),
  }));

  this.$emit(&quot;addImage&quot;, { index, images: newImages });
  event.target.value = &quot;&quot;;
},</code></pre>
<blockquote>
<h3 id="💬-코멘트">💬 코멘트</h3>
<p>미리보기 구현에만 집중하다 보니, 이후의 처리를 충분히 고려하지 못했던 것 같다. 그래서 <code>URL.createObjectURL</code> 을 적용했지만, 메모리 누수를 방지하기 위해 <code>URL.revokeObjectURL</code>을 사용해야 한다는 점을 간과했다. 앞으로는 이러한 후처리까지 신중하게 고려하며 개발해야겠다.</p>
</blockquote>
<br/>

<hr>
<h2 id="🚨-이미지-추가-시-슬롯-위치-벗어남">🚨 이미지 추가 시 슬롯 위치 벗어남</h2>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/61771197-b6a9-43b2-a9d1-a28a7d92a8bb/image.png" alt=""></p>
<blockquote>
<h3 id="🚨-문제점">🚨 문제점</h3>
<p>이미지를 추가하면 의도된 슬롯에 위치해야 하지만, 제대로 렌더링 되지 않고, 레이아웃을 벗어나 예상치 못한 위치에 렌더링 되는 문제 발생</p>
</blockquote>
<pre><code class="language-vue">&lt;post-edit-img-card
  v-if=&quot;images.length &gt; 0&quot;
  :imgSrc=&quot;images[0].preview || images[0]&quot;
  :size=&quot;&#39;w-[400px] h-[400px] lg:w-[440px] lg:h-[440px]&#39;&quot;
  :opacity=&quot;100&quot;
  @click=&quot;handleRemoveImage(0)&quot;
&gt;&lt;/post-edit-img-card&gt;</code></pre>
<h3 id="📎-vue의-동작-방식">📎 Vue의 동작 방식</h3>
<ul>
<li>vue는 화면을 그리기 위해 <code>Virtual DOM</code> 이라는 가상 화면을 사용한다. 이 가상 화면은 실제 HTML을 미리 계산해서 효율적으로 업데이트 한다.</li>
<li><code>v-if</code>만 있을 때는, 조건이 거짓일 때 그 부분이 없어지는데, vue는 혼란스러울 수 있다.</li>
<li><code>postEditImgCard</code> 는 같은 컴포넌트를 재사용하기 때문에 이 부분에서 혼란이 생길 수 있다.</li>
</ul>
<blockquote>
<h3 id="💡-문제-분석">💡 문제 분석</h3>
</blockquote>
<ul>
<li><code>images.length &gt; 0</code> 일 때 0번 슬롯에 미리보기를 보여주고 클릭 시 해당 이미지를 삭제하게 조건부 처리를 해둔 상태</li>
<li>하지만 <code>images.length &gt; 0</code>이 거짓일 때 어떻게 할지 명확하게 지정되어 있지 않고 있다.</li>
<li>결과적으로 이미지를 추가하면, 0번 슬롯에 미리보기가 들어가야 하지만 Vue가 레이아웃을 잘못 재정렬해서 다른 곳에 배치되게 만들었다는 가정</li>
</ul>
<pre><code class="language-vue">&lt;post-edit-img-card
  v-else
  @click=&quot;triggerFileSelect(0)&quot;
&gt;&lt;/post-edit-img-card&gt;</code></pre>
<blockquote>
<h3 id="✅--v-else-를-추가">✅  v-else 를 추가</h3>
</blockquote>
<ul>
<li><code>v-else</code>를 추가하여 <code>images.length &gt; 0</code>이 거짓일 때 렌더링 할 조건을 명확하게 함</li>
<li><code>v-else</code>가 없었을 때는 갱신 과정이 불안정해서 0번 슬롯이 레이아웃을 벗어났지만, 규칙이 제대로 잡히니 깔끔하게 갱신됨 </li>
</ul>
<blockquote>
<h3 id="🧐-다른-해결법--key-속성으로-위치-고정">🧐 다른 해결법 : key 속성으로 위치 고정</h3>
</blockquote>
<ul>
<li><code>key</code> 속성을 주어서, Vue가 같은 컴포넌트를 새로 그리더라도 각각을 구분할 수 있게 함<pre><code class="language-js">&lt;post-edit-img-card
v-if=&quot;images.length &gt; 0&quot;
:key=&quot;&#39;image-&#39; + images[0].preview&quot;
:imgSrc=&quot;images[0].preview&quot;
&gt;&lt;/post-edit-img-card&gt;</code></pre>
</li>
</ul>
<hr>
<h2 id="📍-구현-로직-요약">📍 구현 로직 요약</h2>
<pre><code class="language-vue">&lt;!-- 이미지 영역 --&gt;
&lt;post-edit-img-card
  v-if=&quot;images.length &gt; 0&quot;
  :imgSrc=&quot;images[0].preview || images[0]&quot;
  :size=&quot;&#39;w-[400px] h-[400px] lg:w-[440px] lg:h-[440px]&#39;&quot;
  :opacity=&quot;100&quot;
  @click=&quot;handleRemoveImage(0)&quot;
&gt;&lt;/post-edit-img-card&gt;</code></pre>
<ul>
<li>게시글 업로드와 수정 기능에 컴포넌트를 같이 사용하므로, 업로드된 첫 번째 이미지가 있으면 렌더링하여 표시하도록 했다.</li>
<li>또한 <code>@click</code> 이벤트를 추가하여 클릭 시 해당 이미지를 삭제할 수 있게 했다.</li>
</ul>
<pre><code class="language-vue">&lt;post-edit-img-card
  v-else
  @click=&quot;triggerFileSelect(0)&quot;
&gt;&lt;/post-edit-img-card&gt;</code></pre>
<ul>
<li>만약 첫 번째 이미지가 없다면, 클릭 시 파일 업로드가 되도록 했다.</li>
</ul>
<br/>


<blockquote>
<h3 id="🛠️-추가-개선점">🛠️ 추가 개선점</h3>
</blockquote>
<ul>
<li>드래그 앤 드랍 기능 추가 : 이미지 업로드 UX 개선</li>
<li>이미지 추가/삭제 시 애니메이션 추가 : 부드러운 전환</li>
<li>파일 크기 유효성 검사 : 용량이 클 경우에 대한 검증 부재</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ COMMA 아카이브 ] #0. 프로젝트 기획 및 회고 : Background]]></title>
            <link>https://velog.io/@swallowed_o0/COMMA-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-0.-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D-%EB%B0%8F-%ED%9A%8C%EA%B3%A0-Background</link>
            <guid>https://velog.io/@swallowed_o0/COMMA-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-0.-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D-%EB%B0%8F-%ED%9A%8C%EA%B3%A0-Background</guid>
            <pubDate>Thu, 13 Mar 2025 00:52:44 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/161d831c-ac1a-4335-93de-11084912477b/image.jpg" alt=""></p>
<h2 id="💬-comma-의-배경">💬 COMMA 의 배경</h2>
<h3 id="📍-어떤-니즈가-있을까">📍 어떤 니즈가 있을까?</h3>
<blockquote>
<p>현대 사회는 고물가, 빠듯한 직장 생활, 치열한 학업 등으로 인해 많은 사람들이 스트레스를 해소할 공간을 찾고 있다. 스트레스 해소 방법을 고민하던 중, 문득 게임이 떠올랐다. 게임을 즐기는 짧은 순간에도 도파민이 분비되어 정서적 안정감을 주는 등, 게임은 훌륭한 스트레스 해소 수단이 될 수 있을 것 같았다.</p>
</blockquote>
<blockquote>
<p>또한 자료 조사에 따르면, 현대 사회에서 레트로 문화가 하나의 휴식 방법으로 자리 잡고 있으며, 과거의 경험을 재현하는 것이 심리적 위안을 준다는 분석이 나왔다.</p>
</blockquote>
<br>

<h3 id="📍-어떻게-도출되었을까">📍 어떻게 도출되었을까?</h3>
<blockquote>
<p>그렇게 <code>&quot;바쁜 현대인들에게 잠깐의 여유를 제공하고, 어린 시절 오락실의 감성을 되살릴 수 있는 공간이 있다면 어떨까?&quot;</code> 하는 생각이 도출되었고 자연스럽게 <code>쉬다 -&gt; 쉼 -&gt; 쉼표</code> 로 이어져 &quot;추억을 꺠우는 즐거운 쉼표!&quot; 의 COMMA가 탄생했다.</p>
</blockquote>
<blockquote>
<p>COMMA는 향수를 자극하는 경험과 새로운 소셜 연결을 동시에 제공하는 미니게임&amp;커뮤니티 플랫폼으로 누구나 쉽게 접근할 수 있는 미니게임으로 짧은 시간 동안 스트레스를 해소하고 성취감을 느낄 수 있다.</p>
</blockquote>
<br>

<hr>
<h2 id="💬-comma의-기획">💬 COMMA의 기획</h2>
<h3 id="📍-주요-기능-및-게임-종류">📍 주요 기능 및 게임 종류</h3>
<ul>
<li>소셜 로그인</li>
<li>전광판</li>
<li>게임 커뮤니티</li>
<li>게임 (테트리스 / 플래피부 / 바운스볼 / 슈팅게임 / 지뢰찾기 )</li>
<li>자유 커뮤니티</li>
<li>게시글 작성/수정</li>
<li>댓글</li>
<li>랭킹</li>
<li>마이페이지</li>
</ul>
<h3 id="📍-ia">📍 IA</h3>
<blockquote>
<p>서비스의 전반적인 기능과 메뉴 구조를 볼 수 있게 작성하였다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/swallowed_o0/post/31ce0647-6dc5-4bbb-a6f8-1d0875bbec6f/image.png" width=80%>ㅤ</p>
<h2 id="💬-kpt-회고">💬 KPT 회고</h2>
<blockquote>
<h3 id="🚀-keep">🚀 Keep</h3>
</blockquote>
<ul>
<li>퍼블리싱이 확실히 익숙해지고 빨라졌다는 것을 느낄 수 있었다. 다른 팀원분들 것도 도와주면서 더 많이 익숙해진 것 같다. </li>
<li>가장 뿌듯했던 건 이번에 처음으로 라이브러리를 활용해보았는데, 라이브러리는 이렇게 활용하고 사용하는 거구나를 알 수 있는 계기가 된 것 같아서 잘한 선택인 것 같다.</li>
</ul>
<blockquote>
<h3 id="🚨-problem">🚨 Problem</h3>
</blockquote>
<ul>
<li>가장 아쉬웠던 점은 <code>supabase</code> 를 공부하고 싶었는데, 그렇게 하지 못했다는 점이다. 아무래도 기간이 3주밖에 없어서 잘 아는 팀원이 붙어서 빠르게 작업을 할 수밖에 없었다. 하지만 배워가는 시기인데, 한 번 무모하게 도전을 해봐도 좋았을 거라는 생각이 든다.</li>
<li>그래서 이번 기회에 짜여져있는 것들을 참고하면서 <code>supabase</code> 에 대해서 좀 더 공부해보려고 한다.</li>
</ul>
<blockquote>
<h3 id="🔥-try">🔥 Try</h3>
</blockquote>
<ul>
<li>이번 프로젝트는 <code>Vue</code> 로 진행했는데, 이걸 다시 <code>React</code>로 마이그레이션 해보려고 한다. </li>
<li>또한 <code>Supabase</code> 백업본을 받아서 이걸 옮기는 작업도 해보면서 한번 유지 관리를 해보려고 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚 코딩 테스트 합격자 되기 - 자바스크립트 편 #04]]></title>
            <link>https://velog.io/@swallowed_o0/%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%A9%EA%B2%A9%EC%9E%90-%EB%90%98%EA%B8%B0-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%8E%B8-04</link>
            <guid>https://velog.io/@swallowed_o0/%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%A9%EA%B2%A9%EC%9E%90-%EB%90%98%EA%B8%B0-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%8E%B8-04</guid>
            <pubDate>Sun, 19 Jan 2025 13:17:27 GMT</pubDate>
            <description><![CDATA[<h2 id="🎯-학습-목표">🎯 학습 목표</h2>
<hr>
<blockquote>
<ul>
<li><code>학습 방식</code>  : 챕터별로 함께 읽고 공유 및 문제 풀이</li>
</ul>
</blockquote>
<ul>
<li><code>학습 시간</code> : AM 10:00 ~ PM 12:00</li>
<li><code>학습 목표</code> : ~ 09. 트리</li>
</ul>
<br>


<h2 id="💬-새로-알게-된-것">💬 새로 알게 된 것</h2>
<hr>
<h3 id="01-트리">01. 트리</h3>
<blockquote>
<h3 id="📕--p-289---09-트리">📕 [ P. 289 - 09 트리]</h3>
<p>&quot; 트리는 데이터를 저장하고 탐색하기에 유용한 구조를 갖고 있습니다... 프로그래밍 분야에서는 트리는 계층 구조를 표현하는 용도로 많이 사용합니다.&quot;</p>
</blockquote>
<ul>
<li><code>자동 완성 기능</code> : 트리는 문자열 처리에도 많이 활용된다. 예를 들어 검색 엔진에서 <code>자동 검색어 추천 기능</code> 도 <code>트라이</code> 라는 독특한 트리 구조를 활용한 것이다. 이를 활용하면 <code>접두사나 패턴 검색을 쉽게 할 수 있다.</code></li>
<li><code>데이터베이스</code> : 데이터를 쉽게 검색, 삽입, 삭제를 할 수 있도록 트리를 활용해서 데이터를 구조화하고 인덱싱한다. 이때 <code>B-트리</code> 나 <code>B+트리</code> 를 많이 사용한다.</li>
</ul>
<br>

<p>(1) 트리의 구조</p>
<ul>
<li><code>노드</code> : 트리를 구성하는 요소로 간선으로 연결된 노드들은 <code>부모-자식</code> 관계가 있다고 표현한다. 가장 맨 위에 있는 노드를 <code>루트 노드</code> 이고 상대적으로 위에 있는 노드를 <code>부모 노드</code> , 아래에 있는 노드를 <code>자식 노드</code> 라고 한다. 또한, 같은 부모 노드를 갖는 노드는 <code>형제 노드</code> 라고 한다.</li>
<li><code>edge</code> : 노드와 노드 사이를 이어주는 선으로, 단방향 간선으로 연결되 어 있다. 루트 노드로부터 특정 노드까지 거쳐가는 최소한의 간선 수를 <code>레벨</code> 로 표현한다.</li>
<li><code>리프 노드</code> : 자식이 없는 노드이다. 우리말로는 말단 노드라고 한다.</li>
<li><code>차수(degree)</code> : 특정 노드에서 아래로 향하는 간선의 개수이다.</li>
</ul>
<br>

<h3 id="02-이진-트리">02. 이진 트리</h3>
<blockquote>
<h3 id="📕--p-291---09-트리-">📕 [ P. 291 - 09 트리 ]</h3>
<p>&quot; 트리 종류는 굉장히 다양하지만 코딩 테스트에서는 이진 트리만 제대로 알고 있으면 충분합니다. <code>이진 트리란 모든 노드의 최대 차수가 2를 넘지 않는 트리</code> 를 말합니다. ( 간선이 최대 2개인 트리입니다. ) &quot;</p>
</blockquote>
<ul>
<li>이진 트리는 <code>배열</code> 이나 <code>포인터</code> 로 구현할 수 있다.</li>
</ul>
<br>

<h3 id="03-배열로-구현">03. 배열로 구현</h3>
<blockquote>
<p>배열은 선형 자료구조이고 트리는 계층 자료구조이다. 따라서 배열로 트리를 표현하려면 다음과 같은 3가지 규칙이 필요하다. </p>
</blockquote>
<ul>
<li>루트 노드는 배열 인덱스 1번에 저장한다.</li>
<li><code>왼쪽 자식 노드</code> 의 배열 인덱스는 <code>부모 노드의 배열 인덱스 X 2</code></li>
<li><code>오른쪽 자식 노드</code> 의 배열 인덱스는 <code>부모 노드의 배열 인덱스 X 2+1</code></li>
</ul>
<blockquote>
<p>⚠️ 루트 노드를 0 혹은 1로 정해야 할 경우, 다르니 주의!!</p>
</blockquote>
<ul>
<li><code>0</code> 의 경우, <code>왼쪽 자식 노드</code>는 <code>부모 노드의 배열 인덱스 X 2 + 1</code> 이 되고, <code>오른쪽 자식 노드</code> 는 <code>부모 노드의 배열 인덱스 X 2 + 2</code> 가 된다.</li>
</ul>
<h3 id="03-2-배열로-표현한-트리">03-2 배열로 표현한 트리</h3>
<blockquote>
<p>트리를 표현한 배열을 보면 빈 값이 꽤 많이 보인다. 노드들의 <code>부모-자식 관계</code> 를 <code>곱셈 연산하여 배열의 인덱스로 사용</code> 하기 떄문에 <code>실제 노드 개수보다 많은 공간을 사용할 수밖에 없다.</code></p>
</blockquote>
<ul>
<li>즉, 배열로 트리를 표현하면 자식이 없거나 쓰지 않는 인덱스들은 모두 빈 값이므로 메모리가 낭비된다는 단점이 있다.</li>
<li>그렇다고 해서 배열 표현 방법이 나쁘지 않다. 메모리만 넉넉하다면 구현 시간을 단축하는 용도로 좋다.</li>
</ul>
<br>

<h3 id="04-이진-트리-순회하기">04. 이진 트리 순회하기</h3>
<p>(1) 전위 순회 ( preorder )</p>
<ul>
<li>현재 노드를 부모 노드로 생각했을 때, <code>부모 노드 → 왼쪽 자식 노드 → 오른쪽 자식 노드</code> 순서로 방문</li>
</ul>
<p>(2) 중위 순회 ( inorder )</p>
<ul>
<li>현재 노드를 부모 노드로 생각했을 때, <code>왼쪽 자식 노드 → 부모 노드 → 오른쪽 자식 노드</code> 순서로 방문</li>
</ul>
<p>(3) 후위 순회 ( postorder )</p>
<ul>
<li>현재 노드를 부모 노드로 생각했을 때, <code>왼쪽 자식 노드 → 오른쪽 자식 노드 → 부모 노드</code> 순서로 방문</li>
</ul>
<br>

<h3 id="05-이진-트리-탐색하기">05. 이진 트리 탐색하기</h3>
<blockquote>
<p>이진 트리에서 가장 중요한 것은 바로 탐색을 효율적으로 할 수 있도록 트리를 구축하는 것이다.</p>
</blockquote>
<ul>
<li>데이터 크기를 따져 현재 노드보다 <code>값이 작으면 왼쪽 자식</code> 위치에, <code>크거나 같으면 오른쪽 자식</code> 위치에 배치하는 독특한 정렬 방식</li>
<li>데이터를 하나씩 삽입하면서 이진 탐색 트리를 구축한다. 즉, <code>삽입과 동시에 정렬을 한다.</code></li>
</ul>
<ol>
<li>찾으려는 값이 현재 노드의 값과 같으면 탐색을 종료하고 크면 오른쪽 노드를 탐색한다.</li>
<li>본인이 찾으려는 값이 현재 노드의 값보다 작으면 왼쪽 노드를 탐색한다.</li>
<li>값을 찾으면 종료한다. 노드가 없을 때까지 계속 탐색했는데 값이 없으면 현재 트리에 값이 없는 것이다.</li>
</ol>
<blockquote>
<p>이러한 이진 탐색 트리의 시간 복잡도는 <code>트리 균형에 의존</code> 한다. 균현이 유지되었다고 가정했을 때, 삽입ㅂ과 탐색 연산 시 이진 탐색 트리에 저장된 노드가 N개라고 하면 <code>O(logN)</code>이다.</p>
</blockquote>
<br>


<h2 id="📌-문제-풀이">📌 문제 풀이</h2>
<hr>
<blockquote>
<h3 id="🎯--트리-순회-문제">🎯  트리 순회 문제</h3>
</blockquote>
<ul>
<li>이진 트리를 표현한 배열 nodes를 인자로 받습니다.</li>
<li>예를 들어 nodes가 [1, 2, 3, 4, 5, 6, 7]이면</li>
<li>이진 트리에 대하여 전위 순회, 중위 순회, 후위 순회 결과를 반환하는 solution() 함수를 구현하세요.<blockquote>
<hr>
<p>✔ 제한 조건</p>
</blockquote>
</li>
<li>입력 노드값의 개수는 1개 이상 1,000개 이하입니다</li>
<li>노드값은 정수형이며, 중복되지 않습니다</li>
</ul>
<blockquote>
<p>✔ 입출력 예</p>
</blockquote>
<ul>
<li>nodes : <code>[1, 2, 3, 4, 5, 6, 7]</code></li>
<li>return : <code>[&quot;1 2 4 5 3 6 7&quot;, &quot;4 2 5 1 6 3 7&quot;, &quot;4 5 2 6 7 3 1&quot;]</code> </li>
</ul>
<h3 id="1--배열로-구현">#1 : 배열로 구현</h3>
<ul>
<li>전위 순회(pre-order) : <code>&quot;현재 노드 -&gt; 왼쪽 자식 -&gt; 오른쪽 자식&quot;</code></li>
<li>중위 순회(in-order) : <code>&quot;왼쪽 자식 -&gt; 현재 노드 -&gt; 오른쪽 자식&quot;</code></li>
<li>후위 순회(post-order) : <code>&quot;왼쪽 자식 -&gt; 오른쪽 자식 -&gt; 현재 노드&quot;</code></li>
</ul>
<pre><code class="language-js">
function preOrderTreeArray(tree, index = 0, result = []) {
  if (index &lt; tree.length &amp;&amp; tree[index] !== undefined) {
    result.push(tree[index]);
    preOrderTreeArray(tree, 2 * index + 1, result);
    preOrderTreeArray(tree, 2 * index + 2, result);
  }
  return result;
}

function inOrderTreeArray(tree, index = 0, result = []) {
  if (index &lt; tree.length &amp;&amp; tree[index] !== undefined) {
    inOrderTreeArray(tree, 2 * index + 1, result);
    result.push(tree[index]);
    inOrderTreeArray(tree, 2 * index + 2, result);
  }
  return result;
}

function postOrderTreeArray(tree, index = 0, result = []) {
  if (index &lt; tree.length &amp;&amp; tree[index] !== undefined) {
    postOrderTreeArray(tree, 2 * index + 1, result);
    postOrderTreeArray(tree, 2 * index + 2, result);
    result.push(tree[index]);
  }
  return result;
}

function treeArray(nodes) {
  const tree = nodes;
  return {
    preOrder: preOrderTreeArray(tree),
    inOrder: inOrderTreeArray(tree),
    postOrder: postOrderTreeArray(tree)
  };
}

const nodes = [1, 2, 3, 4, 5, 6, 7];
const result = treeArray(nodes);
console.log(&#39;Pre-order:&#39;, result.preOrder);
console.log(&#39;In-order:&#39;, result.inOrder);
console.log(&#39;Post-order:&#39;, result.postOrder);
</code></pre>
<br>

<blockquote>
<h3 id="🎯-lv2-예상-대진표">🎯 Lv.2 예상 대진표</h3>
</blockquote>
<ul>
<li>게임 형식 : N명이 참가하는 토너먼트(2의 지수 승). <code>1번↔2번</code> , <code>3번↔4번</code> 등으로 대결하며, 승자는 다음 라운드에 진출</li>
<li>참가자 번호 재배정 : 승자는 다음 라운드에서 <code>1번부터 차례로 번호를 재배정</code> 받음</li>
<li>목표 : A번 참가자와 B번 참가자가 <code>몇 번째 라운드에서 만나는지</code> 구하기<blockquote>
<hr>
<p>✔ 제한 조건</p>
</blockquote>
</li>
<li>N: 21 이상 220 이하 (2의 지수 승)</li>
<li>A, B: N 이하 자연수, A ≠ B</li>
</ul>
<pre><code class="language-js">function solution(n,a,b)
{
    let round = 1;
    while(a!==b) {
        a = Math.ceil(a/2);
        b = Math.ceil(b/2);
        round++;
    }
    return round;
}
</code></pre>
<ul>
<li>a 와 b가 같아질 때까지 반복문을 돌린다.</li>
<li>매 라운드마다 a와 b의 다음 라운드 번호로 갱신해준다.<ul>
<li>모든 그룹은 2명씩 짝이기 때문에 <code>/2</code> 로 나누고 <code>Math.ceil</code> 로 올려주면 다음 라운드 번호가 나온다.</li>
</ul>
</li>
</ul>
<br>



<h2 id="🚀-회고">🚀 회고</h2>
<hr>
<ul>
<li>트리 같은 경우, 정처기를 공부하면서 이미 한 번 봤던 개념이라 좀 더 친숙하게 다가왔던 것 같다.</li>
<li>그리고 앞서 큐나 해시보다는 구현하기가 어렵지 않았다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BaaS] Supabase로 소셜 로그인 기능 구현]]></title>
            <link>https://velog.io/@swallowed_o0/BaaS-Supabase%EB%A1%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@swallowed_o0/BaaS-Supabase%EB%A1%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sat, 18 Jan 2025 12:32:44 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-supabase">📌 Supabase</h2>
<blockquote>
<p>일단 <code>Supabase</code> 를 본격적으로 공부하기 전, 이게 어떤 것이고 어떤 점이 좋은지 알아보자.</p>
</blockquote>
<h3 id="✅ㅤ장점">✅ㅤ장점</h3>
<ul>
<li>오픈 소스 : <code>Supabase</code> 는 오픈 소스 프로젝트로, 코드가 공개되어 있어 커스텀마이징이 가능하다.</li>
<li>PostgreSQL 기반 : <code>PostgreSQL</code> 을 사용하여 데이터베이스ㅡㄹ 관리하므로, 관계형 데이터베이스의 강력한 기능을 활용할 수 있다.</li>
<li>실시간 데이터 : 실시간 데이터베이스 업데이트를 지원하여, 실시간 애플리케이션 개발에 유리하다.</li>
<li>자동 API 생성 : 데이터베이스 테이블을 기반으로 자동으로 <code>RESTFul API</code> 를 생성해 주어, API 서버 구축이 간편하다.</li>
</ul>
<blockquote>
<h3 id="📎ㅤpostgresql-이란">📎ㅤPostgreSQL 이란?</h3>
</blockquote>
<ul>
<li>오픈소스 관계형 데이터베이스 관리 시스템으로, <code>복잡한 쿼리와 트랜잭션을 처리하는 데 강력한 기능을 제공</code>한다.</li>
<li><code>관계형 데이터베이스</code> : 데이터를 테이블 형식으로 저장하고, SQL을 사용해 데이터를 관리한다.</li>
<li><code>ACID 준수</code> : 데이터 무결성 및 일관성을 보장하는 트랜잭션을 지원한다.</li>
<li><code>확장성</code> : 대규모 데이터 처리에 유리하며, 다양한 데이터 타입과 함수 확장이 가능하다.</li>
<li><code>오픈소스</code> : 무료로 사용 가능하며, 커스텀마이징이 가능하다.</li>
<li><code>고급 기능</code> : JSON, XML, GIS 데이터 처리 등 다양한 고급 기능을 제공한다.</li>
</ul>
<h3 id="✅ㅤ단점">✅ㅤ단점</h3>
<ul>
<li>기능 제한 : <code>Firebase</code> 에서 제공하는 일부 고급 기능 (ex. <code>Cloud Functions</code> , <code>Firebase ML</code> 등)은 <code>Supabase</code> 에서는 제공되지 않거나, 구현이 부족할 수 있다.</li>
<li>확장성 : 대규모 애플리케이션에서는 <code>Supabase</code> 의 확장성이 <code>Firebase</code>보다 떨어질 수 있다.</li>
</ul>
<br>

<hr>
<blockquote>
<p>사이드 프로젝트에서 <code>Supabase</code> 를 이용해서 소셜 로그인 기능을 구현하고자. 팀원분들과 각자 공부해오기로 했다. 그래서 <code>소셜 로그인 기능</code> 을 중심으로 정리해보고자 한다.</p>
</blockquote>
<h2 id="📌ㅤsupabase에서의">📌ㅤSupabase에서의...</h2>
<h3 id="1-프로젝트-설정">1. 프로젝트 설정</h3>
<ul>
<li>프로젝트 대시보드 : 프로젝트를 생성하면 대시보드로 이동한다.</li>
<li><code>API 키 확인</code> : 대시보드에서 <code>Project Settings</code> &gt; <code>API</code> 로 가면 <code>anon</code> API 키와 <code>service_role</code> API 키를 확인할 수 있다. ( 이 키를 나중에 사용 )</li>
</ul>
<h3 id="2-인증-설정--소셜-로그인-활성화-">2. 인증 설정 ( 소셜 로그인 활성화 )</h3>
<ul>
<li><p><code>Authentication 설정</code> : 대시보드에서 <code>&quot;Authentication&quot;</code> 메뉴를 클릭하고, <code>&quot;Providers&quot;</code> 탭으로 이동한다.</p>
</li>
<li><p>소셜 로그인 제공자 추가 : <code>&quot;Enable Sign in with ----&quot;</code> 섹션에서 사용할 소셜 로그인을 활성화한다. 예를 들어, Google 로그인을 사용하려면 &quot;Enable Sign in with Google&quot;를 활성화 한다.</p>
<ul>
<li><p><code>Google 로그인 설정</code> : &quot;Client IDs&quot;와 &quot;Client Secret (for OAuth)&quot;가 있는데 이는 아이디와 비밀번호와 같다. Google API 콘솔에서 발급받은 클라이언트 ID와 클라이언트 비밀번호를 입력해야 한다.</p>
</li>
<li><p>🚫 <code>&quot;Skip nonce checks&quot;</code> : 이는 <code>ID 토큰에서 nonce 값을 확인하지 않도록 설정하는 옵션</code> 이다. 즉, 인증 요청과 응답을 매칭하기 위한 고유 식별자로 보통 <code>보안을 강화하기 위해 사용</code> 되며, 이 옵션을 <code>활성화시키면 보안 수준이 낮아질 수 있어서 권장되지는 않는다.</code>  ( 보통은 iOS 앱과 같이 <code>nonce</code> 를 설정할 수 없는 환경에서 사용한다.)</p>
</li>
<li><p><code>Callback URL (for OAuth)</code> : 로그인 인증 후 사용자에게 다시 리디렉션되는 URL로, 만약 Google이면 Google OAuth 인증이 완료되면 사용자가 Supabase로 돌아올 수 있도록 지정된 URL이다. ➡️ 이 URL은 <code>Google API</code> 콘솔에 등록해야 한다. ( 사용자가 Google 로그인 후, 인증 결과를 <code>Supabase가 처리할 수 있도록 연결하는 역할</code> 을 한다. )</p>
<blockquote>
<h3 id="📎ㅤ사용자-로그인-예시-과정">📎ㅤ사용자 로그인 예시 과정</h3>
<ol>
<li>사용자가 로그인 버튼을 클릭 → Google 로그인 화면으로 이동</li>
<li>Google 로그인 성공 → <code>Callback URL로 리디렉션</code></li>
<li><code>Supabase가 이 URL에서 결과를 받아 사용자를 인증 처리</code> 🌟</li>
<li>사용자는 애플리케이션의 화면으로 이동</li>
</ol>
</blockquote>
</li>
</ul>
</li>
</ul>
<pre><code>- Google 개발자 콘솔에 가서 &quot;새 프로젝트 생성&quot; 후, OAuth 2.0 클라이언트 ID를 생성하고, 이를 Supabase 설정에 입력헤야 한다.

&gt; ###  📎ㅤGoogle API 콘솔에서 생성하는 단계
&gt; 1. `Google Cloud Console` 에 접속
&gt; 2. Google 계정으로 로그인 → 로그인 후, 프로젝트를 생성하거나 기존 프로젝트를 선택 할 수 있다.
&gt; 3. 상단의 `&quot;프로젝트 선택&quot;` 을 클릭 → 새 프로젝트 만들기 선택 → 프로젝트 이름 입력, (필요시 조직 선택) 만들기 클릭 → `생성한 프로젝트 선택`
&gt; 4. 왼쪽 메뉴에서 `APls 및 서비스 &gt; OAuth 동의` 클릭 → 외부를 선택하고 만들기 클릭 → `앱 이름` , `사용자 이메일` , `앱 로고(선택)` 를 입력 → `앱 도메인` 입력 → 개인정보처리방침 및 서비스 약관 URL 입력 → 개발자 연락처 정보 → 저장 및 계속
&gt; 5. `API 및 서비스 &gt; 라이브러리` 선택 → 검색창에 `Google Identity Services API` 를 검색 → 활성화
&gt; 6. `API 및 서비스 &gt; 사용자 인증 정보` 선택 → ` + 사용자 인증 정보 만들기` 클릭 후 OAuth 클라이언트 ID를 선택 → 애플리케이션 유형 선택 → 클라이언트 ID 이름을 입력 → Supabase에서 제공한 `Callback URL` 를 입력 → 만들기</code></pre><br>


<h2 id="📌ㅤ프로젝트-파일에서의">📌ㅤ프로젝트 파일에서의...</h2>
<h3 id="1-supabase-클라이언트-설정">1. Supabase 클라이언트 설정</h3>
<ul>
<li><code>Supabase JavaScript 라이브러리 설치</code> : 먼저 <code>Supabase 클라이언트</code> 를 설치해야 한다.<pre><code class="language-bash">npm install @supabase/supabase-js</code></pre>
</li>
<li><code>Supabase 클라이언트 초기화</code> : 프로젝트의 적절한 파일에 Supabase 클라이언트를 초기화 한다.<pre><code class="language-js">import { createClient } from &#39;@supabase/supabase-js&#39;;
</code></pre>
</li>
</ul>
<p>const supabase = createClient(&#39;SUPABASE_URL&#39;, &#39;SUPABASE_ANON_KEY&#39;);</p>
<pre><code>- `SUPABASE_URL` 과 `SUPABASE_ANON_KEY`는 대시보드에서 확인한 `API URL`과 `anon` 키를 입력한다.

### 2. 소셜 로그인 기능 구현
- 로그인 함수 : `signInWithOAuth` 메서드로 소셜 로그인을 처리한다.
```js
const signInWithGoogle = async () =&gt; {
  const { user, session, error } = await supabase.auth.signInWithOAuth({
    provider: &#39;google&#39;,
  });

  if (error) {
    console.error(&#39;로그인 오류:&#39;, error.message);
  } else {
    console.log(&#39;로그인 성공:&#39;, user);
  }
};</code></pre><h3 id="3-로그인-상태-확인-🌟">3. 로그인 상태 확인 🌟</h3>
<ul>
<li><code>로그인 상태 추적</code> : 로그인한 사용자의 정보를 추적하려면 <code>supabase.auth.onAuthStateChange</code> 를 사용하여 로그인 상태가 변경될 때마다 처리할 수 있다.</li>
<li><code>onAuthStateChange</code> 는 로그인, 로그아웃, 세션 갱신 등의 이벤트를 감지한다. ( <code>useEffect</code> 와 함께 사용하기에 적합하다. )<pre><code class="language-js">import { useEffect, useState } from &#39;react&#39;;
</code></pre>
</li>
</ul>
<p>const App = () =&gt; {
  const [user, setUser] = useState(null);</p>
<p>  useEffect(() =&gt; {
    const { data: authListener } = supabase.auth.onAuthStateChange(
      (event, session) =&gt; {
        setUser(session?.user ?? null);
      }
    );</p>
<pre><code>return () =&gt; {
  authListener?.unsubscribe();
};</code></pre><p>  }, []);</p>
<pre><code>
### 4. 로그아웃 기능
```js
const signOut = async () =&gt; {
  await supabase.auth.signOut();
  setUser(null);
};

const LogoutButton = () =&gt; {
  return &lt;button onClick={signOut}&gt;로그아웃&lt;/button&gt;;
};</code></pre><ul>
<li><code>signOut</code> 메서드로 현재 세션을 종료할 수 있다.</li>
<li><code>로그아웃 후 상태를 초기화</code>하려면 <code>setUser(null)</code>처럼 상태를 업데이트해야 한다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>