<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>슬기로운 코딩생활</title>
    <link>https://old-pumpkin.tistory.com/</link>
    <description>갓생살기 프로젝트</description>
    <language>ko</language>
    <pubDate>Tue, 30 Jun 2026 02:21:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>민슬기</managingEditor>
    <image>
      <title>슬기로운 코딩생활</title>
      <url>https://tistory1.daumcdn.net/tistory/6555269/attach/51fe662182ab41e283196f5ebfad4586</url>
      <link>https://old-pumpkin.tistory.com</link>
    </image>
    <item>
      <title>MVC부터 React의 단방향 데이터 흐름까지</title>
      <link>https://old-pumpkin.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React를 처음 공부할 때 &quot;왜 React는 이렇게 설계되었을까?&quot;라는 질문을 자연스럽게 갖게 된다. 그 답을 이해하려면 React 이전에 프론트엔드가 어떤 문제를 겪었는지 알아야 한다. MVC, MVVM, 관찰자 패턴 &amp;mdash; 이 개념들은 모두 React가 단방향 데이터 흐름을 선택하게 된 맥락과 직결된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVC란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC는 &lt;b&gt;Model&lt;/b&gt;, &lt;b&gt;View&lt;/b&gt;, &lt;b&gt;Controller&lt;/b&gt; 세 가지 역할로 코드를 나누는 아키텍처 패턴이다. 원래는 백엔드에서 많이 쓰이던 개념이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Model&lt;/b&gt;: 데이터와 비즈니스 로직을 담당한다. DB에서 데이터를 가져오고 어떻게 가공할지 결정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;View&lt;/b&gt;: 사용자가 직접 보는 화면, 즉 HTML과 UI 요소들을 담당한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Controller&lt;/b&gt;: 중간 관리자 역할로, 사용자 입력을 받아 Model을 업데이트하고 View에 반영한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;cos&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;사용자 입력 &amp;rarr; Controller &amp;rarr; Model 업데이트 &amp;rarr; View 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 앱이 커지면서 생긴다. View가 여러 Model을 참조하고, Model이 다른 Model을 업데이트하면서 의존성이 복잡하게 얽힌다. Facebook은 실제로 이 문제를 겪었는데, 대표적인 사례가 읽지 않은 메시지 숫자가 계속 틀리게 표시되는 버그였다. 어디서 상태가 바뀐 건지 추적하는 게 불가능해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVVM과 양방향 데이터 바인딩&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM은 MVC의 복잡성을 줄이기 위해 등장한 패턴이다. &lt;b&gt;Model&lt;/b&gt;, &lt;b&gt;View&lt;/b&gt;, &lt;b&gt;ViewModel&lt;/b&gt;로 구성되며, 핵심은 View와 ViewModel 사이의 &lt;b&gt;양방향 데이터 바인딩&lt;/b&gt;이다. Vue와 Angular가 이 패턴을 사용한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cos&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;Model &amp;harr; ViewModel &amp;harr; View&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue의 v-model이 대표적인 예시다. input 값이 바뀌면 데이터가 자동으로 바뀌고, 데이터가 바뀌면 화면도 자동으로 업데이트된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;html xml&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!-- Vue &amp;mdash; 양방향 바인딩 --&amp;gt;
&amp;lt;input v-model=&quot;username&quot; /&amp;gt;
&amp;lt;!-- input 바꾸면 username 자동 변경, username 바꾸면 input 자동 변경 --&amp;gt;

&amp;lt;!-- React &amp;mdash; 단방향 --&amp;gt;
&amp;lt;input value={username} onChange={(e) =&amp;gt; setUsername(e.target.value)} /&amp;gt;
&amp;lt;!-- 이벤트 &amp;rarr; 상태 변경 &amp;rarr; 리렌더링 순서를 직접 명시해야 한다 --&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향이라 개발이 편리하지만, 앱이 커지면 어디서 데이터가 변경되었는지 추적하기 어려워진다는 단점이 여전히 남아있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;관찰자 패턴&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관찰자(Observer Pattern) 패턴은 사실 이미 익숙하게 쓰고 있는 개념이다. 핵심은 이렇다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 대상의 상태가 바뀌면, 그것을 구독하고 있는 대상들에게 자동으로 알려준다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;Subject(발행자) &amp;rarr; 상태 변경 &amp;rarr; Observer들에게 notify&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;addEventListener가 바로 관찰자 패턴이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;button.addEventListener('click', handleClick)
// button = Subject
// handleClick = Observer&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 &lt;b&gt;State가 Subject&lt;/b&gt;다. State가 바뀌면 그것을 구독하고 있는 컴포넌트들이 자동으로 리렌더링된다. React의 핵심 동작 원리가 관찰자 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React의 단방향 데이터 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 MVC와 MVVM의 복잡성 문제를 &lt;b&gt;단방향 데이터 흐름&lt;/b&gt;으로 해결했다. 항상 이 방향만 존재한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cos&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;State &amp;rarr; View &amp;rarr; 이벤트 발생 &amp;rarr; State 변경 &amp;rarr; 리렌더링&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View가 직접 State를 건드리지 않는다. 이 덕분에 데이터가 어디서 왔는지 추적하기 쉽고, 같은 State면 항상 같은 화면이 나온다는 예측 가능성이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 철학이 &lt;b&gt;Flux 패턴&lt;/b&gt;으로 정립되었고, Redux와 Zustand 같은 상태관리 라이브러리도 이 연장선에 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cos&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;Action &amp;rarr; Dispatcher &amp;rarr; Store(State) &amp;rarr; View&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;MVC&lt;/td&gt;
&lt;td&gt;MVVM&lt;/td&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 흐름&lt;/td&gt;
&lt;td&gt;양방향 (복잡)&lt;/td&gt;
&lt;td&gt;양방향 (바인딩)&lt;/td&gt;
&lt;td&gt;단방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 사례&lt;/td&gt;
&lt;td&gt;Backbone.js&lt;/td&gt;
&lt;td&gt;Vue, Angular&lt;/td&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장점&lt;/td&gt;
&lt;td&gt;역할 분리&lt;/td&gt;
&lt;td&gt;편리한 동기화&lt;/td&gt;
&lt;td&gt;예측 가능성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단점&lt;/td&gt;
&lt;td&gt;앱 커지면 복잡&lt;/td&gt;
&lt;td&gt;추적 어려움&lt;/td&gt;
&lt;td&gt;코드량 많아짐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</description>
      <category>  STUDY/Frontend</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/60</guid>
      <comments>https://old-pumpkin.tistory.com/60#entry60comment</comments>
      <pubDate>Mon, 29 Jun 2026 23:44:28 +0900</pubDate>
    </item>
    <item>
      <title>z-index 무한루프에서 벗어나기 &amp;mdash; React Portal 제대로 이해하기</title>
      <link>https://old-pumpkin.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 스터디를 하면서 React Portal이라는 개념을 처음 알게 되었다. 평소에 z-index를 아무리 높게 줘도 모달이나 툴팁이 의도한 대로 안 보이는 경우가 종종 있었는데 이게 Portal로 깔끔하게 해결된다는 걸 배웠다. 그래서 이참에 왜 z-index가 안 먹히는지, Portal이 어떻게 그 문제를 해결하는지 정리해 봤다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;z-index가 안 먹히는 진짜 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;z-index를 아무리 높게 줘도 원하는 대로 안 되는 상황을 겪어본 적이 있다. 모달이 다른 요소 뒤에 숨거나, 툴팁이 카드 밖으로 나가지 못하고 잘려버리는 경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 &lt;b&gt;stacking context(쌓임 맥락)&lt;/b&gt; 때문이다. z-index는 같은 stacking context 안에서만 비교된다. 문제는 부모 요소가 아래 속성 중 하나라도 가지면 새로운 stacking context를 만든다는 점이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;position: relative/absolute/fixed/sticky + z-index 값이 있는 경우&lt;/li&gt;
&lt;li&gt;opacity가 1보다 작은 경우&lt;/li&gt;
&lt;li&gt;transform, filter, will-change 등이 적용된 경우&lt;/li&gt;
&lt;li&gt;overflow: hidden이 적용된 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 구조라면 문제가 생긴다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;&amp;lt;div style={{ position: 'relative', zIndex: 1, overflow: 'hidden' }}&amp;gt;
  &amp;lt;Tooltip style={{ position: 'absolute', zIndex: 9999 }}&amp;gt;
    안녕하세요
  &amp;lt;/Tooltip&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tooltip의 z-index: 9999는 어디까지나 부모 div가 만든 stacking context 안에서만 의미가 있다. 부모가 overflow: hidden이면 부모 밖으로 삐져나간 부분은 그냥 잘려버리고, z-index를 아무리 키워도 부모 바깥의 요소 위로는 올라갈 수 없다. &lt;b&gt;부모라는 틀 자체에 갇혀있는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Portal이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Portal은 컴포넌트를 &lt;b&gt;부모 DOM 트리 바깥의 다른 DOM 노드에 렌더링 할&lt;/b&gt; 수 있게 해주는 기능이다. react-dom에서 제공하는 createPortal을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { createPortal } from 'react-dom';

function MyModal({ children }) {
  return createPortal(
    children,
    document.getElementById('modal-root')
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createPortal(child, container) 형태로 호출하며, 첫 번째 인자는 렌더링 할 React 노드, 두 번째 인자는 실제로 삽입될 DOM 컨테이너다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Portal이 문제를 해결하는 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Portal은 z-index 싸움으로 해결하는 게 아니라 &lt;b&gt;DOM 위치를 통째로 옮겨서&lt;/b&gt; 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 트리상으로는 여전히 부모 컴포넌트의 자식이지만, 실제로 브라우저에 그려지는 DOM 위치는 body 바로 아래 같은 곳으로 빠져나간다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;일반 렌더링                  Portal 사용
────────────────             ────────────────
body                         body
└── Card div                 ├── Card div
    └── Tooltip (잘림)        └── Tooltip (portal로 렌더링)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 Card의 overflow: hidden이나 stacking context와 완전히 무관해지기 때문에 z-index 문제가 근본적으로 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 document.body에 직접 박기보다, index.html에 전용 컨테이너를 만들어 거기에 렌더링 하는 방식을 많이 쓴다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- index.html --&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div id=&quot;portal-root&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;createPortal(children, document.getElementById('portal-root'))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DOM 트리는 분리되지만 React 트리는 그대로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Portal을 처음 보면 헷갈릴 수 있는 부분이다. DOM 위치는 분리되어도 &lt;b&gt;React 컴포넌트 트리에서는 호출한 컴포넌트의 자식으로 그대로 존재&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 동작이 모두 유지된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모의 Context(useContext로 가져오는 값들)가 정상적으로 전달된다.&lt;/li&gt;
&lt;li&gt;Portal 안에서 발생한 이벤트가 React 트리를 따라 부모로 버블링 된다. DOM상으로는 멀리 떨어져 있어도 마찬가지다.&lt;/li&gt;
&lt;li&gt;React DevTools에서도 원래 위치의 자식으로 표시된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점이 단순히 document.body.appendChild(div)로 DOM을 직접 조작하는 것과 본질적으로 다른 이유다. 직접 조작하면 React의 상태 관리, 이벤트 시스템, Context 전부와 단절되어 버린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;1152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLpteq/dJMcadbjKMX/ZvuYOhaAWRraTXRpbToUFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLpteq/dJMcadbjKMX/ZvuYOhaAWRraTXRpbToUFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLpteq/dJMcadbjKMX/ZvuYOhaAWRraTXRpbToUFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLpteq%2FdJMcadbjKMX%2FZvuYOhaAWRraTXRpbToUFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;928&quot; height=&quot;1152&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;1152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어디에 쓰나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Portal이 주로 쓰이는 곳은 부모의 레이아웃 제약을 벗어나 화면 최상단에 떠야 하는 UI들이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모달&lt;/li&gt;
&lt;li&gt;툴팁&lt;/li&gt;
&lt;li&gt;드롭다운 메뉴&lt;/li&gt;
&lt;li&gt;토스트 / 알림&lt;/li&gt;
&lt;li&gt;컨텍스트 메뉴&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;z-index를 아무리 올려도 해결이 안 될 때는 숫자를 더 키우는 게 아니라 stacking context 문제를 의심해야 한다. Portal은 이 문제를 z-index 싸움이 아닌 &lt;b&gt;DOM 위치 자체를 바꾸는 방식으로 근본적으로 해결해 준다.&lt;/b&gt; 그러면서도 React 트리상의 관계는 그대로 유지되기 때문에 Context나 이벤트 처리를 따로 신경 쓸 필요가 없다는 점이 핵심이다.&lt;/p&gt;</description>
      <category>  STUDY/Frontend</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/58</guid>
      <comments>https://old-pumpkin.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 22 Jun 2026 22:04:18 +0900</pubDate>
    </item>
    <item>
      <title>SQL vs NoSQL</title>
      <link>https://old-pumpkin.tistory.com/55</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;SQL과 NoSQL이란?&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SQL (관계형 데이터베이스)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 표(테이블) 형태로 저장&lt;/li&gt;
&lt;li&gt;저장 전에 틀(스키마)을 미리 정해야 함&lt;/li&gt;
&lt;li&gt;데이터 간의 관계를 표현하는 데 강함&lt;/li&gt;
&lt;li&gt;대표 DB: MySQL, PostgreSQL, Oracle&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;NoSQL (비관계형 데이터베이스)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;표 형식 없이 자유로운 구조로 저장&lt;/li&gt;
&lt;li&gt;틀을 미리 안 정해도 됨 (유연함)&lt;/li&gt;
&lt;li&gt;대량의 데이터를 빠르게 처리하는 데 강함&lt;/li&gt;
&lt;li&gt;대표 DB: MongoDB, Redis, Cassandra&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;SQL&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;NoSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;데이터 형태&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;표 (행/열)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;문서, 키-값 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;스키마&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;고정 (미리 설계)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;유연 (자유롭게 변경)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;확장&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;서버 업그레이드&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;서버 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;트랜잭션&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;강력 (ACID)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;상대적으로 약함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;적합한 데이터&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;정형 데이터&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;비정형 데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;데이터 저장 방식 예시&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;SQL &lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;표 형태로 저장, 모든 행이 같은 컬럼을 가져야 한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;id&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;이름&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;나이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;슬기&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;짱구&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;✅ 구조가 일정하고 명확함&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;❌ &amp;ldquo;영희&amp;rdquo;에게만 주소를 추가하고 싶어도 모든 행에 주소 칸을 만들어야 함&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;NoSQL&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;자유로운 형태로 저장 (문서형 예시),&amp;nbsp;각 데이터마다 다른 항목을 가져도 된다.&lt;/p&gt;
&lt;pre class=&quot;Javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Javascript&quot;&gt;&lt;code&gt;{ &quot;이름&quot;: &quot;철수&quot;, &quot;나이&quot;: 25 }
{ &quot;이름&quot;: &quot;영희&quot;, &quot;나이&quot;: 30, &quot;취미&quot;: [&quot;독서&quot;, &quot;게임&quot;], &quot;주소&quot;: &quot;서울&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;✅ 항목이 달라도 저장 가능, 구조 변경이 자유로움&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;❌ 데이터 일관성 관리가 어려울 수 있음&lt;br /&gt;&lt;br /&gt;비유하자면 SQL은 모든 학생이 똑같은 칸을 채워야하는 학생 출석부 느낌이고, NoSQL은 사람마다 적고 싶은 것을 자유롭게 적는 메모장 느낌이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;사용하는 곳&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 언제 사용하는 것이 적합할까?&lt;br /&gt;&lt;br /&gt;1. SQL이 적합한 경우&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;은행, 결제 시스템 &amp;rarr; 정확성과 일관성이 중요할 때&lt;/li&gt;
&lt;li&gt;쇼핑몰 주문/재고 관리 &amp;rarr; 데이터 간 관계가 복잡할 때&lt;/li&gt;
&lt;li&gt;데이터 구조가 잘 안 바뀔 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;2. NoSQL이 적합한 경우&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인스타그램, 트위터 &amp;rarr; 게시물마다 데이터 구조가 다를 때&lt;/li&gt;
&lt;li&gt;실시간 채팅, 게임 &amp;rarr; 대규모 트래픽을 빠르게 처리할 때&lt;/li&gt;
&lt;li&gt;서비스 초기 개발 &amp;rarr; 구조가 자주 바뀔 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;NoSQL의 4가지 유형&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;유형&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;대표 DB&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;특징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;문서형&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;MongoDB, CouchDB&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;JSON 형태로 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;키-값형&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Redis, DynamoDB&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;빠른 단순 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;컬럼형&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Cassandra, HBase&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;대용량 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;그래프형&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Neo4j&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;관계 네트워크 표현&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>  STUDY/CS</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/55</guid>
      <comments>https://old-pumpkin.tistory.com/55#entry55comment</comments>
      <pubDate>Fri, 12 Jun 2026 21:57:14 +0900</pubDate>
    </item>
    <item>
      <title>Next.js App Router의 렌더링 방식 이해하기</title>
      <link>https://old-pumpkin.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 서버에서 HTML을 미리 생성해 초기 로딩과 SEO 문제를 해결한다.&amp;nbsp; 그런데 실제 프로젝트에서는 어떻게 사용할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js App Router는 렌더링, 데이터 패칭, API 작성, SEO 설정까지 하나의 구조 안에서 처리할 수 있도록 설계되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 App Router를 기준으로 자주 사용하는 기능을 정리해보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;App Router의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router에서는 서버 컴포넌트(Server Component)가 기본값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 보통 브라우저에서 데이터를 가져오고 화면을 만들었다. 반면, App Router는 필요한 HTML을 서버에서 먼저 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 버튼 클릭, 입력 처리처럼 상호작용이 필요한 부분만 클라이언트 컴포넌트(Client Component)로 내려보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터 조회는 서버에서 처리하고, 필요한 HTML을 미리 생성하여 인터랙션만 브라우저에서 담당하는 구조다. 이 구조 덕분에 초기 로딩 성능과 SEO를 모두 챙길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;렌더링 방식 선택하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router에서는 fetch의 캐시 전략에 따라 렌더링 동작이 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전 Pages Router에서는 getServerSideProps, getStaticProps 같은 함수를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 App Router에서는 서버 컴포넌트 안에서 fetch를 사용하는 것만으로도 렌더링 전략을 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. SSG : 정적으로 생성&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
    .then(r =&amp;gt; r.json())

  return (
    &amp;lt;div&amp;gt;
      {posts.map(post =&amp;gt; (
        &amp;lt;p key={post.id}&amp;gt;{post.title}&amp;lt;/p&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 동적 설정이 없다면 Next.js는 결과를 정적으로 캐시 한다. 빌드 시점에 HTML을 생성해 두고 이후 요청에는 캐시 된 결과를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 페이지에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 블로그&lt;/li&gt;
&lt;li&gt;회사 소개 페이지&lt;/li&gt;
&lt;li&gt;마케팅 랜딩 페이지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. SSR : 요청마다 생성&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;const posts = await fetch('https://api.example.com/posts', {
  cache: 'no-store',
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cache: 'no-store' 옵션을 사용하면 요청마다 새로운 HTML을 생성한다. 항상 최신 데이터를 보여줘야 하는 경우 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 페이지에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 재고&lt;/li&gt;
&lt;li&gt;관리자 대시보드&lt;/li&gt;
&lt;li&gt;사용자별 개인화 페이지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. ISR : 일정 주기로 갱신&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;const posts = await fetch('https://api.example.com/posts', {
  next: {
    revalidate: 60,
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISR은 SSG와 SSR의 중간 형태다. 정적 페이지처럼 빠르게 응답하면서도 일정 시간이 지나면 새로운 HTML을 다시 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뉴스 목록이나 상품 목록처럼 데이터가 자주 변경되지만 실시간까지는 필요 없는 경우에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 무엇을 사용할까&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상황&lt;/td&gt;
&lt;td&gt;방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;거의 변하지 않는 페이지&lt;/td&gt;
&lt;td&gt;SSG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일정 주기로 갱신되는 페이지&lt;/td&gt;
&lt;td&gt;ISR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요청마다 데이터가 달라지는 페이지&lt;/td&gt;
&lt;td&gt;SSR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자 인터랙션 중심 기능&lt;/td&gt;
&lt;td&gt;Client Component&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로젝트에서는 하나만 사용하는 경우보다 페이지별로 적절히 조합하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 패칭&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 자체를 async 함수로 만들 수 있다. 그래서 별도의 데이터 패칭 함수 없이 서버 컴포넌트 안에서 바로 데이터를 가져올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;export default async function ProductPage({
  params,
}: {
  params: { id: string }
}) {
  const product = await fetch(
    `https://api.example.com/products/${params.id}`
  ).then(r =&amp;gt; r.json())

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{product.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{product.price}원&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pages Router의 getServerSideProps보다 훨씬 단순한 구조다. 또한 서버에서 실행되기 때문에 API Key 같은 민감한 정보도 안전하게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트에서 데이터를 가져와야 하는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 데이터를 서버에서 가져올 수 있는 것은 아니다. 좋아요 버튼, 실시간 입력, 사용자 인터랙션처럼 브라우저 상태가 필요한 경우에는 클라이언트 컴포넌트를 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;'use client'

import { useState, useEffect } from 'react'

export default function LikeButton() {
  const [liked, setLiked] = useState(false)

  useEffect(() =&amp;gt; {
    fetch('/api/like')
      .then(r =&amp;gt; r.json())
      .then(data =&amp;gt; setLiked(data.liked))
  }, [])

  return (
    &amp;lt;button onClick={() =&amp;gt; setLiked(!liked)}&amp;gt;
      {liked ? '❤️' : ' '}
    &amp;lt;/button&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState, useEffect, 이벤트 핸들러를 사용하려면 반드시 use client가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Route Handler로 API 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router에서는 별도의 Express 서버 없이 API를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app/api 폴더 안에 route.ts 파일을 만들면 자동으로 API 엔드포인트가 생성된다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;app
└─ api
   └─ posts
      ├─ route.ts
      └─ [id]
         └─ route.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Route Handler를 사용하면 DB 연결, 인증, 파일 업로드 같은 백엔드 작업도 Next.js 안에서 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;metadata로 SEO 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;정적 Metadata&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;export const metadata = {
  title: '내 서비스',
  description: '서비스 설명',
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js가 자동으로 head 태그를 생성해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;페이지별 title 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;export const metadata = {
  title: {
    default: '내 서비스',
    template: '%s | 내 서비스',
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;export const metadata = {
  title: '블로그',
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;블로그 | 내 서비스
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;동적 Metadata&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;export async function generateMetadata({
  params,
}: {
  params: { id: string }
}) {
  const post = await getPost(params.id)

  return {
    title: post.title,
    description: post.summary,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세 페이지마다 다른 title, description, Open Graph 정보를 생성할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router의 핵심은 서버 컴포넌트를 기본값으로 사용한다는 점이다. 필요한 HTML은 서버에서 만들고, 인터랙션이 필요한 부분만 클라이언트로 내려보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 과정에서 아래와 같은 과정을 하나의 프레임워크 안에서 처리할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fetch로 렌더링 전략 선택&lt;/li&gt;
&lt;li&gt;async 컴포넌트로 데이터 패칭&lt;/li&gt;
&lt;li&gt;Route Handler로 API 작성&lt;/li&gt;
&lt;li&gt;metadata로 SEO 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React가 브라우저 중심 구조였다면 Next.js App Router는 서버와 클라이언트의 역할을 다시 분리해 성능과 개발 경험을 모두 개선하려는 방향에 가깝다.&lt;/p&gt;</description>
      <category>  STUDY/Frontend</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/54</guid>
      <comments>https://old-pumpkin.tistory.com/54#entry54comment</comments>
      <pubDate>Thu, 11 Jun 2026 23:22:29 +0900</pubDate>
    </item>
    <item>
      <title>Next.js는 React의 어떤 문제를 해결할까?</title>
      <link>https://old-pumpkin.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React를 처음 배우면 보통 이런 구조를 보게 된다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 처음 받는 HTML에는 실제 화면 내용이 거의 없다. 대신 브라우저가 JavaScript를 다운로드하고 실행한 뒤에야 React가 화면을 만들어낸다. 이런 방식을 &lt;b&gt;CSR(Client Side Rendering)&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 왜 React는 굳이 이런 구조를 선택했을까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React SPA가 등장한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 웹사이트는 보통 &lt;b&gt;MPA(Multi Page Application)&lt;/b&gt; 방식으로 동작했다. 그러나 페이지를 이동할 때마다 아래와 같은 문제가 발생했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 HTML 요청&lt;/li&gt;
&lt;li&gt;전체 페이지 새로고침&lt;/li&gt;
&lt;li&gt;화면 깜빡임&lt;/li&gt;
&lt;li&gt;상태 초기화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 이런 불편함을 해결하기 위해 등장했다. 브라우저 안에서 JavaScript로 화면을 만들고 필요한 부분만 다시 렌더링하면서 더 부드러운 사용자 경험을 제공한 것이다. 그래서 페이지 전환이 자연스럽고, 상태 관리가 쉬운 인터랙션 중심 서비스에 강한 구조를 만들 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CSR의 동작 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 CSR은 어떻게 동작할까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lbqfO/dJMcadWz2ic/5p5omdb0mRktw7vrGv2QUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lbqfO/dJMcadWz2ic/5p5omdb0mRktw7vrGv2QUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lbqfO/dJMcadWz2ic/5p5omdb0mRktw7vrGv2QUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlbqfO%2FdJMcadWz2ic%2F5p5omdb0mRktw7vrGv2QUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;368&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 앱을 빌드하면 서버는 처음에 빈 HTML을 보낸다. 브라우저는 이걸 받아도 그릴게 없다. JS 번들을 다운받고 &amp;rarr; 실행하고 &amp;rarr; React가 DOM을 그린 후에야 비로소 화면이 보인다. 이 동안 FCP는 0이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 사용자가 실제 콘텐츠를 보기 위해서는 JavaScript 실행이 끝날 때까지 기다려야 한다는 점이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;FCP (First Contentful Paint) 는 사용자가 처음으로 콘텐츠를 볼 수 있게 되는 시점을 의미한다.&lt;br /&gt;CSR 환경에서는 JavaScript 실행 전까지 실제 콘텐츠가 없기 때문에 FCP가 늦어질 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CSR의 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 초기 로딩 시 빈 화면이 보인다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 JavaScript를 다운로드하고 실행하기 전까지 화면을 제대로 그릴 수 없다. 번들 크기가 크거나 네트워크 환경이 느리면 사용자는 한동안 빈 화면을 보게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 자체의 문제가 아니라 &quot;모든 렌더링을 브라우저에 수행한다&quot;는 구조적 특징에 가까운 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. SEO에 불리하다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색엔진은 우선 HTML을 읽어 페이지 내용을 파악한다. 하지만 CSR 구조에서는 초기 HTML에 실제 콘텐츠가 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 검색엔진이 처음 받는 HTML은 이런 형태에 가깝다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 본문 콘텐츠, 메타데이터, Open Graph 정보 등이 제대로 포함되지 않을 수 있다.&amp;nbsp;블로그, 쇼핑몰, 랜딩페이지처럼 검색 유입이 중요한 서비스에서는 불리하게 작용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Next.js와 React의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Next.js는 무엇을 바꿨을까? 바로 렌더링 시점을 바꿨다. 서버에서 이미 내용이 채워진 HTML을 생성해 전달하는 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z7flN/dJMcaayPSmv/XryWhxFQFHMlQykQzg9V40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z7flN/dJMcaayPSmv/XryWhxFQFHMlQykQzg9V40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z7flN/dJMcaayPSmv/XryWhxFQFHMlQykQzg9V40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz7flN%2FdJMcaayPSmv%2FXryWhxFQFHMlQykQzg9V40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;368&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 이 HTML을 받는 순간 바로 화면을 그릴 수 있다.즉,&amp;nbsp; FCP가 즉시 발생하는 것이다. 사용자는 더 빠르게 화면을 볼 수 있고, 검색엔진도 콘텐츠를 쉽게 읽을 수 있다.&amp;nbsp;이 과정을 &lt;b&gt;사전 렌더링(Pre-rendering)&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hydration이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 HTML만 있으면 React는 어떻게 동작할까?&amp;nbsp;서버가 HTML을 미리 만들어 보내더라도 그 상태만으로는 단순한 정적 문서에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 버튼 클릭, 이벤트 처리 같은 React 기능은 아직 동작하지 않는다. 그래서 기존 HTML과 연결하여 이벤트를 등록하고 React 상태를 복원한다. 이 과정을 &lt;b&gt;Hydration(수화)&lt;/b&gt;라고 부른다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 &amp;ldquo;HTML을 다시 만드는 것&amp;rdquo;이 아니라 이미 존재하는 HTML에 React를 연결하는 과정이라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;화면은 빨리 보이는데 왜 아직 느릴 수 있을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 서버에서 HTML을 미리 생성하기 때문에 CSR보다 FCP를 앞당길 수 있다. 즉, 사용자는 더 빠르게 화면을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 화면이 보인다고 해서 바로 상호작용 가능한 것은 아니다. 예를 들어 버튼 클릭이나 상태 변경 같은 동작은 아직 바로 실행되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 실제로 사용자가 정상적으로 상호작용할 수 있게 되는 시점을 &lt;b&gt;TTI (Time To Interactive)&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, 화면은 빠르게 보이지만 &lt;/span&gt;&lt;span&gt;실제로 클릭하거나 동작하기까지는 JavaScript 실행이 필요하다. &lt;/span&gt;&lt;span&gt;SSR은 화면을 먼저 보여주는 데 강점이 있지만 결국 상호작용을 위해서는 브라우저에서 JavaScript가 실행되어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React의 CSR 구조는 SPA 경험에 강력한 장점을 제공했지만, 아래와 같은 문제도 함께 가져왔다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 로딩&lt;/li&gt;
&lt;li&gt;SEO&lt;/li&gt;
&lt;li&gt;JavaScript 의존성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 서버에서 미리 HTML을 생성하는 방식으로 이런 문제를 해결하려고 등장한 프레임워크다. 하지만 모든 페이지를 항상 SSR로 처리하는 것이 정답은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 따라 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;정적으로 미리 생성할지, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;요청마다 서버에서 렌더링할지, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;일정 주기로 다시 생성할지 &lt;/span&gt;선택이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  STUDY/Frontend</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/52</guid>
      <comments>https://old-pumpkin.tistory.com/52#entry52comment</comments>
      <pubDate>Mon, 8 Jun 2026 18:31:44 +0900</pubDate>
    </item>
    <item>
      <title>React 클라이언트 전역 상태 관리</title>
      <link>https://old-pumpkin.tistory.com/51</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태(State)란 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태란 UI를 결정하는 데이터다. 버튼 클릭 여부, 장바구니 목록, 로그인 여부 등 시간에 따라 변하고 그 변화가 화면에 반영되는 모든 데이터가 상태에 해당한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태는 크게 두 가지로 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지역 상태(Local State) : 특정 컴포넌트 안에서만 사용되는 상태다. useState로 관리하며, 모달 열림/닫힘이나 입력 폼 값처럼 해당 컴포넌트에서만 의미 있는 데이터가 여기에 해당한다.&lt;/li&gt;
&lt;li&gt;전역 상태(Global State) : 여러 컴포넌트에서 공유해야 하는 상태다. 로그인한 유저 정보, 장바구니 데이터처럼 컴포넌트 트리의 여러 곳에서 동시에 접근하고 변경해야 할 때 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 전역 상태 관리가 필요한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 기본적으로 부모에서 자식으로 데이터를 props를 통해 전달한다. 컴포넌트 depth가 얕을 때는 문제없지만, 트리가 깊어질수록 Props Drilling 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Props Drilling이란 중간 컴포넌트는 해당 데이터가 필요하지 않음에도 불구하고 하위 컴포넌트에 전달하기 위해 props를 계속 받아서 넘겨줘야 하는 상황이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;App (cartItems 보유)
 └── Layout (cartItems 전달만 함)
      └── Header (cartItems 전달만 함)
           └── CartIcon (cartItems 실제 사용)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서 Layout과 Header는 cartItems가 필요 없지만 전달을 위해 props를 받아야 한다. 컴포넌트 간 결합도가 높아지고, 중간 컴포넌트를 수정할 때마다 props 전달 코드도 함께 수정해야 해서 유지보수가 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 상태 관리는 이 문제를 해결한다. 상태를 컴포넌트 트리 바깥에 두고, 필요한 컴포넌트가 직접 꺼내 쓰는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태의 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 관리 도구를 선택하기 전에 관리하려는 상태가 어떤 종류인지 먼저 구분하는 것이 중요하다. 상태의 종류에 따라 적합한 도구가 달라지기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 상태 :&amp;nbsp;서버와 무관하게 브라우저 안에서만 존재하는 상태다. 장바구니, 모달 열림 여부, 다크모드 설정 등이 여기에 해당한다. Zustand, Jotai 같은 라이브러리가 적합하다.&lt;/li&gt;
&lt;li&gt;서버 상태 :&amp;nbsp;서버에서 가져온 데이터로, 캐싱과 동기화가 핵심 과제다. API 응답 데이터가 대표적이며 TanStack Query, SWR이 이 영역을 전담한다.&lt;/li&gt;
&lt;li&gt;URL 상태 :&amp;nbsp;주소창의 쿼리 파라미터나 경로에 담긴 상태다. 검색 필터, 현재 페이지 번호 등이 여기에 해당하며 React Router로 관리한다.&lt;/li&gt;
&lt;li&gt;폼 상태 : 입력값과 유효성 검사 상태로, React Hook Form 같은 전용 라이브러리가 일반적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 경우 전역 상태 관리 라이브러리에 서버 상태까지 넣으려다 복잡해진다. 클라이언트 상태와 서버 상태를 분리하는 것만으로도 전역 상태가 훨씬 단순해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Flux 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux는 Facebook이 React와 함께 고안한 데이터 흐름 설계 방식이다. 핵심은 단방향 데이터 흐름이다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;Action &amp;rarr; Dispatcher &amp;rarr; Store &amp;rarr; View
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태를 변경하고 싶으면 반드시 Action을 통해야 한다. View가 직접 상태를 건드리는 것을 허용하지 않는다. 예를 들어 장바구니에 상품을 추가하고 싶다면 &quot;ADD_ITEM&quot;이라는 Action을 발행하고, Store가 그 Action을 받아 상태를 업데이트한 뒤, View가 변경된 상태를 반영하는 순서로 흐른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 장점은 예측 가능성이다. 상태가 어디서 왜 바뀌었는지 Action 로그만 봐도 파악할 수 있어서 디버깅이 쉽다. 단점은 간단한 상태 변경에도 Action 정의, Reducer 작성 등 준비 코드가 많다는 것이다. Redux가 이 패턴의 대표 구현체고, Zustand는 같은 철학을 훨씬 단순하게 구현한 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Atomic 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Atomic 패턴은 상태를 하나의 큰 덩어리로 관리하는 대신, atom이라는 최소 단위로 쪼개서 관리하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux 패턴에서는 Store 하나에 여러 상태가 모여 있어서 Store의 일부가 바뀌면 해당 Store를 구독하는 컴포넌트가 모두 리렌더링 될 수 있다. Atomic 패턴은 이 문제를 해결한다. 각 컴포넌트가 필요한 atom만 골라서 구독하기 때문에, 그 atom이 변경될 때만 해당 컴포넌트가 리렌더링 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점은 리렌더링 최적화가 자연스럽게 이뤄진다는 것이다. 상태를 잘게 쪼갤수록 불필요한 리렌더링이 줄어든다. 단점은 atom이 너무 많아지면 어떤 atom이 어디서 쓰이는지 파악하기 어려워질 수 있다는 것이다. Jotai와 Recoil이 이 패턴을 따른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zustand&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand는 독일어로 &quot;상태&quot;를 의미한다. 2019년 Pmndrs 팀이 만들었으며, Redux의 복잡한 보일러플레이트 없이 전역 상태를 간단하게 관리하기 위해 설계됐다. 현재 React 생태계에서 가장 빠르게 성장하고 있는 상태 관리 라이브러리 중 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create 함수로 store를 정의하고, store 안에 상태와 상태를 변경하는 함수를 함께 선언한다. 컴포넌트에서는 커스텀 훅처럼 꺼내 쓰며 Provider로 감쌀 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;// store/useCartStore.js
import { create } from 'zustand'

const useCartStore = create((set) =&amp;gt; ({
  cartItems: [],

  addItem: (item) =&amp;gt; set((state) =&amp;gt; {
    const exists = state.cartItems.find(i =&amp;gt; i.id === item.id)
    if (exists) {
      return {
        cartItems: state.cartItems.map(i =&amp;gt;
          i.id === item.id ? { ...i, qty: i.qty + 1 } : i
        )
      }
    }
    return { cartItems: [...state.cartItems, { ...item, qty: 1 }] }
  }),

  removeItem: (id) =&amp;gt; set((state) =&amp;gt; ({
    cartItems: state.cartItems.filter(i =&amp;gt; i.id !== id)
  })),
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트에서는 필요한 상태나 함수만 selector로 꺼내 쓴다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;function CartIcon() {
  // cartItems만 구독 &amp;rarr; 다른 상태 변경 시 리렌더링 안 됨
  const cartItems = useCartStore((state) =&amp;gt; state.cartItems)
  return &amp;lt;span&amp;gt;장바구니 ({cartItems.length})&amp;lt;/span&amp;gt;
}

function ProductCard({ product }) {
  // addItem 함수만 꺼내옴
  const addItem = useCartStore((state) =&amp;gt; state.addItem)
  return &amp;lt;button onClick={() =&amp;gt; addItem(product)}&amp;gt;담기&amp;lt;/button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 Provider 없이 바로 사용할 수 있어 설정이 간단하고, 보일러플레이트가 거의 없다. selector를 통해 필요한 상태만 구독할 수 있어 불필요한 리렌더링을 줄일 수 있고 Redux DevTools와도 연동이 가능하다. 다만 자유도가 높은 만큼 팀 내 컨벤션을 별도로 정하지 않으면 store 구조가 사람마다 제각각이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Jotai&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jotai는 일본어로 &quot;상태(状態)&quot;를 의미한다. Zustand와 같은 Pmndrs 팀이 만들었으며, Recoil에서 영감을 받아 더 단순하게 설계한 Atomic 패턴 라이브러리다. Recoil이 사실상 관리 중단 상태가 되면서 그 후계자로 주목받고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태를 atom 단위로 쪼개서 정의하고, 각 컴포넌트는 필요한 atom만 구독한다. useState와 사용법이 거의 동일해서 러닝커브가 낮다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// store/cartAtoms.js
import { atom } from 'jotai'

// 기본 atom &amp;mdash; 장바구니 목록
export const cartAtom = atom([])

// derived atom &amp;mdash; cartAtom에서 파생된 총 금액
export const totalPriceAtom = atom(
  (get) =&amp;gt; get(cartAtom).reduce((sum, item) =&amp;gt; sum + item.price * item.qty, 0)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function CartIcon() {
  const [cartItems] = useAtom(cartAtom)
  return &amp;lt;span&amp;gt;장바구니 ({cartItems.length})&amp;lt;/span&amp;gt;
}

function TotalPrice() {
  // 읽기만 할 때는 useAtomValue
  const total = useAtomValue(totalPriceAtom)
  return &amp;lt;span&amp;gt;총 합계: ₩{total.toLocaleString()}&amp;lt;/span&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState와 사용법이 비슷해서 React에 익숙한 개발자라면 바로 적응할 수 있다. atom 단위 구독으로 리렌더링 최적화가 자연스럽게 이뤄지고, derived atom으로 계산된 값을 깔끔하게 관리할 수 있다. 다만 atom이 많아지면 관리가 복잡해질 수 있고, Zustand에 비해 국내 레퍼런스가 적다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;선택 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; Zustand &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; &amp;nbsp;Jotai &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;패턴&lt;/td&gt;
&lt;td&gt;Flux&lt;/td&gt;
&lt;td&gt;Atomic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상태 구조&lt;/td&gt;
&lt;td&gt;하나의 store&lt;/td&gt;
&lt;td&gt;잘게 쪼갠 atom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용법&lt;/td&gt;
&lt;td&gt;커스텀 훅&lt;/td&gt;
&lt;td&gt;useState와 유사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리렌더링 최적화&lt;/td&gt;
&lt;td&gt;selector로 직접 설정&lt;/td&gt;
&lt;td&gt;atom 단위로 자동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적합한 상황&lt;/td&gt;
&lt;td&gt;중대규모, 명확한 store 구조&lt;/td&gt;
&lt;td&gt;리렌더링 최적화가 중요할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 라이브러리가 절대적으로 좋은 것은 없다. 중요한 것은 도구 자체보다 어떤 상태를, 어디서, 어떻게 관리할지를 먼저 설계하는 것이다.&lt;/p&gt;</description>
      <category>  STUDY/Frontend</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/51</guid>
      <comments>https://old-pumpkin.tistory.com/51#entry51comment</comments>
      <pubDate>Sun, 7 Jun 2026 17:49:53 +0900</pubDate>
    </item>
    <item>
      <title>Storybook이란?  &amp;mdash; props가 많아질수록 개발이 느려진다고 느꼈다면</title>
      <link>https://old-pumpkin.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React로 컴포넌트를 만들다 보면 어느 순간 이런 상황을 마주하게 된다.&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;Button
  size=&quot;sm&quot;
  variant=&quot;danger&quot;
  disabled
  loading
  onClick={handleClick}
  fullWidth
  iconLeft={&amp;lt;TrashIcon /&amp;gt;}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;props가 하나둘 늘어나면서 &quot;이거 어떻게 쓰는 거지?&quot; 하고 직접 코드를 까봐야 하는 순간들이다.. 확인하려면 페이지에 직접 하드코딩하고, 저장하고, 브라우저 열고, 다시 되돌리는 이런 반복작업이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 속도가 느려지는 건 실력 문제가 아니라 구조 문제일 수 있다. 그 구조 문제를 해결하기 위해 나온 도구가 &lt;b&gt;Storybook&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wC7jY/dJMcaciX3Bf/OwiCXkURRq3o8MIGFNBDc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wC7jY/dJMcaciX3Bf/OwiCXkURRq3o8MIGFNBDc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wC7jY/dJMcaciX3Bf/OwiCXkURRq3o8MIGFNBDc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwC7jY%2FdJMcaciX3Bf%2FOwiCXkURRq3o8MIGFNBDc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;597&quot; height=&quot;185&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;185&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;!-- HTML 모드에서 직접 버튼처럼 만들기 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;display: inline-block; padding: 10px 16px; background: #FF4785; color: white; border-radius: 6px; text-decoration: none; font-weight: bold;&quot; href=&quot;https://storybook.js.org/tutorials/intro-to-storybook/react/ko/get-started/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;   Storybook 공식 튜토리얼 &amp;rarr; &lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Storybook이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Storybook은 UI 컴포넌트를 &lt;b&gt;앱과 완전히 독립적으로&lt;/b&gt; 개발하고, 시각화하고, 문서화할 수 있는 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 &lt;b&gt;컴포넌트만 따로 모아둔 브라우저 환경&lt;/b&gt;이라고 생각하면 된다. 페이지 전체를 켜지 않아도, 특정 컴포넌트만 꺼내서 다양한 상태로 확인할 수 있다. React뿐 아니라 Vue, Angular, Svelte도 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Storybook이 어떻게 동작하는지에 대해 알아보겠다. 우선 Storybook을 사용하려면 기존 컴포넌트 파일과 별도로 &lt;b&gt;Story 파일&lt;/b&gt;을 작성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Button.tsx          &amp;rarr;  기존 컴포넌트 (그대로 유지)
Button.stories.tsx  &amp;rarr;  내가 새로 만드는 Story 파일
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Story 파일에서는 &quot;이 컴포넌트를 어떤 상태로 보여줄 것인가&quot;를 정의한다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// Button.stories.tsx
export default {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'], // 자동 문서 페이지 생성
};

export const Primary = {
  args: { label: '확인', variant: 'primary' },
};

export const Disabled = {
  args: { label: '확인', disabled: true },
};

export const LoadingAndDisabled = {
  args: { label: '확인', disabled: true, loading: true },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Story를 등록해두면 브라우저에서 각 상태를 바로 확인할 수 있고, props 값도 패널에서 &lt;b&gt;실시간으로 조작&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 문서화의 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Storybook 내부에서는 &lt;b&gt;react-docgen-typescript라는&lt;/b&gt; 라이브러리를 사용해서 빌드 시점에 컴포넌트의 TypeScript 타입과 JSDoc 주석을 정적으로 분석한다.&lt;/p&gt;
&lt;pre class=&quot;scss&quot;&gt;&lt;code&gt;interface ButtonProps {
  /** 버튼에 표시될 텍스트 */
  label: string;
  /** 비활성화 여부 */
  disabled?: boolean;
  variant: 'primary' | 'secondary' | 'danger';
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 타입 정보가 자동으로 &lt;b&gt;Props 테이블&lt;/b&gt;로 변환된다. 즉, 외부 API를 호출하는 게 아니라 &lt;b&gt;내가 이미 작성한 TypeScript 코드가 그대로 문서가 된다.&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 118px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; text-align: center; width: 42.907%;&quot;&gt;&lt;b&gt;자동 생성되는 것&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center; width: 57.093%;&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 42.907%;&quot;&gt;Props 이름 / 타입 / 기본값&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 57.093%;&quot;&gt;TypeScript 타입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 42.907%;&quot;&gt;Props 설명&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 57.093%;&quot;&gt;JSDoc 주석 (/** */)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 42.907%;&quot;&gt;인터랙티브 컨트롤&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 57.093%;&quot;&gt;args 기반 자동 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 42.907%;&quot;&gt;코드 스니펫&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 57.093%;&quot;&gt;Story 코드 그대로&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 실제로는 뭐가 좋을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 엣지 케이스를 개발 중에 잡을 수 있다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loading이랑 disabled가 동시에 오는 케이스, 페이지 개발 중엔 놓치기 쉽다. Story로 그 조합을 미리 정의해 두면 UI가 깨지는 걸 사전에 발견할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 협업이 쉬워진다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자이너나 기획자한테 URL 하나만 던져줘도 직접 props를 조작하면서 확인할 수 있다. &quot;피그마랑 실제 구현이 다른데요?&quot;를 코드 없이 바로 비교 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 컴포넌트 설계가 좋아진다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Story를 쓰다 보면 자연스럽게 &quot;이 props 설계 이상한데?&quot; 를 개발 중에 발견하게 된다. 결국 Storybook을 쓰는 것 자체가 더 좋은 컴포넌트를 만드는 훈련이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디자인 시스템과 Storybook&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Storybook을 깊게 이해하려면 &lt;b&gt;디자인 시스템&lt;/b&gt;이라는 개념을 먼저 알아야 한다. 프로젝트 규모가 커질수록 이런 문제가 생긴다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 페이지의 버튼은 border-radius가 8px인데, B 페이지는 4px다. 어떤 곳은 primary 색이 #3B82F6이고, 어떤 곳은 #2563EB다. 팀원이 늘어날수록 UI가 조금씩 어긋나기 시작한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 해결하기 위해 나온 개념이 디자인 시스템이다. &lt;b&gt;디자인 시스템&lt;/b&gt;은 쉽게 말해 &quot;우리 서비스는 이렇게 생겼다&quot; 를 정의한 규칙과 도구의 집합이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;디자인 시스템의 구성요소&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 디자인 토큰&lt;b&gt; :&lt;/b&gt; 색상, 폰트, 간격, 그림자 같은 가장 기본적인 값을 변수로 정의한 것&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// tokens.ts
export const colors = {
  primary: '#3B82F6',
  danger: '#EF4444',
  gray100: '#F3F4F6',
};

export const spacing = {
  sm: '8px',
  md: '16px',
  lg: '24px',
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 정의해두면 팀 전체가 같은 값을 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 컴포넌트 라이브러리 : 토큰을 기반으로 만든 재사용 가능한 UI 컴포넌트. ex) Button, Input, Modal, Toast&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 문서 : 컴포넌트를 어떻게 쓰는지, 어떤 상태가 있는지, 언제 쓰면 안 되는지를 설명하는 가이드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Stroybook이 디자인 시스템에서 어떤 역할일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 시스템의 세 구성요소 중 &lt;b&gt;문서화 + 컴포넌트 시각화를&lt;/b&gt; 담당하는 게 바로 Storybook이다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;디자인 토큰    &amp;rarr;  코드로 정의 (CSS 변수, JS 객체)
컴포넌트      &amp;rarr;  React 컴포넌트로 구현
문서 + 시각화  &amp;rarr;  Storybook ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Storybook이 없다면 컴포넌트를 다 만들어도 &quot;이게 어떻게 생겼는지&quot;, &quot;어떻게 쓰는지&quot;를 팀원이 코드를 직접 열어봐야 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Storybook이 있으면 디자인 시스템이 &lt;b&gt;살아있는 문서&lt;/b&gt;가 된다. 코드가 바뀌면 문서도 자동으로 바뀌기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 기업 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국내외 많은 기업들이 Storybook으로 디자인 시스템을 외부에 공개하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;토스&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://github.com/toss/toss-design-system&quot;&gt;Toss Design System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;카카오&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://developers.kakao.com/docs/ko/documentation-guideline/document-style-open&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kakao Style Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://primer.style/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Primer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 회사들이 Storybook을 쓰는 이유는 단순하다. 수십 명의 개발자가 같은 컴포넌트를 쓰는 환경에서 문서가 코드와 따로 노는 순간 혼란이 생긴다. Storybook은 &lt;b&gt;코드 자체가 문서&lt;/b&gt;가 되도록 만들어주기 때문에 그 문제를 근본적으로 해결한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 126px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt; Storybook 없을 때 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt; Storybook 있을 때 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;props 확인&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;페이지에 직접 하드코딩&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;Story에서 실시간 조작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;팀원 공유&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;코드 직접 설명&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;URL 하나로 끝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;문서화&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;코드랑 따로 관리&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;코드가 곧 문서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;엣지케이스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;놓치기 쉬움&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;Story로 명시적 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;디자인 시스템&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;구성요소가 흩어져 있음&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;한 곳에서 시각화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 단순히 &quot;컴포넌트 문서화 도구&quot;로만 알고 있었는데, 디자인 시스템이라는 맥락에서 보니 왜 대기업들이 필수로 쓰는지가 이해됐다. 컴포넌트가 많아질수록, 팀이 커질수록, 문서와 코드가 따로 노는 문제가 커지고 Storybook은 그걸 구조적으로 해결하는 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;props가 많아질수록 개발이 답답하게 느려지는 경험을 해봤다면 Storybook이 그 문제를 어느 지점에서 해결해 주는지 바로 와닿을 것 같다.&lt;/p&gt;</description>
      <category>  STUDY/Frontend</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/50</guid>
      <comments>https://old-pumpkin.tistory.com/50#entry50comment</comments>
      <pubDate>Mon, 1 Jun 2026 20:51:37 +0900</pubDate>
    </item>
    <item>
      <title>오픈소스 협업에서 배운 PR 리뷰 흐름</title>
      <link>https://old-pumpkin.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;OSSCA JavaScript 오픈소스 과정에 참여하면서 PR을 올리고 리뷰를 주고받을 기회가 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 단순히 번역 내용을 수정하고 PR을 제출하는 것이 전부라고 생각했다. 하지만 실제 오픈소스 프로젝트에서는 코드를 작성하는 것만큼이나 리뷰를 주고받는 과정이 중요하다는 것을 알게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub 리뷰 시스템 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 코드에 댓글만 남기면 리뷰가 등록되는 줄 알았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 GitHub에서는 댓글을 작성한 뒤 &lt;/span&gt;&lt;span&gt;Submit review&lt;/span&gt;&lt;span&gt; 버튼까지 눌러야 실제 리뷰가 전달된다는 것을 뒤늦게 알게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lWcZA/dJMcad26t5b/0pknkddRE0IJKAa1cwE091/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lWcZA/dJMcad26t5b/0pknkddRE0IJKAa1cwE091/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lWcZA/dJMcad26t5b/0pknkddRE0IJKAa1cwE091/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlWcZA%2FdJMcad26t5b%2F0pknkddRE0IJKAa1cwE091%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1384&quot; height=&quot;425&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;425&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰를&amp;nbsp;제출할&amp;nbsp;때는&amp;nbsp;단순&amp;nbsp;댓글(Comment)&amp;nbsp;외에도&amp;nbsp;Approve,&amp;nbsp;Request&amp;nbsp;changes&amp;nbsp;같은&amp;nbsp;상태를&amp;nbsp;함께&amp;nbsp;전달할&amp;nbsp;수&amp;nbsp;있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cK0Fsk/dJMcafzVa74/0SS4RhdkagIve4Q2jLEPg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cK0Fsk/dJMcafzVa74/0SS4RhdkagIve4Q2jLEPg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cK0Fsk/dJMcafzVa74/0SS4RhdkagIve4Q2jLEPg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcK0Fsk%2FdJMcafzVa74%2F0SS4RhdkagIve4Q2jLEPg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1014&quot; height=&quot;292&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 &quot;댓글을 남기는 기능&quot; 정도로 생각했는데, 실제로는 코드 리뷰와 수정 요청, 승인 과정을 GitHub 안에서 체계적으로 관리할 수 있도록 만들어진 시스템이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;단순히 코드를 수정하는 것이 아니라 협업 과정 자체를 관리한다는 점이 인상적이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 리뷰란 무엇일까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰를 처음 작성할 때는 &quot;여기 예시가 누락된 것 같아요.&quot; 같이 간단하게 의견만 남기는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;틀린 말은 아니지만, 상대방 입장에서는 무엇을 어떻게 수정해야 하는지 판단하기 어려울 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 다른 기여자들의 리뷰와 메인테이너의 피드백을 보면서 좋은 리뷰는 단순히 문제를 지적하는 것에서 끝나지 않는다는 것을 알게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPYY45/dJMcaar0Ls8/a1kad9pjINR4NEHNK5gBx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPYY45/dJMcaar0Ls8/a1kad9pjINR4NEHNK5gBx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPYY45/dJMcaar0Ls8/a1kad9pjINR4NEHNK5gBx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPYY45%2FdJMcaar0Ls8%2Fa1kad9pjINR4NEHNK5gBx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;503&quot; height=&quot;264&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 리뷰는 단순히 &quot;수정해 주세요&quot;가 아니라 왜 수정이 필요한지, 무엇과 통일하려는 건지, 어떻게 수정하면 좋을지까지 함께 전달한다. 상대방이 수정 방향을 이해하기 쉽고, 리뷰의 의도도 명확하게 전달할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;질문도 좋은 리뷰가 될 수 있다&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또 하나 인상 깊었던 점은 리뷰가 반드시 정답을 알려주는 형태일 필요는 없다는 것이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ExWaf/dJMcaalbQB2/Lw1TramalOAHTqWIUiPyEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ExWaf/dJMcaalbQB2/Lw1TramalOAHTqWIUiPyEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ExWaf/dJMcaalbQB2/Lw1TramalOAHTqWIUiPyEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FExWaf%2FdJMcaalbQB2%2FLw1TramalOAHTqWIUiPyEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;513&quot; height=&quot;607&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;607&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 리뷰가 &quot;수정 요청&quot;이라고만 생각했지만, 실제로는 변경 의도를 확인하고 함께 더 나은 방향을 찾기 위한 대화에 가까웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;오픈소스에서는 리뷰어가 항상 정답을 알고 있는 것이 아니다. &lt;/span&gt;&lt;span&gt;그래서 &quot;왜 이렇게 변경했나요?&quot;, &quot;이렇게 작성한 이유가 있을까요?&quot; 와 같은 질문도 충분히 의미 있는 리뷰가 될 수 있다는 것을 배웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;리뷰를 받는 사람의 입장&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 리뷰가 달리면 내가 잘못한 부분을 지적받는 것처럼 느껴지기도 했다. &lt;/span&gt;&lt;span&gt;하지만 여러 번 리뷰를 주고받으면서 생각이 달라졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;리뷰의 목적은 누군가의 실수를 찾는 것이 아니라 결과물을 더 좋게 만들기 위한 협업에 있었다. &lt;/span&gt;&lt;span&gt;실제로 리뷰를 통해 내가 놓친 부분을 발견하기도 했고, 반대로 내가 남긴 의견이 다른 사람의 작업 품질을 높이는 데 도움이 되기도 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;리뷰를 받는 과정 자체가 학습 과정이라는 점이 특히 인상 깊었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;혼자 프로젝트를 할 때는 commit과 push 정도만 사용했다. &lt;/span&gt;&lt;span&gt;하지만 오픈소스에서는 PR 작성, 리뷰 요청, 리뷰 반영, 재검토 과정까지 모두 협업의 일부였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 리뷰는 단순히 오류를 지적하는 과정이 아니라 수정 이유를 설명하고, 의도를 확인하며, 더 나은 방향을 함께 찾아가는 대화라는 점을 배울 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 경험을 통해 코드를 작성하는 능력뿐만 아니라 다른 사람과 함께 작업하는 방법도 중요한 개발 역량이라는 것을 느낄 수 있었다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>  DAILY/IT 활동</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/49</guid>
      <comments>https://old-pumpkin.tistory.com/49#entry49comment</comments>
      <pubDate>Mon, 1 Jun 2026 20:49:10 +0900</pubDate>
    </item>
    <item>
      <title>커밋 히스토리 정리하기 - Interactive Rebase 경험기</title>
      <link>https://old-pumpkin.tistory.com/48</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;OSSCA JavaScript 오픈소스 과정에 참여하면서 PR을 올렸다. &lt;/span&gt;&lt;span&gt;처음에는 단순히 리뷰를 반영하고, 오타를 수정하고, 충돌을 해결하는 데만 집중했다. 하지만 작업을 이어가다 보니 커밋 히스토리가 점점 지저분해졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;작업 과정 자체는 문제가 없었지만, 나중에 히스토리를 보니 하나의 기능이나 변경 사항을 설명하기보다는 작업 과정이 그대로 기록된 상태였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러던 중 메인테이너 분께 이런 피드백을 받았다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;오픈소스 PR이 머지됐을 때, 이 커밋이 유의미한지 생각해봐야 해요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그 말을 듣고 나서야 커밋을 바라보는 관점이 달라졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;오픈소스 프로젝트에서는 단순히 코드만 남는 것이 아니라 커밋 히스토리도 프로젝트의 기록으로 남는다. 따라서 각 커밋은 &quot;무엇을 변경했는가&quot;를 설명할 수 있어야 하고, 리뷰 반영이나 오타 수정 같은 과정 자체가 아니라 최종 변경 내용을 중심으로 정리되어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 커밋 상태 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;먼저 &lt;/span&gt;&lt;span&gt;git rebase -i&lt;/span&gt;&lt;span&gt;를 실행하여 내가 만든 커밋 목록을 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKNGaS/dJMcafUis8e/vradJxT8gwtG0akBKQnd2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKNGaS/dJMcafUis8e/vradJxT8gwtG0akBKQnd2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKNGaS/dJMcafUis8e/vradJxT8gwtG0akBKQnd2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKNGaS%2FdJMcafUis8e%2FvradJxT8gwtG0akBKQnd2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;954&quot; height=&quot;850&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span&gt;그리고 &lt;/span&gt;&lt;span&gt;git show --stat&lt;/span&gt;&lt;span&gt; 명령어를 사용해 각 커밋에 어떤 변경 사항이 들어있는지 하나씩 확인했다.&lt;/span&gt; &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;613&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6T8EL/dJMcadIPA0n/d9BB0sRny3TA99o8HtyCjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6T8EL/dJMcadIPA0n/d9BB0sRny3TA99o8HtyCjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6T8EL/dJMcadIPA0n/d9BB0sRny3TA99o8HtyCjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6T8EL%2FdJMcadIPA0n%2Fd9BB0sRny3TA99o8HtyCjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;613&quot; height=&quot;311&quot; data-origin-width=&quot;613&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Interactive Rebase 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;커밋을 정리하기 위해 Interactive Rebase를 사용했다. &lt;/span&gt;&lt;span&gt;자주 사용하는 옵션은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;squash&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 커밋을 이전 커밋과 합친다. &lt;/span&gt;&lt;span&gt;커밋 내용은 모두 유지되지만 커밋 경계가 사라진다. 이후 여러 커밋 메시지를 하나의 메시지로 정리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;fixup&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 커밋을 이전 커밋과 합친다. &lt;/span&gt;&lt;span&gt;동작 자체는 squash와 비슷하지만 현재 커밋의 메시지는 버리고 이전 커밋 메시지만 유지한다. &lt;/span&gt;&lt;span&gt;리뷰 반영이나 오타 수정처럼 별도 커밋 메시지가 필요 없는 경우 주로 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;reword&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;커밋 내용은 그대로 두고 커밋 메시지만 수정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;335&quot; data-start=&quot;322&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;335&quot; data-start=&quot;322&quot; data-ke-size=&quot;size23&quot;&gt;실제 커밋 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Part 1의 6.10 함수 바인딩 작업에는 다음과 같은 커밋들이 쌓여 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1780482757822&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;69a8e3e4f [충돌해결] Part1 6.10 함수 바인딩
f56d9b8b1 [충돌해결 및 번역] Part1 6.10 함수 바인딩 백틱 제거
88fe59f8f [충돌해결 및 번역] Part1 6.10 함수 바인딩
8a63e419a [번역] 리뷰 반영
50a37f8fe [번역] 리뷰 수정
0ce3339f0 [번역] 리뷰 반영
3a7315847 [번역] 피동형 표현 복구&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사실상 하나의 작업이 여러 개의 커밋으로 쪼개져 있는 상태였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 Interactive Rebase를 통해 하나의 의미 있는 커밋으로 정리하기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9a50S/dJMcaayGNqY/yZRqGx6iNYu8UDuIauZVN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9a50S/dJMcaayGNqY/yZRqGx6iNYu8UDuIauZVN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9a50S/dJMcaayGNqY/yZRqGx6iNYu8UDuIauZVN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9a50S%2FdJMcaayGNqY%2FyZRqGx6iNYu8UDuIauZVN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;376&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;충돌 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;커밋을 합치는 과정에서 충돌도 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;충돌이 발생하면 단순히 최신 버전을 선택하는 것이 아니라 각 커밋에 들어 있던 변경 사항을 확인하면서 실제로 남겨야 할 내용을 직접 선택해야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TX9Ob/dJMcaii9JHg/HuCNhIkTSTYQKc2fEf4q9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TX9Ob/dJMcaii9JHg/HuCNhIkTSTYQKc2fEf4q9K/img.png&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;407&quot; data-is-animation=&quot;false&quot; style=&quot;width: 56.8454%; margin-right: 10px;&quot; data-widthpercent=&quot;57.51&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TX9Ob/dJMcaii9JHg/HuCNhIkTSTYQKc2fEf4q9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTX9Ob%2FdJMcaii9JHg%2FHuCNhIkTSTYQKc2fEf4q9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWXsLc/dJMcacXxDiy/2YsbsJZqX4pleo7lF4xSvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWXsLc/dJMcacXxDiy/2YsbsJZqX4pleo7lF4xSvK/img.png&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;693&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;42.49&quot; style=&quot;width: 41.9918%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWXsLc/dJMcacXxDiy/2YsbsJZqX4pleo7lF4xSvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWXsLc%2FdJMcacXxDiy%2F2YsbsJZqX4pleo7lF4xSvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;888&quot; height=&quot;693&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커밋 합치기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;리뷰 반영, 오타 수정, 충돌 해결 커밋들은 모두 &lt;/span&gt;&lt;span&gt;fixup&lt;/span&gt;&lt;span&gt;으로 합쳤다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pWWDL/dJMcaf7L719/ibRYm3IGZE9kz62L0KHkXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pWWDL/dJMcaf7L719/ibRYm3IGZE9kz62L0KHkXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pWWDL/dJMcaf7L719/ibRYm3IGZE9kz62L0KHkXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpWWDL%2FdJMcaf7L719%2FibRYm3IGZE9kz62L0KHkXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;745&quot; height=&quot;375&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이후 &lt;/span&gt;&lt;span&gt;reword&lt;/span&gt;&lt;span&gt;를 사용해 최종 커밋 메시지를 변경했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyiWFF/dJMcafms2lI/DYlJLt9Chh2KdJ3KEhJzf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyiWFF/dJMcafms2lI/DYlJLt9Chh2KdJ3KEhJzf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyiWFF/dJMcafms2lI/DYlJLt9Chh2KdJ3KEhJzf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyiWFF%2FdJMcafms2lI%2FDYlJLt9Chh2KdJ3KEhJzf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;658&quot; height=&quot;650&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 남아 있던 Prettier 관련 커밋도 별도의 의미를 갖는 변경이 아니었기 때문에 Interactive Rebase를 다시 실행해 기존 커밋에 합쳤다.&lt;/p&gt;
&lt;pre id=&quot;code_1780482053555&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;squash 62b4912 fix: Prettier 자동 포맷팅 되돌리기 및 충돌 재해결&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PR 반영&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;모든 정리가 끝난 뒤 원격 저장소에 반영했다. &lt;/span&gt;&lt;span&gt;Rebase는 커밋 히스토리를 재작성하는 작업이므로 일반 &lt;/span&gt;&lt;span&gt;git push&lt;/span&gt;&lt;span&gt;는 사용할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 git push --force-with-lease를 사용했다. &lt;span&gt;--force&lt;/span&gt;&lt;span&gt;와 달리 다른 사람이 중간에 push 한 내용이 있는 경우 덮어쓰지 않기 때문에 조금 더 안전하게 히스토리를 업데이트할 수 있다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cu60w2/dJMcaaMbcaE/o0GXrJ2dkbgsFb876ldmY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cu60w2/dJMcaaMbcaE/o0GXrJ2dkbgsFb876ldmY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cu60w2/dJMcaaMbcaE/o0GXrJ2dkbgsFb876ldmY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcu60w2%2FdJMcaaMbcaE%2Fo0GXrJ2dkbgsFb876ldmY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;216&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 작업을 통해 &lt;b&gt;커밋을 남기는 것&lt;/b&gt;과 &lt;b&gt;좋은 커밋 히스토리를 만드는 것&lt;/b&gt;은 다르다는 것을 배웠다. 오픈소스에서는 코드의 결과뿐만 아니라 변경 과정도 함께 공유된다. 그래서 커밋 하나하나가 어떤 의도를 담고 있는지가 중요하다는 점을 배웠다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  STUDY/Git</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/48</guid>
      <comments>https://old-pumpkin.tistory.com/48#entry48comment</comments>
      <pubDate>Tue, 26 May 2026 22:50:19 +0900</pubDate>
    </item>
    <item>
      <title>React는 왜 Suspense를 만들었을까?</title>
      <link>https://old-pumpkin.tistory.com/47</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 스터디를 하면서 스터디원이 Suspense에 대해서 발표하였다. 내가 전혀 몰랐던 내용이라 흥미를 느꼈고, 프로젝트에서 사용하면 정말 유용할 거라는 생각이 들었다. 그래서 이번 기회로 Suspense를 다시 한번 제대로 공부하고 이해한 내용을 정리해보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 React의 비동기 처리 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 데이터를 가져올 때는 이렇게 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1779339210232&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() =&amp;gt; {
  fetchData().then(res =&amp;gt; {
    setData(res);
    setLoading(false);
  });
}, []);

if (loading) return &amp;lt;Spinner /&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 코드는 크게 문제가 없어 보이지만 실제 앱에서는 데이터가 하나만 있는 경우가 드물다. 사용자 정보, 게시글 목록, 댓글, 알림 등 여러 데이터를 동시에 가져와야 하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 앱에서의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 많아질수록 loading state가 많아지기 시작한다.&lt;/p&gt;
&lt;pre id=&quot;code_1779339278239&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const [userLoading, setUserLoading] = useState(true);
const [feedLoading, setFeedLoading] = useState(true);
const [commentLoading, setCommentLoading] = useState(true);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 조건문도 늘어난다.&lt;/p&gt;
&lt;pre id=&quot;code_1779339300063&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return (
  &amp;lt;&amp;gt;
    {profileLoading
      ? &amp;lt;ProfileSkeleton /&amp;gt;
      : &amp;lt;Profile /&amp;gt;}

    {feedLoading
      ? &amp;lt;FeedSkeleton /&amp;gt;
      : &amp;lt;Feed /&amp;gt;}
  &amp;lt;/&amp;gt;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트가 많아질수록 관리 복잡도가 증가하는 등 문제가 생기기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 Spinner만 보여주면 되지 않을까 생각했다. 그런데 실제 서비스에서는 다르다.&lt;/p&gt;
&lt;pre id=&quot;code_1779339643998&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;페이지
 ├─ 프로필
 ├─ 게시글
 └─ 추천 친구&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이렇게 여러 영역이 있다. 만약 프로필은 이미 로딩 완료 했지만 게시글은 느리다면? 사용자는 프로필이라도 먼저 보고 싶을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 보통은 이렇게 전체 페이지를 막아버리는 경우가 많다.&lt;/p&gt;
&lt;pre id=&quot;code_1779339692788&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (loading) return &amp;lt;Spinner /&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 페이지 전체가 하나의 로딩 상태에 묶여있는 상태가 된다.&amp;nbsp;그래서 부분별 로딩 처리가 필요해진다. 프로필을 먼저 보여주고 게시글만 skeleton으로 표시한다고 했을 때 기존 방식은 직접 관리해야 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1779339751714&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return (
  &amp;lt;&amp;gt;
    {profileLoading
      ? &amp;lt;ProfileSkeleton /&amp;gt;
      : &amp;lt;Profile /&amp;gt;}

    {feedLoading
      ? &amp;lt;FeedSkeleton /&amp;gt;
      : &amp;lt;Feed /&amp;gt;}
  &amp;lt;/&amp;gt;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트가 많아질수록 loading state가 증가하고, 조건문 증가, 관리 복잡도가 증가하는 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useEffect fetch의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나의 문제는 fetch가 useEffect 이후에 실행된다는 점이었다.&lt;/p&gt;
&lt;pre id=&quot;code_1779341154844&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  fetchUser().then(user =&amp;gt; {
    fetchPosts(user.id)
  })
}, [])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user fetch 완료 대기, 이후 posts fetch 시작 이 순서로 실행된다. 즉, fetch가 연쇄적으로 이어지게 되는데 이를 waterfall이라고 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;waterfall이란?&lt;br /&gt;하나의 작업이 끝나야 다음 작업이 시작되는 흐름을 의미한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Suspense는 무엇일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Suspense는 &lt;b&gt;준비되지 않은 컴포넌트의 렌더링을 잠시 보류하는 메커니즘&lt;/b&gt;이다. 데이터를 fetch하는 기능도 아니고 spinner를 보여주는 기능도 아니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Suspnese를 이해하기 가장 쉬운 예시는 lazy였다.&lt;/p&gt;
&lt;pre id=&quot;code_1779341328024&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Page from &quot;./Page&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 앱이 시작될 때 Page 컴포넌트 코드도 함께 다운로드된다. 그런데 여기서 문제가 발생한다. React가 &amp;lt;Page /&amp;gt;를 렌더링 하려고 했는데 아직 JS 다운로드가 끝나지 않은 상태가 된다. 즉, React 입장에서는 렌더링 하려는 컴포넌트가 아직 준비되지 않은 상황이 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;lazy란?&lt;br /&gt;
&lt;pre id=&quot;code_1779341366817&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Page = lazy(() =&amp;gt; import(&quot;./Page&quot;));​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 시점에 컴포넌트를 다운로드 하는 것이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Suspnse가 등장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1779341513918&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Suspense fallback={&amp;lt;Loading /&amp;gt;}&amp;gt;
  &amp;lt;Page /&amp;gt;
&amp;lt;/Suspense&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;React가 &amp;lt;Page /&amp;gt; 렌더링 시도&lt;/li&gt;
&lt;li&gt;아직 다운로드되지 않음&lt;/li&gt;
&lt;li&gt;렌더링 잠시 보류&lt;/li&gt;
&lt;li&gt;fallback UI 표시&lt;/li&gt;
&lt;li&gt;다운로드 완료 후 렌더링 재개&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 fallback은 기다리는 동안 보여주는 UI이다. 또, 이 영역을 Suspense Boundary라고 부른다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Boundary 내부에서 준비되지 않은 컴포넌트가 발생하면 fallback UI로 대체된다. Suspense는 &lt;b&gt;렌더링을 기다리는 경계(boundary)처럼 동작&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 Suspense를 단순히 loading UI를 보여주는 기능이라고 생각했다. 하지만 공부하면서 느낀 점은 &lt;b&gt;React가 준비되지 않은 컴포넌트를 어떻게 기다릴지&lt;/b&gt;를 렌더링 흐름 안에서 관리한다는 점이었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;기존에는 개발자가 loading state를 직접 만들고 조건문으로 UI를 분기했다면, Suspense는 React가 렌더링을 잠시 보류하고 fallback UI를 보여주는 방식으로 동작한다는 점이 인상적이었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>  STUDY/Frontend</category>
      <author>민슬기</author>
      <guid isPermaLink="true">https://old-pumpkin.tistory.com/47</guid>
      <comments>https://old-pumpkin.tistory.com/47#entry47comment</comments>
      <pubDate>Thu, 21 May 2026 14:39:20 +0900</pubDate>
    </item>
  </channel>
</rss>