<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>gahui_dev.log</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 개발자입니다.</description>
        <lastBuildDate>Thu, 20 Nov 2025 07:41:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>gahui_dev.log</title>
            <url>https://velog.velcdn.com/images/gahui_dev/profile/8d91480c-f534-4b79-84f5-17c03726ede6/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. gahui_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/gahui_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Next + PM2 배포 과정 이해하기]]></title>
            <link>https://velog.io/@gahui_dev/Next-PM2-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%A9%B4%EC%84%9C-%EA%B6%81%EA%B8%88%ED%96%88%EB%8D%98-%EA%B2%83%EB%93%A4</link>
            <guid>https://velog.io/@gahui_dev/Next-PM2-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%A9%B4%EC%84%9C-%EA%B6%81%EA%B8%88%ED%96%88%EB%8D%98-%EA%B2%83%EB%93%A4</guid>
            <pubDate>Thu, 20 Nov 2025 07:41:33 GMT</pubDate>
            <description><![CDATA[<p>Next.js 무중단 배포를 GitHub Actions 환경에서 직접 구성해보면서, 자연스럽게 여러 가지 기본적인 궁금증이 생겼다. 특히 <strong>“CI에서 캐시는 왜 쓰는 거지?”</strong>, <strong>“어떤 캐시가 효과가 있고 어떤 건 의미가 없지?”</strong> 같은 부분은 실제로 적용해보지 않으면 감이 잘 오지 않는다.</p>
<p>배포 자동화를 처음 구성할 때는 인터넷에 떠도는 스크립트나 AI가 만들어준 예시들을 그대로 가져다 쓰기 쉽지만, 결국 <strong>내 프로젝트의 구조와 환경에 맞는 선택을 할 수 있어야 한다</strong>는 걸 이번에 깊이 느꼈다.</p>
<p>그래서 배포 파이프라인을 직접 구현하면서, 과정 중에 생겼던 작은 궁금증들—<code>pnpm</code> 설치 방식, <code>pnpm store</code> 캐싱, <code>.next/cache</code> 캐싱, lockfile 전략 등—을 하나씩 검증해보며 정리해 보았다.</p>
<p>이번 글은 그 과정에서 이해한 내용과, 실제로 적용했을 때 어떤 효과가 있었는지 기록한 것이다.</p>
<h1 id="gha에서-pnpm를-설치하는-두가지-방법">GHA에서 pnpm를 설치하는 두가지 방법</h1>
<h2 id="1-npm을-이용하여-전역으로-pnpm을-설치하는-방법">1. npm을 이용하여 전역으로 pnpm을 설치하는 방법</h2>
<pre><code class="language-yaml">  - name: Install pnpm
          run: npm install -g pnpm</code></pre>
<p>이 방법은 <strong>절대절대 권장하지 않는다.</strong> npm을 통해서 pnpm을 설치하면 다음과 같은 문제가 있다. </p>
<ul>
<li><p><strong>느리다. npm자체가 느리다.</strong></p>
<ul>
<li>npm의 의존성 알고리즘 자체가 느리다.<ul>
<li>registry에서 pnpm 패키지를 다운로드해야하고,</li>
<li>pnpm cli내부 sub-dependencies를 처리하고,</li>
<li>global node_modules 경로 생성/검증 하고</li>
<li>모든 dependency를 node_modules에 풀어서 설치해야하고</li>
<li>실행 파일을 <code>/usr/local/bin/pnpm</code> 같은 곳에 symlink해야한다.</li>
</ul>
</li>
<li>이 과정을 매 빌드마다 반복해야 하니 느릴수 밖에 없다.</li>
<li>특히, CI는 매번 컨테이너가 초기화된 상태라서 global npm node_modules이 없고 의존성이 많으면 더 느려진다.</li>
</ul>
</li>
<li><p><strong>pnpm-store 캐시의 연동이 좋지 않다.</strong></p>
<ul>
<li><p>“npm istall -g pnpm&quot; 방식은 GitHub Actions의 toolcache와 연동되지 않는다.</p>
</li>
<li><p>그래서 actions/cache로 ~/pnpm-store를 직접 캐시 설정할 수는 있지만,</p>
</li>
<li><p>pnpm 버전 / 경로 / store 경로 등을 전부 우리가 직접 맞춰줘야 하고,</p>
</li>
<li><p>pnpm/action-setup 처럼 깔끔하게 자동 연동되지 않는다.</p>
</li>
<li><p>실무에서 보면, 관리 비용에 비해 얻는 이점이 적어서 사실상 캐시 활용이 좋지 않다고 보는게 좋다.</p>
</li>
<li><p>일반적으로 npm 전역 설치 경로는 OS / Node /NVM 버전에 따라서 매번 달라지고 github actions은 당연히 이를 예측할수가 없기 때문이다.</p>
  <aside>
  💡

<p>  <strong>Toolcache란,</strong></p>
<p>  CI에서 필요한 실행 파일(Node, pnpm 등)을 한 번 받아두고 다음 워크플로우에서 빠르게 재사용하기 위한 저장소이다. </p>
<p>  pnpm/action-setup같은 액션은 pnpm 바이너리를 이 toolcache에 넣어두고, 필요할 때마다 꺼내 쓰기 때문에 설치 속도가 매우 빠르다.</p>
  </aside>


</li>
</ul>
</li>
</ul>
<h2 id="2-pnpmaction-setup을-사용하는-방법">2. pnpm/action-setup을 사용하는 방법</h2>
<pre><code class="language-yaml">  - name: Setup pnpm
    uses: pnpm/action-setup@v4
    with:
      version: 9
      run_install: false</code></pre>
<p>이 방법은, github actions와 pnpm 공식문서에서도 <strong>권장하는 방식</strong>이다. 사실 위에서 npm -g pnpm을 쓰지 않아야 하는 이유에서 모든 정답이 나와있긴하지만, 다시한번 정리해보면,</p>
<ul>
<li><strong>pnpm/action-setup은 github이 관리하는 경로(ToolCache)에 pnpm을 설치한다.</strong><ul>
<li>GitHub actions가 이 pnpm 바이너리 위치를 정확히 알고 있기 때문에, 같은 버전의 pnpm을 다시 설치할 필요 없이 바로 재사용할 수 있다.</li>
</ul>
</li>
<li><strong>pnpm/action-setup은 실제로는 “설치”를 하지 않는다.</strong><ul>
<li>GHA의 toolcache에서 해당 버전의 <strong>pnpm 바이너리가 있는지 확인</strong>한다.</li>
<li>있다면, 그대로 꺼내쓰고</li>
<li>없으면, 공식 서버에서 압축된 <strong>pnpm 바이너리 하나를 다운</strong>받는다.</li>
<li>압축을 해제하고 PATH에 폴더를 추가하기 한다.</li>
</ul>
</li>
</ul>
<p><a href="https://github.com/pnpm/action-setup?utm_source=chatgpt.com">https://github.com/pnpm/action-setup?utm_source=chatgpt.com</a>
<a href="https://pnpm.io/continuous-integration?utm_source=chatgpt.com">https://pnpm.io/continuous-integration?utm_source=chatgpt.com</a></p>
<h1 id="pnpm-store를-왜-캐시할까">pnpm store를 왜 캐시할까?</h1>
<pre><code class="language-yaml">      - name: Get pnpm store directory
        id: pnpm-store
        run: echo &quot;STORE_PATH=$(pnpm store path)&quot; &gt;&gt; $GITHUB_OUTPUT

      - name: Cache pnpm store
        uses: actions/cache@v4
        with:
          path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles(&#39;pnpm-lock.yaml&#39;) }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-</code></pre>
<p>많은 개발자들이 pnpm을 사용하는 가장 큰 이유 중 하나는 <strong>설치 속도가 빠르기 때문</strong>이다. 실제로 동일한 환경에서 여러 번 사용해보면 npm보다 설치 속도가 확실히 빠른데, 이는 pnpm이 내부적으로 완전히 다른 방식으로 패키지를 관리하기 때문이다. </p>
<p>pnpm은 패키지를 프로젝트마다 중복해서 설치하지 않고, <strong>“전역 스토어 + 프로젝트별 링크” 구조</strong>를 사용한다. </p>
<ul>
<li><strong>공통 저장소</strong>: pnpm store</li>
<li>각 프로젝트의 node_modules안에는 실제 패키지 파일이 아니라 <strong>하드 링크/심볼릭 링크</strong>만 둔다.</li>
</ul>
<h2 id="pnpm-intall을-하면-무슨일이-일어날까">pnpm Intall을 하면 무슨일이 일어날까?</h2>
<p>pnpm install 을 하게 되면 다음과 같은 과정이 진행된다.</p>
<ol>
<li>먼저, pnpm store에서 해당 버전의 패키지가 있는지 확인한다.</li>
<li>있다면, 링크만 건다. (다운로드를 하지 않는다. 왜? 이미 전역에 설치되어있어서)</li>
<li>없다면, 새로 다운로드해서 store에 넣고, 링크를 건다.</li>
</ol>
<p>pnpm의 node_modules는 <strong>“가짜 node_modules” 구조</strong>로, 실제 패키지 파일은 전역 store에 있고 node_modules에는 하드링크/심볼릭 링크만 존재한다. 그래서 파일을 들여다 보면 다음과 같이 표시된다.</p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/7e15da3d-f428-4d3f-94c5-825e0baaa3cd/image.png" alt=""></p>
<p>반면, npm으로 설치한 경우 실제 패키지 파일을 그대로 복사해서 두기때문에, 프로젝트 수가 많아진다면, 같은 패키지가 디스크에 여러 번 중복 될 수도 있고, 프로젝트가 커지면, 매우 느려질 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/7bc94cc0-c938-4a30-af82-7b73ef46c89d/image.png" alt=""></p>
<p>pnpm을 사용한다면, 로컬 환경에서 특히, 여러 프로젝트가 pnpm의 store를 공유할 수 있고 store가 커질수록 install 속도도 상승해서 pnpm의 장점이 극대화 된다. </p>
<p>반면, GitHub Actions의 CI 환경에서는 <strong>매 실행마다 컨테이너가 초기화</strong>되기 때문에, 로컬처럼 여러 프로젝트가 하나의 pnpm store를 공유하는 구조는 사용할 수 없다. </p>
<p>다만, actions/cache로 <strong>pnpm store 자체를 복원해주면 로컬과 거의 동일하게 pnpm의 빠른 설치 속도</strong>를 그대로 가져올 수 있다. 실제로 이 구성을 적용해본 결과, 설치 시간이 평균 14초에서 4~5초로 감소해서 <strong>약 10초 이상의 개선 효과</strong>를 확인할 수 있었다. </p>
<h1 id="pnpm-install-옵션으로-조금-더-안정성-있게-최적화-하기">pnpm install 옵션으로 조금 더 안정성 있게 최적화 하기</h1>
<p>pnpm install 을 운영환경에서 사용할때 쓰는 주요 옵션이 두가지가 있다. 하나씩 알아보자 </p>
<h2 id="1-frozen-lockfile">1. —frozen-lockfile</h2>
<p>이 옵션은 배포 환경에서 “<strong>예측 가능한 설치</strong>”를 강제로 보장한다. </p>
<p>lockfile과 package.json이 조금이라도 달라지면 의존성이 “<strong>미묘하게 다른 버전”</strong>으로 설치 될 수 있다. 기본적으로 pnpm은 lockfile이 package.json과 맞지 않으면 lockfile을 수정하려고 시도하기 때문이다. 이렇게 되면, 로컬에서는 되는데, CI환경에서는 되지 않거나 어제는 되는데 오늘은 안되는 문제가 발생할 수 있다. </p>
<p>그렇기 때문에, 운영/CI 환경에서는 파일 수정 금지, 새로운 diff 발생 금지, 잠재적 충돌 금지가 필요하다. 
—fronze-lockfile은 lockfile을 절대 수정하지 말라는 의미이므로 CI에서 변조될 가능성을 제거하여 안정성과 예측 가능성을 확보하는게 좋다. </p>
<h2 id="2-prefer-offline">2. —prefer-offline</h2>
<aside>
💡 값이 true이면, 캐시된 데이터에 대한 최신 여부(staleness) 검사는 건너뛰지만, 캐시에 없는 데이터는 서버에 요청됩니다. 완전한 오프라인 모드를 강제로 사용하려면 `--offline` 옵션을 사용하세요.
</aside>


<p><code>-prefer-offline</code> 옵션은 설치 과정에서 <strong>캐시 우선 전략</strong>을 적용한다. 즉, 이미 존재하는 캐시된 패키지는 최신 여부 확인 없이 즉시 사용하고, 캐시에 없는 항목만 remote registry에서 다운로드한다.
이를 통해 다음과 같은 장점을 얻을 수 있다:</p>
<ul>
<li>네트워크 장애에 영향을 덜 받는다.</li>
<li><strong>레지스트리 요청을 최소화하여 설치 속도가 빨라진다.</strong></li>
<li><strong>네트워크 이슈로 인한 CI 빌드 실패를 줄일 수 있다.</strong></li>
</ul>
<p>(최근 AWS 네트워크 이슈로 빌드가 막혀서 고생한 적이 있었는데, 이 옵션을 썼다면 야근을 피할 수 있었을지도… 😭)</p>
<h1 id="nextcache가-빌드-속도를-빠르게-하는-방법">.next/cache가 빌드 속도를 빠르게 하는 방법</h1>
<p>Next.js 프로젝트를 CI 환경(GitHub Actions)에서 빌드하다 보면 가장 아까운 시간이 있다.</p>
<p><strong>매번 새 컨테이너에서 ‘처음부터 다시’ 빌드하는 시간</strong>이다. 로컬에서는 빠른데 CI만 가면 빌드가 느려지는 이유도 여기에 있다. 그런데 사실 Next.js는 <strong>이미 자체 캐시(.next/cache)를 통해 빌드를 빠르게 할 수 있는 구조</strong>를 가지고 있다. 단지 CI에서 새로운 컨테이너이기 빌드하기 때문에 이 캐시를 재사용하지 못하는것 뿐이다.</p>
<p>Next.js의 빌드는 크게 다음 3단계로 이루어진다.</p>
<ol>
<li><strong>TS/JS 트랜스파일 + 번들링 (SWC 기반)</strong></li>
<li><strong>페이지/컴포넌트 의존성 분석 (modules graph)</strong></li>
<li><strong>이미지, 폰트 등 정적 자산 처리</strong></li>
</ol>
<p>이때 <code>.next/cache</code>에는 이전 빌드에서 계산된 다양한 결과가 저장된다.</p>
<h3 id="1-swc-트랜스파일-캐시">1. SWC 트랜스파일 캐시</h3>
<ul>
<li>Next.js는 Babel 대신 <strong>SWC</strong>를 사용한다.</li>
<li>각 TS/JS 파일을 컴파일한 결과를 <code>.next/cache/swc</code>에 저장한다.</li>
<li>코드가 바뀌지 않은 파일은 절대 다시 컴파일하지 않는다.</li>
</ul>
<h3 id="2-의존성-그래프-캐시">2. 의존성 그래프 캐시</h3>
<ul>
<li>&quot;어떤 페이지가 어떤 파일을 가져다 쓰는지&quot; 분석한 그래프가 저장된다.</li>
<li>파일이 변경되지 않았다면 다시 계산하지 않는다.</li>
</ul>
<h3 id="3-이미지-최적화-캐시">3. 이미지 최적화 캐시</h3>
<ul>
<li><code>next/image</code>가 처리한 최적화 이미지가 다시 계산되지 않는다.</li>
</ul>
<p>즉, Next.js는 “<strong>안 바뀐 파일은 처리하지 않는 방식</strong>”으로 빌드 시간을 단축한다. 하지만 CI 환경에서는 매번 새로운 컨테이너가 뜨기 때문에, 이 캐시가 전혀 사용되지 않는다. 그래서 <strong>actions/cache로 <code>.next/cache</code> 자체를 캐싱하는 이유</strong>이다.</p>
<h2 id="nextcache의-캐싱-전략">.next/cache의 캐싱 전략</h2>
<p><code>.next/cache</code>를 <code>actions/cache</code>로 저장해 빌드 시간을 단축하는 것은 좋은 방법이다. 하지만 여기서 자연스럽게 이런 의문이 생길 수 있다. <strong>“Next의 캐시는 언제 무효화해야 하는게 좋을까? 코드는 늘 바뀌니까 `src/</strong>/<em>`도 key에 넣어야 하는 거 아닌가?”*</em></p>
<p>결론부터 말하자면, src/<strong>/* 은 key에 절대 넣으면 안 된다. Key는 아래와 같이 **빌드 환경 자체가 달라지는 요소들만 포함</strong>하는 것이 좋다.</p>
<ul>
<li><code>pnpm-lock.yaml</code> (또는 yarn.lock, package-lock.json)</li>
<li><code>next.config.*</code></li>
<li><code>tsconfig.json</code></li>
<li>OS 정보(runner.os) 등..</li>
</ul>
<p>즉, <strong>환경이 바뀌어 캐시가 재사용될 수 없는 경우</strong>만 key로 관리하는게 좋다. </p>
<h3 id="왜-src를-key에-넣으면-안-될까">왜 <code>src/**/*</code>를 key에 넣으면 안 될까?</h3>
<ul>
<li>코드가 한 줄만 바뀌어도 <code>src/**/*</code> 의 해시가 전체적으로 바뀐다.<ul>
<li><strong>즉 매 커밋마다 캐시 key가 새로 생성되며,</strong></li>
<li>이전 <code>.next/cache</code>를 한 번도 재사용하지 못함</li>
<li>결과적으로 매번 “<strong>최초 빌드</strong>”와 다를바 없는 빌드 시간이 발생한다.</li>
</ul>
</li>
</ul>
<p>그렇기 때문에 src/*<em>/</em>을 key에 넣어버리면 <code>.next/cache</code> 캐싱은 사실상 의미가 없어지게 된다.</p>
<hr>
<h2 id="abc-커밋-시나리오로-보는-실제-동작">A/B/C 커밋 시나리오로 보는 실제 동작</h2>
<h3 id="1-a-커밋--첫-빌드">1. A 커밋 — 첫 빌드</h3>
<ul>
<li>Actions는 <code>.next/cache</code>를 찾지 못함 → 처음부터 빌드</li>
<li>모든 파일을 컴파일하고 <code>.next/cache</code>를 생성</li>
<li>이 캐시가 <strong>A라는 키 이름으로 GitHub에 저장</strong></li>
</ul>
<hr>
<h3 id="2-b-커밋--buttontsx만-수정됨">2. B 커밋 — Button.tsx만 수정됨</h3>
<ol>
<li>Actions는 <strong>A 캐시를 그대로 가져온다</strong></li>
<li><code>next build</code> 실행 → Next가 내부에서 이렇게 판단함:</li>
</ol>
<table>
<thead>
<tr>
<th>파일</th>
<th>캐시 속 해시</th>
<th>현재 소스</th>
<th>처리</th>
</tr>
</thead>
<tbody><tr>
<td>Button.tsx</td>
<td>v1</td>
<td>v2</td>
<td>재컴파일</td>
</tr>
<tr>
<td>Table.tsx</td>
<td>v1</td>
<td>v1(변경 없음)</td>
<td>캐시 재사용</td>
</tr>
</tbody></table>
<p>즉, <strong>캐시가 A 시점이더라도, Next는 내부적으로 모든 파일의 해시를 대조한다.</strong></p>
<p>그래서 <strong>변경된 파일은 반드시 다시 컴파일된다.</strong> 캐시 때문에 잘못된 빌드가 나오는 일은 절대 없다.</p>
<hr>
<h3 id="3-c-커밋--tabletsx만-수정됨">3. C 커밋 — Table.tsx만 수정됨</h3>
<p>여기서도 똑같다.</p>
<ul>
<li><p>GitHub에서 A 캐시가 내려옴</p>
</li>
<li><p>Next는 A 캐시를 참고해서</p>
<p>  <em>변경된 파일만 다시 컴파일</em></p>
</li>
<li><p>최종 <code>.next</code> 폴더는 <strong>항상 최신 코드 기준으로 생성됨</strong></p>
</li>
</ul>
<hr>
<h2 id="오래된-캐시를-계속-써도-문제가-되는지">오래된 캐시를 계속 써도 문제가 되는지..?</h2>
<p><code>.next/cache</code>를 Actions 캐시에 넣어서 빌드 속도를 줄이기 시작했을 때 가장 먼저 든 의문은 <strong>“코드는 계속 바뀌는데, 이렇게 오래된 캐시를 계속 써도 되는 걸까?</strong>”였다. 초기 빌드 이후 커밋이 쌓이면 캐시와 실제 코드의 차이가 점점 벌어질 텐데, 그게 빌드 결과물에 영향을 주거나 오히려 캐시를 무의미하게 만들지 고민이 있었다.</p>
<p><strong>정확도 측면에서는 아무 문제도 없다.</strong> 오래된 캐시를 가져오더라도 Next.js는 매 빌드마다 변경된 파일과 의존성을 다시 확인한다. 캐시가 있으면 “안 바뀐 파일”만 스킵할 뿐이고, 바뀐 파일은 캐시를 무시하고 다시 컴파일한다. 그래서 캐시가 오래된 상태라도 잘못된 빌드가 나온 적은 단 한 번도 없었다.</p>
<p><strong>영향이 생기는 부분은 속도였다</strong>. 변경된 코드가 많아질수록 Next가 “이건 캐시 쓰면 안 된다”라고 판단하는 영역이 늘어나기 때문에, 빌드 속도는 점점 레거시한 캐시를 가져올수록 약간씩 느려진다. 그래도 “캐시가 완전히 없는 빌드”와 비교하면 여전히 차이가 크다. 즉, 오래된 캐시라도 어느 정도 성능 이득은 계속 유지된다.</p>
<p>그래서 지금은  <strong>환경이 바뀌는 요소들(패키지 lockfile, next.config, tsconfig, OS 등)만 key에 넣고, 코드 변경으로는 key를 갱신하지 않는다.</strong> 코드를 key에 넣으면 캐시를 아예 못 쓸 수 있기 때문에, 대신 빌드 속도가 체감상 많이 느려졌다고 판단되는 시점이나 큰 리팩터링(?) 이후나 오래된 캐시를 너무 오래 썼다고 느껴질 때만, 수동으로 캐시를 날려 새로 쌓는방법을 채택했다. </p>
<p>만약 코드 변경이 잦은 환경이라면, 일정 주기로 캐시를 초기화하는 전략도 가능하다. 결국 프로젝트 특성과 배포 주기에 맞춰 캐시 갱신 주기를 선택하면 된다.</p>
<p><strong>결론적으로, <code>.next/cache</code>를 캐싱한 뒤로 빌드 시간이 평균 50초~1분 정도 줄었다. 단순한 설정 변경만으로 이 정도 효과가 난 건 꽤 인상적이었다.</strong></p>
<h1 id="pm2-클러스터-모드와-fork모드-그리고-인스턴스는-뭘까">PM2 클러스터 모드와 Fork모드 그리고 인스턴스는 뭘까?</h1>
<p>PM2에는 <strong><code>fork</code> 모드와 <code>cluster</code> 모드라는 두 가지 실행 방식</strong>이 있다. </p>
<p>그리고 이 둘을 이해하려면 먼저 “Node.js가 어떻게 동작하는가?”를 알아야 한다. Next.js를 운영 환경에 배포할 때 PM2를 선택한 이유도 여기와 관련돼 있다. 우리 환경에서는 서버가 재부팅되거나 Node 프로세스가 죽는 일이 종종 있었는데, PM2는 이런 상황에서도 프로세스 감시와 자동 재시작을 간단한 명령어로 해결할 수 있어서 가장 간단한 해결책이였다.</p>
<h3 id="nodejs와-pm2의-fork-모드"><strong>Node.js와 PM2의 Fork 모드</strong></h3>
<p>Node는 기본적으로 “<strong>단일 스레드</strong>”로 동작한다. Next.js를 배포하면 결국 Node 런타임 위에서 하나의 서버 프로세스가 떠 있는 구조이고, 이 프로세스가 모든 요청을 처리한다. 사용자가 늘어나면 이 단일 프로세스가 모든 요청을 감당해야 하기 때문에 병목이 생길 수 있다. </p>
<p>PM2의 <strong>Fork 모드</strong>는 이 기본 구조 그대로이다. 말 그대로, 단일 Node 프로세스를 하나 실행하는 것이 전부다. 개인 프로젝트나 아주 작은 트래픽 환경에서는 Fork모드만으로 충분하다고 생각한다. </p>
<h3 id="pm2의-cluster-모드와-인스턴스"><strong>PM2의 Cluster 모드와 인스턴스</strong></h3>
<p>운영 환경이라면, 조금 다른 옵션을 고려해봐야한다고 생각한다. 아무리 사용자 수가 적다고 해도, 단일 스레드 서버는 장애 대응이나 성능 면에서 한계가 있다. PM2가 제공하는 <strong>Cluster 모드</strong>는 이런 문제를 해결하기 위한 기능이다. </p>
<p>클러스터 모드는 다음과 같이 동작한다.</p>
<ul>
<li>하나의 Node.js 서버를 여러 프로세스로 복사해서 실행</li>
<li>요청은 여러 프로세스에 분산 처리</li>
<li>CPU 코어 수만큼 인스턴스를 띄우는 것이 기본 권장된다.</li>
</ul>
<p>즉, Cluster 모드는 Node의 단일 스레드 한계를 넘기 위한 PM2의 스케일링 전략이다. 운영 환경에서 안정성을 확보하려면 대부분 Cluster 모드를 사용한다. </p>
<p>여기서 중요한 개념이 바로 “<strong>인스턴스 수</strong>”이다. 인스턴스는 몇 개의 node 프로세스를 띄울 것인가를 의미한다. </p>
<p>그리고 이 인스턴스 개수는 CPU 코어 수를 초과할 수 없다. 예를 들어 EC2의 t3.micro는 vCPU가 2개 이기 때문에 PM2 클러스터 모드에서 최대 인스턴스 수는 2개가 된다. </p>
<h3 id="인스턴스를-무조건-코어-수만큼-쓰는-게-좋은걸까"><strong>인스턴스를 무조건 코어 수만큼 쓰는 게 좋은걸까?</strong></h3>
<p>꼭 그렇지는 않다. 나도 운영 환경에서 고민했던 부분이 바로 이 지점이었다. </p>
<p>인스턴스를 늘릴수록 요청 분산은 좋아지지만, 각 Node 프로세스가 메모리를 잡아먹기 때문에 비용이 증가한다. 특히 EC2처럼 리소스가 한정된 머신에서는 메모리 낭비로 이어질 수 있다. 예를 들어, 내가 만들었던 골프장 직원용 어드민 서비스는 트래픽이 폭발적으로 증가할 일이 없었기 때문에 최대 인스턴스 수를 2개까지만 두고 Cluster모드로 안정성을 확보했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React 이슈 까보기]
React에서 <title>이 사라지는 이상한 버그?]]></title>
            <link>https://velog.io/@gahui_dev/React-%EC%9D%B4%EC%8A%88-%EA%B9%8C%EB%B3%B4%EA%B8%B0React%EC%97%90%EC%84%9C-title%EC%9D%B4-%EC%82%AC%EB%9D%BC%EC%A7%80%EB%8A%94-%EC%9D%B4%EC%83%81%ED%95%9C-%EB%B2%84%EA%B7%B8</link>
            <guid>https://velog.io/@gahui_dev/React-%EC%9D%B4%EC%8A%88-%EA%B9%8C%EB%B3%B4%EA%B8%B0React%EC%97%90%EC%84%9C-title%EC%9D%B4-%EC%82%AC%EB%9D%BC%EC%A7%80%EB%8A%94-%EC%9D%B4%EC%83%81%ED%95%9C-%EB%B2%84%EA%B7%B8</guid>
            <pubDate>Mon, 26 May 2025 14:54:16 GMT</pubDate>
            <description><![CDATA[<p>React로 프로젝트를 개발하던 중, <code>&lt;title&gt;</code> 태그를 분명 JSX에 작성했음에도 <strong>브라우저 탭 제목이 표시되지 않는</strong> 경험을 해본 적 있으신가요?
콘솔 에러도 없고, 코드상으로는 아무 문제가 없어 보이는데 말이죠.</p>
<p>이번 글에서는 실제로 React 공식 이슈 트래커에 올라온 이 현상을 소개하고,
왜 이런 일이 발생하는지, 그리고 어떻게 해결할 수 있는지를 정리해보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/20d07215-02ab-4087-8353-bfc2f652f5b6/image.png" alt="">
<a href="https://github.com/facebook/react/issues/33341">이슈 링크 보기</a></p>
<hr>
<h2 id="❓-무슨-일이-있었을까">❓ 무슨 일이 있었을까?</h2>
<p>질문을 간단히 요약하자면, 다음과 같이 <code>&lt;title&gt;</code> 태그에 JSX 표현식을 사용하는 경우 브라우저 탭 제목이 표시되지 않습니다.</p>
<pre><code class="language-jsx">&lt;title&gt;{projectName} | Projects&lt;/title&gt;
&lt;title&gt;{projectName}{section}&lt;/title&gt;</code></pre>
<p>이유는 간단합니다. <code>&lt;title&gt;</code> 내부에 두 개 이상의 자식(JSX 노드)이 들어가게 되고,
React는 이를 HTML <code>&lt;head&gt;</code>로 hoisting하는 과정에서 <strong>유효하지 않은 구조</strong>로 판단해 <strong>빈 문자열로 처리</strong>해버리기 때문입니다.
결과적으로 아무것도 렌더링되지 않으며, 콘솔에도 경고나 오류는 출력되지 않습니다.</p>
<hr>
<h2 id="🧩-react는-title을-어떻게-처리할까">🧩 React는 <code>&lt;title&gt;</code>을 어떻게 처리할까?</h2>
<p>React에서 <code>&lt;title&gt;</code>을 포함한 <code>&lt;head&gt;</code> 관련 태그는 일반 DOM처럼 직접 렌더링되지 않습니다.
이러한 태그들은 React 내부에서 자동으로 <code>&lt;head&gt;</code>로 <strong>hoisting(끌어올림)</strong> 되며,
이 과정에서 React는 <code>&lt;title&gt;</code>의 자식으로 <strong>정확히 하나의 문자열 노드</strong>만 있을 때만 정상적으로 처리합니다.</p>
<p>예를 들어 아래 JSX는:</p>
<pre><code class="language-jsx">&lt;title&gt;{projectName}{section}&lt;/title&gt;</code></pre>
<p>다음과 같이 변환됩니다:</p>
<pre><code class="language-js">React.createElement(&quot;title&quot;, null, projectName, section);</code></pre>
<p>즉, 두 개의 자식 노드가 들어간 상태가 되고, 이는 HTML 명세에 위배되기 때문에
React는 해당 요소를 무시하거나 빈 문자열로 대체하게 되는 것입니다.</p>
<hr>
<h2 id="🤔-왜-자동으로-문자열로-합쳐주지-않을까">🤔 왜 자동으로 문자열로 합쳐주지 않을까?</h2>
<p>이쯤에서 자연스럽게 이런 의문이 들었습니다.
JSX에서 <code>{projectName}{section}</code>처럼 작성된 코드는
개발자 입장에서는 하나의 문자열처럼 보이기 때문입니다.
React가 이를 내부적으로 <code>projectName + section</code>으로 자동 변환해 처리해주면
실수도 줄이고 예상치 못한 버그도 예방할 수 있지 않을까 싶었습니다.</p>
<hr>
<h2 id="📌-그럼에도-react가-자동으로-처리하지-않는-이유">📌 그럼에도 React가 자동으로 처리하지 않는 이유</h2>
<p>React는 이런 모호한 상황에서 <strong>개발자가 명확하게 의도를 표현하기를 요구</strong>합니다.
자동으로 문자열을 합쳐버릴 경우, 다음과 같은 문제가 생길 수 있기 때문입니다:</p>
<ul>
<li><p><code>projectName</code>이나 <code>section</code> 값이 숫자나 undefined, 혹은 React 요소일 수도 있습니다. 이럴 경우 React가 자동으로 더하려고 하면 &quot;undefinedProjects&quot; 같은 예상치 못한 문자열이 만들어질 수 있어요.</p>
</li>
<li><p>그리고 <code>&lt;title&gt;</code>처럼 브라우저의 <code>&lt;head&gt;</code>에 삽입되는 요소는, React 내부에서도 특별하게 관리됩니다. 특히 next/head나 react-helmet 같은 서드파티 라이브러리와 함께 쓸 경우,
내부에서 어떤 값이 들어왔는지 정확하게 예측 가능해야 합니다. 그런데 자동으로 문자열을 합쳐버리면,
어떤 조합으로 만들어졌는지 추적하기 어려워지고,
이게 나중에 예상치 못한 동작(중복 삽입, 값 덮어쓰기 등)을 일으킬 수 있습니다.</p>
</li>
</ul>
<p>따라서 React는 <code>&lt;title&gt;</code>처럼 민감한 위치에 들어가는 요소일수록
<strong>엄격하게 명세를 따르고</strong>, <strong>개발자가 의도를 명확히 드러내도록 유도하는 방향</strong>을 택하고 있습니다.</p>
<hr>
<h2 id="✅-올바른-사용-예">✅ 올바른 사용 예</h2>
<p>문제를 피하려면, 아래처럼 <strong>하나의 문자열로 합쳐서</strong> 전달하면 됩니다:</p>
<pre><code class="language-jsx">&lt;title&gt;{`${projectName} | ${section}`}&lt;/title&gt;</code></pre>
<p>또는</p>
<pre><code class="language-jsx">&lt;title&gt;{projectName + &quot; | &quot; + section}&lt;/title&gt;</code></pre>
<p>이렇게 작성하면 React가 올바르게 문자열 하나로 인식하여 <code>&lt;head&gt;</code>에 정상적으로 삽입됩니다.</p>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTML/Reference/Elements/title">MDN <code>&lt;title&gt;</code></a>
<a href="https://react.dev/reference/react-dom/components/title">React - <code>&lt;title&gt;</code></a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PEC 2-3주차 - 사용자의 문제를 어떻게 해결할 것인가? ]]></title>
            <link>https://velog.io/@gahui_dev/PEC2-3%EC%A3%BC%EC%B0%A8-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%95%A0-%EA%B2%83%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@gahui_dev/PEC2-3%EC%A3%BC%EC%B0%A8-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%95%A0-%EA%B2%83%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Fri, 21 Mar 2025 06:21:03 GMT</pubDate>
            <description><![CDATA[<p><strong>PEC 1주차에서는 하나의 프러덕트를 만들 때 가장 먼저 &quot;WHY&quot;를 정의하는 과정에 집중했다.</strong>  즉, 무엇을 만들 것인가보다 <strong>왜 만들 것인가</strong>를 고민하며, 사용자 인터뷰를 통해 문제를 깊이 이해하고 페르소나를 완성하는 단계였다. 하지만 <strong>문제를 정의했다고 해서 곧바로 해결책이 보이는 것은 아니었다.</strong> 진짜 중요한 것은 <strong>이 문제를 어떻게 해결할 것인가(HOW)</strong>에 대한 고민이었다.  </p>
<p>다시 한번 <strong>골든 서클(Golden Circle)</strong>을 떠올려보면,  </p>
<ul>
<li><strong>WHY(왜?)</strong>: 1주차에서 문제 정의와 사용자 페르소나 분석을 통해 명확히 했다.  </li>
<li><strong>HOW(어떻게?)</strong>: 2~3주차에서는 이 문제를 해결하는 방법을 찾는 과정이다.  </li>
<li><strong>WHAT(무엇을?)</strong>: 이후 단계에서는 실제 프로덕트를 만들어가는 과정이 진행될 것이다.  </li>
</ul>
<p>이번 글에서는 <strong>2~3주차 동안 문제를 해결하기 위해 어떻게 접근하고 구체화했는지</strong> 다뤄보려고 한다.<br>사용자 인터뷰에서 얻은 데이터를 정리하고 의미 있는 정보로 정제한 후, 이를 기반으로 서비스의 흐름(Service Flow)을 설계했다.  </p>
<p>이 과정에서 중요한 것은 단순히 기능을 추가하는 것이 아니라, <strong>사용자가 어떤 방식으로 서비스를 경험하게 될지를 명확하게 정의하는 것</strong>이었다. 즉, <strong>단순한 아이디어를 실제 서비스로 구체화하는 과정이 어떻게 이루어졌는지를 살펴보려 한다.</strong></p>
<h3 id="데이터data란-그리고-정보information이란"><strong>데이터(Data)란? 그리고 정보(Information)이란?</strong></h3>
<p>먼저, <strong>데이터(Data)와 정보(Information)의 차이</strong>를 짚어볼 필요가 있다.</p>
<p><strong>데이터(Data)</strong>란 <strong>가공되지 않은 사실이나 값</strong>을 의미한다. 예를 들어, 사용자 인터뷰에서 <strong>&quot;이 기능이 필요해요&quot;</strong>라는 응답을 여러 번 들었다고 하자. 이 자체는 단순한 데이터일 뿐이다.</p>
<p>그러나 이러한 응답이 반복적으로 나타난다면, 우리는 단순한 데이터가 아니라 <strong>&quot;이 기능이 사용자들에게 정말 필요하다&quot;</strong>는 정보를 도출할 수 있다. <strong>정보(Information)</strong>란 바로 이러한 과정, 즉 <strong>데이터를 분석하고 정리하여 의미를 부여한 것</strong>을 의미한다.</p>
<p>즉, <strong>데이터는 단순한 사실의 나열이지만, 이를 어떻게 가공하고 해석하느냐에 따라 정보가 되고, 결국 서비스 기획의 방향을 결정하는 중요한 요소가 된다.</strong></p>
<p>처음에는 <strong>&quot;왜 갑자기 데이터와 정보를 이야기하는 걸까?&quot;</strong>라는 의문이 들었지만, 2~3주차 세션을 진행하며, 이 과정이 결국 <strong>더 나은 서비스 설계를 위한 필수적인 단계</strong>임을 깨닫게 되었다.</p>
<p>이 과정을 거쳐야만 <strong>사용자가 정말 원하는 것이 무엇인지 파악할 수 있고, 불필요한 기능을 걸러내며, 효과적인 서비스 흐름을 만들 수 있다.</strong> 단순히 데이터를 쌓는 것이 아니라, <strong>데이터 속에서 의미를 찾고, 이를 서비스 설계에 반영하는 과정이 중요했다.</strong></p>
<h3 id="information-flow-추상화">Information Flow (추상화)</h3>
<p>2주차에서 중요한 개념은 <strong>Information Flow</strong>였다. 위에서 계속 이야기했지만, 이는 수집한 데이터를 단순히 나열하는 것이 아니라, 이를 분석하여 의미 있는 정보로 변환하는 과정이다.</p>
<p>사용자 인터뷰에서 특정 기능이 많이 언급되었다고 해서 곧바로 추가해야 하는 것은 아니다. 단순히 빈도만을 기준으로 기능을 결정하는 방식은 서비스의 복잡성을 증가시키고, 핵심 사용자 경험을 흐릴 수 있다. 따라서, <strong>느낌이나 직관에 의존하기보다는, 체계적인 분석과 구조화 과정을 통해 데이터가 실제로 의미하는 바를 파악하는 것이 중요하다.</strong></p>
<p>이를 위해 2주차에서는 <strong>Service Flow, Information Mind Map, User Workflow</strong>를 작성하는 작업을 진행했다. 이러한 과정은 단순한 피드백의 모음이 아니라, <strong>사용자의 문제를 해결하기 위한 정보의 흐름을 정의하고, 이를 바탕으로 서비스의 방향성을 구체화하는 과정</strong>이었다.</p>
<p>이 과정에서 중요한 개념이 <strong>추상화(Abstraction)</strong>였다. Information Flow를 만드는 것은 마치 그림을 그릴 때 <strong>스케치를 하는 과정과 같다.</strong></p>
<p>초보자는 그림을 그릴 때 세부적인 부분부터 그리려 하지만, <strong>완성도 높은 그림을 그리려면 먼저 전체적인 구도를 잡고 큰 형태를 그린 후, 점차 디테일을 더해 나가야 한다.</strong> 마찬가지로, 서비스 기획에서도 처음부터 모든 기능을 세부적으로 정의하는 것이 아니라, <strong>추상화를 통해 핵심적인 흐름을 먼저 잡고 점차 구체화해 나가는 과정</strong>이 필요하다.</p>
<p>즉, <strong>Information Flow는 서비스의 스케치 단계와 같고, 추상화를 통해 불필요한 요소를 덜어내고 핵심적인 흐름을 먼저 설계하는 것이 중요하다.</strong> 이후, 이를 바탕으로 점차 세부적인 기능과 사용자의 행동을 구체화하며 서비스의 형태를 완성해 나가는 것이 중요하다. </p>
<p>2주차 세션을 참고하여 <strong>Service Flow, Information Mind Map, User Workflow</strong>를 쉽게 정리하면 다음과 같다.</p>
<ul>
<li><strong>Service Flow</strong>: 사용자가 서비스를 이용하면서 문제를 해결하는 과정을 정리한 것이다. 어떤 기능이 언제, 어떻게 연결되는지를 정리해서 서비스의 흐름을 한눈에 볼 수 있도록 만든다.</li>
<li><strong>Information Mind Map</strong>: 사용자 경험과 데이터를 정리하고, 관련된 요소들을 연결하여 시각적으로 정리한 것이다. 이를 통해 서비스에서 중요한 정보가 무엇인지 쉽게 파악할 수 있다.</li>
<li><strong>User Workflow</strong>: 사용자가 서비스를 실제로 사용할 때 어떤 과정을 거치는지를 단계별로 정리한 것이다. 이를 통해 사용자가 서비스를 어떻게 탐색하고 이용하는지를 이해하고, 더 편리한 경험을 제공할 수 있도록 설계한다.</li>
</ul>
<p>각자 <strong>Service Flow, Information Mind Map, User Workflow</strong>를 작성하였다. 아래는 내가 정리한 서비스 플로우, 정보 마인드맵, 그리고 사용자 워크플로우이다.</p>
<h3 id="1-service-flow"><strong>1. Service Flow</strong></h3>
<p>1주차에 작성한 <strong>페르소나</strong>를 기반으로, 서비스의 핵심 기능을 큰 흐름으로 나누고, 각 기능의 상세한 플로우를 정의하는 방식으로 접근했다.
<strong>프로폴리오(서비스명)</strong>를 기준으로 <strong>주요 기능을 먼저 정의한 후</strong>, 각 기능의 흐름을 정리하였다.</p>
<p>주요 기능은 다음과 같다.</p>
<ul>
<li><strong>프로필 작성</strong>: 사용자가 자신의 정보를 입력하고 프로필을 생성하는 과정</li>
<li><strong>프로필 목록</strong>: 여러 프로필을 확인하고, 검색 및 필터링할 수 있는 기능</li>
<li><strong>프로필 공유</strong>: 생성된 프로필을 링크 혹은 다른 방법을 통해 공유하는 기능</li>
<li><strong>프로필 보관</strong>: 사용자가 프로필을 관리하고, 보관 또는 수정할 수 있는 기능</li>
</ul>
<p>이러한 기능을 중심으로 <strong>사용자의 행동이 어떻게 흐르는지(Service Flow)</strong>를 구조화하여 작성하였다.</p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/79c84171-2531-4b05-92bb-994dff4000df/image.png" alt="Service Flow"></p>
<h3 id="2-information-mind-map"><strong>2. Information Mind Map</strong></h3>
<p>Information Mind Map은 <strong>서비스에서 필요한 데이터를 정리하고, 백엔드와 프론트엔드 에서 필요한 데이터들을 구조화하는 과정</strong>이다. 내가 작성한 Mind Map은 <strong>User(사용자), Profile(프로필), Share(공유), Save(보관)</strong> 등 주요 기능과 연관된 데이터를 한눈에 파악할 수 있도록 구성했다.</p>
<h3 id="📌-주요-구성-요소"><strong>📌 주요 구성 요소</strong></h3>
<ol>
<li><strong>사용자(User) 정보</strong><ul>
<li><strong>ID 관리</strong>: 네이버, 카카오, 구글 로그인 및 패스워드 기반 로그인 지원</li>
<li><strong>기본 필드</strong>: 이름, 닉네임, 이메일, 성별, 생년월일 등 (선택적 입력)</li>
<li><strong>커스텀 필드 지원</strong>: 확장성을 고려한 데이터 구조</li>
</ul>
</li>
<li><strong>프로필(Profile) 관리</strong><ul>
<li><strong>자동/수동 프로필 작성</strong></li>
<li><strong>테마 설정</strong>: 테마 색상, 타입 선택 가능</li>
<li><strong>다중 프로필 지원</strong> (<code>add profile</code>, <code>is_main</code> 필드)</li>
</ul>
</li>
<li><strong>프로필 공유(Share)</strong><ul>
<li><strong>이미지 변환, URL, QR 코드 공유 지원</strong></li>
</ul>
</li>
<li><strong>프로필 보관(Save)</strong><ul>
<li><strong>보관된 프로필 리스트 관리 (<code>profile_idx</code>)</strong></li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/b9d5cdd9-24f1-41c5-9210-46437d7cc280/image.png" alt=""></p>
<h3 id="3-user-workflow-작성-과정"><strong>3. User Workflow 작성 과정</strong></h3>
<p>User Workflow는 <strong>사용자가 서비스를 이용하는 전체적인 흐름을 정리한 것</strong>으로, 사용자가 수행하는 <strong>주요 활동(User Activities), 세부 작업(User Tasks), 그리고 실제 실행되는 스토리(User Stories)</strong>를 구조적으로 정리하였다.</p>
<p>내가 작성한 User Workflow는 <strong>프로필 작성, 프로필 공유, 보관</strong>의 세 가지 핵심 기능을 중심으로 구성되며, 사용자의 행동이 어떻게 연결되는지를 시각적으로 표현하였다.</p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/87f26896-c5d5-4b1d-9c3a-ec7dcbe6c89e/image.png" alt=""></p>
<h3 id="개인-작업에서-팀-작업으로-information-flow-통합-과정"><strong>개인 작업에서 팀 작업으로: Information Flow 통합 과정</strong></h3>
<p>개별적으로 작성한 <strong>Service Flow, Information Mind Map, User Workflow</strong>가 끝이 아니라, 이제는 <strong>각각의 Information Flow를 하나로 합치는 과정</strong>이 필요하다.
이때 단순히 <strong>모든 기능을 합치는 것</strong>이 아니라, <strong>정의한 기능들이 실제로 사용자가 필요로 하는 것인지 다시한번 검증하는 과정</strong>을 거쳐야 한다. 이를 위해, 우리는 다시 한 번 <strong>사용자 인터뷰를 진행</strong>하여 기존에 정의한 기능들을 검토하고, <strong>불필요한 기능을 걸러내고 정말 필요한 기능만을 남기는 과정</strong>을 거쳤다.</p>
<ol>
<li><strong>프로필 생성</strong><ul>
<li>사용자가 자신의 정보를 입력하여 프로필을 생성할 수 있는 기능</li>
</ul>
</li>
<li><strong>프로필 목록</strong><ul>
<li>사용자가 생성한 프로필들을 한눈에 보고, 관리할 수 있는 기능</li>
</ul>
</li>
<li><strong>테마 관리</strong><ul>
<li>사용자가 프로필의 테마를 직접 생성하고, 색상 및 스타일을 커스터마이징할 수 있는 기능</li>
</ul>
</li>
</ol>
<hr>
<h3 id="3주차-서비스-구체화-">*<em>3주차: 서비스 구체화 *</em></h3>
<p>3주차에는 2주차에서 정의한 정보 흐름을 바탕으로, <strong>실제 서비스 구조를 설계하고 사용자 경험을 구체화하는 과정</strong>이 진행되었다. 이 과정의 목표는 <strong>보다 직관적인 UX를 구성하고, 기능 간의 연계를 명확하게 정리하는 것</strong>이었다. 이를 위해, 다음과 같은 세 가지 과정을 거쳤다.
이를 위해 다음과 같은 세 가지 과정을 통해 서비스를 구체화하였다.</p>
<ul>
<li><strong>서비스 프로세스</strong>: 사용자가 서비스를 이용하는 흐름을 정리하고, 각 기능이 어떤 순서로 연결되는지 설계하는 과정</li>
<li><strong>메뉴 구성</strong>: 사용자가 필요한 정보를 빠르게 찾을 수 있도록 메뉴를 정리하고, 더 편리하게 이용할 수 있도록 구성하는 과정</li>
<li><strong>사이트맵 구성</strong>: 서비스의 각 페이지와 기능이 어떻게 연결되는지 정리하여, 사용자가 쉽게 이해하고 탐색할 수 있도록 만드는 과정</li>
</ul>
<p>그렇게 다음과 같은 과정을 진행했다. </p>
<h3 id="1-서비스-프로세스service-process-설계"><strong>1. 서비스 프로세스(Service Process) 설계</strong></h3>
<p>서비스 프로세스를 설계하면서, <strong>사용자가 원하는 목표를 효과적으로 달성할 수 있도록 주요 기능을 정리</strong>하였다. 이를 위해 <strong>프로필 작성, 프로필 목록, 프로필 공유, 테마 목록</strong> 네 가지 기능을 정의하고, 사용자가 각 기능을 어떻게 이용할 수 있는지 설계했다.</p>
<p>이 과정에서 중요한 점은, <strong>각 기능이 다른 기능에 의존하지 않고 독립적으로 존재해야 한다는 것</strong>이다. 기능들이 서로 연결되면, 사용자는 특정 기능을 이용하기 위해 불필요한 단계를 거쳐야 할 수도 있다. 이를 방지하기 위해 <strong>각 기능이 독립적으로 작동할 수 있도록 설계하여, 사용자가 원하는 작업을 직관적으로 수행할 수 있도록 구성</strong>했다.</p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/3eaf0d8c-f340-4fcb-ab63-844f5920df60/image.png" alt=""></p>
<h3 id="2-메뉴-구성-navigation-design"><strong>2. 메뉴 구성 (Navigation Design)</strong></h3>
<p>이 과정은 <strong>우리 서비스에서 어떤 네비게이션(메뉴)이 필요한지 정리하는 작업</strong>이다. 사용자가 서비스를 이용할 때 <strong>어떤 경로로 이동하며, 각 기능에 쉽게 접근할 수 있도록 구성하는 것이 중요했다.</strong></p>
<p>메뉴를 정리하면서 로그인 후 화면이 <strong>프로필 목록과 테마 목록</strong>으로 나뉘는 것이 적절하다고 판단했다.</p>
<ul>
<li><strong>프로필 목록(메인 화면)</strong> → 사용자가 생성한 프로필을 확인하는 공간<ul>
<li>프로필을 선택하면 <strong>상세 페이지</strong>로 이동</li>
<li>상세 페이지에서 <strong>프로필 수정, 공유, 복사, 삭제 등의 기능을 수행 가능</strong></li>
</ul>
</li>
<li><strong>테마 목록</strong> → 프로필의 테마를 관리하는 공간<ul>
<li>테마 미리 보기 기능 제공</li>
<li>새로운 테마를 추가하거나 삭제할 수 있음</li>
</ul>
</li>
</ul>
<p>이러한 구조를 통해, <strong>사용자가 필요한 기능을 직관적으로 찾을 수 있도록 메뉴를 구성했다.</strong></p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/40714c3c-3252-4934-946e-1d66d0aca409/image.png" alt=""></p>
<h3 id="3-사이트맵-sitemap-구성"><strong>3. 사이트맵 (Sitemap) 구성</strong></h3>
<p>사이트맵은 <strong>서비스 내에서 사용자가 어떤 경로를 따라 이동하는지를 시각적으로 표현한 것</strong>이다. 이를 통해 <strong>각 페이지가 어떻게 연결되는지 정리하고, 사용자 경험을 고려한 최적의 탐색 경로를 설계</strong>할 수 있다.</p>
<p>처음 사이트맵을 구성할 때 실수했던 부분은 <strong>기능을 중심으로 구성했다는 점</strong>이다. 기능 위주로 정리하다 보니, 이전에 작성했던 <strong>서비스 플로우와 차별성이 없어지는 문제가 발생했다.</strong> 이때, 멘토님이 <strong>사이트맵은 기능이 아니라, 실제 Router로 등록될 모든 페이지를 정의해야 한다</strong>고 조언해 주셨다.</p>
<p>이를 반영하여 단순히 기능 간의 흐름을 정리하는 것이 아니라, <strong>서비스에서 사용될 모든 페이지를 구조적으로 정리하는 것</strong>을 목표로 했다. 즉, <strong>기능 흐름이 아니라, 실제 라우터(Router)에 등록될 페이지를 기준으로 구성</strong>해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/4b93458e-4bf9-49f0-91f0-ebb5265efd01/image.png" alt=""></p>
<h2 id="마무리">마무리</h2>
<p>2~3주차를 지나면서, 단순히 문제를 정의하는 것을 넘어, <strong>이 문제를 실제로 어떻게 해결할 것인가?</strong>에 대한 고민이 깊어졌다.</p>
<p>처음에는 사용자 인터뷰에서 나온 피드백을 모으고 기능을 정리하는 과정이 전부라고 생각했다. 하지만 진행하면서 단순히 기능을 추가하는 것이 아니라, <strong>사용자가 서비스를 어떻게 경험할지를 중심으로 설계하는 것이 더 중요하다는 것</strong>을 깨달았다.</p>
<p>특히, <strong>서비스 프로세스, 메뉴 구성, 사이트맵을 만들면서 &quot;기능을 나열하는 것&quot;과 &quot;사용자의 흐름을 고려하는 것&quot;은 전혀 다르다는 점</strong>을 실감했다. 처음에는 기능 중심으로 정리하다 보니 기존 서비스 플로우와 차별성이 없었고, 이걸 어떻게 정리해야 할지 막막했다. 그런데 멘토님이 <strong>&quot;사이트맵은 기능이 아니라, 실제 Router에 등록될 모든 페이지를 기준으로 구성해야 한다&quot;</strong>라고 말씀해 주셨고, 이걸 반영하면서 서비스 구조가 훨씬 명확해졌다.</p>
<p>또 한 가지, 이 과정을 거치면서 <strong>서비스의 구조를 문서화하는 게 얼마나 중요한지</strong>도 새삼 깨달았다. 그냥 기능을 만들면서 &quot;이건 이렇게 하면 되겠지?&quot; 하는 것과, 실제로 서비스의 흐름을 시각적으로 정리하는 것은 전혀 달랐다. 이 작업을 먼저 했더라면, 개발할 때 훨씬 수월했을 것 같다. 그리고 팀원들 간의 협업이나, 나중에 새로운 사람이 합류했을 때도 이걸 보면 빠르게 서비스 구조를 이해할 수 있을 거란 생각이 들었다.</p>
<p>이제 다음 단계는 <strong>실제 구현 단계</strong>로 넘어간다. 구체화한 내용을 바탕으로 바로 코드를 작성하는 것이 아니라, <strong>FSD(Folder Structure Design) 아키텍처를 기반으로 폴더와 파일 구조를 먼저 설계하는 과정</strong>을 진행할 예정이다. 이를 통해 <strong>코드 작성 전에 서비스의 전체적인 설계를 정리하고, 기능 간의 명확한 역할 분리를 할 수 있도록 준비하는 과정을 다룰 예정이다!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[모든 경우의 수를 다 찾는, 완전탐색 알고리즘]]></title>
            <link>https://velog.io/@gahui_dev/exhaustivesearch</link>
            <guid>https://velog.io/@gahui_dev/exhaustivesearch</guid>
            <pubDate>Sun, 09 Mar 2025 00:52:44 GMT</pubDate>
            <description><![CDATA[<p> 완전탐색은 <strong>탐색 알고리즘의 한 종류</strong>로, 가능한 <strong>모든 경우의 수를 탐색하여 정답을 찾아내는 방법</strong>입니다.<br>가능한 모든 경우를 하나씩 대입하여 확인하는 방식이므로, <strong>&quot;무식하게(brute force)&quot; 푸는 방법</strong>이라고도 불립니다.  </p>
<p>보통 알고리즘은 <strong>최대한 빠르게 정답을 찾아내는 것이 중요</strong>하지만, 완전탐색은 <strong>모든 경우를 전부 계산</strong>하기 때문에<br>효율적인 알고리즘이라고 하기엔 다소 어려움이 있습니다.<br>하지만 <strong>최적의 해를 반드시 보장</strong>하기 때문에, 경우의 수가 적을 때는 충분히 사용할 수 있는 방법입니다.  </p>
<hr>
<h2 id="🔍-완전탐색-알고리즘의-종류"><strong>🔍 완전탐색 알고리즘의 종류</strong></h2>
<p>완전탐색에는 여러 가지 방법이 있으며, 대표적으로 다음과 같은 알고리즘이 있습니다.  </p>
<ol>
<li>*<em>Brute Force *</em>  </li>
<li><strong>재귀 (Recursion)</strong>  </li>
<li><strong>비트마스크 (Bitmasking)</strong>  </li>
<li><strong>순열 (Permutation)</strong>  </li>
<li><strong>백트래킹 (Backtracking)</strong>  </li>
<li><strong>비 선형구조 탐색 : DFS / BFS</strong> (다음 포스팅에서 다룰 예정)</li>
</ol>
<p>이제 하나씩 살펴보겠습니다.  </p>
<hr>
<h2 id="1-brute-force"><strong>1. Brute Force</strong></h2>
<p>가장 기본적인 완전탐색 방법으로, <strong><code>for</code>문, <code>while</code>문, <code>if</code>문</strong> 등을 이용하여 모든 경우를 전부 탐색하는 방식입니다.<br>무식하지만 확실하게 답을 찾을 수 있으며, 경우의 수가 적을 때 사용할 수 있습니다.</p>
<h3 id="✅-예제-배열에서-특정-숫자-찾기"><strong>✅ 예제: 배열에서 특정 숫자 찾기</strong></h3>
<pre><code class="language-js">const arr = [1, 2, 3, 4, 5];
const target = 3;

for (let i = 0; i &lt; arr.length; i++) {
  if (arr[i] === target) {
    console.log(`정답 : ${arr[i]}`);
  }
}</code></pre>
<p>위 코드에서는 배열을 처음부터 끝까지 <strong>전부 확인</strong>하면서 <code>target</code> 값을 찾고 있습니다.<br>이처럼 <strong>모든 경우를 하나씩 확인하는 것이 Brute Force 방식</strong>입니다.</p>
<h3 id="📌-brute-force의-시간-복잡도"><strong>📌 Brute Force의 시간 복잡도</strong></h3>
<p>위 예제에서는 <strong>단순히 <code>for</code>문을 한 번만 실행</strong>하고, <code>target</code> 값을 찾는 순간 종료할 수도 있습니다.<br>하지만 일반적으로 <strong>모든 경우를 탐색해야 한다는 점에서 Brute Force의 시간 복잡도는 다음과 같이 계산됩니다.</strong>  </p>
<h3 id="1-단순-탐색의-경우"><strong>1 단순 탐색의 경우</strong></h3>
<ul>
<li>배열의 크기가 <code>N</code>일 때, 최악의 경우 <strong><code>N</code>번 반복</strong>해야 함.</li>
<li>즉, 시간 복잡도는 <strong>O(N)</strong>.</li>
</ul>
<h3 id="2-중첩-반복문을-사용하는-경우"><strong>2 중첩 반복문을 사용하는 경우</strong></h3>
<ul>
<li>Brute Force는 경우의 수를 <strong>모두 시도</strong>하기 때문에, <strong>중첩된 <code>for</code>문</strong>이 있을 경우 O(N²) 이상의 시간 복잡도가 발생할 수 있습니다.</li>
</ul>
<pre><code class="language-js">const arr = [1, 2, 3, 4, 5];

for (let i = 0; i &lt; arr.length; i++) {
  for (let j = 0; j &lt; arr.length; j++) {
    console.log(arr[i], arr[j]); // 모든 경우의 수 탐색
  }
}</code></pre>
<p>📌 <strong>이 경우 시간 복잡도는 <code>O(N²)</code></strong> → 입력 크기가 커질수록 연산량이 기하급수적으로 증가합니다.</p>
<p><strong>즉, Brute Force 방식은 단순한 문제에서는 효과적이지만, 경우의 수가 많아질수록 비효율적일 수 있습니다.</strong>  </p>
<hr>
<h2 id="2-재귀-함수-recursion"><strong>2. 재귀 함수 (Recursion)</strong></h2>
<p>재귀 함수는 <strong>문제를 작은 단위로 쪼개어 해결하는 방법</strong>입니다.<br>특정 조건을 만족하면 <strong>자기 자신을 계속 호출</strong>하는 방식으로 동작합니다. </p>
<h3 id="✅-예제-1부터-n까지-출력하는-재귀-함수"><strong>✅ 예제: 1부터 N까지 출력하는 재귀 함수</strong></h3>
<pre><code class="language-js">function printNumbers(n) {
  if (n === 0) return;  // 종료 조건
  printNumbers(n - 1);
  console.log(n);
}

printNumbers(5);</code></pre>
<p>위 코드는 <code>printNumbers(5)</code>를 호출하면, <strong><code>5 → 4 → 3 → 2 → 1</code> 순서로 재귀 호출</strong>이 이루어집니다.<br>재귀를 활용하면 <strong>완전탐색을 더 효율적으로 구현</strong>할 수 있습니다.</p>
<h3 id="📌-재귀-함수의-시간-복잡도-">*<em>📌 재귀 함수의 시간 복잡도 *</em></h3>
<p>위 예제의 <code>printNumbers(n)</code> 함수는 <strong>재귀적으로 호출되며</strong>, <code>n</code>에서 <code>0</code>까지 감소하는 방식입니다.  </p>
<pre><code class="language-js">function printNumbers(n) {
  if (n === 0) return;  // 종료 조건
  printNumbers(n - 1);  // 재귀 호출
  console.log(n);  // 출력
}
printNumbers(5);</code></pre>
<p>✅ <strong>호출 과정</strong> (<code>printNumbers(5)</code>)  </p>
<pre><code>printNumbers(5)  → printNumbers(4)  
→ printNumbers(3)  → printNumbers(2)  
→ printNumbers(1)  → printNumbers(0) (종료)</code></pre><p>➡ 총 <code>n + 1</code>번 호출됨 (<code>n</code>부터 <code>0</code>까지)</p>
<p>✅ <strong>시간 복잡도 계산</strong>  </p>
<ul>
<li>함수가 한 번 호출될 때마다 <code>printNumbers(n - 1)</code>을 <strong>한 번만 호출</strong> 합니다. 즉, 호출 횟수는 <code>n</code>번이며, 각 호출에서 O(1)의 연산이 수행됩니다. 따라서 <strong>총 시간 복잡도는 <code>O(n)</code></strong>  </li>
</ul>
<p>만약, 재귀 함수가 <strong>여러 번 호출되는 경우</strong>, 시간 복잡도가 증가할 수 있습니다.</p>
<h4 id="✅-예제-피보나치-수열-fibonacci-sequence"><strong>✅ 예제: 피보나치 수열 (Fibonacci Sequence)</strong></h4>
<pre><code class="language-js">function fibonacci(n) {
  if (n &lt;= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(5));
</code></pre>
<p>✅ <strong>호출 과정 (<code>fibonacci(5)</code>)</strong></p>
<pre><code>fibonacci(5) → fibonacci(4) + fibonacci(3)
              → (fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1))
              → ...</code></pre><p>➡ *<em>한 번의 호출에서 두 번의 재귀 호출이 발생하기 때문에, 트리 형태의 호출 구조가 생기며, 총 호출 횟수는 `O(2^n)이 됩니다. *</em></p>
<hr>
<h2 id="3-비트마스크-bitmasking"><strong>3. 비트마스크 (Bitmasking)</strong></h2>
<p>비트 연산자 (<code>|</code>, <code>&amp;</code>, <code>&lt;&lt;</code> 등)를 활용해 문제를 해결하는 방식으로, <strong>부분 집합을 구하거나 특정 조건을 빠르게 체크할 때 유용</strong>합니다.</p>
<h3 id="✅-비트-연산자-bitwise-operators"><strong>✅ 비트 연산자 (Bitwise Operators)</strong></h3>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/4ac6c94a-0e00-4c63-a6f1-f444e1e91aa4/image.png" alt=""></p>
<h3 id="✅-비트마스크-활용-예제-부분-집합-구하기"><strong>✅ 비트마스크 활용 예제: 부분 집합 구하기</strong></h3>
<pre><code class="language-js">const arr = [1, 2, 3];
const n = arr.length;

for (let i = 0; i &lt; (1 &lt;&lt; n); i++) {  // 2^n 개의 부분집합 탐색
  let subset = [];
  for (let j = 0; j &lt; n; j++) {
    if (i &amp; (1 &lt;&lt; j)) subset.push(arr[j]); // j번째 비트가 1이면 해당 원소 포함
  }
  console.log(subset);
}</code></pre>
<p>✅ <strong>시간 복잡도 <code>O(2^N)</code>, 연산 속도가 빠름</strong><br>✅ <strong><code>(1 &lt;&lt; j)</code>를 사용하여 특정 비트가 포한되어 있는지 확인</strong>  </p>
<p>전에 백엔드 개발자분께서 <strong>권한 관리를 비트로 한다고</strong> 했을 때, 당시에는 이해가 안 갔지만,  공부하면서 <strong>비트마스크를 활용하면 메모리를 절약하고 연산을 빠르게 수행할 수 있다는 점</strong>을 조금은 이해할 수 있었습니다..<br>사실 아직도, <strong>굳이 이걸 써야 하나?</strong> 싶긴 하지만, <strong>메모리 관리가 중요한 시스템에서는 유용하게 쓰일 것 같긴합니다.</strong> 
예를 들어, <strong>권한을 관리하거나, 대량의 데이터를 효율적으로 저장할 때는 꽤 효과적인 방식</strong>이라는 점이라고 생각합니다.</p>
<p>다만, <strong>프론트엔드 개발에서는 비트마스크를 사용할 일이 거의 없을 것 같다는 생각이 듭니다.</strong><br>오히려 <strong>비트 연산을 쓰면 코드가 난독화되어 다른 프론트엔드 개발자가 읽기 어려운 코드가 될 가능성이 높다고 판단됩니다.</strong> 
프론트엔드에서는 <strong>가독성이 더 중요한 경우가 많으니, 굳이 비트 연산을 사용할 필요는 없지 않을까?</strong> 하는 생각이 듭니다.</p>
<hr>
<h2 id="4-순열-permutation"><strong>4. 순열 (Permutation)</strong></h2>
<p>순열이란 <strong>주어진 숫자들을 배치할 수 있는 모든 경우</strong>를 의미합니다.<br>즉, 숫자의 <strong>순서가 다르면 서로 다른 경우로 취급</strong>합니다.</p>
<p>예를 들어, <code>[1, 2, 3]</code>의 경우  </p>
<ul>
<li><code>[1, 2, 3]</code>과 <code>[3, 2, 1]</code>은 서로 다른 경우로 인정됩니다.  </li>
</ul>
<p>즉, 순열을 구한다는 것은 <strong>모든 가능한 배치를 나열하는 것</strong>을 의미합니다.</p>
<h3 id="✅-순열을-구하는-사고-과정"><strong>✅ 순열을 구하는 사고 과정</strong></h3>
<p>순열을 구하는 과정은 <strong>재귀적(Recursive) 사고 방식</strong>을 사용합니다.</p>
<h4 id="📌-1-숫자가-한-개일-때"><strong>📌 1. 숫자가 한 개일 때</strong></h4>
<pre><code class="language-js">[1] // 그 자체가 순열이 됩니다. (1)</code></pre>
<hr>
<h4 id="📌-2-숫자가-두-개일-때"><strong>📌 2. 숫자가 두 개일 때</strong></h4>
<pre><code class="language-js">[1, 2] → [1, 2], [2, 1]</code></pre>
<h4 id="📌-3-숫자가-세-개일-때-1-2-3"><strong>📌 3. 숫자가 세 개일 때 (<code>[1, 2, 3]</code>)</strong></h4>
<p>각 숫자를 처음에 놓고, 나머지 숫자로 다시 순열을 만듭니다.</p>
<ul>
<li><strong><code>1</code>을 첫 번째 자리에 놓고 <code>[2, 3]</code>의 순열을 구함</strong></li>
<li><strong><code>2</code>를 첫 번째 자리에 놓고 <code>[1, 3]</code>의 순열을 구함</strong></li>
<li><strong><code>3</code>을 첫 번째 자리에 놓고 <code>[1, 2]</code>의 순열을 구함</strong></li>
</ul>
<pre><code class="language-js">1️⃣  [1] + 순열([2, 3]) → [1, 2, 3], [1, 3, 2]
2️⃣  [2] + 순열([1, 3]) → [2, 1, 3], [2, 3, 1]
3️⃣  [3] + 순열([1, 2]) → [3, 1, 2], [3, 2, 1]</code></pre>
<p>➡ <strong>모든 경우의 수를 나열하면 다음과 같습니다.</strong></p>
<pre><code>[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]</code></pre><p>이 과정을 반복하면 어떤 숫자가 주어지더라도 <strong>순열을 구할 수 있습니다!</strong>  </p>
<hr>
<h3 id="✅-순열을-구하는-코드"><strong>✅ 순열을 구하는 코드</strong></h3>
<p>위의 개념을 <strong>재귀(Recursive Function)</strong> 를 이용해 코드로 표현하면 아래와 같습니다.</p>
<pre><code class="language-js">function permute(arr, selected = []) {
  if (arr.length === 0) {
    console.log(selected); // 모든 숫자를 선택한 경우 출력
    return;
  }

  for (let i = 0; i &lt; arr.length; i++) {
    let newArr = [...arr];  // 배열 복사
    newArr.splice(i, 1);    // 현재 선택한 숫자 제외
    permute(newArr, [...selected, arr[i]]); // 선택한 숫자를 추가하여 재귀 호출
  }
}

permute([1, 2, 3]);</code></pre>
<ul>
<li><strong>처음 숫자를 선택하고, 나머지 숫자로 순열을 구하는 방식</strong>  </li>
<li><strong>배열이 비어있으면 하나의 순열을 완성한 것이므로 출력</strong>  </li>
</ul>
<h3 id="📌-다음-순열next-permutation-문제-해결-방법"><strong>📌 다음 순열(Next Permutation) 문제 해결 방법</strong></h3>
<p>만약 <strong>다음 순열을 구하는 문제</strong>가 주어진다면, 다음과 같은 <strong>공식</strong>으로 풀 수 있습니다. </p>
<p>예를 들어, 다음과 같은 배열이 주어졌다고 가정해 본다면,  </p>
<pre><code class="language-js">let arr = [1, 2, 3, 6, 5, 4];</code></pre>
<p>이 배열의 <strong>다음 순열</strong>을 구하는 공식적인 방법은 다음과 같습니다.</p>
<h4 id="✅-다음-순열을-구하는-공식적인-방법"><strong>✅ 다음 순열을 구하는 공식적인 방법</strong></h4>
<ol>
<li><p><strong>배열을 뒤에서부터 탐색하며 가장 긴 &#39;내림차순&#39;을 찾는다.</strong>  </p>
<ul>
<li>즉, <strong>처음으로 증가하는 위치를 찾는다.</strong>  </li>
<li>이 위치가 <strong>순열을 변경할 기준점</strong>이 됨.  </li>
</ul>
</li>
<li><p><strong>그 위치의 값보다 큰 값 중, 가장 오른쪽에 있는 작은 값과 Swap(교환)</strong>  </p>
<ul>
<li>사전순으로 다음 순열을 만들기 위해, <strong>가장 작은 증가 요소</strong>를 찾아 교환.  </li>
</ul>
</li>
<li><p><strong>기준점 이후의 숫자들을 &#39;오름차순&#39; 정렬</strong>  </p>
<ul>
<li>Swap 후에는 <strong>뒷부분을 오름차순 정렬</strong>하여 사전순으로 다음에 오는 순열을 만든다.  </li>
</ul>
</li>
</ol>
<pre><code class="language-js">function nextPermutation(arr) {
  let i = arr.length - 2;

  // 1. 뒤에서부터 탐색하며 증가하는 지점 찾기
  while (i &gt;= 0 &amp;&amp; arr[i] &gt;= arr[i + 1]) {
    i--;
  }

  if (i &gt;= 0) { 
    let j = arr.length - 1;

    // 2. arr[i]보다 크면서 가장 작은 값을 찾기
    while (arr[j] &lt;= arr[i]) {
      j--;
    }

    // 3. Swap (값 교환)
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }

  // 4. i 이후의 숫자들을 오름차순 정렬
  let left = i + 1, right = arr.length - 1;
  while (left &lt; right) {
    [arr[left], arr[right]] = [arr[right], arr[left]];
    left++;
    right--;
  }
}

let arr = [1, 2, 3, 6, 5, 4];
nextPermutation(arr);
console.log(arr); // [1, 2, 4, 3, 5, 6]</code></pre>
<p>순열의 경우 <strong>모든 숫자를 배치하는 방법을 고려</strong>해야 하므로 <strong><code>N!</code>(팩토리얼) 개의 경우가 존재</strong>합니다. 즉, <strong>시간 복잡도는 <code>O(N!)</code></strong> 이 됩니다. 그렇기 때문에, N이 커질수록 계산량이 급격히 증가하므로 최적화 필요할듯합니다. </p>
<h2 id="5-백트래킹-backtracking"><strong>5. 백트래킹 (Backtracking)</strong></h2>
<p>백트래킹(Backtracking)은 <strong>가능성이 없는 경로는 미리 탐색을 중단하는 방식</strong>입니다.<br>즉, <strong>&quot;가망이 없는 경우는 빨리 포기하고 되돌아간다(Backtrack)&quot;</strong>는 원리로 동작합니다.<br>완전 탐색(Brute Force)보다 <strong>더 효율적인 탐색 방법</strong>으로 사용됩니다.  </p>
<blockquote>
<p><strong>백트래킹이 중요한 이유</strong>  </p>
<ol>
<li><strong>모든 경우의 수를 탐색해야 하지만, 불필요한 탐색은 줄이고 싶을 때</strong>  </li>
<li><strong>가능한 해를 찾는 문제에서 효율적인 탐색이 필요할 때 즉, 불필요한 탐색을 줄일때</strong>  </li>
</ol>
</blockquote>
<p>백트래킹 문제를 풀 때 가장 중요한 개념은 다음 두 가지입니다.</p>
<h3 id="-pruning-가지치기">** Pruning (가지치기)**</h3>
<ul>
<li><strong>불필요한 경로를 미리 제거</strong>하여 탐색을 줄이는 기법  </li>
<li>즉, 가능성이 없는 경로라면 <strong>더 깊이 탐색하지 않고 되돌아가는 것</strong>  </li>
</ul>
<h3 id="-재귀적-탐색--원상복구">** 재귀적 탐색 + 원상복구**</h3>
<ul>
<li>백트래킹을 진행하면서, <strong>탐색했던 값을 재귀적으로 처리</strong>한 후 <strong>원상복구하는 과정</strong>이 필수  </li>
</ul>
<h3 id="백트래킹-예제-n-queen-문제">백트래킹 예제: N-Queen 문제</h3>
<p>백트래킹을 설명할때, 가장 많이 나오는 예제는 N-Queen 인듯 합니다. 이 문제로, 백트래킹을 어떻게 구현할 수 있는지 설명해보겠습니다.
N-Queen 문제는 <strong>N x N 체스판에 N개의 퀸을 서로 공격하지 않도록 배치하는 문제</strong>입니다.<br>퀸은 같은 행, 같은 열, 그리고 대각선 방향으로 이동할 수 있기 때문에<br><strong>백트래킹을 활용하여 탐색을 줄이는 것이 중요합니다.</strong>  </p>
<h4 id="-해결-방법">** 해결 방법**</h4>
<ol>
<li><strong>퀸을 한 줄씩 배치하면서 조건을 만족하는지 체크</strong>  </li>
<li><strong>이미 공격받는 위치라면 더 이상 탐색하지 않음 (Pruning)</strong>  </li>
<li><strong>모든 퀸을 배치하면 정답으로 저장</strong>  </li>
<li><strong>재귀적으로 탐색하며, 원상복구하여 다른 경우도 탐색</strong>  </li>
</ol>
<pre><code class="language-js">function solveNQueen(N) {
  let board = new Array(N).fill(-1);
  let result = [];

  function backtrack(row) {
    if (row === N) {
      result.push([...board]); // 하나의 해를 저장
      return;
    }

    for (let col = 0; col &lt; N; col++) {
      if (isValid(board, row, col)) {
        board[row] = col;   // 🔹 현재 행(row)에 퀸 배치
        backtrack(row + 1); // 🔹 다음 행 탐색 (재귀 호출)
        board[row] = -1;    // 🔹 원상복구 (다른 경우 탐색을 위해)
      }
    }
  }

  function isValid(board, row, col) {
    for (let i = 0; i &lt; row; i++) {
      if (board[i] === col || Math.abs(board[i] - col) === Math.abs(i - row)) {
        return false;
      }
    }
    return true;
  }

  backtrack(0);
  console.log(result);
}

solveNQueen(4);</code></pre>
<p>✅ <strong>불필요한 탐색을 피하기 위해 <code>isValid()</code> 함수로 Pruning 적용</strong><br>✅ <strong>재귀적으로 탐색을 진행하면서 유효한 경우에만 진행</strong><br>✅ <strong>탐색이 끝나면 원상복구(<code>board[row] = -1</code>)하여 다른 경우도 탐색 가능하게 함</strong>  </p>
<p>백트래킹 문제를 만났을때, 다음과 같은 사고과정을 통하면 어떻게 문제를 풀지 좀 더 감이 잡히는듯 합니다. </p>
<ol>
<li><strong>문제를 보고 &quot;모든 경우의 수&quot;를 탐색해야 한다면, 백트래킹을 고려합니다.</strong>  </li>
<li><strong>탐색 중 불필요한 경우를 빨리 포기하는 &quot;Pruning&quot;을 적용</strong> 즉, 재귀 탈출 조건을 고려합니다.   </li>
<li><strong>현재 상태를 저장하거나 원상복구하여 다른 경우를 탐색할 준비</strong>  </li>
</ol>
<hr>
<h2 id="📌-마무리"><strong>📌 마무리</strong></h2>
<p>완전탐색(Brute Force)은 <strong>모든 경우의 수를 탐색하여 해를 구하는 방식</strong>입니다.<br>하지만 경우의 수가 많아지면 시간이 오래 걸리므로,<br>👉 <strong>백트래킹 등의 기법을 활용하여 최적화하는 것이 중요합니다!</strong>  </p>
<p>다음 포스팅에서는 <strong>DFS와 BFS를 활용한 탐색 기법</strong>에 대해 다뤄보겠습니다. 🚀  </p>
<blockquote>
<p><strong>완전탐색 알고리즘 팁</strong>
완전탐색 알고리즘 부터 난이도가 확 올리가, 이해하는데 어려움을 많이 겪었는데, Chat-gpt와 다음과 같이 공부하면서 이해도를 높였습니다. 저처럼 공부해보시면 좋을것같습니다!
<img src="https://velog.velcdn.com/images/gahui_dev/post/a084e7c9-decc-4083-8991-d583aa3f7785/image.png" alt=""></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[PEC 1주차 - 사용자의 문제를 찾기위해 필요한것]]></title>
            <link>https://velog.io/@gahui_dev/PEC-1semaine</link>
            <guid>https://velog.io/@gahui_dev/PEC-1semaine</guid>
            <pubDate>Sun, 02 Mar 2025 03:28:53 GMT</pubDate>
            <description><![CDATA[<p>불과 몇 년 전만 해도 프론트엔드와 백엔드는 명확히 구분된 영역이었다. 리액트(React), 뷰(Vue), 앵귤러(Angular) 같은 프론트엔드 프레임워크가 급속도로 발전하면서, 프론트엔드 개발자는 UI와 사용자 경험(UX)에 집중하고, 백엔드 개발자는 서버와 데이터 관리를 책임지는 역할로 나뉘는 것이 일반적이었다.  </p>
<p>하지만 기술이 발전하면서 이러한 경계는 점점 흐려지기 시작했다. 특히, ChatGPT와 같은 AI 기술의 등장으로 개발 패러다임 자체가 바뀌기 시작했다. 코드 작성의 효율성이 극대화되면서, <strong>문제를 정의하고 해결하는 능력</strong>이 더욱 중요해졌다. 여기에 Next.js의 API Routes 기능과 같은 기술이 발전하면서, 프론트엔드 개발자가 간단한 서버 로직을 다룰 수 있는 환경도 마련되었다. 이제는 프론트엔드와 백엔드를 따로 구분하기보다, 전체적인 제품 개발을 이해하고 문제를 해결할 수 있는 <strong>&quot;풀스택 개발자&quot;</strong> 혹은 <strong>&quot;프러덕트 엔지니어(Product Engineer)&quot;</strong>를 원하는 기업이 늘어나고 있다.  </p>
<p>이러한 변화를 마주하면서, 나 또한 <strong>&quot;프론트엔드 개발자&quot;라는 역할에만 머물러 있어도 괜찮을까?&quot;</strong> 하는 고민이 들기 시작했다. 점점 더 많은 기업이 단순한 UI 개발이 아니라, <strong>&quot;사용자의 문제를 정의하고, 이를 기술로 해결할 수 있는 개발자&quot;</strong>를 원하고 있었다. 단순한 기능 구현을 넘어, 문제를 발견하고 해결하는 능력을 갖춘 개발자가 되는것이 중요해 질것이라는 생각이 들었다. </p>
<hr>
<h2 id="product-engineer란-무엇인가"><strong>Product Engineer란 무엇인가?</strong></h2>
<p>프러덕트 엔지니어(Product Engineer)는 단순히 기능을 구현하는 개발자가 아니다. 사용자의 문제를 정의하고, 이를 기술적으로 해결하는 개발자다. 무엇을 만들 것인지 고민하기 전에, 왜 만들어야 하는지부터 생각하는 태도가 중요하다.</p>
<p>어떤 일을 할 때 <strong>&quot;이걸 왜 하는가?&quot;</strong>를 고민하지 않으면, 프로젝트가 진행될수록 방향이 흔들리고 금방 지치게 된다. 처음에는 좋은 아이디어 같아도, 문제의 본질이 명확하지 않으면 지속하기 어렵다. 그래서 단순한 기능 구현이 아니라, 문제를 제대로 정의하는 과정이 중요하다.</p>
<p>PEC의 8주 과정에서도 코드 작성은 마지막 단계에 배치된다. 기술보다 문제 정의가 먼저이기 때문이다. 특히 1주차에서는 단순히 <strong>&quot;이게 불편하니까 만들어야겠다&quot;</strong>가 아니라, 이게 정말 문제인지, 해결했을 때 사용자에게 어떤 변화를 줄 수 있는지 깊이 고민하는 시간을 갖는다.</p>
<p>결국, 기술은 문제를 해결하는 도구일 뿐, 가장 중요한 것은 해결해야 할 문제를 제대로 정의하는 것이다. 이런 사고방식이야말로 프러덕트 엔지니어가 갖춰야 할 가장 중요한 역량이라고 생각한다.</p>
<h2 id="골든-서클golden-circle과-문제-정의"><strong>골든 서클(Golden Circle)과 문제 정의</strong></h2>
<p>문제를 찾는 과정에서 가장 중요하게 다뤄진 개념이 <strong>골든 서클(Golden Circle)</strong>이다. 이는 문제 해결을 위한 <strong>체계적이고 지속 가능한 의사결정 원칙</strong>으로, 단순히 <strong>&quot;무엇을 만들 것인가?&quot;</strong>가 아니라, <strong>&quot;왜 만들어야 하는가?&quot;</strong>부터 고민하는 방식이다.  </p>
<h3 id="골든-서클이란"><strong>골든 서클이란?</strong></h3>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/a49e7e3a-e92a-4027-961d-ca26d99ca121/image.png" alt="골든서클">  </p>
<ol>
<li><strong>WHY</strong> – 왜 문제를 해결해야 하는가?  </li>
<li><strong>HOW</strong> – 어떤 방법으로 해결할 것인가?  </li>
<li><strong>WHAT</strong> – 무엇을 만들 것인가?  </li>
</ol>
<h3 id="골든-서클-적용-방식"><strong>골든 서클 적용 방식</strong></h3>
<h4 id="일반적인-접근"><strong>일반적인 접근</strong></h4>
<blockquote>
<p>&quot;우리는 강력한 배터리를 가진 스마트폰을 만들었습니다. 화면이 크고 성능이 뛰어납니다.&quot;  </p>
</blockquote>
<h4 id="골든-서클-접근"><strong>골든 서클 접근</strong></h4>
<blockquote>
<p>&quot;우리는 사람들의 일상을 더 자유롭게 만들고 싶습니다. 그래서 배터리 걱정 없이 온종일 사용할 수 있도록 만들었고, 누구나 편리하게 쓸 수 있도록 디자인했습니다. 이제, 강력한 배터리를 가진 스마트폰이 탄생했습니다.&quot;  </p>
</blockquote>
<p>많은 사람들이 사이드 프로젝트를 시작할 때 <strong>WHAT</strong>에서 출발한다. &quot;이런 기능이 있으면 좋겠다&quot;라는 생각으로 시작하지만, WHY 없이 진행하면 방향성을 잃고 프로젝트가 흐지부지되기 쉽다.  </p>
<p>그래서 PEC 1주차에서는 단순히 <strong>&quot;무엇을 만들 것인가?&quot;</strong>가 아니라, <strong>&quot;왜 이걸 만들어야 하는가?&quot;</strong>에 대한 깊은 고민을 하는 시간을 갖는다. <strong>WHY가 명확해야 HOW와 WHAT도 탄탄해진다.</strong></p>
<hr>
<h2 id="사용자-인터뷰의-중요성"><strong>사용자 인터뷰의 중요성</strong></h2>
<p>그렇다면, 이런 WHY을 어떻게 찾아 낼 수 있을까? <strong>어떤 문제가 존재하는지, 사용자들이 무엇을 불편해하는지 먼저 파악하는 과정이 필요하다.</strong> 그리고 이를 알아내는 가장 효과적인 방법이 바로 <strong>&quot;사용자 인터뷰&quot;</strong>다.  </p>
<p>1주 차 교안에서는 사용자 인터뷰와 개발의 관계에 대해 다루고 있었다.<br><img src="https://velog.velcdn.com/images/gahui_dev/post/95c4afae-9be5-4f05-ba6a-d018284d44cd/image.png" alt=""></p>
<p>대충 정리하자면, 아래와 같이 요약할 수 있다. </p>
<ul>
<li><p><strong>사용자 인터뷰란?</strong>  </p>
<ul>
<li>사용자의 실제 경험을 듣고 문제점을 파악하는 과정  </li>
<li>단순한 의견 청취가 아니라, 사용자의 행동과 심리를 분석하는 과정  </li>
</ul>
</li>
<li><p><strong>사용자 인터뷰가 개발에 기여하는 점</strong>  </p>
<ul>
<li>사용자 중심의 제품 설계 가능  </li>
<li>사용성 테스트를 통해 UI/UX 개선  </li>
<li>데이터 기반 의사결정 가능  </li>
<li>접근성 문제 해결  </li>
</ul>
</li>
</ul>
<p>처음에는 <strong>&quot;사용자 인터뷰가 Product Engineer와 무슨 관련이 있을까?&quot;</strong>라는 의문이 들었다. &quot;문제를 정의하라&quot;라고 하지만, 이미 수많은 서비스를 사용하고 있는 입장에서 <strong>어떤 문제를 찾아야 할지 감이 잡히지 않았다.</strong>  </p>
<p><strong>&quot;내가 불편함을 느끼지 않는다고 해서, 모든 사람이 불편함이 없는 것은 아니다.&quot;</strong><br>내가 겪지 못한 불편함이 존재할 수도 있고, 실제 사용자들은 전혀 다른 시각에서 불편함을 느낄 수도 있다.  </p>
<p>결국, 사용자 인터뷰는 문제를 발견하는 과정이며, 이를 통해 무엇을 해결해야 할지 명확히 정의하는 것이 중요하다고 깨닫게 되었다.</p>
<hr>
<h2 id="why를-찾는-과정"><strong>WHY를 찾는 과정</strong></h2>
<p>처음에 만들고 싶은 서비스가 없었지만, 아이디어가 많은분이 계셨다. 그래서 그분의 아이디어를 기반으로 주제를 선정했다.</p>
<h3 id="선정된-주제-프로폴리오"><strong>선정된 주제: &quot;프로폴리오&quot;</strong></h3>
<blockquote>
<p>네트워킹 자리에서 자기소개가 어려운 사람들을 위해, 짧은 시간 안에 자신의 개성과 특징을 효과적으로 전달할 수 있도록 돕는 서비스.</p>
</blockquote>
<p>이를 구체화하기 위해 <strong>반구조화된 사용자 인터뷰 질문지</strong>를 먼저 작성했다.</p>
<hr>
<h2 id="반구조화된-사용자-인터뷰"><strong>반구조화된 사용자 인터뷰</strong></h2>
<p>반구조화된 인터뷰는 <strong>기본적인 질문을 준비하되, 사용자의 응답에 따라 추가 질문을 하며 더 깊이 있는 통찰을 얻는 방식</strong>이다. 즉, 미리 정해진 틀에 따라 진행하는 것이 아니라, <strong>사용자의 반응과 답변을 바탕으로 대화를 확장해 나가는 과정</strong>이다.  </p>
<p>아래는 교안에서 설명하는 반구조화 인터뷰의 개념이다.<br><img src="https://velog.velcdn.com/images/gahui_dev/post/80a02ff5-301c-4185-902e-1c47ab84a92f/image.png" alt="">  </p>
<h3 id="인터뷰에서-가장-중요한-두-가지-요소"><strong>인터뷰에서 가장 중요한 두 가지 요소</strong></h3>
<ol>
<li><p><strong>공감</strong>  </p>
<ul>
<li>사용자는 본인이 어떤 문제를 가지고 있는지조차 인지하지 못하는 경우가 많다. 따라서 인터뷰어가 사용자의 입장에서 공감하며 이야기를 이끌어가는 것이 중요하다.  </li>
</ul>
</li>
<li><p><strong>유도 질문을 지양해야 한다</strong>  </p>
<ul>
<li>이미 머릿속으로 서비스에 대한 그림을 그려둔 상태에서, 원하는 답을 이끌어내기 위한 &quot;답정너&quot;식 질문을 던지면 안 된다. 이렇게 되면 인터뷰 결과가 편향될 수 있고, 다양한 시각을 반영하기 어려워진다. 결국, 많은 인터뷰의 답변이 비슷해지는 문제가 발생할 수 있다.  </li>
</ul>
</li>
</ol>
<h3 id="인터뷰-구성"><strong>인터뷰 구성</strong></h3>
<ol>
<li><strong>기본 질문 (Ice Breaking)</strong>  <ul>
<li>사용자가 긴장을 풀고 편안하게 대화할 수 있도록 간단한 질문을 던진다.  </li>
</ul>
</li>
<li><strong>현재의 경험과 상황</strong>  <ul>
<li>네트워킹이나 자기소개 경험에 대해 질문하며, 사용자의 기존 행동 패턴을 파악한다.  </li>
</ul>
</li>
<li><strong>문제와 필요</strong>  <ul>
<li>사용자가 겪고 있는 불편함과 어려움을 탐색한다.  </li>
</ul>
</li>
<li><strong>서비스 기대와 피드백</strong>  <ul>
<li>이런 서비스가 있다면 실제로 사용할지, 어떤 기능이 필요할지 논의한다.  </li>
</ul>
</li>
<li><strong>추가 질문</strong>  <ul>
<li>인터뷰어가 자유롭게 질문하며, 사용자가 더욱 깊은 의견을 제시할 수 있도록 유도한다.  </li>
</ul>
</li>
</ol>
<p>사용자 인터뷰를 진행하면서 깨달은 점은, <strong>사용자는 본인이 겪고 있는 문제를 명확하게 인식하지 못한다는 것</strong>이다. 그렇기 때문에, 인터뷰를 통해 사용자의 내면에 숨겨진 니즈를 끌어내는 과정이 무엇보다 중요하다.</p>
<p>아래는 위에서 설명한 내용을 기반으로 작성한 인터뷰지이다. 
<img src="https://velog.velcdn.com/images/gahui_dev/post/ee51e6df-7479-4277-9e17-f209c0bd47b7/image.png" alt=""></p>
<p>이렇게 각자의 인터뷰지를 완성하면, 하나의 인터뷰지로 통일하는 과정을 거치고, 아래의 인터뷰지를 통해, 사용자 인터뷰를 진행한다. </p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/1b76bd92-d1d5-4dd2-bad4-ebb9603c8ee7/image.png" alt=""></p>
<h3 id="인터뷰-결과-요약"><strong>인터뷰 결과 요약</strong></h3>
<p>인터뷰를 진행하며 얻은 주요 응답을 정리하면, 다음과 같은 문제점과 니즈가 도출되었다.  </p>
<ul>
<li>네트워킹 자리에서 <strong>구두로만 자기소개를 하다 보니</strong>, 상대방이 자신을 제대로 이해하지 못하는 경우가 많았다.  </li>
<li>소셜 프로필을 작성하는 과정이 <strong>번거롭고 어렵게 느껴졌다</strong>.  </li>
<li>자기소개를 <strong>간편하게 정리해주는 프로필이 있다면</strong>, 네트워킹이 훨씬 수월해질 것 같았다.  </li>
<li><strong>키워드를 활용해 자신의 개성을 표현할 수 있는 기능</strong>이 있으면 좋겠다는 의견이 많았다.  </li>
<li>상황에 따라 다르게 활용할 수 있도록, <strong>공식적인 자리와 사적인 자리를 구분할 수 있는 멀티 프로필 기능</strong>이 필요하다고 느꼈다.  </li>
</ul>
<p>이러한 피드백을 통해, 단순한 프로필 생성이 아니라 <strong>자신을 효과적으로 표현하고, 상황에 맞게 활용할 수 있는 시스템이 필요함을 확인할 수 있었다.</strong></p>
<hr>
<h2 id="페르소나persona-작성"><strong>페르소나(Persona) 작성</strong></h2>
<p>인터뷰 데이터를 바탕으로 <strong>가상의 사용자 페르소나</strong>를 작성했다. 페르소나는 특정 타겟 사용자의 행동과 니즈를 정리한 것으로, <strong>WHY와 HOW를 구체화하는 데 도움을 주는 도구</strong>다. 이를 통해 사용자의 실제 고민과 기대를 보다 명확하게 파악할 수 있다.  </p>
<h3 id="페르소나-구성-요소"><strong>페르소나 구성 요소</strong></h3>
<ul>
<li><strong>행동(Actions)</strong> – 사용자가 현재 어떤 방식으로 문제를 해결하고 있는가?  </li>
<li><strong>동기(Motivations)</strong> – 무엇이 사용자를 움직이게 하는가?  </li>
<li><strong>문제(Pains)</strong> – 사용자가 겪고 있는 어려움은 무엇인가?  </li>
<li><strong>가치(Values)</strong> – 사용자가 제품에서 중요하게 여기는 요소는 무엇인가?  </li>
</ul>
<p>각자의 인터뷰 데이터를 바탕으로, 다음과 같은 <strong>페르소나를 정리하는 과정</strong>을 거쳤다.  </p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/045802ff-d1ac-47c3-936a-542db1bc5932/image.png" alt="">  </p>
<p>이 과정을 통해, 단순한 기능 개발이 아니라 <strong>사용자의 실질적인 문제를 해결할 수 있는 방향으로 서비스의 핵심을 구체화할 수 있었다.</strong></p>
<hr>
<h2 id="마무리"><strong>마무리</strong></h2>
<p>1주 차 세션을 통해, 단순히 <strong>&quot;무엇을 만들까?&quot;</strong>에서 출발하는 것이 아니라, WHY를 정의하는 것이 얼마나 중요한지 배울 수 있었다. 그리고 그 WHY를 찾기 위해서는 사용자 인터뷰가 필수적이라는 점을 깨닫게 되었다.</p>
<p>이번 주차에서는 사용자 인터뷰를 통해 문제를 찾아내고, 이를 바탕으로 하나의 페르소나를 완성하는 과정을 거쳤다. 이 과정에서 가장 중요한 점은 인터뷰어의 마음가짐이었다. 사용자의 눈높이에서 문제를 바라보고, 공감하는 자세를 갖는 것, 그리고 답정너식 질문을 지양하며 열린 대화를 유도하는 것이 핵심이었다.</p>
<p>다음 주차에서는 이 페르소나를 기반으로 서비스를 구체화하는 과정을 다룰 예정이다.</p>
<h3 id="참고한글"><strong>참고한글</strong></h3>
<ul>
<li><a href="https://salaryblues.tistory.com/6">WHW(Why? How? What? 골든 서클 이론</a></li>
<li><a href="https://www.beusable.net/blog/?p=6357">페르소나 정하는데도 데이터가 필요한가요?</a></li>
<li>PEC 1주차 교안 및 Miro</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[회사일만 열심히했는데, 뒤돌아 보니 남은것들...(a.k.a PEC 신청계기)]]></title>
            <link>https://velog.io/@gahui_dev/PEC-%EC%B0%B8%EC%97%AC-%EA%B3%84%EA%B8%B0</link>
            <guid>https://velog.io/@gahui_dev/PEC-%EC%B0%B8%EC%97%AC-%EA%B3%84%EA%B8%B0</guid>
            <pubDate>Thu, 06 Feb 2025 15:06:46 GMT</pubDate>
            <description><![CDATA[<h3 id="회사일만-열심히했는데-뒤돌아-보니-남은것들-">*<em>회사일만 열심히했는데, 뒤돌아 보니 남은것들.. *</em></h3>
<p>작년 9월, 회사에서 정리해고가 시행되었고, 나 역시 그 대상에 포함되었다. 1년여 동안 팀원들과 야근을 해가며 프로젝트를 수행해왔던 터라, 막상 해고 대상이 되었다는 소식을 들었을 때 멍해졌다. 입으로는 <strong>&quot;퇴사하고 싶다&quot;</strong>, <strong>&quot;차라리 짤리고 싶다&quot;</strong>는 말을 가볍게 내뱉곤 했지만, 실제로 내 일이 되자 눈물이 고였다. <del>(다행히, 아직은 같은 회사에 다니고 있다)</del></p>
<p>1년 넘게 일한 회사였지만, 어쩔 수 없이 다시 이력서를 준비하기 시작했다. 돌이켜보면, 그동안 새롭게 맡았던 프로젝트가 많았기 때문에 이번 이직은 좀 더 수월하지 않을까 하는 근거 없는 자신감도 있었다. 다행히도 가고 싶었던 회사들에서 서류 통과 소식을 들을 수 있었고, 면접을 위해 이력서를 기반으로 기술 개념들을 다시 복습했다.</p>
<p>하지만 면접에서 받은 질문들은 내가 예상했던 수준과는 달랐다. 단순히 <strong>&quot;A가 무엇인가요?&quot;</strong> 같은 개념 설명이 아니라, <strong>&quot;왜 그렇게 했나요?&quot;</strong>, <strong>&quot;어떤 고민을 했나요?&quot;</strong> 같은 본질적인 질문들이 이어졌다. 기술적인 개념을 묻는 데서 그치지 않고, 문제 해결 과정에서 내가 어떤 생각을 했고, 스스로 더 나은 방법을 찾아 개선해본 경험이 있는지를 확인하려 했다.</p>
<p>기술 면접에서 탈탈 털린 후, 나의 문제를 깨닫게 되었다. 나는 그동안 <strong>주어진 기획서를 그대로 구현하고, 일정에 맞춰 개발하는 것에만 집중</strong>했다. 사실 이것만으로도 나에겐 너무 벅찼다. 또다른 한편으로는 회사에서 개선해보고 싶은 것들이 너무 많이 있었는데, 도입해보려 했던 것들이 계속해서 좌절되다 보니 어느샌가 그냥 주어진 일만 하고 있었다.</p>
<p>이런 상황이 반복되다 보니 <strong>&quot;뭔가라도 해야 할 것 같다.&quot;</strong>는 압박감에 여러 스터디에도 참여해봤다. 타입스크립트 스터디, 디자인 패턴 스터디 등 여러 가지를 시도해봤지만, 결국 책을 읽고 예제 코드를 작성하는 것에서 그쳤다. 실무에서 적용할 기회가 없으니 결국 다시 제자리로 돌아오곤 했다.</p>
<p><strong>&quot;회사 탓인가? 아니면 내 탓인가?&quot;</strong>
<strong>&quot;어디서부터 잘못된 걸까? 무엇을 바꿔야 할까?&quot;</strong></p>
<p>그러던 중, 우연히 본 유튜브 영상이 뼈를 때렸다.</p>
<p><img src="https://velog.velcdn.com/images/gahui_dev/post/75dc2696-74bb-4eaf-a046-e844078a5d99/image.png" alt="개발자는 왜 커리어 악순환을 겪는가">
<a href="https://youtu.be/Oez86nYw7QQ?si=gdXaBvTg-gbeBUCG">📌 영상 링크(영상을 보다 눈물이 날수 있으니 조심)</a></p>
<p>이 영상에서는 이직과 커리어의 악순환에 대해 이야기하고 있었다.</p>
<blockquote>
<p>&quot;급한 마음으로 이직을 결심하고, 겉핥기식 학습을 하게 되면, 연봉은 올릴 수 있을지 몰라도 결국 비슷한 회사로 가게 된다.
그리고 연봉이 올랐다는 이유로 잘못된 효용감을 가지게 되고, 커리어의 악순환이 반복된다.&quot;</p>
</blockquote>
<p>이 말이 마치 나를 저격하는 것 같았고, 이 악순환을 끊고 싶었다. </p>
<p>그러던 중 <strong>Product Engineer Camp(PEC)</strong>를 알게 되었다.
<strong>&quot;단 0.1%라도 나에게 도움이 될 수 있지 않을까?&quot;</strong>
하는 마음으로 망설임 없이 신청했다.</p>
<p>적어도 이번에는 책으로만 배우는 것이 아니라, 실제 적용하며 변화를 만들어보려 한다. </p>
<hr>
<h3 id="알고리즘-공부를-다시-시작하다">알고리즘 공부를 다시 시작하다</h3>
<p>PEC 참여를 결정하고, 고민을 PEC 멘토님(경찬님)께 털어놨다. 그러자 멘토님이 말했다.</p>
<p><strong>&quot;PEC 시작 전에 알고리즘을 공부해보는 게 좋겠다.&quot;</strong>
그 말을 듣는 순간, 속으로 <strong>‘또 알고리즘인가…’</strong> 하는 생각이 들었다. 사실 알고리즘 공부에 대한 회의감이 컸다.</p>
<p>알고리즘은 성실함이 필요한데, 나는 그동안 꾸준히 하지 못했다. 대학 때도 알고리즘 수업을 재수강할 정도로 재미를 못 붙였고, 이제는 AI 시대인데 GPT에게 물어보면 되는 거 아닌가 싶었다. 그런데 경찬님은 오히려 그럴수록 알고리즘을 공부해야 한다고 했다.</p>
<blockquote>
<p>&quot;AI가 점점 발전하면서 앞으로 우리가 하는 많은 일이 AI로 대체될 거다. 그렇기 때문에 변하지 않는 걸 공부해야 한다. 알고리즘을 공부하면서 논리력과 사고력을 키우는 게 중요하다.&quot;</p>
</blockquote>
<p>그제야 ‘단순히 문제를 푸는 게 아니라 문제를 해결하는 방식 자체를 고민해야 한다’는 생각이 들었다. 나는 여태 코드가 돌아가면 됐지, 왜 이 방식이 최적인지 깊게 생각해본 적은 없었다. 문제를 해결하는 능력은 AI가 쉽게 대체할 수 없는 영역이고, 그 핵심이 알고리즘이었다.</p>
<p>그래서 이번에는 제대로 해보기로 했다. 하지만 예전처럼 무작정 문제를 풀지는 않았다. 멘토링을 받으면서, 일단 내가 직접 학습 순서를 짜는 것부터 시작했다. 바로 문제 풀이로 들어가는 게 아니라, 개념을 먼저 확실히 이해하는 방식이었다.</p>
<p>예를 들어 스택을 공부할 때도 그냥 &quot;스택은 LIFO 구조다&quot; 하고 넘어가는 게 아니라, 직접 구현해보고, 내가 이해한 내용을 블로그에 정리하면서 개념을 내 것으로 만들었다. 이렇게 공부하니 알고리즘 문제를 푸는 게 막막하게 느껴지지는 않았다. 그동안은 개념을 대충 알고 문제를 풀다 보니 중간에 막히는 일이 많았는데, 이번에는 개념을 먼저 다지고 문제를 풀었더니 이해하는 속도도 빨라졌다.</p>
<p>물론 지금은 잠시 멈춰 있지만, 여유가 생기면 다시 이어갈 생각이다.(<del>당장 다음주..?</del>) 이전과 같은 겉핥기식 공부가 아니라, 한 단계씩 차근히 쌓아가는 방식으로.</p>
<hr>
<h3 id="product-engineer-vs-project-engineer">Product Engineer vs. Project Engineer</h3>
<p>PEC 6기에 참여하면서 ‘Product Engineer’라는 개념을 처음 접했다. PEC 시작 전에 <strong>경찬님이</strong> <strong>&quot;Product형 개발자인가? Project형 개발자인가?&quot;</strong>라고 물었는데, 단 한 번도 생각해 본 적이 없는 주제라 머뭇거리며 이상한 답변을 했던 것 같다.</p>
<p>곱씹어 보니, 나는 지금까지 Project형 개발자로 일해왔다. 주어진 기획을 충실히 구현하고, 일정에 맞춰 프로젝트를 완수하는 데 집중했다. 프로젝트가 끝나면 거기서 역할이 끝났고, 이후 서비스가 어떻게 운영되는지, 사용자가 어떻게 반응하는지는 크게 신경 쓰지 않았다.</p>
<p>그런데 Product Engineer는 접근 방식이 달랐다. 단순히 개발을 수행하는 것이 아니라, 사용자 경험과 제품 개선을 고민하고 능동적으로 문제를 해결하는 개발자였다. <strong>&quot;이 기능이 왜 필요한가?&quot;, &quot;이걸 더 나은 방식으로 만들 수 없을까?&quot;, &quot;비즈니스적으로 더 좋은 선택은 뭘까?&quot;</strong> 같은 질문을 스스로 던지며, 개발 자체가 아니라 제품을 만든다는 관점에서 접근하는 역할이었다.</p>
<p>PEC를 진행하면서, 나는 단순히 프로젝트 단위의 개발이 아니라 제품 자체를 고민하는 시각이 필요하다는 것을 깨닫고 있다. 여전히 익숙한 방식에서 벗어나는 과정이 쉽지는 않지만, 기존과는 다른 사고방식을 가져야 한다는 점에서 고민의 깊이가 달라지고 있다.</p>
<p>이런 표현이 맞을지 모르겠지만... PEC 과정을 많은 사람이 알았으면 좋겠다. 또 한편으로는 너무 유명해지지 않았으면 하는, 마치 내가 발견한 숨은 맛집 같은 느낌이다. 그럼에도 불구하고,  연차가 쌓이는 것이 두려운 개발자들과 앞으로 어떤 방향으로 나아가야 할지 고민하는 분들께 꼭 추천하고 싶다.</p>
<p>참고로, 글 작성하는 시점기준으로 <strong>PEC 7기 추가 모집</strong>을 한다고 하니 고민하고 있다면, 얼른 신청 했으면 좋겠다!! </p>
<h1 id="pec에-대해-궁금하다면-아래-링크-클릭"><strong>PEC에 대해 궁금하다면? 아래 링크 클릭!</strong></h1>
<p><a href="https://slashpage.com/pec?lang=en">https://slashpage.com/pec?lang=en</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 [정렬]- 가장 큰 수 ]]></title>
            <link>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A0%95%EB%A0%AC-%EA%B0%80%EC%9E%A5-%ED%81%B0-%EC%88%98</link>
            <guid>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A0%95%EB%A0%AC-%EA%B0%80%EC%9E%A5-%ED%81%B0-%EC%88%98</guid>
            <pubDate>Tue, 24 Dec 2024 05:14:57 GMT</pubDate>
            <description><![CDATA[<ul>
<li>플랫폼 : 프로그래머스</li>
<li>레벨 : 2</li>
<li>링크 : <a href="https://school.programmers.co.kr/learn/courses/30/lessons/42746">https://school.programmers.co.kr/learn/courses/30/lessons/42746</a></li>
</ul>
<p>문제에서 주어진 값 : 정수 배열
결과 값 : 주어진 정수들을 이여붙여 만들 수 있는 숫자들 중 가장 큰 수</p>
<p>수도 코드</p>
<ol>
<li>앞자리가 큰순서대로 정렬되야 한다.</li>
<li>주어진 배열을 위 순서대로 내림차순 정렬한다.</li>
<li>정렬된 배열을 reduce함수를 이용해서 하나의 문자열 숫자로 만든다.</li>
</ol>
<p>단, 같은 숫자로 시작하는 숫자의 경우, 두 값을 비교해서 정렬한다. 
예를들어, 3과 31의 경우 [3, 31] 순으로 정렬되어야 한다.
3과 36의 경우 [36, 3] 순으로 정렬되어야 한다. </p>
<p>이 조건을 어떻게 정렬할 수 있을까? </p>
<ul>
<li>자바스크립트의 sort로 비교하되 두 값을 문자열 비교한다.</li>
<li>자바스크립트의 문자열 비교 함수 localeCompare를 이용한다. </li>
</ul>
<p>위 내용을 기반으로 작성된 코드는 다음과 같습니다. </p>
<pre><code class="language-js">function solution(numbers) {

    const sorted = numbers.map(String).sort((a, b) =&gt; {
        return (b + a).localeCompare(a + b)
    })

    return sorted.reduce((acc, curr) =&gt; {
        return `${acc}${curr}`
    })    
}</code></pre>
<p>다만, 위 코드로 제출시 한 문제가 통과가 되지 않았습니다. 생각하지 못한 경우의 수는 뭐가 있을까요? </p>
<p>바로, 0만으로 이루어진 배열의 경우 다음과 같은 문제가 발생합니다. 
[0,0,0,0] =&gt; 결과 &quot;0000&quot; </p>
<p>이러한 경우를 방지하기 위해 reduce함수에 다음 조건문을 추가해 줍니다. </p>
<pre><code class="language-js">        if(acc === &quot;0&quot;) {
            return acc;
        }</code></pre>
<p>완성된 전체 코드는 다음과 같습니다. </p>
<pre><code class="language-js">function solution(numbers) {

    const sorted = numbers.map(String).sort((a, b) =&gt; {

        return (b + a).localeCompare(a + b)

    })

    return sorted.reduce((acc, curr) =&gt; {
        if(acc === &quot;0&quot;) {
            return acc;
        }
        return `${acc}${curr}`
    })    
}</code></pre>
<p>이렇게 프로그래머스 &quot;<strong>정렬</strong>&quot; 문제인 큰 수 찾기 문제를 해결할 수 있었습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[퀵정렬과 병합정렬에 대해 알아보자 ( JS의 Sort함수는 어떤 알고리즘을 사용했을까?)]]></title>
            <link>https://velog.io/@gahui_dev/%ED%80%B5%EC%A0%95%EB%A0%AC%EA%B3%BC-%EB%B3%91%ED%95%A9%EC%A0%95%EB%A0%AC%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@gahui_dev/%ED%80%B5%EC%A0%95%EB%A0%AC%EA%B3%BC-%EB%B3%91%ED%95%A9%EC%A0%95%EB%A0%AC%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 19 Dec 2024 06:16:42 GMT</pubDate>
            <description><![CDATA[<p>이전에 버블, 삽입, 선택 정렬에 대해 공부했을때 이 세가지 정렬 알고리즘은 작은데이터를 정렬할때 맞는 알고리즘이였습니다. 그렇다면 큰 데이터에 맞는 퀵정렬과 병합정렬에 대한 내용을 공유해보고자 합니다. </p>
<h2 id="퀵정렬">퀵정렬</h2>
<p>퀵정렬은 큰데이터를 효과적으로 정렬할 수 있는 알고리즘 중 하나입니다. 퀵정렬은 다른원소와 비교를 통해서 정렬하는 &quot;비교 정렬&quot;에 속한다고 합니다. 또한 분할 정복 방법을 통해서 리스트를 정렬합니다. 분할 정복 방법을 사용하는것의 이점은 하나의 큰 문제를 작은 문제로 쪼게 병렬적으로 해결할 수 있기 때문에 여러 정렬 알고리즘 중 시간적 효율이 좋은 정렬 방법이라고 할 수 있습니다.</p>
<p>퀵정렬에 대한 설명중 다음과 같은 설명이 있습니다. </p>
<blockquote>
<p>퀵 정렬의 내부 루프는 대부분의 컴퓨터 아키텍처에서 효율적으로 작동하도록 설계되어 있고(그 이유는 메모리 참조가 지역화되어 있기 때문에 CPU 캐시의 히트율이 높아지기 때문이다.), 대부분의 실질적인 데이터를 정렬할 때 제곱 시간이 걸릴 확률이 거의 없도록 알고리즘을 설계하는 것이 가능하다.</p>
</blockquote>
<p>퀵 정렬은 메모리 접근 패턴이 지역적(locality)인 특성을 가지고 있습니다. 이는 데이터가 메모리에서 연속적으로 저장되고 접근되기 때문에, CPU가 데이터를 처리할 때 캐시를 효과적으로 활용할 수 있다는 뜻입니다. CPU는 한 번 로드한 메모리 데이터를 캐시에 저장하므로, 가까운 시점에 다시 사용할 때 빠르게 접근할 수 있습니다.</p>
<p>여기서 <strong>지역적</strong> 이라는 말의 의미는 데이터 접근이 특정 메모리 영역에 집중되는 패턴으로 퀵 정렬이 재귀적으로 배열을 분할하면서 작은 범위에서 작업하기 때문에 발생합니다.</p>
<p>또한, 위 설명을 좀 더 이해하기 위해 퀵정렬의 시간복잡도를 미리 살펴보겠습니다. 퀵 정렬의 최악 시간 복잡도는 O(n2)입니다. 하지만, 이 상황이 발생할 확률은 매우 낮습니다. 이미 정렬된 데이터에서 항상 최악의 피벗을 선택(예: 배열의 첫 번째나 마지막 요소)하면 O(n2) 시간이 걸립니다. </p>
<p>실질적인 구현에서는 &quot;<strong>랜덤 피벗 선택</strong>&quot;, &quot;<strong>중간값 피벗</strong>&quot;, &quot;<strong>Median-of-three</strong>(첫 번째 요소, 중간 요소, 마지막 요소 세 가지 값중 중간값을 선택하는 것)&quot;과 같은 전략을 사용해 이런 상황을 회피합니다. (이런 피벗 선택방법을 선택하면 되기 떄문에 최악의 시간 복잡도가 발생할 확률이 낮습니다.) 결과적으로, 대부분의 경우 퀵 정렬의 <strong>시간 복잡도는 O(nlogn)</strong>에 가깝게 됩니다.</p>
<p>그러면, 이런 퀵정렬은 어떻게 구현할 수 있을까요? 다음과 같은 순서를 따르면 됩니다. </p>
<ol>
<li>종료 조건 : 배열의 길이가 1이하라면, 함수를 종료합니다.</li>
<li>피벗을 선택합니다. </li>
<li>피벗을 기준으로 작은값은 left배열에 높은 값은 right배열에 넣습니다.</li>
<li>left와 right배열을 재귀적으로 정렬합니다. </li>
<li>[left배열, 피벗, right 배열] 배열을 합쳐 반환합니다. </li>
</ol>
<p>위와 같이 작성한 JavaScript코드는 다음과 같습니다. Median-of-three 으로 구현해보았습니다. </p>
<pre><code class="language-js">function quickSort(list) {
  if (list.length &lt;= 1) {
    return list; 
  }

  // Median-of-three 방식으로 피벗 선택
  const first = list[0]; // 첫 번째 요소
  const midIndex = Math.floor(list.length / 2); // 중간 요소의 인덱스
  const middle = list[midIndex]; // 중간 요소
  const last = list[list.length - 1]; // 마지막 요소

  // 첫 번째, 중간, 마지막 요소 중 중간값 선택
  const pivot = [first, middle, last].sort((a, b) =&gt; a - b)[1];

  const pivotIndex = list.indexOf(pivot);

  // 배열 분할 초기화
  const left = [];
  const right = [];

  // 배열 순회하면서 분할
  for (let i = 0; i &lt; list.length; i++) {
    if (i === pivotIndex) continue; // 피벗 요소는 건너뜀
    if (list[i] &lt; pivot) {
      left.push(list[i]);
    } else {
      right.push(list[i]);
    }
  }

  // 재귀적으로 정렬 후 병합
  return [...quickSort(left), pivot, ...quickSort(right)];
}</code></pre>
<p>퀵 정렬의 시간복잡도는 입력 데이터의 <strong>분포</strong>에 따라 달라집니다. 퀵 정렬의 시간복잡도는 다음과 같습니다. </p>
<p><strong>1. 최선의 경우 (Best Case):  O(n log n)</strong>
매번 피벗이 배열을 <strong>완벽하게 균등하게 나눌 때</strong> 발생합니다. 즉, 피벗이 항상 배열의 중앙값에 가까운 경우입니다.</p>
<ol>
<li>배열이 n개의 요소로 시작한다고 가정합니다.</li>
<li>각 단계에서 배열을 절반씩 나누며, 분할 작업에 O(n)이라는 시간복잡도가 소요됩니다.<ul>
<li>분할 작업: 배열 전체를 순회하며 피벗보다 작은 값과 큰 값을 나누는 데 O(n) 시간이 걸립니다.</li>
</ul>
</li>
<li>배열이 절반씩 나뉘므로, 총 분할 단계 수는 log n에 비례합니다.
결과적으로, ( O(n)) 분할 작업이( log n) 단계 동안 반복되므로 *<em>총 시간복잡도는  O(nlog n) *</em>입니다.</li>
</ol>
<p>*<em>2. 최악의 경우 (Worst Case): O(n2) *</em></p>
<ul>
<li>피벗이 매번 배열의 <strong>가장 큰 값</strong> 또는 <strong>가장 작은 값</strong>을 선택할 때 발생합니다.</li>
</ul>
<ol>
<li>피벗 선택이 배열을 크게 나누지 못하고, 한쪽에만 데이터를 몰아넣게 됩니다.</li>
<li>분할 단계에서 첫 번째는 ( n ), 두 번째는 ( n-1 ), 세 번째는 ( n-2 )개 요소를 처리하므로 O(n2)이 됩니다. </li>
</ol>
<p><strong>3. 평균적인 경우 (Average Case): ( O(n \log n) )</strong>
피벗이 배열을 <strong>적절히 균등하게 분할하는 경우</strong>.</p>
<ul>
<li>평균적으로 각 분할 단계에서 배열이 약 절반씩 나뉜다고 가정합니다.</li>
<li>따라서 분할 단계 수는 log n, 각 단계의 분할 작업은 O(n)이므로 평균 시간복잡도는 O(nlog n)이 됩니다.</li>
</ul>
<p>퀵 정렬이 실질적으로 빠른 이유는 다음과 같습니다. </p>
<ul>
<li>시간복잡도 O(nlog n)를 평균적으로 유지하여 효율적입니다. </li>
<li>데이터 접근 패턴이 지역적(locality)을 띠어 캐시 효율이 높습니다.</li>
<li><strong>제자리 정렬</strong>로 추가 메모리가 거의 필요하지 않으므로 공간 효율적입니다.</li>
</ul>
<h2 id="병합-정렬">병합 정렬</h2>
<p><strong>병합정렬</strong> 또한 퀵정렬과 마찬가지로 비교 정렬에 속합니다. 또한, <strong>분할 정복알고리즘</strong>으로 정렬을 구현하는것또한 동일합니다. 퀵정렬과 다른 점은 병합 정렬은 <strong>안정정렬</strong>에 속한다는 것입니다. 보통 <strong>n-way 합병 정렬</strong>이라고 합니다. n-way는 하나의 리스트를 몇개만큼 분할할지에 대한 방법으로 일반적으로 <strong>2-way 병합정렬</strong>을 사용한다고 합니다. </p>
<p>병합 정렬은 다음과 같이 설명할 수 있습니다. </p>
<ol>
<li>리스트의 길이가 1 이하이면 이미 정렬된 것으로 간주합니다. </li>
<li>그렇지 않으면, 비슷한 크기의 두개의 리스트로 나눕니다. </li>
<li>각 부분을 재귀적으로 정렬합니다. </li>
<li>두 부분 리스트를 다시 하나의 정렬된 리스트로 합병합니다. </li>
<li>이 과정을 반복합니다. </li>
</ol>
<p>위 과정을 자바스크립트 코드로 옮긴건 다음과 같습니다. </p>
<pre><code class="language-js">function mergeSort(list) {
  if (list.length &lt;= 1) {
    return list;
  }

  // 반으로 나눈다.
  const mid = Math.floor(list.length / 2);
  const left = mergeSort(list.slice(0, mid));
  const right = mergeSort(list.slice(mid));

  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let i = 0,
    j = 0;

  while (i &lt; left.length &amp;&amp; j &lt; right.length) {
    if (left[i] &lt; right[j]) {
      result.push(left[i]);
      i++;
    } else {
      result.push(right[j]);
      j++;
    }
  }

  return result.concat(left.slice(i)).concat(right.slice(j));
}</code></pre>
<p>병합 정렬(Merge Sort)의 시간복잡도는 입력 데이터의 분포와 관계없이 <strong>항상</strong> O(nlog n)입니다. 병합 정렬의 시간복잡도를 다시한번 단계별로 설명해 보면 다음과 같습니다. </p>
<p>병합 정렬은 <strong>분할 정복(Divide and Conquer)</strong> 알고리즘으로 작동합니다. </p>
<ol>
<li><strong>분할(Divide)</strong> : 배열을 한 요소로 나뉠 때까지 절반씩 나눠 재귀적으로 분할합니다.</li>
<li><strong>정복(Conquer)</strong> : 분할된 배열들을 두 개씩 정렬하며 병합합니다.</li>
<li><strong>병합(Merge)</strong> : 병합 단계에서는 두 정렬된 배열을 하나로 합치며 정렬합니다.</li>
</ol>
<h3 id="시간복잡도">시간복잡도</h3>
<p><strong>(1) 분할 단계 시간복잡도</strong></p>
<ul>
<li>배열을 계속 절반으로 나누므로, 분할 작업의 단계 수는 log n 입니다. </li>
<li>분할 자체는 배열을 나누기만 하기 때문에 O(1) 시간에 이루어집니다.</li>
</ul>
<p><strong>(2) 병합 단계의 시간복잡도</strong></p>
<ul>
<li>각 병합 단계에서 두 배열을 합치는 데  O(n) 시간이 소요됩니다.<ul>
<li>병합 과정에서는 두 배열을 순회하며 원소를 비교하고, 결과 배열에 정렬된 값을 추가합니다.</li>
</ul>
</li>
<li>배열이 log n 단계로 나뉘었으므로, 병합 작업은 각 단계에서 O(n) 시간씩 log n 단계 동안 반복됩니다.</li>
</ul>
<p><strong>(3) 전체 시간복잡도</strong></p>
<ul>
<li>각 병합 단계에서 O(n) 작업이 필요하고, 이런 작업이 log n 단계 동안 반복합니다.</li>
<li>따라서 병합 정렬의 총 시간복잡도는: O(nlog n) 입니다. </li>
</ul>
<p>** 병합 정렬은 왜 항상 O(n \log n)일까? **</p>
<ul>
<li>배열을 항상 절반으로 나누기 때문에, 최악의 경우에도 균등하게 분할됩니다.</li>
<li>또한, 병합 정렬은 이미 정렬된 데이터에서도 동일한 절차를 따르므로 항상 O(nlog n)을 유지합니다.</li>
</ul>
<h3 id="자바스크립트의-sort함수는-어떤-정렬을-사용할까">자바스크립트의 sort함수는 어떤 정렬을 사용할까?</h3>
<p>이전에 자바스크립트의 sort 함수에 대해 간략히 설명한 적이 있습니다. 정렬 알고리즘을 공부하다 보니, 자바스크립트에서 제공하는 내장 함수인 sort 함수가 정확히 어떤 알고리즘을 사용하는지 궁금해졌습니다.</p>
<p>자바스크립트의 sort 함수는 사용 중인 엔진에 따라 구현 방식이 조금씩 다를 수 있습니다. 하지만 대부분의 경우 비슷한 방식으로 구현되어 있습니다. 예를 들어, 우리가 많이 사용하는 <strong>V8 엔진(Chrome과 Node.js)</strong>에서는 sort 함수가 <strong>TimSort 알고리즘</strong>을 사용합니다.</p>
<p>관련해서 모던 자바스크립트 Deep Dive 책에서 Array.prototype.sort에 대해 언급한 내용을 찾아보니 다음과 같은 내용이 있었습니다. 과거에는 자바스크립트의 sort 메서드가 퀵 정렬(Quick Sort) 알고리즘을 사용했는데, 이 알고리즘은 동일한 값의 요소가 중복될 때 초기 순서를 유지하지 못하는 단점이 있었습니다. 이는 불안정한 정렬 알고리즘의 특징이기도 합니다. 이러한 문제를 해결하기 위해 ECMAScript 2019부터는 TimSort 알고리즘이 사용되도록 변경되었습니다.</p>
<hr>
<p><strong>TimSort란?</strong>
TimSort는 <strong>합병 정렬(Merge Sort)</strong>과 <strong>삽입 정렬(Insertion Sort)</strong>을 결합한 정렬 알고리즘입니다. 이 알고리즘은 다음과 같은 장점을 가지고 있습니다:</p>
<ul>
<li>동일한 값의 요소가 있을 경우, 초기 순서를 유지합니다.</li>
<li>데이터가 이미 부분적으로 정렬된 경우(예: 대부분 정렬된 데이터), 매우 빠르게 동작하도록 설계되었습니다.</li>
</ul>
<hr>
<p>결론적으로, 자바스크립트의 sort 함수는 과거에는 퀵 정렬을 기반으로 작동했지만, ECMAScript 2019 이후부터는 안정성과 효율성을 고려해 TimSort 알고리즘으로 전환되었습니다. 이를 통해 동일한 값의 요소 순서를 유지하며, 대부분의 실제 데이터에서 더 빠르게 정렬을 수행할 수 있습니다.</p>
<h3 id="결론">결론</h3>
<p>실제로 아무 생각 없이 sort 함수를 사용해 왔지만, 이번에 <strong>합병 정렬(Merge Sort)</strong>과 <strong>퀵 정렬(Quick Sort)</strong>을 공부하면서 자바스크립트의 sort 함수가 어떤 정렬 알고리즘을 사용하는지 이해하게 되었습니다.</p>
<p>프론트엔드 개발을 하다 보면 종종 클라이언트 쪽에서 정렬 작업을 처리해야 할 때가 있는데, 특히 대규모 데이터를 정렬해야 할 때 성능을 고려한 최적화가 중요하다는 걸 다시금 느꼈습니다. 이번 공부를 통해, 만약 이 내용을 미리 알았다면 당시 개발에서 더 나은 성능 최적화를 고민하며 구현할 수 있지 않았을까 하는 아쉬움도 들었습니다.</p>
<p>정렬 알고리즘에 대해 이해하고 sort 함수의 작동 방식을 알게 된 만큼, 앞으로는 성능과 효율성을 더 신경 쓰며 정렬 작업을 처리할 수 있을 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[정렬 - 버블, 삽입, 선택 정렬 차이점에 대해서 알아보자 ]]></title>
            <link>https://velog.io/@gahui_dev/%EC%A0%95%EB%A0%AC-%EB%B2%84%EB%B8%94-%EC%82%BD%EC%9E%85-%EC%84%A0%ED%83%9D</link>
            <guid>https://velog.io/@gahui_dev/%EC%A0%95%EB%A0%AC-%EB%B2%84%EB%B8%94-%EC%82%BD%EC%9E%85-%EC%84%A0%ED%83%9D</guid>
            <pubDate>Mon, 09 Dec 2024 06:18:06 GMT</pubDate>
            <description><![CDATA[<p>정렬 알고리즘 중 버블, 선택, 삽입 정렬에 대해서 정리한 내용을 공유하려고 합니다.
이 세정렬의 공통점은 다음과 같습니다. </p>
<ul>
<li>모두 같은 최악, 최선의 시간복잡도를 갖는다. </li>
<li>단순 정렬이다. (구현이 간단하다)</li>
<li>작은 데이터의 정렬에 적합하다.</li>
<li>제자리 정렬 알고리즘이다. (추가적인 배열이나 리스트를 사용하지 않고, 주어진 배열 내에서 요소를 교환하여 정렬합니다.)</li>
</ul>
<p>이러한 세 정렬에 대해서 각각 알아보고자 합니다. </p>
<h2 id="버블-정렬">버블 정렬</h2>
<p>버블 정렬은 이름처럼 인접한 두 요소를 비교하며, 값이 &quot;버블&quot;처럼 배열의 끝으로 올라가는 방식으로 동작합니다.
간단한 정렬 알고리즘 중 하나로, 구현하기는 쉽지만 시간 복잡도가 좋지 않아 실제 환경에서는 거의 사용되지 않는다고 합니다. </p>
<h3 id="버블-정렬의-기본-개념">버블 정렬의 기본 개념</h3>
<p>버블 정렬의 핵심 로직은 다음과 같습니다. (엄청 간단합니다):</p>
<ol>
<li>배열에서 인접한 두 요소를 비교합니다.</li>
<li>각 반복이 끝날 때마다 가장 큰(혹은 작은) 값이 배열의 끝에 정렬됩니다.</li>
<li>이후에는 정렬이 완료된 마지막 요소를 제외하고 다시 비교를 반복합니다.</li>
<li>이를 배열의 모든 요소가 정렬될 때까지 반복합니다.</li>
</ol>
<h3 id="버블-정렬의-기본-구현-javascript">버블 정렬의 기본 구현 (JavaScript)</h3>
<p>다음은 버블 정렬 알고리즘을 자바스크립트로 구현한 코드입니다.</p>
<pre><code>function bubbleSort(list) {
  for (let i = 0; i &lt; list.length - 1; i++) {
    for (let j = 0; j &lt; list.length - i - 1; j++) {
      if (list[j] &gt; list[j + 1]) {
        let temp = list[j];
        list[j] = list[j + 1];
        list[j + 1] = temp;
      }
    }
  }
  return list;
}</code></pre><p>간단히 구현할 수 있지만, 이 방식에는 비효율적인 점이 있습니다.</p>
<p>위 코드의 최악 및 평균 시간 복잡도는 O(n²)입니다. 이유는 모든 요소를 반복적으로 비교해야 하기 때문입니다. 다만, 개선할 여지가 있는데요. 만약 배열이 이미 정렬된 상태라면 불필요한 연산을 줄일 수 있습니다.</p>
<p>정렬이 이미 끝난 상태라면 반복문을 더 이상 실행할 필요가 없습니다. 이를 위해 <strong>flag(플래그)</strong>를 사용하여 최적화할 수 있습니다. 수정한 버블 정렬 코드는 다음과 같습니다:</p>
<pre><code>function bubbleSort(list) {
  let isChanged = false;

  for (let i = list.length - 1; i &gt;= 0; i--) {
    console.log(&quot;hi&quot;);
    for (let j = 0; j &lt; i - 1; j++) {
      let swap = list[j];
      let next = list[j + 1];

      if (swap &gt; next) {
        list[j] = next;
        list[j + 1] = swap;
        isChanged = true;
      }
    }
    if (!isChanged) {
      break;
    }
    isChanged = false;
  }
  return list</code></pre><h4 id="시간-복잡도">시간 복잡도</h4>
<p>개선된 시간 복잡도는 다음과 같습니다.</p>
<p><strong>최악의 경우</strong> (배열이 완전히 정렬되지 않은 경우) : 𝑂(𝑛2)<br><strong>최선의 경우</strong> (배열이 이미 정렬된 경우) : 𝑂(𝑛)
<strong>평균의 경우</strong> (배열이 랜덤하게 주어진 경우) : 𝑂(𝑛2)</p>
<p>결론적으로, 버블정렬의 경우 요소의 개수가 많아질수록 연산량이 급격히 증가하기 때문에, 실무에서 다루는 대규모 데이터에는 적합하지 않습니다. </p>
<h2 id="삽입정렬">삽입정렬</h2>
<p>삽입 정렬은 요소를 올바른 위치에 삽입하여 정렬하는 자료구조입니다. 쉬운 예로 손에 든 카드를 정렬하는 방법이 있습니다. 첫번째 카드는 건너뛰고 두번째 카드부터 앞 카드와 비교하면서 정렬해 가는 방법입니다. </p>
<h3 id="삽입-정렬의-기본-개념">삽입 정렬의 기본 개념</h3>
<p>삽입 정렬의 로직은 다음과 같습니다. </p>
<ol>
<li>배열의 두 번째 요소부터 시작합니다. (첫 번째 요소는 이미 정렬된 상태로 간주)</li>
<li>현재 요소가 n번째 라고 한다면, 0부터 n-1까지 돌아가며 현재요소가 삽입할 적절한 위치를 찾습니다. </li>
<li>모든 배열을 순회할때까지 반복합니다. </li>
</ol>
<h3 id="삽입-정렬의-기본-구현-javascript">삽입 정렬의 기본 구현 (JavaScript)</h3>
<p>아래는 위 로직을 코드로 구현한것입니다. </p>
<pre><code>function insertionSort(list) {
  for (let i = 1; i &lt; list.length; i++) {
    const current = list[i];
    let compareIndex = i - 1;

    while (compareIndex &gt;= 0 &amp;&amp; list[compareIndex] &gt; current) {
      list[compareIndex + 1] = list[compareIndex];
      compareIndex--;
    }

    list[compareIndex + 1] = current;
  }

  return list;
}</code></pre><p>삽입정렬또한, 버블정렬과 같은 시간복잡도를 갖습니다. </p>
<p><strong>최악의 경우</strong> (배열이 완전히 정렬되지 않은 경우) : 𝑂(𝑛2)
<strong>최선의 경우</strong> (배열이 이미 정렬된 경우) : 𝑂(𝑛)
<strong>평균의 경우</strong> (배열이 랜덤하게 주어진 경우) : 𝑂(𝑛2)</p>
<p>정렬시, 요소를 하나씩 뒤로 밀어야 하기때문에 큰 데이터에는 맞지 않은 정렬 방법입니다. </p>
<h3 id="선택-정렬">선택 정렬</h3>
<p>선택 정렬은 정렬되지 않은 요소들중 가장 작은 값 선택해서 정렬하는 방법입니다. 앞서 설명했던 버블 정렬과 삽입정렬처럼 간단하게 구현할 수 있습니다. </p>
<pre><code>function selectionSort(list) {
  for (let i = 0; i &lt; list.length - 1; i++) {
    let minIndex = i;

    for (let j = i + 1; j &lt; list.length; j++) {
      if (list[j] &lt; list[minIndex]) {
        minIndex = j; 
      }
    }

    if (minIndex !== i) {
      const temp = list[i];
      list[i] = list[minIndex];
      list[minIndex] = temp;
    }
  }

  return list;
}</code></pre><p><strong>최악의 경우</strong> (배열이 완전히 정렬되지 않은 경우) : 𝑂(𝑛2)
<strong>최선의 경우</strong> (배열이 이미 정렬된 경우) : 𝑂(𝑛2)
<strong>평균의 경우</strong> (배열이 랜덤하게 주어진 경우) : 𝑂(𝑛2)</p>
<p>선택정렬은 버블 정렬보다 더 나은 복잡도를 갖습니다. 버블 정렬은 불필요하게 많은 스왑 연산을 수행할 수 있고 스왑 연산은 메모리 이동이 발생하기 때문에 비교 연산보다 상대적으로 느릴 수 있습니다. 반면, 선택 정렬은 최소한의 스왑만 수행하기 때문에 메모리 이동이 적어 더 효율적입니다.</p>
<p>시간복잡도는 비교 횟수를 기반으로 계산합니다. 버블 정렬과 선택 정렬 모두 O(n2)번의 비교를 수행하므로, 이론적인 시간복잡도는 동일하게 𝑂(𝑛2) 입니다.</p>
<p>하지만 실제 성능에서는 스왑 횟수가 적은 선택 정렬이 버블 정렬보다 더 빠르게 동작합니다. 이러한 이유는 선택 정렬이 불필요한 메모리 이동을 줄이기 때문입니다.</p>
<p>세 가지 기본적인 정렬 알고리즘(버블 정렬, 삽입 정렬, 선택 정렬)의 성능을 세 가지 다른 상황에서 테스트했습니다.</p>
<ul>
<li>랜덤하게 섞인 배열 : [4, 6, 2, 1, 0, 5, 3, 6, 5]</li>
<li>이미 정렬된 배열 : [1, 2, 3, 4, 5, 6, 7, 8, 9]</li>
<li>완전히 역순으로 정렬된 배열 : [9, 8, 7, 6, 5, 4, 3, 2, 1]</li>
</ul>
<p>각 케이스별로 3회씩 실행하여 실행 시간을 측정했습니다.</p>
<h4 id="1-랜덤-배열의-경우">1. 랜덤 배열의 경우</h4>
<pre><code class="language-js">1회차: 버블(0.374ms) / 삽입(0.196ms) / 선택(0.5ms)
2회차: 버블(0.59ms) / 삽입(0.282ms) / 선택(0.309ms)
3회차: 버블(0.943ms) / 삽입(0.528ms) / 선택(0.599ms)</code></pre>
<p>삽입 정렬이 일관되게 가장 좋은 성능을 보여주었습니다.</p>
<h4 id="2-정렬된-배열의-경우">2. 정렬된 배열의 경우</h4>
<pre><code class="language-js">1회차: 버블(0.365ms) / 삽입(0.39ms) / 선택(0.36ms)
2회차: 버블(0.42ms) / 삽입(0.413ms) / 선택(0.42ms)
3회차: 버블(0.256ms) / 삽입(0.188ms) / 선택(0.172ms)</code></pre>
<p>세 알고리즘 모두 매우 비슷한 성능을 보여주었습니다.</p>
<h4 id="3-역순으로-정렬된-배열의-경우">3. 역순으로 정렬된 배열의 경우</h4>
<pre><code class="language-js">1회차: 버블(0.205ms) / 삽입(0.171ms) / 선택(0.173ms)
2회차: 버블(0.42ms) / 삽입(0.413ms) / 선택(0.381ms)
3회차: 버블(0.19ms) / 삽입(0.188ms) / 선택(0.181ms)</code></pre>
<p>역순 배열을 실행할때, 조금 의외였던 점이 이론적으로는 최악의 케이스여야 하지만, 실제로는 좋은 성능을 보여주었습니다.
세 알고리즘 모두 비슷한 성능을 보여주었고, 어떤 때에는 랜덤한 배열보다 수행시간이 짧았습니다. </p>
<h3 id="결론">결론</h3>
<p>정렬 알고리즘의 실제 성능이 이론적 예측과 다를 수 있다는 것을 알게되었습니다. 특히 삽입 정렬이 버블정렬과 선택정렬보다 랜덤 데이터에서 더 나은 성능을 갖는다는걸 알게되었습니다. 물론 데이터가 그렇게 많지 않았고, 제 컴퓨터에서만 돌려본 거라 실제로 엄청 큰 데이터를 다뤄야 하는 상황이라면 결과가 많이 달라질 수 있을것같습니다. 다만, 이 세 정렬을 공부하면서 느끼게 된 것은, &#39;이게 제일 좋은 방법이야!&#39;라고 생각는게 아닌 실제로 어떤 데이터를 다루는지, 어떤 환경에서 돌리는지에 따라서 더 잘 맞는 다른 방법이 있을 수 있기 때문에 상황에 맞는 알맞은 해결 방법을 찾는 문제 해결 능력을 길러야 한다는 생각이 많이 들었습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] Queue "다리위를 건너는 트럭" 풀이 과정]]></title>
            <link>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Stack-%EB%8B%A4%EB%A6%AC%EC%9C%84%EB%A5%BC-%EA%B1%B4%EB%84%88%EB%8A%94-%ED%8A%B8%EB%9F%AD-%ED%92%80%EC%9D%B4-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Stack-%EB%8B%A4%EB%A6%AC%EC%9C%84%EB%A5%BC-%EA%B1%B4%EB%84%88%EB%8A%94-%ED%8A%B8%EB%9F%AD-%ED%92%80%EC%9D%B4-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Mon, 25 Nov 2024 13:41:36 GMT</pubDate>
            <description><![CDATA[<p>프로그래머스 자료구조 큐 문제인 다리위를 건너는 트럭이라는 문제 풀이 과정을 공유해 보려합니다.</p>
<p>레벨 2 <a href="https://school.programmers.co.kr/learn/courses/30/lessons/42583">문제 링크 </a></p>
<h3 id="문제-해설">문제 해설</h3>
<h4 id="주어진-값">주어진 값</h4>
<ul>
<li><strong>bridge_length</strong> : 다리에 올라갈 수 있는 트럭 (숫자형)</li>
<li><strong>weight</strong> : 다리가 견딜 수 있는 무게 (숫자형)</li>
<li><strong>truck_weights</strong> : 트럭별 무게 (배열)</li>
</ul>
<h4 id="구해야하는-값">구해야하는 값</h4>
<ul>
<li>모든 트럭이 다리를 건널때 까지 걸리는 시간(초단위)</li>
</ul>
<h4 id="문제-이해를위해-추가로-알아야할-점">문제 이해를위해 추가로 알아야할 점</h4>
<ul>
<li>다리에 오르지 않은 트럭은 무시한다.</li>
<li>트럭은 주어진 배열(truck_weights) 순서대로 건너야 한다. 즉, 0번째 트럭이 다리를 건너기 전까지, 다음 트럭은 다리를 건널 수 없다. </li>
</ul>
<p>다음으로, 문제에서 설명이 부족해서 이해하기 어려웠던점은 다음과 같습니다. 주어진 값 bridge_length라는 이름을 보고 이해해야 합니다. </p>
<ul>
<li>트럭 한대가 다리를 지나가는데 걸리는 시간은 bridgh_length이다.</li>
<li>즉, 트럭은 1초당 1 length만큼 이동할 수 있다. </li>
</ul>
<p>문제에 주어진 예시가 처음엔 잘 이해가 안갔는데, 위에 설명이 부족했던 탓이였어요. <del>(이부분은 프로그래머스측에서 문제 보완을 해주셨으면 좋겠네요.)</del></p>
<h4 id="예시-설명">예시 설명</h4>
<p>문제에 설명된 예시를 조금 더 쉽게 이해해 봅시다. </p>
<p>주어진 조건이 다음과 같다고 가정합시다.
bridge_weight: 2
weight : 10
truck_weights : [7,4,5,6]</p>
<p>4개의 트럭은 다리위에 최대 2대까지 올라갈 수 있고, 총 무게가 10kg를 넘으면 안되는 다리를 건너야 합니다.</p>
<p><strong>0초일때</strong>
모든 트럭은 다리를 건널 준비를 합니다. </p>
<p><strong>1초일때</strong></p>
<ul>
<li>다리를 건너고 있는 트럭이 없습니다.</li>
<li>첫번째 트럭은 총 무게 10을 넘지 않습니다.</li>
<li>다리를 건넙니다. </li>
</ul>
<p><strong>2초일때</strong></p>
<ul>
<li>첫번째 트럭이 다리를 건넌지 1초가 지났습니다. </li>
<li>다리를 건널 수 있는 트럭의 수는 총 2대이므로 한대가 더 건널 수 있는지 확인합니다. </li>
<li>다음 건너야 하는 트럭(4)이 다리를 건널 경우 총 무게 10이 넘기 때문에 대기합니다.</li>
</ul>
<p><strong>3초일때</strong> </p>
<ul>
<li>첫번째 트럭은 다리를 건넌지 2초가 되어 다리를 빠져 나갑니다.</li>
<li>다리를 건너고 있는 트럭은 0이기때문에 다음 트럭(4)가 다리에 진입합니다. </li>
</ul>
<p><strong>4초일때</strong></p>
<ul>
<li>두번째 트럭(4)가 다리를 건넌지 1초가 경과했습니다.</li>
<li>세번째 트럭(5)가 건널수 있는지 판별합니다.</li>
<li>현재 한대 더 다리에 오를 수 있고 다리에 오를 수 있는 남은 무게는 6이기 때문에 세번째 트럭이 다리에 오릅니다. </li>
</ul>
<p><strong>5초일때</strong></p>
<ul>
<li>두번째 트럭(4)가 다리를 건넌지 2초가 되어 다리를 빠져나갑니다.</li>
<li>세번째 트럭(5)는 다리를 건넌지 1초가 경과했습니다.</li>
<li>네번째 트럭(6)은 다리를 건널수 있는지 판별합니다.</li>
<li>1대 더 오를 수 있지만 남은 무게가 5이기때문에 무게가 6인 트럭은 대기합니다.</li>
</ul>
<p><strong>6초일때</strong></p>
<ul>
<li>세번째 트럭(5)는 다리를 건넌지 2초가 되어 다리를 빠져나갑니다.</li>
<li>네번째 트럭(6)이 다리를 건널수 있는지 판별합니다.</li>
<li>다리를 건너고 있는 트럭이 없기때문에 네번째 트럭은 다리에 진입합니다. </li>
</ul>
<p><strong>7초일때</strong></p>
<ul>
<li>네번째 트럭이 다리를 건넌지 1초가 지났습니다.</li>
</ul>
<p><strong>8초일때</strong></p>
<ul>
<li>네번째 트럭이 다리를 건넌지 2초가 되어 다리를 빠져나갑니다.</li>
</ul>
<p>즉, 이 트럭들이 다리를 지나가는데 걸리는 최소 시간은 <strong>8초가</strong> 됩니다. </p>
<p>위 문제 해설에서 아래와 같은 내용들이 반복되고 있습니다. </p>
<ul>
<li>1초가 지날때 마다,</li>
<li>현재 다리위에 있는 트럭이 몇초가 지났는지 확인합니다.</li>
<li>2초(bridge_length)가 지나면 다리를 다 건넌걸로 간주 합니다.</li>
<li>대기중인 트럭을 다리위에 진입시키기 위해 아래 두가지 사항을 확인합니다.</li>
<li>현재 다리에 2대(bridge_length)가 차있다면, 차는 다음 기회를 기다려야 합니다. </li>
<li>2대가 다 차있지 않더라도, 지나가야하는 트럭의 무게가 다리에 오를수 있는 여유가 되는지 확인합니다. </li>
</ul>
<h3 id="문제-해결">문제 해결</h3>
<p>트럭이 다리에 오르고 다리에 오른 순서대로 빠져나가야 하기 때문에, 이 문제는 큐(Queue)라는 자료구조를 이용합니다. 
그렇다면, enQueue(다리에 진입시키고), deQueue(다리에서 빠져나가는) 조건은 어떻게 될까요? </p>
<h4 id="enqueue조건">enQueue조건</h4>
<ul>
<li>대기중인 트럭의 무게 &lt;= 최대 오를수 있는 트럭의 무게 - 현재 다리위에 있는 모든 트럭의 무게 합 (작거나 같음)</li>
<li>남은 수용 무게를 매번 계산하지 않는 방법 : enQueue할때 마다, 현재 다리위에 있는 트럭의 무게를 누적해서 더한다.</li>
</ul>
<h4 id="dequeue-조건">deQueue 조건</h4>
<ul>
<li>다리위에 있는 트럭중 맨 첫번째 트럭이 진입한 시간을 enQueue시 기록해 두고</li>
<li>현재 시간 - 트럭이 진입한 시간이 bridge_length 이상인지 확인한다.</li>
<li>bridge_length이상이면, deQueue한다.</li>
<li>enQueue시 누적해 두었던 트럭의 무게에서 빠져나간 트럭의 무게를 뺀다. </li>
</ul>
<p>위 조건을 고려하여 작성된 코드는 다음과 같습니다. </p>
<pre><code>function solution(bridge_length, weight, truck_weights) {

    let time = 0; // 몇초가 지났는지 기록한다.
    let passedCount = 0; // 현재 다리를 건넌 트럭의 갯수

    const waitingTrucks = [...truck_weights]; // 대기중인 트럭 배열
    const passingQueue = []; // 다리를 건너고 있는 트럭 배열

    let currentWeight = 0; // 다리위에 있는 트럭의 무게 합

    while(passedCount &lt; truck_weights.length) {
        // deQueue 조건
        if(passingQueue.length &gt; 0 &amp;&amp; time - passingQueue[0] === bridge_length) {
            passingQueue.shift()
            currentWeight -= truck_weights[passedCount]
            passedCount += 1;
        }

        // enQueue 조건
        const restWeight = weight - currentWeight;

        if(restWeight &gt;= waitingTrucks[0]) {
            const truck = waitingTrucks.shift();
            currentWeight += truck;
            passingQueue.push(time)
        }

        time++;
    }

    return time;
}</code></pre><h3 id="결론">결론</h3>
<p>결론적으로, 이 문제는 다리를 건너는 트럭들의 상태를 관리하기 위해 큐(Queue) 자료구조를 활용하는 것이 중요하다고 생각합니다. 
enQueue 조건은 대기 중인 트럭의 무게가 다리의 최대 수용 무게에서 현재 다리 위에 있는 트럭들의 무게 합을 뺀 값보다 작거나 같을 때입니다. 이를 계산하기 위해 enQueue할 때마다 현재 다리 위의 트럭 무게를 누적하여 관리합니다.
deQueue 조건은 다리 위에 있는 트럭 중 맨 앞의 트럭이 다리에 진입한 시간과 현재 시간의 차이가 다리의 길이(bridge_length) 이상일 때입니다. 이를 위해 enQueue 시점에 트럭이 진입한 시간을 기록해두고, 현재 시간과 비교하여 deQueue 여부를 판단하면 쉽게 풀수 있습니다. </p>
<p>코드의 시간 복잡도는 O(n)입니다. 여기서 n은 트럭의 개수를 의미합니다. 코드에서 while 루프는 모든 트럭이 다리를 건너기 전까지 반복되므로, 트럭의 개수에 비례하여 루프가 실행됩니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 개발자가 알아야할 덱이란?]]></title>
            <link>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC%ED%95%A0-%EB%8D%B1%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC%ED%95%A0-%EB%8D%B1%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Tue, 05 Nov 2024 05:51:50 GMT</pubDate>
            <description><![CDATA[<p>오늘은 덱에 대해서 이야기 해보려 합니다. 스택과 큐는 각각 선입 후출(Last In First Out) 그리고 선입 선출 (First In First Out)이라는 특징을 가집니다. 이 두 자료구조의 특징은 삽입과 삭제 하는 곳이 한곳으로 정해져 있어 빠른 시간 복잡도를 가진다는 장점이 있습니다. 다만, 이러한 장점에 따라 다음과 같은 단점 또한 갖고 있습니다. </p>
<ul>
<li>스택 : 한 쪽 끝에서만 삽입과 삭제가 가능함으로 데이터가 쌓이는 방향에 따라 작업이 제한됩니다.</li>
<li>큐 : 앞쪽에서만 삭제, 뒤쪽에서만 삽입이 가능함으로 데이터를 양방향으로 삽입과 삭제가 필요한 상황에 대응하기 어렵습니다. </li>
</ul>
<p>위 두 자료구조의 한계를 극복하기 위한 자료구조로 <strong>덱</strong>이 등장하였습니다.</p>
<h2 id="덱double-ended-queue이란">덱(Double-Ended Queue)이란?</h2>
<p>덱은 서두에 설명했다시피, 스택과 큐의 한계를 극복합니다. 즉, <strong>양방향으로 삽입과 삭제</strong>가 가능합니다.  양방향으로 데이터를 넣고 뺄수 있는것의 장점은 데이터의 유연한 처리가 가능하다는 것인데요. 이런 특성덕분에 덱 자체를 스택으로 혹은 큐로도 사용할 수 있습니다. 실제로 프론트엔드 개발에서 &quot;Infinite Scroll&quot; 혹은 &quot;가상 스크롤&quot; 같은 기능이 구현체가 완전히 동일하지는 않지만, 이럭 덱의 자료구조를 이용한것이라고 생각할 수 있습니다. </p>
<p>큐와 마찬가지로 덱은 <strong>배열</strong>과 <strong>링크드 리스트</strong>로 구현할 수 있습니다. 우선 디큐를 구현하기 위해 다음과 같은 요소가 필요 합니다.</p>
<ul>
<li>addFront : 앞에서 요소를 추가합니다.</li>
<li>addRear : 뒤에서 요소를 추가합니다.</li>
<li>removeFront: 앞의 요소를 삭제합니다.</li>
<li>removeRear : 뒤의 요소를 삭제합니다.</li>
</ul>
<h2 id="array를-이용한-덱-구현">Array를 이용한 덱 구현</h2>
<p>코드를 작성하기 전 Array를 이용해서 큐나 스택을 구현했을 때의 문제점은 요소를 제거하거나 삭제할때 모든 요소의 위치를 하나씩 이동시켜야 한다는 한계가 있었습니다. 그렇다면, 어떻게 이런 한계를 극복할 수 있을까요? 
바로 <strong>&quot;원형 큐&quot;</strong>를 이용하는 방식입니다. 원형 큐를 사용하면, 요소를 삭제 했을때의 메모리 낭비나 요소을 전부 이동시키는 문제를 해결 할 수 있습니다. 위에서 설명한 덱에서 구현해줘야할 요소들의 로직은 다음과 같아야 합니다. 
큐의 앞을 가리키는 front와 맨 뒤 요소를 가리키는 rear를 설정합니다. </p>
<ul>
<li>addFront: 요소가 가득찾는지 확인 후, 요소가 가득차 있지 않다면 이전 index에 새 요소를 추가합니다.</li>
<li>addRear : 요소가 가득 차있는지 확인 후, 요소가 가득차 있지 않다면 다음 index에 새 요소를 추가합니다.</li>
<li>removeFront: 요소가 비어있는지 확인 후, 요소가 비어있지 않다면, front를 다음 인덱스로 이동시킵니다.</li>
<li>removeRear : 요소가 비어있는지 확인 후, 요소가 비어있지 않다면, rear를 이전 인덱스로 이동시킵니다. </li>
</ul>
<p>다음은 이를 이용해서 구현한 자바스크립트 코드 입니다. </p>
<pre><code class="language-js">class Deque {
  constructor(maxSize) {
    this.capacity = maxSize;
    this.size = 0;
    this.front = 0;
    this.rear = 0;
    this.queue = Array(maxSize);
  }

  isFull() {
    return this.size === this.capacity;
  }

  isEmpty() {
    return this.size === 0;
  }

  addFront(value) {
    if (this.isFull()) {
      return false;
    }
    this.front = (this.front - 1 + this.capacity) % this.capacity;
    this.queue[this.front] = value;
    this.size += 1;
    return true;
  }

  addRear(value) {
    if (this.isFull()) {
      throw Error(&quot;Queue is full&quot;);
    }
    this.queue[this.rear] = value;
    this.rear = (this.rear + 1) % this.capacity;
    this.size += 1;
  }

  removeFront() {
    if (this.isEmpty()) {
      throw Error(&quot;Queue is empty&quot;);
    }
    // 먼저 현재 front 위치의 값을 null로 설정
    this.queue[this.front] = null;
    this.front = (this.front + 1) % this.capacity;
    this.size -= 1;
  }

  removeRear() {
    if (this.isEmpty()) {
      throw Error(&quot;Queue is empty&quot;);
    }
    // 먼저 현재 rear 위치의 값을 null로 설정
    this.queue[this.rear] = null;
    this.rear = (this.rear - 1 + this.capacity) % this.capacity;
    this.size -= 1;
  }
}</code></pre>
<p>원형 큐에서 이전 인덱스로 이동 할때와 다음 인덱스로 이동할때 아래와 같은 공식을 사용하면, front와 rear가 맨 앞 혹은 맨 뒤에 있을때에도 다음 인덱스 0 혹은 맨 마지막 Index로 이동할 수 있습니다. </p>
<pre><code class="language-js">(this.front - 1 + this.capacity) % this.capacity</code></pre>
<pre><code class="language-js">(this.rear + 1 ) % this.capacity</code></pre>
<p>배열을 이용한 덱의 구현은 위의 공식과 같은 간단한 로직으로 쉽게 구현할 수 있다는 장점이 있습니다. 그렇기 때문에 이에따른 시간 복잡도도 O(1)로 매우 빠릅니다. </p>
<ul>
<li>삽입 : rear와 front위치에서 삽입해주면 되기 때문에 O(1)</li>
<li>삭제 : rear와 front 위치에서 삭제해주면 되기 때문에 O(1)</li>
<li>요소 찾기 : 만약 특정 index를 알고 있다면 O(1)이지만 index를 모르고 요소를 찾아야 할 경우 모든 요소를 확인 해야 하기 때문에 O(n)이 됩니다. </li>
</ul>
<h2 id="linkedlist를-이용한-덱-구현">LinkedList를 이용한 덱 구현</h2>
<p>일반적으로, 덱의 경우 배열보다는 LinkedList를 이용해서 구현을 합니다. 특히, 덱의 경우 양방향으로 삽입과 삭제가 가능하다는 특징이 있기 때문에 단방향 LinkedList가 아닌, 양방향 LinkedList로 구현을 합니다. 양방향 LinkedList랑 하나의 노드가 이전 값과 다음 값을 알고 있는 것을 양방향LinkedList라고 합니다. </p>
<blockquote>
<p>LinkedList으로 구현할 경우 배열과 달리, 원형큐가 아닌 선형큐방식으로 구현 합니다. 굳이 복잡성을 증가할 필요가 없기 때문입니다. </p>
</blockquote>
<p>다음은 양방향 LinkedList로 덱을 구현한 JavaScript 코드 입니다. </p>
<pre><code class="language-js">class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

class DequeLinkedList {
  constructor() {
    this.front = null;
    this.rear = null;
    this.size = 0;
  }

  isEmpty() {
    return this.size === 0;
  }
  hasOnlyOneNode() {
    return this.size === 1;
  }

  addFront(value) {
    const newNode = new Node(value);
    if (this.isEmpty()) {
      this.front = newNode;
      this.rear = newNode;
    } else {
      newNode.next = this.front;
      this.front.prev = newNode;
      this.front = newNode;
    }
    this.size += 1;
  }
  addRear(value) {
    const newNode = new Node(value);
    if (this.isEmpty()) {
      this.front = newNode;
      this.rear = newNode;
    } else {
      newNode.prev = this.rear;
      this.rear.next = newNode;
      this.rear = newNode;
    }
    this.size += 1;
  }

  removeFront() {
    if (this.isEmpty()) {
      throw Error(&quot;Queue is empty&quot;);
    }
    if (this.hasOnlyOneNode()) {
      this.front = null;
      this.rear = null;
    } else {
      this.front = this.front.next;
      this.front.prev = null;
    }

    this.size -= 1;
  }
  removeRear() {
    if (this.isEmpty()) {
      throw Error(&quot;Queue is empty&quot;);
    }
    if (this.hasOnlyOneNode()) {
      this.front = null;
      this.rear = null;
    } else {
      this.rear = this.rear.prev;
      this.rear.next = null;
    }
    this.size -= 1;
  }
}</code></pre>
<ul>
<li>Node는 현재 자신의 값, 이전 값, 다음 값을 갖고 있습니다.</li>
<li>addFront :  큐가 비어있는 경우 front와 rear모두 새 노드로 설정합니다. 비어 있지 않은 경우 기존 front값의 앞에 놓아야 하기 때문에 front.next = newNode를 통해 새로운 값과 이전 값을 연결 시켜 주고 front를 새 노드 값으로 갱신 합니다. </li>
<li>addRear : 큐가 비어있는 경우 addFront와 동일합니다. 비어있지 않은 경우 마지막 요소 뒤에 붙이는 것이기 때문에 rear.next에 새로운 노드를 연결 시켜준 후 rear을 새 노드 값으로 갱신 시킵니다. </li>
<li>reamoveFront: 만약 노드가 하나일 경우 front 와 rear모두 초기값 null로 갱신 시킵니다. 요소가 두개 이상일 경우 맨 앞 요소를 제거 하는 것이기 때문에 front를 다음 노드(front.next)로 갱신시킨후 이전에 연결 시켰던 front.prev를 초기화 시킵니다.</li>
<li>removeRear: 노드가 하나일 경우 removeFront와 동일 하나. 두개 이상일 경우, 마지막 요소를 제거 하는 것이기 때문에 rear에 rear.prev로 설정한 후 연결 시켰던 rear.next를 null로 초기화 합니다. </li>
</ul>
<p>이중 연결 리스트(이중 링크드 리스트)로 덱을 구현하면, 배열과 달리 미리 크기를 정할 필요가 없으므로 메모리를 동적으로 늘리거나 줄일 수 있습니다. 이는 연결 리스트로 구현된 자료구조의 공통적인 장점입니다. 또한, 배열에서 인덱스를 통해 중간 요소에 바로 접근할 수 있는 비정상적인 접근을, 연결 리스트를 사용함으로써 방지할 수 있습니다.</p>
<p>LinkedList를 이용한 시간 복잡도 또한 배열로 구현한 것과 동일합니다.</p>
<ul>
<li>요소 삭제 : 덱의 앞쪽(front)과 뒤쪽(rear)에 새로운 요소를 추가할 때, 연결 리스트는 새로운 노드를 생성하고 포인터만 조정하면 되므로 O(1) 시간에 완료됩니다.</li>
<li>요소 추가 : 덱의 앞쪽 또는 뒤쪽에서 요소를 제거할 때도 포인터만 조정하면 되므로 O(1) 시간에 수행됩니다. 연결 리스트는 노드의 연결만 변경하면 되기 때문에 데이터 이동이 필요 없습니다.</li>
<li>요소 찾기 : 다만 특정 요소를 찾는경우 걸리는 시간은 리스트의 길이에 비례하게 되어 <strong>O(n)</strong>의 시간 복잡도를 가집니다.</li>
</ul>
<h2 id="결론">결론</h2>
<p>사실 프론트엔드 개발에서는 양방향 LinkedList(이중 연결 리스트)를 자주 사용하지 않습니다. 프론트엔드 개발은 주로 DOM 조작, 서버와의 비동기 처리 등 UI 동작과 관련된 작업이 주를 이루기 때문에, 코드의 복잡성만 증가시키는 LinkedList를 직접 구현하는 것은 적절하지 않다고 생각됩니다. 또한, 프론트엔드에서는 큰 데이터를 다루는 일이 드물고, 가능하면 큰 데이터 처리를 피하는 것이 좋다고 봅니다. 그렇기에 프론트엔드 개발자로서 이 자료구조를 실제로 활용할 일이 있을지 의문이 들기도 합니다.</p>
<p>그럼에도 불구하고, 성능 최적화 기법을 적용하거나 다른 라이브러리의 내부 코드를 분석해야 할 때 기본적인 자료구조에 대한 이해가 있으면, 이를 활용해야 하는 상황에서는 큰 도움이 되지 않을까 생각합니다. 자료구조를 이해하고 있으면 더 나은 해결책을 찾고, 효율적으로 문제에 대응할 수 있을것 같습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript를 이용해 원형 큐 구현해보기]]></title>
            <link>https://velog.io/@gahui_dev/JavaScript%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%9B%90%ED%98%95-%ED%81%90-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@gahui_dev/JavaScript%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%9B%90%ED%98%95-%ED%81%90-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 01 Nov 2024 07:21:13 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 이전 포스팅에이어서 오늘은 원형 큐에 대해서 공부한 내용을 공유해보고자 합니다. </p>
<p>원형 큐는 일반적인 선형큐의 단점을 보완한 확장 개념입니다. 우선, 선형큐에 대해 간단히 복습해보겠습니다. 선형큐는 배열이나 링크드 리스트로 구현할 수 있지만, 배열로 구현할때 다음과 같은 문제가 발생할 수 있습니다. </p>
<ul>
<li><strong>비효율적인 요소이동</strong> : 요소를 삽입하거나 삭제 할때마다 모든 요소를 앞으로 혹은 뒤로 밀어야 합니다.</li>
<li>** 공간 낭비 ** : 삭제된 요소의 자리를 재활용하지 않으면, 앞 공간이 계속 비어있게 되어 큐가 비어있지 않더라도, 새요소를 삽입할 수 없게 됩니다. </li>
</ul>
<p>이러한 문제를 해결하기 위해서 <strong>원형큐</strong>가 도입되었습니다. 원형 큐는 큐의 마지막 포인터(rear)를 첫 번째 포인터(front)와 연결하여 끝과 끝이 이어진 원형 구조를 만들어 공간을 효율적으로 활용합니다.</p>
<p>원형 큐의 특징은 다음과 같습니다. </p>
<ul>
<li><strong>원형 구조</strong> : rear기 배열의 끝에 도달하여도 front가 가리키는 요소로 되돌하가서 비어있는 공간을 더 사용할 수 있습니다. </li>
<li>** 공간 낭비** : 삭제된 요소의 공간을 재활용하기 때문에 메모리를 최적화 할 수 있습니다. </li>
</ul>
<h4 id="enqueue-연산">enQueue 연산</h4>
<ol>
<li>Queue가 이미 가득 차 있는지 확인 합니다.</li>
<li>큐가 비어있다면 front와 rear을 0으로 초기화 합니다. </li>
<li>그렇지 않다면, 한칸 이동 시키되, 원형으로 처리하기 위해 다음 수식을 rear에 대입합니다. rear = (rear +1) %          MaxSize</li>
<li>rear의 위치에 queue에 값을 넣습니다. </li>
</ol>
<h4 id="dequeue-연산">deQueue 연산</h4>
<ol>
<li>Queue가 비어있는지 확인합니다.</li>
<li>큐가 비어있지 않다면, 요소를 제거할 수 있다는 의미이므로, front가 가르키는 index 값을 null로 변경 합니다.</li>
<li>만약, front와 rear의 값이 같다면, 요소가 하나만 존재한다는 의미이므로 두 값 모두 -1로 초기화 합니다.</li>
<li>그렇지 않다면, 다음 연산을 이용해 front의 위치를 이동 시킵니다. front = (front +1) % MaxSize</li>
</ol>
<h2 id="array를-이용한-원형-큐-구현">Array를 이용한 원형 큐 구현</h2>
<pre><code class="language-js">class CircularQueue {
  constructor(MAX_SIZE) {
    this.queue = [];
    this.MAX_SIZE = MAX_SIZE;
    this.front = -1;
    this.rear = -1;
  }

  isFull() {
    return (this.rear + 1) % this.MAX_SIZE === this.front;
  }
  isEmpty() {
    return this.front === -1 &amp;&amp; this.rear === -1;
  }

  enQueue(item) {
    if (this.isFull()) {
      throw Error(&quot;Queue is full&quot;);
    }

    if (this.isEmpty()) {
      this.front = this.rear = 0;
    } else {
      // rear 포인터를 한 칸 이동시키되, 원형으로 처리
      this.rear = (this.rear + 1) % this.MAX_SIZE;
    }

    this.queue[this.rear] = item;
  }

  deQueue() {
    if (this.isEmpty()) {
      throw Error(&quot;Queue is empty&quot;);
    }
    this.queue[this.front] = null;

    if (this.front === this.rear) {
      this.front = this.rear = -1;
    } else {
      this.front = (this.front + 1) % this.MAX_SIZE;
    }
  }
}</code></pre>
<p>원형 큐는 대부분의 로직이 기존 선형 큐와 유사하지만, 주요 차이점은 삽입과 삭제 시 rear와 front를 연결하여 순환 구조를 유지하는 방식입니다. 이때, (rear + 1) % maxSize를 통해 rear 포인터가 front로 돌아가도록 설정합니다. 이 방식으로 배열의 끝에 도달해도 다시 처음으로 이동할 수 있게 되어 효율적인 메모리 활용이 가능합니다.</p>
<p>배열을 사용해 구현된 원형 큐의 연산별 시간 복잡도는 다음과 같습니다:</p>
<p>삽입 (enQueue): Queue가 가득 찼는지 확인한 후, 올바른 위치에 요소를 삽입합니다. 요소를 삽입하는 작업은 상수 시간이므로, 시간 복잡도는 𝑂(1) 입니다. </p>
<p>삭제 (deQueue): Queue가 비었는지 확인하고, 요소를 삭제한 후 front 포인터를 이동시키거나, 큐가 비었을 경우 front와 rear를 초기화합니다. 이 역시 상수 시간에 수행되므로, 시간 복잡도는 O(1)입니다.</p>
<p>특정 요소 찾기: 특정 요소를 찾기 위해서는 모든 요소를 순회해야 하므로, 최악의 경우 O(n)의 시간 복잡도가 발생합니다. 이 부분은 일반적인 선형 큐와 동일합니다.</p>
<p>따라서, 원형 큐는 효율적인 메모리 활용과 간단한 삽입/삭제 연산을 제공하며, 삽입과 삭제는 O(1), 특정 요소를 찾는 연산은  O(n)의 시간 복잡도를 가집니다.</p>
<h2 id="linkedlist를-이용한-원형-큐">LinkedList를 이용한 원형 큐</h2>
<p>다음은 자바스크립트와 LinkedList로 구현한 원형 큐입니다. </p>
<pre><code class="language-js">class NewNode {
  constructor(value, priority) {
    this.value = value;
    this.priority = priority;
    this.next = null;
  }
}

class PriorityLinkedListQueue {
  constructor() {
    this.head = null;
    this.tail = null;
  }

  printQueue() {
    let current = this.head;
    while (current) {
      current = current.next;
    }
  }

  enQueue(value, priority) {
    const newNode = new NewNode(value, priority);
    // 리스트가 비어있을 경우
    if (!this.head &amp;&amp; !this.tail) {
      this.head = newNode;
      this.tail = newNode;
      return;
    }
    // 새 노드가 head보다 우선순위가 높을 경우
    if (this.head.priority &lt; newNode.priority) {
      newNode.next = this.head;
      this.head = newNode;
      return;
    }

    let current = this.head;
    let previous = null;

    while (current) {
      if (current.priority &lt;= newNode.priority) {
        previous = current;
        current = current.next;
      }
    }

    if (!current) {
      this.tail.next = newNode;
      this.tail = newNode;
    } else {
      previous.next = newNode;
      newNode.next = current;
    }
  }

  deQueue() {
    this.head = this.head.next;

    if (!this.head.next) {
      this.tail = null;
    }
  }
}</code></pre>
<p>원형 큐는 요소를 삽입할 때 마지막 요소의 next를 front와 연결해 순환 구조를 유지하는 것 외에는 일반적인 선형 큐와 거의 동일한 로직을 사용합니다.</p>
<p>삽입 (enQueue): 마지막 위치에 요소를 추가하고, rear.next를 front와 연결해 원형 구조를 만듭니다. 삽입은 큐의 끝에서 이루어지므로 상수 시간에 수행되며, 시간 복잡도는 O(1)입니다.</p>
<p>삭제 (deQueue): 큐의 맨 앞 요소를 제거하고, 새로운 front와 rear.next를 연결해 순환 구조를 유지합니다. 이 역시 큐의 앞부분에서 한 번의 연산으로 처리되므로, 시간 복잡도는 O(1)입니다.</p>
<p>따라서 원형 큐는 <strong>삽입과 삭제 모두 O(1)</strong>의 시간 복잡도를 가지며, 효율적인 메모리 사용과 간단한 연산을 제공하는 구조입니다.</p>
<h2 id="결론">결론</h2>
<p>배열로 선형 큐를 구현할 때 발생하는 한계를 해결하기 위해 원형 큐에 대해 공부해 보았습니다.</p>
<p>선형 큐를 배열로 구현하면 삽입과 삭제 시 앞쪽 공간이 비더라도 재사용할 수 없거나, 모든 요소를 이동시켜야 하는 문제가 있습니다. 이때, 배열을 사용해야 한다면 원형 큐 구조를 활용해 끝과 끝을 연결하여 이러한 문제를 효과적으로 해결할 수 있습니다.</p>
<p>반면, 연결 리스트(Linked List)로 큐를 구현할 경우, 선형 큐와 원형 큐 사이에 큰 차이가 없습니다. 연결 리스트는 삭제된 요소의 자리를 자연스럽게 활용할 수 있으므로, 굳이 원형 큐 구조를 사용할 필요가 없다는 생각이 들었습니다. </p>
<p>따라서, 배열로 큐를 구현해야 하는 상황이라면 원형 큐를 이용하는게 더 좋은 선택이 될 것 같습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Heap을 이용한 우선순위 큐 구현 ]]></title>
            <link>https://velog.io/@gahui_dev/Heap%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@gahui_dev/Heap%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Thu, 31 Oct 2024 10:33:57 GMT</pubDate>
            <description><![CDATA[<p>이전 포스팅에서는 큐의 기본 개념과 가장 기본적인 형태인 선형 큐에 대해 다루었습니다. 오늘은 선형 큐외에 우선순위 큐에 대해 공부한 내용을 공유하려고 합니다.</p>
<p>선형큐는 가장 처음으로 들어간 요소가 가장 처음으로 나오는 자료구조형니다(FIFO). 하지만, 특정 요소가 우선순위에 따라서 먼저 처리되어야 하는 상황이 있을 때는 어떻게 할까요? 이러한 필요에의해서 <strong><em>우선순위 큐</em></strong>가 만들어졌습니다. 이번 글에서는 <strong><em>우선순위 큐의 개념</em></strong>과 <strong><em>JS로 구현하는 방법</em></strong> 을 공부해보겠습니다. </p>
<p>또한, 리액트 18부터 도입된 Concurrent Mode에서 우선순위 큐가 렌더링 최적화에 어떻게 활용되는지, 실제 React코드를 통해 알아보겠습니다.</p>
<h1 id="우선순위-큐">우선순위 큐</h1>
<p>우선순위 큐는 말그대로 <strong>*&quot;우선 순위가 높은&quot;요소를 먼저 제거 할 수 있도록*</strong> 하는 큐의 확장형 자료 구조 형입니다. 이를 위해 각 요소에는 <strong><em>우선순위를 나타내는 값</em></strong>이 필요합니다. 동일한 우선순위를 가진 요소들 간에는 일반적인 큐의 특성에 따라, 먼저 삽입된 요소가 먼저 처리됩니다. </p>
<p>우선 순위 큐는 배열, 링크드 리스트, 힙 등 다양한 방법으로 구현할 수 있으며, 구현 방식에 따라 효율성과 특성이 달라집니다. </p>
<h2 id="array를-이용한-우선순위-큐">Array를 이용한 우선순위 큐</h2>
<pre><code class="language-js">class PriorityArrayQueue {
  constructor() {
    this.queue = [];
  }

  get get_queue() {
    return this.queue;
  }

  enQueue(value, priority) {
    const element = { value, priority };
    let added = false;

    for (let i = 0; i &lt; this.queue.length; i++) {
      if (this.queue[i].priority &gt; priority) {
        this.queue.splice(i, 0, element);
        added = true;
        break;
      }
    }

    if (!added) {
      this.queue.push(element);
    }
  }

  deQueue() {
    this.queue.shift();
  }
}</code></pre>
<p>PriorityArrayQueue는 요소를 삽입할 때마다 요소를 우선순위에 맞는 위치에 배치하는 방식을 사용합니다. 이때 Array의 정렬된 위치에 요소를 삽입해야 하기 때문에, 삽입 연산은 O(n)의 시간 복잡도를 가집니다. 
또한, 요소 제거는 가장 앞에 있는 요소를 제거하는데, 배열의 첫 요소를 제거하면 나머지 요소들을 한칸씩 앞으로 이동시켜야 하므로 이역시 O(n)이라는 시간 복잡도를 가집니다. </p>
<h4 id="시간-복잡도-요약">시간 복잡도 요약</h4>
<ul>
<li>삽입 : for문을 통해 적절한 위치를 찾아 splice로 삽입하기 때문에 O(n)</li>
<li>삭제 : 삭제 또한 splice로 첫번째 요소를 제거한 후, 모든 요소를 앞으로 한칸씩 밀어야 하기 때문에 O(n)이라는 시간복잡도를 갖습니다. </li>
<li>요소 찾기 : 특정 요소를 찾는 연산 또한 배열 전체를 순회 하며 찾아야 하기 때문에 O(n) 시간복잡도를 갖습니다. </li>
</ul>
<p>이와 같은 배열 기반 우선순위 큐는 데이터 크기가 작을때는 구현하기 좋지만, 요소 삽입과 삭제 시 이동이 발생해 큰 데이터를 처리할 때는 성능이 떨어질 수 있습니다. </p>
<h2 id="linkedlist를-이용한-우선순위-큐">LinkedList를 이용한 우선순위 큐</h2>
<p>Array기반 우선순위 큐의 단점을 극복하기 위해 LinkedList를 사용하여 구현할 수도 있습니다. 다음은 LinkedList로 구현한 우선순위 큐의 예제 코드입니다. </p>
<pre><code class="language-js">class NewNode {
  constructor(value, priority) {
    this.value = value;
    this.priority = priority;
    this.next = null;
  }
}

class PriorityLinkedListQueue {
  constructor() {
    this.head = null;
    this.tail = null;
  }

  printQueue() {
    let current = this.head;
    while (current) {
      current = current.next;
    }
  }

  enQueue(value, priority) {
    const newNode = new NewNode(value, priority);
    // 리스트가 비어있을 경우
    if (!this.head &amp;&amp; !this.tail) {
      this.head = newNode;
      this.tail = newNode;
      return;
    }
    // 새 노드가 head보다 우선순위가 높을 경우
    if (this.head.priority &lt; newNode.priority) {
      newNode.next = this.head;
      this.head = newNode;
      return;
    }

    let current = this.head;
    let previous = null;

    while (current) {
      if (current.priority &lt;= newNode.priority) {
        previous = current;
        current = current.next;
      }
    }

    if (!current) {
      this.tail.next = newNode;
      this.tail = newNode;
    } else {
      previous.next = newNode;
      newNode.next = current;
    }
  }

  deQueue() {
    this.head = this.head.next;

    if (!this.head.next) {
      this.tail = null;
    }
  }
}</code></pre>
<h4 id="시간-복잡도">시간 복잡도</h4>
<ul>
<li><p>삽입 : 새 요소를 추가할 때는 현재 요소들을 순회하며 적절한 위치를 찾아 삽입합니다. 이 과정에서 O(n)의 시간이 소요됩니다.</p>
</li>
<li><p>삭제 : 가장 첫 요소(우선순위가 가장 높은 요소)를 제거하기 때문에 O(1)의 시간이 소요됩니다. </p>
</li>
<li><p>장점 : 요소를 제거할 때는 head를 삭제하면 되기 때문에 빠르게 처리할 수 있습니다.</p>
</li>
<li><p>단점 : 배열과 달리 인덱스를 통해서 접근할 수 없기 때문에 특정 요소에 접근해야 하는경우 효율적이지 않습니다. </p>
</li>
</ul>
<p>LinkedList를 사용한 우선순위 큐는 삽입과 삭제가 빈번한 상황에서는 효율적이지만, 특정 인덱스에 접근하거나 요소를 빠르게 찾는 경우에는 적합하지 않습니다. </p>
<h2 id="힙을-이용한-우선순위-큐">힙을 이용한 우선순위 큐</h2>
<p>Heap은 특정 조건을 만족하는 이진 트리 기반의 자료구조입니다. 주로 <strong><em>최소힙</em></strong>과 <strong><em>최대힙</em></strong> 으로 나뉘는데요. 루트의 노드가 최소값이냐 최대값이냐에 따라 구분됩니다. </p>
<p>힙은 배열을 이용해서 구현하는 경우가 많습니다. 아래는 자바스크립트로 구현한 최소 힙의 예제 코드입니다. </p>
<pre><code class="language-js">class MinHeap {
  constructor() {
    this.heap = [];
  }

  insert(value) {
    this.heap.push(value);
    this.bubbleUp();
  }
  remove() {
    if (this.heap.length === 0) {
      return null;
    }

    if (this.heap.length === 1) {
      return this.heap.pop();
    }
    this.heap[0] = this.heap.pop();

    this.bubbleDown();
  }

  parentIndex(index) {
    return Math.floor((index - 1) / 2);
  }

  leftChildIndex(index) {
    return index * 2 + 1;
  }
  rightChildIndex(index) {
    return index * 2 + 2;
  }

  bubbleUp() {
    let index = this.heap.length - 1;
    while (index &gt; 0) {
      // 부모랑 비교 한다.
      const parentIndex = this.parentIndex(index);
      const parent = this.heap[parentIndex];
      const current = this.heap[index];

      if (parent &lt;= current) {
        break;
      }
      this.heap[parentIndex] = current;
      this.heap[index] = parent;
      index = parentIndex;
    }
  }

  bubbleDown() {
    let index = 0;
    const length = this.heap.length;
    const element = this.heap[0];

    while (true) {
      const leftChildIndex = this.leftChildIndex(index);
      const rightChildIndex = this.rightChildIndex(index);

      let swap = null;
      let leftChild, rightChild;

      if (leftChildIndex &lt; length) {
        leftChild = this.heap[leftChildIndex];
        if (leftChild &lt; element) {
          swap = leftChildIndex;
        }
      }

      if (rightChildIndex &lt; length) {
        rightChild = this.heap[rightChildIndex];
        if (
          (!swap &amp;&amp; rightChild &lt; element) ||
          (swap &amp;&amp; rightChild &lt; leftChild)
        ) {
          swap = rightChildIndex;
        }
      }

      if (!swap) {
        break;
      }
      this.heap[index] = this.heap[swap];
      this.heap[swap] = element;
      index = swap;
    }
  }

  display() {
    console.log(this.heap);
  }
}</code></pre>
<p>간단한 코드 설명을 하자면</p>
<ul>
<li>BubbleUp : 요소를 추가할 때, 새로운 요소가 부모 노드보다 작으면 부모와 그 위치를 교환하며 위로 이동시킵니다. </li>
<li>BubbleDown: 요소를 제거한 후, 힙의 규칙을 유지하기 위해 상위 노드와 하위 노드를 비교하며 자리를 교환해 트리 구조를 재조정합니다.</li>
</ul>
<p>힙은 요소를 추가할때 트리의 가장 하위에 요소를 추가한 후, 힙의 규칙에 따라 필요한 경우 부모와 위치를 교환하기 때문에 O(log n)의 시간 복잡도로 조금 더 효율적인 삽입이 가능합니다. 이는 정렬된 배열이나 리스트 기반 우선순위 큐에서의 O(n) 복잡도보다 훨씬 효율적입니다. </p>
<p>요소를 삭제할때도 최상단 노드를 제거한 후, 마지막 노드를 최상단으로 올리고 트리의 높이 만큼 Bubble Down 시키기 때문에 O(log n)의 시간복잡도를 가집니다. </p>
<p>즉, 정렬된 배열이나 리스트를 우선순위 큐로 사용할 경우, 삽입시마다 요소들을 이동해야 하므로 시간이 오래 걸릴 수 있습니다. 반면, 힙은 트리의 높이만큼만 이동하기 때문에 큰 데이터를 다룰 때도 빠르게 삽입과 삭제가 가능합니다.</p>
<h4 id="우선순위-큐를-사용한-리액트의-리렌더링-최적화">우선순위 큐를 사용한 리액트의 리렌더링 최적화</h4>
<p>리액트18 이후부터 기존 렌더링을 최적화하기 위해 Concurrent Mode라는 개념이 도입되었습니다. Concurrent Mode는 여러 요소가 렌더링 되어야 할때, 각 요소의 우선순위를 판단하여 렌더링 작업을 중지하거나 실행할 수 있게 합니다. </p>
<p>이러한 우선순위 제어는 React의 Scheduler라이브러리가 담당하며, 내부적으로 <strong><em>최소힙</em></strong>으로 구현되어 있습니다. Scheduler는 작업의 우선순위를 효율적으로 관리하여 우선순위가 높은 작업을 먼저 실행할 수 있도록 도와줍니다. </p>
<p>이번에는 리액트 스케줄러가 실제로 어떤 방식으로 구현되어 있는지, 실제 코드를 통해 살펴보고자 합니다. </p>
<pre><code class="language-js">/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict
 */

type Heap&lt;T: Node&gt; = Array&lt;T&gt;;
type Node = {
  id: number,
  sortIndex: number,
  ...
};

export function push&lt;T: Node&gt;(heap: Heap&lt;T&gt;, node: T): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

export function peek&lt;T: Node&gt;(heap: Heap&lt;T&gt;): T | null {
  return heap.length === 0 ? null : heap[0];
}

export function pop&lt;T: Node&gt;(heap: Heap&lt;T&gt;): T | null {
  if (heap.length === 0) {
    return null;
  }
  const first = heap[0];
  const last = heap.pop();
  if (last !== first) {
    // $FlowFixMe[incompatible-type]
    heap[0] = last;
    // $FlowFixMe[incompatible-call]
    siftDown(heap, last, 0);
  }
  return first;
}

function siftUp&lt;T: Node&gt;(heap: Heap&lt;T&gt;, node: T, i: number): void {
  let index = i;
  while (index &gt; 0) {
    const parentIndex = (index - 1) &gt;&gt;&gt; 1;
    const parent = heap[parentIndex];
    if (compare(parent, node) &gt; 0) {
      // The parent is larger. Swap positions.
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // The parent is smaller. Exit.
      return;
    }
  }
}

function siftDown&lt;T: Node&gt;(heap: Heap&lt;T&gt;, node: T, i: number): void {
  let index = i;
  const length = heap.length;
  const halfLength = length &gt;&gt;&gt; 1;
  while (index &lt; halfLength) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    // If the left or right node is smaller, swap with the smaller of those.
    if (compare(left, node) &lt; 0) {
      if (rightIndex &lt; length &amp;&amp; compare(right, left) &lt; 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (rightIndex &lt; length &amp;&amp; compare(right, node) &lt; 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // Neither child is smaller. Exit.
      return;
    }
  }
}

function compare(a: Node, b: Node) {
  // Compare sort index first, then task id.
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}</code></pre>
<p>위 코드는 실제 리액트 스케줄러의 packages/scheduler/src/SchedulerMinHeap.js 의 코드입니다.</p>
<ul>
<li>Node: 각 노드에는 id와 sortIndex가 있으며, sortIndex가 작은 값일수록 우선순위가 높습니다.</li>
<li>push/pop: 새로운 노드를 힙에 추가하거나 삭제하는 함수로, 노드를 추가할 때는 push, 최상단 노드를 제거할 때는 pop을 이용합니다. </li>
<li>ShiftUp: 새로운 노드가 추가되면, 부모 노드보다 작을 경우, 부모와 위치를 교환하여 위로 이동시킵니다. </li>
</ul>
<blockquote>
<p>parentIndex는 (index - 1) &gt;&gt;&gt; 1 은 JavaScript의 비트 연산자 중 하나로, 부호 없는 우측 시프트 연산입니다. 이를 쉽게 설명하자면, (index - 1) &gt;&gt;&gt; 1은 index - 1 값을 오른쪽으로 1비트(즉, 2로 나눈 몫)를 이동시키는 연산입니다. (Math.floor((index -1) /2 )와 동일</p>
</blockquote>
<ul>
<li>shiftDown의 경우, 현재 노드와 자식 노드를 비교하며 더 작은 쪽과 자리를 바꿔 아래로 이동시킵니다. 자식 중 작은 값과 위치를 교환하는 방식으로 힙 속성을 유지하고, 두 자식이 모두 현재 노드보다 크면 종료됩니다.</li>
</ul>
<p>여기서 주목할 부분은 shiftDown의 &quot;helfLength&quot;부분인데요. halfLength는 반복의 범위를 힙의 중간까지만 제한하여 불필요한 비교를 줄이기 위해서 사용됩니다. </p>
<ul>
<li>힙에서 부모 노드는 인덱스가 0부터 시작하며, 자식 노드가 있는 마지막 부모 노드는 배열의 중간까지만 존재합니다.</li>
<li>힙의 배열 인덱스에서 자식 노드가 없는 마지막 부모 노드의 인덱스는 전체 길이의 절반까지입니다.</li>
</ul>
<p>이러한 halfLength 제한을 사용해 비교 연산을 최적화하는 방식은 React의 렌더링 성능을 높일수 있습니다. 이처럼 힙 연산을 개선함으로써 React는 우선순위가 높은 작업을 더 빠르게 처리할 수 있게 됩니다. 이를 이용해서도 기존 우선순위 큐 연산을 더 개선시킬 수 있을것 같습니다. </p>
<p>마지막으로, React의 Concurrent Mode에서 힙을 이용한 Scheduler가 어떻게 렌더링을 최적화하는지 알아보았습니다. React의 Scheduler는 최소 힙을 이용하여 작업의 우선순위를 관리함으로써, 우선순위가 높은 작업을 먼저 처리하여 렌더링 성능을 높이는 중요한 역할을 합니다. </p>
<p>이런 자료구조를 공부함으로써 우리가 사용하는 프레임워크나 라이브러리의 내부 동작을 앞으로 더 깊이 이해할 수 있을것같습니다.</p>
<p>참조
<a href="https://github.com/facebook/react/blob/main/packages/scheduler/src/SchedulerMinHeap.js">리액트 스케줄러 최소 힙 코드</a></p>
<p><a href="https://ko.react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react">React의 Concurrent란?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔가 개발자가 알아두면 좋을 자료구조 큐]]></title>
            <link>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EA%B0%80-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%95%8C%EC%95%84%EB%91%90%EB%A9%B4-%EC%A2%8B%EC%9D%84-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%81%90</link>
            <guid>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EA%B0%80-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%95%8C%EC%95%84%EB%91%90%EB%A9%B4-%EC%A2%8B%EC%9D%84-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%81%90</guid>
            <pubDate>Tue, 29 Oct 2024 07:13:00 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 개발자라면 누구나 한 번쯤 자바스크립트의 태스크 큐에 대해 들어보았을 텐데요. 태스크 큐는 자바스크립트의 비동기 처리를 관리하는 중요한 역할을 합니다. 여기까지는 많이 알려진 사실이지만, &#39;태스크 큐&#39;라는 이름의 유래나, 왜 큐라는 자료구조가 사용되었는지에 대해 깊이 생각해 본 적은 없었습니다. 이번에 자료구조 큐를 공부하면서 태스크 큐가 왜 큐를 사용하게 되었는지 이해하게 되었고, 이에 대해 내용을 공유하고자 합니다.</p>
<h2 id="큐">큐</h2>
<p>큐는 스택과 마찬가지로 선형 자료구조이며, FIFO (First-In-First-Out) 방식으로 작동합니다. 즉, 먼저 들어간 요소가 가장 먼저 나오는 구조를 가지고 있습니다. 이 원리만 이해하면 큐는 비교적 단순한 자료구조처럼 보입니다.</p>
<p>이전 글에서 다룬 스택은 배열(Array)이나 연결 리스트(LinkedList)로 구현할 수 있었습니다. 큐 역시 비슷하게 배열이나 연결 리스트를 사용하여 구현할 수 있습니다. 다만, 큐는 요소가 들어오고 나가는 방향이 다르다는 점에서 스택과 차이를 보입니다.</p>
<h3 id="array로-큐를-구현할수-있을까">Array로 큐를 구현할수 있을까?</h3>
<p>처음에는 JavaScript의 Array를 이용해서 큐를 구현해 보았는데요. 다음은 제가 작성한 코드 입니다. </p>
<pre><code class="language-js">class ArrayQueue {
  constructor(maxSize) {
    this.queue = Array(maxSize).fill(Infinity);
    this.front = 0;
    this.rear = -1;
  }
  isFull() {
    return this.rear === this.queue.length - 1;
  }
  isEmpty() {
    return this.front &gt; this.rear;
  }
  enqueue(item) {
    if (this.isFull()) {
      throw Error(&quot;Queue is Full&quot;);
    }
    this.rear += 1;
    this.queue[this.rear] = item;
  }
  dequeue() {
    if (this.isEmpty()) {
      throw Error(&quot;Queue is Empty&quot;);
    }
    for (let i = 0; i &lt; this.rear; i++) {
      this.queue[i] = this.queue[i + 1];
    }

    --this.rear;
  }

}</code></pre>
<p>Array를 사용하여 큐를 구현한 방식에서, dequeue메서드를 실행할때마다 모든 요소를 앞으로 이동시켜야 한다는 문제가 발생했습니다. 물론 front를 단순히 +=1로 증가시키는 방식으로 수정할 수도 있지만, 또 다른 문제가 생깁니다. 배열의 길이가 제한되어 있기 때문에, 일정 크기를 넘어서는 요소를 enqueue할 수 없게 됩니다.</p>
<p>결국 Dequeue시 모든 요소를 이동시키는건 O(n)의 시간 복잡도가 발생하며, 이는 효율적이지 않은 방법이라 판단했습니다!</p>
<h3 id="linkedlist로-구현한-선형-큐">LinkedList로 구현한 선형 큐</h3>
<p>오늘도 혼자 구현을 하다 결국 gpt의 도움으로 LinkedList를 이용한 선형 큐를 만들 수 있었습니다. T.T </p>
<pre><code class="language-js">class Node {
  constructor(newNode) {
    this.value = newNode;
    this.next = null;
  }
}
class LinkedListQueue {
  constructor() {
    this.first = null;
    this.rear = null;
  }
  isEmpty() {
    return !this.first &amp;&amp; !this.rear;
  }
  enQueue(item) {
    const newNode = new Node(item);
    if (!this.rear) {
      this.current = newNode;
      this.rear = newNode;
      return;
    }

    this.rear.next = newNode;
    this.rear = newNode;
  }
  deQueue() {
    if (this.isEmpty()) {
      throw Error(&quot;Queue is Empty&quot;);
    }
    const value = this.first;
    this.first = this.first.next;

    if (!this.front) {
      this.rear = null;
    }
  }
</code></pre>
<p>이 코드에서 LinkedListQueue는 연결 리스트를 사용하여 큐를 구현합니다. 동작 원리는 다음과 같습니다. </p>
<ul>
<li>큐의 <strong><em>첫번째(front)</em></strong>와 <strong><em>마지막(rear)</em></strong> 노드를 가리키는 포인터로 시작과 끝을 추적합니다. </li>
<li>enQueue연산시, 큐가 비어있지 않다면 rear.next를 새 노드로 설정하고, rear 포인터를 새 노드로 생신합니다.</li>
<li>deQueue연산시, 큐의 <strong><em>첫번째 요소</em></strong>를 제거하고 front를 다음 노드로 이동시킵니다. </li>
</ul>
<h3 id="연결리스트-큐의-장점과-단점">연결리스트 큐의 장점과 단점</h3>
<p>연결 리스트로 구현된 큐는 enQueue와 deQueue 연산을 O(1) 시간 복잡도로 수행할 수 있습니다. 따라서 배열 기반 큐에서 발생하는, deQueue시 모든 요소를 앞으로 이동시키는 추가 연산이 필요하지 않다는 장점이 있습니다. </p>
<p>하지만 연결리스트로 구현된 큐는 각 노드가 다음 노드에 대한 포인터를 가지고 있어야 하므로 추가적인 메모리 공간이 필요합니다. 또한, 특정 요소를 찾을때는 순차 접근만 가능하여 시간복잡도가 O(n)이 됩니다. 이 점은 큐에서의 일반적인 요소 접근에는 영향을 주지 않지만, 특정 요소 검색이 필요한 경우에는 성능에 제약이 될 수 있을거라 판단이 됩니다. </p>
<h3 id="태스크큐는-어떤-큐일까">태스크큐는 어떤 큐일까?</h3>
<p>지금까지 제가 이야기한 큐는 큐의 4가지 종류중 가장 기본적인 &quot;선형 큐&quot;에 해당합니다. 서두에 이야기 했던 태스크 큐는 비동기 작업을 순서대로 실행하도록 보장하는 큐입니다. 태스크 큐는 작업에 대한 우선순위나 특정한 순환 방식 없이 단순하게 작업을 순차적으로 실행합니다. 따라서 기본적인 <strong><em>선형 큐</em></strong>를 사용하는 것이 적합합니다. </p>
<h3 id="프론트엔드-개발에서-큐-사용-사례">프론트엔드 개발에서 큐 사용 사례</h3>
<p>프론트엔드 개발을 하면서 큐를 직접 구현해본 경험이 두가지가 있습니다. 여기서는 <strong><em>네트워크 통신 관리</em></strong>와 <strong><em>JWT토큰 만료 처리</em></strong>라는 두 가지 상황에서 큐를 어떻게 활용했는지 공유해보려합니다. </p>
<ol>
<li><p>네트워크 통신 관리
특정 네트워크 통신 장치와 클라이언트 간에 지속적으로 통신을 유지해야 하는 경우, 웹소켄을 통해 요청을 전송하는 경우가 있는데요. 하지만 때때로, 네트워크의 불안정으로 인해 장치로부터 응답이 없거나 요청이 실패할 수 있습니다. 이때 각 요청을 임시 큐인 waitingQueue에 저장해 두고, 모든 요청이 정상적으로 완료되지 않은 경우 재요청을 보내는 방식으로 문제를 해결할 수 있습니다. 이때 큐는 실패한 요청을 차례로 다시 시도하도록 해줍니다. </p>
</li>
<li><p>JWT토큰 만료 처리
JWT기반 인증 서비스에서 토큰이 만료되면 서버 요청이 거부되거나 실패하게 됩니다. 이떄 ProcessQueue라는 간단한 큐를 만들어 실패한 요청을 담아 두고, 토큰을 갱신 시킨후 순차적으로 재실행되도록 하여 사용자들의 UX를 향상시킬수 있습니다.
이때 큐의 역할은 토큰이 유효하지 않아도 요청이 손실되지 않도록 해주며, 토큰 갱신 후에도 실행 순서를 유지할 수 있습니다. </p>
</li>
</ol>
<h3 id="왜-배열로-구현했을까">왜 배열로 구현했을까?</h3>
<p>사실 위 두가지 사례에서 배열을 사용해 큐를 구현했는데, 이는 연결 리스트의 복잡성이 불필요 했기 때문인데요. 단순히 요청을 순서대로 담아두고 필요한 시점에 순차적으로 실행하면 되는 상황이었기에 배열을 선택했습니다. 배열 큐는 접근성과 메모리 측면에서 간단하게 문제를 해결할 수 있기에 선택하게 되었습니다. </p>
<p>프론트엔드 개발에서 큐는 비동기 작업의 흐름을 관리하고, 사용자 경험을 원활하게 하기위한 유용한 도구인것같습니다. 실제로 프론트엔드에서 배열을 이용한 큐 구조는 간단하면서도 비동기 작업으리 순서를 보장하는 데 충분한 역할을 할 수 있습니다. 앞으로 더 복잡한 비동기 작업이나 데이터 흐름을 처리해야 하는 상황이 온다면, LinkedList 기반의 큐를 이용할 수 있다면 활용해 보는것도 좋은 시도가 될것같다고 생각했습니다!</p>
<p>다음 포스팅은 이어서 큐의 또 다른 종류인 우선순위 큐, 원형큐, 덱 에대해서 공유하는 글을 이어서 작성해보겠습니다. 
감사합니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 개발자가 스택을 알아야 하는 이유]]></title>
            <link>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%8A%A4%ED%83%9D%EC%9D%84-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@gahui_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%8A%A4%ED%83%9D%EC%9D%84-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 27 Oct 2024 09:01:58 GMT</pubDate>
            <description><![CDATA[<p>지난 글에서 스택과 큐에 대해 다뤘지만, 피드백을 받으며 스택에 대해 제가 혼자만의 오해를 가지고 있었다는 걸 알게 되었습니다. 그래서 오늘은 그 오해를 풀고, 스택 자료구조에 대해 다시 한번 정리해보고자 합니다.</p>
<p>사실, 스택의 개념에 대해서는 저보다 훨씬 뛰어난 개발자분들이 이미 쉽게 풀어 쓴 글들이 많기 때문에, 이 글에서는 개념을 간략히 짚고 넘어가려 합니다. 대신 JavaScript로 스택을 직접 구현해보고, 프론트엔드 개발자로서 이 자료구조에 대한 저의 생각을 공유하고자 합니다.</p>
<p>스택을 이해하고 나니 다양한 상황에서 실질적인 활용이 가능하다는 걸 깨달았고, 오늘 글을 통해 이러한 점을 함께 나누고 싶습니다.</p>
<h3 id="스택이란">스택이란?</h3>
<p>스택은 마지막에 넣은 요소가 가장 처음으로 제거 된다는 간단한 자료 구조입니다. </p>
<p>관련된 용어로는 다음과 같습니다.</p>
<ul>
<li>요소를 넣는 것 : push</li>
<li>요고를 제거하는 것 : pop</li>
<li>스택 공간 초과 : stack overflow</li>
<li>스택 공간 부족 : stack underflow</li>
</ul>
<p>스택 오버플로우가 발생한다는 것은 스택이 고정된 크기 제한을 가지고 있다는 의미입니다. 다만, 스택의 종류에 따라 스택 오버플로우가 발생하지 않을 수도 있습니다. 여기서 말하는 스택 오버플로우는 메모리 초과(Out of Memory) 오류를 제외하고, 스택 자체의 크기 제한에 의해 발생하는 오류를 의미합니다.</p>
<h3 id="array--stack-인가">Array === Stack 인가?</h3>
<p>스택의 종류를 설명하기에 앞서, 제가 처음 스택을 공부하며 했던 오해에 대해 먼저 이야기해보려 합니다. 처음엔 JavaScript의 Array가 push와 pop 메서드를 제공하니, Array 자체가 스택 자료구조라고 생각했습니다. 하지만, <strong>Array는 스택인가?</strong>라는 질문에 대한 답은 사실 <strong>&quot;아니요&quot;</strong>입니다.</p>
<ul>
<li>스택은 Array뿐 아니라 Linked List로도 구현할 수 있습니다. 즉, Array를 이용해 스택을 만들 수는 있지만, Array만이 스택을 구성하는 유일한 방법은 아닙니다.</li>
<li>스택은 오직 상단(top)에서만 접근할 수 있는 제한이 있습니다. 하지만 Array는 push와 pop 외에도 맨 앞에 요소를 추가할 수 있는 unshift등 다양한 메서드를 제공합니다. 이 때문에 Array는 스택의 엄격한 특성을 만족하지 못합니다.</li>
</ul>
<p>결론적으로, Array는 스택을 구현할 수 있는 도구일 뿐, 그 자체로 스택 자료구조라고 할 수는 없습니다.</p>
<h3 id="스택의-종류">스택의 종류</h3>
<p>stack overflow 오류가 발생하냐 안하냐는 다음 두 종류의 스택에 따라서 달라지는데요.</p>
<h4 id="배열-기반-스택">배열 기반 스택</h4>
<p>배열 기반 스택의 경우 말그대로 Array를 기반으로 만들어진 것인데요. 스택의 크기가 제한적이라는 특징이 있습니다. 
관련해서 작성 해본 코드는 다음과 같습니다. 간단히, pop과 push기능만 들어간 코드를 작성해보았습니다. </p>
<pre><code class="language-js">class Stack {
  constructor(maxSize) {
    this.stack = [];
    this.maxSize = maxSize;
  }
  get get_stack() {
    return this.stack;
  }

  push(item) {
    if (this.maxSize === this.stack.length) {
      throw Error(&quot;stack overflow&quot;);
    }
    this.stack.push(item);
  }

  pop() {
    if (this.stack.length === 0) {
      throw Error(&quot;stack underflow&quot;);
    }
    this.stack.pop();
  }
}</code></pre>
<h4 id="linked-list-기반-스택">Linked List 기반 스택</h4>
<p>Linked List는 연결된 리스트라는 의미로 하나의 데이터와 그다음 데이터의 위치를 저장해 서로 연결시킨 구조를 말합니다. 프론트엔드 개발자에겐 다소 생소한 개념일 수 있을 것 같습니다. 왜냐하면, JavaScript에서는 Linked List가 없기 때문입니다. 
다시 본론으로 돌아와서, Linked List를 이용해 만든 스택을 다이나믹 스택이라고도 부르는데요. 앞서 이야기 했다시피, 이 스택의 장점은 크기를 정해놓지 않아서 Stack overflow가 발생하지 않는 다는 장점이 있습니다. </p>
<p>JavaScript를 이용해서 이 LinkedList stack를 구현을 어떻게 할수 있을지에 대해 고민하다. 저한테는 조금 어려워.. 결국, gpt의 도움을 받아 다음과 같이 완성할 수 있었습니다!</p>
<pre><code class="language-js">class Node {
  constructor(value) {
    this.value = value;
    this.next = null; // 다음 node에 대한 정보를 저장합니다.
  }
}

class LinkedListStack {
  constructor() {
    this.head = null;
    this.size = 0;
  }
  push(item) {
    const newNode = new Node(item);

    newNode.next = this.head;
    this.head = newNode;
    this.size++;
  }

  pop() {
    if (!this.size) {
      throw new Error(&quot;stack underflow&quot;);
    }
    this.head = this.head.next;
    this.size--;
  }
}</code></pre>
<p>코드에 대해서 간단히 설명해 보자면, </p>
<ul>
<li>Node 클래스: value와 next를 속성으로 가지며, 다음 노드를 가리킵니다.</li>
<li>push 메서드: 새 노드를 스택의 head에 추가합니다. 새로운 노드의 next를 현재 head로 설정한 후, head를 새 노드로 업데이트합니다.</li>
<li>pop 메서드: 현재 head의 값을 반환하고, head를 다음 노드로 이동시킵니다. 만약 head가 없으면(스택이 비어 있으면) Stack Underflow 에러를 발생시킵니다.</li>
</ul>
<h3 id="스택의-시간공간-복잡도">스택의 시간/공간 복잡도</h3>
<p>스택의 시간과 공간 복잡도는 사용하는 연산에 따라 달라집니다.</p>
<p>시간 복잡도:
<strong><em>push와 pop 연산:</em></strong> 스택은 항상 상단(top)에서만 요소를 추가하거나 제거하기 때문에 특정 위치를 찾거나 이동할 필요가 없습니다. 따라서 push와 pop 연산은 O(1)의 시간 복잡도를 가집니다.
<strong><em>특정 요소 접근:</em></strong> 스택에서는 특정 요소에 접근하려면 상단부터 해당 요소까지 차례로 탐색해야 합니다. 따라서 특정 요소를 읽거나 검색하는 연산의 시간 복잡도는 O(n)이 될 수 있습니다.
공간 복잡도:</p>
<p><strong><em>전체 공간 복잡도:</em></strong> 스택의 공간 복잡도는 현재 스택에 저장된 요소의 수에 비례하며 O(n)입니다. 이는 스택에 n개의 요소가 있을 때, 이들 모두를 저장하기 위한 공간이 필요하기 때문입니다.
각 연산의 공간 복잡도: push와 pop 연산은 요소 하나를 추가하거나 제거할 때 필요한 고정된 공간만 차지하기 때문에 각 연산의 공간 복잡도는 O(1)입니다.</p>
<h3 id="나도-모르게-사용하고-있던-스택-자료구조">나도 모르게 사용하고 있던 스택 자료구조</h3>
<p>&quot;프론트엔드 개발자가 스택을 다룰 일이 있을까요?&quot; 예전엔 그렇지 않다고 생각했습니다. 그러나 이번 공부를 통해 생각이 바뀌게되는 계기가 되었습니다. </p>
<p>사실, 저는 이미 저도 모르게 스택 자료구조를 사용하고 있었는데요. 모달 컴포넌트를 구현할 때, 모달을 겹겹이 쌓아 올리고, 가장 최근에 열린 모달부터 닫는 방식은 바로 스택의 후입선출(LIFO) 원리를 활용한 것이었습니다.</p>
<p>프론트엔드 개발에서 스택은 이런 식으로 자연스럽게 쓰이며, 다양한 UI/UX 기능에서도 중요한 역할을 합니다. 그러나 스택은 항상 상단(top) 요소에만 접근할 수 있다는 제한이 있어 원하는 위치의 요소를 쉽게 찾거나 조작하기 어렵다는 단점이 있습니다. </p>
<p>예를 들어, 모달에 &quot;하위에 있는 모달을 클릭했을 때 그 모달이 최상단으로 올라오게 해주세요&quot;와 같은 요구 사항이 추가된다면, 스택 자료구조는 더 이상 적합하지 않게 됩니다. 이럴 땐 요소에 자유롭게 접근할 수 있는 큐나 배열을 활용한 관리 방식이 더 적합할 것입니다.</p>
<p>그럼에도 불구하고 스택은 특정 기능에서 아주 유용하게 쓰일 수 있습니다. 예를 들어, 방문 기록을 저장하거나 알림 창을 관리하는 기능에서도 스택 구조는 큰 도움이 됩니다.</p>
<p>프론트엔드 개발에서 클린 코드란 단순히 코드가 깔끔해 보이는 걸 넘어서, 효율적이고 유지보수하기 쉽게 만드는 것을 의미합니다. 이런 자료구조를 잘 이해하고 있으면 제품의 기능을 더 개선해 나갈 수 있고, 사용자에게도 더 나은 경험을 제공할 수 있을 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료 구조, 스택과 큐에 대하여 ]]></title>
            <link>https://velog.io/@gahui_dev/%EC%9E%90%EB%A3%8C-%EA%B5%AC%EC%A1%B0-%EC%8A%A4%ED%83%9D%EA%B3%BC-%ED%81%90%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@gahui_dev/%EC%9E%90%EB%A3%8C-%EA%B5%AC%EC%A1%B0-%EC%8A%A4%ED%83%9D%EA%B3%BC-%ED%81%90%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Thu, 24 Oct 2024 05:05:46 GMT</pubDate>
            <description><![CDATA[<p>자료구조를 공부할때 가장 먼저 접하는것은 “스택(Stack)”과 “큐(Queue)” 입니다. 제가 느끼기에는 우리 생활에서 가장 많이 사용하고 있는 자료구조가 아닐까? 라는 생각을 합니다.</p>
<p>이 글에서는 스택과 큐의 간단한 개념과 관련된 용어 그리고 실제로 어디서 쓰이고 있는지를 알아보려고 합니다. </p>
<h2 id="1-stack">1. Stack</h2>
<p>Stack의 경우 후입 선출 즉 “<strong>Last In First Out</strong>”, “가장 먼저 들어간 것이 가장 마지막에 나온다”는 알고리즘 입니다. 더 쉽게 이야기 하자면, <strong>차곡차곡 쌓아 올린 구조</strong>라고 할수 있습니다. </p>
<p>JavaScript에서 Array에서 배열에 어떠한 요소를 삽입하는 API로 “push()”를 많이 사용하는데요. 이 연산을 사용하면 기존 배열의 맨 끝에 요소를 추가해줍니다. 또한, pop이라는 메소드를 이용해서 배열의 가장 마지막 요소를 제거 할 수 도 있습니다. 즉, 배열에 이런 요소들을 push하고 pop하는 방식을 스택에서 요소를 삽입하고 삭제하는 연산과 동일 합니다.</p>
<p>정리하자면, 스택을 통해서 요소를 넣고 빼는 것은 한군데 top을 통해서만 가능하고 이렇게 요소를 삽입하고 삭제하는 연산을 push와 pop이라고 부릅니다. </p>
<blockquote>
<p>비어있는 스택에서 원소를 추출할때는 Stack Underflow라고 말하고, 스택이 넘치는 경우를 stack Overflow라고 말합니다. 잘못된 재귀 연산을 통해 끝나지 않은 함수를 무한으로 호출할 경우 JavaScript의 스택오버플로우 에러가 발생하는것과 동일합니다.</p>
</blockquote>
<h4 id="활용-예시">활용 예시</h4>
<ul>
<li>Git Stash ⇒ 변경 사항을 임시로 저장하고 싶을때 사용하는 명령어 인데, 이때 스택 자료구조를 이용해서 저장합니다. git stash pop 명령어를 입력할때, 가장 최근에 삽입한 변경사항이 리턴 되는 것과 동일합니다.</li>
<li>웹 브라우저의 방문 기록 : 웹 브라우저는 페이지를 새로 이동 (push) 했을 때와 뒤로가기(pop)할때 스택 자료구조를 사용합니다.</li>
<li>자바스크립트의 Call Stack 도 이런 스택 자료구조를 이용합니다.</li>
</ul>
<h2 id="1-queue">1. Queue</h2>
<p>큐의 경우 선입 선출 ‘First in First out’로 요소를 삽입, 삭제 합니다. 즉 “먼저 들어간 것이, 가장 처음으로 나온다”라는 의미입니다. </p>
<p>큐에서 요소를 삽입하고 삭제하는 용어는 스택과 다른데요. 삽입 연산 : enQueue라고 부르고, 삭제 연산을 : deQueue라고 합니다. 또한, 삽입이 이루어지는 곳을 Rear(리어), 그리고 삭제가 일어나는 곳을 (Front)라고 합니다. </p>
<h4 id="활용-예시-1">활용 예시</h4>
<ul>
<li>자바스크립트에서 처리해야 할 일을 순서대로 담아 놓는 역할을 하는 태스크 큐가 이런 자료구조를 사용합니다.</li>
<li>우리가 테이블링/ 캐치테이블과 같은 어플에서 식당에 대기줄을 서는 것도 Queue라고 할 수 있습니다.</li>
</ul>
<p>공부하면서, 스택과 큐라는 자료구조가 실제로는 우리 실생활에서 많이 사용되고 있는 형태라는것을 알 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[i18next를 활용한 다국어 지원: 번역 파일을 효율적으로 로드하고 관리하는 방법 (2)]]></title>
            <link>https://velog.io/@gahui_dev/i18next%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90-%EB%B2%88%EC%97%AD-%ED%8C%8C%EC%9D%BC%EC%9D%84-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EB%93%9C%ED%95%98%EA%B3%A0-%EA%B4%80%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-2</link>
            <guid>https://velog.io/@gahui_dev/i18next%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90-%EB%B2%88%EC%97%AD-%ED%8C%8C%EC%9D%BC%EC%9D%84-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EB%93%9C%ED%95%98%EA%B3%A0-%EA%B4%80%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-2</guid>
            <pubDate>Wed, 11 Sep 2024 05:09:56 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 i18next를 사용하여 다국어 지원을 구현하기로 결정한 후, 번역 파일을 어떻게 효율적으로 관리하고 로드할지에 대한 고민이 시작되었습니다. 다국어 지원을 위해 단순히 번역 파일을 작성하는 것만으로는 충분하지 않았고, 각 언어 파일을 어떻게 애플리케이션에 불러오고, 관리할 것인지에 대한 전략이 필요했습니다. 여러 가지 방법을 검토한 끝에, 최종적으로 i18next-http-backend 라이브러리를 사용하기로 결정했으며, 그 과정에서의 고민과 결정을 공유하고자 합니다.</p>
<h3 id="1-고민했던-번역-파일-로드-방법들">1. 고민했던 번역 파일 로드 방법들</h3>
<p>다양한 방법을 비교하면서, 각각의 접근 방식이 어떤 장점과 단점을 가지고 있는지 분석했습니다. 다음은 고려했던 주요 방법들입니다.</p>
<h4 id="1-1-번역-리소스를-코드에-직접-포함하기-inline-resources">1-1. 번역 리소스를 코드에 직접 포함하기 (Inline Resources)</h4>
<p>가장 먼저 떠오른 방법은 번역 리소스를 코드에 직접 포함하는 것이었습니다. 이 방식은 초기 설정이 매우 간단하고, 소규모 프로젝트에서는 빠르게 구현할 수 있다는 장점이 있었습니다. 그러나 번역 데이터가 많아지면 유지보수가 어렵고, 코드가 복잡해진다는 문제가 있었습니. 특히, 번역 파일 구조를 페이지별로 나누어 사용하기 어려워, 필요한 번역 리소스만 로드하는 것이 불가능해진다는 큰 단점이 있었습니다.</p>
<pre><code>import i18n from &#39;i18next&#39;;
import { initReactI18next } from &#39;react-i18next&#39;;

i18n
  .use(initReactI18next)
  .init({
    resources: {
      en: { translation: { hello: &quot;Hello World&quot; } },
      fr: { translation: { hello: &quot;Bonjour le monde&quot; } },
    },
    lng: &#39;en&#39;,
    fallbackLng: &#39;en&#39;,
  });

export default i18n;
</code></pre><h4 id="1-2-번역-파일을-직접-import하여-로드하기">1-2. 번역 파일을 직접 import하여 로드하기</h4>
<p>두 번째로 검토한 방법은 번역 파일을 별도로 JSON 형식으로 관리하고, 이를 코드에서 직접 import하는 방식입니다. 이 방식은 번역 파일을 독립적으로 관리할 수 있어 코드의 가독성이 좋아지고, 쉽게 수정할 수 있다는 장점이 있었습니다. 하지만 프로젝트가 커지면서 많은 파일을 import할 때 초기 로딩 시간이 늘어날 수 있는 단점이 있었습니다.</p>
<pre><code class="language-ts">import i18n from &#39;i18next&#39;;
import { initReactI18next } from &#39;react-i18next&#39;;
import enTranslation from &#39;./locales/en/translation.json&#39;;
import frTranslation from &#39;./locales/fr/translation.json&#39;;

i18n
  .use(initReactI18next)
  .init({
    resources: {
      en: { translation: enTranslation },
      fr: { translation: frTranslation },
    },
    lng: &#39;en&#39;,
    fallbackLng: &#39;en&#39;,
  });

export default i18n;</code></pre>
<h4 id="1-3-custom-backend-구현하기">1-3. custom backend 구현하기</h4>
<p>번역 파일을 서버에서 직접 로드하기 위해 커스텀 백엔드를 구현하는 방법도 고려했습니다. i18next의 백엔드 플러그인 구조를 활용해 HTTP 요청을 직접 관리할 수 있었지만, 이를 위해 추가적인 구현이 필요했습니다. 직접 서버에서 번역 파일을 가져오도록 설정하면 유연하게 관리할 수 있는 장점이 있었지만, 커스텀 로직을 작성해야 해서 복잡성이 증가했습니다.</p>
<pre><code class="language-ts">import i18n from &#39;i18next&#39;;
import { initReactI18next } from &#39;react-i18next&#39;;

const customBackend = {
  type: &#39;backend&#39;,
  read: function (language, namespace, callback) {
    fetch(`/locales/${language}/${namespace}.json`)
      .then((response) =&gt; response.json())
      .then((data) =&gt; callback(null, data))
      .catch((error) =&gt; callback(error, false));
  },
};

i18n
  .use(customBackend)
  .use(initReactI18next)
  .init({
    lng: &#39;en&#39;,
    fallbackLng: &#39;en&#39;,
  });

export default i18n;</code></pre>
<h4 id="1-4-최종-결정-i18next-http-backend-i18next-resources-to-backend라이브러리-사용하기">1-4. 최종 결정: i18next-http-backend, i18next-resources-to-backend라이브러리 사용하기</h4>
<p>위의 방법들을 비교한 결과, 최종적으로 i18next-http-backend 라이브러리를 사용하기로 결정했습니다. 이 라이브러리는 i18next의 공식 백엔드 플러그인으로, 서버에서 번역 파일을 동적으로 가져올 수 있게 해주며, 설정이 간단하면서도 안정적인 동작을 보장합니다. 사용하게 될 라이브러리에 대해서 간략히 설명하겠습니다.</p>
<p><strong>i18next-chained-backend:</strong>
여러 백엔드 플러그인을 체인으로 연결하여 순차적으로 번역 리소스를 가져올 수 있도록 해주는 플러그인입니다. 이를 통해 번역 데이터를 효율적으로 관리하고 우선순위에 따라 번역 리소스를 로드할 수 있습니다.
<strong>i18next-http-backend:</strong></p>
<p>서버에서 번역 파일을 동적으로 가져오는 역할을 합니다. XMLHttpRequest 또는 fetch API를 사용하여 서버와 통신하여 최신 번역 데이터를 유지할 수 있습니다. 이 방식은 새로운 번역 파일이 추가되거나 기존 번역이 수정될 때 유용합니다.
<strong>i18next-resources-to-backend:</strong></p>
<p>클라이언트 내부에 포함된 번역 리소스를 먼저 로드하는 역할을 합니다. 이를 통해 초기 로딩 시간을 단축하고, 네트워크 요청을 최소화할 수 있습니다. 브라우저에서 번역 파일을 직접 로드하기 때문에 오프라인 상태에서도 번역 기능을 사용할 수 있는 장점이 있습니다.</p>
<pre><code>pnpm add i18next-chained-backend i18next-http-backend i18next-resources-to-backend</code></pre><h3 id="usetranslation-커스텀-훅-생성">useTranslation 커스텀 훅 생성</h3>
<pre><code class="language-ts">import {
  createInstance,
  FlatNamespace,
  i18n as I18nInstance,
  InitOptions,
  Resource,
  KeyPrefix as I18nKeyPrefix,
} from &quot;i18next&quot;;
import i18nConfig, { Locale } from &quot;./i18nConfig&quot;;
import { initReactI18next } from &quot;react-i18next/initReactI18next&quot;;
import ChainedBackend from &quot;i18next-chained-backend&quot;;
import HttpBackend from &quot;i18next-http-backend&quot;;
import resourcesToBackend from &quot;i18next-resources-to-backend&quot;;

function addBackendResourceLoader(i18nInstance: I18nInstance) {
  i18nInstance.use(ChainedBackend);
}

function createInitOptions({
  locale,
  namespaces,
  resources,
  options,
}: {
  locale: Locale;
  namespaces: FlatNamespace[];
  resources?: Resource;
  options?: InitOptions;
}): InitOptions {
  return {
    lng: locale,
    resources,
    fallbackLng: i18nConfig.defaultLocale,
    supportedLngs: i18nConfig.locales,
    defaultNS: namespaces[0],
    fallbackNS: namespaces[0],
    ns: namespaces.map((ns) =&gt; ns.toString()),
    preload: resources ? [] : i18nConfig.locales,
    backend: {
      backends: [
        HttpBackend,
        resourcesToBackend(
          (language: Locale, namespace: string) =&gt;
            import(`@/app/i18n/locales/${language}/${namespace}.json`)
        ),
      ],
      backendOptions: [
        {
          load: (language: Locale, namespace: string) =&gt;
            import(`@/app/i18n/locales/${language}/${namespace}.json`),
        },
      ],
    },
    ...options,
  };
}

interface UseTranslationsProps&lt;
  KPrefix extends I18nKeyPrefix&lt;string&gt; = undefined
&gt; {
  locale: Locale;
  namespaces: FlatNamespace[];
  i18nInstance?: I18nInstance;
  resources?: Resource;
  options?: InitOptions;
  keyPrefix?: KPrefix;
}
interface UseTranslationsResult {
  i18n: I18nInstance;
  resources: Resource;
  t: I18nInstance[&quot;t&quot;];
}

export default async function useTranslation&lt;
  KPrefix extends I18nKeyPrefix&lt;string&gt; = undefined
&gt;({
  locale,
  namespaces,
  i18nInstance = createInstance(),
  resources,
  keyPrefix,
  options,
}: UseTranslationsProps&lt;KPrefix&gt;): Promise&lt;UseTranslationsResult&gt; {
  i18nInstance.use(initReactI18next);

  // resource가 없을경우, 백엔드에서 가져오도록 설정
  if (!resources) {
    addBackendResourceLoader(i18nInstance);
  }
  //i18n 초기화 옵션 설정
  const initOptions = createInitOptions({
    locale,
    namespaces,
    resources,
    options,
  });

  await i18nInstance.init(initOptions);

  return {
    i18n: i18nInstance,
    resources: i18nInstance.services.resourceStore.data,
    t: i18nInstance.getFixedT(locale, namespaces[0], keyPrefix),
  };
}</code></pre>
<ul>
<li>locale과 namespaces를 기반으로 i18next 인스턴스를 생성합니다.</li>
<li>백엔드 설정을 통해 번역 파일을 서버 또는 클라이언트에서 적절히 로드합니다.</li>
<li>초기화 옵션을 설정하여 i18next를 초기화하고, 번역 인스턴스와 번역 함수를 반환합니다.</li>
</ul>
<p>서버 컴포넌트에서는 다음과 같이 작성하면 됩니다. </p>
<pre><code class="language-tsx">import useTranslation from &quot;@/app/i18n&quot;;
import { Locale } from &quot;@/app/i18n/i18nConfig&quot;;
interface FooterProps {
  locale: Locale;
}

const Footer = async ({ locale }: FooterProps) =&gt; {
  const { t } = await useTranslation({
    namespaces: [&quot;common&quot;],
    locale,
  });
  return (
    &lt;footer className=&quot;bg-gray-800 text-white py-4&quot;&gt;
      &lt;div className=&quot;container mx-auto px-4&quot;&gt;
        &lt;p className=&quot;text-center&quot;&gt;{t(&quot;footer&quot;)}&lt;/p&gt;
      &lt;/div&gt;
    &lt;/footer&gt;
  );
};

export default Footer;</code></pre>
<h3 id="결론">결론</h3>
<p>이번 포스팅에서는 i18next를 활용하여 다국어 지원을 구현하는 과정에서 번역 파일을 효율적으로 관리하고 로드하는 다양한 방법을 고민하고, 최종적으로 i18next-http-backend와 i18next-resources-to-backend 라이브러리를 사용하기로 결정한 배경을 공유했습니다.</p>
<p>이 방법은 서버와 클라이언트 모두에서 번역 파일을 동적으로 관리할 수 있게 해주며, 초기 로딩 시간을 최적화하고 네트워크 요청을 최소화하는 장점이 있습니다. 또한, 번역 파일이 변경되거나 업데이트될 때 유연하게 대응할 수 있어 확장성과 유지보수 측면에서도 좋은 선택이었습니다.</p>
<p>이를 통해 프로젝트에서 다국어 지원을 효율적으로 구축할 수 있었으며, 커스텀 훅을 통해 번역 파일 로드와 초기화를 간편하게 처리할 수 있는 방안을 제시했습니다.</p>
<p>다음 포스팅에서는 Context API를 활용하여 클라이언트 컴포넌트에서 번역 데이터를 관리하는 방법에 대해 다룰 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[마이크로 상태 관리 정리]]></title>
            <link>https://velog.io/@gahui_dev/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@gahui_dev/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 09 Sep 2024 05:41:55 GMT</pubDate>
            <description><![CDATA[<h3 id="상태state란-무엇인가"><strong>상태(state)란 무엇인가?</strong></h3>
<p><strong>상태(state)</strong>는 컴포넌트의 렌더링과 UI의 동작을 결정하는 동적인 데이터를 말합니다. 예를 들어, 사용자가 입력한 값, 버튼 클릭 여부, API로부터 받은 응답 등이 상태에 포함됩니다. 상태는 시간이 지나면서 변화하며, 그 변화에 따라 UI가 다시 렌더링됩니다. 이 동적인 특성이 바로 상태의 핵심입니다.</p>
<h4 id="데이터data와-상태state의-차이점"><strong>데이터(data)와 상태(state)의 차이점</strong></h4>
<ul>
<li><strong>데이터(data):</strong> 변화하지 않는 고정된 값이나 외부에서 주어진 값입니다. 예를 들어, 서버로부터 가져온 JSON 응답, 정적인 텍스트, 기본 설정 값 등이 이에 해당합니다.</li>
<li><strong>상태(state):</strong> 특정 시점에서 UI와 상호작용하면서 변화할 수 있는 데이터입니다. 상태는 시간에 따라 달라지고, 사용자 인터랙션, API 호출, 이벤트 등에 반응하여 변경됩니다.</li>
</ul>
<h3 id="왜-데이터가-아니라-상태로-정의하는가"><strong>왜 &#39;데이터&#39;가 아니라 &#39;상태&#39;로 정의하는가?</strong></h3>
<ol>
<li><strong>동적인 변화:</strong> 상태는 사용자 인터랙션에 따라 변화하며 UI의 변화를 유발합니다. 이는 데이터가 가지지 않는 동적인 특성입니다.</li>
<li><strong>컴포넌트의 책임:</strong> 상태는 특정 컴포넌트가 관리하는 정보로, 그 컴포넌트의 책임과 동작을 정의합니다. 즉, 상태는 컴포넌트가 &#39;어떻게 행동할지&#39;를 결정하는 중요한 요소입니다.</li>
<li><strong>렌더링과 연결:</strong> 상태가 변하면 React는 그에 맞춰 UI를 자동으로 다시 렌더링합니다. 반면, 데이터는 이러한 자동적이고 직접적인 UI 변화와는 연결되어 있지 않습니다.</li>
<li><strong>UI와 상호작용:</strong> 상태는 사용자의 행동, 컴포넌트 간의 상호작용 등과 밀접하게 연관되어 있어 UI의 현재 상태를 반영합니다.</li>
</ol>
<h3 id="전통적인-상태-관리-방식---중앙-집중적인-방식"><strong>전통적인 상태 관리 방식 - 중앙 집중적인 방식</strong></h3>
<p>전통적인 상태 관리 방식은 애플리케이션의 전체 상태를 한 곳에서 중앙 집중적으로 관리하여, 상태의 일관성과 데이터 흐름을 유지하려는 접근을 의미합니다. 이 방식은 특히 규모가 큰 애플리케이션에서 상태 관리의 복잡성을 줄이기 위해 사용되며, 대표적으로 Redux, MobX, Context API 등이 있습니다.</p>
<h4 id="장점"><strong>장점</strong></h4>
<ul>
<li><strong>예측 가능성:</strong> 상태가 중앙에서 관리되므로, 상태의 흐름과 변화를 예측하고 추적하기 쉽습니다.</li>
<li><strong>일관된 데이터 흐름:</strong> 단방향 데이터 흐름으로 인해 데이터가 어떤 경로로 이동하고 변하는지 명확하게 이해할 수 있습니다.</li>
</ul>
<h4 id="단점"><strong>단점</strong></h4>
<ul>
<li><strong>복잡한 설정:</strong> 초기 설정이 복잡하고, 보일러플레이트 코드가 많아 코드 작성이 번거롭습니다.</li>
<li><strong>성능 문제:</strong> 모든 상태가 중앙에서 관리되다 보니, 불필요한 리렌더링이나 성능 저하가 발생할 수 있습니다.</li>
<li><strong>작은 프로젝트에 부담:</strong> 단순한 애플리케이션에는 과도한 도입이 될 수 있습니다.</li>
</ul>
<h3 id="마이크로-상태-관리란"><strong>마이크로 상태 관리란?</strong></h3>
<p><strong>마이크로 상태 관리</strong>는 개별 컴포넌트 또는 컴포넌트 그룹 내에서 필요한 작은 상태를 관리하는 접근 방식을 의미합니다. React의 기본 훅(<code>useState</code>, <code>useReducer</code> 등)을 사용하여 필요한 부분에서만 상태를 관리하는 것을 강조합니다.</p>
<h4 id="마이크로-상태-관리의-필요성"><strong>마이크로 상태 관리의 필요성</strong></h4>
<ol>
<li><strong>폼 상태의 특수성:</strong> 폼 상태는 입력 필드의 값, 오류, 유효성 검사 결과 등 컴포넌트 내에서만 중요한 데이터를 관리합니다. 전역 상태로 관리할 경우, 잦은 상태 변경으로 인해 불필요한 리렌더링과 성능 저하가 발생할 수 있습니다.</li>
<li><strong>서버 캐시 상태의 특성:</strong> 서버 캐시 상태는 서버로부터 받아온 데이터를 클라이언트에 저장하고, 필요할 때 이를 재사용하거나 업데이트합니다. 이러한 상태는 데이터의 동기화, 유효성, 리패칭 등의 고유한 관리 전략이 필요하며, 일반적인 전역 상태와 동일하게 처리하는 것은 부적합합니다.</li>
<li><strong>네비게이션 상태의 특수성:</strong> 네비게이션 상태는 브라우저의 URL, 히스토리, 현재 페이지의 위치 등과 관련된 상태로, 브라우저가 직접 관리하는 영역입니다. 이 상태를 전역 상태로 관리하면 복잡한 설정이 필요하고 브라우저의 기본 동작과 충돌할 수 있습니다.</li>
</ol>
<h3 id="리액트-훅을-이용한-마이크로-상태-관리"><strong>리액트 훅을 이용한 마이크로 상태 관리</strong></h3>
<p>React 훅을 사용하면, 컴포넌트 내부에서 상태를 관리하고, 필요에 따라 사용자 정의 훅을 만들어 상태 관리 로직을 재사용할 수 있습니다.</p>
<ol>
<li><strong><code>useState</code>:</strong> 컴포넌트 내부에서 상태를 생성하고 관리할 수 있습니다. 사용자 정의 훅을 만들어 로직을 재사용할 수 있습니다.</li>
<li><strong><code>useReducer</code>:</strong> 복잡한 상태 관리가 필요한 경우, <code>useReducer</code>를 사용하여 상태 관리 로직을 캡슐화할 수 있습니다.</li>
<li><strong><code>useEffect</code>:</strong> 컴포넌트의 생명 주기와 함께 작동하는 비동기 로직을 실행할 수 있습니다.</li>
</ol>
<h4 id="예시-폰-번호-입력-폼"><strong>예시: 폰 번호 입력 폼</strong></h4>
<p>폰 번호 입력 필드에서 사용자가 입력한 값을 실시간으로 포맷팅하여 보여주는 예시를 생각해 봅시다. 이 로직을 반복적으로 사용해야 할 경우, 사용자 정의 훅을 만드는 것이 유리합니다.</p>
<pre><code class="language-jsx">// 사용자 정의 훅: usePhoneNumber
import { useState } from &#39;react&#39;;

function usePhoneNumber(initialValue = &#39;&#39;) {
  const [phoneNumber, setPhoneNumber] = useState(initialValue);

  // 입력 값을 포맷팅하고 상태를 변경하는 함수
  const handlePhoneNumberChange = (value) =&gt; {
    value = value.replace(/[^0-9]/g, &#39;&#39;); // 숫자만 남기기
    if (value.length &gt; 3 &amp;&amp; value.length &lt;= 6) {
      value = value.replace(/(\d{3})(\d+)/, &#39;$1-$2&#39;); // 중간에 하이픈 추가
    } else if (value.length &gt; 6) {
      value = value.replace(/(\d{3})(\d{3})(\d+)/, &#39;$1-$2-$3&#39;); // 전체 형식화
    }
    setPhoneNumber(value);
  };

  // 상태와 포맷팅 함수를 반환
  return [phoneNumber, handlePhoneNumberChange];
}

export default usePhoneNumber;

// 컴포넌트에서 사용
import React from &#39;react&#39;;
import usePhoneNumber from &#39;./usePhoneNumber&#39;;

function PhoneNumberForm() {
  const [phoneNumber, setPhoneNumber] = usePhoneNumber();

  return (
    &lt;div&gt;
      &lt;input
        type=&quot;text&quot;
        value={phoneNumber}
        onChange={(e) =&gt; setPhoneNumber(e.target.value)}
        placeholder=&quot;Enter your phone number&quot;
      /&gt;
      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
    &lt;/div&gt;
  );
}

export default PhoneNumberForm;</code></pre>
<p>React에서 전역 상태를 구현하는 것이 간단하지 않은 이유는 React의 철학과 구조에서 기인합니다. React는 컴포넌트 기반 아키텍처를 중심으로 설계되었으며, 각 컴포넌트가 자신의 상태를 관리하도록 권장합니다. 전역 상태 관리는 프로젝트가 커질수록 더욱 복잡해지기 때문에, 이를 효과적으로 다루기 위해서는 올바른 전략과 도구를 사용하는 것이 중요합니다.</p>
<h3 id="전역-상태-탐구하기"><strong>전역 상태 탐구하기</strong></h3>
<p>React에서 전역 상태를 관리하는 것은 단순한 작업이 아닙니다. React의 기본적인 상태 관리 방식은 <code>useState</code>와 <code>useReducer</code> 같은 훅을 사용해 개별 컴포넌트에서 상태를 관리하는 것이며, 이는 React가 권장하는 방향이기도 합니다. 컴포넌트가 전역 상태에 의존하지 않는 것이 이상적이며, 다음과 같은 이유에서 전역 상태 구현은 주의가 필요합니다:</p>
<ol>
<li><strong>컴포넌트의 독립성 유지:</strong> 각 컴포넌트는 독립적으로 동작하도록 설계되어야 합니다. 전역 상태에 과도하게 의존하게 되면, 컴포넌트 간의 결합도가 높아져 유지보수가 어려워질 수 있습니다.</li>
<li><strong>상태의 예측 가능성 감소:</strong> 전역 상태는 전체 애플리케이션에 걸쳐 영향을 미치기 때문에, 상태 변화를 추적하고 관리하기 어려워질 수 있습니다. 이는 특히 애플리케이션이 복잡해질수록 문제가 됩니다.</li>
<li><strong>리렌더링 최적화의 어려움:</strong> 전역 상태의 변화는 많은 컴포넌트에 영향을 줄 수 있으며, 불필요한 리렌더링이 발생할 수 있습니다. 이는 성능 저하로 이어질 수 있습니다.</li>
</ol>
<p>이러한 이유로 전역 상태 관리를 구현할 때는 React의 기본 훅을 적절히 활용하여 상태 관리를 분리하거나, Context API, Redux, Zustand와 같은 전역 상태 관리 라이브러리를 사용하는 것이 일반적입니다.</p>
<h3 id="usestate와-베일아웃bailout"><strong><code>useState</code>와 베일아웃(Bailout)</strong></h3>
<p><strong>베일아웃</strong>은 React의 상태 업데이트 최적화 기술로, 상태가 업데이트되었지만 실제 값이 변하지 않은 경우, 리렌더링을 하지 않도록 하는 메커니즘입니다. 이는 성능 최적화에 중요한 역할을 하며, 상태 변화가 없을 때 불필요한 렌더링을 방지할 수 있습니다.</p>
<h4 id="usestate에서의-베일아웃-동작-방식"><strong><code>useState</code>에서의 베일아웃 동작 방식</strong></h4>
<ul>
<li><p><strong>값으로 상태 갱신하기:</strong></p>
<ul>
<li><p><code>useState</code>에서 상태를 업데이트할 때 새로운 값이 이전 값과 같다면, React는 리렌더링을 건너뜁니다.</p>
<pre><code class="language-jsx">const Component = () =&gt; {
  const [count, setCount] = useState(0);

  return (
    &lt;div&gt;
      {count}
      &lt;button onClick={() =&gt; setCount(1)}&gt;click&lt;/button&gt;
    &lt;/div&gt;
  );
};</code></pre>
<p>위 예제에서 <code>count</code>가 이미 <code>1</code>인 상태에서 버튼을 클릭해도 상태가 실제로 변경되지 않기 때문에, React는 리렌더링을 수행하지 않습니다. 이를 베일아웃이라고 합니다.</p>
</li>
</ul>
</li>
<li><p><strong>객체나 배열로 상태 갱신하기:</strong></p>
<ul>
<li><p>만약 객체나 배열을 상태로 관리하는 경우, 새로운 객체가 이전 객체와 참조적으로 동일하지 않다면, React는 리렌더링을 수행합니다.</p>
<pre><code class="language-jsx">const Component = () =&gt; {
  const [state, setState] = useState({ count: 0 });

  return (
    &lt;div&gt;
      {state.count}
      &lt;button onClick={() =&gt; setState({ count: 1 })}&gt;click&lt;/button&gt;
    &lt;/div&gt;
  );
};</code></pre>
<p>여기서는 <code>{ count: 1 }</code>이라는 새로운 객체가 매번 생성되므로, 리렌더링이 일어나며 베일아웃이 발생하지 않습니다. 이는 객체의 참조가 변경되기 때문입니다.</p>
</li>
</ul>
</li>
<li><p><strong>함수형 업데이트:</strong></p>
<ul>
<li><p>상태를 함수형으로 업데이트하면, 이전 상태를 기반으로 새로운 상태를 계산할 수 있어 정확한 베일아웃이 가능합니다.</p>
<pre><code class="language-jsx">&lt;button onClick={() =&gt; setCount((prevCount) =&gt; prevCount + 1)}&gt;
  Increment Count
&lt;/button&gt;</code></pre>
<p>이 방식은 특히 비동기적으로 여러 업데이트가 이루어질 때 유용하며, 상태 관리의 예측 가능성을 높입니다.</p>
</li>
</ul>
</li>
</ul>
<h3 id="usereducer의-지연-초기화와-상태-관리"><strong><code>useReducer</code>의 지연 초기화와 상태 관리</strong></h3>
<p><code>useReducer</code>는 <code>useState</code>의 대안으로, 상태가 복잡하거나 여러 액션에 의해 변경되어야 할 때 유용하게 사용됩니다. <code>useReducer</code>는 상태와 업데이트 로직을 분리할 수 있어, 로직이 명확하고 유지보수가 용이해집니다.</p>
<h4 id="지연-초기화-lazy-initialization"><strong>지연 초기화 (Lazy Initialization)</strong></h4>
<p>지연 초기화는 <code>useReducer</code>에서 상태 초기화 비용이 큰 경우에 사용됩니다. 상태를 초기화하는 함수는 첫 번째 렌더링에서만 호출되며 이후에는 호출되지 않아 성능을 최적화할 수 있습니다.</p>
<ul>
<li><p><strong>초기화 함수 사용:</strong></p>
<pre><code class="language-jsx">const init = (initialCount) =&gt; {
  return { count: initialCount, text: &#39;hi&#39; };
};

const reducer = (state, action) =&gt; {
  switch (action.type) {
    case &#39;INCREMENT&#39;:
      return { ...state, count: state.count + 1 };
    case &#39;SET_TEXT&#39;:
      return { ...state, text: action.text || state.text };
    default:
      return state;
  }
};

function Counter() {
  const [state, dispatch] = useReducer(reducer, 0, init);

  return (
    &lt;div&gt;
      Count: {state.count}
      &lt;button onClick={() =&gt; dispatch({ type: &#39;INCREMENT&#39; })}&gt;Increment&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>위 코드에서 <code>init</code> 함수는 <code>useReducer</code>의 세 번째 인자로 전달되어, 초기 상태를 설정하는 데 사용됩니다. 이 방법은 초기화가 복잡하거나 성능이 중요한 상황에서 매우 유용합니다.</p>
</li>
</ul>
<h4 id="usereducer와-usestate의-비교"><strong><code>useReducer</code>와 <code>useState</code>의 비교</strong></h4>
<ul>
<li><strong><code>useState</code>:</strong> 간단한 상태 관리에 적합하며, 사용하기 쉽고 직관적입니다. 내부적으로는 <code>useReducer</code>를 기반으로 구현되어 있습니다.</li>
<li><strong><code>useReducer</code>:</strong> 복잡한 상태 관리에 적합하며, 상태 업데이트 로직을 컴포넌트 외부로 분리할 수 있습니다. 이를 통해 코드의 가독성과 유지보수성을 높일 수 있습니다.</li>
</ul>
<h3 id="usestate와-usereducer의-내부-구현-비교"><strong><code>useState</code>와 <code>useReducer</code>의 내부 구현 비교</strong></h3>
<p>React는 <code>useState</code>를 내부적으로 <code>useReducer</code>를 사용하여 구현합니다. <code>useReducer</code>의 동작을 간단히 살펴보면, 상태 업데이트 로직이 함수형으로 정의되고, 액션에 따라 상태를 업데이트합니다.</p>
<pre><code class="language-jsx">const useState = (initialState) =&gt; {
  const [state, dispatch] = useReducer((prevState, action) =&gt; {
    return typeof action === &#39;function&#39; ? action(prevState) : action;
  }, initialState);

  return [state, dispatch];
};</code></pre>
<p>이러한 구조는 <code>useState</code>가 <code>useReducer</code>의 단순화된 버전임을 보여줍니다. <code>useState</code>는 주로 단순한 상태를 다루기 위한 것이고, <code>useReducer</code>는 복잡한 상태 업데이트 로직을 처리하기 위해 설계되었습니다.</p>
<h3 id="결론"><strong>결론</strong></h3>
<ul>
<li><strong>전역 상태 관리의 어려움:</strong> React는 컴포넌트가 독립적으로 상태를 관리하도록 설계되어 있으며, 전역 상태는 적절한 도구와 전략이 없으면 복잡성을 초래할 수 있습니다.</li>
<li><strong><code>useState</code>와 베일아웃:</strong> 값이 변경되지 않을 때 리렌더링을 방지하는 최적화로, 성능 개선에 중요한 역할을 합니다.</li>
<li><strong><code>useReducer</code>의 지연 초기화:</strong> 복잡한 상태 관리에서 성능을 최적화할 수 있는 방법이며, 상태 초기화 비용을 줄이는 데 유용합니다.</li>
<li><strong>상태 관리의 선택:</strong> 애플리케이션의 복잡도에 따라 <code>useState</code>와 <code>useReducer</code>를 적절히 선택하고, 필요할 경우 전역 상태 관리 도구를 도입하여 상태 관리의 일관성과 성능을 유지하는 것이 중요합니다.</li>
</ul>
<p>이러한 개념들을 통해 React에서 상태 관리의 본질과 각각의 훅의 역할을 이해하고, 프로젝트에 맞는 최적의 상태 관리 방법을 선택하는 데 도움이 될 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발자님 저희 사이트가 검색이 안돼요 - 
Next.js App Router에 i18next 다국어 적용하기 (1)]]></title>
            <link>https://velog.io/@gahui_dev/Next.js-App-Router%EC%97%90-i18next-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@gahui_dev/Next.js-App-Router%EC%97%90-i18next-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Sat, 07 Sep 2024 06:51:06 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요~! 이번 글에서는 Next.js로 마이그레이션을 결정하게 된 계기와 그 방법에 대해 소개해드리려고 합니다.</p>
<p>해당 프로젝트는 회사에 대한 소개 위주의 글로벌 홈페이지로, 처음에는 Vue 3의 CSR(Client-Side Rendering) 방식을 사용해 빠르게 구축되었습니다. 당시 제한된 개발 기간으로 인해 Vue 3라는 프레임워크를 사용하게 되었습나더. Vue3 프레임워크는 빠른 개발을 가능하게 해줬고, 복잡한 기능이 없는 단순한 소개 사이트로는 충분하다고 생각했습니다.</p>
<p>그러나 사이트 운영이 지속되면서 CSR 방식의 몇 가지 한계가 눈에 띄기 시작했습니다:</p>
<p>첫째, <strong>초기 로딩 속도 문제</strong>입니다. CSR 특성상 모든 JavaScript 파일이 클라이언트 측에서 실행되다 보니, 첫 화면이 로딩되는 데 시간이 걸렸습니다. 이로 인해 사용자가 첫 화면을 보기까지 기다려야 하는 시간이 길어졌고, 이탈률이 높아지는 문제를 겪게 되었습니다.</p>
<p>둘째, <strong>SEO 최적화의 어려움</strong>입니다. CSR 방식에서는 초기 HTML이 거의 비어있고, 모든 콘텐츠가 클라이언트 측에서 렌더링되기 때문에 검색 엔진이 페이지를 제대로 인덱싱하지 못했습니다. 그 결과, 검색 엔진 결과에서 우리 사이트의 노출도가 낮아지는 문제가 발생했습니다.</p>
<p>이런 문제들을 해결하기 위해 Next.js 14로의 마이그레이션을 결정했습니다. Next.js는 Server-Side Rendering(SSR)과 Static Site Generation(SSG)을 지원하여 초기 로딩 속도를 크게 개선할 수 있었고, SEO 친화적인 구조로 사이트가 검색 엔진에 더 잘 노출될 수 있도록 해줬습니다. 또한, Next.js 14의 최신 기능인 App Router와 i18next 통합을 통해 더욱 빠르고 유연한 다국어 지원이 가능해졌습니다.</p>
<p>이 글에서는 Next.js로의 전환 과정을 설명드리겠습니다.</p>
<h3 id="1-i18next-라이브러리-사용">1. i18next 라이브러리 사용</h3>
<p>Next.js 14로의 전환에서 중요한 부분 중 하나는 다국어 지원이었습니다. 기존 Vue 3 프로젝트에서는 i18n이라는 다른 국제화 라이브러리를 사용했지만, Next.js로 전환하면서 i18next와 react-i18next를 사용하기로 했습니다. i18next는 Next.js의 App Router와 잘 맞물려 작동하며, 서버 사이드와 클라이언트 사이드 모두에서 안정적으로 다국어 처리를 지원해주기 때문에 선택하게 되었습니다.</p>
<pre><code>$ pnpm install react-i18next i18next --save</code></pre><h3 id="2-파일구조">2. 파일구조</h3>
<pre><code>project-root/
│
├── app/
│   ├── [locale]/
│   │   ├── page.tsx
│   │   └── layout.tsx
│   │
│   └── i18n/
│       ├── locales/
│       │   ├── en / common.json
│       │   └── ko / common.json
│       └── index.ts
│
└── package.json
</code></pre><h4 id="1-파일-설명">1. 파일 설명</h4>
<pre><code>app/[locale]/page.tsx
</code></pre><p>이 파일은 각 언어별로 페이지를 구성하는 파일입니다. [locale]은 URL 경로에서 사용되는 언어 코드로, 예를 들어 en이나 ko가 될 수 있습니다. 이 구조 덕분에 URL 경로에 따라 자동으로 해당 언어 페이지를 렌더링할 수 있습니다.</p>
<pre><code>app/i18n/locales/
</code></pre><p>언어별 번역 파일은 이곳에 위치합니다. 번역 파일은 각각의 언어와 관련된 모든 페이지와 레이아웃에서 사용되며, 다국어 지원의 핵심을 담당합니다.</p>
<p>*<em>왜 이런 구조를 선택했을까요? *</em></p>
<ul>
<li>언어별 관리의 효율성: app/[locale]/ 구조를 통해 언어별로 페이지와 레이아웃을 분리함으로써, 각 언어에 맞는 디자인이나 콘텐츠를 쉽게 적용하고 관리할 수 있습니다.</li>
<li>SEO 및 사용자 경험 향상: URL 경로에 언어 코드를 포함시키는 구조로, 검색 엔진이 각 언어 페이지를 정확하게 인식할 수 있게 하여 SEO에 긍정적인 영향을 미칩니다.</li>
<li>유지보수의 용이성: 언어별로 독립된 파일을 사용해 수정이 필요한 경우 해당 언어의 파일만 업데이트하면 되어 유지보수가 쉬워집니다.</li>
</ul>
<blockquote>
<p>component 폴더의 구조는 Atomic 디자인패턴을 도입했으나 이 글의 목적과는 맞지 않아 설명은 생략하겠습니다.</p>
</blockquote>
<h3 id="2-동적-라우팅을-위한-설정">2. 동적 라우팅을 위한 설정</h3>
<p>웹사이트를 다국어로 제공할 때 사용자 경험을 최적화하기 위해 가장 중요한 요소 중 하나는 사용자의 브라우저 언어를 감지하여 올바른 언어 페이지로 리디렉션하는 것입니다. Next.js 14와 App Router를 사용하여 기본 URL로 접근할 때 사용자의 브라우저 언어를 감지하고, 올바른 언어 페이지로 자동으로 리디렉션하는 방법에 대해서 설명하겠습니다.</p>
<h4 id="원하는-동작-방식">원하는 동작 방식</h4>
<p>저희 팀이 원하는 동작 방식은 다음과 같았습니다. </p>
<ol>
<li>기본 언어 설정: 기본 언어는 en(영어)로 설정합니다. 모든 페이지는 언어 코드가 포함된 URL로 접근하게 됩니다. 
 예를 들어:</li>
</ol>
<ul>
<li>영어 페이지: localhost:3000/en</li>
<li>프랑스어 페이지: localhost:3000/fr</li>
</ul>
<ol start="2">
<li>브라우저 언어 감지를 통한 리디렉션:</li>
</ol>
<ul>
<li>사용자가 localhost:3000과 같이 언어 코드가 없는 기본 URL로 접근할 때, 서버 측 미들웨어가 실행되어 브라우저의 언어 설정을 감지합니다.</li>
<li>감지된 언어가 fr이면 localhost:3000/fr로 리디렉션하고, 지원되지 않는 언어라면 기본 언어인 en으로 리디렉션합니다.</li>
</ul>
<h4 id="2-1--i18nconfig-파일-생성">2-1)  i18nConfig 파일 생성</h4>
<pre><code>// app/i18n/i18nConfig.ts
// 지원하는 언어 코드 타입 정의
export type Locale = &quot;en&quot; | &quot;fr&quot; | &quot;th&quot;; // 지원하는 언어 코드 목록

// 설정 타입 정의
interface I18nConfig {
  locales: Locale[]; // 지원하는 언어 목록
  defaultLocale: Locale; // 기본 언어 설정
}

// 설정 객체
const i18nConfig: I18nConfig = {
  locales: [&quot;en&quot;, &quot;fr&quot;, &quot;th&quot;], // Locale 타입만 허용됨
  defaultLocale: &quot;en&quot;,
};

export default i18nConfig;
</code></pre><h4 id="2-2--미들웨어-설정에서-i18nconfig-사용하기">2-2 ) 미들웨어 설정에서 i18nConfig 사용하기</h4>
<p>이제 이 i18nConfig를 미들웨어에서 사용하여 동적 라우팅을 처리하도록 설정을 수정합니다. i18nConfig를 import하여 지원 언어와 기본 언어를 간편하게 사용할 수 있게 됩니다.</p>
<pre><code>// middleware.ts
import { NextRequest, NextResponse } from &quot;next/server&quot;;
import i18nConfig, { Locale } from &quot;./app/i18n/i18nConfig&quot;; // i18nConfig 파일 import

export function middleware(request: NextRequest) {
  // 현재 URL 경로
  const { pathname } = request.nextUrl;

  // 이미 언어가 경로에 포함된 경우
  if (i18nConfig.locales.some((locale) =&gt; pathname.startsWith(`/${locale}`))) {
    return NextResponse.next();
  }

  // 쿠키에서 선호하는 언어를 가져옴
  const preferredLocale = request.cookies.get(&quot;preferredLocale&quot;)?.value;

  // 브라우저 언어를 감지 (쿠키에 값이 없을 때만 사용)
  const browserLanguage = request.headers
    .get(&quot;accept-language&quot;)
    ?.split(&quot;,&quot;)[0]
    .slice(0, 2);

  // 타입 가드를 통해 Locale 타입으로 변환
  const isLocale = (lang: string | undefined): lang is Locale =&gt;
    lang !== undefined &amp;&amp; i18nConfig.locales.includes(lang as Locale);

  // 쿠키 언어 &gt; 브라우저 언어 &gt; 기본 언어 순으로 결정
  const locale: Locale = isLocale(preferredLocale)
    ? preferredLocale
    : isLocale(browserLanguage)
    ? (browserLanguage as Locale)
    : i18nConfig.defaultLocale;

  // 언어에 맞는 경로로 리디렉션
  return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}

export const config = {
  matcher: [&quot;/((?!api|_next|static|.*\\..*|favicon.ico).*)&quot;],
};
</code></pre><p>middleware에서 원하는 동작은 다음과 같았습니다.</p>
<ol>
<li><strong>언어가 URL에 포함된 경우</strong>: 사용자가 이미 /en, /fr 등의 경로로 접근했을 때는 별도의 리디렉션 없이 해당 언어 페이지를 그대로 보여줍니다.</li>
<li><strong>언어가 URL에 포함되지 않은 경우</strong>: localhost:3000과 같이 언어 코드가 없는 URL로 접근했을 때:</li>
</ol>
<ul>
<li>우선, 쿠키에서 사용자가 설정한 언어(preferredLocale)가 있는지 확인합니다.</li>
<li>쿠키에 언어가 없을 경우, 브라우저의 Accept-Language 헤더에서 언어를 감지합니다.</li>
<li>두 방법 모두 실패하면 기본 언어인 en으로 설정합니다.</li>
</ul>
<h4 id="왜-이렇게-구현했는가">왜 이렇게 구현했는가?</h4>
<ol>
<li>사용자 경험 개선: 사용자 설정을 우선시하고, 사용자가 언어를 변경하지 않은 경우에도 적절한 언어로 페이지를 제공하여 일관된 경험을 유지합니다.</li>
<li>유연한 언어 감지: 쿠키와 브라우저 설정을 유연하게 사용하여 다양한 사용자의 환경에 대응할 수 있도록 설계되었습니다.</li>
<li>SEO 최적화: 각 언어가 고유한 URL을 가지므로, 검색 엔진이 각 언어 페이지를 정확하게 인덱싱할 수 있어 SEO에 긍정적인 영향을 줍니다.</li>
</ol>
<p>이번 포스팅에서는 Next.js에서 미들웨어를 활용해 사용자의 언어를 감지하고 적절한 페이지로 리디렉션하는 방법을 다뤄봤습니다. 이를 통해 더 나은 사용자 경험을 제공하고, SEO 성능을 최적화할 수 있는 방법을 확인할 수 있었습니다.</p>
<p>다음 포스팅에서는 클라이언트 컴포넌트와 서버 컴포넌트에서 i18n을 적용하는 방법에 대해 설명하겠습니다. 특히, Next.js의 최신 기능을 활용하여 효율적으로 다국어를 처리하는 방법과 Context API를 이용해 글로벌 상태를 관리하는 방식에 대해 깊이 있게 다룰 예정입니다.</p>
]]></description>
        </item>
    </channel>
</rss>