<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kxun_ii.log</title>
        <link>https://velog.io/</link>
        <description>Frontend Developer 😆 | PM</description>
        <lastBuildDate>Tue, 07 Apr 2026 09:44:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>kxun_ii.log</title>
            <url>https://velog.velcdn.com/images/kxun_ii/profile/e94072d0-6e58-464f-95c8-cd7d2e8a477c/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. kxun_ii.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kxun_ii" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[새로고침해도 음악이 이어진다 (feat. IndexedDB) - 5]]></title>
            <link>https://velog.io/@kxun_ii/%EC%83%88%EB%A1%9C%EA%B3%A0%EC%B9%A8%ED%95%B4%EB%8F%84-%EC%9D%8C%EC%95%85%EC%9D%B4-%EC%9D%B4%EC%96%B4%EC%A7%84%EB%8B%A4-feat.-IndexedDB-5</link>
            <guid>https://velog.io/@kxun_ii/%EC%83%88%EB%A1%9C%EA%B3%A0%EC%B9%A8%ED%95%B4%EB%8F%84-%EC%9D%8C%EC%95%85%EC%9D%B4-%EC%9D%B4%EC%96%B4%EC%A7%84%EB%8B%A4-feat.-IndexedDB-5</guid>
            <pubDate>Tue, 07 Apr 2026 09:44:53 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-왜-필요한가">🤔 왜 필요한가?</h2>
<p>음악을 틀고 다른 페이지로 이동하거나 새로고침하면 어떻게 될까요?</p>
<p>Jotai atom은 <strong>메모리에만 존재</strong>합니다. 새로고침하는 순간 플레이리스트, 현재 곡, 볼륨이 모두 초기화됩니다.</p>
<p>스트리밍 앱에서 새로고침할 때마다 음악이 처음부터 다시 시작된다면 불편하겠죠.
<strong>재생 상태를 브라우저에 저장</strong>해서 새로고침 후에도 이어 들을 수 있게 만들고 싶었습니다.</p>
<br>
<hr>
<br>

<h2 id="🚨-문제-예상-원인">🚨 문제 예상 (원인)</h2>
<h3 id="localstorage는-안-될까">localStorage는 안 될까?</h3>
<p>가장 먼저 떠오르는 건 localStorage입니다. 하지만 몇 가지 한계가 있습니다.</p>
<pre><code class="language-typescript">// localStorage로 플레이리스트 저장한다면...
localStorage.setItem(&#39;playlist&#39;, JSON.stringify(playlist));</code></pre>
<table>
<thead>
<tr>
<th>문제</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>용량 제한</strong></td>
<td>보통 5MB 이하. 트랙 데이터(썸네일 URL, 제목 등)가 쌓이면 금방 한계에 도달</td>
</tr>
<tr>
<td><strong>동기 I/O</strong></td>
<td>저장/읽기가 메인 스레드를 블로킹. 데이터가 많으면 UI가 멈출 수 있음</td>
</tr>
<tr>
<td><strong>구조화 불가</strong></td>
<td>문자열만 저장 가능. JSON.stringify/parse 비용 + 관계형 데이터 표현 어려움</td>
</tr>
<tr>
<td><strong>중복 저장</strong></td>
<td>좋아요 목록과 플레이리스트에 같은 트랙이 있으면 데이터가 중복</td>
</tr>
</tbody></table>
<pre><code class="language-javascript">// 재생목록이 여러 개가 된다면 (v2 확장 시 예상되는 문제)
localStorage:
  likedTracks: [{ videoId: &quot;abc&quot;, title: &quot;노래A&quot;, thumbnail: &quot;...&quot; }, ...]
  playlist_1:  [{ videoId: &quot;abc&quot;, title: &quot;노래A&quot;, thumbnail: &quot;...&quot; }, ...]  // 중복!
  playlist_2:  [{ videoId: &quot;abc&quot;, title: &quot;노래A&quot;, thumbnail: &quot;...&quot; }, ...]  // 중복!</code></pre>
<br>
<hr>
<br>

<h2 id="💡-아이디어--해결">💡 아이디어 + 해결</h2>
<p><strong>IndexedDB</strong>는 브라우저 내장 NoSQL 데이터베이스입니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>localStorage</th>
<th>IndexedDB</th>
</tr>
</thead>
<tbody><tr>
<td>저장 방식</td>
<td>문자열 only</td>
<td>JS 객체, Blob 등</td>
</tr>
<tr>
<td>용량</td>
<td>~5MB</td>
<td>수백MB (디스크 여유 공간 기준)</td>
</tr>
<tr>
<td>I/O</td>
<td>동기 (블로킹)</td>
<td>비동기 (논블로킹)</td>
</tr>
<tr>
<td>구조화</td>
<td>불가</td>
<td>인덱스, 쿼리 가능</td>
</tr>
</tbody></table>
<p>중복 저장 문제는 <strong>정규화된 스키마 설계</strong>로 해결했습니다.
트랙 데이터를 한 곳(<code>tracks</code>)에만 저장하고, 다른 스토어는 <strong>ID만 참조</strong>합니다.</p>
<br>
<hr>
<br>

<h2 id="🗂️-스키마-설계-정규화">🗂️ 스키마 설계: 정규화</h2>
<h3 id="문제-같은-곡이-여러-곳에-중복-저장된다">문제: 같은 곡이 여러 곳에 중복 저장된다</h3>
<p>&quot;노래A&quot;를 좋아요 추가하고, 플레이리스트 2개에도 넣으면 어떻게 될까요?</p>
<pre><code class="language-javascript">❌ 비정규화 구조 (중복 발생)

likedTracks:   [{ videoId: &quot;abc&quot;, title: &quot;노래A&quot;, thumbnail: &quot;...&quot; }]
playlist_1:    [{ videoId: &quot;abc&quot;, title: &quot;노래A&quot;, thumbnail: &quot;...&quot; }]  ← 같은 데이터
playlist_2:    [{ videoId: &quot;abc&quot;, title: &quot;노래A&quot;, thumbnail: &quot;...&quot; }]  ← 같은 데이터</code></pre>
<p>썸네일 URL이 바뀌거나 제목이 수정되면 <strong>3곳을 전부 업데이트</strong>해야 합니다.
어느 한 곳을 빠뜨리면 데이터가 달라지는 불일치가 생깁니다.</p>
<h3 id="해결-tracks를-단일-저장소로-분리">해결: tracks를 단일 저장소로 분리</h3>
<p>트랙 데이터는 <code>tracks</code> 스토어 <strong>한 곳에만</strong> 저장하고,
다른 스토어는 <strong>ID만 참조</strong>합니다.</p>
<pre><code class="language-javascript">✅ 정규화 구조

tracks (단일 저장소)
 └─ &quot;abc&quot; → { title: &quot;노래A&quot;, thumbnail: &quot;...&quot;, ... }  // 딱 1번만 존재

playlists (재생목록 목록)
 ├─ &quot;__liked__&quot;
 └─ &quot;__playlist__uuid1&quot;

playlistTracks (관계 테이블 - 다대다)
 ├─ &quot;__liked__:abc&quot;         → playlistId: &quot;__liked__&quot;,        trackId: &quot;abc&quot;, order: 0
 └─ &quot;__playlist__uuid1:abc&quot; → playlistId: &quot;__playlist__uuid1&quot;, trackId: &quot;abc&quot;, order: 0</code></pre>
<p>&quot;노래A&quot;가 몇 개의 재생목록에 들어가 있든 <code>tracks</code>에는 <strong>딱 1개</strong>만 존재합니다.
데이터 수정이 필요하면 <code>tracks</code> 한 곳만 바꾸면 됩니다.</p>
<h3 id="관계-구조-한눈에-보기">관계 구조 한눈에 보기</h3>
<pre><code>tracks ◀──────────────────── playlistTracks ──────────────────▶ playlists
 └─ &quot;abc&quot; (노래A)             ├─ &quot;__liked__:abc&quot;                  ├─ &quot;__liked__&quot;
 └─ &quot;def&quot; (노래B)             └─ &quot;__playlist__:abc&quot;               └─ &quot;__playlist__uuid&quot;</code></pre><p>관계형 DB의 <strong>다대다(N:M) 관계</strong>를 <code>playlistTracks</code>라는 중간 테이블로 표현한 것입니다.
한 트랙이 여러 재생목록에 속할 수 있고, 한 재생목록에 여러 트랙이 담길 수 있습니다.</p>
<br>
<hr>
<br>

<h2 id="⚙️-indexeddb-적용">⚙️ IndexedDB 적용</h2>
<h3 id="idb-라이브러리로-편리하게">idb 라이브러리로 편리하게</h3>
<p>raw IndexedDB API는 콜백 기반이라 코드가 복잡합니다. <code>idb</code> 라이브러리를 사용하면 Promise 기반으로 깔끔하게 쓸 수 있습니다.</p>
<pre><code class="language-bash">npm install idb</code></pre>
<h3 id="스키마-정의">스키마 정의</h3>
<pre><code class="language-typescript">// lib/indexedDB/index.ts
export interface PlayerDBSchema extends DBSchema {
  // 트랙 단일 저장소 - 좋아요/재생목록 공통 사용, 중복 방지
  tracks: {
    key: string;        // videoId
    value: PlaylistItem;
  };

  // 유저 재생목록 (폴더)
  playlists: {
    key: string;
    value: {
      id: string;       // &quot;__liked__&quot; | &quot;__playlist__{uuid}&quot;
      title: string;
      createdAt: number;
      updatedAt: number;
    };
  };

  // 재생목록 ↔ 트랙 관계 테이블 (다대다)
  playlistTracks: {
    key: string;        // `${playlistId}:${trackId}`
    value: {
      id: string;
      playlistId: string;
      trackId: string;  // tracks.key 참조
      order: number;
      addedAt: number;
    };
    indexes: {
      &#39;by-playlist&#39;: string;
      &#39;by-track&#39;: string;
    };
  };

  // 현재 플레이어 상태
  playerState: {
    key: string;
    value: {
      playlist: string[];              // trackId 배열만 저장 (full 객체 X)
      currentVideoId: string | null;
      playlistSource: PlaylistSource;
    };
  };
}</code></pre>
<p><code>playerState</code>에도 트랙 <strong>ID 배열만 저장</strong>하는 게 포인트입니다. 읽을 때 <code>tracks</code>에서 조인합니다.</p>
<h3 id="db-초기화--마이그레이션">DB 초기화 + 마이그레이션</h3>
<pre><code class="language-typescript">const DB_NAME = &#39;player-db&#39;;
const DB_VERSION = 4;

export const getPlayerDB = () =&gt; {
  if (!dbPromise) {
    dbPromise = openDB&lt;PlayerDBSchema&gt;(DB_NAME, DB_VERSION, {
      upgrade(db, oldVersion, newVersion, transaction) {

        // v1 → v2: 기존 스토어 구조를 정규화된 구조로 교체
        if (oldVersion &lt; 2) {
          const legacyDb = db as any;

          // 레거시 스토어 제거
          if (legacyDb.objectStoreNames.contains(&#39;likedPlaylist&#39;)) {
            legacyDb.deleteObjectStore(&#39;likedPlaylist&#39;);
          }

          // 새 스토어 생성
          db.createObjectStore(&#39;tracks&#39;, { keyPath: &#39;id&#39; });

          const playlistStore = db.createObjectStore(&#39;playlists&#39;, { keyPath: &#39;id&#39; });
          // 시스템 플레이리스트 초기 데이터
          playlistStore.put({
            id: LIKED_PLAYLIST_ID,
            title: &#39;좋아요한 플레이리스트&#39;,
            createdAt: Date.now(),
            updatedAt: Date.now(),
          });

          const relationStore = db.createObjectStore(&#39;playlistTracks&#39;, { keyPath: &#39;id&#39; });
          relationStore.createIndex(&#39;by-playlist&#39;, &#39;playlistId&#39;);
          relationStore.createIndex(&#39;by-track&#39;, &#39;trackId&#39;);

          db.createObjectStore(&#39;playerState&#39;);
        }

        // v2 → v3: 좋아요 플레이리스트 title 변경
        if (oldVersion &lt; 3) {
          const playlistStore = transaction.objectStore(&#39;playlists&#39;);
          playlistStore.put({
            id: LIKED_PLAYLIST_ID,
            title: &#39;좋아요한 플레이리스트&#39;,
            updatedAt: Date.now(),
            createdAt: Date.now(),
          });
        }
      },
    });
  }

  return dbPromise;
};</code></pre>
<p><code>oldVersion &lt; N</code> 패턴으로 버전별 마이그레이션을 순차 적용합니다.
기존 사용자의 DB도 자동으로 최신 버전으로 업그레이드됩니다.</p>
<h3 id="플레이어-상태-저장불러오기">플레이어 상태 저장/불러오기</h3>
<pre><code class="language-typescript">// lib/indexedDB/playerStateDB.ts

// 저장: 트랜잭션으로 atomic하게
export const savePlayerState = async (state: PlayerStateValue) =&gt; {
  const db = await getPlayerDB();

  // playerState + tracks 두 스토어를 하나의 트랜잭션으로 묶음
  const tx = db.transaction([&#39;playerState&#39;, &#39;tracks&#39;], &#39;readwrite&#39;);

  // tracks 스토어에도 최신 정보 저장 (put = upsert, 중복 방지)
  for (const track of state.playlist) {
    await tx.objectStore(&#39;tracks&#39;).put({ ...track, id: track.videoId });
  }

  // playerState에는 ID 배열만 저장
  await tx.objectStore(&#39;playerState&#39;).put(
    {
      playlist: state.playlist.map(track =&gt; track.videoId),
      currentVideoId: state.currentVideoId,
      playlistSource: state.playlistSource,
    },
    PLAYER_STATE_KEY,
  );

  await tx.done; // 트랜잭션 커밋
};

// 불러오기: ID 배열 → tracks 조인
export const getPlayerState = async (): Promise&lt;PlayerStateValue | undefined&gt; =&gt; {
  const db = await getPlayerDB();
  const entity = await db.get(&#39;playerState&#39;, PLAYER_STATE_KEY);

  if (!entity) return undefined;

  // ID 배열로 실제 트랙 데이터 조회
  const tracks = await Promise.all(
    entity.playlist.map(id =&gt; db.get(&#39;tracks&#39;, id))
  );

  return {
    playlist: tracks.filter(Boolean) as PlaylistItem[],
    currentVideoId: entity.currentVideoId,
    playlistSource: entity.playlistSource,
  };
};</code></pre>
<p><strong>트랜잭션으로 묶는 이유</strong>: <code>tracks</code> 저장 중 실패하면 <code>playerState</code>도 저장되지 않습니다.
부분 저장으로 인한 데이터 불일치를 방지합니다.</p>
<h3 id="useplayercore에서-연동">usePlayerCore에서 연동</h3>
<pre><code class="language-typescript">export const usePlayerCore = () =&gt; {
  // 앱 시작 시 저장된 상태 복원
  const restorePlayerState = useCallback(async () =&gt; {
    const savedState = await playerStateDB.getPlayerState();
    if (!savedState) return;

    setPlaylist(savedState.playlist);
    setPlaylistSource(savedState.playlistSource);
    setCurrentVideoId(savedState.currentVideoId);
  }, [setPlaylist, setPlaylistSource, setCurrentVideoId]);

  useEffect(() =&gt; {
    restorePlayerState();
  }, [restorePlayerState]);

  // 상태 변경 시 자동 저장
  useEffect(() =&gt; {
    if (!playlistSource) return;
    if (playlist.length === 0) return;

    playerStateDB.savePlayerState({ playlist, currentVideoId, playlistSource });
  }, [playlist, currentVideoId, playlistSource]);
};</code></pre>
<h3 id="db-무결성-보장-validateandrepairdb">DB 무결성 보장: validateAndRepairDB</h3>
<p>앱 시작 시 DB 상태를 검사하고 이상이 있으면 자동 복구합니다.</p>
<pre><code class="language-typescript">export const validateAndRepairDB = async () =&gt; {
  const db = await getPlayerDB();

  // 좋아요 플레이리스트가 없으면 복구
  const liked = await db.get(&#39;playlists&#39;, LIKED_PLAYLIST_ID);
  if (!liked) {
    await db.put(&#39;playlists&#39;, {
      id: LIKED_PLAYLIST_ID,
      title: &#39;좋아요한 플레이리스트&#39;,
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
  }

  // 고아 트랙 정리 (존재하지 않는 플레이리스트를 참조하는 관계 레코드 삭제)
  const allPlaylistTracks = await db.getAll(&#39;playlistTracks&#39;);
  const allPlaylists = await db.getAll(&#39;playlists&#39;);
  const playlistIds = new Set(allPlaylists.map(p =&gt; p.id));

  const orphanTracks = allPlaylistTracks.filter(
    track =&gt; !playlistIds.has(track.playlistId)
  );

  await Promise.all(orphanTracks.map(track =&gt; db.delete(&#39;playlistTracks&#39;, track.id)));
};</code></pre>
<pre><code class="language-typescript">// app/providers/DBProvider.tsx
export function DBProvider({ children }: { children: React.ReactNode }) {
  useEffect(() =&gt; {
    validateAndRepairDB(); // 앱 마운트 시 한 번 실행
  }, []);

  return &lt;&gt;{children}&lt;/&gt;;
}</code></pre>
<br>
<hr>
<br>

<h2 id="📊-before--after">📊 Before / After</h2>
<h3 id="before-v1-yoon-play-·-localstorage">Before: v1 (yoon-play) · localStorage</h3>
<p>이전에 만들었던 yoon-play v1에서는 보관함이 하나(좋아요 목록)뿐이었고,
YouTube API 응답을 <strong>그대로</strong> localStorage에 저장했습니다.</p>
<pre><code class="language-json">{
  &quot;#myPlayListState&quot;: [
    {
      &quot;kind&quot;: &quot;youtube#searchResult&quot;,
      &quot;etag&quot;: &quot;je810BRbL_DSPPdeAju-JAVqI8w&quot;,
      &quot;id&quot;: { &quot;kind&quot;: &quot;youtube#video&quot;, &quot;videoId&quot;: &quot;U34kLXjdw90&quot; },
      &quot;snippet&quot;: {
        &quot;title&quot;: &quot;지브리의 피아노 OST 모음&quot;,
        &quot;channelTitle&quot;: &quot;Soothing Piano Relaxing&quot;,
        &quot;thumbnails&quot;: {
          &quot;default&quot;: { &quot;url&quot;: &quot;...&quot;, &quot;width&quot;: 120, &quot;height&quot;: 90 },
          &quot;medium&quot;:  { &quot;url&quot;: &quot;...&quot;, &quot;width&quot;: 320, &quot;height&quot;: 180 },
          &quot;high&quot;:    { &quot;url&quot;: &quot;...&quot;, &quot;width&quot;: 480, &quot;height&quot;: 360 }
        },
        &quot;publishedAt&quot;: &quot;2021-07-30T01:25:42Z&quot;,
        &quot;channelId&quot;: &quot;UC3fcwju0wMjPhmmAWr0dUKQ&quot;
        // ... 그 외 불필요한 필드들
      }
    }
  ]
}</code></pre>
<p>실제로 필요한 건 4개 필드뿐인데, API 응답 전체를 저장하고 있었습니다.🥲</p>
<pre><code class="language-json">필요한 것:  videoId, title, channelTitle, thumbnail  → ~200 bytes
실제 저장:  YouTube API 응답 전체                    → ~1,000 bytes (5배)</code></pre>
<p>v1은 보관함이 하나뿐이라 중복 문제는 없었지만,
<strong>v2(yoon-play2)에서 재생목록이 여러 개로 늘어나는 순간 이 구조 그대로 쓰면 중복이 바로 터질 상황이었습니다.</strong>
그리고 새로고침하면 재생 상태가 초기화되는 문제도 그대로였고요. 😢</p>
<h3 id="after-v2-yoon-play2-·-indexeddb--정규화">After: v2 (yoon-play2) · IndexedDB + 정규화</h3>
<pre><code>[새로고침 전 상태]
  노래A를 좋아요 + 플레이리스트 2개에 추가

IndexedDB:
  tracks:         { &quot;abc&quot;: { title: &quot;노래A&quot;, ... } }   // 딱 1번만 저장 ✅
  playlists:      { &quot;__liked__&quot;, &quot;__playlist__uuid1&quot;, &quot;__playlist__uuid2&quot; }
  playlistTracks: { &quot;__liked__:abc&quot;, &quot;uuid1:abc&quot;, &quot;uuid2:abc&quot; }
  playerState:    { playlist: [&quot;abc&quot;, ...], currentVideoId: &quot;abc&quot; }

[새로고침 후]
  DBProvider 마운트 → validateAndRepairDB 실행
  usePlayerCore → getPlayerState → ID로 tracks 조인 → atom 복원
  이어 듣기 시작 ✅</code></pre><table>
<thead>
<tr>
<th></th>
<th>Before (v1 · localStorage)</th>
<th>After (v2 · IndexedDB)</th>
</tr>
</thead>
<tbody><tr>
<td>새로고침 후 재생 상태</td>
<td>초기화 😢</td>
<td>복원 ✅</td>
</tr>
<tr>
<td>저장 데이터</td>
<td>YouTube API 응답 전체 (~1,000 bytes/곡)</td>
<td>필요한 필드만 (~200 bytes/곡) ✅</td>
</tr>
<tr>
<td>트랙 데이터 중복</td>
<td>보관함 1개라 없었지만, 여러 재생목록 확장 시 즉시 발생</td>
<td>정규화로 원천 차단 ✅</td>
</tr>
<tr>
<td>저장 용량</td>
<td>~5MB 한계</td>
<td>수백MB ✅</td>
</tr>
<tr>
<td>메인 스레드 블로킹</td>
<td>있음 (동기)</td>
<td>없음 (비동기) ✅</td>
</tr>
<tr>
<td>데이터 무결성</td>
<td>수동 관리</td>
<td>트랜잭션 + 자동 복구 ✅</td>
</tr>
</tbody></table>
<br>
<hr>
<br>

<h2 id="🎓-회고">🎓 회고</h2>
<h3 id="1-db-설계는-처음부터-정규화를-고민하자">1. DB 설계는 처음부터 정규화를 고민하자</h3>
<p>처음엔 좋아요 목록과 재생목록에 트랙 데이터를 그냥 통째로 저장했습니다.
한 곡이 여러 목록에 있으면 데이터가 중복되고, 썸네일이 바뀌면 모든 곳을 수정해야 했습니다.
<code>tracks</code>를 단일 저장소로 분리하고 나서야 일관성이 생겼습니다.
관계형 DB의 정규화 원칙이 IndexedDB에서도 그대로 적용됩니다.</p>
<h3 id="2-트랜잭션은-데이터-일관성의-보험이다">2. 트랜잭션은 데이터 일관성의 보험이다</h3>
<p><code>tracks</code>와 <code>playerState</code>를 각각 따로 저장하면, 중간에 실패했을 때 한쪽만 저장되는 상황이 생길 수 있습니다.
<strong>트랜잭션으로 묶으면 전부 성공하거나 전부 실패합니다.</strong></p>
<h3 id="3-마이그레이션은-미리-설계하자">3. 마이그레이션은 미리 설계하자</h3>
<p><code>oldVersion &lt; N</code> 패턴으로 버전별 마이그레이션을 관리하니, 스키마가 바뀌어도 기존 사용자 데이터를 안전하게 유지할 수 있었습니다.
처음부터 버전 관리를 염두에 두지 않았다면, 스키마를 바꿀 때마다 기존 데이터가 날아가거나 오류가 생길 수 있어요.</p>
</br>
<hr>
</br>


<p>지금까지 긴 글 읽어주셔서 감사합니다 :)</p>
<blockquote>
<p>💬 비슷한 문제를 겪으셨거나, 더 좋은 해결 방법이 있다면 댓글로 공유해주세요!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[긴 플레이리스트, 전부 그려야 할까? (feat. React Virtual) -4]]></title>
            <link>https://velog.io/@kxun_ii/%EA%B8%B4-%ED%94%8C%EB%A0%88%EC%9D%B4%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%B6%80-%EA%B7%B8%EB%A0%A4%EC%95%BC-%ED%95%A0%EA%B9%8C-feat.-React-Virtual-4</link>
            <guid>https://velog.io/@kxun_ii/%EA%B8%B4-%ED%94%8C%EB%A0%88%EC%9D%B4%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%B6%80-%EA%B7%B8%EB%A0%A4%EC%95%BC-%ED%95%A0%EA%B9%8C-feat.-React-Virtual-4</guid>
            <pubDate>Tue, 07 Apr 2026 09:12:50 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-왜-필요한가">🤔 왜 필요한가?</h2>
<p>보관함 페이지에는 플레이리스트별 트랙 목록이 있습니다.
트랙이 10~20개면 문제없지만, 좋아하는 곡을 계속 추가하다 보면 100곡, 200곡이 될 수 있습니다.</p>
<p>처음엔 단순하게 <code>map()</code>으로 전체를 렌더링했습니다.</p>
<pre><code class="language-typescript">// 단순 구현
export default function TrackList({ tracks }: { tracks: PlaylistItem[] }) {
  return (
    &lt;ul&gt;
      {tracks.map(track =&gt; (
        &lt;PlayerQueueItem key={track.videoId} item={track} /&gt;
      ))}
    &lt;/ul&gt;
  );
}</code></pre>
<p>직관적이고 동작도 합니다. 
하지만 <strong>&quot;트랙이 많아지면 어떻게 될까?&quot;</strong> 라는 의문이 생겼습니다!</p>
<h2 id="문제-예상원인">문제 예상(원인)</h2>
<h3 id="dom에-모든-요소가-존재한다는-것">DOM에 모든 요소가 존재한다는 것</h3>
<p>화면에는 한 번에 약 10개의 트랙만 보입니다.
하지만 map()으로 렌더링하면 200곡이면 200개의 DOM 노드가 전부 존재합니다.</p>
<pre><code>화면에 보이는 트랙: 10개
실제 DOM에 존재하는 트랙: 200개 💥</code></pre><p><strong>DOM 노드가 많을수록:</strong></p>
<ul>
<li>초기 렌더링 비용 증가 (200개를 한 번에 그림)</li>
<li>메모리 사용량 증가 (보이지 않는 요소도 메모리 차지)</li>
<li>스크롤 성능 저하 (브라우저가 관리할 노드가 많아짐)</li>
</ul>
<h3 id="🤔-실제로-문제가-될까">🤔 실제로 문제가 될까?</h3>
<p>트랙 하나가 단순한 텍스트라면 괜찮을 수 있습니다.
그런데 <code>PlayerQueueItem</code>은 썸네일 이미지, 버튼, 아이콘 등을 포함한 복합 컴포넌트입니다.</p>
<pre><code>200개 × (이미지 + 텍스트 + 버튼 + 아이콘) = 상당한 DOM 크기</code></pre><p>지금 당장 느리지 않더라도, 데이터가 많아질수록 문제가 될 가능성이 충분합니다.
<strong>&quot;느려지고 나서 고치기&quot;</strong>보다 <strong>&quot;예방하기&quot;</strong> 를 선택했습니다.</p>
<br>
<hr>
<br>

<h2 id="💡-아이디어--해결">💡 아이디어 + 해결</h2>
<h3 id="핵심-아이디어-보이는-것만-그리자">핵심 아이디어: 보이는 것만 그리자</h3>
<p>스크롤 위치를 기준으로 현재 화면에 보이는 트랙만 DOM에 렌더링하면 됩니다.
스크롤을 내리면 새 항목이 그려지고, 화면을 벗어난 항목은 DOM에서 제거합니다.</p>
<pre><code>[트랙 1]  ← DOM에 없음 (화면 위로 벗어남)
[트랙 2]  ← DOM에 없음
──────────────────── 화면 시작
[트랙 3]  ← DOM에 있음 ✅
[트랙 4]  ← DOM에 있음 ✅
[트랙 5]  ← DOM에 있음 ✅
[트랙 6]  ← DOM에 있음 ✅
[트랙 7]  ← DOM에 있음 ✅
──────────────────── 화면 끝
[트랙 8]  ← DOM에 없음 (화면 아래)
[트랙 9]  ← DOM에 없음
...</code></pre><p>이 개념을 <strong>가상 리스트(Virtual List)</strong> 라고 합니다.
직접 구현할 수도 있지만, <strong>@tanstack/react-virtual</strong>이 이 로직을 제공합니다.</p>
<h3 id="⚙️-가상-리스트-적용">⚙️ 가상 리스트 적용</h3>
<h4 id="1-설치">1. 설치</h4>
<p><code>npm install @tanstack/react-virtual</code></p>
<h4 id="2-구현">2. 구현</h4>
<pre><code class="language-typescript">&#39;use client&#39;;

import { useCallback, useRef } from &#39;react&#39;;
import { useVirtualizer } from &#39;@tanstack/react-virtual&#39;;

export default function TrackList({ tracks, context }: TrackListProps) {
  const { currentVideoId, setPlayerListFromContext } = usePlayerCore();

  // 1. 스크롤 컨테이너 ref
  const parentRef = useRef&lt;HTMLDivElement | null&gt;(null);

  // 2. 가상화 설정
  const rowVirtualizer = useVirtualizer({
    count: tracks.length,        // 전체 트랙 수
    getScrollElement: () =&gt; parentRef.current, // 스크롤 컨테이너
    estimateSize: () =&gt; 56,      // 트랙 하나의 높이 (px)
    overscan: 2,                 // 화면 밖으로 미리 렌더링할 개수
  });

  const handlePlay = useCallback(
    (videoId: string) =&gt; {
      const track = tracks.find(t =&gt; t.videoId === videoId);
      if (track) setPlayerListFromContext(tracks, track);
    },
    [tracks, setPlayerListFromContext],
  );

  // 3. 실제 렌더링되는 가상 아이템 목록
  const virtualItems = rowVirtualizer.getVirtualItems();

  return (
    // 4. 스크롤 컨테이너
    &lt;div
      ref={parentRef}
      className=&#39;overflow-auto mt-8&#39;
      style={{ height: &#39;calc(100vh - 380px - 80px)&#39; }}
    &gt;
      {/* 5. 전체 높이를 확보하는 컨테이너 (스크롤바가 올바르게 표시되도록) */}
      &lt;ul
        className=&#39;relative w-full&#39;
        style={{ height: `${rowVirtualizer.getTotalSize()}px` }}
      &gt;
        {/* 6. 화면에 보이는 항목만 렌더링 */}
        {virtualItems.map(virtualRow =&gt; {
          const track = tracks[virtualRow.index];
          return (
            &lt;li
              key={track.videoId}
              className=&#39;absolute left-0 top-0 w-full&#39;
              style={{
                // 7. translateY로 정확한 위치에 배치
                transform: `translateY(${virtualRow.start}px)`,
              }}
            &gt;
              &lt;PlayerQueueItem
                item={track}
                isActive={track.videoId === currentVideoId}
                onClick={handlePlay}
                context={context}
                showLikeButton={false}
              /&gt;
            &lt;/li&gt;
          );
        })}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="핵심-개념-뜯어보기">핵심 개념 뜯어보기</h3>
<h4 id="gettotalsize--전체-높이-확보">getTotalSize() — 전체 높이 확보</h4>
<pre><code>style={{ height: `${rowVirtualizer.getTotalSize()}px` }}
// 200개 × 56px = 11,200px</code></pre><p>실제로 DOM에는 10개만 있지만, 스크롤바는 200개짜리처럼 동작해야 합니다.
getTotalSize()가 반환한 전체 높이를 컨테이너에 지정해서 스크롤 범위를 확보합니다.</p>
<h4 id="translatey--올바른-위치에-배치">translateY — 올바른 위치에 배치</h4>
<pre><code>style={{ transform: `translateY(${virtualRow.start}px)` }}
// 3번째 트랙이라면: translateY(112px) = 56px × 2</code></pre><p>모든 아이템이 <code>position: absolute</code>로 컨테이너 좌상단에 쌓입니다.
<code>virtualRow.start</code>를 <code>translateY</code>에 적용해 각 아이템을 정확한 위치에 배치합니다.
<code>width</code> 대신 <code>transform</code>을 사용하므로 Layout 재계산 없이 GPU가 처리합니다.</p>
<h4 id="overscan-2--스크롤-버벅임-방지">overscan: 2 — 스크롤 버벅임 방지</h4>
<pre><code>overscan: 2</code></pre><p>화면에 보이는 영역 위아래로 2개씩 미리 렌더링합니다.
스크롤할 때 새 항목이 즉시 나타나 버벅임이 없습니다.
<strong>너무 크면 렌더링할 DOM이 많아져 오히려 역효과가 납니다.</strong></p>
<br>
<hr>
<br>

<h2 id="📊-before--after-가상-시나리오">📊 Before / After (가상 시나리오)</h2>
<p>200개 트랙 기준으로 예상되는 차이입니다.</p>
<h3 id="before-전체-렌더링">Before: 전체 렌더링</h3>
<pre><code>페이지 진입
  ↓
200개 트랙 전부 DOM 생성
  ↓
초기 렌더링: 200개 × (이미지 + 텍스트 + 버튼) 처리
  ↓
메모리: 200개 DOM 노드 상주
  ↓
스크롤 시: 200개 노드를 기준으로 레이아웃 계산
</code></pre><h3 id="after-가상-리스트">After: 가상 리스트</h3>
<pre><code>페이지 진입
  ↓
화면에 보이는 트랙 10개 + overscan 4개 = 14개만 DOM 생성
  ↓
초기 렌더링: 14개만 처리 ✅
  ↓
메모리: 14개 DOM 노드만 상주 ✅
  ↓
스크롤 시: 진입/이탈 항목만 추가/제거 ✅</code></pre><p><img src="https://velog.velcdn.com/images/kxun_ii/post/ca333e50-230e-46ce-8451-86f361c345f3/image.png" alt=""></p>
<blockquote>
<p>트랙 수가 늘어도 렌더링 비용이 거의 변하지 않는다는 게 핵심입니다.</p>
</blockquote>
<br>
<hr>
<br>

<h2 id="🎓-회고">🎓 회고</h2>
<h3 id="1-느려지기-전에-설계하자">1. 느려지기 전에 설계하자</h3>
<p>지금 당장 200곡을 가진 사용자가 많지 않을 수 있습니다.
하지만 가상 리스트는 적용 비용이 낮고, 데이터가 쌓일수록 효과가 커집니다.
&quot;느려지고 나서 고치기&quot;보다 &quot;예방하기&quot;가 훨씬 쉽다는 걸 다시 확인했습니다.</p>
<h3 id="2-dom-크기도-성능이다">2. DOM 크기도 성능이다</h3>
<p>React 성능 최적화라고 하면 흔히 memo, useCallback을 떠올립니다.
<strong>하지만 DOM 노드 수 자체를 줄이는 것도 중요한 최적화입니다.</strong>
브라우저가 관리해야 할 노드가 적을수록 레이아웃 계산, 이벤트 버블링, 메모리 모두 고려해보는 습관을 가져보아요.</p>
<br>
<hr>
<br>


<p>다음 글에는 <strong>IndexedDB 스키마 설계 및 데이터 무결성 관리</strong>에 대해 다뤄보려고 합니다!</p>
<p>지금까지 긴 글 읽어주셔서 감사합니다 :)</p>
<blockquote>
<p>💬 비슷한 문제를 겪으셨거나, 더 좋은 해결 방법이 있다면 댓글로 공유해주세요!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[리페인트 최소화하기-3]]></title>
            <link>https://velog.io/@kxun_ii/%EB%A6%AC%ED%8E%98%EC%9D%B8%ED%8A%B8-%EC%B5%9C%EC%86%8C%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kxun_ii/%EB%A6%AC%ED%8E%98%EC%9D%B8%ED%8A%B8-%EC%B5%9C%EC%86%8C%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 07 Apr 2026 08:53:12 GMT</pubDate>
            <description><![CDATA[<h3 id="🎨-progressbar-리페인트-최소화-feat-transform-scalex">🎨 ProgressBar 리페인트 최소화 (feat. transform: scaleX)</h3>
<blockquote>
<p>이전 글에서 훅 분리로 불필요한 리렌더링을 줄였다면,
이번엔 300ms마다 발생하는 <strong>브라우저 렌더링 파이프라인 비용</strong> 자체를 줄여봤습니다.</p>
</blockquote>
<h4 id="📖-리페인트repaint란">📖 리페인트(Repaint)란?</h4>
<p>리페인트는 브라우저가 요소의 <strong>시각적 스타일이 바뀌었을 때 픽셀을 다시 그리는 작업</strong>입니다.</p>
<p>예를 들어 <code>background-color</code>, <code>color</code>, <code>border</code> 같은 속성이 바뀌면 레이아웃은 그대로지만,
해당 영역의 픽셀을 다시 계산해서 화면에 그려야 합니다. 이게 리페인트입니다.</p>
<p>리플로우(Reflow)와 혼동되기 쉬운데, 차이는 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>리플로우 (Reflow)</th>
<th>리페인트 (Repaint)</th>
</tr>
</thead>
<tbody><tr>
<td>발생 조건</td>
<td><code>width</code>, <code>height</code>, <code>margin</code> 등 <strong>레이아웃</strong>이 바뀔 때</td>
<td><code>color</code>, <code>background</code> 등 <strong>시각적 스타일</strong>만 바뀔 때</td>
</tr>
<tr>
<td>비용</td>
<td>더 큼 (레이아웃 재계산 + 리페인트까지 발생)</td>
<td>상대적으로 작음</td>
</tr>
<tr>
<td>영향 범위</td>
<td>해당 요소 + <strong>주변 요소까지</strong> 재계산</td>
<td>해당 요소 영역만 다시 그림</td>
</tr>
</tbody></table>
<blockquote>
<p>리플로우가 발생하면 리페인트도 항상 따라옵니다.
반대로 리페인트는 리플로우 없이도 발생할 수 있습니다.</p>
</blockquote>
<p>ProgressBar는 300ms마다 업데이트됩니다.
<code>width</code>를 쓰면 리플로우 → 리페인트가 반복되고, <code>transform</code>을 쓰면 둘 다 건너뜁니다.</p>
</br>
<hr>
</br>


<h2 id="🚨-문제-발견">🚨 문제 발견</h2>
<h3 id="초기-구현-inputtyperange">초기 구현: input[type=&quot;range&quot;]</h3>
<p>처음 ProgressBar는 네이티브 HTML 요소로 구현했습니다.</p>
<pre><code class="language-typescript">const ProgressBar = () =&gt; {
  const { duration, currentTime, playerRef, setCurrentTime } = usePlayer();

  const handleChange = (e: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const newTime = Number(e.target.value);
    setCurrentTime(newTime);
    playerRef.seekTo(newTime, true);
  };

  ...

  return (
    &lt;input
      type=&#39;range&#39;
      min=&#39;0&#39;
      max={duration || 0}
      value={currentTime}
      step=&#39;1&#39;
      onChange={handleChange}
      className=&#39;w-full h-1&#39;
    /&gt;
  );
};</code></pre>
<p>위와 같이 간단하게 만들 수 있었지만 두 가지 문제가 있었습니다. 🥹</p>
<ol>
<li><strong>커스텀 스타일링 한계</strong> - 브라우저마다 기본 스타일이 달라 디자인 일관성 유지가 어려움</li>
<li><strong>성능 문제</strong> - 브라우저가 내부적으로 value 변화를 처리하는 방식이 GPU 가속과 거리가 있음</li>
</ol>
<p>그래서 커스텀 div 기반으로 다시 만들기로 했고,이 과정에서 <strong>렌더링 파이프라인</strong>에 대해 <strong>고민하게 됐습니다.</strong></p>
</br>
<hr>
</br>


<h2 id="🔍-원인-분석-브라우저-렌더링-파이프라인">🔍 원인 분석: 브라우저 렌더링 파이프라인</h2>
<p>브라우저가 화면을 그리는 과정은 3단계입니다.</p>
<pre><code>Layout (Reflow) → Paint (Repaint) → Composite</code></pre><ul>
<li>Layout: 요소의 위치와 크기를 계산</li>
<li>Paint: 픽셀을 실제로 그림</li>
<li>Composite: GPU가 레이어를 합성해 화면에 출력</li>
</ul>
</br>

<p>단계가 앞으로 갈수록 비용이 큽니다.
width 같은 레이아웃 속성을 변경하면 Layout부터 전체 파이프라인을 다시 실행합니다.</p>
<h3 id="width를-쓰면-어떻게-될까">width를 쓰면 어떻게 될까?</h3>
<pre><code>// 300ms마다 이렇게 업데이트한다면...
&lt;div style={{ width: `${progress}%` }} /&gt;

currentTime 업데이트 (300ms마다)
  ↓
width 변경
  ↓
Layout 재계산 (Reflow) 💥
  ↓
Paint (Repaint) 💥
  ↓
Composite</code></pre><p>음악 재생 내내 300ms마다 <strong>Layout + Paint 비용이 발생</strong>합니다.</p>
</br>
<hr>
</br>

<h2 id="💡-해결-transform-scalex">💡 해결: transform: scaleX()</h2>
<p>transform과 opacity는 브라우저가 Composite 단계에서만 처리합니다.
GPU가 레이어를 합성하는 것이라 CPU 부담이 거의 없습니다.</p>
<pre><code>currentTime 업데이트 (300ms마다)
  ↓
transform 변경
  ↓
Composite만 실행 ✅ (Layout, Paint 건너뜀)</code></pre><h3 id="after-커스텀-div--transform-scalex">After: 커스텀 div + transform: scaleX()</h3>
<pre><code class="language-typescript">const ProgressBar = memo(function ProgressBar({ className }: { className?: string }) {
  const { duration, currentTime, playerRef, setCurrentTime } = usePlayerTime();
  const progressBarRef = useRef&lt;HTMLDivElement&gt;(null);
  const [isDragging, setIsDragging] = useState(false);

  const progress = duration &gt; 0 ? (currentTime / duration) * 100 : 0;

  ...

  return (
    &lt;div className={`progress-bar relative w-full ${className || &#39;&#39;}`}&gt;
      &lt;div
        ref={progressBarRef}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        className=&#39;h-[3px] hover:h-[6px] bg-white/60 cursor-pointer relative overflow-hidden touch-none&#39;
      &gt;
        {/* ✅ width 대신 transform: scaleX() */}
        &lt;div
          className=&#39;progress absolute inset-0 h-full origin-left will-change-transform&#39;
          style={{
            transform: `scaleX(${progress / 100})`,
            background: &#39;linear-gradient(90deg, #5E9F94 0%, #78B7AC 65%, #A9D8CF 100%)&#39;,
          }}
        /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
});</code></pre>
<h3 id="핵심-포인트-3가지">핵심 포인트 3가지</h3>
<h4 id="1-transform-scalex0--1">1. transform: scaleX(0 ~ 1)</h4>
<pre><code class="language-typescript">// progress가 60%라면
transform: scaleX(0.6) // 0부터 1 사이 값으로 표현</code></pre>
<p>width를 60%로 변경하는 대신,** 요소를 X축으로 0.6배 늘립니다.**
브라우저 입장에서는 <strong>Layout 재계산 없이 GPU 레이어만 다시 합성</strong>하면 됩니다.</p>
<h4 id="2-origin-left">2. origin-left</h4>
<pre><code class="language-typescript">className=&#39;origin-left&#39; // transform-origin: left</code></pre>
<p>scaleX는 기본적으로 요소의 중앙을 기준으로 늘어납니다.
origin-left를 설정해야 왼쪽 기준으로 늘어나 진행바처럼 동작합니다.</p>
<pre><code class="language-typescript">origin-center (기본):  ←[====중앙====]→  // 양쪽으로 늘어남
origin-left:           [====왼쪽→    ]   // 왼쪽에서 오른쪽으로 늘어남 ✅</code></pre>
<h4 id="3-will-change-transform">3. will-change: transform</h4>
<pre><code class="language-typescript">className=&#39;will-change-transform&#39;</code></pre>
<p>브라우저에게 <strong>&quot;이 요소는 곧 transform이 자주 바뀔 거야&quot;</strong> 라고 미리 알려줍니다.
브라우저는 해당 요소를 별도의 GPU 레이어로 미리 분리해두어 합성 비용을 더 낮춥니다.</p>
<blockquote>
<p>⚠️ will-change는 남발하면 오히려 메모리 낭비가 됩니다.
실제로 애니메이션이 자주 발생하는 요소에만 사용하는 것이 좋습니다.</p>
</blockquote>
</br>
<hr>
</br>

<h2 id="결과-비교">결과 비교</h2>
<p><img src="https://velog.velcdn.com/images/kxun_ii/post/b67f0ffa-dbdb-4176-9e76-dfebf2471657/image.png" alt=""></p>
</br>
<hr>
</br>

<h2 id="🎓-회고">🎓 회고</h2>
<h3 id="1-css-속성마다-렌더링-비용이-다르다">1. CSS 속성마다 렌더링 비용이 다르다</h3>
<p>모든 CSS 변경이 동일한 비용을 갖지 않습니다.
특히 자주 바뀌는 값이라면 어떤 속성을 쓰느냐가 체감 성능에 영향을 줍니다.</p>
<pre><code>비용 낮음 ← transform, opacity
비용 높음 → width, height, top, left, margin, padding</code></pre><p>자주 업데이트되는 애니메이션 요소라면 transform/opacity 사용을 우선 고려해보세요!!</p>
<h3 id="2-will-change는-힌트-만능-해결책이-아니다">2. will-change는 힌트, 만능 해결책이 아니다</h3>
<p><code>will-change-transform</code>을 모든 요소에 붙인다고 빨라지지 않습니다.
GPU 레이어 분리는 메모리를 소비하므로, 실제로 변화가 잦은 요소에만 적용해야 합니다.</p>
</br>
<hr>
</br>

<p>다음 글에는 *<em>가상리스트 (React Virtual) *</em>에 대해 다뤄보려고 합니다!</p>
<p>지금까지 긴 글 읽어주셔서 감사합니다 :)</p>
<blockquote>
<p>💬 비슷한 문제를 겪으셨거나, 더 좋은 해결 방법이 있다면 댓글로 공유해주세요!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[리렌더링 문제 해결하기(feat.usePlaeyr 훅 분리) - 2]]></title>
            <link>https://velog.io/@kxun_ii/usePlayer-%ED%9B%85%EC%9D%98-%EA%B3%BC%EB%8F%84%ED%95%9C-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0feat.%ED%9B%85-%EB%B6%84%EB%A6%AC</link>
            <guid>https://velog.io/@kxun_ii/usePlayer-%ED%9B%85%EC%9D%98-%EA%B3%BC%EB%8F%84%ED%95%9C-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0feat.%ED%9B%85-%EB%B6%84%EB%A6%AC</guid>
            <pubDate>Tue, 20 Jan 2026 10:23:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>💡 &quot;왜 특정 컴포넌트의 상태만 업데이트되는데 전체 컴포넌트가 리렌더링되지?&quot;</p>
</blockquote>
</br>



<h3 id="player-컴포넌트-구조">Player 컴포넌트 구조</h3>
<pre><code class="language-typescript">const Player = () =&gt; {
  return (
    &lt;&gt;
      &lt;PlayerBar /&gt;
      &lt;PlayerPanel /&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>음악 플레이어를 만들면서 <code>PlayerBar</code>와 <code>PlayerPanel</code> 컴포넌트를 만들었습니다.</p>
<ul>
<li><strong>PlayerBar</strong>: 화면 하단에 고정되어 있는 음악 플레이어 바</li>
<li><strong>PlayerPanel</strong>: <code>PlayerBar</code> 를 클릭하면 토글되며 보여지는 패널 (현재 재생 중인 음악 + 재생 목록)</li>
</ul>
</br>

<h2 id="🚨-문제-발견">🚨 문제 발견</h2>
<h3 id="1-playerbar-컴포넌트-내부-구조">1. PlayerBar 컴포넌트 내부 구조</h3>
<p><code>PlayerBar</code>는 음악을 제어할 수 있는 여러 컴포넌트들로 구성되어 있습니다.</p>
<pre><code class="language-typescript">&lt;PlayerControl.Frame /&gt;
&lt;PlayerControl.ProgressBar /&gt;
&lt;PlayerControl.Buttons /&gt;
&lt;PlayerControl.Volume /&gt;
</code></pre>
<p><code>PlayerControl</code>은 플레이어를 제어하는 여러 컴포넌트들을 하나로 묶어 관리하기 위해 다음과 같이 구성했습니다.</p>
<pre><code class="language-typescript">export const PlayerControl = {
  Frame: PlayerFrame,
  Buttons: PlayerButtons,
  ProgressBar,
  Volume: PlayerVolumeControl,
};</code></pre>
<p>각 컴포넌트는 독립적인 역할을 가지고 있으며, 모두 <code>usePlayer()</code> 훅을 사용해 플레이어의 상태와 제어 함수를 가져옵니다.</p>
<h3 id="2-문제의-시작-progressbar">2. 문제의 시작: ProgressBar</h3>
<p>음악 플레이어를 만들면서 <code>ProgressBar</code>가 특정 몇초바마 업데이트되도록 구현했습니다.</p>
<pre><code class="language-typescript">// ProgressBar 컴포넌트 내부
const updateTime = (newTime: number) =&gt; {
  if (!playerRef) return;

  setCurrentTime(newTime); // 👈 currentTime 업데이트

  if (seekTimeoutRef.current) {
    clearTimeout(seekTimeoutRef.current);
  }

  seekTimeoutRef.current = setTimeout(() =&gt; {
    playerRef.seekTo(newTime, true);
  }, 50);
};</code></pre>
<p>당연히 현재 Time을 계산하는 <code>ProgressBar</code> 컴포넌트 에서만 리렌더링이 일어날 줄 알았으나,</p>
<p>개발자 도구를  열고 확인해보니, <code>PlayerButtons</code>나 <code>PlayerVolumeControl</code>등 <strong>다른 컴포넌트에서도 리렌더링이 일어나는 문제를 발견했습니다. 🤦🏻‍♀️</strong></p>
<h4 id="코드-예시">코드 예시</h4>
<pre><code class="language-typescript">// PlayerButtons 컴포넌트
const PlayerButtons = () =&gt; {
  console.log(&#39;PlayerButtons 렌더링!&#39;); // 👈 1초마다 출력됨!?

  const { isPlaying, currentIndex, lastIndex, ... } = usePlayer();
  // ...
}

// PlayerVolumeControl 컴포넌트  
const PlayerVolumeControl = () =&gt; {
  console.log(&#39;PlayerVolumeControl 렌더링!&#39;); // 👈 이것도!?

  const { volume, handleVolume } = usePlayer();
  // ...
}</code></pre>
<p>-&gt; <code>usePlayer()</code> 훅을 사용하는 모든 컴포넌트에서 time이 업데이트될 때 마다 리렌더링 </p>
</br>
<hr/>
</br>

<h2 id="🔍-원인-분석">🔍 원인 분석</h2>
<h3 id="before-하나의-거대한-훅">Before: 하나의 거대한 훅</h3>
<p>처음에는 모든 플레이어 상태와 로직을 하나의 <code>usePlayer</code> 훅에서 관리했습니다.</p>
<pre><code class="language-typescript">export const usePlayer = () =&gt; {
  const [playlist, setPlaylist] = useAtom(playlistState);
  const [currentVideoId, setCurrentVideoId] = useAtom(currentVideoIdAtom);
  const [isPlaying, setIsPlaying] = useAtom(isPlayingState);
  const [currentTime, setCurrentTime] = useAtom(currentTimeAtom); // 👈 1초마다 변경
  const [duration, setDuration] = useAtom(durationAtom); // 👈 1초마다 변경
  const [volume, setVolume] = useAtom(volumeAtom);
    // ... 기타 상태들


  const nextPlay = () =&gt; { /* ... */ };
  const prevPlay = () =&gt; { /* ... */ };
  const togglePlay = () =&gt; { /* ... */ };
  // ... 기타 함수들

  return {
    // 모든 상태와 함수를 반환
    playlist,
    currentVideoId,
    isPlaying,
    currentTime, // 👈 문제의 원인
    duration,    // 👈 문제의 원인
    volume,
    nextPlay,
    prevPlay,
    togglePlay,
    // ...
  };
};
</code></pre>
<h3 id="왜-문제가-발생했을까">왜 문제가 발생했을까?</h3>
<p>핵심은 React Hook의 동작 방식에 있다는 것을 알아차렸습니다.</p>
<pre><code class="language-typescript">const [currentTime, setCurrentTime] = useAtom(currentTimeAtom);</code></pre>
<p>이 코드는 <code>currentTime</code>이 변경될 때마다 usePlayer 훅 전체를 리렌더링시킵니다.</p>
</br>

<p><strong>따라서 훅이 리렌더링되면:</strong></p>
<ol>
<li>내부의 모든 함수가 재생성됩니다 (nextPlay, prevPlay, togglePlay 등)</li>
<li>반환되는 객체가 새로운 참조를 가집니다</li>
<li>이 훅을 사용하는 모든 컴포넌트가 리렌더링됩니다.</li>
</ol>
<p><strong>PlayerButtons 컴포넌트의 경우:</strong></p>
<pre><code class="language-typescript">const PlayerButtons = () =&gt; {

  const { isPlaying, currentIndex, lastIndex, prevPlay, nextPlay, togglePlay } 
    = usePlayer();  //  👈 currentTime, duration을 사용하지 않는데도 리렌더링!
  // ...
}</code></pre>
<p><code>currentTime</code>을 사용하지 않아도, <code>usePlayer()</code>훅 자체가 리렌더링되면서 반환하는 값들이 모두 새로운 참조가 되기 때문에 컴포넌트가 리렌더링됩니다.</p>
</br>

<p><strong>결과적으로</strong>
ProgressBar 업데이트 -&gt; <code>currentTime</code> 변경 -&gt; <code>usePlayer</code> 훅 전체 리렌더링 -&gt; <strong><code>usePlayer</code> 훅을 사용하는 모든 컴포넌트 리렌더링💥</strong></p>
</br>
<hr/>
</br>


<h2 id="💡-해결-훅-분리-전략">💡 해결: 훅 분리 전략</h2>
<h3 id="after--역할별로-분리된-훅">After : 역할별로 분리된 훅</h3>
<pre><code class="language-typescript">// 1️⃣ 핵심 플레이어 제어 (재생/일시정지/곡 변경 등)
export const usePlayerCore = () =&gt; {
  const [playlist, setPlaylist] = useAtom(playlistState);
  const [currentVideoId, setCurrentVideoId] = useAtom(currentVideoIdAtom);
  const [isPlaying, setIsPlaying] = useAtom(isPlayingState);
  const [playerRef, setPlayerRef] = useAtom(playerRefAtom);
  const [isPlayerReady, setIsPlayerReady] = useAtom(isPlayerReadyAtom);
  // currentTime, duration은 포함하지 않음! ✅

  const currentIndex = useAtomValue(currentIndexAtom);
  const currentVideo = useAtomValue(currentVideoAtom);

  // ...

  const nextPlay = (e: MouseEvent&lt;HTMLButtonElement&gt;) =&gt; { /* ... */ };
  const prevPlay = (e: MouseEvent&lt;HTMLButtonElement&gt;) =&gt; { /* ... */ };
  const togglePlay = (e: MouseEvent&lt;HTMLButtonElement&gt;) =&gt; { /* ... */ };

  return {
    playlist,
    currentVideoId,
    isPlaying,
    currentIndex,
    currentVideo,
    nextPlay,
    prevPlay,
    togglePlay,
    // ...
  };
};

// 2️⃣ 시간 관련 (ProgressBar 전용)
export const usePlayerTime = () =&gt; {
  const [currentTime, setCurrentTime] = useAtom(currentTimeAtom); // 👈 여기로 격리
  const [duration, setDuration] = useAtom(durationAtom); // 👈 여기로 격리
  const playerRef = useAtomValue(playerRefAtom);

  const seekTo = (seconds: number) =&gt; {
    if (playerRef) {
      playerRef.seekTo(seconds, true);
    }
  };

  return {
    currentTime,
    setCurrentTime,
    duration,
    setDuration,
    seekTo,
  };
};

// 3️⃣ 볼륨 관련 (Volume Control 전용)
export const usePlayerVolume = () =&gt; {
  const [volume, setVolume] = useAtom(volumeAtom);
  const playerRef = useAtomValue(playerRefAtom);

  const handleVolume = (volume: number) =&gt; {
    if (playerRef) {
      playerRef.setVolume(volume);
    }
  };

  return {
    volume,
    setVolume,
    handleVolume,
  };
};

// 4️⃣ 기존 API 유지 (하위 호환성)
export const usePlayer = () =&gt; {
  const core = usePlayerCore();
  const time = usePlayerTime();
  const volumeCtrl = usePlayerVolume();

  return {
    ...core,
    ...time,
    ...volumeCtrl,
  };
};</code></pre>
<h3 id="이후-컴포넌트별-적용수정">이후 컴포넌트별 적용/수정</h3>
<p>분리한 훅을 각 컴포넌트에 맞게 적용했습니다.</p>
<p><strong>PlayerButtons - usePlayerCore 사용</strong></p>
<pre><code class="language-typescript">const PlayerButtons = () =&gt; {
  // ✅ 이제 currentTime 변경에 영향받지 않음!
  const { 
    isPlaying, 
    currentIndex, 
    lastIndex, 
    isActuallyPlayerReady, 
    prevPlay, 
    nextPlay, 
    togglePlay 
  } = usePlayerCore(); 

  // ...
}</code></pre>
<p><strong>ProgressBar - usePlayerTime 사용</strong></p>
<pre><code class="language-typescript">const ProgressBar = () =&gt; {
  // ✅ 시간 관련 상태만 구독
  const { duration, currentTime, playerRef, setCurrentTime } = usePlayerTime();

  const progress = duration &gt; 0 ? (currentTime / duration) * 100 : 0;

  // ...
}</code></pre>
<p><strong>PlayerVolumeControl - usePlayerVolume 사용</strong></p>
<pre><code class="language-typescript">const PlayerVolumeControl = () =&gt; {
  // ✅ 볼륨 관련 상태만 구독
  const { handleVolume, volume, setVolume } = usePlayerVolume();

  // ...
}</code></pre>
</br>
<hr/>
</br>

<h2 id="🔬-한-발-더-playerframe의-숨겨진-리렌더링">🔬 한 발 더: PlayerFrame의 숨겨진 리렌더링</h2>
<p>훅 분리로 <code>PlayerButtons</code>, <code>ProgressBar</code>, <code>PlayerVolumeControl</code>의 불필요한 리렌더링은 해결됐습니다.
그런데 조금 더 살펴보니, <code>PlayerFrame</code>에도 같은 구조의 문제가 남아 있었습니다.</p>
<p><code>PlayerFrame</code>은 300ms마다 현재 재생 시간을 YouTube 플레이어에서 읽어와 <code>currentTimeAtom</code>을 <strong>업데이트(write)</strong> 하는 역할을 합니다.</p>
<p>그런데 기존 코드를 보면:</p>
<pre><code class="language-typescript">// Before: PlayerFrame
const PlayerFrame = () =&gt; {
  const {
    setCurrentTime, // 👈 write만 필요한데
    setDuration,
    volume,
    // ...
  } = usePlayer(); // ← currentTime, duration을 read까지 구독하고 있음!</code></pre>
<p>usePlayer()로 setCurrentTime을 가져오면서, 동시에 currentTimeAtom 값의 변화도 구독(read) 하게 됩니다.</p>
<p><strong>결과: 300ms마다 currentTimeAtom이 업데이트될 때마다,
그 값을 set한 장본인인 PlayerFrame 자신도 리렌더링 발생! 🤦🏻‍♀️</strong></p>
</br>

<h3 id="💡-해결-usesetatom으로-write-only-구독">💡 해결: useSetAtom으로 write-only 구독</h3>
<p>Jotai는 <strong>useSetAtom</strong>이라는 훅을 제공합니다.
이 훅은 atom의 setter만 반환하고, atom 값이 변경되어도 해당 컴포넌트를 리렌더링하지 않습니다.</p>
<pre><code class="language-typescript">// After: PlayerFrame
import { useSetAtom } from &#39;jotai&#39;;
import { currentTimeAtom, durationAtom } from &#39;@/store/player/atom&#39;;

const PlayerFrame = () =&gt; {
  const { ... } = usePlayerCore();              // currentTime 구독 없음 ✅
  const setCurrentTime = useSetAtom(currentTimeAtom); // write-only ✅
  const setDuration = useSetAtom(durationAtom);       // write-only ✅
  const { volume } = usePlayerVolume();

  // 300ms 인터벌에서 setCurrentTime을 호출해도
  // PlayerFrame은 더 이상 리렌더링되지 않음!

  ...</code></pre>
<h4 id="useatom-vs-usesetatom-비교">useAtom vs useSetAtom 비교</h4>
<pre><code class="language-typescript">// useAtom: 값을 읽고 쓸 수 있지만, 값 변경 시 리렌더링 발생
const [currentTime, setCurrentTime] = useAtom(currentTimeAtom);

// useSetAtom: setter만 반환, 값이 바뀌어도 이 컴포넌트는 리렌더링 없음
const setCurrentTime = useSetAtom(currentTimeAtom);
// &quot;write만 필요한 곳에서는 굳이 값을 구독할 필요가 없다&quot;는 원칙을 적용한 것입니다.</code></pre>
<br>
<hr>
<br>



<h2 id="결과">결과</h2>
<h3 id="before">Before</h3>
<pre><code class="language-typescript">ProgressBar 업데이트 (300ms마다)
  ↓
usePlayer 훅 전체 리렌더링
  ↓
PlayerButtons 리렌더링 🚨
PlayerVolumeControl 리렌더링 🚨
기타 컴포넌트들 리렌더링 🚨
</code></pre>
<h3 id="after-훅-분리---step-1">After (훅 분리) - step 1</h3>
<pre><code>ProgressBar 업데이트 (300ms마다)
  ↓
usePlayerTime 훅만 리렌더링
  ↓
ProgressBar만 리렌더링 ✅
PlayerFrame은 여전히 리렌더링 중... 🚨</code></pre><h3 id="after-usesetatom-적용-후--step-2">After (useSetAtom 적용 후)- Step 2</h3>
<pre><code>ProgressBar 업데이트 (300ms마다)
↓
usePlayerTime 훅만 리렌더링
↓
ProgressBar만 리렌더링 ✅
PlayerFrame 리렌더링 없음 ✅</code></pre><h3 id="개선-후">개선 후:</h3>
<ul>
<li>불필요한 리렌더링 감소</li>
<li>성능 향상 체감</li>
</ul>
<br>

<h2 id="🎓-회고">🎓 회고</h2>
<h3 id="1-hook도-컴포넌트처럼-역할별-분리가-중요하다">1. Hook도 컴포넌트처럼 역할별 분리가 중요하다</h3>
<p>처음에는 &quot;한 곳에 모아두면 관리하기 편하겠다&quot;고 생각했지만, 
오히려 성능 문제를 야기하고 디버깅의 어려움도 있었습니다.</p>
<p><strong>좋은 Hook 설계:</strong></p>
<ul>
<li>단일 책임 원칙 적용할 것</li>
<li>역할별로 다른 로직은 분리할 것</li>
<li>변경 빈도가 다른 상태는 분리 고려 해볼 것</li>
</ul>
<h3 id="2-사용하지-않는-값도-리렌더링을-발생시킬-수-있다">2. 사용하지 않는 값도 리렌더링을 발생시킬 수 있다</h3>
<pre><code class="language-typescript">// ❌ currentTime을 안 써도 리렌더링!
const { isPlaying, currentTime } = usePlayer();

// ✅ 필요한 것만 가져오기
const { isPlaying } = usePlayerCore();</code></pre>
<p>hook이 반환하는 객체의 참조가 바뀌면, 구조 분해 할당으로 가져온 값들도 새로운 참조가 된다.</p>
<h3 id="3-성능-문제는-측정해서-발견하자">3. 성능 문제는 측정해서 발견하자</h3>
<p><code>console.log</code>로 간단하게 리렌더링을 확인했지만,
 React DevTools의 Profiler를 사용하면 더 정확한 측정이 가능하다고 하니, 해당 툴 이용해서 디버깅해보자!!</p>
<h3 id="4-write만-필요하면-usesetatom을-쓰자">4. write만 필요하면 useSetAtom을 쓰자</h3>
<p>atom을 업데이트하는 역할만 하는 컴포넌트에서 <code>useAtom</code>을 쓰면,
자신이 업데이트한 값에 의해 자기 자신이 리렌더링되는 역설적인 상황이 생긴다.</p>
<pre><code class="language-typescript">// ❌ set만 쓰는데 value도 구독
const [currentTime, setCurrentTime] = useAtom(currentTimeAtom);

// ✅ setter만 가져오기 → 리렌더링 없음
const setCurrentTime = useSetAtom(currentTimeAtom);
</code></pre>
<br>
<hr>
<br>

<p>다음 글에는 <strong>ProgressBar의 리페인트를 줄이는 방법</strong>에 대해 다뤄보려고 합니다!
<code>width</code> 속성 대신 <code>transform: scaleX()</code>를 사용해서 GPU 가속을 활용하는 방법을 공유할 예정입니다!</p>
<p>지금까지 긴 글 읽어주셔서 감사합니다 :)</p>
<blockquote>
<p>💬 비슷한 문제를 겪으셨거나, 더 좋은 해결 방법이 있다면 댓글로 공유해주세요!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js로 AI 기반 뮤직 플레이어 만들기-1]]></title>
            <link>https://velog.io/@kxun_ii/Next.js%EB%A1%9C-AI-%EA%B8%B0%EB%B0%98-%EB%AE%A4%EC%A7%81-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-feat.yoon-play1-1</link>
            <guid>https://velog.io/@kxun_ii/Next.js%EB%A1%9C-AI-%EA%B8%B0%EB%B0%98-%EB%AE%A4%EC%A7%81-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-feat.yoon-play1-1</guid>
            <pubDate>Wed, 07 Jan 2026 08:47:42 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하며">시작하며</h2>
<p>안녕하세요. 오랜만입니다. 
붉은 말의 새해가 찾아왔네요!🌅 모두들 새해 복 많이 받으세요!ㅎㅎ</p>
<p>저는 작년 10월 정도부터 <strong>Yoon-Play2</strong>라는 <strong>AI 기반 음악 플레이어</strong>를 만들게 되었습니다.</p>
<p><strong>이번에는!</strong> 단순히 음악을 재생하는 플레이어가 아니라, 사용자의 입력 텍스트 기반으로 <strong>동적으로 카테고리를 추천해주는</strong> 스마트한 플레이어를 목표로 하고 있습니다.🥳</p>
<p>이 시리즈에서는 프로젝트를 진행하며 겪은 기술적 챌린지와 해결 과정을 공유하려고 합니다.</p>
<br/>
<br/>

<h2 id="🤔-왜-다시-만들게-되었나">🤔 왜 다시 만들게 되었나?</h2>
<p>사실 이전에 원티드에서 주최했던 프리온보딩 코스를 참여하고, 짧은 기간 안에 yoon-play라는 뮤직 플레이어를 만든적이 있습니다 <del>(3일안에 기획, 디자인, 개발 모두 다 해야했던,,🤦🏻‍♀️)</del></p>
<p><img src="https://velog.velcdn.com/images/kxun_ii/post/1d2f0174-509e-47da-8563-9c0f22d0b723/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kxun_ii/post/175c121b-3c2f-4775-85f5-d2653b92ebc5/image.png" alt=""></p>
<p>기존에는 정적인 감정, 장르 카테고리를 제공하고, 유튜브 api를 활용해서 플레이리스트를 제공하는 형식이었어요.</p>
<p>당시에는 만족스럽던 프로젝트였네요 ㅎㅎ</p>
<p>하지만 실제로 뮤직 플레이어의 필수 기능들이 빠져있기도 하고, 정적 카테고리 말고
<strong>&quot;ai가 사용자의 상태, 기분을 파악해서 카테고리를 추천해주는 것은 없을까?&quot;</strong>라는 생각이 들며 새롭게 만들어 보기로 했습니다!!</p>
<p>프로젝트를 진행하면서 비슷한 기능이나 내용일지도 모릅니다.</p>
<p>하지만, 새로운 기술 스택을 선택하고, 보다 더 좋은 코드로 작성되길 바라는 마음으로 다시 시작하려고 합니다.</p>
<h3 id="🛠️-기술-스택">🛠️ 기술 스택</h3>
<p>이 프로젝트에서 사용할 기술들입니다:</p>
<p><strong>Frontend</strong></p>
<ul>
<li><code>Next.js 15</code>, <code>TypeScript</code>, <code>Tailwind CSS</code></li>
</ul>
<p><strong>상태 관리 &amp; 데이터</strong></p>
<ul>
<li><code>React Query (TanStack Query)</code>,<code>~~(Recoil)~~ Jotai</code>, <code>IndexedDB (idb)</code></li>
</ul>
<p><strong>AI &amp; API</strong></p>
<ul>
<li><code>OpenAI API</code>, <code>YouTube Data API</code></li>
</ul>
<p><strong>Deploy</strong></p>
<ul>
<li><code>Vercel</code></li>
</ul>
<h3 id="🎯-목표와-핵심-기능">🎯 목표와 핵심 기능</h3>
<p><strong>핵심 기능</strong></p>
<p><strong>1. 🤖 AI 기반 카테고리 추천</strong> (핵심 차별화 포인트!)</p>
<ul>
<li>사용자 텍스트 기반으로 음악 카테고리, 장르 추천</li>
<li>&quot;오늘 기분이 우울해&quot; → 위로되는 음악 추천</li>
<li>&quot;집중하고 싶어&quot; → 집중력 높이는 음악 추천</li>
</ul>
<p><strong>2. 기본 플레이어 기능 (Yoon-Play1에서 놓쳤던 것들도 포함)</strong></p>
<ul>
<li>재생/일시정지/이전/다음</li>
<li>볼륨 컨트롤</li>
<li>프로그레스바</li>
</ul>
<p><strong>3. 재생목록 관리(feat. indexedDB)</strong> </p>
<ul>
<li>사용자 재생목록 생성/수정/삭제</li>
<li>좋아요 목록</li>
<li><del>(시간이 된다면?)IndexedDB로 오프라인 저장</del></li>
</ul>
<p><strong>4. 검색 기능</strong></p>
<ul>
<li>자유롭게 플레이리스트 검색</li>
</ul>
<p><strong>5. 성능 최적화</strong></p>
<ul>
<li>대용량 재생목록 <strong>가상 리스트</strong></li>
<li><strong>불필요한 리렌더링 방지</strong></li>
<li><strong>이미지 lazy loading</strong></li>
</ul>
<br/>

<h2 id="📝-시리즈-구성">📝 시리즈 구성</h2>
<p>이 프로젝트를 진행하며 겪은 이슈들과 해결 과정을 정리할 예정입니다 ㅎㅎ</p>
<h3 id="🚀-이제-시작합니다">🚀 이제 시작합니다</h3>
<p>&quot;일단 돌아가게 만들기&quot;에서 벗어나, 제대로 된 웹 애플리케이션을 만들어보려고 합니다.</p>
<p>그 과정에서 마주친 문제들, 고민했던 지점들, 그리고 해결 방법들을 솔직하게 공유하겠습니다.</p>
<p>혹시라도 비슷한 프로젝트를 진행하시는 분들께 도움이 되었으면 좋겠습니다!</p>
<p>다음 글에서는 오디오 플레이어를 만들며 겪은 첫 번째 이슈부터 시작하겠습니다.</p>
<p>감사합니다.</p>
<p>💬 궁금한 점이나 피드백은 댓글로 남겨주세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 한방향 연결리스트]]></title>
            <link>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%95%9C%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B2%B0%EB%A6%AC%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%95%9C%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B2%B0%EB%A6%AC%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Wed, 23 Jul 2025 09:58:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 자료는 유튜브 신찬수 한국외대 교수님 강의를 시청하고 정리되어 있습니다. 🙏🏻</p>
</blockquote>
<h2 id="한방향-연결리스트">한방향 연결리스트</h2>
<h3 id="삽입-연산-with-python강의">삽입 연산 With Python(강의)</h3>
<h4 id="pushfront-pushback">pushFront, pushBack</h4>
<pre><code>class SinglyLinkedList:
  def __init__(self):
    self.head = None
    self.size = 0

  def __len__(self):
    return self.size

  def pushFront(self, key):
    new_node = Node(key)
    new_node.next = self.head;
    self.head = new_node
    self.size += 1

  def pushBack(self, key):
    v = Node(key)
    if len(self) == 0:
      self.head = v
    else:
      tail = self.head
      while tail.next != None:
        tail = tail.next
      tail.next = v
    self.size += 1
</code></pre><br/>



<h3 id="삭제-연산-with-python강의">삭제 연산 With Python(강의)</h3>
<h4 id="popfornt">popFornt</h4>
<pre><code>def popFront(self):
  if len(self) == 0:
    return None
  else:
    x = self.head
    key = x.key
    self.head = x.next
  self.size -= 1
  del x
  return key</code></pre><h4 id="popback">popBack</h4>
<pre><code>def popBack(self):
  if len(self) == 0: return None
  else: # running techinque
    prev, tail = None, self.head
    while tail.next != None:
      prev = tail
      tail = tail.next
    if len(self) == 1:
      self.head = None
    else
      prev.next = tail.next # None
      key = tail.key
      del tail
      self.size -= 1
      return key</code></pre><br/>

<h3 id="탐색-연산-with-python강의">탐색 연산 With Python(강의)</h3>
<pre><code>def search(self, key):
  # key 값의 노드를 리턴, 없으면 None 리턴
  v = self.head
  while v.next != None:
    if v.key == key:
      return v
    v = v.next
  return None # or return v (== None)</code></pre><p>한 방향 연결 리스트를 for...of 루프로 순회하는 법, 즉 제너레이터와 이터러블 구현을 설명해줄게. </p>
<h4 id="제너레이터">제너레이터</h4>
<ul>
<li>한 방향 연결리스트를 for ..of 루프로 <strong>순회하려면 이터러블(iterable) 이어야 한다.</strong> (pytho,js 모두 동일)</li>
<li>따라서 yield가 포함된 제너레이터 함수를 사용하여 간단하게 이터러블을 만든 후 사용해야한다.</li>
</ul>
<h3 id="with-js-코드">With JS 코드</h3>
<pre><code>class MyList
...

// 해당 코드 !!
*[Symbol.iterator]() {
    let current = this.head;
    while (current !== null) {
      yield current.value;
      current = current.next;
    }
  }
</code></pre><h4 id="탐색-사용법">탐색 사용법</h4>
<pre><code>const list = new MyList();
for (let value of list) {
  console.log(value);
}</code></pre><ul>
<li>클래스에 위와 같은 이터러블이 생성되어있다면 그 클래스로 만든 객체 즉 한 방향 연결리스트가 for문으로 순회가 가능하게 된다!!!</li>
</ul>
<br/>


<h3 id="한방향-연결리스트의-수행-시간">한방향 연결리스트의 수행 시간</h3>
<ul>
<li>pushFront: O(1)</li>
<li>popFront: O(1)</li>
<li>pushBack: O(n)</li>
<li>popBack: O(n)</li>
</ul>
<p>pushBack과 popBack은 tail을 쫓아가야하기 때문에 n번 필요</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 연결리스트]]></title>
            <link>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%97%B0%EA%B2%B0%EB%A6%AC%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%97%B0%EA%B2%B0%EB%A6%AC%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Mon, 14 Jul 2025 08:04:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 자료는 유튜브 신찬수 한국외대 교수님 강의를 시청하고 정리했으며
코딩테스트 합격자되기 자바스크립트편 도서를 읽고 추가적으로 정리되어 있습니다. 🙏🏻</p>
</blockquote>
<h3 id="배열-vs-연결리스트">배열 vs 연결리스트</h3>
<ul>
<li>배열은 메모리 공간에 순차적으로 배치되므로 인덱스로 접근이 가능하지만,</li>
<li>연결리스트는 연속된 메모리 공간에 들어있는 것이 아닌 <strong>메모리상에서 흩어져 있어서</strong> 상수시간 접근이 불가능</li>
</ul>
<br/>

<h2 id="연결리스트-linked-list">연결리스트 (Linked List)</h2>
<ul>
<li>노드란 data 값(key)과 주소의 값(link)로 구성된 한쌍을 말하며,
노드와 링크로 연결된 자료구조를 말한다.</li>
<li>가장 앞의 노드를 헤드 노드(head node)로 칭한다.</li>
<li>마지막 노드에는 None/null이 있다</li>
</ul>
<h4 id="장점">장점</h4>
<ul>
<li>배열은 중간에 데이터를 삽입하게 되면 뒤에 있는 데이터들이 n번씩 밀려나면서 이동해야하지만, <strong>연결리스트는 중간에 새로운 노드 삽입 후 다시 연결만 하면 된다. 즉, 두 개의 링크 주소만 바꾸면 된다.</strong> </li>
<li>따라서 insert 작업에는 O(1)(상수시간)이 걸린다</li>
</ul>
<h3 id="연결리스트의-종류">연결리스트의 종류</h3>
<ul>
<li>한 방향, 양 방향 연결리스트 2종류가 있음</li>
</ul>
<h4 id="한-방향-리스트">한 방향 리스트</h4>
<ul>
<li>링크가 한쪽 방향으로만 연결되어 있는 것으로, <strong>한 방향으로만 갈 수</strong> 있고 반대 방향으로는 갈 수 없다.</li>
</ul>
<h4 id="양-방향-리스트">양 방향 리스트</h4>
<ul>
<li>양쪽 방향으로 링크가 있어서 <strong>노드의 양쪽 방향으로 모두 이동할 수</strong> 있다.</li>
</ul>
<br/>



<h3 id="구현하기">구현하기</h3>
<blockquote>
<p>유튜브 강의는 파이썬으로 작성되어 있습니다.</p>
</blockquote>
<pre><code>class Node:
  def __init__(self, key = None):
    self.key = key
    self.next = None
  def __str__(self):
    return str(self.key) //print(v.key) 대신 print(v)로 쓸 수 있다.</code></pre><h4 id="사용-예시">사용 예시</h4>
<pre><code>a = Node(3)
b = Node(9)
c = Node(-1)

a.next = b
b.next = c</code></pre><ul>
<li>c 노드를 테일 노드(tail node)라고 부른다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 큐]]></title>
            <link>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%81%90</link>
            <guid>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%81%90</guid>
            <pubDate>Fri, 11 Jul 2025 11:04:40 GMT</pubDate>
            <description><![CDATA[<h2 id="queue-큐">Queue 큐</h2>
<ul>
<li>FIFO(First In First Out)의 구조를 갖춘 자료구조</li>
<li>먼저 들어온 것이 먼저 나가는 특징</li>
</ul>
<h3 id="스택의-연산">스택의 연산</h3>
<ul>
<li>삽입: push</li>
<li>삭제: pop</li>
</ul>
<h4 id="배열을-이용한-큐">배열을 이용한 큐</h4>
<pre><code>class Queue {
    items = [];
    front = 0;
    rear = 0;

    pop() {
        return this.items[this.front++];
    }

    push(item) {
        this.items.push(item)
        this.rear++;
    }

    isEmpty() {
        return this.front === this.rear
    }
}</code></pre><h4 id="연산의-수행시간">연산의 수행시간</h4>
<ul>
<li><code>pop</code> : O(1)(상수 시간)</li>
<li><code>push</code> : O(1)</li>
<li><code>isEmpty</code> : O(1)</li>
</ul>
<h2 id="큐-예제-1-요세푸스-문제">큐 예제 1) 요세푸스 문제</h2>
<p>(코딩테스트 합격자되기 도서 참고)</p>
<ul>
<li>N명의 사람이 원형으로 서 있다.</li>
<li>임의의 숫자 K가 주어졌을 때 다음과 같이 사람을 없앤다</li>
</ul>
<ol>
<li>1번 번호표를 가진 사람을 기준으로 K번째 사람 제거</li>
<li>제거된 사람 다음 사람을 기준으로 다시 K번째 사람 제거</li>
</ol>
<p>N과 K가 주어질 때 마지막에 살아 남은 사람의 번호는?</p>
<pre><code>class Queue {
    items = [];
    front = 0;
    rear = 0;

    size() {
        return this.rear - this.front;
    }

    pop() {
        return this.items[this.front++];
    }

    push(item) {
        this.items.push(item);
        this.rear++;
    }
}


function Josephus(n, k) {
    const queue = new Queue();

    //1부터 n까지 queue에 넣기
    for(let i = 1; i &lt;= n; i++) {
        queue.push(i);
    }
    // 한 사람이 남을 때까지 반복    
    while(queue.size() &gt; 1) {
        // k-1번째 사람은 pop+ push
        for(let i = 0; i &lt; k-1; i++) {
            queue.push(queue.pop());
        }
        queue.pop();
    }

    return queue.pop();
    // 마지막에 남아있는 사람의 번호
}
</code></pre><ul>
<li>k-1번째까지의 사람은 pop한 뒤 다시 queue에 넣어야함! =&gt; for문</li>
<li>K번째 사람은 pop</li>
<li>한 사람이 남을 때까지 반복</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 스택]]></title>
            <link>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%8A%A4%ED%83%9D</link>
            <guid>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%8A%A4%ED%83%9D</guid>
            <pubDate>Fri, 11 Jul 2025 10:20:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 자료는 유튜브 신찬수 한국외대 교수님 강의를 시청하고 정리했으며 
코딩테스트 합격자되기 자바스크립트편 도서를 읽고 추가적으로 정리되어 있습니다. 🙏🏻</p>
</blockquote>
<br/>

<h2 id="stack-스택">Stack (스택)</h2>
<ul>
<li>삽입은 가장 아래부터 쌓이게 되고, 가장 맨 위에(마지막에 들어온) 있는 값이 가장 먼저 삭제된다. <strong>(LIFO)</strong></li>
</ul>
<h3 id="스택의-연산">스택의 연산</h3>
<ul>
<li>삽입: push</li>
<li>삭제: pop</li>
</ul>
<blockquote>
<p>강의 내에는 파이썬으로 작성되어있어,
해당 코드는 코딩테스트 합격자되기 도서를 참고해서 작성했습니다.</p>
</blockquote>
<pre><code>const stack = []; // 스택 초기화
const maxSize = 10; // 스택의 최대 크기

function isFull(stack) {
    return stack.length === maxSize;
}

function isEmpty(stack) {
    return stack.length === 0;
}

function push(stack, item) {
    if(isFull(stack)) {
        console.log(&#39;가득 찼습니다&#39;);
    } else {
        stack.push(item);
    }
}

function pop(stack){
    if(isEmpty(stack)) {
        return null;
    } else {
        return stack.pop();
    }
}
</code></pre><h3 id="연산의-수행시간">연산의 수행시간</h3>
<ul>
<li><code>isFull</code> : O(1)(상수 시간)</li>
<li><code>isEmpty</code> : O(1)</li>
<li><code>push</code>: O(1)</li>
<li><code>pop</code>: O(1)</li>
</ul>
<br/>


<h2 id="스택-예제-1-괄호-맞추기">스택 예제 1) 괄호 맞추기</h2>
<p>(코딩테스트 합격자되기 도서 참고)</p>
<ul>
<li>소괄호는 짝을 맞춘 열린 괄호&#39;&#39;와 닫힌 괄호&#39;&#39;로 구성</li>
<li>문제에서는 열린 괄호나 닫힌 괄호가 마국 뒤섰인 문자열을 준다.</li>
<li>이때 정상으로 열고 닫혔는지 판별하는 solution()함수 구현</li>
<li>정상적으로 열고 닫혔다면 true, 아니라면 false return</li>
</ul>
<pre><code>function solution(str) {
    cosnt stack = []; // 하나씩 담을 저장소 

    for(const s of str) {
        if(s === &#39;(&#39;) {
            stack.push(s);
        } else if (s === &#39;)&#39;) {
            if(stack.length === 0) {
                return false;
            } else {
                stack.pop();
            }
        }
    }

    return stack.length === 0;
}</code></pre><ul>
<li>열린 괄호 &#39;(&#39;는 언젠가는 닫혀야 하는 괄호이므로 스택에서 기다리게 할 것.</li>
<li>닫는 괄호 &#39;)&#39;는 앞에서 열린 게 있어야 닫을 수 있으니까 스택에서 마지막에 연 괄호를 pop해서 짝을 맞추는 역할</li>
</ul>
<p>case1 &#39;(&#39;가 먼저 있다 =&gt; stack에 push
case2 &#39;)&#39;가 왔고 스택이 비어있다? =&gt; 바로 false =&gt; 예) &#39;)(&#39; 
case3 &#39;)&#39;가 왔고, 스택이 비어있지 않다 =&gt; &#39;(&#39;는 있을테니까 열고 닫히게 되었으므로 stack pop!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 순차적 자료구조 소개]]></title>
            <link>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%88%9C%EC%B0%A8%EC%A0%81-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%88%9C%EC%B0%A8%EC%A0%81-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Thu, 10 Jul 2025 09:33:39 GMT</pubDate>
            <description><![CDATA[<h2 id="1-배열-리스트">1. 배열, 리스트</h2>
<ul>
<li>index로 임의의 원소를 접근할 수 있다.</li>
<li>연산자 []로 접근할 수 있다. 상수시간(O(1))에 값을 알 수 있다.</li>
<li><strong>삽입</strong>: push, shift</li>
<li><strong>삭제</strong>: pop, unshift</li>
</ul>
<h2 id="2-stack-queue-dequeue">2. Stack, Queue, deQueue</h2>
<ul>
<li>제한된 접근(삽입,삭제)만 허용한다.</li>
</ul>
<h3 id="stack">Stack</h3>
<ul>
<li>LIFO(Last In First Out)</li>
<li>마지막에 들어간 것이 첫번째로 나오는 특징</li>
<li><code>push</code>: 아래에서부터 차곡차곡 삽입된다.</li>
<li><code>pop</code>: 맨 위 값(가장 나중에 들어온 값)에서부터 삭제한다.</li>
</ul>
<h3 id="queue">Queue</h3>
<ul>
<li>FIFO(First In First Out)</li>
<li>처음 들어간 것이 첫번째로 나오는 특징 (은행 줄 기다리기)</li>
<li><code>push</code> : 삽입은 아래서부터 차곡차곡 쌓인다.</li>
<li><code>pop</code>: 삭제 역시 아래의 값(가장 먼저 들어온 값)부터 수행한다.</li>
</ul>
<h3 id="dequeue">deQueue</h3>
<ul>
<li>stack + queue 자료구조</li>
<li>삽입은 위, 아래로 수행할 수 있다.</li>
<li>삭제 역시 위, 아래로 수행할 수 있다.</li>
</ul>
<h2 id="3linked-list연결리스트">3.linked list(연결리스트)</h2>
<ul>
<li>데이터를 저장할 때, 각 요소가 다음 요소의 주소(포인터)를 함께 저장해서 연결된 형태로 구성된 자료구조</li>
<li>인덱스를 바로 접근할 수 없다.</li>
<li>포인터로 요소들을 연결해 만든 유연한 자료구조
→ 삽입/삭제엔 강하지만, 인덱스 접근은 느림</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[순차적 자료구조 : 배열과 리스트]]></title>
            <link>https://velog.io/@kxun_ii/%EC%88%9C%EC%B0%A8%EC%A0%81-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EB%B0%B0%EC%97%B4%EA%B3%BC-%EB%A6%AC%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@kxun_ii/%EC%88%9C%EC%B0%A8%EC%A0%81-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EB%B0%B0%EC%97%B4%EA%B3%BC-%EB%A6%AC%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Thu, 10 Jul 2025 09:19:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 자료는 유튜브 신찬수 한국외대 교수님 강의를 시청하며 정리했습니다. 🙏🏻 </p>
</blockquote>
<h2 id="배열array-vs-리스트list">배열(Array) VS 리스트(List)</h2>
<ul>
<li>가장 기본적인 순차적인(순서대로) 자료구조</li>
</ul>
<p>메모리 구조와 동작 방식에서 차이가 있음</p>
<pre><code>const A = [2, 4, 0, 5];</code></pre><h3 id="배열">배열</h3>
<p>배열은 index를 이용해 특정 위치의 값을 상수시간 내에 읽고 쓸 수 있게 해주는 자료구조이다.</p>
<h3 id="리스트">리스트</h3>
<p>리스트는 각 원소가 따로따로 저장되며, 포인터(주소)를 통해 연결되는 자료구조이다.</p>
<p>2, 4, 0, 5는 객체가 되고, 따로 저장되며, A[0]은 2가 저장된 곳의 주소를 가리키고, A[1]은 4가 저장된 곳의 주소를 가리킨다.</p>
<p>A[2] = A[2] + 1을 수행하면 
A[2]가 변경되는 것이 아니라, 그대로 0이 있고 1을 갖고있는 객체가 연결되며,
A[2]가 가리키던 0 객체를 더이상 가리키지 않고, 1을 갖고 있는 객체를 가리키게 된다.</p>
<h3 id="리스트의-이점">리스트의 이점</h3>
<ul>
<li>용량을 자동 조절한다. 공간이 부족하면 더 큰 용량을 할당하여 값을 저장.</li>
<li>동적 배열(Dynamic Array) =&gt; 처음부터 고정된 크기를 가지지 않는다!</li>
</ul>
<pre><code>let arr = [];
for (let i = 0; i &lt; 10000; i++) {
  arr.push(i);  // 크기를 미리 선언하지 않아도 계속 들어감
}
console.log(arr.length); // 10000
</code></pre><h3 id="list의-연산별-시간복잡도big-o">list의 연산별 시간복잡도(Big-O)</h3>
<h4 id="python">Python</h4>
<p>A.append(), A.pop: O(1) 평균
A.insert(), A.remove(): O(n) w.c
A.index(), A.count(): O(n) w.c</p>
<h4 id="js">JS</h4>
<p>A.push(), A.pop() =&gt; O(1)
A.unshift(), A.shift() =&gt; O(n)
A.indexOf(),A.filter()... =&gt; O(n)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 시간복잡도 BigO]]></title>
            <link>https://velog.io/@kxun_ii/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8B%9C%EA%B0%84%EB%B3%B5%EC%9E%A1%EB%8F%84-BigO</link>
            <guid>https://velog.io/@kxun_ii/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8B%9C%EA%B0%84%EB%B3%B5%EC%9E%A1%EB%8F%84-BigO</guid>
            <pubDate>Thu, 10 Jul 2025 08:07:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 자료는 유튜브 신찬수 한국외대 교수님 강의를 시청하며 정리했습니다. 🙏🏻</p>
</blockquote>
<h4 id="앞서-4강에서-본-3개의-함수의-시간-복잡도">앞서 4강에서 본 3개의 함수의 시간 복잡도:</h4>
<ul>
<li>Algorithm1(arrayMax): T1(n) = 2n - 1</li>
<li>Algorithm2(sum1): T2(n) = 4n + 1</li>
<li>Algorithm3(sum2): T3(n) = (3/2)n2 - (3/2)n + 1</li>
</ul>
<h4 id="도출할-수-있는-명제">도출할 수 있는 명제:</h4>
<ul>
<li>Algorithm2가 Algorithm1보다 2배 느리다.</li>
<li>Algorithm3는 n &lt; (5/3) 이면 Algorithm2보다 빠르다.</li>
<li>Algorithm3는 모든 n에 대해서 Algorithm1보다 느리다.</li>
<li>Algorithm3는 n &gt; 5/3이면 항상 Algorithm2보다 느리다
<img src="https://velog.velcdn.com/images/kxun_ii/post/83d54a38-8b7d-4d6e-9bd6-dc7c91abb111/image.png" alt=""></li>
</ul>
<p>T1(n), T2(n)은 n에 대해 선형적으로 증가. 최고차항이 1차항
T3(n)은 n에 대해 제곱으로 증가. 최고차항이 n의 2제곱</p>
<p><strong>n에 대한 최고차항 = 단위 시간의 증가율을 결정</strong></p>
<h3 id="big-o-표기법">Big-O 표기법</h3>
<blockquote>
<p>함수 값을 결정하는 최고차항만으로 간단하게 표기</p>
</blockquote>
<p>T1(n) = 2n-1 =&gt; <strong>T1(n) = O(n)</strong>
T2(n) = 4n+1 =&gt; <strong>T2(n) =  O(n)</strong>
T3(n) = 3/2n2-3/2n+1 =&gt; *<em>T3(n) = O(n^2) *</em></p>
<h4 id="표기방법">표기방법</h4>
<ol>
<li>최고차항만 남긴다.</li>
<li>최고차항 계수(상수) 생략한다.</li>
<li>Big-O로 감싸서 적는다 =&gt; O(최고차항)</li>
</ol>
<h4 id="집합으로-이해하기">집합으로 이해하기</h4>
<p>T1(n) = O(n)
T2(n) = O(n)
위 두 함수는 O(n) 집합 안에 들어있는 것으로 보면 된다. </p>
<p>즉,</p>
<blockquote>
<p>T1(n) ∈ O(n)
T2(n) ∈ O(n)</p>
</blockquote>
<h3 id="문제-예시1">문제 예시1</h3>
<pre><code>def increment_one(a):
    return a + 1</code></pre><p>T1(n) = 1 
=&gt; 0(n^0) 
=&gt; <strong>O(1)</strong></p>
<h3 id="문제-예시2">문제 예시2</h3>
<pre><code>def number_of_bits(n):
    count = 0
    while n &gt; 0 :
        n = n // 2
        count += 1

    return count
</code></pre><p>n/2^count = 1
n = 2^count
10g2n = count
즉 while문 log2n번 실행</p>
<p>T(n) =C log2n + 1
=&gt;** O(log2n)**</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 시간복잡도2]]></title>
            <link>https://velog.io/@kxun_ii/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%842</link>
            <guid>https://velog.io/@kxun_ii/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%842</guid>
            <pubDate>Thu, 10 Jul 2025 07:33:23 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 자료는 유튜브 신찬수 한국외대 교수님 강의를 시청하며 작성되어 있습니다. 🙏🏻</p>
</blockquote>
<h2 id="알고리즘-시간-복잡도time-complexity">알고리즘 시간 복잡도(time complexity)</h2>
<h3 id="알고리즘-시간-복잡도를-계산하는-방법">알고리즘 시간 복잡도를 계산하는 방법</h3>
<ol>
<li>모든 입력에 대해 기본연산 횟수를 더한 후 평균(<strong>현실적으로 불가능</strong>)</li>
<li>가장 안 좋은 입력(Worstcase input)에 대한 기본 연산 횟수를 측정(worstcase time complexity)</li>
</ol>
<p>2번의 방법 경우 어떤 입력에서도 W.T.C보다 수행시간이 크지 않다. 즉 연산 횟수가 보장된다!</p>
<h3 id="알고리즘-수행-시간-복잡도를-정의하는-방법">알고리즘 수행 시간 복잡도를 정의하는 방법</h3>
<blockquote>
<p>알고리즘 수행 시간 = <strong>최악의 입력에 대한 기본 연산 횟수</strong></p>
</blockquote>
<h4 id="예시">예시</h4>
<p>input: n개의 정수를 갖는 배열 A
output: A의 수 중에서 최대값 리턴</p>
<pre><code>alogirithm arrayMax(A, n):
  currentMax = A[0]

  for i = 1 to n-1 do
    if currentMax &lt; A[i]:
      currentMax = A[i]
  return currentMax</code></pre><p>위 코드에서 currentMax &lt; A[i] 조건이 항상 참일 경우 가장 연산횟수가 크다.</p>
<p>예시: A = [2, 5, 7, 9, 15, 26]</p>
<p>for문이 n - 1만큼 실행할 때 기본연산은 2번 수행씩 된다. (if 참인지 비교, 참일경우 하위 코드 실행)</p>
<p>대입연산: <strong>1</strong> 단위시간
반복문: (n - 1) * 2 = <strong>2n - 2</strong> 단위시간</p>
<p>즉, T(n) = 2n - 1</p>
<p>n = 6 일때, T(6) = 12 - 1 = 11</p>
<h3 id="시간-복잡도-예시-1">시간 복잡도 예시 1</h3>
<pre><code>algorithm sum1(A, n):
  sum = 0                  # 대입연산 (1 단위시간)
  for i = 0 to n - 1 do
    if A[i] % 2 == 0:    # 나누고, == 맞는지 확인(2 단위시간)
      sum += A[i]        # 더하고 대입하라(2 단위시간)
  return sum</code></pre><p><strong>A: 모든 값이 짝수인 경우 W.C 입력인 것</strong></p>
<blockquote>
<p>T(n) = 4n + 1</p>
</blockquote>
<p>=&gt; for 문이 n번 돌고, if 2번, 참일 경우 2번 즉 4번, 첫번째 대입 해서 1번!</p>
<h3 id="시간-복잡도-예시-2">시간 복잡도 예시 2</h3>
<pre><code>algorithm sum2(A, n)
  sum = 0                 # 대입 연산(1 단위시간)
  for i = 0 to n - 1 do   
    for j = i to n - 1 do  
      sum += A[i] * A[j] # 더하고 대입(2 단위), 곱하기(1 단위)
  return sum</code></pre><p><img src="https://velog.velcdn.com/images/kxun_ii/post/6fce9fab-39e0-4116-a4fc-8f10024a587b/image.png" alt="">
두번째 for문에 대해 j가 1+2+3+...+n 번인 것임므로 n(n+1)/2</p>
<p>=&gt; for문이 n번 돌고, 그 하위 for문은 n(n+1)/2, if 부분은 3번,
즉 n(n+1) / 2* 3 * n, 첫번째 대입 해서 1번!</p>
<blockquote>
<p>T(n) = ((3/2)n)(n+1) + 1
     = (3/2)n^2 - (3/2)n + 1</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 시간복잡도1]]></title>
            <link>https://velog.io/@kxun_ii/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8B%9C%EA%B0%84%EB%B3%B5%EC%9E%A1%EB%8F%841</link>
            <guid>https://velog.io/@kxun_ii/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8B%9C%EA%B0%84%EB%B3%B5%EC%9E%A1%EB%8F%841</guid>
            <pubDate>Thu, 10 Jul 2025 07:02:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 자료는 유튜브 신찬수 한국외대 교수님 강의를 시청하며 작성되어 있습니다. 🙏🏻</p>
</blockquote>
<h2 id="자료구조와-알고리즘-성능1-가상컴퓨터--가상언어--가상코드">자료구조와 알고리즘 성능1: 가상컴퓨터 + 가상언어 + 가상코드</h2>
<p>자료구조와 이를 해결하기 위한 알고리즘이 어느 정도의 성능을 보이는지 측정하고 비교해보자.</p>
<h3 id="현실의-문제점">현실의 문제점</h3>
<h4 id="문제-1">문제 1</h4>
<ul>
<li>&quot;자료구조&quot;와 &quot;알고리즘&quot;을 생각하여 코드(C, Javascript, Python 등)로 구현하면 컴퓨터에서 동작하게 되지만,</li>
<li>각 컴퓨터마다 HW/SW 환경이 다를 수 있으므로 다른 성능을 낼 수 있다</li>
</ul>
<p>-&gt; 즉 이는 큰 문제가 될 수 있다.</p>
<h4 id="문제-2">문제 2</h4>
<ul>
<li>입력의 크기가 다양할 수 있다.</li>
<li>입력이 커질 수록 실행 시간도 많아지는데, 이 입력에 대해 코드 실행속도를 어떻게 정의하고 비교할 수 있을까?</li>
</ul>
<h3 id="해결책">해결책</h3>
<p>가상의 컴퓨터(Virtual Machine) + 가상언어(Pseudo language) + 가상코드(Pseudo Code) 를 사용하자.</p>
<p>이렇게 하면 HW/SW 환경에 독립적이기 때문에 누구나 같은 환경에서 알고리즘을 객관적으로 비교할 수 있게 된다.</p>
<h3 id="가상컴퓨터">가상컴퓨터</h3>
<p>가상 컴퓨터(Virtual Machine/Random Access Machine)
Turing Machine -&gt; von Neumann: RAM(Random Access Machine)</p>
<p>RAM = CPU + Memory + 기본연산(단위시간에 수행되는 연산들의 모음)</p>
<p>즉, CPU와 Memory와 함께 계산을 하는 것이 알고리즘이고,
알고리즘이 RAM이라는 가상컴퓨터 위에서 돌아가는 것!</p>
<p>RAM은 가상의 컴퓨터 환경으로써 알고리즘 성능을 측정하는 기준 플랫폼이다.</p>
<h4 id="기본연산">기본연산</h4>
<p>배정, 대입, 복사 연산 등과 같이 일반적으로 한 단위 시간 안에 수행되는 연산</p>
<ol>
<li><code>A = B</code> (Read and Write)</li>
<li>산술연산: +, -, *, / (%, 내림, 올림, 반올림 등은 기본 연산으로 보지 않으나, 수업에서는 이들도 단위시간에 들어가는 것으로 간주)</li>
<li>비교연산: &gt;, &gt;=, &lt;, &lt;=, ==, !=</li>
<li>논리연산: AND, OR, NOT</li>
<li>비트연산: bit-AND, OR, NOT</li>
</ol>
<p>즉, 우리는 기본연산으로 정의한 가상의 컴퓨터를 RAM이라고 칭하고, 
이 RAM 모델 위에서 알고리즘이 실행되고 측정되는 시간을 비교하는
 알고리즘 복잡도를 계산하고 비교할 것이다.</p>
<h3 id="가상-언어-pseudovirtual-languages">가상 언어 (Pseudo/Virtual Languages)</h3>
<ol>
<li>배정, 산술, 비교, 논리, bit-논리 연산 등 기본연산을 표현할 수 있으면 됨</li>
<li>비교: if, if else, if elseif... else 등</li>
<li>반복: for, while 등</li>
<li>함수: 정의, 호출, 반환 문법</li>
</ol>
<h3 id="가상코드-pseudovirtual-code">가상코드 (Pseudo/Virtual Code)</h3>
<ol>
<li>실제 돌아가는 코드가 아닌 가상머신(RAM)에서 돌아가는 코드이므로 자유롭게 기술할 수 있다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[다시 시작해보자]]></title>
            <link>https://velog.io/@kxun_ii/%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@kxun_ii/%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 09 Jul 2025 10:12:27 GMT</pubDate>
            <description><![CDATA[<h3 id="퇴사-후-이직-준비">퇴사 후 이직 준비</h3>
<p>작년 9월에 퇴사를 하고 올해부터 다시 이직준비를 하고 있다.🔥
올해는 벌써 상반기가 끝났고 하반기를 앞두고 있지만 여전히 코딩테스트는 어렵고, 이력서는 n번째 수정중이며 잘하고 있는지 불안한건 똑같다 🥲</p>
<br/>
<br/>

<p><img src="https://velog.velcdn.com/images/kxun_ii/post/f4d9e183-ff16-4c57-908e-3af69330b034/image.png" alt=""></p>
<p>[코딩테스트 합격자 되기 자바스크립트편 도서]를 추천받아 읽으면서 문제를 풀고 있고 문제풀이는 
<a href="https://github.com/rkddbs1031/algorithm">여기 깃허브</a>에 올리고 있다🔥</p>
<p>아직 확실히 풀지는 못하고 있지만 계속 하다보면 되겠지..!!!!!!!!</p>
<p>혼자 공부하는 것보다 기록하면서 공부하면 더 이해도가 높아질 것 같아 지금이라도 다시 해보려고 한다!!</p>
<p>자료구조와 알고리즘 강의도 추천받아 듣고 있고,
핑계되며 반은 안/못 읽은 모던 자바스크립트 딥다이브 도서도 다시 읽으면서 기록해야겠다😭</p>
<p>진짜 다시 화이팅이다 !!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조와 알고리즘]]></title>
            <link>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@kxun_ii/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Wed, 09 Jul 2025 09:49:56 GMT</pubDate>
            <description><![CDATA[<h3 id="자료-data-structure-알고리즘algorithm">자료 (Data Structure), 알고리즘(Algorithm)</h3>
<h4 id="자료구조">자료구조</h4>
<ul>
<li><p>자료는 즉 data를 말하며 해당 데이터를 담는 저장공간(memory)이 필요 
=&gt; 데이터 값의 모임</p>
</li>
<li><p>저장된 데이터를 효과적으로 &quot;<strong>읽기, 쓰기, 삽입,삭제, 탐색</strong>&quot;와 같은 연산을 제공하여 수행할 수 있어야 한다.</p>
</li>
</ul>
<h4 id="알고리즘">알고리즘</h4>
<ul>
<li>입력된 데이터를 가지고 문제를 해결하는 논리적인 절차 / 방식</li>
</ul>
<Br/> 
<Br/>
<Br/> 



<h3 id="최대-공약수-알고리즘-gcd">최대 공약수 알고리즘 GCD</h3>
<p>gcd(8, 12) =&gt; max {1,2,4} =&gt; 즉, 4!</p>
<h4 id="1-빼기-방식">1. 빼기 방식</h4>
<ul>
<li>두 수를 비교해서 큰 수, 작은 수를 나타내고,</li>
<li>큰 수에서 작은 수를 빼고 값을 대입,</li>
<li>이 행동을 반복해서 한 수가 0이 되는 경우 그 때 큰수가 최대 공약수인 것.</li>
</ul>
<pre><code>gcd(a, b) {
    while(a !== 0 &amp;&amp; b !== 0) {
        if(a &gt; b) a -= b;
           else b -=a;
    }

    return a + b; // 둘 중에 하나가 0일테니
}</code></pre><p>하지만 큰 수가 100, 작은 수가 2와 같이 큰 차이가 나는 두 수라고 하면 반복되는 횟수가 많아진다.</p>
<h4 id="2-나누기-방식">2. 나누기 방식</h4>
<ul>
<li>두 수를 비교해서 큰 수, 작은 수를 나타내고,</li>
<li>큰 수에서 작은 수를 나누고 나머지 값을 대입</li>
<li>이 행동을 반복해서 한 수가 0이 되는 경우, 그때 큰 수가 최대 공약수</li>
</ul>
<pre><code>gcd(a, b) {
    while(a !== 0 &amp;&amp; b !== 0) {
        if(a &gt; b) a %= b;
        else b %= a;
    }

    return a + b;
}
</code></pre><p>1번 방식으로는 gcd(2,100) -&gt; gcd(2,98) -&gt; gcd(2, 96)... gcd(2,0) 으로 2가 나오지만,</p>
<p>2번 방식으로는 gcd(2,100) -&gt; gcd(2, 0) 한 번 만으로 결과 출력 가능</p>
<p>즉 더 빠른 결과 값을 나타낼 수 있음</p>
<blockquote>
<p>해당 자료는 유튜브 신찬수 한국외대 교수님 강의를 시청하며 작성되어 있습니다. 🙏🏻</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS 알고리즘_28] 나누어 떨어지는 숫자 배열]]></title>
            <link>https://velog.io/@kxun_ii/JS-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%9828-%EB%82%98%EB%88%84%EC%96%B4-%EB%96%A8%EC%96%B4%EC%A7%80%EB%8A%94-%EC%88%AB%EC%9E%90-%EB%B0%B0%EC%97%B4</link>
            <guid>https://velog.io/@kxun_ii/JS-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%9828-%EB%82%98%EB%88%84%EC%96%B4-%EB%96%A8%EC%96%B4%EC%A7%80%EB%8A%94-%EC%88%AB%EC%9E%90-%EB%B0%B0%EC%97%B4</guid>
            <pubDate>Tue, 09 Aug 2022 13:31:30 GMT</pubDate>
            <description><![CDATA[<h3 id="q-array의-각-element-중-divisor로-나누어-떨어지는-값을-오름차순으로-정렬한-배열을-반환하는-함수-solution을-작성해주세요-divisor로-나누어-떨어지는-element가-하나도-없다면-배열에--1을-담아-반환하세요">Q) array의 각 element 중 divisor로 나누어 떨어지는 값을 오름차순으로 정렬한 배열을 반환하는 함수, solution을 작성해주세요. divisor로 나누어 떨어지는 element가 하나도 없다면 배열에 -1을 담아 반환하세요.</h3>
<h4 id="제한사항">제한사항</h4>
<ul>
<li>arr은 자연수를 담은 배열입니다.</li>
<li>정수 i, j에 대해 i ≠ j 이면 arr[i] ≠ arr[j] 입니다.</li>
<li>divisor는 자연수입니다.</li>
<li>array는 길이 1 이상인 배열입니다.</li>
</ul>
<h4 id="입출력-예">입출력 예</h4>
<p><img src="https://velog.velcdn.com/images/kxun_ii/post/a2603788-5d17-4841-8798-e9de26146e45/image.png" alt=""></p>
<h4 id="입출력-예1">입출력 예#1</h4>
<blockquote>
<p>arr의 원소 중 5로 나누어 떨어지는 원소는 5와 10입니다. 따라서 [5, 10]을 리턴합니다.</p>
</blockquote>
<h4 id="입출력-예2">입출력 예#2</h4>
<blockquote>
<p>arr의 모든 원소는 1으로 나누어 떨어집니다. 원소를 오름차순으로 정렬해 [1, 2, 3, 36]을 리턴합니다.</p>
</blockquote>
<h4 id="입출력-예3">입출력 예#3</h4>
<blockquote>
<p>3, 2, 6은 10으로 나누어 떨어지지 않습니다. 나누어 떨어지는 원소가 없으므로 [-1]을 리턴합니다.</p>
</blockquote>
<hr>
<h3 id="나의-풀이">나의 풀이</h3>
<pre><code>function solution(arr, divisor) {
    let answer = [];
    for(let i=0; i&lt;arr.length; i++){
        arr[i] % divisor === 0 &amp;&amp; answer.push(arr[i])
    }
    return answer.length &gt; 0 ? answer.sort((a,b) =&gt; a-b) : [-1];
} </code></pre><ol>
<li>divisor에 의해 나누어지는 배열의 원소값이 있다면 answer에 push한다.</li>
<li>answer에 값이 있다면 오름차순 정렬로 나타낸다.</li>
<li>answer에 값이 없다면 -1을 return한다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS 알고리즘_27] 약수의 개수와 덧셈]]></title>
            <link>https://velog.io/@kxun_ii/JS-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%9827-%EC%95%BD%EC%88%98%EC%9D%98-%EA%B0%9C%EC%88%98%EC%99%80-%EB%8D%A7%EC%85%88</link>
            <guid>https://velog.io/@kxun_ii/JS-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%9827-%EC%95%BD%EC%88%98%EC%9D%98-%EA%B0%9C%EC%88%98%EC%99%80-%EB%8D%A7%EC%85%88</guid>
            <pubDate>Tue, 09 Aug 2022 13:21:19 GMT</pubDate>
            <description><![CDATA[<h3 id="q-두-정수-left와-right가-매개변수로-주어집니다-left부터-right까지의-모든-수들-중에서-약수의-개수가-짝수인-수는-더하고-약수의-개수가-홀수인-수는-뺀-수를-return-하도록-solution-함수를-완성해주세요">Q) 두 정수 left와 right가 매개변수로 주어집니다. left부터 right까지의 모든 수들 중에서, 약수의 개수가 짝수인 수는 더하고, 약수의 개수가 홀수인 수는 뺀 수를 return 하도록 solution 함수를 완성해주세요.</h3>
<hr>
<h3 id="나의-풀이">나의 풀이</h3>
<pre><code>function countFn(num){
    let count = 0;

    for(let i=1; i&lt;=num; i++){
       if(num % i === 0) count++;
    }
    return count;
}

function solution(left, right) {
    let answer = 0;

    for (let i=left; i&lt;=right; i++) {
        let count = countFn(i)
        count % 2 ? answer -= i : answer += i
    }
    return answer
}</code></pre><ol>
<li>left부터 right사이에 있는 숫자를 확인해야하므로 반복문으로 작성한다.</li>
<li>가독성을 위해 countFn으로 따로 함수를 만든다.</li>
<li>countFn함수는 약수의 개수를 구하는 함수이다.</li>
<li>약수의 개수가 짝수이면 answer에 약수의 개수가 짝수인 값을 더하고 홀수라면 뺀다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS 알고리즘_26] 부족한 금액 계산하기]]></title>
            <link>https://velog.io/@kxun_ii/JS%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%9826-%EB%B6%80%EC%A1%B1%ED%95%9C-%EA%B8%88%EC%95%A1-%EA%B3%84%EC%82%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kxun_ii/JS%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%9826-%EB%B6%80%EC%A1%B1%ED%95%9C-%EA%B8%88%EC%95%A1-%EA%B3%84%EC%82%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 09 Aug 2022 09:21:47 GMT</pubDate>
            <description><![CDATA[<h3 id="q-새로-생긴-놀이기구는-인기가-매우-많아-줄이-끊이질-않습니다-이-놀이기구의-원래-이용료는-price원-인데-놀이기구를-n-번-째-이용한다면-원래-이용료의-n배를-받기로-하였습니다-즉-처음-이용료가-100이었다면-2번째에는-200-3번째에는-300으로-요금이-인상됩니다">Q) 새로 생긴 놀이기구는 인기가 매우 많아 줄이 끊이질 않습니다. 이 놀이기구의 원래 이용료는 price원 인데, 놀이기구를 N 번 째 이용한다면 원래 이용료의 N배를 받기로 하였습니다. 즉, 처음 이용료가 100이었다면 2번째에는 200, 3번째에는 300으로 요금이 인상됩니다.</h3>
<p>놀이기구를 count번 타게 되면 현재 자신이 가지고 있는 금액에서 얼마가 모자라는지를 return 하도록 solution 함수를 완성하세요.
단, 금액이 부족하지 않으면 0을 return 하세요.</p>
<p><strong>입출력 예</strong>
<img src="https://velog.velcdn.com/images/kxun_ii/post/3503da1d-83ab-4048-9127-55cc9f2f374f/image.png" alt=""></p>
<p><strong>입출력 예 설명</strong></p>
<blockquote>
<p>이용금액이 3인 놀이기구를 4번 타고 싶은 고객이 현재 가진 금액이 20이라면, 총 필요한 놀이기구의 이용 금액은 30 (= 3+6+9+12) 이 되어 10만큼 부족하므로 10을 return 합니다.</p>
</blockquote>
<hr>
<h3 id="나의-풀이">나의 풀이</h3>
<pre><code>function solution(price, money, count) {
    let totalPrice = 0;

    for(let i=1; i&lt;=count; i++) {
        totalPrice += price * i;
    }
    return money &gt; totalPrice ? 0 : totalPrice - money
}</code></pre><ol>
<li>price 만큼 count번 추가되기 때문에 반복문을 사용하여 작성하기</li>
<li>내가 가지고 있는 money가 총 필요한 금액보다 크다면 0을, 작다면(금액 부족) totalPrice-money 값을 return한다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[브라우저 저장소 정리(feat.WebStorage와 Cookie)]]></title>
            <link>https://velog.io/@kxun_ii/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%A0%95%EB%A6%ACfeat.WebStorage%EC%99%80-Cookie</link>
            <guid>https://velog.io/@kxun_ii/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%A0%95%EB%A6%ACfeat.WebStorage%EC%99%80-Cookie</guid>
            <pubDate>Tue, 19 Jul 2022 07:52:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>브라우저 저장소의 종류는 크게 Cookie와 WebStorage로 나눠지고 Webstorage는 localStorage와 sessionStorage로 이루어져 있다.</p>
</blockquote>
<h3 id="cookie란">cookie란?</h3>
<ul>
<li><p>cookie를 설정하면 이후 모든 요청은 쿠키정보를 포함하여 서버로 전송된다. =&gt; 불필요한 트래픽을 발생시킬 수 있다..!</p>
<blockquote>
<p> 사용자가 따로 요청하지 않아도 브라우저가 Request시에 Request Header를 넣어 자동으로 서버에 전송</p>
</blockquote>
</li>
<li><p>용량에 제한이 있고, 만료 기간을 가지며 일정 시간이 지나면 만료된다.</p>
</li>
<li><p>주로 서버에 HTTP 요청 시 헤더에 같이 집어 넣어 사용된다.</p>
</li>
<li><p>응답 헤더의 Set-Cookie에 담아 Key-Value 쌍의 Text 형식으로 데이터를 저장한다.</p>
</li>
</ul>
<p>Cookie의 가장 큰 단점인 서버로 매번 전송을 하는 점, 그리고 용량 제한에 대한 단점을 극복하여 나타난게 WebStorage이다.</p>
<br/>

<hr>
<br/>

<h3 id="webstorage">WebStorage</h3>
<ul>
<li>LocalStaore와 SessionStorage가 존재한다.</li>
<li>WebStorage는 key-value쌍의 구조로 데이터를 저장하고 key기반으로 데이터를 조회할 수 있다.</li>
</ul>
<h4 id="localstorage">localStorage</h4>
<p>프론트 개인프로젝트를 하면서 가장 많이 사용한 브라우저 저장소가 아닐까 싶다 🙂</p>
<ul>
<li>브라우저가 닫히는 등 세션이 만료되어도 그대로 저장되어 남아있는 특징을 가지고 있다.</li>
<li>저장한 데이터를 지우지 않는 한 영구적으로 데이터가 남아있다라는 것!</li>
</ul>
<h4 id="sessionstorage">SessionStorage</h4>
<ul>
<li>localStorage와 다르게 브라우저가 닫히는 등 세션이 만료되면 저장되어있는 정보가 사라지는 특징을 가지고 있다.</li>
</ul>
<br/>

<hr>
<br/>

<blockquote>
<p>✨요약!!</p>
</blockquote>
<ul>
<li>브라우저 저장소로는 Cookie와 WebStorage기 있다.</li>
<li>Cookie는 매번 사용자가 설정하지 않아도 매번 서버로 전송되며 용량에 제한이 있다. </li>
<li>이러한 단점을 보완하여 나온 것이 WebStorage이다.</li>
<li>WebStorage는 LocalStorage와 SessionStorage가 있다.</li>
<li>LocalStorage는 저장한 데이터를 지우지 않는 한 영구적으로 보관이 가능하다. 브라우저가 닫히는 등 세션이 만료되어도 정보가 남아있음!</li>
<li>SessionStorage는 브라우저가 닫히는 등 세션이 닫히면 정보가 사라진다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>