<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hogu__giriboy.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 02 Jul 2026 03:15:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hogu__giriboy.log</title>
            <url>https://velog.velcdn.com/images/hogu__giriboy/profile/60156fb4-7b41-4db4-a57d-a5a1c350c754/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hogu__giriboy.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hogu__giriboy" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[프로젝트] 내가 좋아하는 사진을 거래한다면? '최애의 포토']]></title>
            <link>https://velog.io/@hogu__giriboy/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%82%B4%EA%B0%80-%EC%A2%8B%EC%95%84%ED%95%98%EB%8A%94-%EC%82%AC%EC%A7%84%EC%9D%84-%EA%B1%B0%EB%9E%98%ED%95%9C%EB%8B%A4%EB%A9%B4-%EC%B5%9C%EC%95%A0%EC%9D%98-%ED%8F%AC%ED%86%A0</link>
            <guid>https://velog.io/@hogu__giriboy/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%82%B4%EA%B0%80-%EC%A2%8B%EC%95%84%ED%95%98%EB%8A%94-%EC%82%AC%EC%A7%84%EC%9D%84-%EA%B1%B0%EB%9E%98%ED%95%9C%EB%8B%A4%EB%A9%B4-%EC%B5%9C%EC%95%A0%EC%9D%98-%ED%8F%AC%ED%86%A0</guid>
            <pubDate>Thu, 02 Jul 2026 03:15:54 GMT</pubDate>
            <description><![CDATA[<p>프로젝트가 끝나면 해야 할 일은 생각보다 많다.</p>
<p>회고를 작성하고, 문서를 정리하고, 부족했던 부분은 다시 개선하고, 그 과정들을 포트폴리오에 녹여내는 것까지 모두 프로젝트의 일부라고 생각한다.</p>
<p>하지만 이를 다 해내기에 가장 크게 다가올 문제는 바로 시간이다.</p>
<p>프로젝트가 끝나고 어영부영하다 보면, 지난 프로젝트를 제대로 마무리할 타이밍을 놓치기 마련이다.</p>
<p>더 늦기 전에 이번 프로젝트를 다시 돌아보며, 내가 무엇을 경험했고 무엇을 배웠는지 차근차근 기록해 보려고 한다.</p>
<hr>
<h2 id="1-프로젝트-소개">1. 프로젝트 소개</h2>
<h3 id="최애의-포토">최애의 포토</h3>
<p><strong>최애의 포토</strong>는 사용자가 자신이 보유한 포토카드를 판매하거나 다른 사용자와 교환할 수 있는 포토카드 거래 플랫폼이다.</p>
<p>단순한 상품 거래 서비스가 아니라 판매, 구매, 교환, 알림 등 거래 전반의 흐름을 구현하는 것을 목표로 개발했다.</p>
<p>백엔드에서는 거래 과정에서 발생할 수 있는 다양한 예외 상황과 데이터 정합성을 고려하며 REST API를 설계하고 구현했다.</p>
<h3 id="프로젝트-개요">프로젝트 개요</h3>
<ul>
<li>개발 기간 : 2026. 06. 01. ~ 2026. 06. 24.</li>
<li>개발 인원 : Backend 2명 / Frontend 3명</li>
<li>담당 : Backend</li>
</ul>
<h3 id="기술-스택">기술 스택</h3>
<h4 id="backend">Backend</h4>
<p>Node.js · Express · Prisma · PostgreSQL · JWT · Zod · Swagger · SSE · bcrypt · node-cron</p>
<h4 id="frontend">Frontend</h4>
<p>React · Next.js · JavaScript · TanStack Query · Tailwind CSS · Storybook</p>
<h4 id="collaboration">Collaboration</h4>
<p>Git · GitHub · Discord · Notion</p>
<h3 id="담당한-기능">담당한 기능</h3>
<h4 id="판매">판매</h4>
<ul>
<li>판매글 등록</li>
<li>판매글 수정</li>
<li>판매 중단</li>
</ul>
<h4 id="조회">조회</h4>
<ul>
<li>마켓 판매글 목록 조회</li>
<li>판매글 상세 조회</li>
<li>보유 포토카드 조회</li>
<li>나의 판매 포토카드 조회</li>
<li>필터별 집계 API</li>
</ul>
<h4 id="거래">거래</h4>
<ul>
<li>포토카드 구매</li>
<li>교환 제안</li>
<li>교환 수락 / 거절 / 취소</li>
<li>판매글의 교환 제안 목록 조회</li>
</ul>
<h2 id="2-프로젝트를-돌아보며">2. 프로젝트를 돌아보며</h2>
<p>이번 프로젝트에서는 단순히 기능을 구현하는 것보다 <strong>&#39;거래&#39;라는 도메인을 안정적으로 처리하는 것</strong>에 가장 많은 시간을 투자했다.</p>
<p>특히 구매와 교환 기능은 여러 데이터가 함께 변경되는 작업이기 때문에 데이터 정합성과 트랜잭션 처리에 대해 많이 고민했다.</p>
<p>프로젝트 후반에는 성능 개선과 리팩토링도 함께 진행하며 단순히 기능을 만드는 것을 넘어 유지보수성과 성능까지 고려해 보려 노력했다.</p>
<p>이 과정에서 느꼈던 점을 4L 방식으로 정리해 보려고 한다.</p>
<h3 id="liked">Liked</h3>
<p>이번 프로젝트에서 가장 재미있었던 부분은 <strong>여러 데이터가 하나의 흐름으로 정확하게 맞물려 동작하도록 만드는 과정</strong>이었다.</p>
<p>이전까지는 하나의 API를 구현하고 정상적으로 동작하는지만 확인하는 경우가 많았다. 하지만 이번 프로젝트에서는 하나의 거래를 완성하기 위해 여러 데이터가 유기적으로 연결되어야 했고, 그 과정 자체가 굉장히 흥미롭게 느껴졌다.</p>
<p>판매와 구매, 교환 기능은 단순히 하나의 테이블만 수정하는 작업이 아니었다. 판매글의 수량이 변경되고, 포토카드의 소유자가 바뀌고, 거래 내역이 저장되고, 포인트가 이동하고, 경우에 따라 알림까지 생성된다.</p>
<p>이 과정에서 어느 하나라도 빠지거나 순서가 잘못되면 데이터 정합성이 깨질 수 있기 때문에 각 단계가 유기적으로 연결되어야 했다.</p>
<p>처음에는 구현하는 것 자체에 집중했지만, 기능을 하나씩 완성해 갈수록 <strong>여러 로직들이 톱니바퀴처럼 맞물려 하나의 거래가 완성되는 과정</strong>이 굉장히 흥미롭게 느껴졌다.</p>
<p>특히 하나의 요청이 들어왔을 때 여러 테이블의 데이터가 순서대로 변경되고, 모든 작업이 정상적으로 끝났을 때만 하나의 거래가 완성되는 모습을 보면서 단순히 기능을 만드는 것 이상의 재미를 느낄 수 있었다.</p>
<p>이전까지는 API 하나를 구현하는 것 자체에 집중했다면, 이번 프로젝트에서는 여러 API와 데이터가 하나의 흐름으로 연결되어 서비스가 동작한다는 점을 처음으로 체감했다.</p>
<p>각각의 로직이 서로 영향을 주고받으며 하나의 기능을 완성해 가는 과정은 <strong>퍼즐을 하나씩 맞춰 나가는 것보다, 여러 톱니바퀴가 정확하게 맞물려 하나의 기능을 완성하는 과정에 더 가까웠다.</strong></p>
<h3 id="learned">Learned</h3>
<p>이번 프로젝트를 진행하면서 가장 크게 바뀐 점은 <strong>개발할 때 생각하는 범위가 넓어졌다는 것</strong>이다.</p>
<p>이전에는 요구사항에 맞게 기능이 정상적으로 동작하면 구현이 끝났다고 생각했다. 하지만 프로젝트를 진행하며 다양한 버그를 경험하고, 멘토링과 QA를 거치면서 그런 생각이 바뀌었다.</p>
<p>특히 구매 기능에서 레이스 컨디션을 알게 되면서 <strong>&quot;정상적으로 동작하는 코드&quot;와 &quot;실제 서비스에서도 안전하게 동작하는 코드&quot;는 다르다</strong>는 것을 깨달았다.</p>
<p>여기서 레이스 컨디션에 관한 내용은 바로 <a href="https://velog.io/@hogu__giriboy/%EB%82%A8%EC%9D%80-%EC%88%98%EB%9F%89-1%EA%B0%9C-%EB%8F%99%EC%8B%9C%EC%97%90-%EA%B5%AC%EB%A7%A4%ED%95%98%EA%B8%B0">이전 글</a>에서 자세히 다뤄놨다.</p>
<p>혼자 테스트했을 때는 문제가 없었던 기능도 여러 사용자가 동시에 같은 요청을 보낸다면 전혀 다른 결과가 나올 수 있다는 점을 처음으로 경험했다.
기능이 요구사항을 만족하는 것만으로는 충분하지 않았고, 실제 서비스 환경에서 발생할 수 있는 다양한 상황까지 고려해야 한다는 것을 배울 수 있었다.</p>
<p>이후부터는 기능을 구현할 때 자연스럽게 스스로 질문을 던지게 되었다.</p>
<ul>
<li>동시에 여러 사용자가 요청하면 어떻게 될까?</li>
<li>중간에 하나의 작업만 실패하면 어떻게 될까?</li>
<li>데이터 정합성은 유지될까?</li>
<li>이 상황 말고도 다른 예외 상황은 없을까?</li>
</ul>
<p>물론 모든 상황을 예측하는 것은 현실적으로 어렵다. 하지만 이번 프로젝트를 통해 <strong>&quot;이 기능은 정상적으로 동작하나?&quot;</strong>보다 <strong>&quot;이 기능에서 어떤 문제가 발생할 수 있을까?&quot;</strong>를 먼저 고민하는 것이 버그를 줄이는 첫걸음이라는 것을 배울 수 있었다.</p>
<h3 id="lacked">Lacked</h3>
<p>이번 프로젝트에서 가장 아쉬웠던 점은 <strong>성능 개선을 끝까지 해보지 못했다는 것</strong>이다.</p>
<p>프로젝트 후반에는 목록 조회 API의 성능을 개선하기 위해 공통 집계 로직을 분리하고, 필요한 데이터만 조회하도록 수정하는 등 여러 개선을 진행했다. 실제로 응답 속도도 이전보다 눈에 띄게 빨라졌다.</p>
<p>하지만 만족스러운 수준은 아니었다.</p>
<p>대용량 데이터를 기준으로 테스트했을 때 여전히 다른 팀보다 느린 구간이 있었고, 원인을 분석해 보니 애플리케이션에서 집계를 수행하는 방식 자체에 한계가 있었다.</p>
<p>DB의 Group By를 활용한 집계나 쿼리 자체를 더 최적화하는 방향까지 시도해 보고 싶었지만, 프로젝트 일정상 실제 적용까지 이어지지는 못했다.</p>
<p>이번 경험을 통해 성능 개선은 단순히 코드를 정리하는 것만으로 해결되는 문제가 아니라, 데이터베이스와 쿼리 구조까지 함께 고민해야 한다는 점을 느낄 수 있었다.</p>
<p>또한 프로젝트 후반에는 기능 구현과 QA가 동시에 진행되면서 새로운 기능을 구현하는 것과 기존 코드를 개선하는 것 사이에서 우선순위를 정해야 하는 상황이 많았다.</p>
<p>프로젝트를 진행하면서 &quot;이렇게 구현하면 더 좋을 것 같은데...&quot;라는 생각이 드는 순간들이 계속 있었다.
실제로 성능 개선이나 리팩토링 과정에서도 여러 아이디어가 떠올랐지만, 프로젝트 일정상 우선적으로 기능을 완성해야 했기 때문에 모든 아이디어를 코드로 옮기지는 못했다.</p>
<p>아쉬움은 남았지만, 동시에 개발에서는 항상 <strong>&#39;지금 가장 중요한 것이 무엇인지&#39;</strong>를 판단하는 것도 중요한 역량이라는 점을 배울 수 있었던 경험이었다.</p>
<h3 id="longed-for">Longed for</h3>
<p>이번 프로젝트를 통해 기능 구현부터 리팩토링, 성능 개선까지 다양한 경험을 할 수 있었지만, 아직 해보고 싶은 것들이 많이 남아 있다.</p>
<p>가장 먼저 도전해 보고 싶은 것은 <strong>DB 레벨에서의 성능 최적화</strong>이다.</p>
<p>이번 프로젝트에서는 애플리케이션 레벨에서 중복 로직을 제거하고 조회 방식을 개선하며 응답 속도를 줄이는 데 집중했다. 하지만 실제로 성능 병목을 분석하면서 애플리케이션만 개선하는 데에는 분명한 한계가 있다는 것도 느낄 수 있었다.</p>
<p>다음 프로젝트에서는 <strong>Group By를 활용한 집계, 인덱스 설계, 실행 계획 분석(EXPLAIN), 쿼리 튜닝</strong>까지 경험하며 데이터베이스 관점에서도 성능을 개선해 보고 싶다.</p>
<p>또한 이번 프로젝트에서는 QA와 멘토링을 통해 예상하지 못했던 예외 상황들을 많이 발견했다. 앞으로는 기능 구현이 끝났다고 바로 다음 기능으로 넘어가기보다, <strong>&quot;이 기능에서 어떤 문제가 발생할 수 있을까?&quot;</strong>를 먼저 고민하고 다양한 상황을 검증하는 개발 습관을 만들어 가고 싶다.</p>
<p>무엇보다 이번 프로젝트를 통해 가장 크게 느낀 점은 <strong>좋은 기능은 단순히 동작하는 기능이 아니라, 여러 상황에서도 안정적으로 동작하는 기능</strong>이라는 것이다.</p>
<p>앞으로도 데이터 정합성과 안정성을 우선으로 고민하며, 더 견고한 서비스를 만들 수 있는 백엔드 개발자로 성장해 나가고 싶다.</p>
<h2 id="4-프로젝트를-마치며">4. 프로젝트를 마치며</h2>
<p>이번 프로젝트는 지금까지 진행했던 프로젝트 중 가장 많은 고민을 했던 프로젝트였다.</p>
<p>단순히 기능을 구현하는 것에서 끝나지 않고, 데이터 정합성은 유지되는지, 여러 사용자가 동시에 요청하면 어떤 문제가 발생하는지, 더 좋은 구조는 없는지 계속 고민하며 개발을 진행했다.</p>
<p>물론 아직 아쉬운 부분도 많다. 성능 개선도 더 해보고 싶었고, 데이터베이스 레벨의 최적화나 테스트 코드 작성도 충분히 경험하지 못했다.</p>
<p>하지만 그보다 더 큰 수확은 <strong>앞으로 무엇을 더 시도해볼 수 있는지 알게 되었다는 점</strong>이다.</p>
<p>이번 프로젝트를 통해 가장 크게 느낀 것은, 하나의 서비스는 하나의 기능으로 완성되는 것이 아니라 <strong>여러 로직과 데이터가 톱니바퀴처럼 맞물릴 때 비로소 완성된다</strong>는 점이었다.</p>
<p>이번 회고를 작성하면서 프로젝트를 다시 돌아보니, 기능을 구현했던 순간보다 고민했던 과정들이 더 많이 기억에 남았다.</p>
<p><strong>다음 프로젝트에서는 이번 경험을 바탕으로 더 많은 톱니바퀴를 안정적으로 맞물리게 만드는 백엔드 개발자가 되고 싶다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[남은 수량 1개, 동시에 구매하기]]></title>
            <link>https://velog.io/@hogu__giriboy/%EB%82%A8%EC%9D%80-%EC%88%98%EB%9F%89-1%EA%B0%9C-%EB%8F%99%EC%8B%9C%EC%97%90-%EA%B5%AC%EB%A7%A4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hogu__giriboy/%EB%82%A8%EC%9D%80-%EC%88%98%EB%9F%89-1%EA%B0%9C-%EB%8F%99%EC%8B%9C%EC%97%90-%EA%B5%AC%EB%A7%A4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 30 Jun 2026 02:06:27 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 상품 구매 기능을 구현하던 중이었다.</p>
<p>구매 로직 자체는 크게 복잡하지 않았다.</p>
<ol>
<li>판매글을 조회한다.</li>
<li>남은 수량을 확인한다.</li>
<li>수량을 차감한다.</li>
<li>구매를 완료한다.</li>
</ol>
<p>혼자 테스트해 봤을 때는 모두 정상적으로 동작했다.</p>
<p>하지만 구현을 마치고 코드를 다시 보던 중 문득 한 가지 의문이 들었다.</p>
<blockquote>
<p>&quot;만약 두 명이 동시에 상품을 구매하면 어떻게 될까?&quot;</p>
</blockquote>
<p>두 요청이 거의 동시에 서버에 도착한다면, 두 요청 모두 남은 수량을 확인한 뒤 구매를 진행해 버릴 수도 있지 않을까?</p>
<p>물론 남은 수량이 여러 개인 상품일 경우 동시에 구매가 되어도 아무런 문제가 없을 것이다.</p>
<p>하지만 남은 수량이 <strong>1개</strong>인 상황이라면 어떨까?</p>
<p>두 요청 모두 남은 수량 확인을 통과한 뒤, 각각 수량 차감까지 진행된다면?</p>
<p>결국 실제 재고는 1개였지만, 두 사용자 모두 구매에 성공하는 상황이 발생할 수도 있지 않을까?</p>
<p>게임에서 흔히 말하는 <strong>&#39;템 복사 버그&#39;</strong>와 비슷한 상황이 되는 것은 아닐까?</p>
<p>그 순간부터 이 기능은 단순히 &quot;구매를 구현하는 문제&quot;가 아니라, 동시에 여러 요청이 들어오는 상황에서는 어떻게 동작하는가에 대한 문제로 보이기 시작했다.</p>
<hr>
<h2 id="1-두-명이-동시에-구매를-시도한다면">1. 두 명이 동시에 구매를 시도한다면?</h2>
<p>그렇다면 실제로 두 명의 사용자가 거의 동시에 같은 상품을 구매한다고 가정해 보자.</p>
<p>판매글의 남은 수량이 <strong>1개</strong>인 상황에서, 두 사용자가 동시에 구매 요청을 보낸다.</p>
<p>위에서 잠깐 언급했듯이 기존 구매 로직은 다음과 같은 순서로 동작했다.</p>
<pre><code>1. 판매글 조회
2. 남은 수량 검증
3. 수량 차감
4. 구매 처리</code></pre><p>언뜻 보면 아무 문제가 없어 보인다.
재고를 먼저 확인한 뒤 차감하기 때문에, 재고가 부족하면 구매가 실패할 것이라고 생각하기 쉽다.</p>
<p>하지만 두 요청이 거의 동시에 실행된다면 이야기가 달라진다.</p>
<pre><code>사용자 A                  사용자 B
   |                       |
판매글 조회                   |
(재고 1개)                판매글 조회
   |                    (재고 1개)
   |                       |
재고 확인 통과             재고 확인 통과
   |                       |
수량 차감                  수량 차감</code></pre><p>두 요청 모두 차감하기 전에 이미 재고를 확인했기 때문에, 둘 다 구매를 진행할 가능성이 생긴다.</p>
<p>언뜻 보면 이상한 상황처럼 보였지만, 그 원인을 바로 설명할 수는 없었다.</p>
<p>조금 더 찾아보니 이러한 문제는 이미 이름이 있는 개념이었다.</p>
<hr>
<h2 id="2-이미-모두가-고민했던-문제">2. 이미 모두가 고민했던 문제</h2>
<p>처음에는 단순히 타이밍이 겹치면 생길 수 있는 문제 정도로 생각했다.</p>
<p>하지만 조금 더 찾아보니, 이러한 문제는 이미 이름이 있는 개념이었다.</p>
<p>바로 <strong>레이스 컨디션(Race Condition)</strong> 이다.</p>
<blockquote>
<p>레이스 컨디션은 여러 요청이나 프로세스가 동시에 같은 데이터를 수정하려고 할 때, 실행 순서에 따라 결과가 달라질 수 있는 상황을 의미한다.</p>
</blockquote>
<p>쉽게 말해, 누가 먼저 데이터를 수정하느냐를 두고 여러 요청이 경쟁하는 것이다.</p>
<p>내가 구현한 구매 기능 역시 같은 데이터를 대상으로 여러 요청이 동시에 들어올 수 있기 때문에, 레이스 컨디션이 발생할 가능성이 있는 구조였다.</p>
<p>그제야 처음 품었던 의문,</p>
<blockquote>
<p><strong>&quot;두 명이 동시에 구매하면 어떻게 될까?&quot;</strong></p>
</blockquote>
<p>라는 질문이 단순한 궁금증이 아니라, 실제 서비스에서 반드시 고려해야 하는 문제라는 것을 알게 되었다.</p>
<hr>
<h2 id="3-검증과-차감을-하나의-작업으로">3. 검증과 차감을 하나의 작업으로</h2>
<p>기존 방식의 문제는 검증과 차감이 서로 분리되어 있다는 점이었다.</p>
<p>그래서 검증과 차감을 하나의 작업으로 수행하도록 구매 로직을 변경했다.</p>
<pre><code>판매글 조회

↓

남은 수량 검증

↓

수량 차감</code></pre><p>이 구조에서는 검증을 통과한 이후에도 다른 요청이 먼저 재고를 차감할 수 있는 여지가 존재했다.</p>
<p>그래서 구매 로직을 <strong>조건부 차감(Conditional Update)</strong> 방식으로 변경했다.</p>
<p>실제 구현은 다음과 같다.</p>
<pre><code class="language-js">// 판매글 잔여 수량 차감
export const decreaseSaleRemainingQuantityRepository = async (
  { saleId, quantity },
  tx = prisma,
) =&gt; {
  return tx.sale.updateMany({
    where: {
      id: saleId,
      status: &quot;SALE&quot;,
      remainingQuantity: {
        gte: quantity,
      },
    },
    data: {
      remainingQuantity: {
        decrement: quantity,
      },
    },
  });
};</code></pre>
<p>여기서 핵심은 <code>remainingQuantity: { gte: quantity }</code> 조건이다.</p>
<p>남은 수량이 구매하려는 수량 이상일 때만 <code>decrement</code>가 실행되므로, 검증과 차감이 하나의 UPDATE 쿼리 안에서 함께 이루어진다.</p>
<p>이제는 별도로 재고를 조회해 검증할 필요가 없어졌다.</p>
<p><strong>차감의 성공 여부만 확인하면 재고가 충분했는지까지 함께 판단할 수 있게 되었기 때문이다.</strong></p>
<p>이후에는 영향을 받은 행(Row)의 개수를 확인하여,</p>
<ul>
<li><strong>1행이 수정되었다면</strong> 구매를 계속 진행하고,</li>
<li><strong>0행이 수정되었다면</strong> 이미 다른 요청이 먼저 구매한 것으로 판단하여 재고 부족 예외를 반환하도록 변경했다.</li>
</ul>
<p>이렇게 검증과 차감을 하나의 UPDATE 쿼리로 처리하면서, 조회와 차감 사이에 다른 요청이 개입할 수 있는 틈을 없앨 수 있었고, 기존 구조에서 발생할 수 있었던 레이스 컨디션을 방지할 수 있었다.</p>
<hr>
<p>이번 경험을 통해 기능이 정상적으로 동작하는 것과, <strong>실제 서비스 환경에서도 안전하게 동작하는 것</strong>은 전혀 다른 문제라는 것을 배웠다.</p>
<p>혼자 테스트할 때는 한 번도 문제가 발생하지 않았기 때문에 구매 기능이 완성되었다고 생각했다.</p>
<p>하지만 <strong>&quot;동시에 두 명이 구매하면 어떻게 될까?&quot;</strong>라는 질문 하나를 던져보면서, 처음으로 동시성이라는 관점을 갖게 되었고 레이스 컨디션이라는 개념도 알게 되었다.</p>
<p>특히 이번 경험에서 가장 크게 느낀 점은 <strong>기능 구현이 끝났다고 해서 고민도 끝나는 것은 아니라는 것</strong>이었다.</p>
<p>앞으로는 기능이 정상적으로 동작하는지뿐만 아니라,</p>
<blockquote>
<p><strong>&quot;여러 사용자가 동시에 같은 요청을 보내면 어떤 일이 발생할까?&quot;</strong></p>
</blockquote>
<p>와 같은 질문을 스스로 던지며 다양한 상황을 고려해 구현하는 습관을 가져야겠다고 생각했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로컬 브랜치보다 앞서 있는 원격 브랜치]]></title>
            <link>https://velog.io/@hogu__giriboy/%EB%A1%9C%EC%BB%AC-%EB%B8%8C%EB%9E%9C%EC%B9%98%EB%B3%B4%EB%8B%A4-%EC%95%9E%EC%84%9C-%EC%9E%88%EB%8A%94-%EC%9B%90%EA%B2%A9-%EB%B8%8C%EB%9E%9C%EC%B9%98</link>
            <guid>https://velog.io/@hogu__giriboy/%EB%A1%9C%EC%BB%AC-%EB%B8%8C%EB%9E%9C%EC%B9%98%EB%B3%B4%EB%8B%A4-%EC%95%9E%EC%84%9C-%EC%9E%88%EB%8A%94-%EC%9B%90%EA%B2%A9-%EB%B8%8C%EB%9E%9C%EC%B9%98</guid>
            <pubDate>Thu, 11 Jun 2026 00:20:03 GMT</pubDate>
            <description><![CDATA[<p>어쩌면 가장 기본적인,
스프린트 과정 중에서도 초반에 다뤘던 브랜치 머지 방식 문제다.</p>
<p>프로젝트 중 한 번 두 번 뜸하게 발생하길래 항상 해결됐으니까 일단 넘어갔었던 문제였는데,
이번에 프로젝트를 진행하면서 빈도가 5번 pull 받으면 3번은 문제가 발생했다.</p>
<hr>
<h2 id="1-git-pull-후-push-거절">1. git pull 후 push 거절</h2>
<p>문제 발생 상황을 따라가보자면,</p>
<p>API를 구현하던 중, 팀원의 <code>merge</code>를 받고 <code>dev</code> 브랜치의 최신 변경사항을 반영할 필요가 있었다.
평소처럼 작업 중이던 브랜치에서 <code>dev</code>의 최신 변경 내용을 가져오기 위해 다음 명령어를 실행했다.</p>
<pre><code class="language-bash">git pull origin dev</code></pre>
<p>별다른 충돌 없이 pull이 완료되었고, 이후 작업 내용을 원격 브랜치에 올리기 위해 push를 진행했다.</p>
<pre><code class="language-bash">git push origin feat/~</code></pre>
<p>그런데 예상치 못한 에러가 발생했다.</p>
<pre><code class="language-bash">! [rejected]        feat/~ -&gt; feat/~ (non-fast-forward)
error: failed to push some refs to ...</code></pre>
<p>에러 메시지를 읽어보면 Git은 현재 로컬 브랜치가 원격 브랜치보다 뒤처져 있다고 판단하고 있었다.</p>
<pre><code class="language-bash">hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use &#39;git pull&#39; before pushing again.
hint: See the &#39;Note about fast-forwards&#39; in &#39;git push --help&#39; for details.</code></pre>
<p>처음에는 이해가 되지 않았다.</p>
<p>방금 <code>git pull</code>까지 완료했고 이 브랜치는 본인만 쓰던 중이었는데,
왜 원격 브랜치보다 뒤쳐져 있다는 것일까?</p>
<p>심지어 작업 과정에서 다른 팀원의 feature 브랜치를 가져온 적도 없었기 때문에 더 의문이었다.
처음에는 단순히 Git이 꼬인 것이라고 생각했지만, 원인을 추적해 보니 Git이 브랜치 히스토리를 보호하기 위해 의도적으로 push를 막고 있었다.</p>
<hr>
<h2 id="2-문제-발생-원인">2. 문제 발생 원인</h2>
<p><code>non-fast-forward</code> 에러를 처음 마주했을 때는 원인을 바로 알 수 없었다.</p>
<p>사실 원인을 찾기 보단 API 작업을 계속 진행해야 했기 때문에,
브랜치를 정상적으로 push 하는 것이 먼저였다.</p>
<p>그렇게 해결 방법을 찾아보던 중 <code>--force-with-lease</code> 옵션을 사용하는 방법을 발견했다.</p>
<p><code>--force-with-lease</code>는 현재 원격 브랜치 상태가 내가 알고 있는 상태와 동일한 경우에만 강제로 push를 허용하는 옵션으로,
일반적인 강제 push인 <code>--force</code>와 달리, 다른 사람이 원격 브랜치에 새로운 커밋을 추가한 경우에는 push를 막아주기 때문에 조금 더 안전한 방식으로 알려져 있다.</p>
<p>그렇게 임시방편으로 본질적인 문제 해결보단, 우선 지금 상태를 해결하고 넘어갔었다.</p>
<p>그 후 수차례 더 발생하자, 더 이상 임시방편으로 문제 해결보단 본질적인 문제 해결의 필요성을 느껴 문제 원인을 파악하기 시작했다.</p>
<h3 id="가설-1-push-직전에-최신화-된-내용-pull-반영-x">가설 1. push 직전에 최신화 된 내용 pull 반영 X</h3>
<p><code>non-fast-forward</code> 에러를 검색하면 가장 흔하게 등장하는 원인 중 하나가
원격 브랜치의 최신 변경사항을 가져오지 않은 상태에서 push를 시도하는 경우다.</p>
<p>처음에는 다들 원인을 그렇게 말하고, 나 역시 그게 이 문제의 원흉으로 의심하고 있었다.</p>
<p>그렇게 이 상황을 의심하기 시작하면서 추후 push 직전에 문제를 신경쓰면서 pull을 당기기 시작했다.</p>
<p>하지만 그러던 와중에도 같은 문제가 발생해버렸고,
이 가설은 문제 원인이 아닌 것으로 판단하고 다른 이유를 찾아봤다.</p>
<h3 id="가설-2-커밋-수정을-위한-git-reset---soft-head1">가설 2. 커밋 수정을 위한 <code>git reset --soft HEAD~1</code></h3>
<p>API를 구현하던 과정에서 커밋하고 push를 앞둔 상황에서 추가로 수정한 사항이 생각난 적이 몇 번 있었는데,
직전 커밋과 같은 내용의 수정이라 커밋을 수정하려고 되돌리기 위해 다음 명령어를 몇 번 사용했었다.</p>
<pre><code class="language-bash">git reset --soft HEAD~1</code></pre>
<p><code>--soft</code> 옵션은 마지막 커밋만 취소하고, 변경된 파일 내용과 스테이징 상태는 그대로 남겨두는 방식이다.
즉, 코드 변경사항은 사라지지 않고 다시 커밋할 수 있는 상태로 돌아간다.</p>
<pre><code class="language-text">커밋 전 상태

A - B - C

git reset --soft HEAD~1 이후

A - B

변경사항 C는 스테이징 영역에 남아 있음</code></pre>
<p>당시에는 단순히 커밋을 다시 정리하는 작업이라고 생각했지만,</p>
<p>Git 입장에서 커밋은 고유한 해시 값을 갖고 있기 때문에,
기존 커밋을 최소한 뒤 다시 커밋하면 겉으로는 같은 작업처럼 보여도 Git 입장에서는 전혀 다른 커밋이 된다.</p>
<pre><code>기존 커밋

A - B - C

다시 커밋한 후

A - B - C&#39;</code></pre><p>그래서 원격 브랜치에는 기존 커밋 <code>C</code>가 있고, 로컬 브랜치에는 새로 만들어진 커밋 <code>C&#39;</code>가 있는 상태가 된 것은 아닐까 의심했다.</p>
<p>이 경우 로컬과 원격의 히스토리가 달라지기 때문에 일반 push가 거절될 수 있으려나 싶어서
<code>git reset --soft HEAD~1</code>로 커밋을 되돌리고 다시 커밋했던 과정이 이번 문제와 관련 있을 수 있다고 판단했다.</p>
<p>하지만 돌이켜 생각해보면 이 과정만으로 원격 브랜치에 문제가 생기지는 않는다.</p>
<p><code>git reset --soft HEAD~1</code>은 로컬의 마지막 커밋만 되돌리는 작업이고,
아직 push하지 않은 커밋이라면 원격 브랜치는 그 커밋의 존재 자체를 알 수 없다.</p>
<p>즉, 원격에 이미 올라간 커밋을 되돌린 것이 아니라면 원격 브랜치와 충돌할 이유가 없다.</p>
<p>무엇보다 이후 같은 커밋 수정 과정을 거치지 않았을 때도 동일한 문제가 발생했기 때문에, 이 가설은 원인에서 제외했다.</p>
<h3 id="가설-3-브랜치-생성-오류">가설 3. 브랜치 생성 오류</h3>
<p>보통 새로운 작업 브랜치는 최신 <code>dev</code> 브랜치에서 생성해야 한다.</p>
<p>브랜치를 <code>dev</code> 브랜치가 아닌 기존 작업 중이던 브랜치에서 파생해도 이와 같은 문제가 발생할 수 있다고 한다.</p>
<pre><code class="language-bash">git switch dev
git pull origin dev
git switch feat/~</code></pre>
<p>하지만 <code>dev</code>가 아니라 이미 작업 중이던 다른 feature 브랜치에서 새 브랜치를 만들었다면?</p>
<pre><code class="language-bash">git switch feat/before
git switch -c feat/after</code></pre>
<p>이 경우 새 브랜치에는 <code>dev</code>에 없는 다른 작업 브랜치의 커밋들이 함께 포함될 수 있다.</p>
<pre><code class="language-text">dev
A - B

feat/before
A - B - C - D

feat/after
A - B - C - D</code></pre>
<p>이렇게 되면 내가 의도하지 않은 커밋까지 포함된 상태에서 작업을 시작하게 된다.</p>
<p>이런 경우도 있다고 하니 첫 브랜치 생성부터 문제였는지 잠깐 의심했지만,
<code>git push</code> 전에 바로 <code>git pull</code>로 최신화했는지는 기억 못해도
로컬 <code>dev</code>로 돌아가 <code>↓M</code> 표시를 확인하고 최신화 시켜 브랜치 분기했던 기억은 있었다.</p>
<p>여기서 <code>↓M</code>는 원격에는 있지만 로컬에는 아직 없는 커밋이 있다는 의미로, <code>git pull</code> 명령 시 해제된다.</p>
<p>돌아가서 브랜치 생성 기준이 잘못되어 발생한 문제는 아니라고 판단했다.</p>
<hr>
<h2 id="3-터미널-로그에서의-단서">3. 터미널 로그에서의 단서</h2>
<p>모든 추측이 다 엇나가고, 터미널 로그를 다시 살펴보다가 한 가지 이상한 점을 발견했다.</p>
<pre><code class="language-bash">remote: Enumerating objects: 16, done.
remote: Counting objects: 100% (16/16), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 10 (delta 6), reused 9 (delta 6), pack-reused 0 (from 0)
Unpacking objects: 100% (10/10), 2.05 KiB | 233.00 KiB/s, done.
From https://github.com/project/project
 * branch            dev        -&gt; FETCH_HEAD
   943eb26..802ec5f  dev        -&gt; origin/dev
Created autostash: 739377e
Applied autostash.
Successfully rebased and updated refs/heads/feat/~.</code></pre>
<p>바로 <code>git pull</code> 하고 나온 로그인데 처음에는 단순히 <code>git pull</code>이 성공했다는 뜻으로 그냥 지나쳤지만,
다시 제대로 보니 <code>git pull origin dev</code>가 <strong>merge 방식이 아닌 rebase 방식으로 동작했다</strong>는 의미였다.</p>
<p>정말 당연한 얘기지만 보통 <code>git pull</code>은 다음과 같이 동작한다.</p>
<pre><code>git pull
=
git fetch
+
git merge</code></pre><p>하지만 내 환경에서는 달랐다.</p>
<pre><code class="language-bash">git config --get pull.rebase</code></pre>
<p><code>git pull</code>의 rebase 설정값 확인 결과 <code>true</code>가 나왔다.</p>
<p>즉, 내 Git은 <code>git pull</code> 실행 시 기본적으로 merge가 아닌 rebase를 수행하도록 설정되어 있었다.</p>
<p>실제로 내가 실행한 명령어도 <code>git pull origin dev</code> 였지만 Git 내부에서는 다음과 같이 동작한 것이다.</p>
<pre><code class="language-bash">git fetch origin dev
git rebase origin/dev</code></pre>
<p>rebase 같은 경우엔 스프린트 초반에 GitHub 협업에 관해 배우면서 잠깐 다뤘었던 내용인데 잠깐 상황을 비유해서 보자면,</p>
<pre><code class="language-text">A - B - C</code></pre>
<p>위와 같은 상황이 있고, <code>C</code>가 내가 작업한 커밋이라고 가정해보자.</p>
<p>그런데 원격 <code>dev</code> 브랜치에 새로운 커밋 <code>D</code>가 추가된 상태에서 rebase가 수행되면
Git은 기존 커밋 <code>C</code>를 그대로 이동시키지 않고,
<code>A - B - D - C&#39;</code>처럼 새로운 기준 위에서 다시 만들어 적용한다.</p>
<p>겉으로 보기에는 같은 작업 내용이지만 Git 입장에서는 <code>C ≠ C&#39;</code>가 된다.</p>
<p>즉, 기존 커밋과 새로운 커밋은 서로 다른 커밋으로 취급된다.</p>
<hr>
<h2 id="4-사건의-원흉-rebase">4. 사건의 원흉: rebase</h2>
<p>이제 왜 문제가 발생했는지 이해할 수 있었다.</p>
<p>원격 브랜치는 기존 커밋 히스토리를 가지고 있었고,
내 로컬 브랜치는 rebase를 거치면서 새롭게 작성된 커밋 히스토리를 가지고 있었다.</p>
<p>결국 Git은 두 브랜치의 히스토리가 서로 다르다고 판단했고,</p>
<pre><code class="language-bash">git push origin feat/~</code></pre>
<p>실행 시</p>
<pre><code class="language-bash">! [rejected]        feat/~ -&gt; feat/~ (non-fast-forward)
error: failed to push some refs to ...</code></pre>
<p>에러를 발생시켜 push를 거절한 것이다.</p>
<p>그제셔야 <code>--force-with-lease</code>로 push가 가능했던 이유도 이해할 수 있었다.</p>
<p>일반 push는 히스토리 변경을 허용하지 않지만, 강제 pus는 새로운 히스토리로 원격 브랜치를 덮어쓸 수 있기 때문이다.</p>
<p>원인을 파악한 뒤 가장 먼저 확인한 건 팀원들의 Git 설정이었고 내 환경에서는 <code>pull.rebase</code>가 <code>true</code>로 설정되어 있었지만,
당장 함께하는 백엔드 팀원의 설정을 확인해보니 <code>false</code>로 되어 있었다.</p>
<p>이전 프로젝트에선 이런 문제가 발생하지 않았기 때문에
후에 이 설정이 현재 레포지토리에만 적용된 설정인지, 아니면 내 개발 환경 전체에 적용된 설정인지 확인할 필요가 있었고,
확인 결과 <code>pull.rebase=true</code>는 특정 프로젝트 설정이 아니라 내 맥북 전체에 적용된 전역 설정이었다.</p>
<p>그래서 같은 백엔드 레포지토리에서 같은 명령어를 실행하더라도 Git이 다르게 동작하고 있었고,
나만 <code>non-fast-forward</code> 문제를 겪을 수 있던 것이었다.</p>
<p>이전 프로젝트에선 아무래도 레포지토리 설정에서 막아두고 작업을 하지 않았을까 싶다.</p>
<p>그래서 보통의 <code>git pull</code> 방식에 맞춰 다음 명령어로 설정을 변경했다.</p>
<pre><code class="language-bash">git config --global pull.rebase false</code></pre>
<p>이후에는 <code>git pull</code>이 merge 방식으로 동작하게 되었고, 동일한 문제를 재현하지 않을 수 있었다.</p>
<hr>
<p>추후에 팀노션에 해당하는 트러블슈팅을 문서화하려다 보니,
프론트팀에서 맥을 쓴다고 하셨던 거 같은 분이 같은 문제를 겪고 있었다.</p>
<p>해당 문제에 대해 나누다 보니 다른 맥을 쓰시는 분들도 이러한 문제를 겪고 계셨다.</p>
<p>아이러니하게도 이와 같은 문제를 겪고 있던 팀원들이 모두 맥북 사용자였다.</p>
<p>그래서 이 부분에 대해 macOS의 초기 Git 설정이 <code>pull.rebase=true</code>인가 의구심이 들었지만,
macOS의 기본 Git 설정이 <code>pull.rebase=true</code>라고 단정할 수는 없다고 하고,
그것보단 맥북 초기 개발 환경 세팅 과정에서 공통으로 적용한 Git 설정일 가능성이 높다고 한다.</p>
<p>확인 가능한 사실은 그저 내 개발 환경의 전역 Git 설정에 <code>pull.rebase=true</code>가 적용되어 있었다는 점이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ZOD: 넌 못 지나간다 — 안전한 데이터 검증하기]]></title>
            <link>https://velog.io/@hogu__giriboy/ZOD-%EB%84%8C-%EB%AA%BB-%EC%A7%80%EB%82%98%EA%B0%84%EB%8B%A4-%EC%95%88%EC%A0%84%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hogu__giriboy/ZOD-%EB%84%8C-%EB%AA%BB-%EC%A7%80%EB%82%98%EA%B0%84%EB%8B%A4-%EC%95%88%EC%A0%84%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 04 Jun 2026 09:07:06 GMT</pubDate>
            <description><![CDATA[<p>개발을 하다 보면 사용자가 예상한 방식으로만 서비스를 이용할 것이라고 생각하기 쉽지만,
실제로는 그렇지 않다.</p>
<p>이에 관련한 경험으로, 초급 프로젝트를 진행할 당시,
상호 팀 간 테스트를 진행한 적이 있었다.</p>
<p>우리 팀은 개발 과정에서 다양한 예외 상황을 직접 테스트해 보기로 계획하고 작업을 진행했기 때문에,
글자수 제한이라던지 모든 예외 상황을 팀 내부에서 먼저 진행해보고,
다른 팀의 프로젝트를 테스트할 때도 같은 방식으로 접근했다.</p>
<p>당시 테스트했던 프로젝트는 기업 투자와 관련된 서비스였다.</p>
<p>나는 입력창마다 허용 범위를 벗어난 값을 넣어보거나,
비정상적으로 긴 값을 입력하는 등 여러 예외 상황을 시도해 보았다.</p>
<p>그 과정에서 투자 금액 입력에 대한 검증이 충분하지 않았고,
결과적으로 서비스가 의도하지 않은 매우 큰 금액을 투자할 수 있는 상황이 발생했다.</p>
<p>물론 실제 서비스에서는 더 많은 방어 로직이 필요하겠지만,
이 경험을 통해 사용자의 입력을 그대로 신뢰하면 안 된다는 사실을 다시 한번 느낄 수 있었다.</p>
<p>사용자는 언제든 예상하지 못한 값을 입력할 수 있고,
때로는 의도적으로 잘못된 데이터를 보내기도 한다.</p>
<p>따라서 서버는 데이터를 사용하기 전에 반드시 검증 과정을 거쳐야 한다.</p>
<p>이러한 과정을 <strong>유효성 검사(Validation)</strong> 라고 하며,
이번 글에서는 JavaScript와 TypeScript 환경에서 많이 사용되는 데이터 검증 라이브러리인
<strong>Zod</strong>를 통해 유효성 검사를 보다 쉽고 체계적으로 관리하는 방법을 알아보려고 한다.</p>
<hr>
<h2 id="1-사용자의-입력을-믿지-않기">1. 사용자의 입력을 믿지 않기</h2>
<p>앞서 이야기했듯이 사용자는 항상 개발자가 의도한 방식대로 입력하지 않는다.
회원가입을 예로 들어보자.
개발자는 다음과 같은 데이터를 기대한다.</p>
<pre><code class="language-json">{
  &quot;email&quot;: &quot;user@example.com&quot;,
  &quot;password&quot;: &quot;12345678&quot;
}</code></pre>
<p>하지만 실제로는 전혀 다른 형태의 데이터가 들어올 수 있다.</p>
<pre><code class="language-json">{
  &quot;email&quot;: 1234,
  &quot;password&quot;: null
}</code></pre>
<p>혹은 필수 값 자체가 누락될 수도 있다.</p>
<pre><code class="language-json">{}</code></pre>
<p>개발자가 의도한 형태의 데이터만 들어온다는 보장은 어디에도 없다.</p>
<h3 id="조건문으로-검증하기">조건문으로 검증하기</h3>
<p>유효성 검사는 반드시 Zod가 있어야만 가능한 것은 아니다.</p>
<p>프론트엔드에서도 유효성 검사를 진행할 수 있으며,
JavaScript만으로도 충분히 구현할 수 있다.</p>
<p>하지만 프론트엔드 검증은 사용자의 편의를 위한 검증일 뿐이다.
사용자는 브라우저 화면을 거치지 않고도 서버에 직접 요청을 보낼 수 있다.</p>
<p>대표적으로 Postman과 같은 API 테스트 도구를 사용하면 프론트엔드의 검증 로직을 모두 우회할 수 있다.</p>
<p>유효성 검사는 JavaScript만으로도 당연히 구현 가능하다.</p>
<pre><code class="language-javaScript">if (!email) {
  throw new Error(&quot;이메일은 필수입니다.&quot;);
}

if (password.length &lt; 8) {
  throw new Error(&quot;비밀번호는 8자 이상이어야 합니다.&quot;);
}</code></pre>
<p>처음에는 간단해 보이지만,
검증해야 할 필드가 많아질수록 조건문도 함께 늘어난다.</p>
<ul>
<li>이메일 검증</li>
<li>비밀번호 검증</li>
<li>닉네임 검증</li>
<li>전화번호 검증</li>
<li>권한 검증</li>
</ul>
<p>검증 규칙이 늘어날수록 코드가 복잡해지고 유지보수도 어려워진다.</p>
<p>그래서 검증 규칙을 한 곳에 모아 체계적으로 관리할 수 있는 도구가 필요하게 되었고,
그중 하나가 바로 오늘 소개할 <strong>Zod</strong>다.</p>
<h2 id="2-zod로-검증-규칙-모아두기">2. Zod로 검증 규칙 모아두기</h2>
<p>앞서 살펴본 것처럼 유효성 검사는 JavaScript만으로도 충분히 구현할 수 있다.
하지만 서비스가 커질수록 검증해야 할 데이터도 함께 늘어난다.</p>
<p>회원가입 기능만 보더라도 이메일, 비밀번호, 닉네임 등 검증해야 할 값이 여러 개 존재한다.
여기에 로그인, 프로필 수정, 게시글 작성 기능까지 추가된다면 검증 로직은 더욱 많아질 수밖에 없다.</p>
<p>이러한 검증 로직이 여러 곳에 흩어져 있으면 코드의 가독성이 떨어지고 유지보수도 어려워진다.
Zod는 이런 문제를 해결하기 위해 <strong>검증 규칙을 하나의 스키마(Schema)로 관리할 수 있도록 도와준다.</strong></p>
<h3 id="스키마-알아보기">스키마 알아보기</h3>
<p>스키마는 쉽게 말해 &quot;데이터의 설계도&quot; 라고 생각하면 된다.</p>
<p>회원가입 요청을 예로 들면 우리는 다음과 같은 데이터를 기대하고 있다.</p>
<pre><code class="language-json">{
  &quot;email&quot;: &quot;user@example.com&quot;,
  &quot;password&quot;: &quot;12345678&quot;
}</code></pre>
<p>그렇다면 Zod에서는 이 데이터의 구조를 미리 정의할 수 있다.</p>
<pre><code class="language-javascript">const signupSchema = z.object({
  email: z.string(),
  password: z.string(),
});</code></pre>
<p>위 코드를 해석해보면 다음과 같다.</p>
<ul>
<li>email은 문자열이어야 한다.</li>
<li>password는 문자열이어야 한다.</li>
</ul>
<p>즉, 회원가입 요청이 들어왔을 때 반드시 이 구조를 따라야 한다는 규칙을 정의한 것이다.</p>
<h3 id="객체-검증하기">객체 검증하기</h3>
<p>Zod에서 가장 많이 사용하는 기능은 <code>object()</code>다.</p>
<pre><code class="language-javascript">const userSchema = z.object({
  email: z.string(),
  password: z.string(),
});</code></pre>
<p>객체 안에 어떤 속성이 존재해야 하는지 정의할 수 있다.
만약 객체 구조가 다르다면 검증에 실패하게 된다.</p>
<pre><code class="language-javascript">{
  email: 1234,
  password: null
}</code></pre>
<p>위 데이터는 email과 password가 문자열이 아니기 때문에 검증에 통과하지 못한다.</p>
<h3 id="문자열-숫자-배열-검증하기">문자열, 숫자, 배열 검증하기</h3>
<p>객체 안에서는 다양한 타입을 검증할 수 있다.</p>
<p>문자열 검증</p>
<pre><code class="language-javascript">z.string();</code></pre>
<p>숫자 검증</p>
<pre><code class="language-javascript">z.number();</code></pre>
<p>배열 검증</p>
<pre><code class="language-javascript">z.array(z.string());</code></pre>
<p>예를 들어 사용자의 관심 태그 목록을 검증한다고 가정해보자.</p>
<pre><code class="language-javascript">const tagSchema = z.object({
  tags: z.array(z.string()),
});</code></pre>
<p>그러면 다음과 같은 데이터만 허용된다.</p>
<pre><code class="language-javascript">{
  tags: [&quot;JavaScript&quot;, &quot;React&quot;, &quot;Node.js&quot;];
}</code></pre>
<p>배열 안에 숫자나 객체가 들어온다면 검증에 실패한다.</p>
<h3 id="조건문-대신-설계도-사용하기">조건문 대신 설계도 사용하기</h3>
<p>JavaScript만 사용할 경우 검증 로직은 보통 이런 형태가 된다.</p>
<pre><code class="language-javascript">if (typeof email !== &quot;string&quot;) {
  throw new Error(&quot;이메일은 문자열이어야 합니다.&quot;);
}

if (typeof password !== &quot;string&quot;) {
  throw new Error(&quot;비밀번호는 문자열이어야 합니다.&quot;);
}</code></pre>
<p>필드가 늘어날수록 조건문도 함께 늘어난다.
반면 Zod는 데이터의 구조를 한 번 정의해두면 된다.</p>
<pre><code class="language-javascript">const signupSchema = z.object({
  email: z.string(),
  password: z.string(),
});</code></pre>
<p>즉, 조건문으로 데이터를 검사하는 것이 아니라,
데이터가 어떤 형태여야 하는지를 먼저 정의하는 방식이다.</p>
<p>이것이 Zod의 가장 큰 특징이다.</p>
<h2 id="3-원하는-조건-추가하기">3. 원하는 조건 추가하기</h2>
<p>앞선 예제에서는 데이터의 형태만 검증했다.
하지만 데이터 형태가 옳다고 해서 모두 올바른 값은 아니다.</p>
<p>예를 들어 비밀번호가 한 글자여도 문자열이고,
이메일이 아닌 값도 문자열이다.</p>
<p>실제 서비스에서는 단순히 타입만 확인하는 것이 아니라, 개발자가 정한 조건까지 만족해야 한다.</p>
<p>Zod는 이러한 조건을 메서드 형태로 추가할 수 있다.</p>
<h3 id="최소-길이와-최대-길이-제한하기">최소 길이와 최대 길이 제한하기</h3>
<p>회원가입을 할 때 닉네임이나 비밀번호 길이를 제한하는 경우가 많다.</p>
<pre><code class="language-javascript">z.string().min(3).max(20);</code></pre>
<p>위 코드는 다음 규칙을 의미한다.</p>
<ul>
<li>최소 3글자 이상</li>
<li>최대 20글자 이하</li>
</ul>
<p>예를 들어 닉네임 검증은 다음과 같이 작성할 수 있다.</p>
<pre><code class="language-javascript">const schema = z.object({
  nickname: z.string().min(2).max(10),
});</code></pre>
<p>이렇게 하면 너무 짧거나 긴 닉네임을 방지할 수 있다.</p>
<h3 id="이메일-형식-검증하기">이메일 형식 검증하기</h3>
<p>이메일은 문자열이라고 해서 모두 허용할 수 없다.
<code>&quot;user@example.com&quot;</code>처럼 이메일 형식을 따라야 한다.</p>
<p>Zod는 이를 위한 메서드를 제공한다.</p>
<pre><code class="language-javascript">z.string().email();</code></pre>
<p>예를 들어</p>
<pre><code class="language-javascript">const schema = z.object({
  email: z.string().email(),
});</code></pre>
<p>이라면</p>
<pre><code class="language-javascript">{
  email: &quot;test@test.com&quot;;
}</code></pre>
<p>은 통과하지만,</p>
<pre><code class="language-javascript">{
  email: &quot;hello&quot;;
}</code></pre>
<p>는 검증에 실패한다.</p>
<h3 id="url-형식-검증하기">URL 형식 검증하기</h3>
<p>프로필 이미지나 웹사이트 주소를 입력받는 경우도 있다.
이럴 때는 URL 형식을 검증할 수 있다.</p>
<pre><code class="language-javascript">z.string().url();</code></pre>
<p>예를 들어</p>
<pre><code class="language-javascript">{
  imageUrl: &quot;https://example.com/profile.png&quot;;
}</code></pre>
<p>는 통과하지만
,</p>
<pre><code class="language-javascript">{
  imageUrl: &quot;내 프로필 사진&quot;;
}</code></pre>
<p>은 URL 형식이 아니므로 실패한다.</p>
<h3 id="허용된-값만-받기">허용된 값만 받기</h3>
<p>때로는 정해진 값만 입력받아야 하는 경우가 있다.
예를 들어 등급을 가정해보자.</p>
<pre><code class="language-javascript">COMMON;
RARE;
SUPER_RARE;</code></pre>
<p>이 외의 값은 허용하면 안 된다.</p>
<p>이럴 때 사용하는 것이 <code>enum()</code> 이다.</p>
<pre><code class="language-javascript">z.enum([&quot;COMMON&quot;, &quot;RARE&quot;, &quot;SUPER_RARE&quot;]);</code></pre>
<p>이렇게 하면 지정한 값만 통과할 수 있다.</p>
<pre><code class="language-javascript">{
  grade: &quot;RARE&quot;;
}</code></pre>
<p>이 경우엔 성공,</p>
<pre><code class="language-javascript">{
  grade: &quot;LEGEND&quot;;
}</code></pre>
<p><code>enum()</code>과 벗어나는 이 경우엔 실패다.</p>
<h3 id="조건을-계속-이어붙이기">조건을 계속 이어붙이기</h3>
<p>Zod의 장점 중 하나는 여러 조건을 자연스럽게 연결할 수 있다는 점이다.</p>
<pre><code class="language-javascript">z.string().min(8).max(20);</code></pre>
<p>또는</p>
<pre><code class="language-javascript">z.string().email();</code></pre>
<p>처럼 필요한 검증 규칙을 체인 형태로 추가할 수 있다.
덕분에 코드만 보더라도 어떤 조건을 검사하는지 쉽게 파악할 수 있다.</p>
<h2 id="4-상황에-따라-유연하게-검증하기">4. 상황에 따라 유연하게 검증하기</h2>
<p>지금까지 살펴본 검증은 모든 값이 반드시 존재한다고 가정했다.
하지만 이것 또한 실제 서비스에서는 항상 그렇지 않다.</p>
<p>예를 들어 회원가입 시 닉네임은 선택 입력일 수도 있고,
페이지 번호는 사용자가 보내지 않았을 때 기본값을 사용하고 싶을 수도 있다.
또한 URL의 Query Parameter처럼 숫자여야 하는 값이 문자열로 전달되는 경우도 자주 발생한다.</p>
<p>Zod는 이런 상황을 위해 다양한 기능을 제공한다.</p>
<h3 id="값이-없어도-괜찮다면-optional">값이 없어도 괜찮다면 <code>optional()</code></h3>
<p>회원가입 시 닉네임을 선택 입력으로 받는다고 가정해보자.</p>
<pre><code class="language-javascript">const schema = z.object({
  nickname: z.string().optional(),
});</code></pre>
<p><code>optional()</code>을 사용하면 해당 값이 없어도 검증에 실패하지 않는다.</p>
<p>예를 들어 다음 두 데이터 모두 통과한다.</p>
<pre><code class="language-javascript">{
  nickname: &quot;hogu__giriboy&quot;;
}</code></pre>
<pre><code>{}</code></pre><p>반면 optional()이 없다면 값이 존재하지 않는 순간 검증은 실패하게 된다.</p>
<h3 id="기본값을-설정하는-default">기본값을 설정하는 <code>default()</code></h3>
<p>페이지네이션 기능을 만든다고 가정해보자.
보통 페이지 번호는 사용자가 직접 입력하지 않는 경우가 많다.
이럴 때는 기본값을 설정할 수 있다.</p>
<pre><code class="language-javascript">const schema = z.object({
  page: z.number().default(1),
});</code></pre>
<p>만약 사용자가 값을 보내지 않았다면
Zod가 자동으로</p>
<pre><code class="language-javascript">{
  page: 1;
}</code></pre>
<p>로 처리해준다.</p>
<p>별도의 조건문을 작성하지 않아도 되기 때문에 코드가 훨씬 간결해진다.</p>
<h3 id="문자열을-숫자로-변환하는-coerce">문자열을 숫자로 변환하는 coerce()</h3>
<p>실무에서 생각보다 자주 만나는 문제 중 하나가 타입 변환이다.
예를 들어 URL의 Query Parameter는 모두 문자열로 전달된다.</p>
<pre><code>?page=1</code></pre><p>개발자는 숫자를 기대하지만 실제로 서버가 받는 값은 다음과 같다.</p>
<pre><code class="language-javascript">{
  page: &quot;1&quot;;
}</code></pre>
<p>따라서 아래 검증은 실패한다.</p>
<pre><code class="language-javascript">z.number();</code></pre>
<p>왜냐하면 <code>&quot;1&quot;</code>은 문자열이기 때문이다.
이럴 때 사용하는 것이 coerce()다.</p>
<pre><code class="language-javascript">z.coerce.number();</code></pre>
<p>그러면 <code>&quot;1&quot;</code>을 <code>1</code>로 자동 변환한 뒤 검증을 수행한다.</p>
<p>실제로 Express에서 페이지네이션을 구현할 때 매우 자주 사용되는 기능이다.</p>
<h3 id="여러-기능을-함께-사용할-수도-있다">여러 기능을 함께 사용할 수도 있다</h3>
<p>Zod의 메서드는 조합해서 사용할 수 있다.</p>
<pre><code class="language-javascript">const schema = z.object({
  page: z.coerce.number().default(1),
});</code></pre>
<p>이렇게 작성하면</p>
<ul>
<li>문자열로 들어와도 숫자로 변환</li>
<li>값이 없으면 기본값 1 사용</li>
</ul>
<p>이라는 두 가지 규칙을 동시에 적용할 수 있다.</p>
<p>검증은 단순히 막는 것이 아니다
처음에는 유효성 검사가 잘못된 데이터를 거부하는 기능이라고 생각할 수 있지만 실제로는</p>
<ul>
<li>선택 입력 처리</li>
<li>기본값 설정</li>
<li>타입 변환</li>
</ul>
<p>처럼 데이터를 사용하기 좋은 형태로 정리하는 역할도 수행한다.
이 덕분에 이후 비즈니스 로직에서는 데이터 형태를 걱정하지 않고 기능 구현에 집중할 수 있다.</p>
<h2 id="5-내가-원하는-규칙-만들기">5. 내가 원하는 규칙 만들기</h2>
<p>앞서 살펴본 <code>min()</code>, <code>max()</code>, <code>email()</code> 같은 기능은 대부분의 상황에서 유용하게 사용할 수 있다.</p>
<p>하지만 실제 프로젝트를 진행하다 보면 기본적으로 제공되는 검증만으로는 부족한 경우가 있다.</p>
<p>예를 들어 다음과 같은 조건을 검증해야 할 수도 있다.</p>
<ul>
<li>이메일에 특정 도메인이 포함되어야 한다.</li>
<li>비밀번호에 특수문자가 포함되어야 한다.</li>
<li>닉네임에 금칙어가 포함되면 안 된다.</li>
<li>비밀번호와 비밀번호 확인 값이 일치해야 한다.</li>
</ul>
<p>이처럼 개발자가 직접 규칙을 정의해야 하는 경우 사용할 수 있는 기능이 <code>refine()</code>이다.</p>
<h3 id="refine-사용하기"><code>refine()</code> 사용하기</h3>
<p>가장 간단한 형태는 다음과 같다.</p>
<pre><code class="language-javascript">z.string().refine((value) =&gt; value.includes(&quot;@&quot;), {
  message: &quot;이메일 형식이 아닙니다.&quot;,
});</code></pre>
<p><code>refine()</code>은 두 가지 값을 받는다.</p>
<ol>
<li>검증 함수</li>
<li>검증 실패 시 보여줄 에러 메시지</li>
</ol>
<p>즉,</p>
<pre><code class="language-javascript">value.includes(&quot;@&quot;);</code></pre>
<p>가 <code>true</code>를 반환하면 검증에 성공하고,
<code>false</code>를 반환하면 검증에 실패한다.</p>
<h3 id="비밀번호에-특수문자-포함시키기">비밀번호에 특수문자 포함시키기</h3>
<p>예를 들어 비밀번호에 특수문자가 반드시 포함되어야 한다고 가정해보자.</p>
<pre><code class="language-javascript">const schema = z.object({
  password: z.string().refine((value) =&gt; /[!@#$%^&amp;*]/.test(value), {
    message: &quot;비밀번호에는 특수문자가 포함되어야 합니다.&quot;,
  }),
});</code></pre>
<p>이 경우</p>
<pre><code class="language-javascript">{
  password: &quot;password123!&quot;;
}</code></pre>
<p>는 통과하지만</p>
<pre><code class="language-javascript">{
  password: &quot;password123&quot;;
}</code></pre>
<p>는 검증에 실패한다.</p>
<h3 id="닉네임-금칙어-검사하기">닉네임 금칙어 검사하기</h3>
<p>특정 단어를 닉네임으로 사용할 수 없도록 제한하는 것도 가능하다.</p>
<pre><code class="language-javascript">const forbiddenWords = [&quot;admin&quot;, &quot;manager&quot;];

const schema = z.object({
  nickname: z
    .string()
    .refine((value) =&gt; !forbiddenWords.some((word) =&gt; value.includes(word)), {
      message: &quot;사용할 수 없는 닉네임입니다.&quot;,
    }),
});</code></pre>
<p>예를 들어</p>
<pre><code class="language-javascript">{
  nickname: &quot;admin123&quot;;
}</code></pre>
<p>은 실패하고,</p>
<pre><code class="language-javascript">{
  nickname: &quot;myNickname&quot;;
}</code></pre>
<p>은 통과하게 된다.</p>
<h3 id="기본-검증과-함께-사용하기">기본 검증과 함께 사용하기</h3>
<p><code>refine()</code>은 단독으로 사용하지 않아도 된다.</p>
<p>기존 검증 규칙 뒤에 추가로 연결할 수 있다.</p>
<pre><code class="language-javascript">const schema = z.object({
  password: z
    .string()
    .min(8)
    .refine((value) =&gt; /[!@#$%^&amp;*]/.test(value), {
      message: &quot;비밀번호에는 특수문자가 포함되어야 합니다.&quot;,
    }),
});</code></pre>
<p>이렇게 하면</p>
<ul>
<li>최소 8자 이상인지 확인</li>
<li>특수문자가 포함되어 있는지 확인</li>
</ul>
<p>두 가지 검증을 모두 수행할 수 있다.</p>
<p><code>refine()</code>을 사용하기에 앞서
<code>min()</code>, <code>max()</code>, <code>email()</code>처럼 이미 제공되는 기능이 있다면 그것을 사용하는 것이 가장 좋다.</p>
<p>하지만 &quot;우리 서비스만의 규칙&quot;이 필요한 순간에는
<code>refine()</code>이 필요해진다.</p>
<ul>
<li>기본 검증 → Zod 내장 기능 사용</li>
<li>비즈니스 규칙 검증 → refine() 사용</li>
</ul>
<p>이라고 생각하면 된다.</p>
<h2 id="6-검증-실행하기">6. 검증 실행하기</h2>
<p>지금까지 다양한 검증 규칙을 정의하는 방법을 살펴봤다.
하지만 스키마를 정의했다고 해서 자동으로 검증이 이루어지는 것은 아니다.
실제로 데이터를 검증하려면 정의한 스키마를 실행해야 한다.
Zod에서는 대표적으로 <code>safeParse()</code>와 <code>parse()</code>를 사용한다.</p>
<h3 id="safeparse"><code>safeParse()</code></h3>
<p>가장 많이 사용되는 방식은 <code>safeParse()</code>다.</p>
<pre><code class="language-javascript">const result = schema.safeParse(data);</code></pre>
<p>검증에 성공하면 다음과 같은 객체를 반환한다.</p>
<pre><code class="language-javascript">{
  success: true,
  data: ...
}</code></pre>
<p>반대로 검증에 실패하면</p>
<pre><code class="language-javascript">{
  success: false,
  error: ...
}</code></pre>
<p>를 반환한다.</p>
<p>즉, 검증 결과를 직접 확인할 수 있기 때문에 에러를 안전하게 처리할 수 있다.</p>
<pre><code class="language-javascript">const result = schema.safeParse(req.body);

if (!result.success) {
  return res.status(400).json({
    message: &quot;잘못된 요청입니다.&quot;,
  });
}</code></pre>
<p>실무에서 가장 많이 사용하는 방식이며, API 요청 검증에서도 자주 사용된다.</p>
<h3 id="parse"><code>parse()</code></h3>
<p><code>parse()</code>는 조금 다르게 동작한다.</p>
<pre><code class="language-javascript">const data = schema.parse(req.body);</code></pre>
<p>검증에 성공하면 검증된 데이터를 반환한다.
하지만 검증에 실패하면 에러를 반환하는 것이 아니라 ZodError를 발생시킨다.
<code>ZodError</code>는 어떤 필드가 왜 검증에 실패했는지에 대한 정보를 담고 있는 Zod 전용 에러 객체다.
따라서 parse()를 사용할 경우 에러를 처리할 수 있도록 try...catch 문이 필요하다.</p>
<pre><code class="language-javascript">try {
  const data = schema.parse(req.body);
} catch (error) {
  console.error(error);
}</code></pre>
<p>또는 Express의 전역 에러 핸들러와 함께 사용할 수도 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Presentational과 Container: 역할 분리 등장]]></title>
            <link>https://velog.io/@hogu__giriboy/Presentational%EA%B3%BC-Container-%EC%97%AD%ED%95%A0-%EB%B6%84%EB%A6%AC-%EB%93%B1%EC%9E%A5</link>
            <guid>https://velog.io/@hogu__giriboy/Presentational%EA%B3%BC-Container-%EC%97%AD%ED%95%A0-%EB%B6%84%EB%A6%AC-%EB%93%B1%EC%9E%A5</guid>
            <pubDate>Mon, 18 May 2026 00:41:40 GMT</pubDate>
            <description><![CDATA[<h2 id="1-패턴의-등장">1. 패턴의 등장</h2>
<p>React로 개발을 하다 보면
처음에는 자연스럽게 하나의 컴포넌트 안에 모든 걸 넣게 된다.</p>
<pre><code>function UserPage() {
  const [user, setUser] = useState(null);

  useEffect(() =&gt; {
    fetchUser().then(setUser);
  }, []);

  return (
    &lt;div&gt;
      &lt;h1&gt;{user?.name}&lt;/h1&gt;
    &lt;/div&gt;
  );
}</code></pre><p>처음엔 문제 없어 보이지만,
프로젝트 규모가 점점 커지기 시작하면서 컴포넌트 안에 점점 더 많은 것들이 섞이기 시작한다.</p>
<ul>
<li>상태 관리</li>
<li>API 요청</li>
<li>데이터 가공</li>
<li>이벤트 처리</li>
<li>UI 렌더링</li>
</ul>
<p>결국 하나의 컴포넌트가 너무 많은 역할을 맡게 되는 거다.</p>
<p>그래서 React에서는
“역할을 분리하자”라는 흐름이 등장하게 된다.</p>
<hr>
<h2 id="2-presentational--container-패턴">2. Presentational &amp; Container 패턴</h2>
<p>이 패턴의 핵심은 단순하다.</p>
<p>컴포넌트를</p>
<ul>
<li>화면을 보여주는 역할</li>
<li>데이터를 관리하는 역할</li>
</ul>
<p>로 나누는 거다.</p>
<h3 id="presentational-component">Presentational Component</h3>
<p>Presentational Component는
말 그대로 “보여주는 역할”에 집중한다.</p>
<ul>
<li>UI 렌더링</li>
<li>스타일</li>
<li>마크업</li>
</ul>
<p>같은 화면 표현만 담당한다.</p>
<pre><code class="language-js">function UserCard({ name }) {
  return &lt;h1&gt;{name}&lt;/h1&gt;;
}</code></pre>
<p>이 컴포넌트는 데이터를 직접 가져오지 않는다.
그냥 받은 데이터를 화면에 출력만 한다.</p>
<h3 id="container-component">Container Component</h3>
<p>반대로 Container Component는
데이터와 로직을 담당한다.</p>
<ul>
<li>상태 관리</li>
<li>API 호출</li>
<li>이벤트 처리</li>
</ul>
<p>같은 동작을 처리하고,
그 결과를 Presentational 컴포넌트에 전달한다.</p>
<pre><code class="language-js">function UserContainer() {
  const [name, setName] = useState(&quot;철수&quot;);

  return &lt;UserCard name={name} /&gt;;
}</code></pre>
<p>즉,</p>
<p>Container → 데이터 관리
Presentational → 화면 출력</p>
<p>이렇게 역할을 나눈 구조라고 보면 된다.</p>
<hr>
<h2 id="3-이-구조의-존재이유">3. 이 구조의 존재이유</h2>
<p>가장 큰 이유는 유지보수 때문이다.</p>
<p>UI와 로직이 섞여 있으면
컴포넌트가 점점 복잡해진다.</p>
<p>반대로 역할을 분리하면</p>
<p>UI 재사용이 쉬워지고
로직 수정 영향 범위가 줄고
테스트하기 편해지고
협업 구조도 명확해진다</p>
<p>특히 디자이너나 퍼블리셔와 협업할 때
화면 컴포넌트만 따로 관리하기도 좋아졌다.</p>
<p>하지만 점점 다른 문제가 생기기 시작했다</p>
<p>문제는 Container 컴포넌트가 너무 많아진다는 점이었다.</p>
<pre><code class="language-js">&lt;UserContainer&gt;</code></pre>
<p>같은 구조가 계속 생기고,
컴포넌트를 감싸는 구조도 점점 깊어졌다.</p>
<p>또 데이터를 전달하기 위해 props를 계속 넘기다 보니
props drilling 문제도 자주 발생했다.</p>
<p>결국 역할 분리를 위해 만든 패턴이
오히려 구조를 복잡하게 만들기 시작한 거다.</p>
<hr>
<h2 id="4-hooks가-등장하면서-흐름이-바뀌었다">4. Hooks가 등장하면서 흐름이 바뀌었다</h2>
<p>React Hooks가 등장한 이후부터는
Container 역할을 컴포넌트가 아니라 Hook으로 분리하기 시작했다.</p>
<p>예전에는:</p>
<pre><code class="language-js">Container → Presentational</code></pre>
<p>구조였다면,</p>
<p>지금은:</p>
<pre><code class="language-js">Hook → Component</code></pre>
<p>형태로 많이 바뀌었다.</p>
<p>예를 들면:</p>
<pre><code class="language-js">function useUser() {
  const [user, setUser] = useState(null);

  return { user };
}
function UserCard() {
  const { user } = useUser();

  return &lt;div&gt;{user.name}&lt;/div&gt;;
}</code></pre>
<p>이런 식이다.</p>
<hr>
<h2 id="5-지금은-">5. 지금은 ?</h2>
<p>완전히 사라진 건 아니다.</p>
<p>오히려 지금도 React에서는
“로직과 UI를 분리한다”는 철학 자체는 여전히 중요하다.</p>
<p>다만 그 역할을 나누는 방식이
Container 컴포넌트 중심 구조에서 Custom Hook 중심 구조로
조금 바뀌었다고 보는 게 더 정확하다.</p>
<p>즉, Presentational &amp; Container 패턴은
지금 React 구조 설계가 어떤 방향으로 발전했는지를 이해하기 좋은 흐름이라고 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인증의 진화, 그리고 정착]]></title>
            <link>https://velog.io/@hogu__giriboy/%EC%9D%B8%EC%A6%9D%EC%9D%98-%EC%A7%84%ED%99%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%A0%95%EC%B0%A9</link>
            <guid>https://velog.io/@hogu__giriboy/%EC%9D%B8%EC%A6%9D%EC%9D%98-%EC%A7%84%ED%99%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%A0%95%EC%B0%A9</guid>
            <pubDate>Thu, 14 May 2026 05:51:52 GMT</pubDate>
            <description><![CDATA[<p>로그인 기능은 겉으로 보면 단순해 보인다.</p>
<p>입력값을 받고, 기존 데이터와 동일할 경우
로그인 성공 처리를 해주면 끝나는 것처럼 보인다.</p>
<p>그런데 여기서 더 깊게 생각해보면 미처 생각하지 못한 부분이 생긴다.</p>
<blockquote>
<p>&quot;로그인 성공 이후에는 어떻게 로그인 상태를 유지시키지?&quot;</p>
</blockquote>
<p>로그인 요청을 계속 보내는 건 불친절한 기능이다.
한 번의 요청으로도 서비스는 계속 나를
로그인한 사용자로 알아봐야 한다.</p>
<p>그럼 서버가 어떻게 사용자를 기억하게 만들어야 할까?</p>
<p>웹의 기본 통신 방식인 HTTP는
기본적으로 이전 요청을 기억하지 못한다.</p>
<p>즉, 요청 하나가 끝나면
다음 요청은 완전히 새로운 요청처럼 처리된다.</p>
<p>그렇다면 로그인 상태를 유지하기 위해서는
사용자를 기억하기 위한 별도의 방법이 필요하다.</p>
<p>그리고 이 문제를 해결하기 위해 등장한 대표적인 방식이
바로 세션(Session)과 토큰(Token) 기반 인증이다.</p>
<hr>
<h2 id="1-원래-상태를-기억-못하는-http">1. 원래 상태를 기억 못하는 HTTP</h2>
<p>로그인 상태를 유지하는 방식을 이해하려면
우선 HTTP의 기본 구조부터 알아야 한다.</p>
<p>웹에서 클라이언트와 서버는
HTTP 요청(Request)와 응답(Response)을 주고받는다.</p>
<p>브라우저가 서버에게 요청을 보내면,
서버는 그에 맞는 응답을 반환한다.</p>
<p>그런데 여기서 중요한 특징이 하나 있다.</p>
<p>HTTP는 기본적으로 상태(State)를 기억하지 않는다.</p>
<p>즉, 요청 하나와 응답 하나가 끝나면 연결은 죵료되고,
다음 요청이 들어왔을 때 서버는
그 요청이 이전 요청과 같은 사용자의 요청인지 알 수 없다.</p>
<p>예를 들어</p>
<ol>
<li>사용자가 로그인 요청을 보낸다.</li>
<li>서버가 로그인 성공 응답을 보낸다.</li>
<li>이후 사용자가 다시 API 요청을 보낸다.</li>
</ol>
<p>사용자 입장에서는
&quot;아까 로그인했으니까 당연히 로그인된 상태겠지&quot;라고 생각할 수 있지만,
서버 입장에서는 아니라는 말이다.</p>
<p>새로운 요청이 들어왔을 뿐이고,
그 요청이 이전에 로그인했던 사용자의 요청인지
자동으로 알 수 있는 방법이 없다.</p>
<p>즉, HTTP만으로는
로그인 상태를 유지할 수 없는 구조라는 뜻이다.</p>
<p>이런 특징을 상태(State)를 저장하지 않는다는 의미로 Stateless라고 부른다.</p>
<p>그래서 웹 서비스는
사용자를 기억하기 위한 별도의 방법이 필요했고,</p>
<p>그 과정에서 등장한 대표적인 방식이
세션(Session)과 토큰(Token) 기반 인증이다.</p>
<hr>
<h2 id="2-를-해결하고자-등장한-세션-기반-인증">2. 를 해결하고자 등장한 세션 기반 인증</h2>
<p>앞서 말했듯, HTTP는 기본적으로 상태를 기억하지 못한다.</p>
<p>즉, 로그인 요청이 끝난 이후에는
서버가 사용자를 계속 기억할 방법이 없다.</p>
<p>그래서 등장한 방식이
바로 세션(Session) 기반 인증이다.</p>
<p>세션 방식의 핵심은 단순하다.</p>
<blockquote>
<p>서버가 직접 사용자의 로그인 상태를 기억하는 것</p>
</blockquote>
<p>로그인 요청이 성공하면
서버 내부에 사용자의 로그인 정보를 저장한다.</p>
<p>예를 들어</p>
<ul>
<li>어떤 사용자인지</li>
<li>언제 로그인했는지</li>
<li>어떤 권한을 가지고 있는지</li>
</ul>
<p>같은 정보들을 서버가 보관하게 된다.</p>
<p>그리고 이때 서버는
해당 사용자를 구분하기 위한 고유한 ID도 함께 만든다.</p>
<p>이 ID를 세션 ID(Session ID)라고 한다.</p>
<p>그런데 문제는 여기서 끝나지 않는다.</p>
<p>서버는 세션 정보를 저장했지만,
다음 요청에서 어떤 사용자가 어떤 세션의 주인인지 다시 알아야 한다.</p>
<p>그래서 브라우저가 세션 ID를 가지고 다니게 된다.</p>
<p>이때 사용되는 것이 쿠키(Cookie)다.</p>
<p>서버는 로그인 성공 이후
세션 ID를 쿠키에 담아 브라우저로 전달하고,
브라우저는 이후 요청마다 해당 쿠키를 함께 전송한다.</p>
<p>그러면 서버는</p>
<ol>
<li>전달받은 세션 ID를 확인하고</li>
<li>저장된 세션 정보와 비교한 뒤</li>
<li>로그인된 사용자임을 판단하게 된다.</li>
</ol>
<p>즉 세션은 서버에 저장되고 쿠키는 세션 ID를 전달하는 역할을 한다.</p>
<p>쿠키와 세션은 같은 개념이 아니라,
쿠키는 단순한 저장 공간이고,
세션은 서버가 사용자를 기억하기 위한 인증 방식에 가깝다.</p>
<p>이 부분이 핵심이자, 많이 헷갈리는 부분이라고 한다.</p>
<p>이렇게 세션 기반 인증은
HTTP가 상태를 기억하지 못한다는 문제를 해결하기 시작했다.</p>
<hr>
<h2 id="3-의-점차-다가오는-한계">3. 의 점차 다가오는 한계</h2>
<p>세션 기반 인증은
HTTP가 상태를 기억하지 못한다는 문제를 해결해줬다.</p>
<p>하지만 서비스 규모가 커지기 시작하면서
세션 방식도 점점 한계를 드러내기 시작한다.</p>
<p>가장 큰 이유는 세션 정보가 서버에 저장된다는 점이었는데,
사용자가 늘어날수록 서버가 저장해야 하는 세션 데이터도 함께 증가하게 된다.</p>
<p>즉, 로그인한 사용자가 많아질수록
서버는 더 많은 상태를 직접 관리해야 했다.</p>
<p>처음에는 큰 문제가 아니었지만
서비스 규모가 커지고 트래픽이 많아지면서
상황이 많이 달라지기 시작했다.
특히 서버를 여러 대 운영하기 시작하면 문제가 더 복잡해진다.</p>
<p>예를 들어 A 서버에서 로그인했는데,
다음 요청은 B 서버로 들어가는 상황이 생길 수도 있었다.
그런데 세션 정보가 A 서버에만 저장되어 있다면,
B 서버는 해당 사용자가 로그인한 상태인지 알 수 없다.</p>
<p>결국 여러 서버가 같은 세션 정보를 공유해야 하는 문제가 생긴다.</p>
<p>그래서 세션 저장소를 따로 두거나
Redis 같은 외부 저장소를 사용하거나
세션 클러스터링을 구성하는 방식 등이 등장하게 된다.</p>
<p>즉, 세션 방식을 통해 해결된 인증 자체보다
그 세션들을 관리하기 위한 추가적인 구조가 필요해지기 시작한 것이다.</p>
<p>그리고 웹 환경 자체도 점점 변하기 시작했다.</p>
<p>과거에는 서버가 화면까지 함께 렌더링하는 구조가 많았다면,
점점 프론트엔드와 백엔드가 분리되기 시작했고,</p>
<p>SPA(Single Page Application)나
모바일 앱 환경도 늘어나기 시작했다.</p>
<p>이런 환경에서는 서버가 상태를 직접 기억하는 방식보다,
클라이언트가 직접 인증 정보를 들고 다니는 방식이
더 유리해지기 시작했다.</p>
<p>그래서 이런 흐름 속에서 등장한 방식이
바로 토큰(Token) 기반 인증이다.</p>
<hr>
<h2 id="4-를-해결하고자-등장한-토큰-기반-인증">4. 를 해결하고자 등장한 토큰 기반 인증</h2>
<p>세션 방식은 서비스 규모가 커지고, 서버가 여러 대로 분산되고, 프론트와 백엔드가 분리되기 시작하면서
점점 서버가 상태를 직접 저장하는 방식에 부담이 생기기 시작했다.</p>
<p>그래서 등장한 방식이 바로 토큰(Token) 기반 인증이다.</p>
<p>토큰 방식의 핵심은 세션과 반대로,
세션은 서버가 사용자를 기억했다면,
토큰 방식은 클라이언트가 인증 정보를 직접 들고 다닌다.</p>
<p>로그인 요청이 들어오면 서버는 먼저 사용자의 정보를 확인한다.
그리고 로그인이 성공하면 사용자를 증명할 수 있는 토큰(Token)을 발급한다.</p>
<p>브라우저는 이 토큰을 저장하고, 이후 요청마다 함께 전달한다.</p>
<p>대표적으로 Authorization 헤더, Bearer Token 방식 등을 사용한다.</p>
<p>예를 들어 이런 형태다.</p>
<pre><code class="language-http">Authorization: Bearer eyJhbGciOi...</code></pre>
<p>그러면 서버는 요청이 들어올 때마다 토큰이 유효한지 검증하고,
검증이 완료되면 해당 사용자가 로그인된 사용자라고 판단하게 된다.</p>
<p>즉, 세션 방식처럼 서버가 로그인 상태를 직접 저장하지 않아도 되는 구조다.</p>
<p>특히 서버 입장에서는 세션 저장소를 계속 관리할 필요가 줄어들기 때문에
서버 확장이나 프론트/백엔드 분리 구조에서 더 유리해지기 시작했다.</p>
<p>그리고 이런 흐름 속에서
대표적인 토큰 기반 인증 방식으로 떠오른 것이 JWT(JSON Web Token)다.</p>
<p>JWT는 사용자 정보와 만료 시간 등을 포함한 데이터를
하나의 토큰 형태로 만들어 전달하는 방식이다.</p>
<p>덕분에 서버는 별도의 세션 저장소 없이도
토큰 자체를 검증하는 것만으로도 사용자를 확인할 수 있게 되었다.</p>
<p>하지만 토큰 방식 역시 완벽한 방식은 아니었고,
편리한 만큼 새로운 보안 문제와 관리 문제도 함께 가져오게 된다.</p>
<hr>
<h2 id="5-의-점차-보이는-단점">5. 의 점차 보이는 단점</h2>
<p>토큰 기반 인증은 세션 방식의 한계를 해결한 건 사실이다.
하지만 이것 역시도 시간이 지나면서 여러 단점이 드러나기 시작했다.</p>
<p>가장 대표적인 문제는 토큰이 탈취되었을 때의 위험성이다.</p>
<p>세션 방식은 서버가 상태를 직접 관리하기 때문에
특정 세션을 서버에서 강제로 만료시키는 것이 비교적 쉽다.</p>
<p>하지만 JWT는 클라이언트가 토큰 자체를 들고 다니는 구조다.</p>
<p>즉, 토큰이 유효한 동안에는 서버 입장에서 해당 사용자를 정상 사용자로 판단하게 된다.</p>
<p>만약 토큰이 외부에 노출되거나 탈취된다면,
다른 사용자가 그대로 인증된 사용자처럼 동작할 수도 있다는 뜻이다.</p>
<p>그리고 JWT는 흔히 오해하는 것처럼 데이터를 암호화하는 방식이 아니다.</p>
<p>JWT의 Payload는 Base64 방식으로 인코딩되어 있을 뿐이며,
토큰 자체를 열어보는 것은 어렵지 않다.</p>
<p>이는 곧 민강한 정보를 토큰 내부에 그대로 담는 것은 위험할 수 도 있다는 문제로 직결된다.</p>
<p>또 다른 문제는 로그아웃 처리다.</p>
<p>세션 방식은 서버에서 세션을 제거하면 즉시 로그아웃 상태로 만들 수 있다.
하지만 JWT는 이미 클라이언트에게 발급된 토큰이다.</p>
<p>서버가 토큰을 강제로 회수하기 어렵기 때문에,
만료 시간이 끝나기 전까지는 계속 사용할 수 있는 문제가 생긴다.</p>
<p>그래서 등장한 개념이 바로 Access Token과 Refresh Token 구조다.</p>
<p>Access Token은 짧은 시간만 사용하고, Refresh Token으로 새로운 토큰을 재발급받는 방식이다.</p>
<p>즉, 토큰 기반 인증은 세션 방식의 한계를 해결하기 위해 등장했지만,
그 과정에서 또 다른 보안 문제와 관리 문제를 함께 안게 된 것이다.</p>
<p>결국 인증 방식은 &quot;무조건 더 좋은 방식&quot;이 등장하는 흐름이라기보다,
환경 변화에 따라 장단점이 다른 방식들이 계속 발전해온 과정에 가깝다고 볼 수 있다.</p>
<hr>
<h2 id="6-세션과-토큰의-차이">6. 세션과 토큰의 차이</h2>
<p>세션과 토큰은 결국엔 둘 다
HTTP가 상태를 기억하지 못한다는 문제를 해결하기 위해 등장한 방식이다.</p>
<p>세션의 어느 문제가 있고, 토큰에 어느 문제가 있고,
결국 인증의 역할을 똑똑히 하고 있다.</p>
<p>하지만 분명한 차이점이 있다.
먼저 사용자를 기억하는 방식에서부터 차이가 있다.</p>
<p>세션 방식은 서버가 사용자의 로그인 상태를 직접 저장한다.
브라우저는 세션 ID만 전달하고, 실제 로그인 정보는 서버 내부에서 관리된다.</p>
<p>반면 토큰 방식은 클라이언트가 인증 정보를 직접 들고 다닌다.
서버는 별도의 로그인 상태를 저장하기보다, 전달받은 토큰이 유효한지만 검증한다.</p>
<table>
<thead>
<tr>
<th>세션 기반 인증</th>
<th>토큰 기반 인증</th>
</tr>
</thead>
<tbody><tr>
<td>서버가 상태 저장</td>
<td>클라이언트가 토큰 저장</td>
</tr>
<tr>
<td>서버 메모리/저장소 필요</td>
<td>Stateless 구조에 유리</td>
</tr>
<tr>
<td>강제 로그아웃 처리 쉬움</td>
<td>토큰 만료 전까지 유지 가능</td>
</tr>
<tr>
<td>서버 확장 시 관리 필요</td>
<td>분산 환경에 유리</td>
</tr>
<tr>
<td>전통적인 웹 구조에 강함</td>
<td>SPA / 모바일 환경에 강함</td>
</tr>
</tbody></table>
<p>이렇게 각각 장단점이 다르기 때문에 서비스 구조에 따라 더 잘 어울리는 환경도 달라진다.</p>
<p>세션 기반 인증은 아래와 같은 서비스에서 여전히 잘 쓰이고 있다.</p>
<ul>
<li>전통적인 서버 렌더링 기반 웹 서비스</li>
<li>관리자 페이지</li>
<li>금융 서비스</li>
<li>보안이 중요한 내부 시스템</li>
</ul>
<p>그런 전통적인 서비스에서 쓰이는 반면 토큰 기반 인증은 비교적 최신 서비스에 쓰인다.</p>
<ul>
<li>SPA(Single Page Application)</li>
<li>모바일 앱</li>
<li>프론트엔드 / 백엔드 분리 구조</li>
<li>MSA(Micro Service Architecture)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[여러가지 색깔의 DataBase]]></title>
            <link>https://velog.io/@hogu__giriboy/%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80-%EC%83%89%EA%B9%94%EC%9D%98-DataBase</link>
            <guid>https://velog.io/@hogu__giriboy/%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80-%EC%83%89%EA%B9%94%EC%9D%98-DataBase</guid>
            <pubDate>Thu, 07 May 2026 09:50:25 GMT</pubDate>
            <description><![CDATA[<p>이번 초급 프로젝트에서는 PostgreSQL 을 채택해서 진행했다.</p>
<p>그런데 프로젝트와 별개로 진행하던 스프린트 미션에서는 MongoDB 를 사용하고 있었다.</p>
<p>멘토님은 스프린트 미션 코드리뷰도 함께 진행해주시고, 초급 프로젝트 방향까지 같이 봐주시는데,
그러다 보니 자연스럽게 “초급 프로젝트도 MongoDB를 쓰고 있겠구나” 하고 잠깐 오해하신 상황이 있었다.</p>
<p>그 작은 해프닝을 계기로 이런 궁금증이 생겼다.</p>
<ul>
<li>MongoDB와 PostgreSQL은 정확히 뭐가 다를까?</li>
<li>어떤 상황에서 서로 다른 DB를 선택하게 될까?</li>
<li>그리고 DB를 공부한다면, 결국 무엇을 깊게 파야 할까?</li>
</ul>
<p>팀원분도 비슷한 질문을 멘토님께 드렸고, 돌아온 답변은 꽤 인상적이었다.</p>
<blockquote>
<p>현업에서는 하나만 정답처럼 쓰이지 않는다.
서비스 성격과 상황에 따라 정말 다양한 DB가 사용된다.</p>
</blockquote>
<p>그 말을 듣고 나니 단순히 “MongoDB vs PostgreSQL” 비교를 넘어서,
세상에는 어떤 종류의 데이터베이스가 있고, 각각 어디에 쓰이는지 정리해보고 싶어졌다.</p>
<p>그래서 이번 글에서는 다양한 데이터베이스의 종류와 특징, 그리고 어떤 상황에서 선택되는지를 중심으로 정리해보려고 한다.</p>
<hr>
<h2 id="1-다양한-종류의-데이터베이스-왜">1. 다양한 종류의 데이터베이스, 왜?</h2>
<p>처음 데이터베이스를 접했을 때는
그냥 “데이터를 저장하는 공간” 정도로만 생각했었다.</p>
<p>그래서 왜 스프린트 미션에서는 MongoDB 를 사용하고,
초급 프로젝트에서는 PostgreSQL 를 사용하는지 크게 의문을 가지지도 않았다.</p>
<p>그런데 이번 기회에 조금 더 찾아보면서
데이터베이스는 생각보다 훨씬 다양한 종류가 존재한다는 걸 알게 됐다.</p>
<h3 id="모든-데이터가-같은-모습일까">모든 데이터가 같은 모습일까?</h3>
<p>아니다.
이게 어쩌면 이번 섹션에 핵심으로 작용된다.</p>
<p>예를 들어 데이터를 보면</p>
<ul>
<li>사용자 정보처럼 구조가 명확한 데이터</li>
<li>게시글처럼 자유롭게 형태가 바뀌는 데이터</li>
<li>친구 관계처럼 연결 구조가 중요한 데이터</li>
<li>로그처럼 계속해서 대량으로 쌓이는 데이터</li>
</ul>
<p>등이 존재한다.</p>
<p>즉, 데이터마다 다루는 <strong>방식</strong>과 <strong>성격</strong> 자체가 다르다는 뜻이다.</p>
<h3 id="하나의-방식에서-생기는-한계">하나의 방식에서 생기는 한계</h3>
<p>이런 데이터를 모두 하나의 방식으로 처리하려고 하면 분명 문제가 생긴다.</p>
<p>어떤 경우에는 구조가 지나치게 엄격해지고,
어떤 경우에는 성능이 떨어지며,
어떤 경우에는 데이터 관리 자체가 복잡해진다.</p>
<p>결국 데이터의 성격에 따라
더 적합한 저장 방식이 필요해지기 시작한 것이다.</p>
<p>그래서 등장한 것이 바로
<strong>각각 다른 방식</strong>으로 <strong>데이터를 저장</strong>하는 다양한 데이터베이스들이다.</p>
<h3 id="데이터베이스를-나누는-대표적인-기준">데이터베이스를 나누는 대표적인 기준</h3>
<p>정답은 바로 위에서 다뤘다.</p>
<blockquote>
<p><strong>데이터를 어떤 형태로 저장하느냐</strong></p>
</blockquote>
<p>이 기준에 따라 데이터베이스는</p>
<ul>
<li>테이블 형태로 정리해서 저장하는 방식</li>
<li>JSON 문서 형태로 묶어서 저장하는 방식</li>
<li>key-value 형태로 빠르게 저장하는 방식</li>
</ul>
<p>등으로 나뉘게 된다.</p>
<p>그리고 이 저장 방식의 차이가
각 데이터베이스의 특징과 사용 목적을 결정하게 된다.</p>
<hr>
<h2 id="2-데이터를-저장하는-방식">2. 데이터를 저장하는 방식</h2>
<p>앞에서 말했듯이
데이터베이스를 이해하는 가장 큰 기준 중 하나는</p>
<blockquote>
<p>데이터를 어떤 방식으로 저장하느냐</p>
</blockquote>
<p>이다.</p>
<p>이 기준으로 보면
데이터베이스 종류들이 왜 나뉘는지 조금 더 명확하게 보이기 시작한다.</p>
<h3 id="데이터를-정리해서-저장하기">데이터를 &quot;정리해서&quot; 저장하기</h3>
<p>가장 익숙한 방식은
데이터를 정해진 구조 안에서 관리하는 방식이다.</p>
<p>데이터를 <strong>정해진 구조 안에 넣어서 관리하는 방식</strong>이다.</p>
<p>이 방식이 바로 관계형 데이터베이스(RDBMS)이며,
대표적으로 이번 초급 프로젝트에서 채택했던 PostgreSQL, MySQL이 있다.</p>
<h4 id="선-구조-후-데이터">선 구조 후 데이터</h4>
<p>관계형 데이터베이스는 흐름이 비교적 명확하다.</p>
<ol>
<li>테이블 구조를 먼저 정의</li>
<li>정의된 구조에 맞게 데이터 저장</li>
</ol>
<p>예를 들어</p>
<ul>
<li>사용자 테이블</li>
<li>습관 테이블</li>
<li>기록 테이블</li>
</ul>
<p>처럼 데이터를 역할별로 나누고,
각 테이블을 관계로 연결해서 관리한다.</p>
<h4 id="장점">장점</h4>
<p>관계형 데이터베이스의 가장 큰 특징은
데이터의 <strong>정확성</strong>과 <strong>일관성</strong>을 유지하기 쉽다는 점이다.</p>
<p>예를 들어</p>
<ul>
<li>존재하지 않는 사용자에 대한 기록 생성</li>
<li>잘못된 타입의 데이터 입력</li>
<li>관계가 맞지 않는 데이터 저장</li>
</ul>
<p>같은 문제들을 데이터베이스 단계에서 방지할 수 있다.</p>
<p>즉, 데이터 구조를 엄격하게 관리하는 대신
안정성을 높이는 방식이라고 볼 수 있다.</p>
<h4 id="단점">단점</h4>
<p>다만 구조를 엄격하게 관리하는 만큼
설계와 관리가 상대적으로 복잡해진다.</p>
<p>테이블을 나누고,
관계를 연결하고,
여러 데이터를 함께 고려해야 하기 때문이다.</p>
<p>그래서 데이터를 저장하거나 수정하는 과정이
비교적 무겁게 느껴질 수 있다.</p>
<h3 id="데이터를-묶어서-저장하기">데이터를 &quot;묶어서&quot; 저장하기</h3>
<p>반대로 데이터를 <strong>하나의 문서처럼 저장하는 방식</strong>도 존재한다.</p>
<p>이 방식이 바로 문서형 데이터베이스이며,
대표적으로 MongoDB 가 있다.</p>
<h4 id="선-데이터-후-구조">선 데이터 후 구조</h4>
<p>문서형 데이터베이스는 관계형 데이터베이스보다 훨씬 유연하다.</p>
<p>예를 들면 아래처럼 JSON 형태에 가까운 구조 자체를 그대로 저장할 수 있다.</p>
<pre><code class="language-json">{
  &quot;user&quot;: &quot;명곤&quot;,
  &quot;habits&quot;: [{ &quot;name&quot;: &quot;공부&quot;, &quot;done&quot;: false }]
}</code></pre>
<p>하나의 문서 안에 필요한 데이터를 함께 넣어서 관리하는 방식이다.</p>
<h4 id="장점-1">장점</h4>
<p>문서형 데이터베이스는
구조를 엄격하게 미리 정의하지 않아도 된다는 특징이 있다.</p>
<p>그래서</p>
<ul>
<li>빠르게 개발 가능</li>
<li>구조 변경에 유연함</li>
<li>데이터를 한 번에 저장 가능</li>
</ul>
<p>같은 장점을 가진다.</p>
<p>특히 데이터 구조가 자주 바뀌거나
빠른 개발 속도가 중요한 서비스에서 자주 사용된다.</p>
<h4 id="단점-1">단점</h4>
<p>다만 유연한 만큼
데이터 관리 책임이 개발자 쪽으로 많이 넘어온다.</p>
<p>관계형 데이터베이스처럼
강하게 관계를 검증하지 않기 때문에</p>
<ul>
<li>데이터 중복</li>
<li>일관성 문제</li>
<li>복잡한 관계 관리 어려움</li>
</ul>
<p>같은 문제가 발생하기 쉽다.</p>
<p>특히 규모가 커질수록
같은 데이터가 여러 곳에 퍼지면서
어떤 값이 실제 기준 데이터인지 헷갈리는 상황도 생길 수 있다.</p>
<h3 id="sql-vs-mongodb">SQL vs MongoDB</h3>
<p>MongoDB와 SQL 계열 데이터베이스를 비교할 때 자주 나오는 말이 있다.</p>
<blockquote>
<p>MongoDB는 쓰기가 빠르고, SQL은 느리다.</p>
</blockquote>
<p>하지만 이 표현은 조금 단순화된 이야기라고 한다.</p>
<p>관계형 데이터베이스는 데이터를 저장할 때</p>
<ul>
<li>관계 확인</li>
<li>데이터 검증</li>
<li>제약 조건 검사</li>
</ul>
<p>등을 함께 수행한다.</p>
<p>반면 문서형 데이터베이스는
상대적으로 자유로운 구조 안에서 데이터를 저장한다.</p>
<p>그래서 체감상 문서형 데이터베이스가 더 가볍고 빠르게 느껴질 수 있는 것이다.</p>
<blockquote>
<p>MongoDB가 무조건 더 뛰어난 성능을 가진다기보다는
저장 방식 자체가 다르기 때문에 체감 차이가 발생하는 것에 가깝다.</p>
</blockquote>
<p>결국 중요한 건
어떤 데이터와 어떤 서비스 구조에 더 잘 맞느냐이다.</p>
<hr>
<h2 id="3-속도를-위해-태어난-데이터베이스">3. 속도를 위해 태어난 데이터베이스</h2>
<p>앞에서 본 관계형 데이터베이스와 문서형 데이터베이스는
결국 데이터를 저장하고 관리하는 것이 핵심 목적이었다.</p>
<ul>
<li>관계형 데이터베이스 → 구조를 나눠서 저장</li>
<li>문서형 데이터베이스 → 데이터를 묶어서 저장</li>
</ul>
<p>그런데 서비스 규모가 커지면
단순히 “저장”만 잘한다고 끝나지 않는다.</p>
<p>예를 들어 서비스에서는 아래 같은 요청이 정말 자주 발생한다.</p>
<ul>
<li>로그인 상태 확인</li>
<li>오늘의 습관 조회</li>
<li>토글 상태 확인</li>
<li>자주 조회되는 데이터 불러오기</li>
</ul>
<p>이런 요청마다 매번 메인 데이터베이스에 접근하게 되면
점점 성능 부담이 커지기 시작한다.</p>
<p>그래서 등장한 방식이 바로</p>
<blockquote>
<p>데이터를 엄청 빠르게 꺼내는 데 집중한 데이터베이스</p>
</blockquote>
<p>로, 대표적으로 Redis가 있다.</p>
<h3 id="key-value-방식">Key-Value 방식</h3>
<p>이 방식은 구조 자체가 굉장히 단순하다.</p>
<pre><code>key → value</code></pre><p>예를 들면</p>
<ul>
<li>user:1 → 명곤</li>
<li>habit:3 → false</li>
</ul>
<p>처럼 key 하나에 value 하나를 연결해서 저장한다.</p>
<p>구조가 극단적으로 단순한 이유는 하나다.</p>
<blockquote>
<p>오직 <strong>속도</strong>를 위해서다.</p>
</blockquote>
<h3 id="어떻게-이런-속도가">어떻게 이런 속도가?</h3>
<p>핵심은 크게 두 가지다.</p>
<h4 id="1-메모리-기반-저장">1. 메모리 기반 저장</h4>
<p>일반적인 관계형 데이터베이스는
주로 디스크 기반으로 데이터를 저장한다.</p>
<p>반면 Redis는 데이터를 메모리(RAM)에 저장한다.</p>
<ul>
<li>디스크 → 상대적으로 느림</li>
<li>메모리 → 매우 빠름</li>
</ul>
<p>즉, 물리적인 접근 속도 자체에서 차이가 발생한다.</p>
<h4 id="2-구조-자체가-단순함">2. 구조 자체가 단순함</h4>
<p>Redis는 복잡한 관계를 거의 고려하지 않는다.</p>
<ul>
<li>조인 없음</li>
<li>관계 관리 없음</li>
<li>복잡한 쿼리 최소화</li>
</ul>
<p>그냥 key 하나로 바로 데이터를 찾는다.
“빠르게 찾기”에만 집중한 구조인 것인데, 어떻게 느릴 수 있겠는가 ...</p>
<h3 id="빠른-속도가-필요한-사용처">빠른 속도가 필요한 사용처</h3>
<p>중요한 건 Redis가 보통 <strong>메인 데이터베이스</strong> 역할을 하지는 않는다는 점이다.</p>
<p>대신 아래 같은 곳에서 많이 사용된다.</p>
<ul>
<li>로그인 세션 저장</li>
<li>캐시(Cache) 저장</li>
<li>조회 결과 임시 저장</li>
<li>실시간 데이터 처리</li>
</ul>
<p>예를 들어 오늘의 습관 조회 결과를 Redis에 저장해두면,</p>
<ol>
<li>첫 요청 → 메인 DB 조회</li>
<li>조회 결과를 Redis에 저장</li>
<li>다음 요청 → Redis에서 바로 반환</li>
</ol>
<p>처럼 동작할 수 있다.</p>
<p>즉, 매번 데이터베이스를 조회하지 않아도 되기 때문에
서비스 속도가 훨씬 빨라질 수 있다.</p>
<p>실제로 서비스 고도화 과정에서
캐시 전략으로 자주 고려되는 방식이라고 한다.</p>
<h3 id="단점-2">단점</h3>
<p>물론 빠르다고 해서 모든 상황에 적합한 건 아니다.</p>
<p>Redis 같은 Key-Value 방식은</p>
<ul>
<li>데이터 유실 가능성</li>
<li>복잡한 관계 표현 어려움</li>
<li>데이터 일관성 관리 한계</li>
</ul>
<p>같은 단점이 존재한다.</p>
<p>그래서 보통은</p>
<blockquote>
<p>메인 데이터베이스를 보조하는 역할</p>
</blockquote>
<p>로 많이 사용된다.</p>
<hr>
<h2 id="4-관계를-저장하는-데이터베이스">4. 관계를 저장하는 데이터베이스</h2>
<p>앞에서 다룬 관계형 데이터베이스는
테이블을 나누고, 관계를 연결한 뒤 필요한 순간에 JOIN으로 데이터를 가져오는 방식으로,
<strong>관계를 간접적으로 표현하는 구조</strong>에 가깝다.</p>
<p>그런데 세상에는 관계 자체가 핵심인 데이터들도 존재한다.</p>
<p>예를 들면</p>
<ul>
<li>친구의 친구 추천</li>
<li>팔로우 관계</li>
<li>추천 시스템</li>
<li>네트워크 분석</li>
</ul>
<p>같은 경우들이다.</p>
<p>이런 데이터들은 연결이 계속 늘어나고,
관계 구조도 점점 복잡해진다.</p>
<h3 id="관계형-데이터베이스의-한계">관계형 데이터베이스의 한계</h3>
<p>관계형 데이터베이스에서도 이런 구조를 구현할 수는 있다.</p>
<p>다만 관계가 많아질수록</p>
<ul>
<li>테이블 수 증가</li>
<li>JOIN 증가</li>
<li>쿼리 복잡도 증가</li>
</ul>
<p>같은 문제가 발생하기 시작한다.</p>
<p>특히 “연결을 따라가며 탐색”하는 작업이 많아질수록
성능과 관리 복잡도가 함께 커질 수 있다.</p>
<p>그래서 등장한 방식이 바로</p>
<blockquote>
<p>관계 자체를 중심으로 저장하는 데이터베이스</p>
</blockquote>
<p>이다.</p>
<p>대표적으로 Neo4j 가 있다.</p>
<h3 id="그래프-데이터베이스">그래프 데이터베이스</h3>
<p>그래프 데이터베이스는 데이터를 아래처럼 표현한다.</p>
<ul>
<li>노드(Node) → 데이터</li>
<li>엣지(Edge) → 관계</li>
</ul>
<p>여기서 중요한 건</p>
<blockquote>
<p>관계 자체가 하나의 <strong>핵심 데이터</strong>로 다뤄진다는 점</p>
</blockquote>
<p>이다.</p>
<p>예를 들어 관계형 데이터베이스에서는</p>
<ul>
<li>유저 테이블</li>
<li>팔로우 테이블</li>
</ul>
<p>처럼 관계를 별도 테이블로 관리한다.</p>
<p>반면 그래프 데이터베이스는 연결 자체가 구조가 된다.</p>
<pre><code>A --- 친구 --- B
|
팔로우
|
C</code></pre><blockquote>
<p>“누가 누구와 연결되어 있는가”</p>
</blockquote>
<p>를 굉장히 자연스럽게 표현하는 방식으로 볼 수 있다.</p>
<h3 id="장점-2">장점</h3>
<p>그래프 데이터베이스의 가장 큰 장점은
<strong>관계 탐색에 특화</strong>되어 있다는 점이다.</p>
<p>예를 들어</p>
<p>“A의 친구의 친구 찾기”</p>
<p>같은 작업을 수행한다고 하면,</p>
<p>관계형 데이터베이스에서는</p>
<ul>
<li>여러 JOIN 수행</li>
<li>복잡한 쿼리 작성</li>
<li>관계 깊어질수록 성능 부담 증가</li>
</ul>
<p>같은 문제가 생길 수 있다.</p>
<p>반면 그래프 데이터베이스는</p>
<ul>
<li>연결된 노드 따라가기</li>
</ul>
<p>방식으로 탐색이 가능하다.</p>
<p><strong>관계를 따라 이동하는 작업 자체에 최적화된 구조</strong>인 것이다.</p>
<h3 id="관계-탐색이-필요한-사용처">관계 탐색이 필요한 사용처</h3>
<p>그래프 데이터베이스는 보통</p>
<ul>
<li>친구 추천 시스템(SNS)</li>
<li>콘텐츠 추천 알고리즘</li>
<li>네트워크 분석</li>
<li>경로 탐색</li>
</ul>
<p>처럼 “연결 관계”가 핵심인 서비스에서 많이 사용된다.</p>
<h3 id="단점-3">단점</h3>
<p>다만 모든 상황에서 좋은 건 아니다.</p>
<p>그래프 데이터베이스는</p>
<ul>
<li>일반적인 CRUD 작업에는 비효율적일 수 있고</li>
<li>단순 데이터 저장에는 과한 구조가 될 수 있으며</li>
<li>학습 난이도와 설계 난이도가 높은 편</li>
</ul>
<p>이라는 특징도 존재한다.</p>
<p>그래서 보통은</p>
<blockquote>
<p>관계 탐색이 핵심인 특정 문제를 해결하기 위해 사용하는 데이터베이스</p>
</blockquote>
<p>라고 볼 수 있다.</p>
<h2 id="5-데이터를-분석하기-위한-데이터베이스">5. 데이터를 분석하기 위한 데이터베이스</h2>
<p>앞에서 살펴본 데이터베이스들은 공통적으로</p>
<blockquote>
<p>서비스를 운영하기 위한 데이터 저장</p>
</blockquote>
<p>에 초점이 맞춰져 있었다.</p>
<p>예를 들면</p>
<ul>
<li>사용자 정보 저장</li>
<li>습관 데이터 저장</li>
<li>로그인 상태 관리</li>
</ul>
<p>같은 작업들이다.</p>
<p>그런데 서비스 규모가 커지면
단순히 데이터를 저장하는 것을 넘어
데이터를 분석하려는 요구가 생기기 시작한다.</p>
<p>예를 들어</p>
<ul>
<li>“유저들이 어떤 기능을 가장 많이 사용할까?”</li>
<li>“하루 평균 습관 완료율은 얼마일까?”</li>
<li>“이번 주 활동량은 지난주보다 늘었을까?”</li>
</ul>
<p>같은 질문들이다.</p>
<p><strong>데이터를 저장</strong>하는 것을 넘어
<strong>데이터를 통해 흐름과 패턴을 분석</strong>하려는 단계로 넘어가는 것이다.</p>
<h3 id="운영-데이터베이스로-분석하면">운영 데이터베이스로 분석하면?</h3>
<p>물론 기존의 PostgreSQL 같은 관계형 데이터베이스에서도 분석 자체는 가능하다.</p>
<p>하지만 데이터 규모가 커질수록</p>
<ul>
<li>JOIN 증가</li>
<li>복잡한 집계 쿼리 증가</li>
<li>대량 데이터 조회 발생</li>
</ul>
<p>같은 문제가 생기기 쉽다.</p>
<p>그리고 운영 중인 데이터베이스에서
무거운 분석 작업까지 함께 수행하면
실제 서비스 성능에도 영향을 줄 수 있다.</p>
<p>그래서 대규모 서비스에서는 종종</p>
<blockquote>
<p>운영용 데이터베이스와 분석용 데이터베이스를 분리</p>
</blockquote>
<p>해서 사용하기도 한다.</p>
<h3 id="컬럼-기반으로-저장하는-방식">컬럼 기반으로 저장하는 방식</h3>
<p>이런 분석 작업에 특화된 방식 중 하나가 바로</p>
<blockquote>
<p>컬럼형(Column-oriented) 데이터베이스</p>
</blockquote>
<p>이다.</p>
<p>대표적으로 Apache Cassandra 같은 계열이 자주 언급된다.</p>
<h3 id="기존-데이터베이스와의-차이점">기존 데이터베이스와의 차이점</h3>
<p>기존 관계형 데이터베이스는 보통 행(Row) 단위로 저장된다.</p>
<pre><code>id | name | age | habit</code></pre><p>즉, 한 줄 안에 하나의 데이터 정보가 모두 들어간다.</p>
<p>반면 컬럼형 데이터베이스는
같은 컬럼끼리 묶어서 저장한다.</p>
<pre><code>id   → [1, 2, 3]
name → [A, B, C]
age  → [20, 25, 30]</code></pre><p>같은 종류의 데이터들을 연속적으로 저장하는 구조인 것이다.</p>
<h3 id="장점-3">장점</h3>
<p>이 방식은 특정 데이터만 대량으로 분석할 때 굉장히 강력하다.</p>
<p>예를 들어
“전체 유저 평균 나이 계산” 같은 작업을 수행한다고 하면,</p>
<p>행 기반 저장에서는
모든 row를 읽으면서 필요한 데이터를 꺼내야 한다.</p>
<p>반면 컬럼형 데이터베이스는
age 컬럼만 읽기만 하면 된다.</p>
<p>분석에 필요한 데이터만 빠르게 가져올 수 있기 때문에
대규모 집계와 통계 작업에 정말 유리하다.</p>
<h3 id="분석이-필요한-사용처">분석이 필요한 사용처</h3>
<p>이런 데이터베이스는 보통</p>
<ul>
<li>로그 분석</li>
<li>통계 처리</li>
<li>데이터 시각화</li>
<li>BI(Business Intelligence)</li>
<li>대규모 집계 시스템</li>
</ul>
<p>같은 분야에서 사용된다.</p>
<p>서비스 기능 자체보다는
<strong>데이터를 기반으로 의사결정을 내리기 위한 영역</strong>에서 많이 활용된다.</p>
<h3 id="단점-4">단점</h3>
<p>물론 일반 서비스 운영에는 잘 맞지 않는 부분도 존재한다.</p>
<ul>
<li>실시간 CRUD 작업에는 상대적으로 약하고</li>
<li>일반적인 서비스 구조와는 사용 방식이 다르며</li>
<li>데이터 수정이 잦은 환경에는 비효율적일 수 있다.</li>
</ul>
<p>그래서 운영 DB랑 분리해서 사용해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[협업 쉽게 해버리기]]></title>
            <link>https://velog.io/@hogu__giriboy/%ED%98%91%EC%97%85-%EC%89%BD%EA%B2%8C-%ED%95%B4%EB%B2%84%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@hogu__giriboy/%ED%98%91%EC%97%85-%EC%89%BD%EA%B2%8C-%ED%95%B4%EB%B2%84%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Tue, 28 Apr 2026 16:06:46 GMT</pubDate>
            <description><![CDATA[<p>초급 팀 프로젝트를 진행하고 팀장들끼리 모여 회고 하던 중,
다른 팀장님께서 여러 협업 도구들을 알려주셨다.</p>
<p>슬랙, 지라, 리니어 같은 작업 관리 도구부터
프리티어, 린트, 허스키처럼 코드 스타일과 규칙을 관리하는 도구까지.</p>
<p>사실 나는 거의 처음 들어보는 것들이었다.
프리티어 정도만 &quot;저장하면 코드 예쁘게 정리해주는 도구&quot;로만 알고 있었을 뿐이다.</p>
<p>솔직히 충격이었다.</p>
<blockquote>
<p>“PR 형식을 자동으로 맞출 수 있다”
“컨벤션이 안 맞으면 push 자체가 막힌다”</p>
</blockquote>
<p>컨벤션이 안 맞으면 push 자체가 막힌다니.
우린 코드리뷰에서 하나하나 확인하고 알려주기로 했었는데 ...</p>
<p>물론 코드리뷰가 단순히 컨벤션만 보는 과정은 아니지만,
이 부분을 자동으로 처리할 수 있다는 건 꽤 큰 차이라고 느껴졌다.</p>
<p>특히 “push를 막는다”는 표현이 인상적이었다.
단순히 권장하는 수준이 아니라, 아예 지키지 않으면 다음 단계로 못 넘어가게 만든다는 의미니까.</p>
<p>그러던 중 또 다른 팀장님이 이런 말씀을 하셨다.</p>
<blockquote>
<p>“나는 자동사냥처럼 돌아가는 구조를 만들어두는 걸 좋아한다”</p>
</blockquote>
<p>사실 이번에 처음 듣는 말은 아닌데,
지금 다루려는 협업 도구들이랑 꽤 어울리는 말 같아서 인용해왔다.</p>
<p>나 또한 그렇다.
자동화가 가능하면 몸이 덜 고생하지 않겠는가 !</p>
<p>그런 나에게 이런 &quot;자동으로&quot; PR 형식을 맞춘다거나,
컨벤션이 안 맞으면 &quot;자동으로&quot; push 자체를 막는다거나
정말 매혹적인 말이 아닐 수가 없다.</p>
<p>그래서 그런 도구가 어떤 도구인지 살펴보고
그 외에 또 어떤 도구가 있나도 살펴보고 싶어서
이렇게 블로그 주제로 잡아왔다.</p>
<hr>
<h2 id="1-작업-흐름을-보이게-만드는-도구">1. 작업 흐름을 보이게 만드는 도구</h2>
<p>이번 프로젝트에서는
역할을 비교적 명확하게 나누고,
매일 데일리 스크럼을 진행하면서 작업 상황을 공유했다.</p>
<p>덕분에 협업이 크게 꼬이는 경험은 하지 않았다.</p>
<p>다만 느낀 건,
이 흐름을 사람이 계속 직접 관리하고 있었다는 점이다.</p>
<p>누가 어떤 작업을 하고 있는지,
어디까지 진행됐는지,
막힌 부분은 없는지
이런 것들을 매번 대화를 통해 확인해야 했다.</p>
<p>협업 도구는 이 과정을
<strong>시스템</strong>으로 대신해준다.</p>
<p>작업을 이슈 단위로 나누고,
진행 상태를 시각적으로 보여주고,
누가 무엇을 맡고 있는지 한눈에 파악할 수 있게 해준다.</p>
<p>즉, 매번 묻고 확인하지 않아도
<strong>흐름 자체가 자연스럽게 공유되는 구조</strong>가 된다.</p>
<p>물론 소통을 통한 방식도 충분히 협업을 굴릴 수 있고,
팀 분위기를 만드는 데에도 중요한 역할을 한다.</p>
<p>다만 이 흐름을 전적으로 사람에게만 의존하게 되면
상황에 따라 관리가 어려워질 수 있다.</p>
<p>그래서 이런 과정을 도구가 함께 담당해준다면
더 안정적이고 효율적인 협업이 가능해진다.</p>
<p>결국 협업 도구는
단순한 편의 기능이 아니라,</p>
<p>작업 흐름 자체를 관리해주는 역할을 한다.</p>
<h3 id="github-issues">GitHub Issues</h3>
<p><img src="https://velog.velcdn.com/images/hogu__giriboy/post/6662e370-4bac-4b11-bb8a-10002aa31e4f/image.png" alt="github issues"></p>
<p>이번 프로젝트에서는 GitHub의 Issues 기능을 활용해서 작업을 관리했다.</p>
<p>각자 해야 할 일을 이슈 단위로 정리하고,
관련된 PR을 연결하는 방식으로 작업을 진행했다.</p>
<p>코드를 관리하는 GitHub 안에서 바로 사용할 수 있다는 점이 편리했고,
이슈와 PR이 자연스럽게 연결되기 때문에
작업 흐름을 따라가기도 쉬웠다.</p>
<p>다만 첫 프로젝트였던 만큼
이 기능을 충분히 활용하지는 못했던 것 같다.</p>
<p>작업 흐름을 관리하기보다는
칸반 보드처럼 간단히 정리하거나
기록을 남겨두는 용도로 사용하는 데에 더 집중했던 느낌이었다.</p>
<p>다음은 보다 체계적으로 작업 흐름을 관리할 수 있는 도구들을 다뤘다.</p>
<h3 id="jira">Jira</h3>
<p><img src="https://velog.velcdn.com/images/hogu__giriboy/post/0bcc056c-a4e4-4bd0-b970-a9024e1c1462/image.png" alt="Jira"></p>
<p>지라는 이러한 이슈 기반 작업 관리를
더 체계적으로 확장한 도구다.</p>
<p>작업을 단순히 나열하는 것이 아니라,
진행 상태를 단계별로 나누고 보드 형태로 시각화해서
팀 전체 흐름을 한눈에 파악할 수 있게 해준다.</p>
<p>또한 스프린트 관리나 자동화 기능 등
협업을 위한 다양한 기능을 제공한다는 점에서
보다 구조적인 작업 관리 도구라고 볼 수 있다.</p>
<h3 id="linear">Linear</h3>
<p><img src="https://velog.velcdn.com/images/hogu__giriboy/post/0669627e-e63f-46b9-9513-261920be6197/image.png" alt="Linear"></p>
<p>리니어 역시 이슈 기반으로 작업을 관리하는 도구다.</p>
<p>지라와 유사한 역할을 하지만,
보다 가볍고 직관적인 사용성을 강조한 도구로 알려져 있다.</p>
<p>복잡한 설정 없이도 빠르게 작업을 관리할 수 있어서
소규모 팀이나 스타트업에서 많이 사용되는 편이다.</p>
<p>결국 지라와 리니어는
<strong>작업을 ‘흐름’으로 관리한다는 점</strong>에서는 동일한 도구들이다.</p>
<h3 id="slack">Slack</h3>
<p><img src="https://velog.velcdn.com/images/hogu__giriboy/post/1640c5c3-c696-4608-8c4c-26b8af78b15d/image.png" alt="slack"></p>
<p>이슈 기반으로 작업을 관리하는 것과 별개로,
팀원 간의 소통을 담당하는 도구도 필요하다.</p>
<p>슬랙은 채널을 통해 대화를 주제별로 나눌 수 있어
공지, 작업 공유, 질문 등을 구분해서 관리할 수 있다.</p>
<p>이 덕분에 대화가 뒤섞이지 않고,
필요한 정보를 빠르게 찾을 수 있다.</p>
<p>결국 슬랙은
<strong>팀의 소통을 구조화해주는 도구</strong>라고 볼 수 있다.</p>
<hr>
<h2 id="2-코드-스타일을-맞춰주는-도구">2. 코드 스타일을 맞춰주는 도구</h2>
<p>협업을 하다 보면
생각보다 자주 부딪히는 문제가 하나 있는데,
바로 코드 스타일이다.</p>
<p>같은 기능을 구현하더라도
들여쓰기, 세미콜론, 따옴표, 변수명 등
사람마다 코드 스타일이 조금씩 다르다.</p>
<p>이 차이가 쌓이면
코드를 읽기 어려워지고,
리뷰 과정에서도 불필요한 피드백이 반복되게 된다.</p>
<p>물론 컨벤션을 정해서 맞출 수는 있고,
우리 또한 이러한 코드 컨벤션을 정해놓고
프로젝트를 시작했다.</p>
<p>하지만 문제는
서론에서도 말했듯이
이걸 사람이 계속 지켜야 한다는 점이다.</p>
<p>실수로 놓칠 수도 있고,
사람마다 기준이 다르게 적용될 수도 있다.</p>
<p>그래서 등장하는 것이
코드 스타일을 자동으로 맞춰주는 도구다.</p>
<h3 id="prettier">Prettier</h3>
<p><img src="https://velog.velcdn.com/images/hogu__giriboy/post/db633429-67ca-44e1-801b-7e544cb35e34/image.png" alt="Prettier"></p>
<p>프리티어는 코드 포맷터다.</p>
<p>쉽게 말하면
코드를 ‘자동으로 정리해주는 도구’다.</p>
<p>저장을 하거나 명령을 실행하면
들여쓰기, 줄바꿈, 따옴표 스타일 등을
미리 정해둔 규칙에 맞게 자동으로 수정해준다.</p>
<p>덕분에 사람이 일일이 신경 쓰지 않아도
코드 스타일이 항상 동일하게 유지된다.</p>
<h3 id="eslint">ESLint</h3>
<p><img src="https://velog.velcdn.com/images/hogu__giriboy/post/9d7ddda6-5c46-4f1e-8d44-58b42eca2d7c/image.png" alt="ESLint"></p>
<p>ESLint는 코드 검사 도구다.</p>
<p>단순히 스타일을 맞추는 것을 넘어서
코드가 정해진 규칙을 지키고 있는지 확인해준다.</p>
<p>예를 들어
사용하지 않는 변수, 잘못된 문법, 위험한 코드 패턴 등을
미리 잡아낼 수 있다.</p>
<p>즉, 프리티어가
코드를 ‘보기 좋게’ 만들어준다면,</p>
<p>ESLint는
코드를 ‘올바르게’ 유지하도록 도와준다.</p>
<hr>
<h2 id="3-규칙을-지키게-만드는-도구">3. 규칙을 ‘지키게 만드는’ 도구</h2>
<p>드디어 이 파트다.
사실 이 파트가 이번 포스팅의 메인이다.</p>
<p>앞에서 Prettier와 ESLint를 통해
코드 스타일과 규칙을 자동으로 맞출 수 있다는 걸 알게 됐다.</p>
<p>이제 더 나아가
그걸 항상 지키게 만들어야 할 때다.</p>
<p>아무리 좋은 규칙이 있어도
사람이 실수로 놓치거나,
검사를 건너뛰고 넘어가는 상황은 충분히 발생할 수 있다.</p>
<p>결국 중요한 건
규칙을 만드는 것보다, 지키게 만드는 것이다.</p>
<p>이 역할을 해주는 도구가 바로 Husky다.</p>
<h3 id="husky">Husky</h3>
<p><img src="https://velog.velcdn.com/images/hogu__giriboy/post/5fe4eeae-836c-48ba-a625-470f7ffb04f0/image.png" alt="Husky"></p>
<p>Husky는 Git hook을 활용해서
특정 시점에 원하는 작업을 자동으로 실행할 수 있게 해주는 도구다.</p>
<p>예를 들어
코드를 push 하기 전에</p>
<ul>
<li>Prettier 실행</li>
<li>ESLint 검사</li>
</ul>
<p>같은 작업을 자동으로 돌릴 수 있다.</p>
<p>그리고 만약 이 과정에서
컨벤션이 맞지 않거나 오류가 발생하면</p>
<p><strong>push 자체를 막을 수 있다 !</strong></p>
<p>즉, 단순히 “검사해주는 도구”가 아니라
규칙을 지키지 않으면 다음 단계로 못 넘어가게 만드는 장치다.</p>
<hr>
<h2 id="4-더보기">4. 더보기</h2>
<p>앞에서 살펴본 도구들은
협업에서 자주 사용되는 기본적인 도구들이다.</p>
<p>하지만 찾아보면
이 외에도 협업을 더 효율적으로 만들어주는 도구들이 정 말 많다.</p>
<h3 id="github-actions">GitHub Actions</h3>
<p>GitHub Actions는
코드를 push 하거나 PR을 생성했을 때</p>
<ul>
<li>테스트 실행</li>
<li>빌드</li>
<li>배포</li>
</ul>
<p>같은 작업을 자동으로 수행할 수 있게 해주는 도구다.</p>
<p>즉, 사람이 직접 실행하지 않아도
코드가 올라가는 순간 자동으로 검증과 배포가 이루어진다.</p>
<p>이 도구와 밑에서 다룰 Commitlint는 다른 팀장님이 알려줬던 도구는 아닌데,
이 도구가 정말 너무 충격적이었어서 어떤 식으로 흘러가는 건지 더 알아봤다.</p>
<ol>
<li>이벤트 발생</li>
</ol>
<p>먼저 코드 push 혹은 PR 생성과 같은 상황이 생기면 시작된다.</p>
<ol start="2">
<li>가상 컴퓨터를 하나 띄우기</li>
</ol>
<p>GitHub가 아무것도 없는 깨끗한 상태의
&quot;임시 서버&quot;를 하나 만들어준다.</p>
<ol start="3">
<li>코드 가져오기</li>
</ol>
<p>말 그대로 내 레포 코드를 그 서버에 복사한다.</p>
<ol start="4">
<li>테스트 / 검사</li>
</ol>
<p>여기서 패키지 설치, ESLint 검사, 테스트 실행이 이뤄진다.
하나도 실패하면 워크플로우가 중단되고,
PR에 빨간 X 표시가 된다.</p>
<ol start="5">
<li>빌드</li>
</ol>
<p>React는 정적 파일로 변환하고,
Next는 서버 코드와 번들을 생성한다.</p>
<p>즉 서비스로 돌릴 수 있는 형태로 만드는 과정이다.</p>
<ol start="6">
<li>배포 (배포 플랫폼 호출 / 서버에 직접 업로드)</li>
</ol>
<p>이건 방식이 두 가지가 있는데,</p>
<p>Actions가 명령 실행하면 배포 플랫폼이 받아서 배포하는 방식과
직접 서버에 파일 던지는 방식으로 진행된다.</p>
<h3 id="commitlint">Commitlint</h3>
<p>Commitlint는
커밋 메시지가 정해진 규칙을 따르고 있는지 검사하는 도구다.</p>
<p>예를 들어
feat: 로그인 기능 추가
같은 형식을 강제할 수 있다.</p>
<p>이렇게 하면 커밋 로그만 봐도
어떤 작업이 이루어졌는지 한눈에 파악할 수 있다.</p>
<hr>
<h2 id="5-협업-자동화-돌리기">5. 협업: 자동화 돌리기</h2>
<p>앞에서 살펴본 도구들을
실제로 어떻게 연결되는지
얼마나 자동화로 돌려먹을 수 있는지 한 번 정리해보면 이렇다.</p>
<ol>
<li>작업 정의</li>
</ol>
<p>GitHub Issues 생성해서
작업 단위를 나눈다.</p>
<ol start="2">
<li>개발</li>
</ol>
<p>브랜치를 생성하고 코드 작성한다.</p>
<ol start="3">
<li>commit 단계 (1차 필터)</li>
</ol>
<p>Husky를 실행하여 Prettier와 ESLint 자동 실행한다.</p>
<p>문제 있으면 commit 자체가 안 된다.</p>
<ol start="4">
<li>PR 생성</li>
</ol>
<p>협업을 시작한다.</p>
<ol start="5">
<li>GitHub Actions (2차 필터)</li>
</ol>
<p>GitHub Actions 실행하여</p>
<ul>
<li>패키지 설치</li>
<li>lint 검사</li>
<li>테스트 실행</li>
<li>build</li>
</ul>
<p>같은 작업이 자동으로 수행한다.</p>
<p>이 과정에서 하나라도 실패하면
PR에 오류가 표시되고 머지를 할 수 없다.</p>
<ol start="6">
<li>자동 머지 (옵션)</li>
</ol>
<p>조건을 만족하면 자동 merge 가능하다.</p>
<ol start="7">
<li>자동 배포</li>
</ol>
<p>Vercel / Render 연결하여 완전 자동으로 배포하거나
GitHub Actions 안에서 배포까지 진행해 서버까지 직접 제어한다.</p>
<p>협업 도구를 제대로 구성하면,
개발자는 코드 작성과 PR 생성만 하면 되고 나머지 과정은 모두 자동으로 처리된다.</p>
<p>commit 단계에서 한 번,
GitHub Actions에서 한 번 검증을 거친 뒤,
문제가 없을 경우 자동으로 배포까지 이어지는 구조다.</p>
<hr>
<h2 id="6-협업은-결국-흐름을-만드는-일이다">6. 협업은 결국 ‘흐름’을 만드는 일이다</h2>
<p>이번에 협업 도구들을 살펴보면서 느낀 건
협업은 단순히 같이 일하는 것이 아니라,
하나의 흐름을 만드는 과정이라는 점이었다.</p>
<p>이슈로 작업을 나누고,
코드 스타일을 맞추고,
규칙을 자동으로 검사하고,
필요한 순간에 자동으로 배포까지 이루어지는 구조.</p>
<p>이 모든 과정이 연결되면서
하나의 자연스러운 흐름이 만들어진다.</p>
<p>그리고 협업 도구들은
이 흐름을 사람이 아닌
시스템이 관리할 수 있도록 도와준다.</p>
<p>물론 도구만으로 협업이 완성되는 것은 아니다.</p>
<p>하지만 최소한
사람이 계속 확인하고 관리해야 하는 부담을 줄여주고,
더 안정적인 협업 구조를 만들 수 있게 해준다.</p>
<p>우린 이러한 관리 외에도
집중해야 할 일이 많으니까 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Promise로 병렬 처리하기]]></title>
            <link>https://velog.io/@hogu__giriboy/Promise%EB%A1%9C-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hogu__giriboy/Promise%EB%A1%9C-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 22 Apr 2026 13:28:27 GMT</pubDate>
            <description><![CDATA[<h1 id="1-병렬-처리하기">1. 병렬 처리하기</h1>
<p>리스트를 다루는 기능을 구현하면서
생성, 수정, 삭제 요청을 각각 나눠서 처리해야 하는 상황이 있었다.</p>
<p>처음에는 단순하게 생각했다.
요청을 하나씩 순서대로 보내는 것보다,
한 번에 묶어서 처리하면 더 깔끔하고 효율적일 것 같았다.</p>
<p>특히 생성, 수정, 삭제가 모두 비동기 요청이었기 때문에
이걸 병렬로 처리하면 속도도 더 빨라질 거라고 생각했다.</p>
<p>그래서 자연스럽게
여러 작업을 동시에 처리할 수 있는 방법을 찾게 되었고,
그 과정에서 Promise를 떠올리게 됐다.</p>
<p>예전에 한 번 간단하게 배워본 적은 있었지만,
실제 기능 구현에 제대로 써본 건 이번이 처음이었다.</p>
<p>그래서 이번 기회에 생각나서
생성, 수정, 삭제를 모두 Promise로 묶어서 처리했었고,
하면서 발생했던 일들과 함께
Promise를 다시 한 번 짚고 가려고 한다.</p>
<hr>
<h1 id="2-promise-되짚기">2. Promise 되짚기</h1>
<p>Promise는 비동기 작업의 결과를 다루기 위한 객체다.</p>
<p>지금 당장은 결과를 알 수 없지만,
나중에 완료될 작업의 상태를 가지고 있다고 보면 이해하기 편하다.</p>
<p>이 작업은 크게 세 가지 상태로 나뉜다.</p>
<ul>
<li>pending: 아직 처리 중인 상태</li>
<li>fulfilled: 작업이 성공한 상태</li>
<li>rejected: 작업이 실패한 상태</li>
</ul>
<p>이 상태에 따라 이후 실행 흐름이 결정된다.</p>
<p>그래서 Promise를 사용한다는 건 단순히
비동기 코드를 작성하는 게 아니라</p>
<p><strong>작업이 끝난 이후 흐름을 어떻게 이어갈지를 정의하는 것</strong>에 가깝다.</p>
<p>여기까지는 기본적인 개념이고,
내가 Promise를 떠올리게 된 이유는 조금 달랐다.</p>
<p>나는 이걸 “비동기 처리”보다는
여러 요청을 한 번에 처리할 수 있는 방법으로 먼저 인식했다.</p>
<p>생성, 수정, 삭제 요청을 각각 분기해서 진행해야 하니까
한 번에 묶어서 처리하면 더 효율적일 것 같았고,
그 과정에서 자연스럽게 Promise를 사용하게 됐다.</p>
<p>즉, 처음에는 Promise를
병렬 처리를 위한 도구로 생각하고 접근했던 것이다.</p>
<hr>
<h1 id="3-promise와-생긴-일">3. Promise와 생긴 일</h1>
<p>Promise를 사용해서
생성, 수정, 삭제 요청을 한 번에 묶어서 처리했다.</p>
<p>처음에는 별생각 없었다.
코드도 깔끔히 설계했던 대로 분기해서 처리하고,
처리 속도도 병렬로 처리하니까
빨라졌을 것이다.</p>
<p>그런데 이상한 문제가 발생하기 시작했다.</p>
<p>분명 내가 입력한 순서대로 생성 요청을 보냈는데,
실제로 반영된 결과를 보면
생성 순서가 매번 뒤죽박죽으로 섞여 있었다.</p>
<p>처음에는 단순히 정렬 문제라고 생각했다.</p>
<p>그래서 데이터를 가져온 뒤에
sort를 적용해서 순서를 맞춰보기도 했고,
다른 방식으로 정렬 기준을 바꿔보기도 했다.</p>
<p>하지만 아무리 정렬을 바꿔봐도
근본적인 문제는 해결되지 않았다.</p>
<blockquote>
<p>정렬의 문제가 아니라, 생성 자체가 꼬이고 있었던 것이다.</p>
</blockquote>
<p>그렇게 거의 두 시간을 헤매면서,
이게 왜 안 되는지 정확한 원인을 잡지 못한 채
로그만 계속 찍어내고 있었다.
근데 코드를 좀 멀리서 보기 시작하면서 문제가 보였다.</p>
<p>세부적인 로직에서의 문제가 아닌
이걸 어떻게 처리를 하냐로 시야를 멀리 잡았을 때,</p>
<p>그때 문득 떠올랐던 게 Promise였다.</p>
<p>여러 요청을 Promise로 묶어서 실행하면
각 요청이 동시에 실행되고,
먼저 끝난 요청이 먼저 반영되는 구조가 된다.</p>
<p>즉, 내가 보낸 순서와는 상관없이
서버에서 처리되는 순서가 달라질 수 있다는 뜻이었다.</p>
<p>결국 문제는 여기였다.</p>
<p>나는 단순히 요청을 “같이 보냈다”고 생각했지만,
실제로는 각 요청이 독립적으로 경쟁하면서 처리되고 있었던 것이다.</p>
<p>그래서 확인을 위해
생성만 Promise에서 제외하고,
수정과 삭제만 Promise로 처리하도록 구조를 바꿔봤다.</p>
<p>그 결과는 바로 드러났다.</p>
<p>이전까지 계속 꼬이던 생성 순서가
정상적으로 유지되기 시작했다.</p>
<p>그제서야 확실하게 이해할 수 있었다.</p>
<p><strong>생성은 순서가 중요한 작업</strong>이었고,
그걸 병렬로 처리하면서 문제가 발생했던 것이다.</p>
<hr>
<h1 id="4-수정과-삭제와는-다른-생성">4. 수정과 삭제와는 다른 생성</h1>
<p>생성만 Promise에서 제외하고 나서야
왜 문제가 발생했는지가 조금 더 명확하게 보이기 시작했다.</p>
<p>겉으로 보면 생성, 수정, 삭제 모두
같은 “비동기 요청”처럼 보였지만,
실제로는 성격이 완전히 다른 작업이었다.</p>
<p>먼저, 생성은 순서 자체가 의미를 가진다.</p>
<p>사용자가 입력한 순서대로 데이터가 쌓이길 기대하기 때문에
이 순서가 깨지면 결과가 어색해질 수밖에 없다.</p>
<p>반면 수정과 삭제는 다르다.</p>
<p>이미 존재하는 데이터를 대상으로
값을 바꾸거나 제거하는 작업이기 때문에
처리 순서가 크게 중요하지 않다.</p>
<p>즉,
생성은 순서가 중요하고,
수정과 삭제는 이미 순서가 정해져 있으니 크게 중요하지 않았다.</p>
<p>이 차이를 고려하지 않고
모든 요청을 동일하게 Promise로 묶어버린 것이 문제였다.</p>
<hr>
<h1 id="5-이번에-알게-된-점">5. 이번에 알게 된 점</h1>
<p>이번 경험을 통해 하나 분명해진 게 있다.</p>
<blockquote>
<p>Promise는 병렬로 처리해준다고 사용할 도구가 아니라
작업의 성격을 고려해서 선택해야 하는 도구라는 점이다.</p>
</blockquote>
<p>정리해보면 기준은 이렇게 나눌 수 있다.</p>
<p><strong>순서가 중요한 작업</strong>은 <strong>순차적으로 처리</strong>하고
<strong>서로 독립적인 작업</strong>은 <strong>병렬로 처리</strong>한다.</p>
<p>그리고 가장 크게 느꼈던 건 이거였다.
사실 어떻게 보면 간단한 문제다.</p>
<p>그저 생성에서 순서까지는 미처 생각하지 못해
일어난 문제였다.</p>
<p>아무튼 이번에 나에게 있어 Promise는
코드를 병렬로 처리할 수 있도록 하는 도구가 아니라,
실행 흐름을 어떻게 설계할지 결정하는 도구였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[vite.config와 proxy 설정]]></title>
            <link>https://velog.io/@hogu__giriboy/vite.config%EC%99%80-proxy-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@hogu__giriboy/vite.config%EC%99%80-proxy-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Wed, 15 Apr 2026 14:40:34 GMT</pubDate>
            <description><![CDATA[<h2 id="1-viteconfig를-만나게-되다">1. vite.config를 만나게 되다.</h2>
<p>우선 팀 프로젝트에서 API base URL를 정했는데,
우리 팀은 이 URL이 <code>/api/~</code> 로 시작한다.</p>
<p>그래서 fetch를 받을 때 아무 생각도 없이 그냥 <code>/api/~</code> 로 받아버렸다.</p>
<p>연결을 시도해보니 문제가 발생했다.
API 요청을 보냈는데, 원하는 JSON 데이터가 아니라 전혀 다른 응답이 돌아온 거다.</p>
<p>처음에는 백엔드 문제라고 생각했다.</p>
<p>서버가 켜져 있는지 확인하고,
라우팅이 잘못된 건 아닌지 확인해보고.</p>
<p>하지만 서버도 정상 실행에
해당 API 라우트가 제대로 정의되어 있었고
Postman으로 직접 요청을 보내서 응답도 확인해봐도
Postman에서는 정상적으로 JSON 응답이 잘 내려오고 있었다.</p>
<p>그럼에도 불구하고 프론트에서 요청을 보내면 정상적인 응답이 오지 않았다.
네트워크 탭을 열어서 응답을 확인해보니,
내가 받고 있던 건 API 응답이 아니라 HTML 파일이었다.</p>
<p>이 부분에서 해결 방법이 뭔지 찾아보다가,
vite.config 파일과 proxy 설정을 건들게 되었다.</p>
<hr>
<h2 id="2-proxy-설정-알고-계셨나요">2. proxy 설정, 알고 계셨나요?</h2>
<p>Vite 개발 서버는 기본적으로 프론트 요청을 처리하기 때문에,
/api처럼 백엔드로 보내고 싶은 요청은 별도로 전달해주는 설정이 필요하다.</p>
<p>그게 바로 proxy였다.</p>
<pre><code class="language-js">server: {
  proxy: {
    &#39;/api&#39;: &#39;http://localhost:8080&#39;,
  }
}</code></pre>
<p>설명을 보면, <strong>/api로 시작하는 요청을
지정한 백엔드 서버(localhost:8080)로 전달해준다는 의미</strong>였다.</p>
<p>정확한 동작 원리를 이해한 상태는 아니었지만,
“이걸 적용하면 해결된다”는 흐름을 보고 그대로 적용해봤다.</p>
<p>결과는 바로 나타났다.</p>
<p>이전까지 HTML이 돌아오던 요청이
정상적으로 JSON 데이터를 반환하기 시작한 것이다.</p>
<p>이때는 단순히 문제가 해결됐으니 넘어가자는 생각 뿐이었다.</p>
<p>코드 리뷰를 통해 이 부분에 대한 질문을 받으면서,
그제서야 내가 적용한 방식에 대해 다시 생각해보게 됐다.</p>
<p>사실 우리가 지금까지 학습해온 방식은 풀 URL을 사용하는 방식이었고,
현재 팀 프로젝트 환경에서도 proxy 설정 없이 충분히 동작하는 구조였다.</p>
<p>결국 나는 팀 기준에 맞춰 다시 풀 URL 방식으로 코드를 수정했다.</p>
<p>이 과정을 통해 느낀 건,
proxy 설정이 잘못된 접근이었다기보다는
상황에 따라 선택할 수 있는 방식 중 하나였다는 점이다.</p>
<p>지금은 풀 URL을 사용하고 있지만,
이번 경험 덕분에 proxy 설정, 그리고 vite.config에 관해 찾아보게 됐다.</p>
<hr>
<h2 id="3-proxy가-필요한-상황">3. proxy가 필요한 상황</h2>
<p>그럼 proxy 설정은 언제 사용하는 걸까?</p>
<p>이걸 이해하려면 요청이 어떻게 흐르는지를 먼저 생각해봐야 한다.</p>
<p>프론트는 Vite 개발 서버 위에서 동작하고 있고,
내가 보내는 API 요청도 일단 이 서버를 거치게 된다.</p>
<p>이때 /api처럼 상대 경로로 요청을 보내면,
그 요청은 기본적으로 백엔드가 아니라
프론트 개발 서버로 먼저 전달된다.</p>
<p>그리고 Vite는 이 요청을 백엔드로 넘겨주지 않는다.
별도의 설정이 없다면, 해당 요청을 프론트에서 처리하려고 시도하게 된다.</p>
<p>이 상황에서 등장하는 게 proxy 설정이다.</p>
<p>proxy는 특정 경로(/api)로 들어온 요청을
백엔드 서버로 전달해주는 역할을 한다.</p>
<p>즉, </p>
<ul>
<li>/api/... 요청 → 프론트 서버에서 받음</li>
<li>proxy 설정 → 백엔드(localhost:8080)로 전달</li>
</ul>
<p>이 흐름이 만들어지는 것이다.</p>
<p>그래서 proxy는 다음과 같은 상황에서 유용하다.</p>
<ul>
<li>프론트와 백엔드를 각각 다른 서버에서 개발할 때</li>
<li>상대 경로(/api)를 유지하면서 API를 호출하고 싶을 때</li>
<li>개발 환경에서 CORS 문제를 우회하고 싶을 때</li>
</ul>
<p>반대로,</p>
<ul>
<li>풀 URL(<a href="http://localhost:8080/...)%EB%A1%9C">http://localhost:8080/...)로</a> 직접 요청을 보내는 경우라면</li>
<li>proxy 없이도 충분히 동작할 수 있다</li>
</ul>
<p>결국 proxy는 필수 설정이 아니라,
개발 환경에 따라 선택할 수 있는 방식 중 하나라고 볼 수 있다.</p>
<hr>
<h2 id="4-viteconfig에서-추가로-다루는-설정들">4. vite.config에서 추가로 다루는 설정들</h2>
<p>proxy 설정을 적용하면서 처음으로 vite.config 파일을 제대로 찾아보게 됐는데,
생각보다 다양한 설정들을 만질 수 있었다.</p>
<p>그중에서 몇 가지 설정만 간단히 정리해보면 다음과 같다.</p>
<p>먼저, 개발 서버 포트를 지정하는 설정이다.</p>
<pre><code class="language-js">server: {
  port: 5173,
}</code></pre>
<p>기본 포트가 정해져 있긴 하지만,
여러 프로젝트를 동시에 실행할 경우 포트를 직접 지정해야 할 상황이 생길 수 있다.</p>
<p>다음은 서버 실행 시 브라우저를 자동으로 열어주는 설정이다.</p>
<pre><code class="language-js">server: {
  open: true,
}</code></pre>
<p>개발할 때 매번 주소를 입력하는 번거로움을 줄일 수 있어서,
작지만 편의성을 높여주는 설정이다.</p>
<p>또 하나는 import 경로를 정리할 수 있는 alias 설정이다.</p>
<pre><code class="language-js">resolve: {
  alias: {
    &#39;@&#39;: &#39;/src&#39;,
  },
}</code></pre>
<p>상대 경로를 계속 이어서 쓰는 대신,
@ 같은 별칭을 사용해서 경로를 더 깔끔하게 관리할 수 있다.</p>
<p>마지막으로, 배포 시 경로를 설정하는 base 옵션이 있다.</p>
<pre><code class="language-js">base: &#39;/my-app/&#39;,</code></pre>
<p>로컬에서는 크게 체감되지 않지만,
배포 환경에서는 리소스 경로를 맞추기 위해 필요한 설정이다.</p>
<p>이처럼 vite.config는 단순한 설정 파일이 아니라,
개발 환경 전반을 조정하는 역할을 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[페어 프로그래밍]]></title>
            <link>https://velog.io/@hogu__giriboy/%ED%8E%98%EC%96%B4-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</link>
            <guid>https://velog.io/@hogu__giriboy/%ED%8E%98%EC%96%B4-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</guid>
            <pubDate>Tue, 07 Apr 2026 04:45:30 GMT</pubDate>
            <description><![CDATA[<p>이번에 스프린트에서 프로젝트를 진행한다.
계획서를 자잘자잘하게 쓰고 프로젝트를 진행하게 되는데,
거기서 팀장을 맡아버렸다.</p>
<p>그런 나서는 사람이 아니라 굉장한 부담이 함께하는데,
이 또한 성장 중 하나려니 싶어서
부담이고 자시고 모두 안고 가려고 한다.</p>
<p>본론으로 돌아가서 일정 및 R&amp;R을 비롯한 계획서를 작성하고
멘토님께 피드백을 받았는데
멘토님께서 페어 프로그래밍에 대해 언급해주셨다.</p>
<p>그래서 주제로 잡고 한 발자국 나아가려고 한다!</p>
<hr>
<h2 id="1-혼자-개발하는-방식에서-벗어나기">1. 혼자 개발하는 방식에서 벗어나기</h2>
<h3 id="혼자-개발할-때의-한계">혼자 개발할 때의 한계</h3>
<p>처음엔 강의도 혼자 듣고, 과제도 혼자 풀면서
당연히 혼자 개발하는 게 기본이다.</p>
<p>근데 어느 순간부터 이상한 느낌이 온다.</p>
<ul>
<li>이 코드가 맞는지 확신이 없음</li>
<li>더 좋은 방법이 있는지 모르겠음</li>
<li>그냥 &quot;돌아가니까 OK&quot; 상태가 됨</li>
</ul>
<p>이게 반복되면 뭐가 생기냐면
<strong>성장이 멈춘 느낌</strong>을 받게 된다.</p>
<h3 id="한계의-이유">한계의 이유</h3>
<p>혼자 개발하게 되면 아래와 같은 구조를 갖게 된다.</p>
<ol>
<li>내가 생각</li>
<li>내가 작성</li>
<li>내가 검증</li>
</ol>
<p>이게 문제로 작용한다.</p>
<p>누가 틀렸는지 체크해주는 사람이 없고,
다른 시각이 들어올 틈이 없다.</p>
<p>비유해보자면
혼자 시험 보고, 혼자 채점하는 상황이다.
틀려도 모르고, 맞아도 왜 맞는지 모른다.</p>
<p>그래서 등장한 방식이 <strong>페어 프로그래밍</strong>이다.</p>
<p>혼자 하는 게 아니라
<strong>처음부터 같이 생각하고 같이 만드는 방식</strong>이다.</p>
<hr>
<h2 id="2-같이-코딩처럼-보이는-오해">2. 같이 코딩처럼 보이는 오해</h2>
<p>처음 들으면 그냥 이렇게 생각하기 쉽다</p>
<blockquote>
<p>둘이 앉아서 같이 코딩하는 거 아닌가?</p>
</blockquote>
<p>둘이 같은 내용을 각자 나눠서 코딩하는 방식이라면
그건 오히려 비효율적인 중복 작업에 가깝다.</p>
<p>실제로 모습만 보면 비슷하게 오해할 만하다.</p>
<p>하지만 서로의 역할이 확실하게 구분된다.</p>
<p>페어 프로그래밍은 한 명이 키보드를 잡고 코드를 작성하고,
다른 한 명이 옆에서 화면을 보며 함께 진행하는 구조다.</p>
<p>그냥 같이 있는 게 아니라 역할이 나뉘어 있다는 것이 포인트다.</p>
<h3 id="driver--navigator-역할">Driver / Navigator 역할</h3>
<p>페어 프로그래밍은 기본적으로 역할이 두 개다.</p>
<ul>
<li>Driver → 직접 코드 작성하는 사람</li>
<li>Navigator → 옆에서 방향 잡는 사람</li>
</ul>
<p>여기서 많이 오해하는 게 있는데</p>
<p>Driver는 단순히 코드만 작성하는 사람이 아니라</p>
<blockquote>
<p><strong>왜 이렇게 작성하는지 설명하면서</strong> 코드를 짜야 한다.</p>
</blockquote>
<p>Navigator도 그냥 구경하는 사람이 아니고</p>
<ul>
<li>이 방식이 맞는지</li>
<li>더 좋은 방법이 있는지</li>
<li>놓친 부분이 없는지</li>
</ul>
<p>이걸 계속 생각해야 하는 역할이다.</p>
<h3 id="중요한-건-역할이-아니라-생각의-흐름">중요한 건 역할이 아니라 &quot;생각의 흐름&quot;</h3>
<p>여기서 진짜 중요한 포인트가 하나 있다.</p>
<p>둘이 코드를 나눠서 만드는 게 아니라</p>
<blockquote>
<p><strong>생각을 같이 끌어내는 구조</strong>다.</p>
</blockquote>
<p>혼자 할 때는 머릿속에서만 생각하고 끝났던 것들이
말로 설명되고 바로 피드백 받고
다시 수정되는 흐름이 생긴다.</p>
<p>이게 핵심이다.</p>
<h3 id="이런-구조로-인한-변화">이런 구조로 인한 변화</h3>
<p>이 구조 때문에 생기는 변화가 있다.</p>
<ul>
<li>애매하게 알고 있던 게 바로 들킨다.</li>
<li>그냥 감으로 짜던 코드가 설명 가능한 코드로 바뀐다.</li>
<li>선택 하나하나에 이유가 붙기 시작한다.</li>
</ul>
<blockquote>
<p>결국 코드 퀄리티보다 먼저
<strong>생각하는 방식 자체가 바뀐다.</strong></p>
</blockquote>
<hr>
<h2 id="3-실제-진행-방식">3. 실제 진행 방식</h2>
<h3 id="한-기능을-같이-끝까지">한 기능을 같이 끝까지</h3>
<p>페어 프로그래밍은
중간에 나눠서 하는 방식이 아니다.</p>
<blockquote>
<p>하나의 기능을 처음부터 끝까지 같이 만든다.</p>
</blockquote>
<p>예를 들어
상품 등록 기능을 하나 만든다면
API 설계부터 UI까지
전부 같이 진행하는 구조다.
이 과정 자체가 페어 프로그래밍의 핵심이다.</p>
<h3 id="시작은-항상-같이-생각부터">시작은 항상 &quot;같이 생각&quot;부터</h3>
<p>바로 코드부터 치지 않는다.</p>
<p>먼저 어떻게 만들지, 어떤 방식이 좋을지
같이 이야기하면서 방향을 잡는다.</p>
<p>이 과정이 없으면
그냥 둘이 앉아 있는 혼자 개발이랑 다를 게 없다.</p>
<h3 id="코드-작성도-혼자가-아니라-같이">코드 작성도 혼자가 아니라 같이</h3>
<p>코드를 작성하는 순간에도</p>
<blockquote>
<p>한 명만 생각하는 게 아니다.</p>
</blockquote>
<p>코드를 작성하면서
왜 이렇게 짜는지 설명하고
옆에서는 계속 질문하고 체크한다.</p>
<p>이게 계속 반복된다.</p>
<h3 id="중간중간-계속-수정되는-방향">중간중간 계속 수정되는 방향</h3>
<p>혼자 개발할 때는
한 번 정한 방향을 끝까지 밀고 가는 경우가 많다.</p>
<p>근데 페어 프로그래밍은 다르다.</p>
<ul>
<li>더 좋은 방법이 나오면 바로 바꾸고</li>
<li>놓친 부분이 있으면 바로 잡는다.</li>
</ul>
<p>즉
<strong>계속 방향을 조정하면서 진행한다.</strong></p>
<h3 id="처음-하면-느껴지는-특징">처음 하면 느껴지는 특징</h3>
<p>처음 하면 대부분 이렇게 느낀다.</p>
<ul>
<li>느리다</li>
<li>답답하다</li>
<li>혼자 하는 게 더 빠르다.</li>
</ul>
<p>근데 이게 정상이다.</p>
<p>왜 이렇게 만드는지 이해하고
그 과정을 공유하는 게 목적이다.</p>
<p>그래서 결과적으로는
코드를 만드는 과정 자체가 달라진다.</p>
<hr>
<h2 id="4-해보면-얻을-수-있는-것들">4. 해보면 얻을 수 있는 것들</h2>
<h3 id="강제로-넓어지는-시야">강제로 넓어지는 시야</h3>
<p>혼자 개발할 때는
내가 아는 방식 안에서만 선택하게 된다.</p>
<p>근데 페어 프로그래밍을 하면
다른 사람이 생각하는 방식을 계속 보게 된다.</p>
<ul>
<li>같은 문제를 다르게 접근하는 방식</li>
<li>내가 생각 못한 구조</li>
<li>더 단순한 해결 방법</li>
</ul>
<p>이게 계속 들어온다.</p>
<p>그래서 자연스럽게
<strong>&quot;내 방식 하나&quot;에서 &quot;여러 선택지&quot;로 바뀐다.</strong></p>
<h3 id="바로-들춰지는-애매한-이해">바로 들춰지는 애매한 이해</h3>
<p>혼자 할 때는</p>
<ul>
<li>대충 아는 상태</li>
<li>감으로 짠 코드</li>
</ul>
<p>이게 그냥 넘어간다.</p>
<p>근데 페어 프로그래밍은</p>
<ul>
<li>왜 이렇게 짰는지</li>
<li>이게 어떻게 동작하는지</li>
</ul>
<p>이걸 말로 풀어야 한다.</p>
<p>즉 설명을 해야 한다.</p>
<p>이 순간
<strong>모르면 바로 티가 난다.</strong></p>
<h3 id="코드에-이유가-생기기-시작한다">코드에 이유가 생기기 시작한다.</h3>
<p>혼자 개발하면
&quot;일단 되게 만드는 코드&quot;가 많다.</p>
<p>근데 페어로 하면</p>
<ul>
<li>이 구조 왜 썼는지</li>
<li>이 선택이 맞는지</li>
</ul>
<p>계속 확인을 받는다.</p>
<p>그래서 모든 코드에 이유가 붙기 시작한다.</p>
<h3 id="줄어드는-실수">줄어드는 실수</h3>
<p>혼자 개발하면
내가 놓친 건 끝까지 놓친다.</p>
<p>근데 둘이 보면
한 명이 놓친 걸 다른 한 명이 잡는다.</p>
<p>이게 생각보다 크다.</p>
<h3 id="결국-바뀌는-건-코드가-아닌-사고방식">결국 바뀌는 건 코드가 아닌 사고방식</h3>
<p>페어 프로그래밍을 하면
단순히 코드 퀄리티가 좋아지는 게 아니라
<strong>생각하는 방식 자체가 바뀐다.</strong></p>
<p>설명 가능한 코드만 쓰게 되고
선택에 근거를 붙이게 되고
더 나은 방법을 계속 고민하게 된다.</p>
<p>이게 핵심이다.</p>
<hr>
<h2 id="5-페어-프로그래밍-시작하기">5. 페어 프로그래밍 시작하기</h2>
<h3 id="처음부터-완벽-x">처음부터 완벽 X</h3>
<p>페어 프로그래밍이라고 해서
규칙 완벽하게 지키고
역할 정확하게 나누고
이상적인 방식으로 시작하려고 하면</p>
<p>시작도 못한다.</p>
<p>처음은 그냥
<strong>&quot;같이 해본다&quot; 수준이면 충분하다.</strong></p>
<h3 id="기능-하나-잡고-같이-해보는-게-시작">기능 하나 잡고 같이 해보는 게 시작</h3>
<p>가장 현실적인 시작 방법은 이거다.</p>
<ul>
<li>작은 기능 하나 정한다.</li>
<li>둘이 같이 붙어서 구현해본다.</li>
</ul>
<p>예를 들어</p>
<ul>
<li>상품 목록 가져오기</li>
<li>검색 기능</li>
<li>간단한 폼 처리</li>
</ul>
<p>이런 것부터 시작하면 된다.</p>
<h3 id="처음엔-어색한-게-정상">처음엔 어색한 게 정상</h3>
<p>처음 하면 무조건 이상하다.</p>
<p>말하면서 코딩하는 게 어색하고
옆에서 보는 것도 어색하고
서로 타이밍도 안 맞는다.</p>
<p>이게 정상이다.</p>
<h3 id="속도보다-같이-이해하는-경험에-집중">속도보다 &quot;같이 이해하는 경험&quot;에 집중</h3>
<p>혼자 하면 더 빠른 건 사실이다.</p>
<p>근데 페어 프로그래밍은
<strong>빠르게 만드는 게 목적이 아니다.</strong></p>
<p>왜 이렇게 만드는지
서로 어떻게 생각하는지
이걸 맞춰가는 과정이 핵심이다.</p>
<h3 id="부담을-가지는-순간-오히려-어려워진다">부담을 가지는 순간 오히려 어려워진다.</h3>
<p>많이들 여기서 막힌다.</p>
<ul>
<li>&quot;내가 못하면 어떡하지?&quot;</li>
<li>&quot;민폐 아닐까?&quot;</li>
</ul>
<p>근데 이 구조 자체가
서로 부족한 걸 드러내는 구조다.</p>
<p>그래서 오히려
모르는 걸 말하는 게 맞는 방향이다.</p>
<h3 id="결국-중요한-건-경험">결국 중요한 건 경험</h3>
<p>페어 프로그래밍은</p>
<blockquote>
<p>설명으로 이해하는 게 아니라
해봐야 이해된다.</p>
</blockquote>
<p>한 번만 제대로 해보면
왜 느린지 왜 필요한지
뭐가 달라지는지 바로 체감된다.</p>
<hr>
<p>실제로 스프린트 강사님이 따라오기 어려운 인원들을 모아
각자의 화면을 공유하면서 함께 실습을 진행하시는데</p>
<p>나는 내 모습이 드러나는 게 부담스러워 쉽게 참여하지 못했다.
그런데 돌아보니 그 방식이
페어 프로그래밍과 꽤 비슷했다.</p>
<p>결국에 내가 애매하게 알고 있는 모습들을
다 들춰내기 위한 방식이었고
내 애매함이 들춰지면 그건 또 성장으로 이어질 것이다.</p>
<p>그래서 멘토님이 페어 프로그래밍 방식을 추천하셨던 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[useState vs useSearchParams]]></title>
            <link>https://velog.io/@hogu__giriboy/useState-vs-useSearchParams</link>
            <guid>https://velog.io/@hogu__giriboy/useState-vs-useSearchParams</guid>
            <pubDate>Tue, 31 Mar 2026 02:39:24 GMT</pubDate>
            <description><![CDATA[<p>이번에 스프린트 미션으로 마켓 상품 정렬 및 검색 기능을 다뤘는데,
useState를 주로 써서 useState를 활용해서 작성했다.</p>
<p>그러던 와중에 궁금한 점이 하나 생겼다.</p>
<pre><code class="language-javascript">...
const [page, setPage] = useState(1);
const [orderBy, setOrderBy] = useState(&quot;recent&quot;);
const [keyword, setKeyword] = useState(&quot;&quot;);

useEffect(() =&gt; {
  async function loadProducts() {
    const res = await fetch(
      `https://.../products?page=${page}&amp;pageSize=${pageSize}&amp;orderBy=${orderBy}&amp;keyword=${keyword}`
    );
  }

  loadProducts();
}, [page, pageSize, orderBy, keyword]);
...</code></pre>
<p>위 코드는 useState를 활용한 코드다.
근데 이제 위 코드를
useSearchParams를 활용한 코드로 바꿔보면</p>
<pre><code class="language-javascript">...
const [searchParams, setSearchParams] = useSearchParams();

const page = Number(searchParams.get(&quot;page&quot;)) || 1;
const orderBy = searchParams.get(&quot;orderBy&quot;) || &quot;recent&quot;;
const keyword = searchParams.get(&quot;keyword&quot;) || &quot;&quot;;

useEffect(() =&gt; {
  async function loadProducts() {
    const res = await fetch(
      `/products?page=${page}&amp;orderBy=${orderBy}&amp;keyword=${keyword}`
    );
  }

  loadProducts();
}, [page, orderBy, keyword]);
...</code></pre>
<p>이런 식으로 쓰이고,
<code>fetch</code> 요청 형식이 똑같다.
그래서 의문이 들었던 게 있었다.</p>
<blockquote>
<p>이러면 useSearchParams는 굳이 왜 있는거지?</p>
</blockquote>
<p>그래서 옳거니 하고 바로 블로그 주제로 잡았다.</p>
<hr>
<h2 id="1-요청-코드가-아니라-상태의-위치-차이">1. 요청 코드가 아니라 상태의 위치 차이</h2>
<p>useState를 사용하든,
useSearchParams를 사용하든,
결국 fetch 요청을 보면 이렇게 된다.</p>
<pre><code class="language-js">/products?page=${page}&amp;orderBy=${orderBy}&amp;keyword=${keyword}</code></pre>
<p>서버에서 보내는 요청 형식은 똑같다.
그래서 처음에는 이렇게 생각했다.</p>
<blockquote>
<p>둘 다 결국 같은 요청을 보내는 거면
그냥 useState만 써도 되는 거 아닌가?</p>
</blockquote>
<p>그런데 여기서 중요한 건
<strong>요청 형식이 아니라 값이 어디에서 관리되고 있느냐</strong>였다.</p>
<h3 id="usestate로-관리하는-경우">useState로 관리하는 경우</h3>
<pre><code class="language-js">const [page, setPage] = useState(1);
const [orderBy, setOrderBy] = useState(&quot;recent&quot;);
const [keyword, setKeyword] = useState(&quot;&quot;);</code></pre>
<p>이 값들은 어디에 저장되어 있을까?</p>
<p>정답은 <strong>컴포넌트 내부 상태</strong>다.</p>
<p>즉 이 값들은 이 컴포넌트가 살아있는 동안만 존재하고
페이지를 새로고침하면 초기화되고
URL 주소에는 아무 정보도 남지 않는다.</p>
<p>이 상태는 <strong>컴포넌트 내부에서만 유지되는 상태</strong>다.</p>
<h3 id="usesearchparams로-관리하는-경우">useSearchParams로 관리하는 경우</h3>
<pre><code class="language-js">const [searchParams, setSearchParams] = useSearchParams();

const page = Number(searchParams.get(&quot;page&quot;)) || 1;
const orderBy = searchParams.get(&quot;orderBy&quot;) || &quot;recent&quot;;
const keyword = searchParams.get(&quot;keyword&quot;) || &quot;&quot;;</code></pre>
<p>이번에는 값이 어디에 있을까?</p>
<p>이 값들은 <strong>컴포넌트 내부 상태가 아니라 URL 쿼리스트링에 저장</strong>되어 있다.</p>
<p>주소창을 보면 이렇게 바뀐다.</p>
<pre><code class="language-text">/products?page=2&amp;orderBy=price&amp;keyword=phone</code></pre>
<p>즉 상태가 <strong>컴포넌트가 아니라 URL에 존재</strong>하게 된다.</p>
<p>정리하면 차이는 fetch 요청 코드가 아니라 상태가 어디에 저장되는지에 있다.</p>
<ul>
<li>useState → 상태가 컴포넌트 안에서 관리됨</li>
<li>useSearchParams → 상태가 URL 쿼리스트링에서 관리됨</li>
</ul>
<p>이 차이 때문에 페이지 동작 방식이 완전히 달라진다.</p>
<ul>
<li>새로고침</li>
<li>링크 공유</li>
<li>뒤로가기</li>
<li>앞으로가기</li>
<li>북마크</li>
<li>탭 복사</li>
</ul>
<p>이 모든 동작이 <strong>URL을 기준으로 동작하기 때문</strong>이다.</p>
<p>그래서 검색, 정렬, 페이지 번호 같은 값들은
컴포넌트 상태보다 URL 쿼리스트링으로 관리하는 것이 더 자연스럽다.</p>
<hr>
<h2 id="2-컴포넌트-안에만-있으면-생기는-문제">2. 컴포넌트 안에만 있으면 생기는 문제</h2>
<p>앞에서 정리한 것처럼</p>
<ul>
<li>useState → 상태가 컴포넌트 안에서 관리됨</li>
<li>useSearchParams → 상태가 URL 쿼리스트링에서 관리됨</li>
</ul>
<p>여기까지 보면 단순히 저장 위치만 다른 것처럼 보일 수 있다.
그런데 이 저장 위치 차이가 실제 동작에서 큰 차이를 만든다.</p>
<p>특히 검색, 정렬, 페이지 번호 같은 값들은
컴포넌트 안에만 있으면 여러 문제가 생긴다.</p>
<h3 id="새로고침하면-사라지는-상태">새로고침하면 사라지는 상태</h3>
<p>예를 들어</p>
<ol>
<li>검색어 입력</li>
<li>정렬 선택</li>
<li>3페이지까지 이동</li>
<li>새로고침</li>
</ol>
<p>useState로 관리하고 있었다면 상태는 전부 초기화된다.</p>
<ul>
<li>검색어 → 초기화</li>
<li>정렬 → 초기화</li>
<li>페이지 → 1페이지로 돌아감</li>
</ul>
<p>왜냐하면 useState는 <strong>컴포넌트 내부 메모리에 있는 상태</strong>라서
페이지가 새로 로드되면 다시 처음부터 시작하기 때문이다.</p>
<p>반대로 URL에 이렇게 남아 있다면</p>
<pre><code class="language-text">/products?page=3&amp;orderBy=price&amp;keyword=phone</code></pre>
<p>새로고침을 해도
브라우저는 같은 URL을 다시 요청하고,
컴포넌트는 URL에서 값을 다시 읽어오면 된다.</p>
<p>즉 상태가 유지된다.</p>
<h3 id="링크-공유-불가">링크 공유 불가</h3>
<p>이건 실제 서비스에서 중요한 부분이다.</p>
<p>예를 들어 어떤 상품을 검색해서
필터 + 정렬 + 페이지 이동까지 한 상태라고 해보자.</p>
<pre><code class="language-text">검색어: phone
정렬: 가격순
페이지: 3</code></pre>
<p>이 화면을 다른 사람에게 보여주고 싶어서
주소를 복사해서 보내면 어떻게 될까?</p>
<ul>
<li>useState → 상대방은 기본 화면이 뜬다</li>
<li>useSearchParams → 같은 검색 결과 화면이 뜬다</li>
</ul>
<p>왜냐하면 상태가 컴포넌트 안에 있느냐,
URL에 있느냐의 차이이기 때문이다.</p>
<p>그래서 실제 쇼핑몰, 게시판, 검색 페이지 같은 곳에서는
검색 조건, 정렬, 페이지 번호를 URL 쿼리스트링으로 관리하는 경우가 많다.</p>
<h3 id="뒤로가기--앞으로가기-동작-불가">뒤로가기 / 앞으로가기 동작 불가</h3>
<p>페이지 이동 흐름을 생각해보자</p>
<ul>
<li>1페이지 + 2페이지 → 3페이지</li>
</ul>
<p>이 상태에서 뒤로가기를 누르면
사용자는 보통 이렇게 기대한다.</p>
<ul>
<li>3페이지 → 뒤로가기 → 2페이지</li>
</ul>
<p>그런데 상태가 useState에만 있으면
브라우저는 상태 변화를 기억하지 못한다.</p>
<p>왜냐하면 브라우저는 <strong>URL 이동 기록</strong>만 기억하기 때문이다.</p>
<p>반대로 URL이 이렇게 바뀌고 있었다면</p>
<pre><code class="language-text">/products?page=1
/products?page=2
/products?page=3</code></pre>
<p>뒤로가기를 누르면 URL이 바뀌고,
우리는 URL에서 page 값을 다시 읽으면 된다.</p>
<p>그래서 뒤로가기와 앞으로가기 동작이 자연스럽게 동작한다.</p>
<p>검색, 정렬, 페이지 번호 같은 값들은
단순히 화면을 위한 상태가 아니라
현재 어떤 페이지 상태를 보고 있는지를 설명하는 상태다.</p>
<p>그래서 이 값들은 컴포넌트 상태가 아니라 페이지 상태라고 볼 수 있다.</p>
<p>그리고 페이지 상태는
컴포넌트 안이 아니라
<strong>URL과 함께 움직이는 것이 더 자연스럽다.</strong></p>
<p>그래서 여기서 useSearchParams를 사용하게 되는 거다.</p>
<p>즉 useSearchParams는 단순히 값을 저장하는 도구가 아니라,
페이지 상태를 URL과 동기화하기 위한 도구라고 볼 수 있다.</p>
<hr>
<h2 id="3-usesearchparams의-존재-이유">3. useSearchParams의 존재 이유</h2>
<p>앞에서 정리한 내용을 한 번 이어서 생각해보자.</p>
<p>useState로 관리해도 fetch 요청은 만들 수 있다.
그래서 처음에는 useSearchParams가 왜 필요한지 잘 느껴지지 않을 수 있다.
그런데 검색, 정렬, 페이지 번호는 화면 상태가 아니라 페이지 상태다.
페이지 상태는 URL과 함께 관리되는 것이 더 자연스럽다.
그래서 이런 페이지 상태를 관리할 때 useSearchParams를 사용한다.</p>
<p>여기서 중요한 건</p>
<blockquote>
<p>useSearchParams는 fetch 요청을 쉽게 만들기 위한 훅이 아니다.
URL에 상태를 저장하기 위한 훅이다.</p>
</blockquote>
<p>이게 거의 이 글의 핵심이다.</p>
<h3 id="usestate-vs-usesearchparams">useState vs useSearchParams</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>useState</th>
<th>useSearchParams</th>
</tr>
</thead>
<tbody><tr>
<td>상태 저장 위치</td>
<td>컴포넌트 내부 상태</td>
<td>URL 쿼리스트링</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>
<tr>
<td>사용 용도</td>
<td>UI 상태</td>
<td>페이지 상태</td>
</tr>
</tbody></table>
<p>여기서 가장 중요한 차이는
useState는 화면(UI)을 위한 상태를 저장하고,
useSearchParams는 페이지 상태를 저장한다는 점이다.</p>
<h3 id="언제-무엇을-쓸까">언제 무엇을 쓸까</h3>
<h4 id="usestate를-쓰는-경우">useState를 쓰는 경우</h4>
<ul>
<li>모달 열림 / 닫힘</li>
<li>드롭다운 열림 / 닫힘</li>
<li>input 입력값</li>
<li>탭 선택</li>
<li>체크박스</li>
<li>토글 버튼</li>
<li>hover 상태</li>
</ul>
<p><strong>→ 화면(UI) 안에서만 필요한 상태</strong></p>
<h4 id="usesearchparams를-쓰는-경우">useSearchParams를 쓰는 경우</h4>
<ul>
<li>검색어</li>
<li>정렬 방식</li>
<li>페이지 번호</li>
<li>필터 조건</li>
<li>카테고리 선택</li>
<li>보기 방식 (grid/list)</li>
</ul>
<p><strong>→ 현재 어떤 페이지 상태를 보고 있는지 설명하는 상태</strong></p>
<p>여기서 기준 문장 하나로 정리하면 좋다.</p>
<blockquote>
<p>이 값은 화면 안에서만 필요한 값인가?
아니면 현재 페이지를 설명하는 값인가?</p>
</blockquote>
<p>이 질문으로 대부분의 경우 어떤 상태 관리 방법을 써야 할지 결정할 수 있다.</p>
<p>useState와 useSearchParams의 차이는 기능의 차이가 아니라 상태를 저장하는 위치의 차이다.</p>
<p>컴포넌트 내부 상태는 useState로 관리하고,
페이지 상태는 useSearchParams로 관리한다.</p>
<hr>
<p>처음에는 useState로도 충분히 동작하는 것처럼 보여서 주제를 잡았다.
fetch 요청만 보면 useState와 useSearchParams는 큰 차이가 없어 보이기도 한다.</p>
<p>하지만 차이는 요청 방식이 아니라 상태가 어디에 저장되는지에 있었다.
검색, 정렬, 페이지 번호 같은 값들은 단순히 화면을 위한 상태가 아니라
현재 어떤 페이지 상태를 보고 있는지를 설명하는 상태였다.</p>
<p>그래서 이런 값들은 컴포넌트 안이 아니라 URL과 함께 관리되어야 했고,
그 역할을 하는 것이 useSearchParams였다.</p>
<p>결국 React에서 상태를 관리할 때 중요한 것은
어떤 훅을 사용할 것인가가 아니라
이 상태를 어디에 저장해야 하는지를 먼저 생각하는 것이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI "잘" 쓰기]]></title>
            <link>https://velog.io/@hogu__giriboy/AI-%EC%9E%98-%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@hogu__giriboy/AI-%EC%9E%98-%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Mon, 23 Mar 2026 06:17:12 GMT</pubDate>
            <description><![CDATA[<p>최근에 개발 분야가 아닌 그냥 전기쪽 분야에서 일하는 지인이
좋은 기회로 AI 사용법에 대해 강의를 듣게 됐다고 연락이 왔다.</p>
<p>본인도 좋게 듣긴 했지만,
본인보다 필자한테 더욱 도움이 될 거 같다며
자료 공유 차원에서 연락을 했던 것이었다.</p>
<p>AI를 똑똑하게 만드는 건
얼마나 비싼 AI를 쓰는가도 물론 영향이 있겠지만,
그보다도 더 중요한 건 얼마나 똑똑하게 질문을 하는가라는 것은 알고 있었다.
하지만 그에 맞는 프롬프트 작성법을 따로 자세히 알고 있는 건 아니었다.</p>
<p>그저 한 세션에 정보가 너무 많이 들어가면
세션이 느려지고 답변 품질이 떨어진다는 것 정도만 알고 있어서
세션을 프로젝트별로 나눠 관리하는 정도만 하고 있었다.
처음 질문을 어떻게 해야 하는지까지는 깊게 생각해본 적이 없었던 것 같다.</p>
<p>그래서 자료를 공유받은 김에
프롬프트 작성 방법에 대해 블로그로 정리해보고,
나 스스로도 한 번 정리해보려고 한다.</p>
<h2 id="1-ai는-무엇인가-llm">1. AI는 무엇인가: LLM</h2>
<p>AI를 잘 쓰는 사람이랑 못 쓰는 사람 차이 중 하나가
<strong>AI를 사람처럼 생각하느냐 / 프로그램처럼 생각하느냐</strong> 차이다.</p>
<p>나는 사실 나름 세션별로도 관리하고
AI에게 했던 교육을 또 하는 걸 방지하는 것만으로도
충분히 잘 쓰고 있다고 생각했지만,
저 차이를 보고 머릿속에 고래가 한마리 지나갔다.</p>
<blockquote>
<p>아, 나는 AI를 사람처럼 대하고 있었구나.</p>
</blockquote>
<p>AI는 생각하는 존재가 아니다.
기억하는 존재도 아니다.
이해하는 존재도 아니다.</p>
<p>AI는 그저 문장을 그럴듯하게 이어 붙이는 프로그램이다.</p>
<p>사실 저게 당연한 거겠지만
항상 나는 내가 하고 싶은 것과 그걸 이루기 위해 AI가 해줘야 하는 것을
되게 정말 친구를 설득하듯이 주절주절 말했던 것 같다.</p>
<h3 id="llm">LLM?</h3>
<p>LLM은
지금까지 입력된 토큰을 기반으로
다음 토큰이 등장할 확률을 계산하고,
그 확률 분포를 바탕으로 다음 토큰을 선택하는 모델이다.</p>
<p>쉽게 말해
지금까지 나온 단어들을 보고
다음 단어로 올 확률이 가장 높은 단어를 고르는 것이다.</p>
<p>예를 들어 <code>나는 오늘 아침에 밥을 먹고 학교에 ___</code>를 보자.</p>
<p>AI는 여기 빈칸에 올 단어 다음과 같다고 계산한다.</p>
<ul>
<li>갔다 - 60%</li>
<li>왔다 - 15%</li>
<li>잤다 - 5%</li>
<li>뛰었다 - 3%</li>
<li>울었다 - 1%</li>
</ul>
<p>이 확률 분포를 기반으로 하나의 단어(토큰)을 샘플링하여 선택한다.</p>
<p>이걸 한 번만 하는 게 아니라
<strong>문장이 끝날 때까지 계속 반복한다.</strong></p>
<p>그래서 AI는 사실 이렇게 동작한다.</p>
<ol>
<li>단어</li>
<li>다음 단어 확률 계산</li>
<li>선택</li>
<li>다음 단어 확률 계산</li>
<li>선택</li>
<li>다음 단어 확률 계산</li>
<li>...</li>
<li>문장 완성</li>
</ol>
<p>이게 ChatGPT의 본질이다.</p>
<h3 id="그래서-생기는-ai-특징">그래서 생기는 AI 특징</h3>
<ol>
<li>질문을 다르게 하면 답이 달라진다.</li>
</ol>
<p>왜냐면 앞 문장이 달라지면
<strong>다음 단어 확률이 달라지기 때문이다.</strong></p>
<ol start="2">
<li>같은 질문을 해도 답이 조금씩 다를 수 있다.</li>
</ol>
<p>이는 확률 분포에서 토큰을 샘플링하는 방식으로 문장을 생성하기 때문이다.</p>
<ol start="3">
<li>틀린 말도 그럴듯하게 말한다 (할루시네이션)</li>
</ol>
<p>AI는 사실 여부를 판단하는 시스템이 아니라
확률적으로 <strong>그럴 듯한 문장을 생성하는 모델</strong>이기 때문에
틀린 내용을 생성할 수도 있다.</p>
<p>그래서 AI 쓸 때 제일 중요한 습관은 검증이다.</p>
<p>이건 저번 데브필 강연자분이 하셨던
&quot;AI 시대에 신입 개발자로 성장하기&quot; 특강에서도
중요시 여기셨던 포인트다.</p>
<p>AI는 항상 맞다고 생각하면 안 된다.
중요한 정보는 검증해야 한다.</p>
<hr>
<h2 id="2-프롬프트의-중요성">2. 프롬프트의 중요성</h2>
<p>앞서 AI는 질문을 이해하는 것이 아니라
<strong>앞 문장을 보고 다음 문장을 생성하는 프로그램</strong>이라고 했는데,
그 말은 즉슨</p>
<blockquote>
<p>질문을 어떻게 쓰느냐에 따라
AI가 만들어낼 문장이 완전히 달라진다.</p>
</blockquote>
<p>그래서 프롬프트가 중요해진다.</p>
<h3 id="좋은-프롬프트-vs-나쁜-프롬프트">좋은 프롬프트 vs 나쁜 프롬프트</h3>
<p>예를 들어 AI에게 이렇게 질문할 수 있다.</p>
<pre><code class="language-text">클로저 설명해줘</code></pre>
<p>이 질문도 틀린 질문은 아니지만,
AI에게는 정보가 너무 부족하다.</p>
<p>반대로 이렇게 프롬프트를 작성할 수도 있다.</p>
<pre><code class="language-text">너는 자바스크립트 강사다.
나는 자바스크립트를 공부하는 학생이다.
클로저 개념을 이해하기 쉽게 설명해줘.
비유 하나와 코드 예시를 포함해주고,
마지막에 핵심 정리로 정리해줘.</code></pre>
<p>같은 내용을 요청했지만
프롬프트의 정보량과 요구사항이 완전히 다르다.</p>
<p>AI는 질문을 이해하는 것이 아니라
입력된 문맥을 기반으로 문장을 생성하기 때문에
<strong>프롬프트를 어떻게 작성하느냐에 따라 결과의 품질이 크게 달라진다.</strong></p>
<p>그래서 AI를 잘 사용하기 위해서는 프롬프트를 작성하는 능력이 중요한데,
<strong>프롬프트 작성은 곧 요구사항 정리</strong>이기 때문에
요구사항을 명확하게 정리하는 데 익숙한 개발자가
AI를 잘 활용할 수 있는 직군이 될 가능성이 높다.</p>
<p>그렇다면 프롬프트는 어떻게 작성해야
원하는 결과에 가까운 답을 얻을 수 있을까?</p>
<hr>
<h2 id="3-프롬프트를-잘-작성하는-방법">3. 프롬프트를 잘 작성하는 방법</h2>
<p>여기서 중요한 건
<strong>이론 이름 외우는 게 아니라 &quot;패턴&quot;을 이해하는 것</strong>이다.</p>
<p>프롬프트를 잘 쓰는 사람은
엄청 어려운 걸 쓰는 게 아니라
<strong>정보를 많이 주고, 상황을 많이 설명하고, 결과 형태를 지정</strong>하는 사람이다.</p>
<h3 id="프롬프트-잘-쓰는-기본-구조">프롬프트 잘 쓰는 기본 구조</h3>
<p>프롬프트를 작성할 때 자주 쓰는 기본 구조에 가깝다.</p>
<ol>
<li>역할 지정</li>
<li>상황 설명</li>
<li>해야 할 작업</li>
<li>출력 형식</li>
<li>제약 조건</li>
</ol>
<p>이걸 하나로 합쳐보면 이런 느낌이다.</p>
<pre><code class="language-text">너는 프론트엔드 강사다.
나는 자바스크립트를 공부하는 학생이다.
클로저 개념을 이해하기 쉽게 설명해줘.
비유 하나와 코드 예시를 포함해줘.
마지막에 핵심 정리로 정리해줘.</code></pre>
<p>이처럼 역할, 상황, 작업, 출력 형식이 포함된 프롬프트가 더 좋은 결과를 만들 가능성이 높다.</p>
<p>이런 프롬프트가 더 효과적인 이유는
AI가 참고할 문맥과 요구사항이 많아져 결과의 방향이 더 분명해지기 때문이다.</p>
<ol>
<li>역할 부여하기</li>
</ol>
<pre><code class="language-text">너는 프론트엔드 강사다
너는 데이터 분석가다
너는 글쓰기 코치다</code></pre>
<p>역할을 지정하면 답변의 말투, 설명 방식, 강조하는 관점이 달라질 수 있다.</p>
<ol start="2">
<li>예시를 제공하기</li>
</ol>
<p>AI는 주어진 예시의 패턴을 따라 결과를 생성하는 데 강한 편이다.</p>
<pre><code class="language-text">예시:
사과 → apple
바나나 → banana

포도 → ?</code></pre>
<p>이처럼 원하는 결과 형식의 예시를 주면
AI가 그 패턴을 따라 비슷한 형식으로 결과를 생성하기 쉬워진다.</p>
<ol start="3">
<li>단계적으로 생각하게 하기</li>
</ol>
<pre><code class="language-text">step by step으로 설명해줘
단계적으로 생각해서 설명해줘</code></pre>
<p>이렇게 요청하면 과정을 나누어 설명하거나
단계별 흐름으로 정리해주는 답변을 받을 가능성이 높다.</p>
<ol start="4">
<li>출력 형식 지정하기</li>
</ol>
<pre><code class="language-text">표로 정리해줘
리스트로 정리해줘
마크다운 형식으로 작성해줘</code></pre>
<p>출력 형식을 지정하면 원하는 형태에 더 가까운 결과를 얻기 쉬워진다.</p>
<ol start="5">
<li>제약 조건 주기</li>
</ol>
<pre><code class="language-text">10줄 이내로 설명해줘
초보자 수준으로 설명해줘
전문 용어 없이 설명해줘</code></pre>
<p>제약 조건을 주면 답변의 범위와 방향을 더 분명하게 제한할 수 있다.</p>
<p>프롬프트를 잘 작성한다는 것은
어려운 문장을 쓰는 것이 아니라
AI가 어떤 역할로, 어떤 상황에서, 무엇을 해야 하는지 명확하게 전달하는 것이다.</p>
<hr>
<h2 id="4-프롬프트--함수-호출">4. 프롬프트 = 함수 호출</h2>
<p>프롬프트는 함수로 비유할 수도 있다.</p>
<pre><code class="language-text">AI(프롬프트) → 결과</code></pre>
<p>프롬프트를 함수에 빗대자면</p>
<pre><code class="language-javascript">function AI(
  role, // 역할,
  task, // 해야 할 작업,
  context, // 상황 설명,
  format, // 출력 형식,
  constraint, // 제약 조건,
) {
  return result; // 결과;
}</code></pre>
<p>그냥 함수로 할 수도 있었지만,
지금 배우고 있는 게 JavaScript니
최대한 이해를 살려 JavaScript처럼 예시를 들었다.</p>
<p>이걸 실제와 비슷하게 예시를 살려보면</p>
<pre><code class="language-javascript">function AI({
  role = &quot;자바스크립트 강사&quot;,
  task = &quot;클로저 설명&quot;,
  context = &quot;자바스크립트 공부하는 학생, 쉽게 설명&quot;,
  format = &quot;비유 포함 + 코드 예시 + 핵심 정리&quot;,
  constraint = &quot;10줄 이내&quot;,
}) {
  return &quot;AI가 생성한 결과&quot;;
}</code></pre>
<p>이런 식으로 프롬프트는 함수의 파라미터를 설계하는 것과 비슷하다.</p>
<h2 id="5-ai를-잘-사용하는-방법">5. AI를 &quot;잘&quot; 사용하는 방법</h2>
<p>사람들이 AI를 사용하는 방식은 보통 이렇게 발전한다.</p>
<h3 id="1단계---검색-도구">1단계 - 검색 도구</h3>
<ul>
<li>모르는 개념 검색</li>
<li>용어 의미</li>
<li>간단한 개념 설명</li>
<li>문법 확인</li>
<li>오류 메시지 의미</li>
</ul>
<p>처음에는 AI를 검색 엔진처럼 사용하게 된다.
모르는 개념이 나오면 검색하듯이 질문하고,
간단한 개념 설명을 보는 단계다.</p>
<h3 id="2단계---설명-도구">2단계 - 설명 도구</h3>
<ul>
<li>더 쉽게 설명해줘</li>
<li>비유로 설명해줘</li>
<li>예시로 설명해줘</li>
<li>단계적으로 설명해줘</li>
<li>A랑 B 차이 설명해줘</li>
<li>전체 흐름 설명해줘</li>
</ul>
<p>이 단계부터 AI 느낌이 달라진다.
검색과 AI의 가장 큰 차이는 다시 질문할 수 있다는 점이다.</p>
<h3 id="3단계---작업-보조-도구">3단계 - 작업 보조 도구</h3>
<ul>
<li>코드 왜 안 되는지 분석</li>
<li>코드 리뷰</li>
<li>코드 리팩토링</li>
<li>글 구조 잡기</li>
<li>글 흐름 정리</li>
<li>요약 정리</li>
<li>표 정리</li>
<li>문장 정리</li>
<li>아이디어 정리</li>
</ul>
<p>이 단계부터는 AI를 검색 도구가 아니라
작업을 도와주는 도구처럼 사용하게 된다.</p>
<h3 id="4단계---작업-위임-도구">4단계 - 작업 위임 도구</h3>
<ul>
<li>글 초안 작성</li>
<li>코드 구조 설계</li>
<li>테스트 코드 작성</li>
<li>문서 정리</li>
<li>요구사항 정리</li>
<li>데이터 정리</li>
<li>보고서 작성</li>
<li>아이디어 정리</li>
</ul>
<p>이 단계에서는 AI를 단순히 정보를 찾는 도구로만 쓰는 것이 아니라,
초안 작성이나 정리 같은 작업을 나누어 맡기는 방식으로 활용 범위가 넓어진다.</p>
<p>앞으로 AI를 잘 사용하는 사람은
검색만 하는 사람이 아니라
AI에게 일을 맡길 수 있는 사람이 될지도 모른다.</p>
<p>AI를 사용하는 능력은
결국 질문을 구조화하고 요구사항을 명확하게 정리하는 능력에 가깝다.</p>
<p>즉, 무엇을 원하는지 명확하게 설명할 수 있는 사람이
AI도 더 효과적으로 활용할 수 있다.</p>
<hr>
<h2 id="6-ai-시대에-중요한-능력">6. AI 시대에 중요한 능력</h2>
<p>이 파트의 핵심 논리는 다음과 같다.</p>
<ul>
<li>AI를 잘 쓰는 능력 = 질문을 잘하는 능력</li>
<li>질문을 잘하는 능력 = 문제를 잘 이해하는 능력</li>
<li>문제를 잘 이해하는 능력 = 구조를 이해하는 능력</li>
</ul>
<p>결국 결론은 아이러니하게도</p>
<blockquote>
<p>AI 시대일수록 기초와 개념이 더 중요해진다.</p>
</blockquote>
<p>결국 또 저번에 들었던 특강과 이어진다.</p>
<h3 id="왜-질문하는-능력이-중요해질까">왜 질문하는 능력이 중요해질까?</h3>
<p>AI는 생각하는 존재가 아니라
<strong>입력된 문장을 기반으로 결과를 만들어주는 도구</strong>다.</p>
<p>결과의 품질은 AI의 성능과 프롬프트,
즉 질문에 따라 달라진다.</p>
<p>최근에는 AI 모델들의 성능이 전반적으로 상향 평준화되고 있다.</p>
<p>그렇다면 결국 결과의 차이를 만드는 것은 질문하는 사람이다.</p>
<p>그래서 이런 말이 나오기도 한다.</p>
<blockquote>
<p>AI 시대에는 코딩 실력보다 질문하는 실력이 더 중요해질 수도 있다.</p>
</blockquote>
<h3 id="질문을-잘하려면-">질문을 잘하려면 ?</h3>
<p>좋은 질문은 단순히 떠오르는 대로 던진다고 만들어지는 것이 아니다.</p>
<p>예를 들어보자</p>
<h4 id="나쁜-질문">나쁜 질문</h4>
<pre><code class="language-text">React 왜 안돼?</code></pre>
<h4 id="좋은-질문">좋은 질문</h4>
<pre><code class="language-text">React에서 useState로 상태를 관리하는데,
배열을 수정한 뒤 setState를 했는데도 화면이 바뀌지 않습니다.
map으로 새로운 배열을 만들어야 한다고 들었는데
왜 그런지 원리를 설명해 주세요.</code></pre>
<p>이 두 질문의 차이는 문제를 이해한 정도의 차이에서 나온다.
즉 질문의 수준은 이해의 수준과 거의 비례한다.</p>
<p>그래서 결론은 이렇게 이어진다.</p>
<ol>
<li>좋은 질문을 한다</li>
<li>문제를 잘 이해한다</li>
<li>구조를 이해한다</li>
<li>기초가 탄탄하다</li>
</ol>
<p>그래서 AI 시대라고 해서
기초가 덜 중요해지는 것이 아니라</p>
<p><strong>오히려 기초가 더 중요해진다.</strong></p>
<p>이것도 저번에 들었던 특강과 결국 또 이어진다.
정말 특강을 잘 들었다는 느낌을
이 글을 작성하면서 또 한 번 느끼고 있다.</p>
<h3 id="개발-역사랑-연결해서-보면">개발 역사랑 연결해서 보면</h3>
<p>개발은 계속 더 높은 추상화 수준으로 발전해왔다.</p>
<ol>
<li>기계어</li>
<li>어셈블리</li>
<li>C</li>
<li>Java / Python</li>
<li>라이브러리</li>
<li>프레임워크</li>
<li>AI</li>
</ol>
<p>즉 계속 더 높은 추상화 도구를 사용하게 되는 방향으로 발전했다.</p>
<p>그래서 개발자의 역할도 계속 바뀌었다.</p>
<ol>
<li>예전 개발자: 코드를 직접 다 구현하는 사람</li>
<li>라이브러리 시대: 라이브러리 조합해서 프로그램 만드는 사람</li>
<li>프레임워크 시대: 구조 설계하는 사람</li>
<li>AI 시대: ~</li>
</ol>
<p>AI 시대에서는 다음과 같다.</p>
<ul>
<li>문제 정의하는 사람</li>
<li>요구사항 정리하는 사람</li>
<li>AI에게 작업 지시하는 사람</li>
<li>결과 검증하는 사람</li>
</ul>
<p>즉 개발자의 역할은
코드를 작성하는 사람에서 구조를 설계하는 사람으로,
그리고 점점 문제를 정의하고 해결하는 사람으로 이동하고 있다.</p>
<p>결국 다 정리해서 간단하게 말하자면
AI는 생각을 대신 해주는 도구가 아니라
생각한 것을 대신 만들어주는 도구다.</p>
<p>그래서 AI를 잘 쓰는 사람은
AI를 잘 다루는 사람이 아니라
문제를 잘 이해하고, 구조를 잘 정리하고,
무엇을 원하는지 명확하게 설명할 수 있는 사람일 것이다.</p>
<p>그래서 AI 시대에도 여전히 중요한 것은
새로운 도구를 배우는 것이 아니라
기초를 이해하고 구조를 이해하는 능력이라고 생각한다.</p>
<hr>
<p>약간 요즘 배우고 있는 것들과 조금 엇나가고 있진 않나 싶은 감이 없잖아 있었지만
아무래도 시대가 시대인지라
벨로그 트랜딩 파트에서도 AI 관련 글이 눈에 확 들어온다.</p>
<p>하지만 저번에 들었던 AI 관련 특강에서도, 이 글에서도 결국엔 기본 베이스를 다지는 태초마을로 다시 돌아간다.
단지 얻게 된 정보를 공유하려는 차원으로 시작된 글이었지만,
오히려 지금 방향을 그대로 킵 고잉해도 되겠다는 동기 부여가 되기도 했다.</p>
<p>결국엔 베이스가 있어야 AI와의 티키타카가 잘 어우러질 것이다.</p>
<p>이 정보를 바탕으로, 좀 더 심도 있고, 좀 더 생각해서
AI를 더 똑똑하게 부려먹어 보자 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DOM을 직접 바꾸지 않는 이유: Virtual DOM]]></title>
            <link>https://velog.io/@hogu__giriboy/DOM%EC%9D%84-%EC%A7%81%EC%A0%91-%EB%B0%94%EA%BE%B8%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%9C%A0-Virtual-DOM</link>
            <guid>https://velog.io/@hogu__giriboy/DOM%EC%9D%84-%EC%A7%81%EC%A0%91-%EB%B0%94%EA%BE%B8%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%9C%A0-Virtual-DOM</guid>
            <pubDate>Sat, 21 Mar 2026 08:40:02 GMT</pubDate>
            <description><![CDATA[<h2 id="1-렌더링-이후에-남는-문제">1. 렌더링 이후에 남는 문제</h2>
<p>이전 글에서 살펴봤듯이
React에서 렌더링은 <strong>컴포넌트 함수가 다시 실행되는 과정</strong>이고,
이 자체는 생각보다 큰 비용이 들지 않는 작업이다.</p>
<p>그렇다면 여기서 자연스럽게 하나의 질문이 생긴다.</p>
<blockquote>
<p>그럼 실제로 성능에 영향을 주는 건 무엇일까?</p>
</blockquote>
<p>핵심은 렌더링 자체가 아니라,
<strong>렌더링 이후 실제 화면을 업데이트하는 과정,</strong>
즉 <strong>DOM을 다루는 작업</strong>이다.</p>
<h3 id="dom이-왜-문제일까">DOM이 왜 문제일까</h3>
<p>JavaScript에서 변수 하나 바꾸는 건 가볍다.
하지만 DOM은 다르다.</p>
<p>DOM을 변경하는 순간 브라우저는 단순히 값만 바꾸는 게 아니라
<strong>화면을 다시 계산하는 과정</strong>을 거친다.</p>
<p>예를 들어 생각해보면 이런 느낌이다.</p>
<ul>
<li>JS 연산 → 계산만 하고 끝</li>
<li>DOM 변경 → 계산 + 화면 다시 그리기</li>
</ul>
<p>즉 DOM을 건드린다는 것은 단순히 값을 바꾸는 것이 아니라,
<strong>브라우저에게 화면을 다시 계산하고 그리도록 요청하는 것</strong>과 같다.</p>
<h3 id="그래서-생기는-문제">그래서 생기는 문제</h3>
<p>만약 DOM을 계속 직접 조작하게 되면 어떻게 될까?</p>
<pre><code class="language-javascript">element.textContent = &quot;A&quot;;
element.textContent = &quot;B&quot;;
element.textContent = &quot;C&quot;;</code></pre>
<p>이렇게 여러 번 바꾸면
브라우저는 그때마다 화면을 다시 계산하려고 한다.</p>
<p>결과적으로 불필요한 작업이 계속 발생한다.</p>
<p>그럼 단순하게 DOM을 덜 건드리면 되는 게 아닌가 싶은 생각이 들 수 있다.
그래서 이 지점에서 React의 방식이 등장한다.</p>
<hr>
<h2 id="2-그래서-등장한-virtual-dom">2. 그래서 등장한: Virtual DOM</h2>
<p>앞에서 본 것처럼
문제는 렌더링이 아니라 <strong>DOM을 직접 자주 변경하는 것</strong>이었다.</p>
<p>그렇다면 해결 방법은 단순하다.</p>
<blockquote>
<p><strong>DOM을 바로 건드리지 말고, 한 번 모아서 처리하자</strong></p>
</blockquote>
<p>이 아이디어에서 등장한 것이 바로 <strong>Virtual DOM</strong>이다.</p>
<h3 id="virtual-dom">Virtual DOM?</h3>
<p>Virtual DOM은 말 그대로
<strong>실제 DOM이 아니라, 메모리 위에 만들어진 DOM의 표현(추상화된 구조)</strong>다.</p>
<p>React는 상태가 변경되면
새로운 Virtual DOM을 생성하고 이전 Virtual DOM과 비교하여 <strong>변경 사항을 계산</strong>한다.</p>
<p>구조를 보면 다음과 같다.</p>
<ol>
<li>컴포넌트 실행</li>
<li>새로운 Virtual DOM 생성</li>
<li>이전 Virtual DOM과 비교</li>
<li>바뀐 부분만 실제 DOM에 반영</li>
</ol>
<h3 id="이-구조의-존재-가치">이 구조의 존재 가치</h3>
<p>핵심은 하나다.</p>
<blockquote>
<p>비싼 작업은 최소화하고, 싼 작업은 여러 번 해도 된다</p>
</blockquote>
<ul>
<li>Virtual DOM 비교 → JS 연산 (가벼움)</li>
<li>실제 DOM 변경 → 브라우저 작업 (무거움)</li>
</ul>
<p>그래서 React는 Virtual DOM을 통해 변경 사항을 여러 번 계산하고
실제 DOM 변경은 필요한 부분에만 최소한으로 적용하는 방식을 선택한 것이다.</p>
<p>이번에도 공사로 비유할 수 있다.</p>
<p>직접 DOM을 건드리는 방식은 수정할 때마다 바로 공사하는 느낌이다.
벽 하나 바뀌면 공사, 창문 하나 바뀌면 또 공사하는 방식이라면,</p>
<p>Virtual DOM은 <strong>설계도를 먼저 수정한 뒤, 필요한 부분만 실제 공사에 반영하는 방식</strong>이다.
설계도에서 바뀐 부분을 정리하고 실제 공사는 필요한 부분만 진행하는 방식이다.</p>
<p>즉 Virtual DOM은 화면을 그리기 위한 것이 아니라,
변경 사항을 계산하기 위한 중간 단계라고 볼 수 있다.</p>
<hr>
<h2 id="3-react는-어떻게-최적화하는가">3. React는 어떻게 최적화하는가</h2>
<p>Virtual DOM을 쓴다는 건 알았다.
그럼 중요한 건 이거다.</p>
<blockquote>
<p>어떻게 비교하고, 어떻게 최소 변경만 반영할까?</p>
</blockquote>
<h3 id="이전-결과-vs-새로운-결과">이전 결과 vs 새로운 결과</h3>
<p>React는 상태(state)나 props가 변경되어 렌더링이 발생할 때
새로운 Virtual DOM을 만든다.</p>
<p>그리고 그 이전 Virtual DOM과 새로운 Virtual DOM을
비교해서 <strong>달라진 부분을 찾는다.</strong></p>
<p>이 과정을 보통 <strong>diffing</strong>이라고 한다.</p>
<h3 id="diffing">diffing?</h3>
<p>비교 과정에서 React는 단순히 전체를 바꾸지 않고,
<strong>어디가 바뀌었는지</strong>를 찾는다.</p>
<p>예를 들어</p>
<pre><code class="language-javascript">&lt;h1&gt;Hello&lt;/h1&gt;</code></pre>
<p>에서</p>
<pre><code class="language-javascript">&lt;h1&gt;Hello World&lt;/h1&gt;</code></pre>
<p>로 바뀌었다면</p>
<p><code>&lt;h1&gt;</code> 요소 자체를 교체하는 것이 아니라
<strong>내용만 변경한다고 판단한다.</strong></p>
<h3 id="실제-dom-반영-reconciliation">실제 DOM 반영 (Reconciliation)</h3>
<p>diffing으로 변경된 부분을 찾았으면
이제 그걸 실제 DOM에 반영한다.</p>
<p>이 과정을 <strong>Reconciliation</strong>이라고 한다.</p>
<p>핵심은 <strong>필요한 부분만 업데이트</strong>한다는 것이다.
요소 타입이 같으면 내부 내용과 속성을 업데이트하고,
요소 타입이 다르면 기존 요소를 제거하고 새로 생성한다.</p>
<p>이 방식 때문에 React는
전체를 다시 그리지 않고 필요한 부분만 변경한다.</p>
<p>즉 다음과 같은 구조가 된다.</p>
<ol>
<li>렌더링 (계산)</li>
<li>Virtual DOM 비교</li>
<li>변경된 부분만 실제 DOM 반영</li>
</ol>
<p>이게 가능한 이유는
렌더링 과정은 대부분 JavaScript 계산 작업으로 비교적 가볍고,
실제 DOM 변경은 브라우저의 레이아웃과 화면 업데이트 과정이 포함되어
비용이 크기 때문이다.</p>
<p>그래서 React는 렌더링을 다시 수행하여 Virtual DOM을 만들고
변경 사항을 비교하는 과정은 여러 번 수행하되,
실제 DOM 변경은 필요한 부분에만 최소한으로 적용하는 전략을 선택했다.</p>
<p>결국 React는 화면을 바로 변경하는 방식이 아니라,
먼저 <strong>Virtual DOM을 통해 변경 사항을 계산</strong>하고,
이전 결과와 비교하여 <strong>바뀐 부분만 실제 DOM에 반영하는 방식</strong>으로
화면을 업데이트한다.</p>
<p>즉 React의 렌더링 구조는
<strong>렌더링 → 비교 → 실제 DOM 반영</strong>
이라는 단계로 이루어져 있다고 볼 수 있다.</p>
<hr>
<h2 id="4-핵심-정리">4. 핵심 정리</h2>
<ul>
<li>DOM을 직접 자주 변경하면 브라우저가 레이아웃 계산과 화면 업데이트를 반복하게 되어 성능 비용이 커진다.</li>
<li>React는 실제 DOM을 바로 수정하지 않고, <strong>Virtual DOM이라는 메모리 위의 DOM 구조</strong>를 사용해 변경 사항을 먼저 계산한다.</li>
<li>상태(state)나 props가 변경되면 새로운 Virtual DOM이 생성되고, 이전 Virtual DOM과 비교하여 달라진 부분을 찾는다.</li>
<li>이 비교 과정은 <strong>diffing</strong>, 변경된 부분을 실제 DOM에 반영하는 과정은 <strong>reconciliation</strong>이라고 한다.</li>
<li>React는 전체 화면을 다시 그리는 것이 아니라 <strong>변경된 부분만 실제 DOM에 반영</strong>하는 방식으로 동작한다.</li>
<li>React의 핵심 전략은 <strong>렌더링 계산은 여러 번 수행하되, 실제 DOM 변경은 최소화하는 것</strong>이다.</li>
<li>정리하면 React의 화면 업데이트 과정은 <strong>렌더링 → Virtual DOM 비교 → 실제 DOM 반영</strong> 단계로 이루어진다.</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[렌더링 ≠ 화면 변경: 결과 계산]]></title>
            <link>https://velog.io/@hogu__giriboy/%EB%A0%8C%EB%8D%94%EB%A7%81-%ED%99%94%EB%A9%B4-%EB%B3%80%EA%B2%BD-%EA%B2%B0%EA%B3%BC-%EA%B3%84%EC%82%B0</link>
            <guid>https://velog.io/@hogu__giriboy/%EB%A0%8C%EB%8D%94%EB%A7%81-%ED%99%94%EB%A9%B4-%EB%B3%80%EA%B2%BD-%EA%B2%B0%EA%B3%BC-%EA%B3%84%EC%82%B0</guid>
            <pubDate>Fri, 20 Mar 2026 07:47:57 GMT</pubDate>
            <description><![CDATA[<h2 id="1-렌더링이란">1. &quot;렌더링&quot;이란</h2>
<h3 id="흔히-아는-렌더링">흔히 아는 렌더링</h3>
<p>보통 &quot;렌더링&quot;이라고 하면
<strong>화면을 다시 그리는 것</strong>이라고 생각하는 경우가 많다.</p>
<p>예를 들어
버튼을 눌렀더니, 화면이 바뀌고
뭔가 새로 그려지는 느낌이다.</p>
<p>그래서 자연스럽게
<strong>렌더링은 화면이 바뀐다</strong>라고 받아들이게 된다.</p>
<h3 id="react에서의-렌더링">React에서의 렌더링</h3>
<p>React에서의 렌더링은 조금 다르게 정의해야 한다.</p>
<blockquote>
<p>컴포넌트 함수가 다시 실행되는 것</p>
</blockquote>
<p>예를 들어 이런 코드가 있다고 해보자.</p>
<pre><code class="language-javascript">function App() {
  console.log(&quot;렌더링 실행&quot;);

  return &lt;h1&gt;Hello&lt;/h1&gt;;
}</code></pre>
<p>이 컴포넌트는 처음 실행될 때 한 번 실행되고 끝나는 것이 아니라,</p>
<blockquote>
<p><strong>상태가 바뀔 때마다 다시 실행된다.</strong></p>
</blockquote>
<p>결과적으로는 <strong>화면이 바뀌는 것</strong>처럼 보이기 때문에
렌더링을 <strong>화면을 다시 그리는 것 정도로만</strong> 오해하기 쉽다.</p>
<p>하지만 React 내부에서는</p>
<ol>
<li>함수 실행</li>
<li>결과 생성</li>
<li>(이후 과정 존재)</li>
</ol>
<p>이 흐름을 먼저 가져간다.</p>
<p>렌더링을 <strong>설계도를 만드는 과정</strong>으로 생각하면 훨씬 쉬워진다.</p>
<ul>
<li>컴포넌트 실행 = 설계도 다시 만듦</li>
<li>화면 변경 = 설계도 실제 반영</li>
</ul>
<p>즉 렌더링은 <strong>화면을 바꾸는 단계</strong>가 아니라
<strong>결과를 만드는 단계</strong>다.</p>
<hr>
<h2 id="2-react는-언제-다시-실행될까">2. React는 언제 다시 실행될까</h2>
<p>렌더링이 <strong>컴포넌트 함수 실행</strong>이라는 것을 알았으니
중요한 질문이 하나 남는다.</p>
<blockquote>
<p>그래서 <strong>컴포넌트는 언제 다시 실행될까</strong></p>
</blockquote>
<h3 id="state가-바뀌면">state가 바뀌면</h3>
<p>가장 기본이자 핵심이다.</p>
<pre><code class="language-javascript">const [count, setCount] = useState(0);</code></pre>
<p>여기서</p>
<pre><code class="language-javascript">setCount(1);</code></pre>
<p>이 순간 <strong>컴포넌트 함수가 다시 실행</strong>된다.</p>
<p>state를 <strong>화면을 결정하는 재료</strong>라고 생각하면 편하다.
<strong>재료가 바뀌게 된다면 결과를 다시 계산해야 한다.</strong></p>
<p>그래서 화면을 결정하는 <strong>state가 바뀌면</strong>
<strong>컴포넌트 함수가 다시 실행</strong>된다.</p>
<h3 id="props가-바뀌면">props가 바뀌면</h3>
<p>부모에서 자식으로 데이터를 내려줄 때</p>
<pre><code class="language-javascript">&lt;Child value={count} /&gt;</code></pre>
<p>부모에서 count가 바뀌면 <strong>새로운 props가 전달되면서</strong> 자식 컴포넌트도 다시 실행된다.</p>
<p>구조적으로 이런 식이 된다.</p>
<ol>
<li>부모 실행</li>
<li>새로운 props 전달</li>
<li>자식도 다시 실행</li>
</ol>
<h3 id="부모가-렌더링되면-자식도-같이">부모가 렌더링되면 자식도 같이</h3>
<p>꼭 props가 바뀌지 않아도 실행된다.</p>
<p>예를 들어</p>
<pre><code class="language-javascript">function Parent() {
  const [count, setCount] = useState(0);

  return &lt;Child /&gt;;
}</code></pre>
<p>여기서 setCount 호출하면
Parent가 다시 실행되고, 그 과정에서 Child도 함께 다시 실행된다.</p>
<p>왜냐하면 React 입장에서는</p>
<blockquote>
<p>Parent를 다시 실행되면
그 안에 있는 Child도 다시 실행된다.</p>
</blockquote>
<p>이렇게 보는 것이다.</p>
<p>정리하자면 렌더링 트리거는 다음 세 가지로 볼 수 있다.</p>
<ul>
<li>state 변경</li>
<li>props 변경</li>
<li>부모 컴포넌트 렌더링</li>
</ul>
<hr>
<h2 id="3-렌더링이-발생하면-벌어지는-일">3. 렌더링이 발생하면 벌어지는 일</h2>
<p>컴포넌트 함수는 결국 <strong>JSX를 반환한다.</strong></p>
<pre><code class="language-javascript">function App() {
  return &lt;h1&gt;Hello&lt;/h1&gt;;
}</code></pre>
<p>이 코드를 실행하면 결과로 <strong>JSX 형태의 구조</strong>가 만들어진다.</p>
<p>JSX는 <strong>UI를 선언적으로 표현하기 위한 문법</strong>이다.</p>
<p>이렇게 보면 된다.</p>
<pre><code class="language-javascript">&lt;h1&gt;Hello&lt;/h1&gt;</code></pre>
<p>이건 &quot;이런 UI를 만들겠다&quot;라는
<strong>설계도에 가까운 표현</strong>이다.</p>
<p>이 과정을 정리해보면 다음과 같다.</p>
<ol>
<li>컴포넌트 실행</li>
<li>새로운 JSX가 생성됨</li>
</ol>
<hr>
<h2 id="4-화면을-업데이트하는-방식">4. 화면을 업데이트하는 방식</h2>
<p>우선 다시 떠올려야 할 것이 있다.</p>
<ul>
<li>렌더링 ≠ 화면 변경</li>
<li>렌더링 = 결과 계산</li>
</ul>
<p>즉 state가 변경되어 컴포넌트가 실행되고,
JSX가 생성된 지금 상태는 다음과 같다고 말할 수 있다.</p>
<blockquote>
<p>새로운 UI 설계도는 만들어졌는데
아직 실제 화면에는 반영 안 된 상태</p>
</blockquote>
<h3 id="필요한-부분만">필요한 부분만</h3>
<p>React는 새로운 결과를 바탕으로 <strong>실제 화면을 업데이트한다.</strong>
여기서 중요한 건 <strong>전체를 다 갈아엎지 않는다</strong>는 것이다.</p>
<p>우리가 생각하는 렌더링은 화면 전체를 다시 그리는 방식이다.</p>
<p>하지만 React에서의 렌더링은 결과를 만든 뒤 <strong>필요한 부분만</strong> 반영하는 과정에 가깝다.</p>
<p>설계도로 다시 비유하자면</p>
<ul>
<li>렌더링 = 새 설계도 작성</li>
<li>화면 변경 = 공사</li>
</ul>
<p>근데 공사를 할 때
건물 전체를 부수는 게 아니라 바뀐 부분만 공사한다.</p>
<p>React도 같은 방식으로
렌더링 이후 <strong>이전 결과와 비교해 바뀐 부분만 화면에 반영된다.</strong></p>
<hr>
<h2 id="5-렌더링이-많으면-문제가-될까">5. 렌더링이 많으면 문제가 될까?</h2>
<p>많은 사람들이 헷갈리는 부분이다.</p>
<blockquote>
<p>렌더링이 많이 발생하면 성능이 나쁜 것 아닐까?</p>
</blockquote>
<p>직관적으로는 맞는 말 같다.</p>
<p>자주 실행되고, 많이 반복되면
비효율적이라고 오해하기 쉽다.</p>
<h3 id="react-기준에서-보기">React 기준에서 보기</h3>
<p>React 기준에서 렌더링 자체는 <strong>큰 비용이 드는 작업이 아닐 수 있다.</strong></p>
<p>왜냐하면 렌더링은 기본적으로 <strong>컴포넌트 함수 실행 과정</strong>이기 때문이다.</p>
<p>진짜 비용이 큰 건 실제 화면(DOM)을 바꾸는 작업으로,
이게 훨씬 무겁다.</p>
<p>그래서 구조는 다음과 같이 봐야 한다.</p>
<ul>
<li>렌더링 (가벼움)</li>
<li>실제 화면 변경 (무거움)</li>
</ul>
<p>그래서 React는 렌더링은 다시 수행하되,
<strong>실제 DOM 변경은 최소화하는 방향</strong>으로 동작한다.</p>
<hr>
<h2 id="6-핵심-정리">6. 핵심 정리</h2>
<ul>
<li>렌더링은 컴포넌트 함수가 다시 실행되는 것이다.</li>
<li>state, props, 부모 렌더링에 의해 발생한다.</li>
<li>렌더링 결과로 새로운 JSX 구조가 생성된다.</li>
<li>이 시점에서는 아직 화면이 바뀌지 않는다.</li>
<li>이후 실제 화면이 업데이트되는 과정이 따로 존재한다.</li>
<li>렌더링 자체보다 중요한 것은 <strong>실제 화면 변경 비용</strong>이다.</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[jQuery의 사용률은 70%에 가까운데, 왜 React를 배울까?]]></title>
            <link>https://velog.io/@hogu__giriboy/jQuery%EC%9D%98-%EC%82%AC%EC%9A%A9%EB%A5%A0%EC%9D%80-70%EC%97%90-%EA%B0%80%EA%B9%8C%EC%9A%B4%EB%8D%B0-%EC%99%9C-React%EB%A5%BC-%EB%B0%B0%EC%9A%B8%EA%B9%8C</link>
            <guid>https://velog.io/@hogu__giriboy/jQuery%EC%9D%98-%EC%82%AC%EC%9A%A9%EB%A5%A0%EC%9D%80-70%EC%97%90-%EA%B0%80%EA%B9%8C%EC%9A%B4%EB%8D%B0-%EC%99%9C-React%EB%A5%BC-%EB%B0%B0%EC%9A%B8%EA%B9%8C</guid>
            <pubDate>Tue, 17 Mar 2026 14:41:16 GMT</pubDate>
            <description><![CDATA[<p>벨로그엔 정말 다양한 게시글들이 있고,
이목을 끄는 글들도 많다.</p>
<p>이번 글의 시작은
&quot;대회에서 멘헤라야차서비스 만든 썰&quot; 라는 게시글이었다.</p>
<p>주제부터 너무 유쾌하고 신박해서 쭉 보다가,
우연히 jQuery와 React를 비교하는 글을 보게 됐다.</p>
<p>사실 아직 나는 개발을 막 시작한 입장에서
두 기술 모두 익숙하지 않다.</p>
<p>jQuery는 스프린트 팀에서 현업 경험이 있으신 분이
주로 사용하셨다고 해서 &quot;이런 게 있구나&quot; 정도로 알고 있었고,</p>
<p>React는 요즘 웹 개발에서 많이 사용되는 UI 라이브러리이며,
Next.js와 같은 프레임워크와 함께 사용되는 경우가 많다는 정도만 알고 있었다.</p>
<p><img src="https://velog.velcdn.com/images/hogu__giriboy/post/eac0305d-5c43-4d53-9e53-67566de53243/image.png" alt="JavaScript Libraries">
(해당 자료는 <a href="https://w3techs.com/">해당 링크</a>에서)</p>
<p>근데 이제 통계 자료를 봐도
2026년 1월 1일 기준으로</p>
<p>jQuery는 약 70%,
React는 약 6.2%의 웹사이트에서 사용되고 있었다.</p>
<p>물론 React는 상승세이고,
jQuery를 포함한 일부 라이브러리는 사용 비율이 감소하는 추세지만 ...</p>
<p>이 숫자만 보면서 자연스럽게 한 가지 의문이 생겼다.</p>
<blockquote>
<p>그럼 React보다 jQuery가 더 중요한 기술 아닌가?</p>
</blockquote>
<p>이러한 의문이 들어서 두 라이브러리를
조금 더 깊게 알아보게 되었다.</p>
<hr>
<h2 id="1-사용률이라는-숫자의-의미">1. 사용률이라는 숫자의 의미</h2>
<h3 id="얼마나-쓰이냐-x-얼마나-깔려있냐-o">얼마나 쓰이냐 X 얼마나 깔려있냐 O</h3>
<p>우리가 처음 통계를 봤을 때는 이렇게 생각하기 쉽다.</p>
<blockquote>
<p>jQuery가 70%니까 지금 제일 많이 쓰는 기술이네?</p>
</blockquote>
<p>사실 나도 이렇게 생각해서 이 주제를 잡게 됐지만
사실 이 해석은 살짝 빗나갔다.</p>
<p>여기서 말하는 <strong>사용률</strong>은
<strong>현재 개발에서 얼마나 쓰이는가</strong>가 아니라
<strong>웹사이트에 포함되어 있는가</strong>를 의미한다.</p>
<h3 id="왜-이런-차이가-생기는가">왜 이런 차이가 생기는가</h3>
<p>웹 서비스는 다른 소프트웨어 분야보다 <strong>상대적으로 유지 기간이 긴 편</strong>이다.</p>
<p>예를 들어 하나의 사이트가 만들어지면
1~2년 쓰고 버리는 게 아니라
<strong>5년, 10년 이상 유지되는 경우가 많다.</strong></p>
<p>특히</p>
<ul>
<li>기업 홈페이지</li>
<li>쇼핑몰</li>
<li>워드프레스 사이트</li>
</ul>
<p>이런 것들은 <strong>한 번 만들면 계속 유지, 부분 수정</strong>하는 구조를 갖는다.</p>
<h3 id="계속-남게-되는-jquery">계속 남게 되는 jQuery</h3>
<p>과거 웹 개발은 거의 <code>HTML + CSS + jQuery</code>와 같은 구조였다.</p>
<p>이 시기에 만들어진 사이트들이 아직도 살아있기 때문에
jQuery는 계속 통계에 잡힌다.</p>
<p>반대로 React는 비교적 최근에 등장한 기술이기 때문에,
과거에 만들어진 웹사이트에는 포함되어 있지 않은 경우가 많다.</p>
<p>중요한 건 지금 활발하게 쓰고 있어서가 아니라
<strong>이미 만들어져 있어서 남아있는 것</strong> 이라는 점이다.</p>
<p>비유로 보면 이런 느낌이다.</p>
<pre><code class="language-text">도로 위 자동차 통계

2005년 아반떼 → 아직도 엄청 많음
최신 전기차 → 점점 늘어나는 중</code></pre>
<p>그래서 통계를 보면 옛날 차가 훨씬 많다고 착각할 수 있다.</p>
<p>하지만 실제 흐름은 <strong>신규 프로젝트에서 최신 기술이 주로 선택된다는 점</strong>이다.</p>
<h3 id="실제-개발에서는">실제 개발에서는</h3>
<p>지금 새로 만드는 웹 서비스는 대부분 이렇게 간다.</p>
<ul>
<li>React</li>
<li>Next.js</li>
<li>Vue</li>
</ul>
<p>jQuery로 새 프로젝트를 시작하는 경우는 거의 없다.</p>
<p>즉</p>
<ul>
<li>jQuery → &quot;남아있는 기술&quot;</li>
<li>React → &quot;현재 신규 프로젝트에서 주로 선택되는 기술&quot;</li>
</ul>
<p>이렇게 보는 게 맞다.</p>
<p><img src="https://velog.velcdn.com/images/hogu__giriboy/post/97be3535-46ff-4d6f-be65-f3b3af470272/image.png" alt="npm packege download">
(해당 자료는 <a href="https://npmtrends.com/jquery-vs-react">해당 링크</a>에서)</p>
<p>이건 또다른 통계자료로 npm 패키지 다운로드 수를 비교한 자료인데,
React 관련 패키지의 다운로드 수가 더 높은 흐름을 보이고 있다.</p>
<hr>
<h2 id="2-숫자가-다르게-보이는-이유">2. 숫자가 다르게 보이는 이유</h2>
<p>글을 마무리하고 업로드하려는 과정에서
통계를 보면서 한 가지 더 궁금한 점이 생겨
내용을 보완하기 위해 중간에 섹션을 추가했다.</p>
<p>npm 패키지 다운로드 수를 보면 React가 훨씬 높고,
jQuery는 오히려 그보다 낮은 경우가 많다.</p>
<p>그런데 왜 웹사이트 사용률에서는
jQuery가 압도적으로 높게 나오는 걸까?</p>
<p>이건 두 지표가 서로 다른 기준을 가지고 있기 때문이다.</p>
<p>jQuery는 npm을 사용하지 않고도 사용할 수 있다.
HTML 파일에서 <code>&lt;script&gt;</code> 태그로 불러오기만 해도 바로 사용할 수 있으며,
이 방식은 과거부터 지금까지 계속 유지되고 있다.</p>
<p>그래서 오래된 웹사이트나 워드프레스 같은 환경에서는
npm을 사용하지 않고도 jQuery가 포함된 경우가 많다.</p>
<p>반면 React는 일반적으로 npm 기반으로 사용된다.
프로젝트를 생성할 때부터 설치가 필요하고,
빌드 과정에서도 계속 사용되기 때문에 다운로드 수가 크게 증가한다.</p>
<p>결국 npm 다운로드 수는
지금 <strong>얼마나 활발하게 개발에 사용되고 있는지</strong>를 보여주고,</p>
<p>웹사이트 사용률은
이미 웹에 <strong>얼마나 많이 포함되어 있는지</strong>를 보여주는 지표다.</p>
<p>이 차이를 이해하고 나니
두 통계가 왜 다르게 보이는지 납득할 수 있었다.</p>
<p>따라서 사실상 두 라이브러리를 동일한 기준으로 직접 비교하기는 어렵다.
하지만 웹이 변화하면서 그에 맞는 개발 방식이 바뀌고 있다는 건 뚜렷하기에,
React의 사용 흐름이 증가하고 있다는 점은 확인할 수 있다.</p>
<hr>
<h2 id="3-지금-만들어지는-웹">3. 지금 만들어지는 웹</h2>
<p>아까까지는
웹 전체를 기준으로 통계를 봤다.</p>
<p>그래서 jQuery가 훨씬 많이 쓰이는 것처럼 보였다.</p>
<p>그런데 기준을
<strong>지금 새로 만들어지는 웹</strong>으로 바꾸면
완전히 다른 흐름이 보이기 시작한다.</p>
<h3 id="예전-웹-vs-지금-웹">예전 웹 vs 지금 웹</h3>
<p>과거의 웹은 지금처럼 복잡하지 않았다.</p>
<p>페이지를 이동하면서 내용을 보여주고,
필요할 때 일부 DOM을 수정하는 정도였다.</p>
<p>하지만 지금의 웹은 완전히 다르다.</p>
<p>버튼을 누르면 일부 UI만 바뀌고,
데이터가 바뀌면 화면이 갱신되며,
상태에 따라 화면이 계속 변한다.</p>
<p>이제 웹은 단순한 페이지가 아니라
<strong>하나의 애플리케이션처럼 동작한다.</strong></p>
<p>이러한 변화는 개발 방식 자체에도 영향을 주었다.</p>
<h3 id="예전-웹jquery-vs-지금-웹react">예전 웹(jQuery) vs 지금 웹(React)</h3>
<p>필요한 부분만 직접 찾아서 수정하는 jQuery 방식은
예전 웹에선 크게 문제가 없었다.</p>
<p>하지만 웹의 구조가 점점 변화하면서
jQuery처럼 DOM을 하나씩 직접 수정하는 방식은
수정할 곳이 계속 늘어나고 상태를 관리하기 어려워지고
코드가 점점 복잡해진다.</p>
<p>즉 규모가 커질수록 관리가 어려워진다.</p>
<p>그래서 등장한 방식이 React다.</p>
<p>React는 개발자가 직접 DOM을 조작하기보다는,
<strong>현재 상태를 기준</strong>으로 UI를 선언적으로 표현하고
상태 변화에 따라 필요한 부분만 업데이트한다.</p>
<p>이때 중요한 것은 상태(state)를 중심으로 UI를 관리한다는 점이다.</p>
<p>즉 상태가 바뀌면,
그 변화에 맞게 UI가 다시 계산되어 화면이 업데이트된다.</p>
<p>정리하면
jQuery는 <strong>DOM을 직접 조작하는 방식</strong>이고,
React는 <strong>상태를 기반으로 UI를 선언적으로 구성하는 방식</strong>이다.</p>
<hr>
<h2 id="4-핵심-정리">4. 핵심 정리</h2>
<ul>
<li>jQuery의 높은 사용률은 현재 활발히 사용된다는 의미가 아니라, 과거에 만들어진 웹이 계속 유지되고 있기 때문이다.</li>
<li>사용률 통계는 <strong>얼마나 많이 쓰이는가</strong>보다 <strong>얼마나 포함되어 있는가</strong>에 가까운 지표다.</li>
<li>npm 다운로드 수와 웹사이트 사용률은 서로 다른 기준을 가진 지표이며, 각각 <strong>현재 개발 흐름</strong>과 <strong>누적된 사용 현황</strong>을 보여준다.</li>
<li>기준을 <strong>웹 전체</strong>가 아니라 <strong>지금 새로 만들어지는 웹</strong>으로 바꿔보면 흐름이 완전히 다르게 보인다.</li>
<li>React는 상태 기반으로 UI를 관리하는 방식이며, 복잡한 인터페이스를 다루기 위해 등장했다.</li>
<li>현재 웹 개발은 React와 같은 상태 기반 UI 방식으로 발전하는 흐름을 보이고 있다.</li>
<li>결국 기술을 선택할 때는 단순한 사용률이 아니라, 현재의 흐름과 방향을 기준으로 판단해야 한다.</li>
</ul>
<hr>
<p>어떻게 보면 당연한 흐름이었다.</p>
<p>기술이 발전하면서 웹사이트에서 요구되는 사항이 많아졌을 것이고,
이에 따라 기존에 사용되던 기술들도 확장이나 변화가 필요해졌을 것이다.</p>
<p>jQuery도 꾸준히 업데이트되고 있지만,
기존 방식의 한계가 있었을 것이다.</p>
<p>React처럼 기존 방식과는 다른 접근이 필요했기에
이러한 변화가 나타나지 않았을까 싶다.</p>
<p>하지만 jQuery의 높은 사용률 통계에 이목이 끌렸고,
이 주제를 깊게 살펴보지 않았다면,
사용률만 보고선 왜 React가 현재 주요 선택지인지 이해하기 어려웠을 것이다.</p>
<p>두 기술을 잘 모르는 상태에서 비교해보는 과정 자체가
의미 있는 학습이 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버와 데이터를 주고받는 코드: fetch와 API]]></title>
            <link>https://velog.io/@hogu__giriboy/%EC%84%9C%EB%B2%84%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%A3%BC%EA%B3%A0%EB%B0%9B%EB%8A%94-%EC%BD%94%EB%93%9C-fetch%EC%99%80-API</link>
            <guid>https://velog.io/@hogu__giriboy/%EC%84%9C%EB%B2%84%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%A3%BC%EA%B3%A0%EB%B0%9B%EB%8A%94-%EC%BD%94%EB%93%9C-fetch%EC%99%80-API</guid>
            <pubDate>Mon, 16 Mar 2026 09:29:57 GMT</pubDate>
            <description><![CDATA[<p>블로그를 어떻게 해야 잘 재밌게 쓸 수 있을까 고민하다가
어떤 글을 봤더니 적절히 밈을 섞어서 유쾌하게 풀어냈길래
바로 저번 글에 한 번 소제목에 밈을 섞어봤었다.</p>
<p>근데 영 아닌 거 같아서 이번 글부터 바로 철회했다 ... ...</p>
<hr>
<h2 id="1-브라우저가-요청을-보내는-방법">1. 브라우저가 요청을 보내는 방법</h2>
<p>앞선 글에서는 웹이 <strong>요청(Request)과 응답(Response)</strong> 구조로 동작한다는 것과
브라우저와 서버가 <strong>HTTP를 통해 데이터를 주고받는다</strong>는 흐름을 살펴봤다.</p>
<p>하지만 그 설명은 어디까지나 <strong>통신 구조</strong>에 대한 이야기였다.</p>
<p>이제는 한 단계 더 들어가서
<strong>JavaScript 코드로 실제 서버 요청을 보내는 방법</strong>을 알아볼 차례다.</p>
<p>브라우저에서 서버에 요청을 보내는 방법은 여러 가지가 있지만
현대 웹 환경에서는 <strong>fetch API가 기본적인 방식으로 사용된다.</strong></p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/data&quot;);</code></pre>
<p>이 코드 한 줄이 의미하는 것은 단순하다.</p>
<blockquote>
<p>이 주소로 요청을 보내라.</p>
</blockquote>
<p>브라우저는 이 코드를 실행하면 다음과 같은 일을 한다.</p>
<ol>
<li>JavaScript</li>
<li>브라우저가 HTTP 요청 생성</li>
<li>서버로 요청 전송</li>
<li>서버가 응답 반환</li>
</ol>
<p>즉 <code>fetch</code>는 단순한 함수처럼 보이지만
실제로는 <strong>브라우저에게 서버 요청을 보내라고 지시하는 명령</strong>이다.</p>
<p>여기서 중요한 점 하나가 있다.</p>
<p><code>fetch</code>는 <strong>요청을 보내는 것까지만 담당한다.</strong></p>
<p>서버가 어떤 데이터를 보내는지,
응답이 성공인지 실패인지,
데이터를 어떻게 읽어야 하는지는
<strong>그 다음 단계에서 처리해야 한다.</strong></p>
<p>그래서 <code>fetch</code>를 사용하면
다음과 같은 흐름이 자연스럽게 이어진다.</p>
<ol>
<li>요청 보내기</li>
<li>응답 받기</li>
<li>응답 데이터 읽기</li>
<li>필요한 형태로 사용</li>
</ol>
<p>다음 파트에서는
<code>fetch</code>가 서버 응답을 <strong>어떤 형태로 반환하는지</strong>를 살펴보자.</p>
<p>바로 <strong>Response 객체</strong> 이야기다.</p>
<hr>
<h2 id="2-fetch는-데이터를-바로-주지-않는다">2. fetch는 데이터를 바로 주지 않는다</h2>
<p><code>fetch</code>를 실행하면 서버로 요청이 보내진다.
그렇다면 서버가 데이터를 보내주면 바로 사용할 수 있을까?</p>
<p>실제로는 그렇지 않다.</p>
<pre><code class="language-javascript">const data = fetch(&quot;https://example.com/data&quot;);

console.log(data);</code></pre>
<p>이 코드를 실행하면 우리가 기대하는 <strong>데이터</strong>가 아니라
다음과 같은 값이 출력된다.</p>
<pre><code class="language-text">Promise { &lt;pending&gt; }</code></pre>
<p>여기서 중요한 사실 하나가 있다.</p>
<blockquote>
<p><code>fetch</code>는 데이터를 바로 반환하지 않는다.</p>
</blockquote>
<p>대신 <strong>Promise 객체</strong>를 반환한다.</p>
<p>이 말은 즉, <code>fetch</code>는 <strong>서버 요청을 보내고 Promise를 반환하는 비동기 작업</strong>이라는 뜻이다.</p>
<p>그래서 <code>fetch</code>의 기본 구조는 보통 이렇게 사용된다.</p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/data&quot;).then((response) =&gt; {
  console.log(response);
});</code></pre>
<p>여기서 <code>response</code>는 서버가 반환한 <strong>응답 객체(Response)</strong>다.</p>
<p>하지만 여기서 또 하나 헷갈리는 부분이 있다.</p>
<p><code>response</code> 안에는 우리가 원하는 <strong>데이터가 바로 들어있지 않다.</strong></p>
<p>예를 들어 서버가 다음과 같은 JSON 데이터를 보냈다고 가정해보자.</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;Alice&quot;,
  &quot;age&quot;: 25
}</code></pre>
<p>하지만 <code>response</code>를 그대로 출력하면
이 데이터가 바로 보이지 않는다.</p>
<p>그 이유는 서버 응답이 <strong>Response 객체 형태로 전달되기 때문</strong>이다.</p>
<p>즉 구조는 이렇게 된다.</p>
<ol>
<li>fetch</li>
<li>Promise 반환</li>
<li>Response 객체 도착</li>
<li>JSON 데이터 추출</li>
</ol>
<p>그래서 우리는 한 번 더 작업을 해야 한다.</p>
<p>바로 <strong>응답 데이터를 JSON으로 변환하는 과정</strong>이다.</p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/data&quot;)
  .then((response) =&gt; response.json())
  .then((data) =&gt; {
    console.log(data);
  });</code></pre>
<p>여기서 중요한 포인트가 하나 있다.</p>
<p><code>response.json()</code> 역시 <strong>Promise를 반환한다.</strong></p>
<p>그래서 구조는 사실 이렇게 동작한다.</p>
<ol>
<li>fetch 요청</li>
<li>Response 객체 도착</li>
<li>JSON 파싱</li>
<li>실제 데이터</li>
</ol>
<p>그래서 코드에서는 <code>.then()</code>이 두 번 등장한다.</p>
<pre><code class="language-javascript">fetch(url)
  .then((response) =&gt; response.json())
  .then((data) =&gt; {
    // 실제 데이터 사용
  });</code></pre>
<p>처음 보면 이 구조가 조금 이상하게 느껴질 수 있다.</p>
<p>왜냐하면 우리는 보통 이렇게 생각하기 때문이다.</p>
<blockquote>
<p>요청 → 데이터</p>
</blockquote>
<p>하지만 실제로는 다음과 같은 단계가 존재한다.</p>
<ol>
<li>요청</li>
<li>응답 객체</li>
<li>데이터 파싱</li>
<li>사용</li>
</ol>
<p>이 구조를 이해하면 <code>fetch</code>를 사용할 때
왜 <code>.then()</code>이 여러 번 등장하는지 자연스럽게 이해할 수 있다.</p>
<hr>
<h2 id="3-데이터를-가져오는-가장-기본적인-요청-get-요청">3. 데이터를 가져오는 가장 기본적인 요청: GET 요청</h2>
<p>서버와 통신할 때 가장 먼저 사용하게 되는 요청은
바로 <strong>데이터를 가져오는 요청</strong>이다.</p>
<p>REST API에서는 이 역할을 <strong>GET 요청</strong>이 담당한다.</p>
<p>예를 들어 게시글 목록을 가져오는 API가 있다고 가정해보자.</p>
<pre><code class="language-text">https://example.com/posts</code></pre>
<p>이 주소로 요청을 보내면 서버는 게시글 데이터를 반환한다.</p>
<p>이 요청을 JavaScript 코드로 작성하면 다음과 같다.</p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/posts&quot;)
  .then((response) =&gt; response.json())
  .then((data) =&gt; {
    console.log(data);
  });</code></pre>
<p>이 코드의 흐름을 보면 다음과 같다.</p>
<ol>
<li>fetch 요청 전송</li>
<li>Response 객체 반환</li>
<li>JSON 데이터로 변환</li>
<li>데이터 사용</li>
</ol>
<p>여기서 중요한 점은 <strong>GET 요청은 별도의 옵션 없이 사용할 수 있다는 것</strong>이다.</p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/posts&quot;);</code></pre>
<p>이렇게 작성해도 기본적으로 <strong>GET 요청</strong>이 보내진다.</p>
<p>즉 <code>fetch</code>의 기본 요청 방식은 <strong>GET</strong>이다.</p>
<p>그래서 데이터를 조회할 때는 보통 다음과 같은 구조를 사용하게 된다.</p>
<pre><code class="language-javascript">async function getPosts() {
  const response = await fetch(&quot;https://example.com/posts&quot;);
  const data = await response.json();

  return data;
}</code></pre>
<p>이렇게 함수를 만들어 두면
다른 코드에서는 다음처럼 사용할 수 있다.</p>
<pre><code class="language-javascript">const posts = await getPosts();
console.log(posts);</code></pre>
<p>즉 <strong>데이터 조회 요청을 하나의 함수로 정리</strong>하면
코드를 훨씬 깔끔하게 관리할 수 있다.</p>
<p>하지만 실제 애플리케이션에서는
<strong>여러 개의 요청을 동시에 보내야 하는 상황</strong>이 자주 발생한다.</p>
<p>예를 들어</p>
<ul>
<li>게시글 목록</li>
<li>사용자 정보</li>
<li>댓글 목록</li>
</ul>
<p>같은 데이터를 <strong>동시에 가져와야 할 수도 있다.</strong></p>
<p>이때 사용하는 방법이 바로 다음 파트에서 알아볼 <strong>Promise.all</strong>이다.</p>
<hr>
<h2 id="4-여러-요청을-함께-처리하기-promiseall">4. 여러 요청을 함께 처리하기: Promise.all</h2>
<p>앞에서 살펴본 것처럼 <code>fetch</code>는 <strong>Promise를 반환하는 비동기 작업</strong>이다.
그래서 서버 요청이 하나일 때는 비교적 단순하게 처리할 수 있다.</p>
<p>하지만 실제 애플리케이션에서는
<strong>여러 데이터를 동시에 가져와야 하는 상황</strong>이 자주 발생한다.</p>
<p>예를 들어 페이지를 구성하기 위해 다음과 같은 데이터를 가져와야 할 수도 있다.</p>
<ul>
<li>게시글 목록</li>
<li>사용자 정보</li>
<li>댓글 목록</li>
</ul>
<p>이때 요청을 하나씩 순서대로 보내면
각 요청이 끝날 때까지 다음 요청을 기다리게 된다.</p>
<pre><code class="language-javascript">const posts = await fetch(&quot;/posts&quot;).then((res) =&gt; res.json());
const users = await fetch(&quot;/users&quot;).then((res) =&gt; res.json());
const comments = await fetch(&quot;/comments&quot;).then((res) =&gt; res.json());</code></pre>
<p>이 코드는 동작하지만 실제로는 다음과 같은 흐름으로 실행된다.</p>
<ol>
<li>posts 요청 → 응답 대기</li>
<li>users 요청 → 응답 대기</li>
<li>comments 요청 → 응답 대기</li>
</ol>
<p>즉 요청이 <strong>순차적으로 실행된다.</strong></p>
<p>하지만 이 세 요청은 서로 의존하지 않는다.
그렇다면 굳이 순서대로 기다릴 필요가 없다.</p>
<p>이럴 때 사용하는 것이 <strong>Promise.all</strong>이다.</p>
<pre><code class="language-javascript">const [posts, users, comments] = await Promise.all([
  fetch(&quot;/posts&quot;).then((res) =&gt; res.json()),
  fetch(&quot;/users&quot;).then((res) =&gt; res.json()),
  fetch(&quot;/comments&quot;).then((res) =&gt; res.json()),
]);</code></pre>
<p>이 코드의 실행 흐름은 다음과 같다.</p>
<ol>
<li>posts, users, comments 요청 동시에 시작</li>
<li>모든 요청이 완료될 때까지 대기</li>
<li>모든 응답이 도착하면 결과 반환</li>
</ol>
<p>즉 <code>Promise.all</code>은
<strong>여러 비동기 작업을 동시에 실행하고, 모든 작업이 완료되면 결과를 반환</strong>한다.</p>
<p>그래서 서로 의존하지 않는 요청이라면
<strong>순차 요청보다 훨씬 빠르게 데이터를 가져올 수 있다.</strong></p>
<p>여기서 <code>Promise.all</code>의 결과는
<strong>배열 형태</strong>로 반환된다.</p>
<p>그래서 보통 다음과 같이 <strong>구조 분해 할당</strong>을 사용해 값을 꺼낸다.</p>
<pre><code class="language-javascript">const [posts, users, comments] = ...</code></pre>
<p>이렇게 하면 각각의 데이터에 바로 접근할 수 있다.</p>
<hr>
<h2 id="5-서버에-새로운-데이터를-보내기-post">5. 서버에 새로운 데이터를 보내기: POST</h2>
<p>앞에서는 서버에서 <strong>데이터를 가져오는 요청(GET)</strong>을 살펴봤다.
하지만 실제 서비스에서는 데이터를 조회하는 것만으로는 충분하지 않다.</p>
<p>예를 들어 다음과 같은 상황을 생각해보자.</p>
<ul>
<li>새로운 게시글 작성</li>
<li>회원가입</li>
<li>댓글 작성</li>
</ul>
<p>이런 기능들은 모두 <strong>서버에 새로운 데이터를 보내는 작업</strong>이다.</p>
<p>이때 사용하는 요청이 바로 <strong>POST 요청</strong>이다.</p>
<p><code>fetch</code>로 POST 요청을 보내려면
단순히 URL만 전달하는 것이 아니라 <strong>옵션 객체</strong>를 함께 전달해야 한다.</p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/posts&quot;, {
  method: &quot;POST&quot;,
});</code></pre>
<p>여기서 <code>method</code>는 <strong>HTTP 요청 방식</strong>을 의미한다.</p>
<p>하지만 실제로는 새로운 데이터를 생성해야 하기 때문에
<strong>서버로 데이터를 함께 보내야 한다.</strong></p>
<p>그래서 보통 다음과 같은 형태로 작성한다.</p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/posts&quot;, {
  method: &quot;POST&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
  body: JSON.stringify({
    title: &quot;새로운 게시글&quot;,
    content: &quot;내용입니다&quot;,
  }),
});</code></pre>
<h3 id="method">method</h3>
<pre><code class="language-javascript">method: &quot;POST&quot;;</code></pre>
<p>요청의 종류를 <strong>POST 요청</strong>으로 지정한다.</p>
<h3 id="headers">headers</h3>
<pre><code class="language-javascript">headers: {
  &quot;Content-Type&quot;: &quot;application/json&quot;
}</code></pre>
<p>서버에게 <strong>어떤 형식의 데이터를 보내는지</strong> 알려주는 부분이다.</p>
<p>여기서는 <strong>JSON 형식으로 데이터를 보낸다</strong>는 의미다.</p>
<h3 id="body">body</h3>
<pre><code class="language-javascript">body: JSON.stringify({...})</code></pre>
<p>실제로 서버로 전달되는 데이터다.</p>
<p>여기서 <code>JSON.stringify()</code>를 사용하는 이유는
JavaScript 객체를 <strong>JSON 문자열 형태로 변환하기 위해서</strong>다.</p>
<p>즉 GET 요청이 <strong>데이터 조회</strong>라면
POST 요청은 <strong>데이터 생성</strong>을 담당한다.</p>
<p>하지만 데이터를 생성하는 것만으로는 충분하지 않다.</p>
<p>이미 존재하는 데이터를
<strong>수정하거나 업데이트해야 하는 상황</strong>도 있다.</p>
<hr>
<h2 id="6-데이터를-수정하는-요청-put과-patch">6. 데이터를 수정하는 요청: PUT과 PATCH</h2>
<p>서버에 데이터를 생성할 때는 <strong>POST 요청</strong>을 사용한다.
하지만 이미 존재하는 데이터를 <strong>수정해야 하는 상황</strong>도 자주 발생한다.</p>
<p>예를 들어 다음과 같은 경우다.</p>
<ul>
<li>게시글 내용 수정</li>
<li>사용자 정보 변경</li>
<li>할 일 목록 상태 변경</li>
</ul>
<p>이처럼 기존 데이터를 업데이트할 때 사용하는 요청이
<strong>PUT</strong>과 <strong>PATCH</strong>다.</p>
<p>두 요청이 모두 <strong>데이터 수정</strong>이라는 목적은 같지만
<strong>수정 방식</strong>에서 차이가 있다.</p>
<h3 id="전체를-교체하는-put">전체를 교체하는 PUT</h3>
<p>PUT 요청은 <strong>기존 데이터를 새로운 데이터로 전체 교체</strong>하는 방식이다.</p>
<p>예를 들어 서버에 다음과 같은 데이터가 있다고 가정해보자.</p>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;게시글 제목&quot;,
  &quot;content&quot;: &quot;내용&quot;
}</code></pre>
<p>이 데이터를 PUT 요청으로 수정하면
기존 데이터를 <strong>완전히 새로운 데이터로 덮어쓴다.</strong></p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/posts/1&quot;, {
  method: &quot;PUT&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
  body: JSON.stringify({
    title: &quot;수정된 제목&quot;,
    content: &quot;수정된 내용&quot;,
  }),
});</code></pre>
<p>이 경우 서버는 <strong>기존 데이터를 새로운 데이터로 전체 교체</strong>하게 된다.</p>
<h3 id="일부만-수정하는-patch">일부만 수정하는 PATCH</h3>
<p>PATCH 요청은 <strong>데이터의 일부만 수정</strong>할 때 사용한다.</p>
<p>예를 들어 제목만 수정하고 싶다면
모든 데이터를 다시 보낼 필요는 없다.</p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/posts/1&quot;, {
  method: &quot;PATCH&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
  body: JSON.stringify({
    title: &quot;수정된 제목&quot;,
  }),
});</code></pre>
<p>이 경우 서버는 기존 데이터에서
<code>title</code>만 수정하고 나머지 값은 그대로 유지한다.</p>
<h3 id="put과-patch의-차이">PUT과 PATCH의 차이</h3>
<p>정리하면 다음과 같다.</p>
<ul>
<li>PUT → 전체 교체</li>
<li>PATCH → 일부 수정</li>
</ul>
<p>그래서 API 설계에 따라
<strong>PUT만 사용하는 경우도 있고, PATCH를 함께 사용하는 경우도 있다.</strong></p>
<p>마지막으로 남은 것은
데이터를 <strong>삭제하는 요청</strong>이다.</p>
<hr>
<h2 id="7-데이터를-삭제하는-요청-delete">7. 데이터를 삭제하는 요청: DELETE</h2>
<p>애플리케이션에서는 더 이상 필요하지 않은 데이터를
<strong>삭제해야 하는 상황</strong>도 자주 발생한다.</p>
<p>예를 들어 다음과 같은 경우다.</p>
<ul>
<li>게시글 삭제</li>
<li>댓글 삭제</li>
<li>할 일 목록 삭제</li>
</ul>
<p>이때 사용하는 요청이 바로 <strong>DELETE 요청</strong>이다.</p>
<p><code>fetch</code>로 DELETE 요청을 보내는 방식은 비교적 단순하다.</p>
<pre><code class="language-javascript">fetch(&quot;https://example.com/posts/1&quot;, {
  method: &quot;DELETE&quot;,
});</code></pre>
<p>이 요청은 다음과 같은 의미를 가진다.</p>
<pre><code class="language-text">/posts/1 데이터 삭제 요청</code></pre>
<p>서버는 이 요청을 받으면
해당 ID의 데이터를 찾아 <strong>삭제 처리</strong>를 하게 된다.</p>
<p>DELETE 요청은 보통 <strong>삭제할 데이터의 식별자(ID)</strong>를
URL에 포함하는 방식으로 사용된다.</p>
<p>예를 들어</p>
<pre><code class="language-text">/posts/1</code></pre>
<p>이라는 주소는 &quot;ID가 1인 게시글&quot;을 의미한다.</p>
<p>그래서 삭제 요청은 보통 이런 형태로 작성된다.</p>
<pre><code class="language-javascript">async function deletePost(id) {
  await fetch(`https://example.com/posts/${id}`, {
    method: &quot;DELETE&quot;,
  });
}</code></pre>
<p>이렇게 함수를 만들어 두면
다른 코드에서는 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-javascript">await deletePost(1);</code></pre>
<p>즉 서버에게 &quot;ID가 1인 데이터 삭제&quot; 라는 요청을 보내는 것이다.</p>
<p>여기까지 살펴보면
우리는 서버 데이터를 다루는 기본적인 작업을 모두 이해하게 된다.</p>
<ul>
<li>GET → 데이터 조회</li>
<li>POST → 데이터 생성</li>
<li>PUT → 전체 수정</li>
<li>PATCH → 부분 수정</li>
<li>DELETE → 데이터 삭제</li>
</ul>
<p>이 다섯 가지 요청은
서버 데이터를 관리하는 가장 기본적인 패턴이며
보통 <strong>CRUD 작업</strong>이라고 부른다.</p>
<p>하지만 실제 서버 통신에서는
요청이 항상 성공하는 것은 아니다.</p>
<p>네트워크 문제나 서버 오류로 인해
요청이 실패하는 상황도 충분히 발생할 수 있다.</p>
<hr>
<h2 id="8-요청이-항상-성공하는-것은-아니다-에러-처리">8. 요청이 항상 성공하는 것은 아니다: 에러 처리</h2>
<p>지금까지는 서버 요청이 <strong>정상적으로 동작하는 상황</strong>을 기준으로 살펴봤다.
하지만 실제 네트워크 통신에서는 요청이 항상 성공하는 것은 아니다.</p>
<p>예를 들어 다음과 같은 상황이 발생할 수 있다.</p>
<ul>
<li>존재하지 않는 주소로 요청한 경우</li>
<li>네트워크 연결이 끊어진 경우</li>
<li>서버 내부 오류가 발생한 경우</li>
</ul>
<p>이처럼 요청이 실패할 가능성이 있기 때문에
서버 통신 코드를 작성할 때는 <strong>에러 처리를 함께 고려해야 한다.</strong></p>
<p><code>fetch</code>를 사용할 때 가장 기본적인 방법은
<code>try...catch</code>를 사용하는 것이다.</p>
<pre><code class="language-javascript">async function getPosts() {
  try {
    const response = await fetch(&quot;https://example.com/posts&quot;);
    const data = await response.json();

    return data;
  } catch (error) {
    console.error(&quot;요청 중 오류가 발생했습니다:&quot;, error);
  }
}</code></pre>
<p>이 코드에서 <code>try</code> 블록은
<strong>정상적인 요청 흐름</strong>을 담당한다.</p>
<ol>
<li>fetch 요청</li>
<li>응답 받기</li>
<li>JSON 변환</li>
<li>데이터 반환</li>
</ol>
<p>만약 이 과정에서 오류가 발생하면
<code>catch</code> 블록이 실행된다.</p>
<p>하지만 여기서 한 가지 더 중요한 점이 있다.</p>
<p><code>fetch</code>는 <strong>HTTP 오류를 자동으로 에러로 처리하지 않는다.</strong></p>
<p>예를 들어 서버가 <strong>404 Not Found</strong>나
<strong>500 Internal Server Error</strong>를 반환해도
<code>fetch</code> 자체는 <strong>성공한 요청으로 처리된다.</strong></p>
<p>그래서 응답 상태를 직접 확인해야 한다.</p>
<pre><code class="language-javascript">async function getPosts() {
  const response = await fetch(&quot;https://example.com/posts&quot;);

  if (!response.ok) {
    throw new Error(&quot;서버 요청 실패&quot;);
  }

  const data = await response.json();
  return data;
}</code></pre>
<p>여기서 <code>response.ok</code>는
응답 상태 코드가 <strong>성공 범위(200~299)</strong>인지 확인하는 속성이다.</p>
<p>즉 구조는 이렇게 된다.</p>
<ol>
<li>요청 전송</li>
<li>응답 도착</li>
<li>응답 상태 확인</li>
<li>데이터 파싱</li>
<li>사용</li>
</ol>
<p>이 과정을 통해
요청 실패 상황을 보다 안정적으로 처리할 수 있다.</p>
<hr>
<h2 id="9-반복되는-fetch를-정리하면-api-함수">9. 반복되는 fetch를 정리하면: API 함수</h2>
<p>지금까지 <code>fetch</code>를 사용해 다양한 서버 요청을 살펴봤다.</p>
<p>하지만 실제 코드에서 매번 이런 요청을 직접 작성하면
코드가 금방 복잡해질 수 있다.</p>
<p>예를 들어 게시글 데이터를 가져오는 코드가
여러 곳에서 필요하다고 가정해보자.</p>
<pre><code class="language-javascript">const response = await fetch(&quot;https://example.com/posts&quot;);
const data = await response.json();</code></pre>
<p>이 코드를 페이지 곳곳에서 계속 작성하게 되면
다음과 같은 문제가 생긴다.</p>
<ul>
<li>같은 코드가 여러 곳에 반복된다</li>
<li>API 주소가 바뀌면 모든 코드를 수정해야 한다</li>
<li>요청 로직을 관리하기 어려워진다</li>
</ul>
<p>그래서 보통은 서버 요청 코드를
<strong>하나의 함수로 정리해서 사용한다.</strong></p>
<pre><code class="language-javascript">async function getPosts() {
  const response = await fetch(&quot;https://example.com/posts&quot;);
  const data = await response.json();

  return data;
}</code></pre>
<p>이렇게 정리해두면
다른 코드에서는 훨씬 간단하게 사용할 수 있다.</p>
<pre><code class="language-javascript">const posts = await getPosts();</code></pre>
<p>이 방식의 장점은 명확하다.</p>
<p>먼저 <strong>코드 중복을 줄일 수 있다.</strong></p>
<p>서버 요청 로직을 한 곳에 모아두면
같은 코드를 반복해서 작성할 필요가 없다.</p>
<p>또한 <strong>유지보수가 쉬워진다.</strong></p>
<p>만약 API 주소가 변경되더라도
함수 내부만 수정하면 된다.</p>
<p>마지막으로 <strong>코드 구조가 훨씬 명확해진다.</strong></p>
<p>데이터를 가져오는 코드와
화면을 렌더링하는 코드를 분리할 수 있기 때문이다.</p>
<p>그래서 실제 프로젝트에서는
이런 서버 요청 함수들을 모아서
보통 <strong>API 모듈</strong> 형태로 관리한다.</p>
<pre><code class="language-text">api
⎿ getPosts
⎿ createPost
⎿ updatePost
⎿ deletePost</code></pre>
<p>이렇게 요청 로직을 정리하면
애플리케이션 전체에서
<strong>일관된 방식으로 서버 통신을 관리할 수 있다.</strong></p>
<hr>
<h2 id="10-핵심-정리">10. 핵심 정리</h2>
<ul>
<li>브라우저는 <code>fetch</code>를 사용해 서버에 요청을 보낼 수 있다.</li>
<li><code>fetch</code>는 데이터를 바로 반환하지 않고 <strong>Promise</strong>를 반환한다.</li>
<li>서버 응답은 <strong>Response 객체</strong> 형태로 전달되며 <code>response.json()</code>으로 데이터를 읽을 수 있다.</li>
<li>데이터를 조회할 때는 <strong>GET 요청</strong>을 사용한다.</li>
<li>여러 요청을 동시에 처리할 때는 <strong>Promise.all</strong>을 사용할 수 있다.</li>
<li>데이터를 생성할 때는 <strong>POST 요청</strong>을 사용한다.</li>
<li>데이터를 수정할 때는 <strong>PUT(전체 수정)</strong>과 <strong>PATCH(부분 수정)</strong>을 사용한다.</li>
<li>데이터를 삭제할 때는 <strong>DELETE 요청</strong>을 사용한다.</li>
<li>서버 요청은 실패할 수 있으므로 <strong>에러 처리</strong>가 필요하다.</li>
<li>반복되는 <code>fetch</code> 코드는 <strong>API 함수로 정리하면 관리하기 쉬워진다.</strong></li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript - 변수 선언의 진화: var, let, const]]></title>
            <link>https://velog.io/@hogu__giriboy/JavaScript-%EB%B3%80%EC%88%98-%EC%84%A0%EC%96%B8%EC%9D%98-%EC%A7%84%ED%99%94-var-let-const</link>
            <guid>https://velog.io/@hogu__giriboy/JavaScript-%EB%B3%80%EC%88%98-%EC%84%A0%EC%96%B8%EC%9D%98-%EC%A7%84%ED%99%94-var-let-const</guid>
            <pubDate>Mon, 16 Mar 2026 05:17:39 GMT</pubDate>
            <description><![CDATA[<h2 id="1-javascript의-변수-선언-방식">1. JavaScript의 변수 선언 방식</h2>
<p>JavaScript에서 값을 저장하려면 <strong>변수에 값을 할당(assignment)</strong>해야 하며,
이를 위해 먼저 <strong>변수를 선언(declaration)</strong>한다.
그리고 JavaScript에는 변수를 선언하는 세 가지 방법이 있다.</p>
<ul>
<li>var</li>
<li>let</li>
<li>const</li>
</ul>
<p>지금은 대부분 <code>let</code>과 <code>const</code>를 사용하지만
JavaScript가 처음 등장했을 때는 <code>var</code> <strong>하나만 존재했다.</strong></p>
<p>즉 초기 JavaScript에서는 모든 변수를 이렇게 선언했다.</p>
<pre><code class="language-javascript">var name = &quot;Alice&quot;;
var age = 20;</code></pre>
<p>당시에는 문제가 없어 보였지만
프로그램 규모가 커지면서 <code>var</code>가 여러 문제를 만들기 시작했다.</p>
<h3 id="javascript-초창기-var">JavaScript 초창기: <code>var</code></h3>
<p>JavaScript는 원래 <strong>웹페이지에 동적인 동작을 추가하는 것</strong>이었다.
웹페이지에 간단한 동작을 추가하는 것이 목적이었기 때문에
변수 선언 방식도 단순하게 설계되었다.</p>
<p>그래서 변수 선언은 사실상 <code>var</code> <strong>하나로 통일</strong>되어 있었다.</p>
<pre><code class="language-javascript">var count = 1;
var message = &quot;hello&quot;;</code></pre>
<p>하지만 시간이 지나면서 JavaScript는
단순한 스크립트 언어를 넘어 <strong>대규모 애플리케이션을 만드는 언어</strong>로 발전했다.</p>
<p>이 과정에서 <code>var</code>의 설계가 여러 문제를 만들기 시작했다.</p>
<h3 id="es6에서-등장한-let과-const">ES6에서 등장한 <code>let</code>과 <code>const</code></h3>
<p>이 문제를 해결하기 위해
<strong>ES6(ECMAScript 2015)</strong>에서 새로운 변수 선언 방식이 추가된다.</p>
<ul>
<li>let</li>
<li>const</li>
</ul>
<p>이 두 가지는 단순한 문법 추가가 아니라
<strong>스코프와 변수 재선언 문제를 개선하기 위해 도입된 새로운 변수 선언 방식이다.</strong></p>
<p>정리하면 현재 JavaScript에는 다음 세 가지 선언 방식이 존재한다.</p>
<ul>
<li>var → 과거 방식</li>
<li>let → 재할당 가능한 변수</li>
<li>const → 재할당 불가능한 변수</li>
</ul>
<p>그리고 현대 JavaScript에서는 대부분 다음과 같은 방식으로 사용된다.</p>
<pre><code class="language-javascript">const name = &quot;Alice&quot;;
let count = 0;</code></pre>
<p>즉 <strong>기본은 <code>const</code></strong>,
값이 바뀌어야 할 때만 <strong><code>let</code></strong>을 사용한다.</p>
<p><code>var</code>는 과거 코드에서만 볼 수 있는 경우가 많고
새로운 코드에서는 거의 사용하지 않는다.</p>
<h3 id="변수-선언-방식의-변화-이유">변수 선언 방식의 변화 이유</h3>
<p>핵심 이유는 하나다.</p>
<p><code>var</code>가 <strong>예측하기 어려운 동작을 만들었기 때문</strong>이다.</p>
<p>특히 다음과 같은 문제들이 있었다.</p>
<ul>
<li>함수 스코프</li>
<li>중복 선언 가능</li>
<li>호이스팅</li>
</ul>
<p>이 문제들이 실제 개발에서 <strong>버그의 원인</strong>이 되는 경우가 많았고
이를 해결하기 위해 <code>let</code>과 <code>const</code>가 등장하게 된다.</p>
<hr>
<h2 id="2-var가-만들었던-문제들">2. <code>var</code>가 만들었던 문제들</h2>
<p>초창기 JavaScript에서 <code>var</code>는 유일한 변수 선언 방식이었지만
프로그램 규모가 커지면서 여러 <strong>예측하기 어려운 동작</strong>을 만들기 시작했다.</p>
<p>대표적으로 다음과 같은 특징들이 이해를 어렵게 만들 수 있다.</p>
<ul>
<li>함수 스코프</li>
<li>중복 선언 가능</li>
<li>호이스팅</li>
</ul>
<p>이 세 가지 때문에 코드의 동작을 <strong>직관적으로 이해하기 어려워졌다.</strong></p>
<h3 id="블록이-아닌-함수-기준으로-동작하는-스코프">블록이 아닌 함수 기준으로 동작하는 스코프</h3>
<p>많은 현대 프로그래밍 언어에서는 <code>{}</code> 블록 단위로 스코프가 결정되는
<strong>블록 스코프(block scope)</strong>를 사용한다.</p>
<p>하지만 <code>var</code>는 그렇지 않다.</p>
<p><code>var</code>는 <strong>블록이 아니라 함수 기준으로 스코프가 결정된다.</strong></p>
<pre><code class="language-javascript">if (true) {
  var message = &quot;hello&quot;;
}

console.log(message); // hello</code></pre>
<p><code>if</code> 블록 안에서 선언했는데도
블록 밖에서 변수에 접근할 수 있다.</p>
<p>왜냐하면 <code>var</code>는 <strong>블록 스코프가 아니라 함수 스코프</strong>이기 때문이다.</p>
<p>그래서 코드가 길어질수록
<strong>변수가 어디서 생성된 것인지 파악하기 어려워지는 문제</strong>가 생긴다.</p>
<h3 id="같은-변수를-여러-번-선언할-수-있다">같은 변수를 여러 번 선언할 수 있다.</h3>
<p>또 하나의 문제는 <strong>중복 선언이 가능하다는 것</strong>이다.</p>
<pre><code class="language-javascript">var name = &quot;Alice&quot;;
var name = &quot;Bob&quot;;

console.log(name); // Bob</code></pre>
<p>같은 이름의 변수를 다시 선언해도
에러가 발생하지 않는다.</p>
<p>결과적으로 기존 변수는 그냥 덮어쓰이게 된다.</p>
<p>이런 상황에서는 실수로 변수를 다시 선언해도 프로그램이 그대로 실행된다.</p>
<p>즉 <strong>버그가 발생해도 바로 알아차리기 어려워진다.</strong></p>
<h3 id="코드-위로-끌어올려지는-호이스팅">코드 위로 끌어올려지는 호이스팅</h3>
<p><code>var</code>의 또 다른 특징은 <strong>호이스팅(hoisting)</strong>이다.</p>
<p>호이스팅은 <strong>변수 선언이 실제 코드 위치와 관계없이</strong>
<strong>해당 스코프의 시작 부분에서 선언된 것처럼 동작하는 JavaScript의 동작 방식</strong>이다.</p>
<pre><code class="language-javascript">console.log(count);

var count = 10;</code></pre>
<p>이 코드는 에러가 나지 않는다.</p>
<p>실제로 JavaScript 엔진이 내부적으로
코드를 다음처럼 처리하기 때문이다.</p>
<pre><code class="language-javascript">var count;

console.log(count);

count = 10;</code></pre>
<p>그래서 결과는 <code>undefined</code>가 나온다.</p>
<p>변수가 선언되기 전에 사용했는데도
에러가 발생하지 않고 <code>undefined</code>가 출력된다.</p>
<p>이런 동작은 코드의 실행 흐름을 <strong>직관적으로 이해하기 어렵게 만든다.</strong></p>
<h3 id="점차-드물어지는-var">점차 드물어지는 <code>var</code></h3>
<p>정리하면 <code>var</code>는 다음과 같은 문제를 가지고 있었다.</p>
<ul>
<li>블록이 아닌 <strong>함수 기준 스코프</strong></li>
<li><strong>중복 선언 가능</strong></li>
<li><strong>호이스팅으로 인한 예측 어려움</strong></li>
</ul>
<p>JavaScript가 점점 <strong>대규모 애플리케이션을 만드는 언어</strong>로 발전하면서
이 문제들은 실제 개발에서 큰 혼란을 만들었다.</p>
<p>그래서 ES6에서는 이러한 특성으로 인한 혼란을 줄이기 위해
<strong><code>let</code>과 <code>const</code>라는 새로운 변수 선언 방식이 도입되었다.</strong></p>
<hr>
<h2 id="3-let-블록-스코프의-등장">3. <code>let</code>: 블록 스코프의 등장</h2>
<p><code>var</code>의 특징 중 하나는
<strong>스코프가 블록이 아니라 함수 단위로 결정된다는 것</strong>이다.</p>
<p>이 문제를 해결하기 위해 ES6에서 등장한 것이
바로 <code>let</code>이다.</p>
<p><code>let</code>의 가장 중요한 특징은 <strong>블록 스코프(block scope)</strong>다.</p>
<h3 id="블록-단위로-관리되는-변수">블록 단위로 관리되는 변수</h3>
<p>블록 스코프란
<code>{}</code> 블록 안에서 선언된 변수는 <strong>그 블록 안에서만 사용할 수 있는 것</strong>을 의미한다.</p>
<pre><code class="language-javascript">if (true) {
  let message = &quot;hello&quot;;
}

console.log(message);</code></pre>
<p>이 코드는 <strong>에러가 발생한다.</strong></p>
<pre><code class="language-text">ReferenceError: message is not defined</code></pre>
<p>왜냐하면 <code>message</code>는 <code>if</code> <strong>블록 안에서만 존재하는 변수</strong>이기 때문이다.</p>
<p>이것이 <code>var</code>와 가장 큰 차이다.</p>
<h3 id="var와-let의-스코프-차이"><code>var</code>와 <code>let</code>의 스코프 차이</h3>
<p>두 코드를 비교해 보면 차이가 더 분명해진다.</p>
<pre><code class="language-javascript">if (true) {
  var a = 10;
}

console.log(a); // 10</code></pre>
<p><code>var</code>는 블록을 무시하고
<strong>함수 전체에서 접근 가능하다.</strong></p>
<p>반면 <code>let</code>은 다음과 같다.</p>
<pre><code class="language-javascript">if (true) {
  let b = 10;
}

console.log(b); // ReferenceError</code></pre>
<p><code>let</code>은 <strong>블록을 벗어나면 접근할 수 없다.</strong></p>
<p>즉 변수의 <strong>사용 범위를 더 안전하게 제한할 수 있다.</strong></p>
<h3 id="재할당은-가능하지만-재선언은-불가능">재할당은 가능하지만 재선언은 불가능</h3>
<p><code>let</code>은 값을 <strong>다시 할당하는 것은 가능</strong>하지만
같은 이름으로 <strong>다시 선언하는 것은 불가능하다.</strong></p>
<pre><code class="language-javascript">let count = 1;
count = 2; // 가능</code></pre>
<p>하지만 다음 코드는 에러가 발생한다.</p>
<pre><code class="language-javascript">let count = 1;
let count = 2; // SyntaxError</code></pre>
<p>이 덕분에 실수로 같은 변수를 다시 선언하는 문제를
미리 막을 수 있다.</p>
<h3 id="let이-해결한-문제"><code>let</code>이 해결한 문제</h3>
<p><code>let</code>은 <code>var</code>의 설계로 인해
발생할 수 있는 혼란을 줄이기 위해 도입된 변수 선언 방식이다.</p>
<ul>
<li>블록 스코프 지원</li>
<li>중복 선언 방지</li>
<li>더 예측 가능한 코드 구조</li>
</ul>
<p>그래서 현대 JavaScript에서는
새로운 코드에서 <code>var</code> 대신 <strong><code>let</code>과 <code>const</code>를 사용하는 것이 일반적인 관례</strong>가 되었다.</p>
<p>하지만 여기서 한 가지 질문이 생긴다.</p>
<blockquote>
<p>재할당이 가능한 변수라면
값이 바뀌지 않는 변수는 어떻게 관리해야 할까?</p>
</blockquote>
<p>이 질문에 대한 답이 바로 <strong><code>const</code></strong>다.</p>
<hr>
<h2 id="4-const-변하지-않는-변수">4. <code>const</code>: 변하지 않는 변수</h2>
<p><code>let</code>이 등장하면서 <code>var</code>의 많은 문제가 해결되었지만
여전히 한 가지 고민이 남아 있었다.</p>
<blockquote>
<p>값이 절대 바뀌지 않는 변수는 어떻게 표현할까?</p>
</blockquote>
<p>이 문제를 해결하기 위해 ES6에서는
<strong><code>const</code></strong>라는 새로운 변수 선언 방식이 추가되었다.</p>
<h3 id="const는-재할당이-불가능하다"><code>const</code>는 재할당이 불가능하다</h3>
<p><code>const</code>는 <strong>한 번 값을 할당하면 다시 재할당할 수 없는 변수</strong>다.</p>
<pre><code class="language-javascript">const name = &quot;Alice&quot;;

name = &quot;Bob&quot;; // 에러 발생</code></pre>
<p>이 코드는 다음과 같은 에러가 발생한다.</p>
<pre><code class="language-text">TypeError: Assignment to constant variable</code></pre>
<p>즉 <code>const</code>는 <strong>재할당이 불가능한 변수</strong>다.</p>
<h3 id="선언과-동시에-값-할당">선언과 동시에 값 할당</h3>
<p><code>const</code>는 선언할 때 반드시 <strong>초기값을 함께 작성해야 한다.</strong></p>
<pre><code class="language-javascript">const count = 10;</code></pre>
<p>다음 코드는 에러가 발생한다.</p>
<pre><code class="language-javascript">const count; // SyntaxError</code></pre>
<p>왜냐하면 <code>const</code>는 <strong>재할당이 불가능한 변수</strong>이기 때문에
선언 시점에 초기값이 반드시 필요하기 때문이다.</p>
<h3 id="객체와-배열에서는-조금-다르게-동작한다">객체와 배열에서는 조금 다르게 동작한다</h3>
<p>여기서 많은 사람들이 헷갈리는 부분이 하나 있다.</p>
<p><code>const</code>로 선언한 <strong>객체나 배열의 프로퍼티나 요소는 변경할 수 있다.</strong></p>
<pre><code class="language-javascript">const user = {
  name: &quot;Alice&quot;,
};

user.name = &quot;Bob&quot;; // 가능</code></pre>
<p>이 코드에는 에러가 발생하지 않는다.</p>
<p>이유는 <code>const</code>가 <strong>객체 내부 값까지 보호하는 것이 아니라</strong>
<strong>변수의 참조 자체를 변경하지 못하게 하는 것</strong>이기 때문이다.</p>
<p>즉 다음 코드는 에러가 발생한다.</p>
<pre><code class="language-javascript">const user = { name: &quot;Alice&quot; };

user = { name: &quot;Bob&quot; }; // 에러</code></pre>
<p>객체 <strong>자체를 새로운 객체로 바꾸는 것</strong>은 불가능하다.</p>
<h3 id="기본이-되어버린-const">기본이 되어버린 <code>const</code></h3>
<p>현대 JavaScript에서는 변수를 선언할 때
다음과 같은 <strong>사용 패턴이 널리 사용된다.</strong></p>
<ul>
<li><strong>기본은 <code>const</code></strong></li>
<li>값이 바뀌어야 할 때만 <code>let</code></li>
<li><strong><code>var</code>는 거의 사용하지 않음</strong></li>
</ul>
<pre><code class="language-javascript">const name = &quot;Alice&quot;;
let count = 0;</code></pre>
<p>이 방식은 코드의 의도를 더 명확하게 만든다.</p>
<p><code>const</code>로 선언된 변수를 보면
<strong>&quot;이 값은 바뀌지 않는다&quot;</strong>는 것을 바로 알 수 있기 때문이다.</p>
<hr>
<h2 id="5-실제로-쓰이는-방식">5. 실제로 쓰이는 방식</h2>
<p>지금까지 <code>var</code>, <code>let</code>, <code>const</code>의 특징을 살펴봤다면
다음으로 중요한 것은 <strong>실제 코드에서 어떻게 사용하는가</strong>다.</p>
<p>현재 JavaScript에서는 보통 다음과 같은 기준을 사용한다.</p>
<ul>
<li>기본은 <code>const</code></li>
<li>값이 바뀌면 <code>let</code></li>
<li><code>var</code>는 사용하지 않음</li>
</ul>
<p>즉 변수 선언의 기본 출발점은 항상 <strong><code>const</code></strong>다.</p>
<h3 id="기본은-const">기본은 <code>const</code></h3>
<p>대부분의 변수는 한 번 값이 정해지면
프로그램 실행 동안 <strong>다시 바뀌지 않는 경우가 많다.</strong></p>
<p>예를 들어 이런 값들이다.</p>
<pre><code class="language-javascript">const API_URL = &quot;https://api.example.com&quot;;
const MAX_COUNT = 10;
const userName = &quot;Alice&quot;;</code></pre>
<p>이런 값들은 프로그램 중간에 변경될 필요가 없다.</p>
<p>그래서 처음부터 <strong><code>const</code>로 선언하는 것이 기본적인 방식</strong>이다.</p>
<h3 id="값이-바뀌어야-하면-let">값이 바뀌어야 하면 <code>let</code></h3>
<p>반대로 값이 바뀌는 경우에는 <code>let</code>을 사용한다.</p>
<p>예를 들어 카운트 값이나 반복문 변수 같은 경우다.</p>
<pre><code class="language-javascript">let count = 0;

count = count + 1;
count = count + 1;</code></pre>
<p>또는 반복문에서도 자주 사용된다.</p>
<pre><code class="language-javascript">for (let i = 0; i &lt; 5; i++) {
  console.log(i);
}</code></pre>
<p>이처럼 <strong>값이 변하는 변수</strong>는 <code>let</code>을 사용한다.</p>
<h3 id="var는-사실상-사용하지-않는다"><code>var</code>는 사실상 사용하지 않는다</h3>
<p>현대 JavaScript에서는 새로운 코드에서 <code>var</code>를 사용하는 경우가 거의 없다.
이유는 이미 살펴본 것처럼 <code>var</code>의 특성이 코드 이해를 어렵게 만들 수 있기 때문이다.</p>
<ul>
<li>블록 스코프가 없다</li>
<li>중복 선언이 가능하다</li>
<li>호이스팅으로 예측이 어렵다</li>
</ul>
<p>그래서 대부분의 코드 스타일 가이드에서는
<strong><code>var</code> 사용을 금지하거나 권장하지 않는다.</strong></p>
<h3 id="현재-javascript의-변수-선언-기준">현재 JavaScript의 변수 선언 기준</h3>
<p>정리하면 현재 JavaScript에서 변수 선언은
다음과 같은 기준으로 사용된다.</p>
<pre><code class="language-javascript">const userName = &quot;Alice&quot;; // 기본
let count = 0; // 값이 바뀌는 경우</code></pre>
<p>즉 개발자는 보통 이렇게 생각한다.</p>
<blockquote>
<p>이 값이 바뀔까?</p>
</blockquote>
<ul>
<li><strong>바뀌지 않는다 → <code>const</code></strong></li>
<li><strong>바뀐다 → <code>let</code></strong></li>
</ul>
<p>이 방식이 코드의 의도를 가장 명확하게 표현할 수 있기 때문이다.</p>
<hr>
<h2 id="6-핵심-정리">6. 핵심 정리</h2>
<ul>
<li>JavaScript에는 <strong>세 가지 변수 선언 방식</strong>이 있다: <code>var</code>, <code>let</code>, <code>const</code></li>
<li>초기 JavaScript에서는 <strong><code>var</code>만 존재</strong>했지만 여러 <strong>설계 특성으로 인해</strong> 현대 코드에서는 사용이 줄어들었다</li>
<li><code>var</code>는 <strong>함수 스코프, 중복 선언 가능, 호이스팅</strong> 등의 특징 때문에 예측하기 어려운 코드를 만들 수 있다</li>
<li>ES6에서 등장한 <code>let</code>과 <code>const</code>는 이러한 <strong>특성으로 인해 발생할 수 있는 혼란을 줄이기 위해</strong> 추가되었다</li>
<li><code>let</code>은 <strong>블록 스코프를 가지며 재할당은 가능하지만 재선언은 불가능</strong>하다</li>
<li><code>const</code>는 <strong>재할당이 불가능하며 선언과 동시에 초기화가 필요하다</strong></li>
<li>객체나 배열을 <code>const</code>로 선언해도 <strong>프로퍼티나 요소 수정은 가능하지만 참조 자체를 바꾸는 것은 불가능하다</strong></li>
<li>현대 JavaScript에서는 <strong>기본적으로 <code>const</code>를 사용하고, 값이 변경될 경우에만 <code>let</code>을 사용한다</strong></li>
<li><code>var</code>는 과거 코드에서 주로 볼 수 있으며 <strong>새로운 코드에서는 거의 사용하지 않는다</strong></li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript – 복사했는데 왜 같이 바뀔까?: 얕은 복사와 깊은 복사]]></title>
            <link>https://velog.io/@hogu__giriboy/JavaScript-%EB%B3%B5%EC%82%AC%ED%96%88%EB%8A%94%EB%8D%B0-%EC%99%9C-%EA%B0%99%EC%9D%B4-%EB%B0%94%EB%80%94%EA%B9%8C-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC</link>
            <guid>https://velog.io/@hogu__giriboy/JavaScript-%EB%B3%B5%EC%82%AC%ED%96%88%EB%8A%94%EB%8D%B0-%EC%99%9C-%EA%B0%99%EC%9D%B4-%EB%B0%94%EB%80%94%EA%B9%8C-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC</guid>
            <pubDate>Mon, 16 Mar 2026 05:16:38 GMT</pubDate>
            <description><![CDATA[<h2 id="1-그대로-복사했지만-바뀌는-건-원본">1. 그대로 복사했지만, 바뀌는 건 원본</h2>
<p>JavaScript를 공부하다 보면 한 번쯤 이런 상황을 만나게 된다.</p>
<pre><code class="language-javascript">const user = { name: &quot;Alice&quot; };

const copy = user;

copy.name = &quot;Bob&quot;;

console.log(user.name); //Bob</code></pre>
<p>코드를 보면 <code>user</code>를 <code>copy</code>에
<strong>복사한 것처럼 보이지만 실제로는 같은 객체를 참조하게 된다.</strong></p>
<p>그래서 많은 사람들이 이렇게 생각한다.</p>
<blockquote>
<p>copy를 수정했으니까 copy만 바뀌겠지?</p>
</blockquote>
<p>하지만 실제 결과는 그렇지 않다.</p>
<p><code>copy.name</code>을 <code>&quot;Bob&quot;</code>으로 바꾸면
같은 객체를 참조하고 있기 때문에 <strong>원본인 <code>user.name</code>도 <code>&quot;Bob&quot;</code>으로 바뀐다.</strong></p>
<p>즉 이런 상황이 된다.</p>
<ul>
<li>user → name: Bob</li>
<li>copy → name: Bob</li>
</ul>
<p>여기서 자연스럽게 이런 질문이 생긴다.</p>
<blockquote>
<p>왜 복사했는데 같이 바뀔까?</p>
</blockquote>
<blockquote>
<p>JavaScript에서 복사는 어떻게 동작하는 걸까?</p>
</blockquote>
<p>이 질문의 답을 이해하려면
먼저 <strong>JavaScript에서 데이터가 어떻게 저장되는지</strong>를 알아야 한다.</p>
<p>특히 중요한 포인트는 하나다.</p>
<p>JavaScript에서 객체 변수에는 <strong>객체 자체가 저장되는 것이 아니라</strong>
<strong>객체를 가리키는 참조 값이 저장된다.</strong></p>
<p>이 개념을 이해하면
왜 이런 일이 발생하는지 바로 보이기 시작한다.</p>
<hr>
<h2 id="2-javascript의-객체-저장법">2. JavaScript의 객체 저장법</h2>
<p>앞에서 봤던 예제를 다시 보면 이런 코드였다.</p>
<pre><code class="language-javascript">const user = { name: &quot;Alice&quot; };
const copy = user;

copy.name = &quot;Bob&quot;;</code></pre>
<p>겉으로 보면 <code>user</code>가 <code>copy</code>로 <strong>복사된 것처럼 보인다.</strong>
하지만 JavaScript 내부에서는 실제로 <strong>복사가 일어나지 않는다.</strong></p>
<p>왜냐하면 <strong>객체와 배열은 변수에 객체 자체가 저장되는 것이 아니라</strong>
<strong>객체를 가리키는 참조 값이 저장되기 때문</strong>이다.</p>
<h3 id="원시-타입은-값이-복사된다">원시 타입은 값이 복사된다</h3>
<p>먼저 <strong>원시 타입</strong>을 보자.</p>
<pre><code class="language-javascript">let a = 10;
let b = a;

b = 20;

console.log(a); //10</code></pre>
<p>이 경우에는 <code>a</code>의 <strong>값 자체가 복사된다.</strong></p>
<p>즉 메모리에서는 이렇게 된다.</p>
<ul>
<li>a → 10</li>
<li>b → 10</li>
</ul>
<p>그래서 <code>b</code>를 바꿔도 <code>a</code>는 영향을 받지 않는다.</p>
<h3 id="객체는-참조가-복사된다">객체는 참조가 복사된다</h3>
<p>하지만 <strong>객체나 배열은 다르게 동작한다.</strong></p>
<pre><code class="language-javascript">const user = { name: &quot;Alice&quot; };
const copy = user;</code></pre>
<p>이때 실제 구조는 이런 느낌이다.</p>
<pre><code class="language-text">user ─┐
      ├──&gt; { name: &quot;Alice&quot; }
copy ─┘</code></pre>
<p><code>user</code>와 <code>copy</code>가 <strong>같은 객체를 가리키고 있는 상태</strong>다.</p>
<p>그래서 <code>copy</code>를 수정하면</p>
<pre><code class="language-javascript">copy.name = &quot;Bob&quot;;</code></pre>
<p>실제로는 <strong>같은 객체를 수정하는 것</strong>이 된다.</p>
<pre><code class="language-text">user ─┐
      ├──&gt; { name: &quot;Bob&quot; }
copy ─┘</code></pre>
<p>여기서 중요한 포인트는
객체나 배열에서 <code>=</code>로 대입하면
<strong>객체 자체가 복사되는 것이 아니라 같은 객체를 가리키는 참조 값이 복사된다.</strong></p>
<p>그래서 개발에서는
<strong>객체를 &quot;진짜로 복사&quot;하는 방법</strong>이 필요해진다.</p>
<hr>
<h2 id="3-겉만-복사되는-얕은-복사">3. 겉만 복사되는 얕은 복사</h2>
<p>앞에서 봤듯이 객체를 단순히 대입하면</p>
<pre><code class="language-javascript">const copy = user;</code></pre>
<p>이건 <strong>복사가 아니라 같은 객체를 가리키는 것</strong>이었다.</p>
<p>그래서 개발에서는 <strong>객체를 새로 만들어 값을 복사하는 방법</strong>을 사용한다.
대표적인 방법이 바로 <strong>Spread 문법(<code>...</code>)</strong>이다.</p>
<pre><code class="language-javascript">const user = { name: &quot;Alice&quot; };

const copy = { ...user };

copy.name = &quot;Bob&quot;;

console.log(user.name); // Alice</code></pre>
<p>이번에는 <code>copy</code>를 수정해도
<strong>원본인 <code>user</code>는 바뀌지 않는다.</strong></p>
<p>즉 이번에는 <strong>정말로 복사가 된 것처럼 보인다.</strong></p>
<p>하지만 여기서 중요한 포인트가 있다.</p>
<p>Spread로 복사한 것은 <strong>완전한 복사가 아니라 &quot;얕은 복사&quot;</strong>다.</p>
<h3 id="중첩-객체에서-발생하는-문제">중첩 객체에서 발생하는 문제</h3>
<p>객체 안에 <strong>객체가 들어 있는 경우</strong>를 보자.</p>
<pre><code class="language-javascript">const user = {
  name: &quot;Alice&quot;,
  address: {
    city: &quot;Seoul&quot;,
  },
};

const copy = { ...user };

copy.address.city = &quot;Busan&quot;;

console.log(user.address.city); // Busan</code></pre>
<p>이번에는 다시 <strong>원본이 같이 바뀌어 버린다.</strong></p>
<p>왜 이런 일이 생길까?</p>
<p>Spread는 <strong>객체의 최상위 프로퍼티만 복사하기 때문</strong>이다.</p>
<p>즉 구조는 이렇게 된다.</p>
<pre><code class="language-text">user
 ├ name → &quot;Alice&quot;
 └ address ─┐
            └ { city: &quot;Seoul&quot; }

copy
 ├ name → &quot;Alice&quot;
 └ address ─┘ (같은 객체)</code></pre>
<p><code>name</code> 같은 <strong>원시 값은 새로운 값으로 복사되지만</strong>
<code>address</code> 같은 <strong>객체는 참조 값이 복사된다.</strong></p>
<p>그래서 <code>copy.address.city</code>를 바꾸면
<strong>같은 객체를 보고 있는 <code>user</code>도 같이 바뀐다.</strong></p>
<p>이처럼 <strong>객체의 최상위 프로퍼티만 복사되고 내부 객체는 참조가 공유되는 방식</strong>을
<strong>얕은 복사 (Shallow Copy)</strong>라고 한다.</p>
<hr>
<h2 id="4-완전히-분리되는-깊은-복사">4. 완전히 분리되는 깊은 복사</h2>
<p>앞에서 본 <strong>얕은 복사</strong>는 객체의 <strong>최상위 프로퍼티만 복사</strong>된다.
그래서 내부에 객체가 있으면 여전히 <strong>같은 데이터를 공유하게 된다.</strong></p>
<p>이 문제를 해결하려면 <strong>내부 객체까지 전부 새로 복사</strong>해야 한다.
이것을 <strong>깊은 복사(Deep Copy)</strong>라고 한다.</p>
<p>깊은 복사는 <strong>객체 내부에 있는 모든 중첩 객체까지</strong>
<strong>재귀적으로 복사하여 완전히 새로운 객체 구조를 만드는 것</strong>이다.</p>
<h3 id="json을-이용한-깊은-복사">JSON을 이용한 깊은 복사</h3>
<p>가장 흔히 사용되는 방법 중 하나는 JSON을 이용하는 방식이다.</p>
<pre><code class="language-javascript">const user = {
  name: &quot;Alice&quot;,
  address: {
    city: &quot;Seoul&quot;,
  },
};

const copy = JSON.parse(JSON.stringify(user));

copy.address.city = &quot;Busan&quot;;

console.log(user.address.city); // Seoul</code></pre>
<p>이 방식은 객체를 JSON 문자열로 변환한 뒤 다시 객체로 변환한다.
이 과정에서 <strong>직렬화 가능한 데이터만을 기준으로 새로운 객체가 생성된다.</strong></p>
<p>그래서 이제 구조는 이렇게 된다.</p>
<pre><code class="language-text">user ──&gt; { name: &quot;Alice&quot;, address: { city: &quot;Seoul&quot; } }

copy ──&gt; { name: &quot;Alice&quot;, address: { city: &quot;Seoul&quot; } }</code></pre>
<p>두 객체가 <strong>완전히 분리된 상태</strong>다.</p>
<p>그래서 <code>copy</code>를 수정해도
<code>user</code>에는 영향을 주지 않는다.</p>
<h3 id="structuredclone">structuredClone</h3>
<p>최근에는 JavaScript에서 <strong>깊은 복사를 위한 내장 함수</strong>도 제공한다.</p>
<pre><code class="language-javascript">const copy = structuredClone(user);</code></pre>
<p>이 함수는 객체 구조를 <strong>깊게 복사하도록 설계된 API</strong>다.</p>
<p>그래서 JSON 방식보다 <strong>더 다양한 데이터 타입을 지원하며</strong>
<strong>깊은 복사를 수행할 수 있다.</strong></p>
<p>여기까지 정리해봤을 때 흐름이 이렇게 된다.</p>
<ul>
<li>단순 대입 → <strong>참조 공유</strong></li>
<li>Spread → <strong>얕은 복사</strong></li>
<li>structuredClone/JSON 방식 → <strong>깊은 복사</strong></li>
</ul>
<h2 id="5-실제-개발에서-복사의-중요성">5. 실제 개발에서 복사의 중요성</h2>
<p>얕은 복사와 깊은 복사는 단순히 문법 문제가 아니라
<strong>데이터의 참조 관계와 변경 방식</strong>과 연결된 개념이다.</p>
<p>개발을 하다 보면 객체나 배열의 <strong>데이터를 변경해야 하는 상황</strong>이 자주 생긴다.</p>
<p>예를 들어 어떤 사용자 데이터를 다룬다고 해보자.</p>
<pre><code class="language-javascript">const user = {
  name: &quot;Alice&quot;,
  age: 25,
};</code></pre>
<p>이 데이터를 수정하면서도 <strong>원본 데이터를 유지해야 하는 상황</strong>이 생길 수 있다.</p>
<p>그래서 보통은 <strong>기존 데이터를 복사한 뒤 후 복사본을 수정</strong>한다.</p>
<pre><code class="language-javascript">const updatedUser = { ...user };

updatedUser.age = 26;</code></pre>
<p>이렇게 하면 <strong>원본 데이터는 그대로 두고</strong>
새로운 데이터를 만들어 사용할 수 있다.</p>
<p>이 방식은 특히 다음과 같은 상황에서 중요해진다.</p>
<h3 id="상태-관리">상태 관리</h3>
<p>프론트엔드에서는 상태(state)를 관리할 때
<strong>기존 데이터를 직접 수정하지 않는 방식</strong>을 많이 사용한다.</p>
<p>대신 기존 데이터를 복사하고 필요한 부분만 수정한 뒤 새로운 상태로 교체한다.</p>
<p>이때 <strong>얕은 복사/깊은 복사 개념을 이해하지 못하면</strong>
의도하지 않게 <strong>원본 데이터까지 바뀌는 버그</strong>가 생길 수 있다.</p>
<h3 id="데이터-수정-과정">데이터 수정 과정</h3>
<p>객체 안에 객체가 있는 구조에서는
얕은 복사로 인해 <strong>중첩 데이터가 함께 변경되는 문제</strong>가 자주 발생한다.</p>
<p>그래서 데이터를 다룰 때는 <strong>얕은 복사로 충분한지,</strong>
<strong>내부 객체까지 복사하는 깊은 복사가 필요한지</strong> 상황에 맞게 판단해야 한다.</p>
<hr>
<h2 id="6-핵심-정리">6. 핵심 정리</h2>
<ul>
<li>JavaScript에서 <strong>객체와 배열 변수에는 객체 자체가 아니라 객체를 가리키는 참조 값이 저장</strong>된다.</li>
<li>객체나 배열에서 <code>=</code>로 대입하면 <strong>객체 자체가 복사되는 것이 아니라 같은 객체를 가리키는 참조 값이 복사된다.</strong></li>
<li>Spread 문법(<code>...</code>)은 <strong>얕은 복사</strong>로, 객체의 <strong>최상위 프로퍼티만 복사</strong>된다.</li>
<li>객체 안에 객체가 있는 경우 <strong>내부 객체는 여전히 같은 참조를 공유</strong>한다.</li>
<li>내부 데이터까지 완전히 분리하려면 <strong>깊은 복사</strong>가 필요하다.</li>
<li><code>JSON.parse(JSON.stringify())</code>나 <code>structuredClone()</code> 같은 방법을 사용하면 <strong>중첩 객체까지 새로 복사</strong>할 수 있다.</li>
<li>데이터를 수정할 때 <strong>원본을 유지해야 하는 상황</strong>에서는 복사 방식에 대한 이해가 중요하다.</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[요청과 응답으로 움직이는 웹: HTTP와 REST API]]></title>
            <link>https://velog.io/@hogu__giriboy/%EC%9A%94%EC%B2%AD%EA%B3%BC-%EC%9D%91%EB%8B%B5%EC%9C%BC%EB%A1%9C-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%EC%9B%B9-HTTP%EC%99%80-REST-API</link>
            <guid>https://velog.io/@hogu__giriboy/%EC%9A%94%EC%B2%AD%EA%B3%BC-%EC%9D%91%EB%8B%B5%EC%9C%BC%EB%A1%9C-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%EC%9B%B9-HTTP%EC%99%80-REST-API</guid>
            <pubDate>Sun, 15 Mar 2026 06:47:18 GMT</pubDate>
            <description><![CDATA[<h2 id="1-브라우저만으로는-아무고또-모타죠">1. 브라우저만으로는 아무고또 모타죠</h2>
<p>처음 JavaScript를 배우면 보통 이런 것들을 만든다.</p>
<ul>
<li>버튼 클릭 이벤트</li>
<li>DOM 수정</li>
<li>애니메이션</li>
<li>입력값 처리</li>
</ul>
<p>이걸 보면 자연스럽게
JavaScript는 화면을 움직이는 언어라고 생각하기 쉽다.</p>
<p>하지만 실제 웹 서비스는 전혀 다른 구조 위에서 움직인다.</p>
<p>예를 들어 이런 기능들을 생각해보자.</p>
<ul>
<li>로그인</li>
<li>게시글 목록</li>
<li>댓글 작성</li>
<li>좋아요</li>
<li>상품 주문</li>
</ul>
<p>이 데이터들은 <strong>브라우저 안에 존재하지 않고</strong>
모두 <strong>서버에 저장되어 있다.</strong></p>
<p>그래서 브라우저는 항상 이런 행동을 한다.</p>
<pre><code class="language-text">서버에게 요청
→ 데이터 받아오기
→ 화면에 표시</code></pre>
<p>즉 브라우저는 사실 <strong>데이터를 가져오는 창구</strong>에 가깝다.</p>
<p>조금 다른 비유로 생각해보면 이해가 쉽다.
브라우저를 <strong>식당 손님</strong>이라고 생각해보자.</p>
<pre><code class="language-text">손님(브라우저)
→ 주문 요청
→ 주방(서버)
→ 요리 제작
→ 음식 전달</code></pre>
<p>손님은 요리를 만들 수 없고,
<strong>주문하는 것</strong>만 할 수 있다.</p>
<p>웹도 똑같다.
브라우저는 데이터를 만들지도 못하고,
저장하지도 못하고,
관리하지도 못한다.
이 역할은 모두 <strong>서버</strong>가 담당한다.</p>
<p>예를 들어 우리가 어떤 사이트에서 게시글 목록을 본다고 생각해보자.</p>
<p>브라우저는 서버에게 이런 요청을 보낸다.</p>
<pre><code class="language-text">GET /posts</code></pre>
<p>그러면 서버는 이런 데이터를 반환한다.</p>
<pre><code class="language-json">[
  { &quot;id&quot;: 1, &quot;title&quot;: &quot;Hello&quot; },
  { &quot;id&quot;: 2, &quot;title&quot;: &quot;World&quot; }
]</code></pre>
<p>그리고 브라우저는 이 데이터를 받아서 <strong>화면에 렌더링</strong>한다.</p>
<p>결국 웹은 우리가 처음 생각했던 것처럼</p>
<pre><code class="language-text">화면 → 기능</code></pre>
<p>이 구조가 아니라</p>
<pre><code class="language-text">요청 → 응답 → 화면</code></pre>
<p>이 구조로 움직인다.</p>
<p>그래서 웹을 이해할 때는 <strong>화면보다 먼저 통신 구조를 이해하는 것</strong>이 중요하다.</p>
<p>이제 자연스럽게 다음 질문이 생긴다.</p>
<blockquote>
<p>브라우저는 어떤 방식으로 서버에게 요청을 보내는 걸까?</p>
</blockquote>
<p>이 질문의 답이 바로 다음에서 등장하는 <strong>HTTP</strong>다.</p>
<hr>
<h2 id="2-웹은-결국-요청과-응답으로-움직인다">2. 웹은 결국 요청과 응답으로 움직인다</h2>
<p>앞에서 브라우저는 데이터를 직접 만들거나 저장하지 못한다고 이야기했다.
그래서 브라우저는 항상 <strong>서버에게 요청을 보내고, 응답을 받는 방식</strong>으로 동작한다.</p>
<p>이때 등장하는 것이 바로 <strong>HTTP</strong>다.</p>
<p>HTTP는 <strong>HyperText Transfer Protocol</strong>의 약자로
브라우저와 서버가 데이터를 주고받을 때 사용하는 <strong>통신 규칙</strong>이다.</p>
<p>조금 쉽게 말하면 브라우저와 서버가
서로 대화할 때 사용하는 <strong>약속된 방식</strong>이라고 볼 수 있다.</p>
<p>웹에서 어떤 일이 일어나든 결국 이 구조 안에서 움직인다.</p>
<pre><code class="language-text">브라우저 → 요청(Request)
서버 → 처리
서버 → 응답(Response)
브라우저 → 화면 표시</code></pre>
<p>이 흐름은 우리가 웹에서 하는 거의 모든 행동에 적용된다.</p>
<ul>
<li>페이지를 열 때</li>
<li>데이터를 조회할 때</li>
<li>게시글을 작성할 때</li>
<li>댓글을 수정하거나 삭제할 때</li>
</ul>
<p>이 모든 과정은 결국 <strong>요청과 응답의 반복</strong>이다.</p>
<p>그래서 웹을 이해할 때는
페이지나 화면보다 먼저 <strong>통신 구조</strong>를 이해하는 것이 중요하다.</p>
<p>그리고 여기서 한 가지 질문이 자연스럽게 생긴다.</p>
<p>브라우저는 서버에게 단순히 요청만 보내는 것이 아니라
<strong>여러 종류의 행동</strong>을 요청할 수 있다.</p>
<p>예를 들어</p>
<ul>
<li>데이터를 가져오는 요청</li>
<li>새로운 데이터를 만드는 요청</li>
<li>데이터를 수정하는 요청</li>
<li>데이터를 삭제하는 요청</li>
</ul>
<p>같은 것들이다.</p>
<p>그렇다면 브라우저는 이런 <strong>다양한 행동을 어떻게 구분해서 요청할까?</strong></p>
<p>이 질문의 답이 바로 다음에서 등장하는 <strong>HTTP 메서드</strong>다.</p>
<hr>
<h2 id="3-같은-주소-다른-행동-http-메서드">3. 같은 주소 다른 행동: HTTP 메서드</h2>
<p>웹에서 서버에게 요청을 보낼 때는 항상 <strong>URL</strong>이 존재한다.</p>
<p>예를 들어 이런 주소가 있다고 가정해보자.</p>
<pre><code class="language-text">/posts</code></pre>
<p>이 주소는 게시글과 관련된 자원을 의미한다.</p>
<p>하지만 우리는 이 주소에 대해 <strong>여러 가지 행동</strong>을 할 수 있다.</p>
<p>게시글 목록을 조회할 수도 있고
새로운 게시글을 만들 수도 있고
게시글을 수정할 수도 있고
게시글을 삭제할 수도 있다.</p>
<p>이때 등장하는 것이 바로 <strong>HTTP 메서드</strong>다.</p>
<p>HTTP 메서드는 같은 주소라도
<strong>어떤 행동을 요청하는지 구분하는 역할</strong>을 한다.</p>
<h3 id="get-데이터를-가져오는-요청">GET: 데이터를 가져오는 요청</h3>
<p>GET은 <strong>데이터를 조회할 때 사용하는 메서드</strong>다.</p>
<p>예를 들어 게시글 목록을 가져오는 요청은 이렇게 표현된다.</p>
<pre><code class="language-text">GET /posts</code></pre>
<p>이 요청의 의미는 단순하다.</p>
<pre><code class="language-text">/posts에 있는 데이터를 보내주세요</code></pre>
<p>GET 요청은 <strong>서버의 데이터를 변경하지 않고</strong>
데이터를 <strong>조회할 때 사용</strong>된다.</p>
<h3 id="post-새로운-데이터를-만드는-요청">POST: 새로운 데이터를 만드는 요청</h3>
<p>POST는 <strong>새로운 데이터를 생성할 때 사용하는 메서드</strong>다.</p>
<p>예를 들어 새로운 게시글을 작성할 때는 이런 요청이 만들어진다.</p>
<pre><code class="language-text">POST /posts</code></pre>
<p>이 요청은 보통 <strong>본문(body)</strong>에 데이터를 담아 서버로 보낸다.</p>
<pre><code class="language-json">{
  &quot;title&quot;: &quot;Hello&quot;,
  &quot;content&quot;: &quot;First post&quot;
}</code></pre>
<p>서버는 이 데이터를 받아서 <strong>새로운 게시글을 생성</strong>한다.</p>
<h3 id="put과-patch-데이터를-수정하는-요청">PUT과 PATCH: 데이터를 수정하는 요청</h3>
<p>데이터를 수정할 때는 <strong>PUT</strong>과 <strong>PATCH</strong>를 사용한다.</p>
<p>두 메서드는 비슷해 보이지만 역할이 다르다.</p>
<ul>
<li><strong>PUT</strong> → 데이터를 전체 교체</li>
<li><strong>PATCH</strong> → 일부만 수정</li>
</ul>
<p>예를 들어 게시글을 수정하는 요청은 이렇게 보낼 수 있다.</p>
<pre><code class="language-text">PATCH /posts/1</code></pre>
<p>이 요청은 <strong>게시글 일부 정보를 수정</strong>한다는 의미다.</p>
<h3 id="delete-데이터를-삭제하는-요청">DELETE: 데이터를 삭제하는 요청</h3>
<p>DELETE는 <strong>데이터를 삭제할 때 사용하는 메서드</strong>다.</p>
<p>예를 들어 게시글 하나를 삭제하는 요청은 이렇게 표현된다.</p>
<pre><code class="language-text">DELETE /posts/1</code></pre>
<p>이 요청의 의미는 간단하다.</p>
<pre><code class="language-text">id가 1인 게시글을 삭제해주세요</code></pre>
<p>이처럼 HTTP 메서드는 같은 URL이라도
<strong>어떤 행동을 할 것인지 구분하는 역할</strong>을 한다.</p>
<p>정리하면 다음과 같다.</p>
<ul>
<li>GET: 조회</li>
<li>POST: 생성</li>
<li>PUT: 전체 수정</li>
<li>PATCH: 부분 수정</li>
<li>DELETE: 삭제</li>
</ul>
<p>그래서 웹 API를 설계할 때는
<strong>URL + HTTP 메서드</strong> 조합으로 동작을 정의한다.</p>
<p>이제 여기서 또 하나의 궁금증이 생긴다.</p>
<blockquote>
<p>우리가 요청을 보냈을 때
서버는 <strong>요청이 성공했는지 실패했는지 어떻게 알려줄까?</strong></p>
</blockquote>
<p>이 역할을 하는 것이 바로 다음에서 등장하는 <strong>HTTP 상태 코드</strong>다.</p>
<hr>
<h2 id="4-열려라-상태창-http-상태-코드">4. 열려라 상태창: HTTP 상태 코드</h2>
<h3 id="요청의-결과를-알려주는-숫자">요청의 결과를 알려주는 숫자</h3>
<p>브라우저가 서버에게 요청을 보내면
서버는 단순히 데이터만 보내는 것이 아니라 <strong>요청의 처리 결과</strong>도 함께 알려준다.</p>
<p>이때 사용하는 것이 <strong>HTTP 상태 코드</strong>다.</p>
<p>HTTP 상태 코드는 요청이
<strong>성공했는지, 문제가 있었는지, 혹은 다른 처리가 필요한지</strong>를 나타내는 숫자다.</p>
<p>브라우저는 이 코드를 보고 <strong>요청 결과를 판단</strong>한다.</p>
<h3 id="대표적인-상태-코드">대표적인 상태 코드</h3>
<p>예를 들어 어떤 페이지를 요청했을 때
서버는 이런 응답을 보낼 수 있다.</p>
<pre><code class="language-text">200 OK</code></pre>
<p>이 코드는 요청이 <strong>정상적으로 처리되었다는 의미다.</strong></p>
<p>하지만 항상 성공하는 것은 아니다.
요청한 데이터가 없거나, 서버에 문제가 생길 수도 있다.</p>
<p>예를 들어 이런 경우가 있다.</p>
<pre><code class="language-text">404 Not Found</code></pre>
<p>이 코드는 요청한 자원을 <strong>서버에서 찾을 수 없을 때</strong> 반환된다.</p>
<p>또는 서버 내부에서 오류가 발생하면 이런 응답이 돌아올 수도 있다.</p>
<pre><code class="language-text">500 Internal Server Error</code></pre>
<p>HTTP 상태 코드는
<strong>요청이 어떻게 처리되었는지 알려주는 신호</strong> 역할을 한다.</p>
<p>상태 코드는 보통 <strong>첫 번째 숫자</strong>로 큰 의미를 구분한다.</p>
<ul>
<li>2xx → 요청 성공</li>
<li>4xx → 클라이언트 요청 문제</li>
<li>5xx → 서버 오류</li>
</ul>
<p>그래서 브라우저나 JavaScript 코드에서는
이 상태 코드를 확인해서 <strong>성공 처리나 오류 처리를 분기</strong>하게 된다.</p>
<p>웹 개발을 하다 보면 이 숫자들을 굉장히 자주 보게 된다.
특히 <strong>개발자 도구의 Network 탭</strong>에서 쉽게 확인할 수 있다.</p>
<p>그렇다면 서버와 통신할 때
<strong>URL은 어떤 기준으로 설계해야 할까?</strong></p>
<p>이 질문에서 등장하는 개념이 바로 <strong>REST API</strong>다.</p>
<hr>
<h2 id="5-로마에선-로마법-url에선-rest-api">5. 로마에선 로마법, URL에선 REST API</h2>
<h3 id="규칙-없는-url-복잡복잡의-지름길">규칙 없는 URL, 복잡복잡의 지름길</h3>
<p>서버와 통신할 때는 항상 <strong>URL</strong>을 사용한다.</p>
<p>예를 들어 게시글과 관련된 기능을 만든다고 가정해보자.</p>
<p>규칙 없이 API를 만든다면 URL이 이런 식으로 만들어질 수도 있다.</p>
<pre><code class="language-text">/getPosts
/createPost
/updatePost
/deletePost</code></pre>
<p>이 방식도 동작은 하지만
URL이 <strong>행동 중심으로 설계되어 있어 구조가 점점 복잡해질 수 있다.</strong></p>
<p>기능이 많아질수록</p>
<pre><code class="language-text">/getUserPosts
/createUserPost
/deleteUserPost</code></pre>
<p>처럼 URL이 계속 늘어나게 된다.</p>
<p>그래서 웹에서는 API를 설계할 때
<strong>일관된 규칙을 사용하는 접근 방식</strong>이 필요해졌다.</p>
<p>여기서 등장한 개념이 바로 <strong>REST</strong>다.</p>
<h3 id="자원을-기준으로-url을-설계하는-방식-rest">자원을 기준으로 URL을 설계하는 방식: REST</h3>
<p>REST는 <strong>Representational State Transfer</strong>의 약자로
웹에서 API를 설계할 때 사용하는 <strong>규칙</strong>이다.</p>
<p>REST의 핵심 아이디어는 단순하다.</p>
<p><strong>URL은 자원을 표현한다.</strong></p>
<p>예를 들어 게시글 API라면 URL은 이렇게 표현된다.</p>
<pre><code class="language-text">/posts
/posts/1</code></pre>
<p>여기서 <code>/posts</code>는 게시글이라는 <strong>자원(resource)</strong>을 의미하고
<code>/posts/1</code>은 <strong>id가 1인 게시글</strong>을 의미한다.</p>
<p>즉 REST에서는 <strong>URL이 데이터의 대상을 표현</strong>한다.</p>
<h3 id="행동은-url이-아니라-http-메서드가-담당">행동은 URL이 아니라 HTTP 메서드가 담당</h3>
<p>REST 구조에서는 <strong>행동을 URL에 넣지 않는다.</strong></p>
<p>대신 <strong>HTTP 메서드</strong>가 그 역할을 담당한다.</p>
<p>예를 들어 게시글 API는 이렇게 표현할 수 있다.</p>
<pre><code class="language-text">GET /posts
POST /posts
PATCH /posts/1
DELETE /posts/1</code></pre>
<p>여기서 <code>/posts</code>는 <strong>게시글이라는 자원</strong>을 나타내고
각 요청의 행동은 <strong>HTTP 메서드</strong>가 결정한다.</p>
<p>그래서 REST API는 보통 이런 구조로 정리한다.</p>
<ul>
<li>URL → 자원</li>
<li>HTTP 메서드 → 행동</li>
</ul>
<p>이 방식의 장점은 API 구조가 <strong>일관되고 이해하기 쉬워진다</strong>는 점이다.</p>
<p>그래서 대부분의 웹 서비스는
이와 같은 <strong>REST 방식으로 API를 설계</strong>한다.</p>
<p>그렇다면 서버는 이런 요청에 대해
<strong>어떤 형식으로 데이터를 응답할까?</strong></p>
<p>이때 등장하는 것이 바로 <strong>JSON</strong>이다.</p>
<hr>
<h2 id="6-서버와-브라우저의-papago-json">6. 서버와 브라우저의 PAPAGO: JSON</h2>
<h3 id="서로-다른-환경이-데이터를-주고받는-문제">서로 다른 환경이 데이터를 주고받는 문제</h3>
<p>브라우저와 서버는 서로 <strong>다른 환경</strong>에서 동작한다.</p>
<ul>
<li>브라우저 → JavaScript</li>
<li>서버 → 다양한 언어 (Java, Python, Node.js 등)</li>
</ul>
<p>서로 사용하는 언어가 다르기 때문에
데이터를 그대로 보내면 <strong>서로 이해하지 못할 가능성</strong>이 생긴다.</p>
<p>예를 들어 JavaScript 객체는 이렇게 생겼다.</p>
<pre><code class="language-javascript">const post = {
  id: 1,
  title: &quot;Hello&quot;,
};</code></pre>
<p>하지만 이 객체 구조는 <strong>JavaScript 문법</strong>이다.</p>
<p>서버가 다른 언어로 만들어져 있다면
이 객체를 그대로 이해하기 어렵다.</p>
<p>그래서 웹에서는 데이터를 주고받을 때
<strong>모두가 이해할 수 있는 공통 형식</strong>을 사용한다.</p>
<p>이 역할을 하는 것이 바로 <strong>JSON</strong>이다.</p>
<h3 id="데이터를-표현하는-공통-형식-json">데이터를 표현하는 공통 형식: JSON</h3>
<p>JSON은 <strong>JavaScript Object Notation</strong>의 약자로
데이터를 표현하기 위한 <strong>텍스트 기반 데이터 형식</strong>이다.</p>
<p>JSON의 구조는 JavaScript 객체와 매우 비슷하다.</p>
<p>예를 들어 이런 JSON 데이터가 있을 수 있다.</p>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;Hello&quot;
}</code></pre>
<p>이 구조는 언어와 상관없이
<strong>텍스트 형태로 데이터를 표현</strong>한다.</p>
<p>그래서 브라우저든 서버든
같은 방식으로 데이터를 읽을 수 있다.</p>
<p>즉 JSON은 웹에서 데이터를 주고받을 때 사용하는
<strong>공통 언어</strong>라고 볼 수 있다.</p>
<h3 id="서버-응답은-대부분-json">서버 응답은 대부분 JSON</h3>
<p>웹 API를 호출하면
서버는 보통 <strong>JSON 형식으로 데이터를 응답</strong>한다.</p>
<p>예를 들어 게시글 목록 요청을 보내면
이런 응답이 돌아올 수 있다.</p>
<pre><code class="language-json">[
  { &quot;id&quot;: 1, &quot;title&quot;: &quot;Hello&quot; },
  { &quot;id&quot;: 2, &quot;title&quot;: &quot;World&quot; }
]</code></pre>
<p>브라우저는 이 JSON 데이터를 받아서
JavaScript에서 사용할 수 있는 형태로 변환한 뒤
화면에 렌더링한다.</p>
<p>그렇다면 여기서 한 가지 궁금증이 생긴다.</p>
<p>JSON은 <strong>텍스트 기반 데이터 형식</strong>이다.</p>
<p>하지만 JavaScript에서는 보통 <strong>객체 형태</strong>로 데이터를 다룬다.</p>
<p>그렇다면 우리는 객체와 JSON 변환을 어떻게 해야 할까?</p>
<p>이 역할을 하는 것이 바로 다음에서 등장하는
<strong>JSON.stringify와 JSON.parse</strong>다.</p>
<hr>
<h2 id="7-객체와-문자열-사이를-오가는-두-개의-문">7. 객체와 문자열 사이를 오가는 두 개의 문</h2>
<p>앞에서 JSON은 <strong>텍스트 기반 데이터 형식</strong>이라고 했다.</p>
<p>하지만 JavaScript에서는 보통 데이터를 <strong>객체 형태</strong>로 다룬다.</p>
<p>예를 들어 이런 데이터가 있다고 해보자.</p>
<pre><code class="language-javascript">const post = {
  id: 1,
  title: &quot;Hello&quot;,
};</code></pre>
<p>이 객체는 JavaScript에서는 바로 사용할 수 있지만
서버와 통신할 때는 <strong>JSON 문자열 형태로 변환</strong>해야 한다.</p>
<p>반대로 서버에서 받은 JSON 데이터는
JavaScript에서 사용하려면 <strong>객체 형태로 변환</strong>해야 한다.</p>
<p>이 변환을 담당하는 것이 바로
<strong>JSON.stringify</strong>와 <strong>JSON.parse</strong>다.</p>
<h3 id="객체를-json-문자열로-jsonstringify">객체를 JSON 문자열로: JSON.stringify</h3>
<p><code>JSON.stringify</code>는 JavaScript 객체를
<strong>JSON 문자열로 변환하는 함수</strong>다.</p>
<p>예를 들어 이런 객체가 있다고 해보자.</p>
<pre><code class="language-javascript">const post = {
  id: 1,
  title: &quot;Hello&quot;,
};</code></pre>
<p>이 객체를 JSON 문자열로 변환하면 이렇게 된다.</p>
<pre><code class="language-javascript">JSON.stringify(post);</code></pre>
<p>결과는 다음과 같다.</p>
<pre><code class="language-json">{ &quot;id&quot;: 1, &quot;title&quot;: &quot;Hello&quot; }</code></pre>
<p>이 문자열은 <strong>JSON 형식의 데이터</strong>이기 때문에
서버로 전송할 수 있다.</p>
<p>그래서 새로운 데이터를 서버로 보낼 때
보통 이런 코드가 등장한다.</p>
<pre><code class="language-javascript">fetch(&quot;/posts&quot;, {
  method: &quot;POST&quot;,
  body: JSON.stringify(post),
});</code></pre>
<p>즉 <strong>객체 → JSON 문자열</strong> 변환을 담당하는 것이
<code>JSON.stringify</code>다.</p>
<h3 id="json-문자열을-객체로-바꾸는-jsonparse">JSON 문자열을 객체로 바꾸는 JSON.parse</h3>
<p>반대로 서버에서 데이터를 받으면
JSON 문자열을 <strong>JavaScript 객체로 변환</strong>해야 한다.</p>
<p>이때 사용하는 것이 <code>JSON.parse</code>다.</p>
<p>예를 들어 이런 JSON 데이터가 있다고 해보자.</p>
<pre><code class="language-json">{ &quot;id&quot;: 1, &quot;title&quot;: &quot;Hello&quot; }</code></pre>
<p>이 데이터를 JavaScript에서 사용하려면
이렇게 변환한다.</p>
<pre><code class="language-javascript">JSON.parse(`{&quot;id&quot;:1,&quot;title&quot;:&quot;Hello&quot;}`);</code></pre>
<p>그러면 결과는 다시 <strong>객체 형태</strong>가 된다.</p>
<pre><code class="language-javascript">{
  id: 1,
  title: &quot;Hello&quot;
}</code></pre>
<p>그래서 <code>JSON.parse</code>는
<strong>JSON 문자열 → JavaScript 객체</strong> 변환을 담당한다.</p>
<h3 id="두-함수의-역할-정리">두 함수의 역할 정리</h3>
<p>정리하면 이 두 함수는 서로 반대 역할을 한다.</p>
<ul>
<li>JSON.stringify : 객체 → JSON 문자열</li>
<li>JSON.parse : JSON 문자열 → 객체</li>
</ul>
<p>웹 통신에서는 <strong>객체와 JSON 사이의 변환이 반복적으로 발생</strong>한다.</p>
<ul>
<li>데이터를 <strong>보낼 때</strong> → <code>JSON.stringify</code></li>
<li>데이터를 <strong>받을 때</strong> → <code>JSON.parse</code></li>
</ul>
<p>이 과정을 이해하면
브라우저와 서버 사이의 데이터 흐름이 훨씬 명확해진다.</p>
<p>이제 실제 통신이 어떻게 이루어지는지
<strong>브라우저 개발자 도구</strong>에서 직접 확인해보자.</p>
<hr>
<h2 id="8-진짜-통신은-network-탭에">8. 진짜 통신은 Network 탭에</h2>
<p>지금까지 HTTP, REST API, JSON 같은 개념을 살펴봤다.
하지만 웹에서 실제 통신이 어떻게 이루어지는지는 <strong>브라우저 개발자 도구</strong>에서 직접 확인할 수 있다.</p>
<h3 id="요청과-응답을-직접-확인하는-곳">요청과 응답을 직접 확인하는 곳</h3>
<p>브라우저에는 <strong>개발자 도구(Developer Tools)</strong>라는 기능이 있다.
여기에는 웹 페이지의 동작을 확인할 수 있는 여러 탭이 있는데, 그중 하나가 <strong>Network 탭</strong>이다.</p>
<p>Network 탭은 브라우저가 서버와 주고받는 <strong>모든 요청과 응답을 기록</strong>한다.</p>
<p>예를 들어 어떤 웹 페이지를 열면 브라우저는 자동으로 여러 요청을 보낸다.</p>
<ul>
<li>HTML 요청</li>
<li>CSS 요청</li>
<li>JavaScript 요청</li>
<li>이미지 요청</li>
<li>API 요청</li>
</ul>
<p>이 요청들은 모두 Network 탭에 기록된다.</p>
<h3 id="요청과-응답의-정보가-모두-보인다">요청과 응답의 정보가 모두 보인다.</h3>
<p>Network 탭에서 하나의 요청을 선택하면
서버와 통신하면서 오간 여러 정보를 확인할 수 있다.</p>
<p>대표적으로 다음과 같은 것들이 보인다.</p>
<ul>
<li>요청 URL</li>
<li>HTTP 메서드</li>
<li>HTTP 상태 코드</li>
<li>응답 데이터</li>
</ul>
<p>예를 들어 API 요청을 확인하면
서버가 반환한 <strong>JSON 데이터</strong>도 직접 볼 수 있다.</p>
<p>그래서 웹 개발을 하다 보면 Network 탭은
<strong>가장 자주 확인하게 되는 도구 중 하나</strong>가 된다.</p>
<h3 id="지금까지-살펴본-것들의-연결고리">지금까지 살펴본 것들의 연결고리</h3>
<p>지금까지 살펴본 개념들은 사실 모두 Network 탭에서 확인할 수 있다.</p>
<p>예를 들어 어떤 API 요청을 보면 다음과 같은 정보가 보인다.</p>
<pre><code class="language-text">GET /posts
Status: 200
Response: JSON 데이터</code></pre>
<p>여기에서 우리가 앞에서 배운 것들이 모두 등장한다.</p>
<ul>
<li>HTTP 메서드</li>
<li>HTTP 상태 코드</li>
<li>JSON 응답</li>
</ul>
<p>즉 Network 탭은 <strong>웹 통신이 실제로 어떻게 이루어지는지 보여주는 창</strong>이라고 볼 수 있다.</p>
<p>이제 우리는</p>
<ul>
<li>웹이 요청과 응답 구조로 움직인다는 것</li>
<li>HTTP 메서드로 요청의 행동을 구분한다는 것</li>
<li>상태 코드로 요청 결과를 확인한다는 것</li>
<li>JSON으로 데이터를 주고받는다는 것</li>
</ul>
<p>까지 살펴봤다.</p>
<h2 id="9-핵심정리">9. 핵심정리</h2>
<p>웹은 단순히 <strong>화면을 보여주는 기술</strong>이 아니라
브라우저와 서버가 <strong>요청과 응답을 주고받는 통신 구조</strong> 위에서 동작한다.</p>
<p>브라우저는 서버에게 요청을 보내고
서버는 요청을 처리한 뒤 응답을 반환한다.</p>
<p>이때 브라우저와 서버가 통신할 때 사용하는 규칙이 <strong>HTTP</strong>다.</p>
<p>HTTP에서는 같은 URL이라도
<strong>HTTP 메서드</strong>를 통해 요청의 행동을 구분한다.</p>
<p>대표적으로 다음과 같은 메서드들이 있다.</p>
<ul>
<li>GET → 데이터 조회</li>
<li>POST → 데이터 생성</li>
<li>PUT → 전체 수정</li>
<li>PATCH → 부분 수정</li>
<li>DELETE → 데이터 삭제</li>
</ul>
<p>요청이 처리되면 서버는 <strong>HTTP 상태 코드</strong>를 통해
요청 결과가 성공인지 실패인지 알려준다.</p>
<p>대표적인 상태 코드 범주는 다음과 같다.</p>
<ul>
<li>2xx → 요청 성공</li>
<li>4xx → 클라이언트 요청 오류</li>
<li>5xx → 서버 오류</li>
</ul>
<p>또한 웹에서는 데이터를 주고받을 때
<strong>JSON이라는 텍스트 기반 데이터 형식</strong>을 사용한다.</p>
<p>JavaScript에서는 JSON 데이터를 다루기 위해
다음 두 가지 변환을 사용한다.</p>
<ul>
<li>JSON.stringify: JavaScript 객체 → JSON 문자열</li>
<li>JSON.parse: JSON 문자열 → JavaScript 객체</li>
</ul>
<p>마지막으로 브라우저의 <strong>Network 탭</strong>을 사용하면
이러한 통신 과정을 실제로 확인할 수 있다.</p>
<ul>
<li>요청 URL</li>
<li>HTTP 메서드</li>
<li>HTTP 상태 코드</li>
<li>JSON 응답</li>
</ul>
<p>즉 웹은 <code>요청 → 처리 → 응답</code> 이라는 흐름 위에서 움직이며
HTTP, REST API, JSON 같은 개념들은 모두
이 통신 구조를 이해하기 위한 핵심 요소들이다.</p>
<p>이 구조를 이해하면 브라우저와 서버 사이에서
<strong>데이터가 어떻게 오가는지</strong> 훨씬 명확하게 보이기 시작한다.</p>
]]></description>
        </item>
    </channel>
</rss>