<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>obebe_00.log</title>
        <link>https://velog.io/</link>
        <description>다른 건 노력의 시간</description>
        <lastBuildDate>Thu, 02 Jul 2026 09:18:07 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>obebe_00.log</title>
            <url>https://velog.velcdn.com/images/obebe_00/profile/5ddf4649-3ee5-4ab6-908b-f1cf5d1c146f/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. obebe_00.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/obebe_00" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[중급 프로젝트를 마치며 - 4L 회고]]></title>
            <link>https://velog.io/@obebe_00/%EC%A4%91%EA%B8%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0-4L-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@obebe_00/%EC%A4%91%EA%B8%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0-4L-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 02 Jul 2026 09:18:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>4L : 좋았던 점(Liked), 배운 점(Learned), 부족했던 점(Lacked), 바라는 점(Longed for)을 돌아보는 회고 방식</p>
</blockquote>
<p>이 방식으로 지난 중급 프로젝트 &#39;최애의 포토&#39;에 대한 회고를 진행해보려고 한다.</p>
<hr>
<h1 id="liked-좋았던-점">Liked (좋았던 점)</h1>
<p>처음으로 BE에 집중해보겠다 다짐하고 들어갔던 프로젝트였다. 결국에는 풀스택으로 마무리했지만 이전까지 주로 FE 위주 작업만 해왔던 나로서는 긴장되는 시작이었다. 예상되는 문제도 어떻게 풀어나갈지 고민하며 BE의 재미를 느낄 수 있었던 점, 프로젝트의 전체적인 이해와 발표로 깔끔히 마무리한 점은 좋았다.</p>
<hr>
<h1 id="learned-배운-점">Learned (배운 점)</h1>
<p>단순 코드를 짠다기보다, 생각해야 한다는 점을 배웠다. 하나의 사건에 발생되는 문제들을 개발자는 생각해내야하며 그것을 어떻게 해결할건지에 대한 고찰이 필요하다. 초기 단계에 개발해야 하는 내용을 충분히 이해하고 처음부터 구조를 알맞게 작성하는 것이 효율적이다. 그리고 각 개발에 어떤 기능을 접목시킬지에 대한 정보력 또한 실력이라고 느꼈다.</p>
<hr>
<h1 id="lacked-부족했던-점">Lacked (부족했던 점)</h1>
<p>&#39;주도적&#39;이지 못했다. 팀장이 아니더라도 충분히 주도적으로 기능을 찾아온다던지, 더 좋은 코드는 무엇일지에 대해 노력했어야 했다. 이는 내가 이전에 봐왔던 팀원들에게서 배운 모습인데 내가 그러지 못했기에 프로젝트에서 나는 잘했다고 안심하거나 다른 팀원을 원망하기엔 자격이 없는 것 같다.</p>
<hr>
<h1 id="longed-for-바라는-점">Longed for (바라는 점)</h1>
<p>앞으로의 나에게 바라는 점은 &#39;같이 하고싶은 팀원&#39;이 되는 것이다. 이분이랑 하면 프로젝트가 잘 될 것 같아! 라는 생각이 함께 있는 동기들, 나아가 앞으로 마주할 팀원들에게 들게하고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[낙관적 작금 vs 비관적 잠금 - 동시성 제어]]></title>
            <link>https://velog.io/@obebe_00/%EB%82%99%EA%B4%80%EC%A0%81-%EC%9E%91%EA%B8%88-vs-%EB%B9%84%EA%B4%80%EC%A0%81-%EC%9E%A0%EA%B8%88-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@obebe_00/%EB%82%99%EA%B4%80%EC%A0%81-%EC%9E%91%EA%B8%88-vs-%EB%B9%84%EA%B4%80%EC%A0%81-%EC%9E%A0%EA%B8%88-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Thu, 25 Jun 2026 05:31:39 GMT</pubDate>
            <description><![CDATA[<p>이전 개발 내용과 이어집니다</p>
<hr>
<h2 id="상황-설명">상황 설명</h2>
<p>포토카드 거래 서비스에서 카드 구매 하나가 완료되려면 다음 작업이 모두 이루어져야 한다.</p>
<pre><code class="language-md">1. market_items    — 판매 수량(soldQuantity) 차감, 품절이면 상태 SOLD_OUT 변경
2. my_cards        — 판매자 보유 카드 수량 차감
3. user_points     — 구매자 포인트 차감
4. user_points     — 판매자 포인트 지급
5. orders          — 주문 생성
6. my_cards        — 구매자 카드 추가(upsert)</code></pre>
<p>이 구조에서 동시 요청이 들어올 경우 네 가지 문제가 발생할 수 있다.
<strong>재고 초과 판매</strong> -- 동시 구매 요청이 같은 재고를 중복 차감한다.
<strong>보유량 초과 등록</strong> — 여러 판매글에 보유 수량 이상의 카드를 등록할 수 있다.
<strong>품절 처리 중 교환 신청이 통과되는 불일치</strong> — 품절 처리와 교환 거절이 분리되어 있으면 그 사이에 새 교환 신청이 끼어든다.
<strong>동시 승인으로 발생하는 이중처리</strong> — 동시 승인 요청으로 같은 교환이 두 번 처리된다.</p>
<hr>
<h2 id="solution_1-낙관적-잠금으로-재고-초과-방지">Solution_1 낙관적 잠금으로 재고 초과 방지</h2>
<p><strong>기존 방식의 문제</strong></p>
<pre><code class="language-typescript">const item = await prisma.marketItem.findUnique({ where: { id } });

if (item.quantity - item.soldQuantity &lt; requestedQty) {
  throw new Error(&#39;재고 부족&#39;);
}

await prisma.marketItem.update({
  where: { id },
  data: { soldQuantity: { increment: requestedQty } },
});</code></pre>
<p>두 요청이 같은 재고를 <strong>동시에</strong> 읽으면 둘 다 &quot;재고 있음&quot;으로 판단하여 <strong>중복 차감</strong>이 발생한다.</p>
<pre><code class="language-md">요청 A: findUnique → soldQuantity=4, quantity=5 → 재고 1개 확인 ✓
요청 B: findUnique → soldQuantity=4, quantity=5 → 재고 1개 확인 ✓
요청 A: update soldQuantity → 5 (재고 소진)
요청 B: update soldQuantity → 6 (재고 초과!)</code></pre>
<p>Solution -- 조건부 <strong>updatdeMany</strong>
읽은 시점의 값과 현재 DB 값이 <span style="color: red">정확히 일치</span>할 때만 업데이트되는 조건을 추가한다.</p>
<pre><code class="language-typescript">const marketItem = await tx.marketItem.findUnique({
  where: { id: marketItemId },
});

const updatedStock = await tx.marketItem.updateMany({
  where: {
    id: marketItemId,
    status: &#39;SELLING&#39;,
    quantity: marketItem.quantity,          // ← 읽은 시점의 값과 정확히 일치해야만 갱신
    pricePerCard: marketItem.pricePerCard,
    soldQuantity: {
      lte: marketItem.quantity - quantity,
    },
  },
  data: {
    soldQuantity: { increment: quantity },
  },
});

if (updatedStock.count === 0) {
  throw new AppError(
    &#39;판매 가능한 카드 수량이 부족합니다.&#39;,
    409,
    ERROR_CODES.INSUFFICIENT_STOCK,
  );
}</code></pre>
<p>그 사이 다른 요청이 값을 바꿨다면 WHERE 조건이 맞지 않아 업데이트되는 행이 없게 된다. <code>count === 0</code>이면 충돌이 발생한 것으로 판단해 즉시 실패 처리한다.</p>
<h4 id="같은-원리를-두-가지-케이스에-추가-적용">같은 원리를 두 가지 케이스에 추가 적용</h4>
<p><strong>보유량 초과 등록 방지</strong> — 여러 판매글의 수량 합산을 WHERE 조건에 포함한다.</p>
<pre><code class="language-typescript">const updated = await prisma.marketItem.updateMany({
  where: {
    id: itemId,
    status: &#39;SELLING&#39;,
    soldQuantity: {
      lte: item.quantity,  // ← 기존 판매글 수량 합산을 WHERE 조건에 포함
    },
  },
  data: item,
});

if (updated.count === 0) {
  return null;
}</code></pre>
<p><strong>포인트 차감 음수 방지</strong> — <code>WHERE balance &gt;= amount</code> 조건으로 잔액 부족 시 차감을 원천 차단한다.</p>
<pre><code class="language-typescript">const deductedPoint = await tx.userPoint.updateMany({
  where: {
    userId: buyerId,
    point: { gte: totalPrice },  // ← 잔액이 충분한 경우만
  },
  data: {
    point: { decrement: totalPrice },
  },
});

if (deductedPoint.count === 0) {
  throw new AppError(
    &#39;보유 포인트가 부족합니다.&#39;,
    409,
    ERROR_CODES.INSUFFICIENT_POINTS,
  );
}</code></pre>
<hr>
<h2 id="왜-비관적-잠금이-아닌-낙관적-잠금인가">왜 비관적 잠금이 아닌 낙관적 잠금인가</h2>
<p>비관적 잠금(<code>SELECT FOR UPDATE</code>)은 트랜잭션 동안 row를 잠금 상태로 두기 때문에 다른 요청은 락 해제까지 대기해야 한다.
즉, <strong>병목</strong>과 <strong>데드락</strong>의 위험이 있다.</p>
<table>
<thead>
<tr>
<th></th>
<th>비관적 잠금</th>
<th>낙관적 잠금</th>
</tr>
</thead>
<tbody><tr>
<td><strong>처리 방식</strong></td>
<td>락을 걸고 순차 처리</td>
<td>락 없이 동시 처리</td>
</tr>
<tr>
<td><strong>충돌 감지</strong></td>
<td>대기 후 성공</td>
<td>count === 0이면 즉시 실패</td>
</tr>
<tr>
<td><strong>읽기 성능</strong></td>
<td>블로킹 가능</td>
<td>유지됨</td>
</tr>
<tr>
<td><strong>데드락</strong></td>
<td>위험 있음</td>
<td>없음</td>
</tr>
</tbody></table>
<p>따라서 락 없이 동시처리가 가능하며 충돌 시 즉시 실패를 감지하고, 읽기 성능을 유지하며 대기없이 처리할 수 있다는 점에서 낙관적 잠금을 선택했다.</p>
<p><strong>이중 방어 구조</strong>
낙관적 잠금을 선택한 만큼 안정성을 보완하기 위해 2단계 방어 구조를 구성했다.</p>
<pre><code class="language-md">1차 — 애플리케이션 레벨
  재고 조회 후 수량 확인 → 사전 검증
  → 명백히 불가능한 요청을 빠르게 걸러냄

2차 — DB 레벨 (낙관적 잠금)
  조건부 updateMany 원자 연산
  → 1차를 뚫고 들어온 동시 요청도 차단</code></pre>
<hr>
<p>낙관적 잠금의 핵심은 <code>quantity &gt; 0</code>이 아닌 <code>quantity = 읽은_시점_값</code>이다. 이 차이가 &quot;재고가 0이 되었는지&quot;를 확인하는 것과, &quot;내가 읽은 이후 아무도 건드리지 않았는지&quot;를 확인하는 것의 차이다.</p>
<p><code>updateMany</code>가 영향을 준 row 수(<code>count</code>)를 항상 확인하는 습관을 들이자. 그 숫자가 0이라면 누군가 먼저 선점한 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동시성 버그: 교환 제안에 숨어있던 TOCTOU 문제와 해결 과정]]></title>
            <link>https://velog.io/@obebe_00/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%B2%84%EA%B7%B8-%EA%B5%90%ED%99%98-%EC%A0%9C%EC%95%88%EC%97%90-%EC%88%A8%EC%96%B4%EC%9E%88%EB%8D%98-TOCTOU-%EB%AC%B8%EC%A0%9C%EC%99%80-%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@obebe_00/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%B2%84%EA%B7%B8-%EA%B5%90%ED%99%98-%EC%A0%9C%EC%95%88%EC%97%90-%EC%88%A8%EC%96%B4%EC%9E%88%EB%8D%98-TOCTOU-%EB%AC%B8%EC%A0%9C%EC%99%80-%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Thu, 18 Jun 2026 11:02:13 GMT</pubDate>
            <description><![CDATA[<p><code>데이터 정합성</code> 이것을 키워드로 두고 개발을 하며 포인트를 활용한 거래 시스템에서 재고가 초과 판매 되거나, 포인트가 음수가 되거나, 중복 주문이 생기는 상황을 막기 위해 Prisma의 <code>$transaction</code>과 조건부 갱신(Optimistic Concurrency Control)을 적극적으로 활용해보았다.</p>
<p>이 과정에서 발생한 교환 제안 생성 로직의 <strong>TOCTOU(Time of Check to Time of Use)</strong>에 대해 이야기를 나눠보고자 한다.</p>
<hr>
<h1 id="toctou란">TOCTOU란?</h1>
<p>TOCTOU는 &quot;확인한 시점&quot;과 &quot;사용한 시점&quot; 사이의 간격에서 발생하는 Race Condition이다.</p>
<p>쉽게 말하면 이렇다.</p>
<blockquote>
<p>A가 냉장고에 계란이 1개 있는 걸 확인했다.
B도 냉장고에 계란이 1개 있는 걸 확인했다.
A가 계란을 꺼냈다.
B도 계란을 꺼내려 했지만 이미 없다.</p>
</blockquote>
<p>&quot;확인&quot;과 &quot;사용&quot; 사이에 다른 누군가가 끼어들 수 있다는 게 핵심이다.
싱글스레드 환경이라면 문제가 없지만, 서버는 수많은 요청을 동시에 처리하기 때문에 이 간격이 버그의 원인이 된다.</p>
<hr>
<h1 id="기존-코드와-문제점">기존 코드와 문제점</h1>
<h3 id="기존-코드">기존 코드</h3>
<p>교환 제안 생성 로직 (exchangeService.create())은 아래 두 단계로 이루어진다.</p>
<pre><code class="language-js">// exchangeService.js
const create = async ({ proposerId, marketItemId, offeredCardId }) =&gt; {
  // ... 유효성 검사 생략

  // 1단계: 중복 제안 확인
  const duplicate = await exchangeRepository.findWaitingExchange(
    marketItemId,
    proposerId,
  );

  if (duplicate) {
    throw new AppError(
      &#39;이미 대기 중인 교환 신청이 있습니다.&#39;,
      409,
      ERROR_CODES.DUPLICATE_EXCHANGE,
    );
  }

  // 2단계: 교환 제안 생성
  const proposal = await exchangeRepository.create({
    proposerId,
    marketItemId,
    offeredCardId,
  });

  return proposal;
};</code></pre>
<pre><code class="language-js">// exchangeRepository.js
const findWaitingExchange = async (marketItemId, proposerId) =&gt; {
  return prisma.exchangeProposal.findFirst({
    where: {
      marketItemId,
      proposerId,
      status: &#39;WAITING&#39;,
    },
  });
};

const create = async ({ marketItemId, proposerId, offeredCardId }) =&gt; {
  return prisma.exchangeProposal.create({
    data: {
      marketItemId,
      proposerId,
      offeredCardId,
    },
  });
};</code></pre>
<p>언뜻 보면 문제없어 보인다. 중복 확인을 하고, 없으면 생성한다.</p>
<p>하지만 여기에 TOCTOU가 숨어있다.</p>
<h3 id="문제-발생-시나리오">문제 발생 시나리오</h3>
<p>같은 유저가 클라이언트에서 실수로(혹은 네트워크 재시도로) 동시에 두 개의 요청을 보내는 상황을 가정해보자.</p>
<pre><code class="language-text">시간 →

요청 A: [중복 확인: 없음] -------- [제안 생성 완료]
요청 B:         [중복 확인: 없음] -------- [제안 생성 완료]
                     ↑
              이 사이에 A가 끼어들지 못함
              (확인은 했지만 아직 생성 전)</code></pre>
<p>두 요청이 거의 동시에 들어오면:</p>
<ol>
<li>요청 A가 findWaitingExchange 실행 → 결과: 없음</li>
<li>요청 B가 findWaitingExchange 실행 → 결과: 없음 (A가 아직 생성 안 했으니까)</li>
<li>요청 A가 create 실행 → 제안 생성 완료</li>
<li>요청 B가 create 실행 → 중복 제안 생성됨 ❌</li>
</ol>
<p>중복 확인과 생성이 트랜잭션으로 묶여 있지 않아서 두 작업 사이에 다른 요청이 끼어들 수 있다.</p>
<h3 id="왜-문제가-되는가">왜 문제가 되는가</h3>
<p>이 서비스에서 동일한 유저가 동일한 마켓 아이템에 교환 제안을 두 개 보내면:</p>
<ul>
<li>판매자는 같은 제안이 두 개 쌓여 혼란이 생긴다.</li>
<li>판매자가 두 제안을 모두 승인하려 할 수 있다. (승인 로직에서 두 번째는 막히지만, 상태 관리가 복잡해진다.)</li>
<li>서비스 신뢰도가 떨어진다.</li>
</ul>
<hr>
<h1 id="해결-방법">해결 방법</h1>
<h3 id="방법-1-트랜잭션으로-묶기">방법 1: 트랜잭션으로 묶기</h3>
<p>가장 직관적인 해결책은 중복 확인과 생성을 하나의 트랜잭션으로 묶는 것이다.</p>
<pre><code class="language-js">// exchangeRepository.js - 개선된 create
const create = async ({ marketItemId, proposerId, offeredCardId }) =&gt; {
  return prisma.$transaction(async (tx) =&gt; {
    // 트랜잭션 안에서 중복 확인
    const duplicate = await tx.exchangeProposal.findFirst({
      where: {
        marketItemId,
        proposerId,
        status: &#39;WAITING&#39;,
      },
    });

    if (duplicate) {
      throw new AppError(
        &#39;이미 대기 중인 교환 신청이 있습니다.&#39;,
        409,
        ERROR_CODES.DUPLICATE_EXCHANGE,
      );
    }

    // 같은 트랜잭션 안에서 생성
    return tx.exchangeProposal.create({
      data: {
        marketItemId,
        proposerId,
        offeredCardId,
      },
    });
  });
};</code></pre>
<p>확인과 생성이 하나의 트랜잭션으로 묶이면 그 사이에 다른 트랜잭션이 끼어들 수 없다.</p>
<p>단, Prisma의 기본 트랜잭션 격리 수준은 데이터베이스 설정에 따라 다르다. PostgreSQL 기준 기본값은 READ COMMITTED인데, 이 수준에서도 동시 요청이 완전히 차단되지는 않는다. 확실히 막으려면 격리 수준을 높이거나, 아래 방법 2를 함께 사용하는 것이 좋다.</p>
<h3 id="방법-2-db-유니크-제약-추가">방법 2: DB 유니크 제약 추가</h3>
<p>근본적인 해결책은 <strong>데이터베이스 레벨</strong>에서 중복을 막는 것이다. 애플리케이션 코드가 아무리 잘 짜여 있어도, DB 제약이 없으면 예외 경로에서 중복이 생길 수 있다.</p>
<p>Prisma 스키마에 유니크 제약을 추가한다.</p>
<pre><code class="language-prisma">// schema.prisma
model ExchangeProposal {
  id           Int      @id @default(autoincrement())
  marketItemId Int
  proposerId   String
  offeredCardId Int
  status       ExchangeStatus @default(WAITING)
  createdAt    DateTime @default(now())

  @@unique([marketItemId, proposerId, offeredCardId])
}</code></pre>
<p>이렇게 하면 동일한 <code>(marketItemId, proposerId, offeredCardId)</code> 조합으로 두 번째 <strong>INSERT</strong>가 들어올 때 DB가 자체적으로 에러를 반환한다. 애플리케이션에서 이 에러를 잡아서 적절한 응답으로 변환하면 된다.</p>
<pre><code class="language-js">// exchangeRepository.js
const create = async ({ marketItemId, proposerId, offeredCardId }) =&gt; {
  try {
    return await prisma.exchangeProposal.create({
      data: { marketItemId, proposerId, offeredCardId },
    });
  } catch (e) {
    // Prisma unique constraint 에러 코드: P2002
    if (e.code === &#39;P2002&#39;) {
      throw new AppError(
        &#39;이미 대기 중인 교환 신청이 있습니다.&#39;,
        409,
        ERROR_CODES.DUPLICATE_EXCHANGE,
      );
    }
    throw e;
  }
};</code></pre>
<h3 id="두-방법의-비교">두 방법의 비교</h3>
<table>
<thead>
<tr>
<th></th>
<th>트랜잭션 격리</th>
<th>DB 유니크 제약</th>
</tr>
</thead>
<tbody><tr>
<td>적용 위치</td>
<td>애플리케이션</td>
<td>데이터베이스</td>
</tr>
<tr>
<td>확실성</td>
<td>격리 수준에 따라 다름</td>
<td>항상 보장</td>
</tr>
<tr>
<td>에러 처리</td>
<td>직접 throw</td>
<td>Prisma 에러 코드 처리</td>
</tr>
<tr>
<td>추천 상황</td>
<td>복잡한 비즈니스 로직</td>
<td>단순 중복 방지</td>
</tr>
</tbody></table>
<p>두 방법을 함께 적용하는 것이 가장 안전하다. 트랜잭션으로 애플리케이션 레벨에서 먼저 막고, DB 제약으로 최후의 방어선을 구축하는 방식이다.</p>
<hr>
<h1 id="최종-코드">최종 코드</h1>
<pre><code class="language-js">// exchangeRepository.js - 최종 개선 버전
const create = async ({ marketItemId, proposerId, offeredCardId }) =&gt; {
  try {
    return await prisma.$transaction(async (tx) =&gt; {
      const duplicate = await tx.exchangeProposal.findFirst({
        where: { marketItemId, proposerId, status: &#39;WAITING&#39; },
      });

      if (duplicate) {
        throw new AppError(
          &#39;이미 대기 중인 교환 신청이 있습니다.&#39;,
          409,
          ERROR_CODES.DUPLICATE_EXCHANGE,
        );
      }

      return tx.exchangeProposal.create({
        data: { marketItemId, proposerId, offeredCardId },
      });
    });
  } catch (e) {
    if (e.code === &#39;P2002&#39;) {
      throw new AppError(
        &#39;이미 대기 중인 교환 신청이 있습니다.&#39;,
        409,
        ERROR_CODES.DUPLICATE_EXCHANGE,
      );
    }
    throw e;
  }
};</code></pre>
<hr>
<p>구매와 교환 승인 로직에서는 <code>$transaction</code>과 조건부 갱신으로 동시성 문제를 잘 처리하면서, 정작 교환 제안 생성에서는 이 패턴이 빠져있었다. 기능이 작동한다고 해서 정합성이 보장되는 건 아니다.</p>
<p>TOCTOU는 로컬 테스트에서는 재현하기 어렵다. 동시 요청을 의도적으로 만들지 않으면 발견하기 힘들고, 운영 환경에서 트래픽이 많아질수록 발생 확률이 올라간다.</p>
<p>데이터 정합성은 애플리케이션 레벨과 DB 레벨 두 곳에서 모두 지켜야 한다는 것을 이번 리뷰를 통해 다시 한번 확인했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[낙관적 잠금(Optimistic Locking) - Prisma]]></title>
            <link>https://velog.io/@obebe_00/%EB%82%99%EA%B4%80%EC%A0%81-%EC%9E%A0%EA%B8%88Optimistic-Locking-Prisma</link>
            <guid>https://velog.io/@obebe_00/%EB%82%99%EA%B4%80%EC%A0%81-%EC%9E%A0%EA%B8%88Optimistic-Locking-Prisma</guid>
            <pubDate>Thu, 11 Jun 2026 10:51:34 GMT</pubDate>
            <description><![CDATA[<h1 id="prisma에서-낙관적-잠금optimistic-locking을-직접-구현하기">Prisma에서 낙관적 잠금(Optimistic Locking)을 직접 구현하기</h1>
<blockquote>
<p>포토카드 거래 플랫폼을 만들면서 동시 구매 문제를 해결한 방법을 공유합니다.</p>
</blockquote>
<hr>
<h2 id="문제상황-동시에-두-명이-같은-카드를-사면">문제상황: 동시에 두 명이 같은 카드를 사면?</h2>
<p>마켓플레이스에 잔여 수량이 1장인 포토카드가 올라와 있다고 가정해보자.
A와 B가 동시에 구매 요청을 보내면 어떻게 될까?</p>
<pre><code>[A 요청] 잔여 수량 조회 → 1장 남음 → 구매 진행
[B 요청] 잔여 수량 조회 → 1장 남음 → 구매 진행
[A 요청] soldQuantity 1 증가 → soldQuantity = 1
[B 요청] soldQuantity 1 증가 → soldQuantity = 2  ← 수량이 1인데 2가 팔림</code></pre><p>이것이 <strong>Lost Update 문제</strong>다. 두 트랜잭션이 같은 데이터를 읽고, 서로의 변경을 덮어쓴다.</p>
<hr>
<h2 id="해결-방법은-두-가지">해결 방법은 두 가지</h2>
<h3 id="1-비관적-잠금-pessimistic-locking">1. 비관적 잠금 (Pessimistic Locking)</h3>
<p>&quot;누군가 이 데이터를 바꿀 것이다&quot;고 가정하고, 읽는 시점부터 잠근다.</p>
<pre><code class="language-sql">SELECT * FROM market_items WHERE id = 1 FOR UPDATE;</code></pre>
<ul>
<li>장점: 충돌이 절대 발생하지 않음</li>
<li>단점: 락을 기다리는 동안 다른 모든 트랜잭션이 블로킹됨 → <strong>처리량 저하</strong></li>
</ul>
<h3 id="2-낙관적-잠금-optimistic-locking">2. 낙관적 잠금 (Optimistic Locking)</h3>
<p>&quot;충돌이 드물 것이다&quot;고 가정하고, 잠금 없이 진행한다. 대신 <strong>쓸 때</strong> &quot;내가 읽은 이후로 데이터가 바뀌지 않았는지&quot; 조건을 걸어서 검증한다.</p>
<p>충돌이 감지되면 → 에러를 던지거나 재시도한다.</p>
<p>포토카드 거래처럼 <strong>동시 구매 빈도가 낮고, 쓰기보다 읽기가 훨씬 많은 환경</strong>에 적합하다.</p>
<hr>
<h2 id="일반적인-낙관적-잠금-version-필드">일반적인 낙관적 잠금: version 필드</h2>
<p>교과서적인 낙관적 잠금은 테이블에 <code>version</code> 컬럼을 추가한다.</p>
<pre><code class="language-sql">-- 테이블
ALTER TABLE market_items ADD COLUMN version INT DEFAULT 0;</code></pre>
<pre><code class="language-js">// 읽을 때
const item = await prisma.marketItem.findUnique({ where: { id } });
// item.version = 3

// 쓸 때: version이 내가 읽은 값과 같을 때만 업데이트
const updated = await prisma.marketItem.updateMany({
  where: { id, version: item.version },  // 조건 검증
  data: {
    soldQuantity: { increment: 1 },
    version: { increment: 1 },           // 버전 증가
  },
});

if (updated.count === 0) {
  throw new Error(&#39;다른 사람이 먼저 구매했습니다. 다시 시도해주세요.&#39;);
}</code></pre>
<p><code>updated.count === 0</code>이면 다른 트랜잭션이 먼저 version을 올렸다는 뜻이다.</p>
<blockquote>
<p>참고로 Prisma는 <code>@version</code>을 공식 지원하지 않는다 (<a href="https://github.com/prisma/prisma/issues/4988">GitHub 이슈 #4988</a>). <code>updateMany</code>를 활용해서 직접 구현해야 한다.</p>
</blockquote>
<hr>
<h2 id="우리-프로젝트의-구현-version-없이-비즈니스-조건으로-검증">우리 프로젝트의 구현: version 없이 비즈니스 조건으로 검증</h2>
<p>version 컬럼 없이도 낙관적 잠금을 구현할 수 있다. <strong>비즈니스 불변식(invariant)을 WHERE 조건에 직접 넣으면 된다.</strong></p>
<p>구매 API의 <code>orderRepository.purchase</code>를 살펴보자.</p>
<h3 id="재고-차감-soldquantity-조건부-업데이트">재고 차감: soldQuantity 조건부 업데이트</h3>
<pre><code class="language-js">// orderRepository.js
const updatedStock = await tx.marketItem.updateMany({
  where: {
    id: marketItemId,
    status: &#39;SELLING&#39;,
    quantity: marketItem.quantity,         // 내가 읽은 total quantity와 같아야 함
    pricePerCard: marketItem.pricePerCard, // 가격이 바뀌지 않아야 함
    soldQuantity: {
      lte: marketItem.quantity - quantity, // 아직 구매 가능한 수량이 남아있어야 함
    },
  },
  data: {
    soldQuantity: { increment: quantity },
  },
});

if (updatedStock.count === 0) {
  throw new AppError(
    &#39;판매 가능한 카드 수량이 부족합니다.&#39;,
    409,
    ERROR_CODES.INSUFFICIENT_STOCK,
  );
}</code></pre>
<p>핵심은 <code>soldQuantity: { lte: marketItem.quantity - quantity }</code> 조건이다.</p>
<ul>
<li>내가 트랜잭션을 시작할 때 <code>soldQuantity</code>를 읽었다.</li>
<li>업데이트 시점에 이 조건이 만족하지 않으면 — 즉, 다른 트랜잭션이 먼저 재고를 줄였다면 — <code>updateMany</code>는 아무 행도 수정하지 않고 <code>count: 0</code>을 반환한다.</li>
<li><code>count === 0</code>을 체크해서 충돌을 감지하고 에러를 던진다.</li>
</ul>
<p>이 방식의 장점은 <strong>비즈니스 의미가 명확하다</strong>는 것이다. &quot;재고가 충분한 경우에만 차감한다&quot;는 도메인 규칙이 코드에 그대로 드러난다.</p>
<h3 id="포인트-차감-잔액-조건부-업데이트">포인트 차감: 잔액 조건부 업데이트</h3>
<p>같은 원리가 포인트 차감에도 적용된다.</p>
<pre><code class="language-js">// 구매자 포인트 조건부 차감
const deductedPoint = await tx.userPoint.updateMany({
  where: {
    userId: buyerId,
    point: { gte: totalPrice }, // 잔액이 충분한 경우에만
  },
  data: {
    point: { decrement: totalPrice },
  },
});

if (deductedPoint.count === 0) {
  throw new AppError(
    &#39;보유 포인트가 부족합니다.&#39;,
    409,
    ERROR_CODES.INSUFFICIENT_POINTS,
  );
}</code></pre>
<p><code>point &gt;= totalPrice</code> 조건을 WHERE에 걸었기 때문에, 포인트가 부족한 상태에서는 절대 차감이 일어나지 않는다. 애플리케이션 레벨의 포인트 검증과 DB 레벨의 원자적 차감이 <strong>한 쿼리</strong>에서 동시에 이루어진다.</p>
<p>포인트 박스 시스템에도 동일한 패턴이 사용된다.</p>
<pre><code class="language-js">// pointRepository.js
const updated = await tx.userPoint.updateMany({
  where: {
    userId,
    OR: [
      { lastSpinAt: null },
      { lastSpinAt: { lte: availableBefore } }, // 쿨다운이 지난 경우에만
    ],
  },
  data: {
    point: { increment: amount },
    lastSpinAt: now,
  },
});

if (updated.count === 0) {
  return null; // 쿨다운 중 → 상위에서 PointBoxCooldownError
}</code></pre>
<p>동시에 두 개의 포인트 박스 요청이 들어와도, 첫 번째 요청이 <code>lastSpinAt</code>을 갱신하면 두 번째 요청의 <code>lastSpinAt: { lte: availableBefore }</code> 조건이 실패해서 <code>count: 0</code>이 반환된다.</p>
<hr>
<h2 id="전체-트랜잭션-흐름">전체 트랜잭션 흐름</h2>
<p>구매 로직은 하나의 <code>$transaction</code> 안에서 아래 순서로 실행된다.</p>
<pre><code>$transaction 시작
  │
  ├─ 1. marketItem 조회 (status, quantity, soldQuantity, price 읽기)
  │
  ├─ 2. marketItem.soldQuantity 조건부 증가
  │      WHERE soldQuantity &lt;= quantity - n   ← 낙관적 잠금 포인트 ①
  │      count === 0 → INSUFFICIENT_STOCK 에러
  │
  ├─ 3. myCard.quantity 조건부 감소 (판매자 카드 차감)
  │      WHERE quantity &gt;= n                  ← 낙관적 잠금 포인트 ②
  │      count === 0 → INSUFFICIENT_STOCK 에러
  │
  ├─ 4. userPoint 조건부 차감 (구매자 포인트)
  │      WHERE point &gt;= totalPrice            ← 낙관적 잠금 포인트 ③
  │      count === 0 → INSUFFICIENT_POINTS 에러
  │
  ├─ 5. 판매자 포인트 증가
  │
  ├─ 6. Order 생성
  │
  ├─ 7. 구매자 myCard upsert (카드 지급)
  │
  ├─ 8. PointLog 생성 (구매자 SPEND / 판매자 EARN)
  │
  └─ 9. soldQuantity === quantity면 status → SOLD_OUT
         + WAITING 상태 교환 신청 전부 REJECTED</code></pre><p>각 단계에서 조건이 실패하면 트랜잭션 전체가 롤백된다. 한쪽 포인트만 차감되거나, 카드만 지급되는 <strong>부분 성공 상태가 존재하지 않는다</strong>.</p>
<hr>
<h2 id="service-계층의-사전-검증은-왜-있을까">Service 계층의 사전 검증은 왜 있을까?</h2>
<p><code>orderService.js</code>를 보면 트랜잭션에 진입하기 전에 이미 한 번 검증을 한다.</p>
<pre><code class="language-js">// orderService.js
const marketItem = await marketRepository.findMarketItemById(marketItemId);

if (!marketItem) {
  throw new AppError(&#39;판매글을 찾을 수 없습니다.&#39;, 404, ...);
}

if (marketItem.status !== &#39;SELLING&#39;) {
  throw new AppError(&#39;현재 판매 중인 포토카드가 아닙니다.&#39;, 409, ...);
}

if (marketItem.sellerId === buyerId) {
  throw new AppError(&#39;본인이 등록한 포토카드는 구매할 수 없습니다.&#39;, 409, ...);
}</code></pre>
<p>트랜잭션 안에서도 같은 검증이 다시 일어나는데, 왜 Service에서 미리 할까?</p>
<p>이유는 <strong>사용자 경험</strong>과 <strong>DB 부하</strong> 때문이다.</p>
<ul>
<li>존재하지 않는 상품 ID, 본인 상품 구매처럼 <strong>데이터 경쟁 없이 확정적으로 실패할 케이스</strong>는 트랜잭션을 열기 전에 빠르게 걸러낸다.</li>
<li>트랜잭션은 DB 커넥션을 점유하는 비싼 작업이다. 명백히 실패할 요청에 트랜잭션을 낭비할 필요가 없다.</li>
</ul>
<p>반면 재고, 포인트처럼 <strong>동시성 경쟁이 발생할 수 있는 항목</strong>은 트랜잭션 내에서 조건부 업데이트로 다시 검증한다. Service의 사전 검증은 이 경우를 막아주지 못한다. Service가 읽은 값과 Repository가 실제로 쓰는 시점 사이에 데이터가 바뀔 수 있기 때문이다.</p>
<pre><code>[Service] 재고 1 확인 → OK
          ↕ (이 사이에 다른 요청이 재고를 0으로 만듦)
[Repository] 트랜잭션에서 조건부 업데이트 → count: 0 → 에러</code></pre><hr>
<h2 id="낙관적-잠금-vs-비관적-잠금-정리">낙관적 잠금 vs 비관적 잠금 정리</h2>
<table>
<thead>
<tr>
<th></th>
<th>낙관적 잠금</th>
<th>비관적 잠금</th>
</tr>
</thead>
<tbody><tr>
<td>가정</td>
<td>충돌이 드물다</td>
<td>충돌이 자주 발생한다</td>
</tr>
<tr>
<td>구현</td>
<td>WHERE 조건 검증 후 count 체크</td>
<td><code>SELECT FOR UPDATE</code></td>
</tr>
<tr>
<td>충돌 시</td>
<td>에러 반환 (재시도는 클라이언트 책임)</td>
<td>다른 트랜잭션이 락 해제를 대기</td>
</tr>
<tr>
<td>성능</td>
<td>충돌이 드물면 빠름</td>
<td>경합이 심하면 느림</td>
</tr>
<tr>
<td>적합한 상황</td>
<td>읽기 많고 쓰기 적음, 충돌 드문 경우</td>
<td>포인트 잔액처럼 동시 쓰기가 빈번한 경우</td>
</tr>
</tbody></table>
<p>포토카드 거래 플랫폼처럼 특정 상품에 동시 구매가 몰리는 경우는 드물기 때문에 낙관적 잠금이 적합하다.</p>
<blockquote>
<p>단, 인기 아이돌의 한정판 포토카드처럼 수백 명이 동시에 구매를 시도한다면, 대부분의 요청이 충돌로 실패하게 된다. 이런 경우엔 Redis 기반 분산 락이나 Queue를 통한 직렬화 처리를 고려해야 한다.</p>
</blockquote>
<hr>
<h2 id="정리">정리</h2>
<p>이 프로젝트에서 낙관적 잠금은 별도의 <code>version</code> 컬럼 없이 <strong>비즈니스 조건 자체를 WHERE 절에 넣는 방식</strong>으로 구현했다.</p>
<ul>
<li><code>soldQuantity &lt;= quantity - n</code> → 재고 초과 판매 방지</li>
<li><code>point &gt;= totalPrice</code> → 포인트 음수 방지</li>
<li><code>lastSpinAt &lt;= availableBefore</code> → 포인트 박스 중복 획득 방지</li>
</ul>
<p>핵심은 단순하다. <strong>읽은 값이 쓰는 시점에도 유효한지, 조건을 WHERE에 걸고 <code>updateMany</code>의 count로 검증한다.</strong> Prisma가 낙관적 잠금을 공식 지원하지 않아도, 이 패턴 하나로 동시성 문제를 충분히 방어할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[에러 처리 Feat.  Axios with TanStack Query ]]></title>
            <link>https://velog.io/@obebe_00/%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC-Feat.-Axios-with-TanStack-Query</link>
            <guid>https://velog.io/@obebe_00/%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC-Feat.-Axios-with-TanStack-Query</guid>
            <pubDate>Thu, 04 Jun 2026 10:55:32 GMT</pubDate>
            <description><![CDATA[<p>이전 프로젝트에서는 팀원분이 공통 에러 처리를 너무 깔끔하게 정리해주셨어서 편하게 사용했는데, 새로 프로젝트에 들어가며 내가 담당하게 되어 이전 내용을 복습할 겸 내용을 정리해보고자 한다.</p>
<p>이번에 시작하는 프로젝트에서는 API 요청 도구로 Axios를 사용하고, 서버 상태 관리를 위해 TanStack Query를 도입했다.</p>
<p>공통 에러 처리는 단순하게 API 요청 함수 내에서 처리하면 되는 작업이라고 생각할 수 있지만 화면이 늘어나고 API 요청이 많아졌기에 매번 직접 파싱하지 않는 방법을 찾고자했고, Axios를 사용하여 서버 에러 응답을 FE에서 사용하기 쉬운 에러 객체로 정규화하고 TanStack Query는 그 에러를 서버 상태의 일부로 관리하도록 했다.</p>
<h2 id="axios와-tanstack-query의-역할-분리">Axios와 TanStack Query의 역할 분리</h2>
<p>Axios와 TanStack Query는 모두 API 요청 과정에서 사용되지만, 담당하는 역할은 다르다</p>
<table>
<thead>
<tr>
<th>도구</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Axios</td>
<td>HTTP 요청 수행, 공통 header 설정, 요청/응답 Interceptor 처리</td>
</tr>
<tr>
<td>TanStack Query</td>
<td>서버 상태 캐싱, loading/error 상태 관리, retry/refetch, invalidate 처리</td>
</tr>
</tbody></table>
<p>따라서 두 도구를 함께 사용할 때는 에러 처리 책임도 분리할 수 있다.</p>
<p>Axios에서는 서버 에러 응답을 프론트엔드에서 사용하기 쉬운 형태로 가공하고, TanStack Query에서는 가공된 에러 객체를 <code>error</code> 상태로 관리한다.</p>
<pre><code class="language-text">Backend Error Response
↓
Axios Response Interceptor
↓
Error Code 추출
↓
Frontend Error 객체로 정규화
↓
TanStack Query error 상태로 전달
↓
Component UI 처리</code></pre>
<p>이 구조를 사용하면 컴포넌트에서 매번 서버 응답 구조를 직접 확인하지 않아도 된다.</p>
<p>컴포넌트는 TanStack Query가 전달한 error 객체를 기준으로 화면에 필요한 처리를 담당하면 된다.</p>
<hr>
<h2 id="be-에러-응답-포맷">BE 에러 응답 포맷</h2>
<pre><code class="language-json">{
  &quot;success&quot;: false,
  &quot;code&quot;: &quot;PHOTO_CARD_NOT_FOUND&quot;,
  &quot;message&quot;: &quot;Photo card not found&quot;
}</code></pre>
<p>이전 프로젝트와 동일하게 </p>
<table>
<thead>
<tr>
<th>필드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>success</td>
<td>요청 성공 여부</td>
</tr>
<tr>
<td>code</td>
<td>프론트엔드에서 에러를 식별하기 위한 값</td>
</tr>
<tr>
<td>message</td>
<td>개발 및 디버깅을 위한 메시지</td>
</tr>
</tbody></table>
<p>로 처리하였다.</p>
<p>포인트는 FE에서 <code>message</code>가 아닌 <code>code</code>를 기준으로 에러를 처리한다는 점
즉, 서버의 <code>message</code>는 개발자가 에러 상황을 확인하기 위한 값이고, 사용자에게 보여줄 문구는 FE에서 <code>code</code>를 기준으로 매핑한다.</p>
<hr>
<h2 id="axios-intercepter에서-에러-객체-정규화">Axios intercepter에서 에러 객체 정규화</h2>
<p>백엔드에서 공통 응답을 내려주더라도, 각 컴포넌트에서 매번 <code>error.response.data.code</code>에 접근해 메시지를 매핑하면 같은 로직이 반복된다.</p>
<p>그래서 Axios Response Interceptor에서 서버 에러 응답을 한 번 가공하기로 했다.</p>
<p>Interceptor에서는 서버 응답에서 <code>code</code>를 추출 &rarr; 해당 코드에 맞는 사용자 메시지를 찾아 에러 객체에 추가한다.</p>
<pre><code class="language-js">api.interceptors.response.use(
  (response) =&gt; response,

  (error) =&gt; {
    const code = error.response?.data?.code;

    error.code = code;
    error.userMessage = getErrorMessage(code);

    return Promise.reject(error);
  }
);</code></pre>
<p>이렇게 하면 이후 API 요청을 사용하는 쪽에서는 서버 응답 구조를 직접 알 필요가 없다.</p>
<pre><code class="language-js">try {
  await createPhotoCard(data);
} catch (error) {
  console.log(error.userMessage);
}</code></pre>
<blockquote>
<p>즉, Axios Interceptor는 서버에서 내려온 에러 응답을 프론트엔드에서 다루기 쉬운 형태로 정규화하는 역할을 한다.</p>
</blockquote>
<pre><code class="language-text"># 흐름 정리
server response
{
  success: false,
  code: &quot;INSUFFICIENT_POINT&quot;,
  message: &quot;Not enough point&quot;
}

↓ Axios Response Interceptor

frontend error
{
  code: &quot;INSUFFICIENT_POINT&quot;,
  userMessage: &quot;포인트가 부족합니다.&quot;
}</code></pre>
<hr>
<h2 id="tanstack-query에서-에러-상태-다루기">TanStack Query에서 에러 상태 다루기</h2>
<p>TanStack Query는 <code>queryFn</code> 또는 <code>mutationFn</code>에서 발생한 에러를 <code>error</code> 상태로 관리한다.</p>
<p>이때 중요한 점은 함수 내부에서 에러가 발생하거나 rejected promise가 반환되어야 TanStack Query가 에러 상태로 인식한다는 것이다.</p>
<p>Axios는 HTTP 실패 응답을 자동으로 rejected promise로 처리한다. 따라서 Axios Interceptor에서 에러 객체를 가공한 뒤 <code>Promise.reject(error)</code>로 반환하면, TanStack Query는 이 에러를 그대로 <code>error</code> 상태에 담아준다.</p>
<pre><code class="language-js">const { data, error, isError } = useQuery({
  queryKey: [&quot;photoCards&quot;],
  queryFn: getPhotoCards,
});</code></pre>
<pre><code class="language-js">if (isError) {
  console.log(error.userMessage);
}</code></pre>
<p>Mutation에서도 같은 방식으로 에러를 다룰 수 있다.</p>
<pre><code class="language-js">const mutation = useMutation({
  mutationFn: createPhotoCard,
  onError: (error) =&gt; {
    console.log(error.userMessage);
  },
});</code></pre>
<p>이후 각 화면에서 <code>error.userMessage</code>를 사용해 Toast, Modal, Form Error 등 상황에 맞는 UI 처리를 선택하면 된다.</p>
<hr>
<h2 id="정리">정리</h2>
<pre><code class="language-text">Axios
- HTTP 요청 수행
- Response Interceptor에서 Error Code 추출
- 사용자 메시지 매핑
- 에러 객체 정규화

TanStack Query
- queryFn / mutationFn 실행
- loading / error 상태 관리
- retry / refetch 관리
- 정규화된 error 객체 전달</code></pre>
<p>즉 핵심은</p>
<blockquote>
<p>Axios Interceptor는 에러를 가공하는 계층이고,
TanStack Query는 에러 상태를 관리하는 계층이다.</p>
</blockquote>
<p>다음은 이걸 활용하여 컴포넌트 UI를 작성해보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Tailwind CSS]]></title>
            <link>https://velog.io/@obebe_00/Tailwind-CSS</link>
            <guid>https://velog.io/@obebe_00/Tailwind-CSS</guid>
            <pubDate>Sat, 09 May 2026 08:41:22 GMT</pubDate>
            <description><![CDATA[<p>React를 공부하며 Next.js와 같은 여러 프레임워크를 접해보고 있다.
그러며 <code>Tailwind CSS</code>도 접하게 되었는데, 여러 시니어분들도 추천을 하시고 실제 현업에서도 사용 비율이 점점 높아지고 있다는 이야기를 들으며 더 알아보고자 정리해보려고 한다.</p>
<blockquote>
<p>이 글은 Tailwind CSS v4 기준으로 작성되었다.</p>
</blockquote>
<hr>
<h3 id="기존-css-작성-방식">기존 CSS 작성 방식</h3>
<p><img src="https://velog.velcdn.com/images/obebe_00/post/30205478-3063-4edb-8fc6-b402f3f04d29/image.png" alt=""></p>
<p>처음 React를 공부할 때는 일반 CSS 방식으로 스타일을 작성했다.</p>
<pre><code class="language-js">&lt;button className=&quot;btn-primary&quot;&gt;버튼&lt;/button&gt;</code></pre>
<pre><code class="language-css">.btn-primary {
  background-color: blue;
  padding: 8px 16px;
  border-radius: 8px;
}</code></pre>
<p>이 방식은 가장 익숙하고 이해하기에는 쉬웠다.</p>
<p>하지만 프로젝트가 커질수록 몇 가지 불편한 점이 있다</p>
<ul>
<li>클래스 이름을 계속 고민해야 함</li>
<li>css 파일 이동이 많아짐</li>
<li>스타일 충돌 가능성 존재</li>
<li>어디에서 사용되는 클래스인지 찾기 어려움</li>
</ul>
<p>그래서 이후에는 CSS Module이나 styled-components 방식도 사용해보게 되었다.</p>
<hr>
<h3 id="tailwind를-처음-접하였을-때">Tailwind를 처음 접하였을 때</h3>
<p>솔직히 처음에는 코드가 오히려 복잡해보였다.</p>
<pre><code class="language-jsx">&lt;button className=&quot;bg-blue-500 px-4 py-2 rounded text-white&quot;&gt; 버튼 &lt;/button&gt;</code></pre>
<p>스타일이 전부 className 안에 들어가 있다 보니 HTML이 너무 길어 보인다는 느낌도 있었다.
&quot;오히려 더 불편하지않은가?&quot; 라는 생각도 들었지만 현업에서 많이 사용하는 이유를 찾아보고자 더 공부해보려고 마음먹었다.</p>
<hr>
<h1 id="tailwind의-장점">Tailwind의 장점</h1>
<h2 id="빠른-ui-개발">빠른 UI 개발</h2>
<p>Tailwind는 미리 정의된 utility class를 조합해서 스타일을 작성하는 방식이다.</p>
<pre><code class="language-jsx">&lt;div className=&quot;p-4 bg-white rounded-lg shadow-md&quot;&gt;</code></pre>
<p>기존 CSS처럼</p>
<ul>
<li>CSS 파일 생성</li>
<li>클래스 이름 작성</li>
<li>다시 컴포넌트로 이동</li>
</ul>
<p>과정을 반복하지 않아도 되기 때문에 빠르게 화면을 구성할 수 있다는 장점이 있다고 한다.</p>
<p>특히 프로토타입 제작이나 초기 UI 작업 속도가 빠르다는 의견이 많았다.</p>
<hr>
<h2 id="class명-고민-❌">class명 고민 ❌</h2>
<p>기존 CSS에서는 스타일을 만들 때마다 이름을 계속 지어야 했다.</p>
<p>.container {}
.wrapper {}
.box {}
.card {}</p>
<p>프로젝트가 커질수록 이름이 겹치거나, 어떤 역할인지 애매해지는 경우도 있었다.</p>
<p>하지만 Tailwind는 이미 정의된 utility class를 조합하는 방식이라 이런 고민이 줄어든다고 한다.</p>
<hr>
<h2 id="전역-클래스-명-충돌-걱정-감소">전역 클래스 명 충돌 걱정 감소</h2>
<p>기존 CSS는 전역 스타일 충돌 문제가 자주 발생한다.
특히 여러 사람이 함께 작업하는 프로젝트에서는 같은 클래스 이름을 사용하는 상황도 생길 수 있다.</p>
<p>Tailwind는 필요한 utility class를 직접 조합해서 사용하는 방식이기 때문에, 전역 클래스 이름이 겹칠 걱정이 상대적으로 줄어든다는 장점이 있다.
(CSS Module이나 styled-components처럼 스코프를 완전히 격리해주는 방식은 아니지만, 클래스 이름 충돌로 인한 문제는 훨씬 덜 발생한다.)</p>
<hr>
<h2 id="반응형-작업에-용이">반응형 작업에 용이</h2>
<p>Tailwind는 반응형 스타일을 쉽게 작성할 수 있도록 breakpoint 문법을 제공한다.</p>
<pre><code class="language-jsx">&lt;div className=&quot;text-sm md:text-lg lg:text-2xl&quot;&gt;</code></pre>
<p>별도의 media query를 작성하지 않아도 되기 때문에 반응형 UI를 빠르게 적용할 수 있다는 점이 많이 언급되었다.</p>
<hr>
<h2 id="react--nextjs-생태계와-좋은-궁합">React / Next.js 생태계와 좋은 궁합</h2>
<p>최근 React 기반 프로젝트에서는 Tailwind를 사용하는 경우가 많다고 한다.</p>
<p>특히</p>
<ul>
<li>Next.js</li>
<li>shadcn/ui</li>
<li>Vercel 기반 프로젝트</li>
</ul>
<p>등 최신 React 생태계와 함께 자주 사용된다는 점도 특징이라고 느꼈다.</p>
<p>컴포넌트 기반 개발 방식과 utility class 방식이 잘 어울린다는 의견도 많았다.</p>
<hr>
<h2 id="디자인-규칙-유지">디자인 규칙 유지</h2>
<p>Tailwind는 spacing, color, size 등이 일정한 규칙 안에서 제공된다.</p>
<p>그래서 프로젝트 전체에서 디자인 스타일을 통일하기 쉽다는 장점이 있다고 한다.</p>
<p>여러 명이 함께 작업할 때 UI 일관성을 유지하는 데 도움이 된다는 글들도 많이 볼 수 있었다.</p>
<hr>
<h2 id="현업에서-자주-쓰이는-cn-패턴">현업에서 자주 쓰이는 <code>cn()</code> 패턴</h2>
<p>현업 React + Tailwind 코드를 보면 아래와 같은 패턴이 자주 등장한다.</p>
<pre><code class="language-ts">import { clsx } from &quot;clsx&quot;;
import { twMerge } from &quot;tailwind-merge&quot;;

export function cn(...inputs) {
  return twMerge(clsx(inputs));
}</code></pre>
<pre><code class="language-jsx">&lt;button className={cn(&quot;px-4 py-2 rounded&quot;, isActive &amp;&amp; &quot;bg-blue-500&quot;, &quot;bg-gray-200&quot;)}&gt;
  버튼&lt;/button&gt;</code></pre>
<ul>
<li><code>clsx</code>는 조건부 클래스를 깔끔하게 처리해주고</li>
<li><code>tailwind-merge</code>는 충돌하는 클래스를 자동으로 합쳐준다</li>
</ul>
<p>shadcn/ui 같은 라이브러리에서도 이 패턴을 기본으로 사용하고 있어서, Tailwind를 실제로 사용하게 된다면 함께 알아두면 좋을 것 같다.</p>
<hr>
<h1 id="tailwind-v4에서-달라진-점">Tailwind v4에서 달라진 점</h1>
<p>2025년 초에 Tailwind CSS v4가 정식 릴리즈되면서 설정 방식이 꽤 달라졌다.
공식 문서를 보면서 따라할 때 버전 차이로 혼란스러울 수 있으니 알아두면 좋다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>v3</th>
<th>v4</th>
</tr>
</thead>
<tbody><tr>
<td>설정 파일</td>
<td><code>tailwind.config.js</code></td>
<td>CSS 파일 내부에서 직접 설정</td>
</tr>
<tr>
<td>임포트 방식</td>
<td><code>@tailwind base/components/utilities</code></td>
<td><code>@import &quot;tailwindcss&quot;</code></td>
</tr>
<tr>
<td>content 경로</td>
<td>직접 지정 필요</td>
<td>자동 감지</td>
</tr>
<tr>
<td>빌드 속도</td>
<td>빠름</td>
<td>훨씬 더 빠름</td>
</tr>
</tbody></table>
<hr>
<h1 id="tailwind가-무조건-정답일까">Tailwind가 무조건 정답일까?</h1>
<p>여러 자료를 찾아보면서 Tailwind가 무조건 모든 프로젝트의 정답은 아니라는 의견도 많았다.</p>
<p>대표적인 단점과 함께, 현업에서 어떻게 대응하는지도 정리해보았다.</p>
<table>
<thead>
<tr>
<th>단점</th>
<th>현업에서의 대응</th>
</tr>
</thead>
<tbody><tr>
<td>className이 길어진다</td>
<td>React 컴포넌트로 추상화하거나 <code>cn()</code> 패턴 활용</td>
</tr>
<tr>
<td>처음에는 가독성이 떨어진다</td>
<td>IDE 플러그인(Tailwind CSS IntelliSense)으로 자동완성 지원</td>
</tr>
<tr>
<td>HTML이 복잡해 보인다</td>
<td>컴포넌트 단위로 분리하면 어느 정도 해소됨</td>
</tr>
</tbody></table>
<p>그래서 프로젝트 규모나 팀 스타일에 따라</p>
<ul>
<li>일반 CSS</li>
<li>CSS Module</li>
<li>styled-components</li>
<li>Tailwind</li>
</ul>
<p>중 적절한 방식을 선택하는 것이 중요하다고 느꼈다.</p>
<hr>
<p>처음에는 단순히 “요즘 많이 사용하는 CSS 방식” 정도로 생각했다.</p>
<p>하지만 여러 자료를 찾아보면서 Tailwind는 단순 유행이라기보다 빠른 UI 개발과 유지보수를 고려한 방식이라는 점을 조금 이해할 수 있었다.</p>
<p>아직 직접 많이 사용해보지는 않았지만, 앞으로 React와 Next.js 프로젝트를 진행하면서 Tailwind를 직접 사용해보고 장단점을 더 체감해보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Soft Delete, 언제 그리고 어떻게 사용할까?]]></title>
            <link>https://velog.io/@obebe_00/Soft-Delete-%EC%96%B8%EC%A0%9C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@obebe_00/Soft-Delete-%EC%96%B8%EC%A0%9C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Wed, 29 Apr 2026 11:08:30 GMT</pubDate>
            <description><![CDATA[<h3 id="왜-이-글을-쓰게-됐나">왜 이 글을 쓰게 됐나</h3>
<p>습관 관리 프로젝트에서 &quot;습관 삭제&quot; 기능을 구현하다가 뜻밖의 고민에 빠졌다. 처음엔 당연히 <code>DELETE</code> 쿼리를 쓰면 된다고 생각했는데, 막상 구현하려고 보니 두 가지 문제가 걸렸다.</p>
<p>첫 번째. 습관을 삭제하면 연결된 체크 기록(<code>habitLogs</code>)이 cascade로 함께 날아간다. 사용자가 한 달 동안 쌓아온 기록이 습관 하나 삭제로 전부 사라지는 건 말이 안 됐다.</p>
<p>두 번째. UX 문제였다. 오늘 아침에 이미 체크한 습관을 낮에 삭제했는데 목록에서 즉시 사라지면 어색하다. &quot;삭제했으니 오늘은 유지되고, 내일부터 안 보이는&quot; 동작이 훨씬 자연스럽다.</p>
<p>이 두 가지 이유로 Soft Delete를 선택했다.</p>
<hr>
<h1 id="hard-delete-vs-soft-delete">Hard Delete vs Soft Delete</h1>
<p>먼저 개념부터 정리하자.</p>
<p><strong>Hard Delete</strong>는 <code>DELETE</code> 쿼리로 DB에서 행을 완전히 제거하는 방식이다. 구현이 단순하고 DB를 깔끔하게 유지할 수 있다. 하지만 한 번 지우면 복구가 어렵고, 연관 데이터까지 함께 사라질 수 있다.</p>
<p>Soft Delete는 실제로 삭제하지 않고 &quot;삭제됐다&quot;는 표시만 남기는 방식이다. 행은 DB에 그대로 있고, <code>deletedAt</code>이나 <code>isDeleted</code> 같은 필드로 상태를 관리한다.</p>
<hr>
<h3 id="스키마-설계-isdeleted가-아니라-endat을-선택한-이유">스키마 설계: isDeleted가 아니라 endAt을 선택한 이유</h3>
<p><strong>Soft Delete</strong>를 구현할 때 가장 먼저 결정해야 할 건 어떤 필드로 상태를 표현할 것인가다.
흔히 <code>isDeleted: Boolean</code> 방식을 많이 쓴다. 직관적이고 간단하다. 그런데 이 프로젝트에선 <code>endAt: DateTime?</code>을 선택했다.</p>
<p>이유는 하나다. 삭제 여부뿐만 아니라 삭제 시점도 함께 남길 수 있기 때문이다.</p>
<ul>
<li><code>isDeleted: true</code> → &quot;지워졌다&quot;는 사실만 알 수 있다</li>
<li><code>endAt: 2024-11-15</code> → &quot;언제 지워졌는지&quot;까지 알 수 있다</li>
</ul>
<p>게다가 이 프로젝트에서 핵심 요구사항이 &quot;삭제 당일은 목록에 유지되고, 다음 날부터 제외&quot;였기 때문에, 날짜 기반 필드가 훨씬 다루기 쉬웠다.</p>
<pre><code class="language-prisma">model Habit {
  id        Int       @id @default(autoincrement())
  studyId   Int       @map(&quot;study_id&quot;)
  habitName String    @map(&quot;habit_name&quot;)
  startAt   DateTime  @map(&quot;start_at&quot;)
  endAt     DateTime? @map(&quot;end_at&quot;)  // null이면 활성, 값이 있으면 종료
  // ...
}</code></pre>
<p><code>null</code>이면 현재 활성 상태, 값이 있으면 해당 날짜에 종료된 습관이다.</p>
<hr>
<h1 id="삭제-처리-delete-대신-update">삭제 처리: DELETE 대신 UPDATE</h1>
<p>실제 삭제 로직은 단순하다. DELETE 대신 endAt을 오늘 날짜로 업데이트한다.</p>
<pre><code class="language-jsx">jsexport const deleteHabit = async (studyId, habitId) =&gt; {
  const habit = await prisma.habit.findFirst({
    where: { id: habitId, studyId },
  });

  if (!habit) throw new HabitNotFoundError();

  // 이미 종료된 습관은 다시 삭제 불가
  if (habit.endAt !== null) {
    throw new BadRequestError(&quot;이미 종료된 습관은 삭제할 수 없습니다.&quot;);
  }

  // 실제 삭제 대신 endAt을 오늘 날짜로 설정
  await prisma.habit.update({
    where: { id: habitId },
    data: { endAt: getTodayKST() },
  });
};</code></pre>
<p>한 가지 주의할 점은 <code>if (habit.endAt !== null)</code> 체크다. 이미 종료된 습관에 다시 삭제 요청이 들어왔을 때 멱등하게 처리하거나(아무것도 안 함), 아니면 에러를 던질 수 있다. 이 프로젝트에서는 &quot;이미 종료된 습관&quot;이라는 상태를 명확히 알려주는 게 낫다고 판단해서 에러를 던졌다.</p>
<p><code>getTodayKST()</code>는 <strong>UTC 기준 날짜 밀림 문제를 방지하기 위해 KST 기준으로 오늘 날짜를 반환하는 유틸 함수</strong>다. 한국 서비스라면 이 부분이 꽤 중요하다. (별도 포스팅으로 정리 예정)</p>
<hr>
<h3 id="조회-필터링-삭제-당일을-목록에-유지하는-법">조회 필터링: 삭제 당일을 목록에 유지하는 법</h3>
<p>Soft Delete에서 가장 신경 써야 할 부분이 조회 쿼리다. 조건을 빠뜨리면 삭제된 데이터가 그대로 노출된다.
이 프로젝트의 요구사항은 이랬다:</p>
<blockquote>
<p>삭제 당일은 목록에 유지되고, 다음 날부터 자동으로 제외된다.</p>
</blockquote>
<p>이를 구현한 조회 조건은 다음과 같다.</p>
<pre><code class="language-jsx">const habits = await prisma.habit.findMany({
  where: {
    studyId,
    startAt: { lte: now },
    OR: [
      { endAt: null },           // 종료일이 없는 활성 습관
      { endAt: { gt: today } },  // 종료일이 오늘보다 미래인 습관 (삭제 당일 포함)
    ],
  },
  include: {
    habitLogs: {
      where: { logDate: today },
    },
  },
});</code></pre>
<p><code>endAt: { gt: today }</code> 조건이 핵심이다. <code>endAt</code>이 오늘과 같은 날짜라면 이미 <code>gt</code> 조건에서 걸러지기 때문에, 삭제 당일은 목록에 포함되고 그 다음 날부터 제외된다.</p>
<p>조건을 정리하면 이렇다:</p>
<table>
<thead>
<tr>
<th>상태</th>
<th>endAt 값</th>
<th>오늘 목록에 포함?</th>
</tr>
</thead>
<tbody><tr>
<td>활성 중</td>
<td>null</td>
<td>✅</td>
</tr>
<tr>
<td>오늘 삭제</td>
<td>오늘 날짜</td>
<td>✅ (삭제 당일 유지)</td>
</tr>
<tr>
<td>이전에 삭제</td>
<td>과거 날짜</td>
<td>❌</td>
</tr>
</tbody></table>
<h3 id="실제로-cascade는-어떻게-됐을까">실제로 cascade는 어떻게 됐을까?</h3>
<p>처음 고민의 시작이었던 cascade 문제로 돌아가보자.</p>
<p>Soft Delete를 쓰면 부모 행(<code>Habit</code>)이 실제로 삭제되지 않기 때문에, DB에 설정된 <code>onDelete: Cascade</code>가 동작하지 않는다. 덕분에 <code>habitLogs</code>는 안전하게 보존된다.
다만 여기서 한 가지 확인해야 할 게 있다. </p>
<p><strong>Soft Delete</strong> 상태의 부모에 연결된 자식 데이터를 조회할 때, 자식 쿼리에서도 부모의 soft delete 상태를 체크하는지 확인해야 한다. 이 프로젝트에서는 항상 <code>habitId</code>를 기준으로 조회하고, 습관 자체의 활성 여부는 습관 목록 조회 단계에서 필터링하기 때문에 문제가 없었다.</p>
<hr>
<h3 id="soft-delete의-단점도-솔직하게">Soft Delete의 단점도 솔직하게</h3>
<p>장점만 있는 건 아니다. Soft Delete를 선택할 때 감수해야 할 것들도 있다.</p>
<p><strong>DB</strong>에 데이터가 계속 쌓인다. 삭제해도 행이 남아있으니 테이블이 계속 커진다. 서비스 규모가 커지면 일정 기간이 지난 종료 데이터를 주기적으로 정리하는 배치 작업이 필요하다.</p>
<p>조회 쿼리마다 조건을 추가해야 한다. 깜빡하고 <code>endAt</code> 조건을 빠뜨리면 삭제된 데이터가 그대로 노출된다. 팀 전체가 이 규칙을 공유하고 있어야 하고, <strong>ORM</strong> 레벨에서 글로벌 스코프로 자동 적용하는 방법도 고려해볼 수 있다. Prisma에서는 middleware 또는 <code>$extends</code>를 활용할 수 있다.</p>
<hr>
<h3 id="언제-써야-할까">언제 써야 할까?</h3>
<p>Soft Delete가 항상 정답은 아니다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>선택</th>
</tr>
</thead>
<tbody><tr>
<td>삭제 후 복구 가능성이 있어야 할 때</td>
<td>Soft Delete</td>
</tr>
<tr>
<td>연관 기록(로그, 히스토리)을 보존해야 할 때</td>
<td>Soft Delete</td>
</tr>
<tr>
<td>삭제 시점을 추적해야 할 때</td>
<td>Soft Delete</td>
</tr>
<tr>
<td>단순 임시 데이터, 복구 불필요</td>
<td>Hard Delete</td>
</tr>
<tr>
<td>DB 용량이 민감한 환경</td>
<td>Hard Delete</td>
</tr>
</tbody></table>
<p>이 프로젝트처럼 사용자의 행동 기록이 핵심 가치인 서비스라면 Soft Delete가 더 안전한 선택이다. 데이터는 복구할 수 있지만, 한 번 날린 신뢰는 복구하기 어렵다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 다중 편집 버그 해결 과정]]></title>
            <link>https://velog.io/@obebe_00/React-%EB%8B%A4%EC%A4%91-%ED%8E%B8%EC%A7%91-%EB%B2%84%EA%B7%B8-%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@obebe_00/React-%EB%8B%A4%EC%A4%91-%ED%8E%B8%EC%A7%91-%EB%B2%84%EA%B7%B8-%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Wed, 22 Apr 2026 17:30:37 GMT</pubDate>
            <description><![CDATA[<p>스터디 습관 관리 앱을 개발하는 프로젝트를 진행 중 모달 안에서 여러 습관 항목의 이름을 자유롭게 편집하고, &quot;수정 완료&quot; 버튼 하나로 한 번에 저장하는 기능을 구현해야 했다.
<img src="https://velog.velcdn.com/images/obebe_00/post/9b917503-a43d-46da-9910-e29022cee249/image.png" alt="">
항목을 클릭하면 인풋으로 전환되어 이름을 수정할 수 있고, &quot;수정 완료&quot;를 누르면 변경사항이 서버에 반영되는 구조였다.</p>
<p>언뜻 단순해 보이는 기능이었지만 테스트하다가 이상한 점을 발견했다.</p>
<hr>
<h1 id="버그-발견">버그 발견</h1>
<p>두 가지 이상의 항목을 수정한 뒤 <code>수정 완료</code>를 눌렀더니, 마지막 항목만 저장되고 이전에 수정한 내역들은 반영이 되지 않았다.</p>
<hr>
<h1 id="원인-분석">원인 분석</h1>
<p>코드에 이유가 있었다.</p>
<pre><code class="language-jsx">// 기존 코드
const [editingHabitId, setEditingHabitId] = useState(null);
const [editingValue, setEditingValue] = useState(&quot;&quot;);</code></pre>
<p><code>editingHabitId</code>는 현재 포커스된 항목 하나의 ID만 기억한다. 그렇기에 수정 후 다른 항목을 수정하면서 포커싱이 옮겨지면 <code>editingHabitId</code>가 덮어씌워지고, 이전 변경사항은 상태에 남아있지 않게 되는 것이다.</p>
<p>&quot;수정 완료&quot;로직도 마찬가지였다.</p>
<pre><code class="language-jsx">// editingHabitId 하나만 저장 → 마지막으로 편집한 항목만 API 호출
if (editingHabitId !== null &amp;&amp; editingValue.trim() !== originalHabitName) {
  await editHabit(editingHabitId, editingValue.trim());
}</code></pre>
<p>결국 저장할 수 있는 건 <code>editingHabitId</code> 하나뿐이니, 마지막으로 클릭한 항목만 전송됐던 것이다.</p>
<hr>
<h1 id="해결-방향">해결 방향</h1>
<p>이것을 해결하기 위해 여러 방안을 모색해봤지만 별다른 수가 떠오르지 않았고 생각해낸 방법은 &quot;수정 완료를 누르기 전까지 모든 변경사항을 어딘가에 쌓아두면 좋겠다&quot;였다.</p>
<p>항목 하나를 추적하는 <code>editingHabitId</code>대신, 변경된 모든 항목을 담는 객체를 도입해보기로 했다.</p>
<pre><code class="language-jsx">// { [habitId]: 수정된 이름 } 형태로 누적
stagedEdits = {
  1: &quot;운동하기 (수정됨)&quot;,
  3: &quot;물 마시기 (수정됨)&quot;,
}</code></pre>
<p>스테이징의 의미처럼 아직 서버에는 반영되지 않은, 저장 직전 상태를 나타내기 위해 <code>stagedEdits</code>로 변수명을 정했다.</p>
<p>API가 항목 하나씩만 받더라도 문제없었다. <code>Promiss.all</code>로 병렬 호출하면 여러 항목을 동시에 전송할 수 있다.</p>
<pre><code class="language-jsx">// PATCH /habits/1
// PATCH /habits/3   → 세 요청이 동시에 전송됨
// PATCH /habits/5
await Promise.all(editPromises);</code></pre>
<hr>
<h1 id="구현">구현</h1>
<p>① stagedEdits 상태 추가</p>
<pre><code class="language-jsx">const [stagedEdits, setStagedEdits] = useState({});</code></pre>
<p>② 편집값이 바뀔 때마다 누적</p>
<pre><code class="language-jsx">jsconst handleEditChange = (habitId, value) =&gt; {
  setEditingValue(value);  // 인풋 표시용 (기존 유지)
  setStagedEdits((prev) =&gt; ({ ...prev, [habitId]: value }));  // 변경사항 누적
};</code></pre>
<p>③ 수정 완료 시 stagedEdits 전체를 API로 전송</p>
<pre><code class="language-jsx">jsconst handleConfirm = async () =&gt; {
  const editPromises = Object.entries(stagedEdits)
    .filter(([, name]) =&gt; name.trim())
    .map(([id, name]) =&gt; editHabit(Number(id), name.trim()));

  await Promise.all(editPromises);
  setStagedEdits({});  // 저장 후 초기화
};</code></pre>
<hr>
<h1 id="파생-버그---수정한-내용이-화면에서-사라진다">파생 버그 - 수정한 내용이 화면에서 사라진다</h1>
<p>이렇게 수정 후 테스트를 진행해보니 먼저 수정한 습관 이후에 다른 습관으로 포커싱을 옮겨 수정하려고하면 화면에는 이전 이름이 다시 나타났다.</p>
<p><code>stagedEdits</code>에는 잘 저장되어있는데, 화면만 초기화된 것이다.</p>
<hr>
<p>원인은 각 항목이 편집 모드에서 벗어날 때 <code>habit.habitName</code>(서버의 원본값)을 그대로 표시하기 때문이었다.</p>
<pre><code class="language-jsx">// 편집 모드가 아닐 때 원본값만 표시 → staged된 값이 무시됨
&lt;span&gt;{habit.habitName}&lt;/span&gt;</code></pre>
<p>서버 데이터는 아직 바뀌지 않았으니 어쩌면 당연한 결과였다. <code>stagedEdits</code>에 값이 있으면 그걸 우선 보여주도록 수정했다.</p>
<pre><code class="language-jsx">// 부모 컴포넌트 — stagedName prop 추가
&lt;HabitItem
  stagedName={stagedEdits[habit.id]}
  ...
/&gt;

// HabitItem — staged 값이 있으면 우선 표시, 없으면 원본
&lt;span&gt;{stagedName ?? habit.habitName}&lt;/span&gt;</code></pre>
<p>항목으로 다시 돌아올 때도 이전 편집값이 복원되어야 한다.</p>
<pre><code class="language-jsx">const handleEditStart = (habit) =&gt; {
  setEditingHabitId(habit.id);
  // stagedEdits에 값이 있으면 복원, 없으면 원본값으로 시작
  setEditingValue(stagedEdits[habit.id] ?? habit.habitName);
};</code></pre>
<hr>
<h1 id="파생-버그---삭제한-항목에-api가-호출된다">파생 버그 - 삭제한 항목에 API가 호출된다</h1>
<p>항목을 삭제한 뒤 &quot;수정 완료&quot;를 눌렀더니 에러가 발생했다. 삭제한 항목의 ID가 stagedEdits에 그대로 남아있어, 이미 존재하지 않는 항목에 PATCH 요청이 날아갔기 때문이었다.</p>
<p>삭제 시 stagedEdits도 함께 정리하면 해결된다.</p>
<pre><code class="language-jsx">jsconst handleDelete = async (habitId) =&gt; {
  await removeHabit(habitId);

  setStagedEdits((prev) =&gt; {
    const next = { ...prev };
    delete next[habitId];  // 삭제한 항목은 stagedEdits에서도 제거
    return next;
  });
};</code></pre>
<hr>
<h1 id="최종-흐름">최종 흐름</h1>
<pre><code class="language-md">&quot;운동하기&quot; 클릭 → &quot;헬스&quot; 입력
→ stagedEdits = { 1: &quot;헬스&quot; }

&quot;물 마시기&quot; 클릭 → &quot;운동하기&quot;는 화면에 &quot;헬스&quot;로 유지
→ &quot;물 2L&quot; 입력
→ stagedEdits = { 1: &quot;헬스&quot;, 3: &quot;물 2L&quot; }

수정 완료 클릭
→ PATCH /habits/1, PATCH /habits/3 동시 호출
→ 둘 다 저장 ✅</code></pre>
<hr>
<p>처음에는 단순한 버그처럼 보였지만, 고치는 과정에서 파생 버그가 두 번이나 더 나왔다. 공통된 원인은 하나였다. &quot;저장 전 클라이언트 상태&quot;를 명확하게 설계하지 않으면 화면과 실제 데이터가 어긋난다.</p>
<p><code>stagedEdits</code> 패턴은 할 일 목록, 설정 화면, 인라인 편집이 있는 어떤 UI에서라도 응용할 수 있다고 한다. 다음에 비슷한 UX를 구현하게 된다면 이 구조를 떠올려봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그인 없이 비밀번호 기반 접근 제어 (Session 활용)]]></title>
            <link>https://velog.io/@obebe_00/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%97%86%EC%9D%B4-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EA%B8%B0%EB%B0%98-%EC%A0%91%EA%B7%BC-%EC%A0%9C%EC%96%B4-Session-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@obebe_00/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%97%86%EC%9D%B4-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EA%B8%B0%EB%B0%98-%EC%A0%91%EA%B7%BC-%EC%A0%9C%EC%96%B4-Session-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Sat, 11 Apr 2026 10:51:22 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 &#39;오늘의 습관&#39; 기능을 구현하며 가장 고민됐던 부분은 <strong>접근 제어 방식</strong>이었다.</p>
<p>페이지의 흐름은 아래와 같다.</p>
<blockquote>
<p>생성된 스터디(studyId)에 대해 비밀번호를 입력하고 일치하면 &#39;오늘의 습관&#39;페이지로 이동할 수 있다.</p>
</blockquote>
<p>하지만 로그인 기능이 없는 상태였기에 다음과 같은 문제가 발생했다.</p>
<ul>
<li>URL 직접 접근</li>
<li>새로고침 시 비밀번호 재입력 여부</li>
</ul>
<p>이 글에서는 위 문제를 해결하기 위해 어떤 구조를 설계했고, 그 과정에서 어떤 시행착오를 겪었는지 정리해보겠다.</p>
<hr>
<h1 id="구조">구조</h1>
<p>우선 연관된 페이지의 기능 구조는 아래와 같다.</p>
<h2 id="study-페이지">Study 페이지</h2>
<ul>
<li>스터디 단위의 페이지</li>
<li>각 스터디는 <code>studyId(PK)</code>와 비밀번호(<code>password</code>)를 가진다.</li>
</ul>
<h2 id="오늘의-습관-페이지">오늘의 습관 페이지</h2>
<ul>
<li>각 스터디에 속한 &quot;오늘의 습관&quot; 관리 페이지</li>
</ul>
<pre><code class="language-md">스터디 페이지에서 비밀번호 인증 후 오늘의 습관 페이지로 이동하는 구조</code></pre>
<hr>
<h1 id="문제-해결-접근-방법">문제 해결 접근 방법</h1>
<h2 id="1-비밀번호를-계속-사용">1. 비밀번호를 계속 사용</h2>
<p>가장 먼저 떠올린 방법은 비밀번호를 계속 들고 다니면서 요청마다 검증하는 방식이었다.
하지만 매 요청마다 비밀번호를 처리해야하는 것은 상당히 비효율적이라 생각해 다른 방식을 찾아보았다.</p>
<hr>
<h2 id="2-단순-인증-여부만-저장">2. 단순 인증 여부만 저장</h2>
<p>다음으로 생각한 방법은 비밀번호를 입력하여 입장에 성공하였을 때, 인증 여부만 저장하는 것이었다.
그런데 이렇게 하면 어떤 스터디에 대한 인증인지 구분할 수 없어 제대로 관리가 되지 않을 수 있다.</p>
<hr>
<h2 id="3-studyid를-저장하는-방식">3. studyId를 저장하는 방식</h2>
<p>비밀번호 인증 후 studyId를 세션에 담아 url과 일치하는지를 비교하여 인증 여부를 판단하는 것을 떠올렸다.</p>
<p>이 경우 다른 스터디로 이동하면 어떻게 해야할 지 해결이 되지 않았다.</p>
<hr>
<h1 id="최정-해결-방법-session을-활용한-study-단위-인증-상태를-관리">최정 해결 방법: Session을 활용한 study 단위 인증 상태를 관리</h1>
<p>앞서 고민했던 방식들의 공통 문제는 두 가지였다.</p>
<pre><code>1. 인증 상태를 어떻게 유지할 것인지 (새로고침 시 인증 유지)
2. 인증되지 않은 사용자가 URL을 직접 입력했을 때, 이를 어떻게 막을 것인지(url 직접 접근)</code></pre><hr>
<h2 id="session-사용">Session 사용</h2>
<p>인증 상태를 유지하는 방법으로</p>
<ul>
<li>localStorage에 저장</li>
<li>매 요청마다 비밀번호 입력</li>
<li>Session 사용</li>
</ul>
<p>이렇게 세 가지를 떠올렸고, session을 선택한 이유는 다음과 같다.</p>
<h3 id="인증-상태를-서버에서-안전하게-관리-가능">인증 상태를 서버에서 안전하게 관리 가능</h3>
<ul>
<li>비밀번호를 저장하지 않아도 됨</li>
<li>인증 결과만 서버에 담아둠</li>
</ul>
<p>보안 측면에서 유리</p>
<h3 id="새로고침-시에도-상태-유지-가능">새로고침 시에도 상태 유지 가능</h3>
<ul>
<li>session은 서버에 저장되기에 브라우저는 sessionId를 쿠키로 유지</li>
</ul>
<h3 id="추가-요청-없이-인증-상태-확인-가능">추가 요청 없이 인증 상태 확인 가능</h3>
<ul>
<li>매 요청마다 비밀번호를 다시 확인할 필요가 없음</li>
</ul>
<hr>
<h1 id="session-구조">Session 구조</h1>
<pre><code class="language-jsx">req.session.auth = {
    [studyId]: true
};</code></pre>
<p>: 해당 스터디에 대해 인증이 완료 됨</p>
<hr>
<h2 id="인증-흐름">인증 흐름</h2>
<h3 id="1️⃣-비밀번호-인증">1️⃣ 비밀번호 인증</h3>
<pre><code>if (password가 일치하면) { req.session.auth[studyId] = true; }</code></pre><hr>
<h3 id="2️⃣-이후-요청-처리">2️⃣ 이후 요청 처리</h3>
<pre><code>if (!req.session.auth?.[studyId]) { return res.status(403); }</code></pre><p>이후 매 요청에서 session을 통하여 인증 상태를 확인하고
인증 상태는 스터디 단위로 관리한다</p>
<hr>
<p>session을 처음 사용해보기에 session에 담는 값을 통하여 인증 상태를 관리하는 구조를 처음 접해보고 이해해보며 글을 정리해본다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB - 관계형 데이터베이스 개념 정리]]></title>
            <link>https://velog.io/@obebe_00/DB-%EA%B4%80%EA%B3%84%ED%98%95-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@obebe_00/DB-%EA%B4%80%EA%B3%84%ED%98%95-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 07 Apr 2026 09:40:56 GMT</pubDate>
            <description><![CDATA[<p>Prisma에 대해 <a href="https://velog.io/@obebe_00/Prisma">지난 포스팅</a>에서 살펴보며 느낀 점은  DB 개념을 알아야 이것이 효과가 있다는 것이었다.</p>
<p>그래서! 다시 정리해서 기록해두고자 한다.</p>
<hr>
<h1 id="db">DB</h1>
<p>관계형 데이터베이스</p>
<blockquote>
<p>데이터를 테이블(표) 형태로 저장하는 구조</p>
</blockquote>
<p>like Excel</p>
<ul>
<li>테이블 -&gt; 시트</li>
<li>행(Row) -&gt; 데이터 한 줄</li>
<li>열(Column) -&gt; 데이터 항목</li>
</ul>
<p>이것들이 서로 연관을 지어있기에 이를 관계형 데이터베이스라 한다.</p>
<hr>
<h2 id="primary-key-pk">Primary Key (PK)</h2>
<p><strong>각 데이터를 구분하는 고유한 값</strong></p>
<pre><code>📍 중복 불가능
📍 NULL 불가능
❗ 테이블당 하나 존재</code></pre><p>으로 이것은 <strong>데이터를 식별하는 기준</strong>이 된다.</p>
<hr>
<h2 id="foreign-key-fk">Foreign Key (FK)</h2>
<p>Foreign key는 다른 테이블의 PK를 참조하는 값이다.</p>
<p>예를 들어 하나의 게시글을 두고, 이 게시글이 어떤 사용자의 게시글인지는 <code>users.id</code>를 보면 알 수 있다.</p>
<p>테이블 간 연결을 해주며 데이터의 일관성을 유지하는 역할을 한다.</p>
<h3 id="참조-무결성">참조 무결성</h3>
<p>이런 Foreign Key에서 중요한 개념으로 <strong>참조 무결성</strong>이 있다.</p>
<pre><code class="language-md">FK는 반드시 존재하는 PK만 참조할 수 있다</code></pre>
<p>이것은 존재하는 값들만 사용이 가능하다는 의미이고, 잘못된 데이터 연결을 막아준다</p>
<hr>
<h2 id="관계-relationship">관계 (Relationship)</h2>
<p>관계는 <strong>테이블과 테이블 사이의 연결 구조</strong>를 의미한다.</p>
<h3 id="🔗-1--n-일대다">🔗 1 : N (일대다)</h3>
<p>가장 많이 사용하는 관계</p>
<p>&rarr; 한 명의 사용자가 여러 개의 게시글을 가질 수 있다</p>
<h3 id="🔗-n--1-다대일">🔗 N : 1 (다대일)</h3>
<p>&rarr; 여러 게시글은 하나의 사용자에 속한다</p>
<h3 id="🔗-n--n-다대다">🔗 N : N (다대다)</h3>
<p>&rarr; 여러 개가 서로 여러 개와 연결되는 구조</p>
<pre><code>사용자 ↔ 태그
학생 ↔ 수업</code></pre><p>이 경우 중간 테이블이 필요하다.</p>
<hr>
<h1 id="제약-조건-constraints">제약 조건 (Constraints)</h1>
<p>기본 개념에 대해 알아보았고 이제 제약 조건에 대해 알아보고자 한다</p>
<p>제약조건이라함은</p>
<pre><code>잘못된 데이터가 들어오는 것을 막기 위한 규칙
즉, 데이터의 정확성과 일관성을 유지하기 위한 장치</code></pre><p>대표적인 제약 조건들을 정리해서 보겠다.</p>
<h2 id="제약-조건">제약 조건</h2>
<table>
<thead>
<tr>
<th>제약 조건</th>
<th>의미</th>
<th>특징</th>
<th>사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td>PRIMARY KEY</td>
<td>데이터를 구분하는 고유값</td>
<td>중복 ❌, NULL ❌</td>
<td>id</td>
</tr>
<tr>
<td>FOREIGN KEY</td>
<td>다른 테이블과 연결</td>
<td>참조 무결성 유지</td>
<td>user_id</td>
</tr>
<tr>
<td>NOT NULL</td>
<td>반드시 값 입력</td>
<td>NULL 허용 ❌</td>
<td>name</td>
</tr>
<tr>
<td>UNIQUE</td>
<td>중복 값 금지</td>
<td>같은 값 2번 ❌</td>
<td>email</td>
</tr>
<tr>
<td>DEFAULT</td>
<td>기본값 자동 설정</td>
<td>값 없으면 자동 입력</td>
<td>created_at</td>
</tr>
</tbody></table>
<p>예시를 살펴보자.</p>
<p>실습 과제로 나왔던 온라인 강의 플랫폼 테이블 구조 설계다.</p>
<pre><code>요구사항
**요구사항:**

- 수강생은 이름, 이메일, 가입일 정보를 가진다
- 강사는 이름, 이메일, 소개글, 전문 분야 정보를 가진다
- 강의는 제목, 설명, 가격, 카테고리, 생성일 정보를 가지며, 한 명의 강사가 여러 강의를 만들 수 있다
- 수강생이 강의를 결제하면 수강 등록이 되고, 등록일과 결제 금액이 기록된다
- 수강생은 수강 중인 강의에 별점(1~5)과 텍스트 리뷰를 남길 수 있다
- 한 수강생이 같은 강의에 리뷰를 중복으로 남길 수 없다

**설계 조건:**

- PK, FK 명시
- UNIQUE 제약조건이 필요한 곳을 찾아서 적용
- DEFAULT 값이 적절한 곳에 설정</code></pre><hr>
<h3 id="수강생">수강생</h3>
<table>
<thead>
<tr>
<th>수강생 ID (PK)</th>
<th>이름</th>
<th>이메일 (UNIQUE, NOT NULL)</th>
<th>가입일 (DEFAULT NOW())</th>
</tr>
</thead>
<tbody><tr>
<td>id</td>
<td>name (NOT NULL)</td>
<td>email (UNIQUE, NOT NULL)</td>
<td>created_at</td>
</tr>
</tbody></table>
<h3 id="강사">강사</h3>
<table>
<thead>
<tr>
<th>강사 ID (PK)</th>
<th>이름</th>
<th>이메일 (UNIQUE, NOT NULL)</th>
<th>소개글</th>
<th>전문 분야</th>
</tr>
</thead>
<tbody><tr>
<td>id</td>
<td>name (NOT NULL)</td>
<td>email (UNIQUE, NOT NULL)</td>
<td>bio</td>
<td>specialty</td>
</tr>
</tbody></table>
<h3 id="강의">강의</h3>
<table>
<thead>
<tr>
<th>강의 ID (PK)</th>
<th>제목</th>
<th>설명</th>
<th>가격 (NOT NULL)</th>
<th>카테고리</th>
<th>생성일 (DEFAULT NOW())</th>
<th>강사 ID (FK, NOT NULL)</th>
</tr>
</thead>
<tbody><tr>
<td>id</td>
<td>title (NOT NULL)</td>
<td>description</td>
<td>price (NOT NULL)</td>
<td>category</td>
<td>created_at</td>
<td>instructor_id</td>
</tr>
</tbody></table>
<h3 id="수강--결제">수강 / 결제</h3>
<table>
<thead>
<tr>
<th>결제 ID (PK)</th>
<th>수강생 ID (FK, NOT NULL)</th>
<th>등록일 (DEFAULT NOW())</th>
<th>결제 금액 (NOT NULL)</th>
<th>강의 ID (FK, NOT NULL)</th>
</tr>
</thead>
<tbody><tr>
<td>id</td>
<td>student_id</td>
<td>enrolled_at</td>
<td>payment_amount</td>
<td>lecture_id</td>
</tr>
</tbody></table>
<h3 id="리뷰">리뷰</h3>
<table>
<thead>
<tr>
<th>결제 ID (PK)</th>
<th>수강생 ID (FK, NOT NULL)</th>
<th>등록일 (DEFAULT NOW())</th>
<th>결제 금액 (NOT NULL)</th>
<th>강의 ID (FK, NOT NULL)</th>
</tr>
</thead>
<tbody><tr>
<td>id</td>
<td>student_id</td>
<td>enrolled_at</td>
<td>payment_amount</td>
<td>lecture_id</td>
</tr>
</tbody></table>
<hr>
<p>정답이 맞는지는 모르지만, 이런 식으로 개념과 제약 조건을 사용해보았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Prisma]]></title>
            <link>https://velog.io/@obebe_00/Prisma</link>
            <guid>https://velog.io/@obebe_00/Prisma</guid>
            <pubDate>Tue, 07 Apr 2026 05:41:39 GMT</pubDate>
            <description><![CDATA[<p>직접 DB를 다뤄야 하는 순간이 찾아왔고 그렇게 Prisma를 처음 접하게 되었다.
처음부터 무엇인지에 대한 감은 전혀 잡히지 않았기에 이렇게 정리해보며 다시 공부해보고자 한다.</p>
<h1 id="prisma">Prisma</h1>
<p>Prisma는 ORM으로, 객체를 스키마(schema)로 정의한 다음 그 객체와 내가 선택한 데이터베이스를 연결시켜주는 <strong>매개체</strong>이다.</p>
<h3 id="orm은-무엇인가">ORM은 무엇인가</h3>
<p>여기서 처음 듣는 단어인 <code>ORM</code>이 무엇인지에 대해 짚고 넘어가보자
ORM은 👉 <strong>데이터베이스를 코드(객체)처럼 다루게 해주는 개념</strong>이다.</p>
<p>기존 방식에서는 DB에 접근할 때 SQL을 직접 작성해야 했다.
&#39;어떤 데이터를 어떻게 가져올지&#39;를 쿼리로 직접 표현했던 반면 ORM을 사용하면 DB를 직접 다루는 대신 <strong>코드에서 정의한 모델을 기준으로 데이터를 다룬다.</strong></p>
<pre><code class="language-md">SQL 방식 → DB 중심
ORM 방식 → 코드(객체) 중심</code></pre>
<p>즉, ORM은 DB를 다루는 방식을 &quot;쿼리&quot;에서 &quot;객체&quot;로 바꿔주는 역할을 한다.
이로 인하여 <strong>SQL 작성 부담이 줄어들고, 코드 흐름 안에서 데이터를 다룰 수 있게 되는 것이다</strong>.</p>
<hr>
<h3 id="prisma의-역할">Prisma의 역할</h3>
<p>ORM의 개념에 대해 정리를 해보았고, 다시 돌아와 Prisma를 보자.
Prisma는 그것을 실제로 사용할 수 있게 해주는 도구이다</p>
<p>단순히 DB에 연결하는 역할이 아닌</p>
<ul>
<li><strong>DB 구조를 코드로 정의하고</strong></li>
<li><strong>그 구조를 실제 DB와 연결해주는 역할</strong>을 한다.</li>
</ul>
<p>그럼 어떻게 연결을 해줄까?</p>
<h1 id="prisma의-흐름">Prisma의 흐름</h1>
<pre><code class="language-md">1. schema로 구조 정의
2. DB에 반영
3. 코드에서 사용</code></pre>
<h3 id="1-구조-정의-schema">1. 구조 정의 (schema)</h3>
<p>먼저 <code>schema.prisma</code> 파일에서 DB 구조를 정의한다.</p>
<p>이곳에서</p>
<ul>
<li>테이블(model)</li>
<li>필드</li>
<li>관계
전부 코드로 작성한다.</li>
</ul>
<hr>
<h3 id="2-db-반영-migration">2. DB 반영 (migration)</h3>
<p>schema를 작성한 뒤에 이 내용을 실제 DB에 반영해야 한다.</p>
<p>이 과정을 migration이라고 한다.</p>
<ul>
<li>코드로 정의한 구조를 실제 DB에 적용하는 단계</li>
</ul>
<hr>
<h3 id="3-코드에서-사용-prisma-client">3. 코드에서 사용 (Prisma Client)</h3>
<p>DB가 생성되면 이제 Prisma Client를 통해 데이터를 다룬다.</p>
<p>예를 들어 User 데이터를 조회한다고 하면</p>
<pre><code class="language-jsx">import { PrismaClient } from &quot;@prisma/client&quot;;

const prisma = new PrismaClient();

async function getUsers() {
  const users = await prisma.user.findMany();
  console.log(users);
}

getUsers();</code></pre>
<ul>
<li><code>user</code>라는 모델을 기준으로 <code>findMany()</code> 메서드를 호출해서 데이터를 가져온다.</li>
</ul>
<p>또 다른 예시로 데이터를 생성할 수도 있다.</p>
<pre><code class="language-jsx">await prisma.user.create({
  data: {
    email: &quot;test@test.com&quot;,
  },
});</code></pre>
<ul>
<li>이런 식으로 SQL 없이도 데이터를 조회하고 생성할 수 있다.</li>
</ul>
<hr>
<p>Prisma를 다시 정리해보면,</p>
<pre><code class="language-md">코드로 DB 구조 정의 → DB에 반영 → 코드로 데이터 사용</code></pre>
<p>SQL을 대신하여 코드로 작업할 수 있게 해준다는 점에서 마음에 들기도 했다.</p>
<p>하지만 가장 중요한 데이터베이스 관련 개념이 정리되어있지 않으면 제대로 사용할 수 없기에 이것에 대한 복습도 필요해보인다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 정규화]]></title>
            <link>https://velog.io/@obebe_00/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%A0%95%EA%B7%9C%ED%99%94</link>
            <guid>https://velog.io/@obebe_00/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%A0%95%EA%B7%9C%ED%99%94</guid>
            <pubDate>Mon, 06 Apr 2026 00:01:14 GMT</pubDate>
            <description><![CDATA[<h1 id="데이터베이스-정규화-normalization">데이터베이스 정규화 (Normalization)</h1>
<p>데이터베이스를 설계하다 보면 &#39;데이터베이스를 나누는 기준&#39;에 대한 고민이 생긴다.
이걸 정립해주는 개념이 <strong>정규화(Normalization)</strong>이다.</p>
<p>정규화란 </p>
<pre><code class="language-md">데이터의 중복을 줄이고, 이상 현상을 방지하기 위해 테이블을 구조적으로 나누는 과정</code></pre>
<hr>
<h2 id="정규화를-해야하는-이유">정규화를 해야하는 이유</h2>
<p>정규화를 하지 않았을 경우 발생하는 문제에 대해서 살펴보자.</p>
<hr>
<p><strong>❌ 1. 중복 데이터 발생</strong></p>
<pre><code class="language-md">학생 | 과목 | 교수
------------------
A    | DB   | Kim
A    | OS   | Lee</code></pre>
<p>👉 학생 A가 여러 번 반복됨</p>
<hr>
<p><strong>❌ 2. 수정 이상 (Update Anomaly)</strong></p>
<p>교수 이름이 바뀌면?</p>
<p>👉 모든 행을 다 수정해야 한다</p>
<p>→ 하나라도 빠지면 데이터 불일치 발생</p>
<hr>
<p><strong>❌ 3. 삽입 이상 (Insert Anomaly)</strong></p>
<p>새로운 과목을 추가하려는데 학생이 없다면?</p>
<p>👉 데이터를 넣을 수 없음</p>
<hr>
<p><strong>❌ 4. 삭제 이상 (Delete Anomaly)</strong></p>
<p>학생 A를 삭제하면?</p>
<p>👉 해당 과목 정보까지 같이 사라짐</p>
<hr>
<p>이러한 문제 발생을 방지하기 위하여 정규화를 사용한다.</p>
<h2 id="정규화의-목적">정규화의 목적</h2>
<p>정규화는 단순히 테이블을 나누는 것이 아니다.</p>
<pre><code>데이터를 일관성 있고 효율적으로 관리하기 위한 구조를 만드는 과정이다.</code></pre><ul>
<li>데이터 중복 제거</li>
<li>이상 현상 방지 (Update / Insert / Delete)</li>
<li>데이터 무결성 유지</li>
<li>유지보수 용이성 향상</li>
</ul>
<hr>
<h1 id="정규화-과정">정규화 과정</h1>
<p>정규화는 단계적으로 진행되며, 일반적으로 <strong>제3정규형(3NF)</strong>까지 적용한다.</p>
<h2 id="1️⃣-제1정규형-1nf">1️⃣ 제1정규형 (1NF)</h2>
<p>👉 모든 컬럼은 원자값(Atomic Value)을 가져야 한다</p>
<hr>
<p><strong>❌ 정규화 전</strong></p>
<pre><code>학생 | 전화번호
------------------------
A    | 010-1111, 010-2222</code></pre><p>👉 하나의 컬럼에 여러 값이 존재</p>
<hr>
<p><strong>✅ 정규화 후</strong></p>
<pre><code>학생 | 전화번호
------------------------
A    | 010-1111
A    | 010-2222</code></pre><p>👉 하나의 컬럼에는 하나의 값만 존재</p>
<hr>
<h2 id="2️⃣-제2정규형-2nf">2️⃣ 제2정규형 (2NF)</h2>
<pre><code>부분 함수 종속 제거</code></pre><p>👉 기본키의 일부에만 의존하는 컬럼 제거</p>
<p><strong>❌ 정규화 전</strong></p>
<pre><code>학생ID | 과목ID | 학생이름
---------------------------
1       | DB     | Kim
1       | OS     | Kim</code></pre><p>👉 학생이름은 학생ID에만 의존</p>
<hr>
<p><strong>✅ 정규화 후</strong></p>
<pre><code>학생 테이블
학생ID | 이름
------------
1      | Kim

수강 테이블
학생ID | 과목ID
-------------
1      | DB
1      | OS</code></pre><p>👉 테이블을 분리하여 중복 제거</p>
<hr>
<h2 id="3️⃣-제3정규형-3nf">3️⃣ 제3정규형 (3NF)</h2>
<pre><code>이행 함수 종속 제거</code></pre><p>👉 기본키가 아닌 컬럼이 다른 컬럼에 의존하는 경우 제거</p>
<p><strong>❌ 정규화 전</strong></p>
<pre><code>학생ID | 학과ID | 학과명
-------------------------
1      | 10     | 컴퓨터공학</code></pre><p>👉 학과명은 학과ID에 의존</p>
<hr>
<p><strong>✅ 정규화 후</strong></p>
<pre><code>학생
학생ID | 학과ID

학과
학과ID | 학과명</code></pre><p>👉 관계를 분리하여 구조 개선</p>
<hr>
<p>하지만 이런 정규화에도 한계는 있다.
너무 많이 나누게 되면</p>
<ul>
<li>JOIN 증가</li>
<li>쿼리 성능 저하</li>
<li>구조 복잡도 증가</li>
</ul>
<p>이런 경우에는 <strong>반정규화(Denormalization)</strong>를 고려하기도 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - key 설정 이유와 주의점]]></title>
            <link>https://velog.io/@obebe_00/React-key-%EC%84%A4%EC%A0%95-%EC%9D%B4%EC%9C%A0%EC%99%80-%EC%A3%BC%EC%9D%98%EC%A0%90</link>
            <guid>https://velog.io/@obebe_00/React-key-%EC%84%A4%EC%A0%95-%EC%9D%B4%EC%9C%A0%EC%99%80-%EC%A3%BC%EC%9D%98%EC%A0%90</guid>
            <pubDate>Sun, 05 Apr 2026 23:18:26 GMT</pubDate>
            <description><![CDATA[<p>React에서 배열을 랜더링할 때 <code>key</code>를 설정해야하는 이유 그리고 주의점에 대해서 알아보고자 한다.</p>
<p>React에서 배열을 랜더링 하는 코드는 자주 사용된다.</p>
<p>예를 들어 간단한 리스트를 출력한다고 해보자.</p>
<pre><code class="language-jsx">const items = [&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cherry&quot;];

function App() {
  return (
    &lt;ul&gt;
      {items.map((item) =&gt; (
        &lt;li&gt;{item}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}</code></pre>
<p>이 코드는 정상적으로 동작하지만, 콘솔을 보면 이런 경고가 뜬다.</p>
<blockquote>
<p>Each child in a list should have a unique &quot;key&quot; prop.</p>
</blockquote>
<p>왜 이러한 경고가 발생하는 것일까?
React의 랜더링 방식을 다시 살펴보면 알 수 있다.</p>
<p>React는 상태가 변경될 때마다 이전 화면과 새로운 화면을 비교해서 <strong>바뀐 부분만 업데이트</strong>한다.</p>
<p>그런데 key가 없다면 React는 &#39;이 요소가 이전에 있던 그 요소가 맞는지&#39;에 대하여 정확히 판단할 수 없다.</p>
<hr>
<pre><code class="language-jsx">const items = [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;];</code></pre>
<p>이렇게 key가 없을 때는</p>
<pre><code class="language-jsx">{items.map((item) =&gt; (
  &lt;li&gt;{item}&lt;/li&gt;
))}</code></pre>
<p>이 상태에서 <code>&quot;A&quot;</code>가 삭제되면</p>
<pre><code class="language-jsx">[&quot;B&quot;, &quot;C&quot;]</code></pre>
<p>React는 단순히 <strong>순서 기준</strong>으로 비교하기에</p>
<pre><code class="language-md">A → B (변경됨)
B → C (변경됨)
C → 삭제됨</code></pre>
<blockquote>
<p>즉, 모든 요소가 바뀐 것으로 인식</p>
</blockquote>
<p>이것은 단순한 성능 문제에 그치지 않고 실제 UI에 이러한 일이 일어날 경우 예를 들어</p>
<ul>
<li>input에 입력한 값이 다른 아이템으로 이동</li>
<li>체크박스 상태가 꼬임</li>
<li>애니메이션이 이상하게 동작</li>
</ul>
<p>과 같은 일이 일어날 수 있다</p>
<hr>
<p>그래서 key를 설정해주면</p>
<pre><code class="language-jsx">{items.map((item) =&gt; (
  &lt;li key={item}&gt;{item}&lt;/li&gt;
))}</code></pre>
<p>값이 아닌 key를 기준으로 비교하여</p>
<pre><code class="language-md">A → 삭제됨
B → 그대로 유지
C → 그대로 유지</code></pre>
<p>필요한 부분만 업데이트를 진행한다</p>
<hr>
<h1 id="주의점">주의점</h1>
<p>결론부터 말하면 <strong>아무 값이나 넣으면 안된다</strong></p>
<p>key는 단순히 값이 있으면 되는 것이 아닌</p>
<pre><code class="language-md">요소를 안정적으로 식별할 수 있는 값</code></pre>
<p>이어야 한다.</p>
<p><strong>❌ 잘못된 예</strong></p>
<pre><code class="language-jsx">&lt;li key={Math.random()}&gt;{item}&lt;/li&gt;</code></pre>
<ul>
<li>렌더링될 때마다 값이 바뀜</li>
<li>React 입장에서는 매번 새로운 요소
👉 불필요한 재렌더링 발생 (성능 문제)</li>
</ul>
<hr>
<pre><code class="language-jsx">&lt;li key={index}&gt;{item}&lt;/li&gt;</code></pre>
<ul>
<li>배열 순서가 바뀌면 key도 같이 바뀜
👉 다른 요소를 같은 요소로 착각 (버그 발생)</li>
</ul>
<hr>
<p><strong>✅ 올바른 기준</strong></p>
<pre><code class="language-jsx">&lt;li key={item.id}&gt;{item.name}&lt;/li&gt;</code></pre>
<ul>
<li>고유하다 (unique)</li>
<li>변하지 않는다 (stable)
👉 이 두 가지 조건이 가장 중요하다</li>
</ul>
<p>따라서 key는 아무 값이 아닌, 👉 “변하지 않는 고유한 식별자”를 넣어야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 기본 폴더 구조 정리]]></title>
            <link>https://velog.io/@obebe_00/React-%EA%B8%B0%EB%B3%B8-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@obebe_00/React-%EA%B8%B0%EB%B3%B8-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 01 Apr 2026 13:36:47 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 시작하면서 가장 먼저 고민했던 것 중 하나가 <strong>폴더 구조</strong>였다.</p>
<p>Flutter로 프로젝트를 진행했던 것과는 여러 구조들이 다를 것이라고 생각해서 관련된 내용을 정리하고 시작하고자 한다.</p>
<hr>
<h1 id="📦-기본-폴더-구조">📦 기본 폴더 구조</h1>
<p>React에서 가장 기본적인 폴더 구조를 알아보자.</p>
<pre><code>src/
├── components/     // 재사용 가능한 UI 컴포넌트
├── pages/          // 페이지 단위 컴포넌트
├── styles/         // 전역 스타일 관리
│   ├── colors/
│   │   └── colors.css
│   ├── fonts/
│   │   └── fonts.css
│   └── global/
│       ├── reset.css
│       └── common.css
├── api/            // API 요청 함수
├── hooks/          // custom hook
├── utils/          // 공통 함수
└── assets/         // 이미지, 아이콘
    └── images/</code></pre><h2 id="폴더별-역할-정리">폴더별 역할 정리</h2>
<h3 id="components">components</h3>
<blockquote>
<p>재사용 가능한 UI 조각</p>
</blockquote>
<ul>
<li>Button</li>
<li>Card</li>
<li>Header</li>
<li>ItemCard<pre><code>여러 페이지에서 사용할 수 있도록 만드는 것이 핵심</code></pre></li>
</ul>
<hr>
<h3 id="pages">pages</h3>
<blockquote>
<p>화면 단위</p>
</blockquote>
<ul>
<li>MainPage</li>
<li>Detailpage
등등</li>
</ul>
<pre><code>하나의 화면 = 하나의 페이지</code></pre><hr>
<h3 id="styles">styles</h3>
<blockquote>
<p>전역 스타일 관리</p>
</blockquote>
<ul>
<li>colors.css</li>
<li>fonts.css</li>
<li>reset.css</li>
<li>common.css</li>
</ul>
<pre><code>디자인과 관련된 파일들</code></pre><hr>
<h3 id="api">api</h3>
<blockquote>
<p>서버 통신</p>
</blockquote>
<pre><code>* 예시
export async function getProducts() {
  const res = await fetch(&#39;/products&#39;);
  return res.json();
}</code></pre><pre><code>API 로직을 분류하여 관리하면 컴포넌트가 깔끔해진다</code></pre><hr>
<h3 id="hooks">hooks</h3>
<pre><code class="language-js">function useProducts() {
  // 데이터 fetch 로직
}</code></pre>
<pre><code>로직 재사용을 위한 공간</code></pre><hr>
<h3 id="utils">utils</h3>
<blockquote>
<p>공통 함수</p>
</blockquote>
<hr>
<h3 id="assets">assets</h3>
<blockquote>
<p>이미지, 아이콘</p>
</blockquote>
<hr>
<h3 id="flutter와-react-폴더-구조-차이">Flutter와 React 폴더 구조 차이</h3>
<p>Flutter로 프로젝트를 진행할 때는 MVC, MVVM 같은 <strong>아키텍처 패턴 중심</strong>으로 폴더를 나누는 경우가 많았다.</p>
<p>하지만 React는 조금 다르게 접근한다는 것을 알았다.</p>
<p>⚛️**React는 &#39;역할&#39;보다 &#39;기능(UI)&#39;으로 나눈다</p>
<ul>
<li>이 파일이 어디에 사용되는가</li>
</ul>
<p>컴포넌트가 기본 단위인 React에서는 컴포넌트 하나 자체가 구조 역할을 하기에 이러한 특징이 생긴다.</p>
<p>또한 React는 공식적으로 정해진 폴더 구조가 없다고 한다.</p>
<p>그래서 작은 프로젝트는 단순 구조로 진행되지만, 커질 수록 feature 기반으로 확장되는 경우가 많다고 한다.</p>
<hr>
<p>프로젝트를 시작하기 전에 폴더 구조와 같은 여러 컨벤션들을 미리 정리하고 약속해두고 시작하면서 진행 중에 생기는 크고 작은 이슈들을 미리 방지하기에 유리함을 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 디자인 시스템 정의 시 .js vs .css]]></title>
            <link>https://velog.io/@obebe_00/React-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%95%EC%9D%98-%EC%8B%9C-.js-vs-.css</link>
            <guid>https://velog.io/@obebe_00/React-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%95%EC%9D%98-%EC%8B%9C-.js-vs-.css</guid>
            <pubDate>Wed, 01 Apr 2026 12:11:06 GMT</pubDate>
            <description><![CDATA[<p>프로젝트 시작에 앞서 폴더 구조와 기본 파일들을 정리하며 디자인 시스템도 미리 정의해두고자 했다.</p>
<p>이 과정에서 디자인 시스템을 정리할 때 <code>.css</code>뿐만 아니라 <code>.js</code>로도 관리하는 방법이 있다는 것을 알게 되어 두 방식의 차이를 정리하고 어떤 상황에서 적합한지 비교해보고자 한다.</p>
<hr>
<p>우선 디자인 시스템이 무엇인지 짚고 넘어가자</p>
<h1 id="디자인-시스템이란">디자인 시스템이란</h1>
<blockquote>
<p>프로젝트 전반에서 일관성을 유지하기 위해 미리 정의해두는 값과 규칙들의 집합</p>
</blockquote>
<p>즉,</p>
<ul>
<li>색상</li>
<li>폰트</li>
<li>여백</li>
<li>컴포넌트 스타일
이러한 요소들을 매번 새로 정의하는 것이 아닌, <strong>한 곳에 모아두고 재사용</strong>하는 것을 말한다.</li>
</ul>
<hr>
<h1 id="css로-디자인-시스템-관리하기">.css로 디자인 시스템 관리하기</h1>
<p>가장 일반적인 방법으로 <strong>CSS 변수</strong>를 사용해서 관리하는 것이다.</p>
<pre><code class="language-css">:root {
  --color-primary: #99C08E;
  --color-gray-01: #f5f5f5;
  --font-size-base: 16px;
}</code></pre>
<p>사용 시</p>
<pre><code class="language-css">.button {
  background-color: var(--color-primary);
  font-size: var(--font-size-base);
}</code></pre>
<h4 id="특징">특징</h4>
<ul>
<li>스타일을 CSS 레벨에서 정의</li>
<li>전역적으로 사용 가능</li>
<li>브라우저가 직접 처리</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>가장 표준적인 방식</li>
<li>스타일과 자연스럽게 연결됨</li>
<li>재사용성이 높음</li>
<li>협업에 유리 (디자이너와 공유하기 쉬움)</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>동적인 처리에 한계가 있음</li>
<li>조건에 따라 값을 바꾸기 어려움</li>
</ul>
<hr>
<h1 id="js로-디자인-시스템-관리하기">.js로 디자인 시스템 관리하기</h1>
<p>디자인 값을 JavaScript 객체로 관리하는 방법도 있다.</p>
<pre><code class="language-js">export const colors = {
  primary: &quot;#99C08E&quot;,
  gray01: &quot;#f5f5f5&quot;,
};

export const fontSize = {
  base: &quot;16px&quot;,
};</code></pre>
<p>사용 시</p>
<pre><code class="language-js">&lt;div style={{ color: colors.primary, fontSize: fontSize.base }} /&gt;</code></pre>
<h4 id="특징-1">특징</h4>
<ul>
<li>디자인 값을 데이터처럼 관리</li>
<li>필요한 곳에서 import 해서 사용</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>조건 처리 가능 (다크모드 등)</li>
<li>JS 로직과 쉽게 결합 가능</li>
<li>자동완성, 타입 지원 가능</li>
<li><em>단점*</em></li>
<li>인라인 스타일이 많아질 수 있음</li>
<li>CSS와 분리되어 어색할 수 있음</li>
<li>스타일 재사용성이 떨어질 수 있음</li>
</ul>
<hr>
<h1 id="📃정리">📃정리</h1>
<table>
<thead>
<tr>
<th>기준</th>
<th><code>.css</code></th>
<th><code>.js</code></th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>스타일 정의</td>
<td>값(데이터) 관리</td>
</tr>
<tr>
<td>사용 방식</td>
<td>class / var</td>
<td>import</td>
</tr>
<tr>
<td>동적 처리</td>
<td>제한적</td>
<td>자유로움</td>
</tr>
<tr>
<td>협업</td>
<td>유리</td>
<td>상대적으로 제한</td>
</tr>
</tbody></table>
<hr>
<h2 id="언제-무엇을-사용해야-할까">언제 무엇을 사용해야 할까</h2>
<h4 id="css가-적합한-경우">.css가 적합한 경우</h4>
<ul>
<li>색상, 폰트, spacing 같은 기본 디자인 토큰</li>
<li>공통 스타일 (버튼, 레이아웃 등)</li>
<li>프로젝트 전체에서 사용하는 값</li>
</ul>
<blockquote>
<p>👉 정적인 디자인 정의</p>
</blockquote>
<h4 id="js가-적합한-경우">.js가 적합한 경우</h4>
<ul>
<li>다크모드 같은 조건에 따라 변하는 값</li>
<li>계산이 필요한 스타일</li>
<li>컴포넌트 내부에서만 사용하는 값</li>
</ul>
<blockquote>
<p>👉 동적인 로직이 필요한 경우</p>
</blockquote>
<hr>
<p>정적인 <strong>디자인</strong> 처리는 <code>.css</code>, 동적인 처리는 <code>.js</code>가 어울린다고 정리할 수 있다.
상황에 따라 다르지만 <code>.js</code>를 컴포넌트처럼 import해서 사용하는 점에 있어 더 쉽고 다양하게 사용할 줄 알았는데 디자인 시스템을 구성하는 관점에서 <code>.css</code>가 프로젝트에 용이할 것 같다는 점이 의외였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React- useMemo, useCallback]]></title>
            <link>https://velog.io/@obebe_00/React-useMemo-useCallback</link>
            <guid>https://velog.io/@obebe_00/React-useMemo-useCallback</guid>
            <pubDate>Sun, 29 Mar 2026 09:45:47 GMT</pubDate>
            <description><![CDATA[<h1 id="usememo--usecallback">useMemo / useCallback</h1>
<p><code>useState</code>, <code>useEffect</code> 등 <code>use-</code>로 시작하는 함수들을 자주 보게 된다.
이들 모두 React에서 제공하는 <strong>Hook</strong>이다.</p>
<p>이러한 <strong>Hook</strong> 중 <code>useMemo</code>와 <code>useCallback</code>에 대해서 알아보고자 한다.</p>
<h2 id="usememo">useMemo</h2>
<p><code>useMemo</code>는 <strong>값을 기억하는 Hook</strong>이다.</p>
<pre><code class="language-jsx">const memoizedValue = useMemo(() =&gt; {
  return 계산값;
}, [의존성]);</code></pre>
<p>특정 값의 계산 결과를 저장해두고 <strong>의존성(dependency)</strong>이 변경될 때만 다시 계산한다.</p>
<blockquote>
<p>불필요한 반복 계산을 막기 위한 Hook</p>
</blockquote>
<pre><code class="language-jsx">const sortedList = useMemo(() =&gt; {
  return [...items].sort((a, b) =&gt; a - b);
}, [items]);</code></pre>
<p>위 코드를 보면 <code>items</code>가 바뀔 때만 정렬을 다시 수행한다. 그렇지 않으면 이전에 계산된 결과(sortedList)를 그대로 재사용한다.</p>
<p>여기서 헷갈리기 쉬운 부분이 있다.
의존성 배열<code>[items]</code>는 값을 저장하는 것이 아닌, 다시 계산할지 판단하는 기준이다.</p>
<p>정리하자면</p>
<ul>
<li><code>useMemo</code>는 계산 결과를 기억하고, 의존성이 변경될 때만 다시 실행된다.</li>
<li>불필요한 계산을 줄이기 위해 사용한다. </li>
</ul>
<hr>
<h3 id="남용하면-생기는-문제">남용하면 생기는 문제</h3>
<pre><code class="language-js">const sum = useMemo(() =&gt; a + b, [a, b]);</code></pre>
<p>이건 그냥</p>
<pre><code class="language-js">const sum = a + b;</code></pre>
<p>이렇게 가벼운 계산에는 그냥 쓰는 것이 좋다 -&gt; 코드의 복잡성만 향상</p>
<ul>
<li><p><strong>성능 저하</strong>
<code>useMemo</code>도 비용이 있다.
의존성 비교 비용, 메모리 사용, Hook 실행 비용이 있기에 무조건 빠르게만 해주는 도구가 아니라는 점을 인지해야한다.</p>
</li>
<li><p><strong>의존성 관리 실수</strong></p>
<pre><code class="language-jsx">const value = useMemo(() =&gt; {
return count * 2;
}, []);</code></pre>
<p><code>count</code>가 바뀌어도 값이 안바뀐다.</p>
</li>
</ul>
<p>정리하자면 <code>useMemo</code>는 </p>
<blockquote>
<p>비싼 계산을 줄이기 위한 선택적 최적화</p>
</blockquote>
<p>이기에</p>
<pre><code>- 계산 비용이 크거나
- 렌더링마다 실행이 부담되거나
- 실제 성능 문제가 발생했을 때</code></pre><p>사용하는 것이 좋다.</p>
<h2 id="usecallback">useCallback</h2>
<p><code>useCallback</code>은 <strong>함수를 기억하는 Hook</strong>이다.</p>
<pre><code class="language-jsx">const memoizedFn = useCallback(() =&gt; {
  실행할 함수
}, [의존성]);</code></pre>
<p>특정 함수를 저장해두고, 의존성(dependency)이 변경될 때만 새로운 함수를 생성한다</p>
<blockquote>
<p>불필요한 함수 재생성을 막기 위한 Hook</p>
</blockquote>
<p>천천히 알아보자.</p>
<p>❌ useCallback 없이</p>
<pre><code class="language-jsx">function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = () =&gt; {
    console.log(&quot;클릭&quot;);
  };

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;+&lt;/button&gt;
      &lt;Child onClick={handleClick} /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>여기서 <code>handleClick</code> 함수는 <strong>랜더링이 될 때마다 새로 생성</strong>된다</p>
<pre><code class="language-md">버튼을 눌러 `count`증가 -&gt; Parent 다시 렌더링 -&gt; `handleClick` 새로 생성</code></pre>
<p>이렇게 되면 React 기본 구조상 부모 컴포넌트가 렌더링되면 자식 컴포넌트도 함께 렌더링된다.</p>
<p>사실 <code>handleClick</code>이 새로 생성되는 것이 직접적인 문제가 되는 것은 아니다.
하지만 <strong>최적화를 했을 때</strong>는 문제가 발생한다.</p>
<p>예를 들어, 자식 컴포넌트를 <code>React.memo</code>로 감싸 불필요한 렌더링을 막으려고 한다고 가정해보자.</p>
<pre><code class="language-jsx">const Child = React.memo(({ onClick }) =&gt; {
  console.log(&quot;Child 렌더링&quot;);
  return &lt;button onClick={onClick}&gt;Click&lt;/button&gt;;
});</code></pre>
<p>이 경우 props인 onClick이 변경되지 않으면 Child는 다시 렌더링되지 않아야 한다.
하지만</p>
<pre><code class="language-jsx">const handleClick = () =&gt; {
  console.log(&quot;클릭&quot;);
};</code></pre>
<p>이 함수는 렌더링이 될 때마다 새로 생성되기 때문에 props가 변경된 것으로 인식된다
결과적으로 Child 컴포넌트의 리렌더링을 막을 수 없는 것이다.
이 문제를 해결하기 위해 <code>useCallback</code>를 사용한다.</p>
<pre><code class="language-jsx">function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() =&gt; {
    console.log(&quot;클릭&quot;);
  }, []);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;+&lt;/button&gt;
      &lt;Child onClick={handleClick} /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>이렇게 하면 렌더링이 다시 발생해도 <code>handleClick</code>함수는 새로 생성되지 않고 유지된다.
Child 컴포넌트의 불필요한 리렌더링을 방지할 수 있는 것이다.</p>
<hr>
<h2 id="남용하면-생기는-문제-1">남용하면 생기는 문제</h2>
<blockquote>
<p><code>useCallback</code>은 기본이 아닌 최적화 도구</p>
</blockquote>
<p>▪️ <strong>코드가 오히려 복잡해진다</strong></p>
<pre><code class="language-jsx">const handleClick = useCallback(() =&gt; {
  console.log(&quot;클릭&quot;);
}, []);</code></pre>
<p>단순한 함수라면 굳이 필요가 없다</p>
<p>▪️ <strong>성능이 더 나빠질 수도 있다</strong></p>
<ul>
<li>의존성 배열을 비교하는 비용 발생</li>
<li>메모이제이션 자체도 비용이 있음
&rarr; 작은 코드에서는 오히려 손해</li>
</ul>
<p>▪️ <strong>의존성 관리 실수</strong></p>
<pre><code class="language-jsx">const handleClick = useCallback(() =&gt; {
  console.log(count);
}, []);</code></pre>
<p><code>count</code>가 바뀌어도 반영되지 않음 (버그)</p>
<hr>
<p><code>useMemo</code>와 <code>useCallback</code>에 대해 정리해보았다. React의 여러 Hook을 알게 될 때마다 어떻게 사용해야 더 좋은 코드가 나오는지 생각해보게 되는 것 같다.</p>
<p>익숙해질 수 있도록 여러 응용을 다뤄보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 컴포넌트, 함수형 vs 클래스 컴포넌트]]></title>
            <link>https://velog.io/@obebe_00/React-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%95%A8%EC%88%98%ED%98%95-vs-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</link>
            <guid>https://velog.io/@obebe_00/React-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%95%A8%EC%88%98%ED%98%95-vs-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</guid>
            <pubDate>Sun, 29 Mar 2026 08:50:09 GMT</pubDate>
            <description><![CDATA[<p>앞서 다뤘던 주제들을 다시 읽어보다 &#39;컴포넌트&#39;에 대해 짚고 넘어가는 것이 좋을 것 같아 내용을 정리하는 와중에 위클리 페이퍼 토픽으로 &#39;컴포넌트와 함수형 컴포넌트, 클래스 컴포넌트의 차이점에 대해 설명&#39;하는 것이 나와서 이 주제를 다뤄보려고 한다.</p>
<hr>
<h1 id="컴포넌트란">컴포넌트란?</h1>
<blockquote>
<p>UI를 구성하는 가장 독립적인 단위</p>
</blockquote>
<p>React에서 화면은 하나의 덩어리가 아니라,
여러 개의 작은 조각들로 나뉘어 만들어진다.</p>
<p>이 작은 조각 하나하나를 <strong>컴포넌트(Component)</strong>라고 한다.</p>
<p>Flutter에서 <code>Widget</code>과 비슷한 역할이라고 볼 수 있다.</p>
<p>다만 React에서 컴포넌트는 단순한 UI 요소뿐만 아니라
상태(state), 이벤트, 로직까지 함께 관리한다는 점에서 더 넓은 개념이다.</p>
<hr>
<h2 id="컴포넌트-구성">컴포넌트 구성</h2>
<p>앞에서 컴포넌트는 단순한 UI 조각이 아니라 <code>상태(state)</code>, <code>event</code>, <code>logic</code>을 함께 관리하는 단위라고 했다.</p>
<p>그렇다면 남은 구성요소는 무엇일까?</p>
<hr>
<h3 id="-props-데이터-전달">• Props (데이터 전달)</h3>
<blockquote>
<p>부모 컴포넌트에서 자식 컴포넌트로 전달되는 값</p>
</blockquote>
<p>앞서 <a href="https://velog.io/@obebe_00/React%EB%A5%BC-%EC%95%8C%EC%95%84%EA%B0%80%EB%B3%B4%EC%9E%90-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%B6%84%EB%A6%AC">컴포넌트 분리</a>를 다루면서 알아보았던 <code>props</code>다.</p>
<h3 id="-state-상태-관리">• State (상태 관리)</h3>
<blockquote>
<p>컴포넌트 내부에서만 관리되는 값</p>
</blockquote>
<p>이것도 <a href="https://velog.io/@obebe_00/React%EB%A5%BC-%EC%9D%B4%ED%95%B4%ED%95%B4%EB%B3%B4%EC%9E%90">State를 알아보자</a>에서 다루어보았다.</p>
<h3 id="-event-이벤트-처리">• Event (이벤트 처리)</h3>
<p>간단하게 말하자면 사용자의 행동(클릭, 입력)등을 처리하는 부분이다.</p>
<h3 id="-logic-비즈니스-로직">• Logic (비즈니스 로직)</h3>
<p>조건문, 반복문, 데이터 처리 등 실제 동작을 담당하는 부분이다.</p>
<h3 id="-ui-렌더링">• UI (렌더링)</h3>
<p>최종적으로 화면에 보여지는 부분이다.</p>
<p>컴포넌트는 이렇게 구성되어있는 <code>하나의 작은 프로그램 단위</code> 라고 볼 수 있다.</p>
<hr>
<h2 id="컴포넌트-작성">컴포넌트 작성</h2>
<p>이제 컴포넌트를 어떻게 만드는지 알아보자</p>
<p>두 가지의 방식이 존재한다</p>
<pre><code class="language-md">- 함수형 컴포넌트 (Functional Component)
- 클래스 컴포넌트 (Class Component)</code></pre>
<h3 id="-함수형-컴포넌트-functional-component">• 함수형 컴포넌트 (Functional Component)</h3>
<pre><code class="language-jsx">function Hello() {
  return &lt;h1&gt;Hello World&lt;/h1&gt;;
}</code></pre>
<p>또는</p>
<pre><code class="language-jsx">const Hello = () =&gt; {
  return &lt;h1&gt;Hello World&lt;/h1&gt;;
};</code></pre>
<p>이런 식으로 함수 형태로 작성되는 컴포넌트이다.
이러한 함수형 컴포넌트 특징으로는</p>
<ul>
<li>코드가 간결하고 직관적</li>
<li><code>useState</code>,<code>useEffect</code>와 같은 Hook을 사용할 수 있다</li>
</ul>
<hr>
<h3 id="-클래스-컴포넌트-class-component">• 클래스 컴포넌트 (Class Component)</h3>
<pre><code class="language-jsx">import React, { Component } from &quot;react&quot;;

class Hello extends Component {
  render() {
    return &lt;h1&gt;Hello World&lt;/h1&gt;;
  }
}</code></pre>
<p>클래스 문법을 사용해 만드는 컴포넌트로
-<code>this</code>를 사용해야하며</p>
<ul>
<li>lifecycle 매서드를 사용하고</li>
<li>상대적으로 코드가 복잡하다.</li>
</ul>
<hr>
<h2 id="함수형-컴포넌트-vs-클래스-컴포넌트">함수형 컴포넌트 vs 클래스 컴포넌트</h2>
<p>작성 방식을 간단하게 알아보았고, 차이점을 정리해보자</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>함수형 컴포넌트</th>
<th>클래스 컴포넌트</th>
</tr>
</thead>
<tbody><tr>
<td>문법</td>
<td>함수</td>
<td>class</td>
</tr>
<tr>
<td>상태 관리</td>
<td>useState</td>
<td>this.state</td>
</tr>
<tr>
<td>사이드 이펙트</td>
<td>useEffect</td>
<td>lifecycle 메서드</td>
</tr>
<tr>
<td>코드 길이</td>
<td>짧고 간결</td>
<td>상대적으로 김</td>
</tr>
<tr>
<td>사용 추세</td>
<td>✅ 현재 표준</td>
<td>❌ 레거시</td>
</tr>
</tbody></table>
<p>클래스 컴포넌트는 아직 친숙하지 않은 느낌이라 이해가 쉽지는 않다.
어떤 때에 어떤 것을 사용하는 것이 좋은지도 알아보자</p>
<hr>
<h2 id="어떤-것을-사용해야-할까">어떤 것을 사용해야 할까?</h2>
<p>쉽게 결론이 나왔다.</p>
<p>과거에는 <code>state</code>와 <code>lifecycle</code>을 사용하기 위해 클래스 컴포넌트를 사용해야 했지만,
<code>Hook</code>이 등장하면서 함수형 컴포넌트에도 동일한 기능을 모두 구현할 수 있게 되었다.</p>
<blockquote>
<p>코드도 더 짧고, 가독성도 좋아졌다.</p>
</blockquote>
<hr>
<p>마지막으로 내용을 정리해보자면,</p>
<pre><code class="language-md">- 컴포넌트는 UI를 구성하는 독립적인 단위이면서 props, state, event, logic, UI로 구성된다
- 함수형 / 클래스 두 가지의 방식이 존재하며 현재는 함수형 컴포넌트 방식이 많이 사용된다</code></pre>
<p>컴포넌트 분리도 공부해보며 React를 다루면서 컴포넌트라는 단어는 많이 들었지만 제대로 이해하고나니 다른 개념들도 확실하게 잡히는 것 같아 좋았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React를 알아가보자 - 컴포넌트 분리]]></title>
            <link>https://velog.io/@obebe_00/React%EB%A5%BC-%EC%95%8C%EC%95%84%EA%B0%80%EB%B3%B4%EC%9E%90-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%B6%84%EB%A6%AC</link>
            <guid>https://velog.io/@obebe_00/React%EB%A5%BC-%EC%95%8C%EC%95%84%EA%B0%80%EB%B3%B4%EC%9E%90-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%B6%84%EB%A6%AC</guid>
            <pubDate>Tue, 24 Mar 2026 12:34:16 GMT</pubDate>
            <description><![CDATA[<h1 id="react에서-컴포넌트-분리">React에서 컴포넌트 분리</h1>
<p>컴포넌트 분리, 이전에 <code>Flutter</code>에서 Widget을 따로 분리해서 사용했던 것처럼 쉬울 줄 알았다.
오만했다고 볼 수 있다. React에서 해보니 영- 감을 못잡았기 때문이다.</p>
<hr>
<h2 id="기준">기준</h2>
<p>그냥 Widget을 똑 떼어내어 사용할 곳에서 import 해오는 방식과는 무엇인가 달랐다. 그 이질감의 원인을 알아내고자 컴포넌트 분리의 기준을 찾아보았다.</p>
<blockquote>
<p>이 UI가 독립적으로 재사용 가능한가?</p>
</blockquote>
<p>이렇게 생각해도 확 와닿지 않았기에 코드로 살펴보겠다.</p>
<pre><code class="language-jsx">&lt;div&gt;
  &lt;h2&gt;할 일 목록&lt;/h2&gt;
  &lt;input /&gt;
  &lt;button&gt;추가&lt;/button&gt;
&lt;/div&gt;</code></pre>
<p>이 코드를</p>
<pre><code class="language-js">function TodoInput() {
  return (
    &lt;div&gt;
      &lt;input /&gt;
      &lt;button&gt;추가&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>이렇게 하면 <code>TodoInput</code>을 불러와 다른 곳에서도 사용할 수 있다.
처음에 느꼈던 것과 비슷해서 반복인 느낌이 들었기에 더 찾아보니 나는 작성법에서 어려움을 느꼈던 것이었다.</p>
<p>고로 <code>분리해서 필요한 곳에서 재사용한다</code>는 것은 맞는 흐름이었고, 분리 과정에서 데이터 전달 방법과 같은 작성이 어려웠던 것이기에 이걸 다시 알아보고자 한다.</p>
<hr>
<h2 id="props">props</h2>
<blockquote>
<p>컴포넌트를 나누는 순간, 데이터의 흐름도 같이 설계해야 한다</p>
</blockquote>
<p>먼저 분리하기 전 코드를 살펴보자.</p>
<pre><code class="language-jsx">function App() {
  const [title, setTitle] = useState(&quot;&quot;);

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    if (title.trim() === &quot;&quot;) return;

    console.log(title);
    setTitle(&quot;&quot;);
  };

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      &lt;input
        value={title}
        onChange={(e) =&gt; setTitle(e.target.value)}
      /&gt;
      &lt;button type=&quot;submit&quot;&gt;추가&lt;/button&gt;
    &lt;/form&gt;
  );
}</code></pre>
<p>여기서 <code>TodoForm</code>으로 입력 부분을 분리해보겠다.</p>
<pre><code class="language-jsx">function TodoForm() {
  const [title, setTitle] = useState(&quot;&quot;);

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    if (title.trim() === &quot;&quot;) return;

    console.log(title);
    setTitle(&quot;&quot;);
  };

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      &lt;input
        value={title}
        onChange={(e) =&gt; setTitle(e.target.value)}
      /&gt;
      &lt;button type=&quot;submit&quot;&gt;추가&lt;/button&gt;
    &lt;/form&gt;
  );
}</code></pre>
<p>이렇게 뚝 떼어내어서 가져오면 문제가 발생한다
할 일을 추가하는 로직은 부모인 <code>App</code>에 있어야하는데 자식인 <code>TodoForm</code>으로 들어가있다.</p>
<p>이렇게되면 상태 관리와 UI 역할이 섞이게 되어 문제가 된다</p>
<p>📌<strong>여러 컴포넌트에서 사용해야 하는 state는 부모에서 관리하는 것이 더 적절하기 때문이다</strong></p>
<hr>
<p>이걸 해결하기 위해 부모에 함수를 만들고</p>
<pre><code class="language-jsx">const handleAdd = (title) =&gt; {
  ...
};</code></pre>
<p>이걸 props안 <code>onAdd</code>로 전달한다.</p>
<pre><code class="language-js">&lt;TodoForm onAdd={handleAdd} /&gt;</code></pre>
<p>그리고 자식에서 이 함수를 받아서 실행한다</p>
<pre><code class="language-js">function TodoForm({ onAdd }) {
  ...
  onAdd(title);
}</code></pre>
<p>이렇게 해서 TodoForm의 코드는</p>
<pre><code class="language-jsx">import { useState } from &#39;react&#39;;

function TodoForm({ onAdd }) {
  const [title, setTitle] = useState(&#39;&#39;);

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    if (title.trim() === &#39;&#39;) return;
    onAdd(title.trim());
    setTitle(&#39;&#39;);
  };

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      &lt;input
        type=&quot;text&quot;
        value={title}
        onChange={(e) =&gt; setTitle(e.target.value)}
        placeholder=&quot;할일 입력...&quot;
      /&gt;
      &lt;button type=&quot;submit&quot;&gt;
        추가
      &lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>이렇게 된다.</p>
<hr>
<p>이제 추출을 해서 사용해보자</p>
<h2 id="export--import">export / import</h2>
<p>만든 <code>TodoForm</code> 마지막에</p>
<pre><code class="language-jsx">export default TodoForm;</code></pre>
<p><code>export default</code>를 넣어 외부에서 사용할 수 있게 만들고</p>
<p><code>App</code>으로 돌아와서 </p>
<pre><code class="language-jsx">import TodoForm from &#39;./components/TodoForm&#39;;</code></pre>
<p>를 통해 <code>import</code> 해준다.</p>
<p>그리고 </p>
<pre><code class="language-jsx">    &lt;form onSubmit={handleSubmit}&gt;
      &lt;input
        value={title}
        onChange={(e) =&gt; setTitle(e.target.value)}
      /&gt;
      &lt;button type=&quot;submit&quot;&gt;추가&lt;/button&gt;
    &lt;/form&gt;</code></pre>
<p>이 부분을</p>
<pre><code class="language-jsx">      {/* props로 함수 전달 */}
      &lt;TodoForm onAdd={handleAdd} /&gt;</code></pre>
<p>이렇게 변경해주면 깔끔하게 사용할 수 있다.</p>
<hr>
<h2 id="props-vs-state">props vs state</h2>
<p>마지막으로 둘의 차이를 비교해보자</p>
<h3 id="📦-props">📦 props</h3>
<blockquote>
<p>외부(부모)에서 전달받는 데이터</p>
</blockquote>
<pre><code class="language-jsx">&lt;TodoForm onAdd={handleAdd} /&gt;
function TodoForm({ onAdd }) {
  return &lt;button onClick={onAdd}&gt;추가&lt;/button&gt;;
}</code></pre>
<pre><code class="language-md">부모 → 자식으로 전달
읽기 전용 (수정 불가)
컴포넌트 입장에서는 “받아서 쓰는 값”</code></pre>
<hr>
<h3 id="⚙️-state">⚙️ state</h3>
<blockquote>
<p>컴포넌트 내부에서 관리하는 데이터</p>
</blockquote>
<pre><code class="language-jsx">const [title, setTitle] = useState(&quot;&quot;);</code></pre>
<pre><code class="language-md">컴포넌트 내부에서 생성
setState로 변경 가능
값이 바뀌면 UI가 다시 렌더링됨</code></pre>
<h3 id="🔥-가장-중요한-차이">🔥 가장 중요한 차이</h3>
<p>👉 누가 데이터를 바꾸는가?</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>props</th>
<th>state</th>
</tr>
</thead>
<tbody><tr>
<td>소유자</td>
<td>부모</td>
<td>자기 자신</td>
</tr>
<tr>
<td>수정 가능</td>
<td>❌ 불가능</td>
<td>✅ 가능</td>
</tr>
<tr>
<td>역할</td>
<td>전달</td>
<td>관리</td>
</tr>
</tbody></table>
<pre><code class="language-jsx">function App() {
  const [todos, setTodos] = useState([]);

  return &lt;TodoForm onAdd={setTodos} /&gt;;
}</code></pre>
<ul>
<li><p>todos → state (App이 관리)</p>
</li>
<li><p>onAdd → props (TodoForm에게 전달)</p>
</li>
<li><p>데이터는 <code>App</code>이 가지고 | 행동은 <code>TodoForm</code>이 실행</p>
</li>
</ul>
<hr>
<p>컴포넌트 분리에서 막혔던 작성법을 다시 공부해보았다. 컴포넌트 분리, 그리고 <code>props</code>가 조금은 이해되었다. 다음 응용에서는 잘해보자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React를 이해해보자 - state]]></title>
            <link>https://velog.io/@obebe_00/React%EB%A5%BC-%EC%9D%B4%ED%95%B4%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@obebe_00/React%EB%A5%BC-%EC%9D%B4%ED%95%B4%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 23 Mar 2026 06:44:12 GMT</pubDate>
            <description><![CDATA[<h1 id="react에서-state는-언제-필요할까">React에서 state는 언제 필요할까?</h1>
<p>React를 처음 배우면서 가장 많이 사용하게 되는 것이 바로 <code>state</code>다.</p>
<p>그런데 사용하다보니 한 가지 의문이 들었다.</p>
<blockquote>
<p>모든 값을 state로 관리해야 할까?</p>
</blockquote>
<hr>
<h2 id="처음에-했던-생각">처음에 했던 생각</h2>
<p>처음에는 이렇게 생각했다.</p>
<ul>
<li>값이 있으면 일단 state로 만들자</li>
<li>바뀔 가능성이 있으면 state
그러다 보니, 그냥 다 state 쓰면 되는 거 아닌가?</li>
</ul>
<p>그래서 이런 코드가 자연스럽게 나왔다.</p>
<pre><code class="language-jsx">const [name, setName] = useState(&quot;Kim&quot;);
const [age, setAge] = useState(20);
const [title, setTitle] = useState(&quot;Hello&quot;);</code></pre>
<p>이해보다는 받아들여서 쓰기 시작하다보니</p>
<blockquote>
<p>“모든 값을 state로 관리해야 하는 것인가?”</p>
</blockquote>
<p>하는 궁금증이 생겼다.</p>
<hr>
<h2 id="💡-state와-일반-변수의-차이">💡 state와 일반 변수의 차이</h2>
<p>찾아보다보니 기준을 하나 잡을 수 있었다.</p>
<p>👉 <strong>“이 값이 바뀔 때 화면(UI)이 다시 그려져야 하는가?”</strong></p>
<hr>
<h2 id="react의-렌더링이란">React의 렌더링이란?</h2>
<p>이것에 대해 이해하기 위해 렌더링을 짚어보고 넘어가겠다.</p>
<p>React는 상태(state)가 변경되면 <strong>컴포넌트를 다시 실행하고 UI를 다시 그린다</strong></p>
<pre><code>state 변경 → 컴포넌트 함수 재실행 → UI 업데이트</code></pre><p>즉, state는 단순한 값이 아니라 <strong>렌더링을 발생시키는 기준이다</strong></p>
<hr>
<h3 id="✅-그냥-변수로-충분한-경우">✅ 그냥 변수로 충분한 경우</h3>
<pre><code class="language-jsx">const title = &quot;Hello&quot;;</code></pre>
<p>👉 특징:</p>
<ul>
<li>값이 바뀌지 않음</li>
<li>UI에 영향 없음</li>
</ul>
<hr>
<h3 id="✅-state가-필요한-경우">✅ state가 필요한 경우</h3>
<pre><code class="language-jsx">const [count, setCount] = useState(0);</code></pre>
<p>👉 특징:</p>
<ul>
<li>값이 변경됨</li>
<li>변경되면 UI가 다시 렌더링되어야 함</li>
</ul>
<hr>
<h2 id="📌-핵심-기준">📌 핵심 기준</h2>
<pre><code class="language-md">👉 렌더링을 발생시키지 않는 값은 state가 아니다</code></pre>
<hr>
<h2 id="⚠️-state를-남용하면-생기는-문제">⚠️ state를 남용하면 생기는 문제</h2>
<h3 id="1️⃣-불필요한-리렌더링">1️⃣ 불필요한 리렌더링</h3>
<p>state가 바뀌면 컴포넌트가 다시 렌더링된다.
👉 필요 없는 값까지 state로 만들면 성능에 영향을 줄 수 있다.</p>
<hr>
<h3 id="2️⃣-코드-복잡도-증가">2️⃣ 코드 복잡도 증가</h3>
<ul>
<li>state 많아짐</li>
<li>관리 어려움</li>
<li>읽기 힘든 코드</li>
</ul>
<hr>
<h3 id="3️⃣-의도-파악이-어려워짐">3️⃣ 의도 파악이 어려워짐</h3>
<p>👉 어떤 값이 중요한 상태인지 헷갈림</p>
<hr>
<h2 id="🔥-다시-정리해보면">🔥 다시 정리해보면</h2>
<p>처음에는 이렇게 생각했다.</p>
<blockquote>
<p>“state는 그냥 값 저장하는 도구다”</p>
</blockquote>
<p>하지만 지금은 이렇게 이해하고 있다.</p>
<pre><code class="language-md">👉 state는 “UI를 바꾸기 위한 상태”다</code></pre>
<hr>
<h2 id="🧠-정리">🧠 정리</h2>
<p>React는 단순히 값을 저장하는 방식이 아니라</p>
<p>👉 <strong>“상태 변화에 따라 UI를 다시 그리는 구조”</strong>였다</p>
<p>그래서 중요한 건</p>
<ul>
<li>값을 저장하는 것보다</li>
<li>어떤 값을 state로 관리할지 판단하는 것</li>
</ul>
<p>이었다.</p>
<pre><code class="language-md">모든 값이 state일 필요는 없다  
렌더링이 필요한 값만 state로 관리하자</code></pre>
<hr>
<h2 id="실습-적용">실습 적용</h2>
<pre><code class="language-md">이전에 TodoList를 구현해보면서 state가 언제 필요한지 더 명확하게 이해할 수 있었다.</code></pre>
<ul>
<li><strong>할 일 추가</strong><pre><code class="language-jsx">const [todos, setTodos] = useState([]);
</code></pre>
</li>
</ul>
<p>function handleAddTodo(title) {
  const newTodo = {
    id: Date.now(),
    title,
    completed: false,
  };</p>
<p>  setTodos([...todos, newTodo]);
}</p>
<pre><code></code></pre><p>할 일을 추가하면 todos 상태가 변경되고 UI도 함께 업데이트된다.
이 경우 state가 반드시 필요하다</p>
<pre><code>* **완료 처리**
```jsx
function handleToggleTodo(id) {
  const updated = todos.map((todo) =&gt;
    todo.id === id
      ? { ...todo, completed: !todo.completed }
      : todo
  );

  setTodos(updated);
}</code></pre><pre><code>completed 값이 바뀌면서 UI에서 할일/완료 목록이 이동한다.
state 변화 → UI 변화가 연결되는 구조였다

👉 결국 중요한 것은 state를 사용하는 것이 아니라 언제 사용해야 하는지를 아는 것이었다</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[React TodoList - Lv.1 (기초)]]></title>
            <link>https://velog.io/@obebe_00/React-TodoList-Lv.1-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@obebe_00/React-TodoList-Lv.1-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Thu, 19 Mar 2026 00:18:16 GMT</pubDate>
            <description><![CDATA[<p>React 과정에 들어가면서 todolist 만들기 실습이 있었다.
lv.1 ~ lv.3까지 있었는데 우선 lv.1을 정리해보고자 한다.</p>
<h1 id="핵심-개념-정리">핵심 개념 정리</h1>
<p><strong>🔹 useState</strong></p>
<p>React에서 데이터를 관리하기 위해 사용한다.</p>
<pre><code>const [title, setTitle] = useState(&quot;&quot;);
const [todos, setTodos] = useState([]);</code></pre><ul>
<li>title → input 값 저장</li>
<li>todos → 할 일 목록 저장</li>
</ul>
<hr>
<p><strong>🔹 React의 데이터 흐름</strong></p>
<p>TodoList를 만들면서 가장 중요했던 개념은 다음 흐름이었다.</p>
<pre><code>입력 → state 변경 → 화면 자동 업데이트</code></pre><p>React는 <strong>state</strong>가 바뀌면 UI가 자동으로 다시 그려진다.</p>
<hr>
<h1 id="todo-데이터-구조">Todo 데이터 구조</h1>
<p>각 todo는 다음과 같은 형태로 구성했다.</p>
<pre><code>{
  id: Date.now(),
  title: title,
  completed: false,
  createdAt: new Date().toLocaleString()
}</code></pre><ul>
<li><p><code>id</code> → 고유값</p>
</li>
<li><p><code>title</code> → 할 일 내용</p>
</li>
<li><p><code>completed</code> → 완료 여부</p>
</li>
<li><p><code>createdAt</code> → 생성 시간</p>
</li>
</ul>
<hr>
<h1 id="기능-구현-과정">기능 구현 과정</h1>
<h3 id="할-일-추가-create">할 일 추가 (Create)</h3>
<p><strong>💡 핵심</strong></p>
<ul>
<li><p>input 값 → state로 관리</p>
</li>
<li><p>버튼 클릭 → 배열에 추가</p>
<pre><code>function handleAddTodo() {
if (title === &quot;&quot;) return;

const newTodo = {
  id: Date.now(),
  title: title,
  completed: false,
  createdAt: new Date().toLocaleString(),
};

setTodos([...todos, newTodo]);
setTitle(&quot;&quot;);
}</code></pre></li>
</ul>
<p><strong>❗ 중요 포인트</strong></p>
<blockquote>
</blockquote>
<p>기존 배열을 직접 수정하지 않고
새로운 배열을 만들어야 한다</p>
<p>👉 그래서 spread 사용</p>
<hr>
<h3 id="목록-출력-read">목록 출력 (Read)</h3>
<p>배열을 화면에 출력할 때는 <code>map</code>을 사용한다.</p>
<pre><code>&lt;ul&gt;
  {todos.map((t) =&gt; (
    &lt;li key={t.id}&gt;
      &lt;div&gt;{t.title}&lt;/div&gt;
      &lt;div&gt;{t.createdAt}&lt;/div&gt;
    &lt;/li&gt;
  ))}
&lt;/ul&gt;</code></pre><p><strong>❗ 중요 포인트</strong></p>
<ul>
<li><p>map은 배열을 순회하면서 UI를 만든다</p>
</li>
<li><p>key는 반드시 필요</p>
</li>
</ul>
<hr>
<h3 id="할일-삭제-delete">할일 삭제 (Delete)</h3>
<p>삭제는 <code>filter</code>를 사용한다.</p>
<pre><code>function handleDeleteTodo(id) {
  setTodos(todos.filter((t) =&gt; t.id !== id));
}</code></pre><p><strong>💡 핵심</strong></p>
<blockquote>
</blockquote>
<p>삭제 = 특정 값을 제외하고 남긴다</p>
<hr>
<h3 id="완료-처리-update">완료 처리 (Update)</h3>
<p>완료 상태 변경은 map을 사용한다.</p>
<pre><code>function handleDoneTodo(id) {
  const newTodos = todos.map((t) =&gt; {
    if (t.id === id) {
      return {
        ...t,
        completed: !t.completed,
      };
    }
    return t;
  });

  setTodos(newTodos);
}</code></pre><p><strong>💡 핵심</strong></p>
<blockquote>
</blockquote>
<p>수정 = map
삭제 = filter</p>
<hr>
<h3 id="ui-처리">UI 처리</h3>
<p><strong>🔹 완료 상태 스타일</strong></p>
<pre><code>&lt;li className={t.completed ? &quot;completed&quot; : &quot;&quot;}&gt;</code></pre><hr>
<h1 id="회고">회고</h1>
<p>아직 제대로 TodoList를 마스터하지 못한 것 같다. <code>map</code>과 <code>filter</code>를 사용함에 있어서, 그리고 구조대로 코드를 작성함에 있어서 미숙해서 복습 겸 정리를 진행했다.</p>
]]></description>
        </item>
    </channel>
</rss>