<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>harubanbo 님의 블로그</title>
    <link>https://harubanbo.tistory.com/</link>
    <description>harubanbo 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 13 Aug 2026 03:00:45 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>harubanbo</managingEditor>
    <item>
      <title>패키지 업데이트 시 Breaking Change와 Peer Dependency 이해하기 2</title>
      <link>https://harubanbo.tistory.com/11</link>
      <description>&lt;h2&gt;그러면 이런 이슈들은 언제 어디서 확인할 수 있지?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Peer Dependency 경고는 보통 패키지 설치 시점&lt;/strong&gt;에 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;pnpm install&lt;/code&gt; 등을 실행하면 패키지 매니저가 peer dependency 관련 문제를 알려준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;WARN  Issues with peer dependencies found
packages/next
└─┬ next-intl 3.5.0
  └── ✕ unmet peer next@^14.0.0: found 15.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 peer dependency 이슈는 보통 빌드가 아니라 설치 시점에 먼저 드러난다.&lt;/p&gt;
&lt;p&gt;다만 패키지 매니저나 설정에 따라 단순 경고로 끝날 수도 있고, 설치가 실패할 수도 있다.&lt;/p&gt;
&lt;p&gt;설치가 되었다고 해서 무조건 안전한 것도 아니다. 빌드는 성공할 수도 있고, 런타임에서 에러가 날 수도 있다.&lt;/p&gt;
&lt;p&gt;반대로 &lt;strong&gt;Breaking Change 이슈는 빌드 시점 또는 런타임 시점에 발생&lt;/strong&gt;할 수 있어서 더 위험하다.&lt;/p&gt;
&lt;p&gt;예를 들어 아래와 같은 경우가 있다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시점&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;빌드 시&lt;/td&gt;
&lt;td&gt;타입이 바뀌어서 TypeScript 컴파일 에러 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빌드 시&lt;/td&gt;
&lt;td&gt;삭제된 API를 import해서 module not found 에러 발생&lt;/td&gt;
&lt;/tr&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;기존 테스트 케이스가 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이런 이슈들은 일부는 빌드 단계에서 바로 잡히지만, 일부는 빌드는 통과하는데 실제 기능 테스트를 해보면 동작이 달라지는 경우도 있다.&lt;/p&gt;
&lt;p&gt;그래서 패키지를 업데이트할 때는 단순히 설치와 빌드만 확인하는 것이 아니라, 주요 기능이 실제로 정상 동작하는지도 확인하는 것이 좋다.&lt;/p&gt;
&lt;p&gt;패키지 업데이트 시 모든 공식 사이트를 일일이 찾아가서 확인하기에는 부담이 있으므로, 아래와 같은 방식으로 핵심 변경사항을 확인하면 된다.&lt;/p&gt;
&lt;h2&gt;방법 1. 패키지의 CHANGELOG나 Release Note 확인하기&lt;/h2&gt;
&lt;p&gt;가장 정석적인 방법은 패키지의 &lt;code&gt;CHANGELOG&lt;/code&gt;나 Release Note를 확인하는 것이다.&lt;/p&gt;
&lt;p&gt;대부분의 패키지는 GitHub에 릴리즈 노트를 남긴다.&lt;/p&gt;
&lt;p&gt;예를 들면 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://github.com/vercel/next.js/releases&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 전체 내용을 처음부터 끝까지 다 읽기보다는 &lt;code&gt;BREAKING&lt;/code&gt;, &lt;code&gt;Breaking Changes&lt;/code&gt;, &lt;code&gt;Migration&lt;/code&gt; 같은 섹션을 우선 확인하면 된다.&lt;/p&gt;
&lt;h2&gt;방법 2. 마이그레이션 가이드 참고하기&lt;/h2&gt;
&lt;p&gt;메이저 버전 업데이트에서는 보통 공식 마이그레이션 가이드를 제공하는 경우가 많다.&lt;/p&gt;
&lt;p&gt;예를 들어 아래와 같은 문서가 있을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Next.js → Upgrading Guide
React → How to Upgrade&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;v13 → v14&lt;/code&gt;, &lt;code&gt;v14 → v15&lt;/code&gt;처럼 메이저 버전을 올리는 경우에는 단순히 패키지만 업데이트하지 말고, 해당 버전의 마이그레이션 가이드를 같이 확인하는 것이 좋다.&lt;/p&gt;
&lt;h2&gt;방법 3. npm outdated, pnpm outdated로 현황 파악하기&lt;/h2&gt;
&lt;p&gt;현재 프로젝트의 패키지들이 얼마나 오래되었는지 확인하려면 &lt;code&gt;npm outdated&lt;/code&gt;나 &lt;code&gt;pnpm outdated&lt;/code&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm outdated&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 결과가 아래처럼 나올 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Package       Current    Latest
next          14.1.0     15.2.0
tailwindcss   3.3.2      3.4.1
dayjs         1.11.10    1.11.13&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 버전 차이를 보고 업데이트 위험도를 대략적으로 판단할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;next
14.1.0 → 15.2.0
Major 업데이트이므로 breaking change 가능성이 있음

tailwindcss
3.3.2 → 3.4.1
Minor 업데이트이므로 대체로 안전한 편이지만 변경사항 확인 필요

dayjs
1.11.10 → 1.11.13
Patch 업데이트이므로 보통 버그 수정 수준일 가능성이 높음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정리하면 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Patch&lt;/strong&gt; (&lt;code&gt;1.0.0 → 1.0.1&lt;/code&gt;): 보통 버그 수정 수준이라 비교적 안전한 편&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minor&lt;/strong&gt; (&lt;code&gt;1.0.0 → 1.1.0&lt;/code&gt;): 기능 추가가 포함될 수 있고, 대체로 호환성을 유지하는 편&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Major&lt;/strong&gt; (&lt;code&gt;1.0.0 → 2.0.0&lt;/code&gt;): breaking change가 있을 수 있으므로 반드시 확인 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;물론 모든 패키지가 SemVer를 완벽하게 지키는 것은 아니므로 Patch나 Minor라고 해서 무조건 안전하다고 단정하면 안 된다.&lt;/p&gt;
&lt;p&gt;다만 일반적으로는 Major 업데이트가 가장 주의가 필요하다.&lt;/p&gt;
&lt;h2&gt;방법 4. npx npm-check-updates로 한 번에 확인하기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;npm-check-updates&lt;/code&gt;를 사용하면 업데이트 가능한 패키지 목록을 한눈에 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx npm-check-updates&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또는 줄여서 아래처럼 사용할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx ncu&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 도구는 &lt;code&gt;package.json&lt;/code&gt; 기준으로 업데이트 가능한 패키지 버전을 확인할 때 유용하다.&lt;/p&gt;
&lt;h2&gt;방법 5. Renovate, Dependabot으로 자동화하기&lt;/h2&gt;
&lt;p&gt;Renovate나 Dependabot을 사용하면 패키지 업데이트가 있을 때 자동으로 PR을 만들어줄 수 있다.&lt;/p&gt;
&lt;p&gt;GitHub에 설정해두면 의존성 업데이트가 필요할 때 자동으로 PR이 생성된다.&lt;/p&gt;
&lt;p&gt;CI가 설정되어 있다면 해당 PR에서 빌드와 테스트까지 자동으로 실행할 수 있다.&lt;/p&gt;
&lt;p&gt;이렇게 해두면 업데이트 후 breaking change가 있는지 어느 정도 빠르게 확인할 수 있다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;결론적으로 패키지 업데이트는 대략 아래와 같은 흐름으로 처리하면 된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;pnpm outdated&lt;/code&gt;로 업데이트 현황을 파악한다.&lt;/li&gt;
&lt;li&gt;Patch/Minor 업데이트는 비교적 안전한 편이므로 일괄 업데이트 후 빌드와 테스트를 확인한다.&lt;/li&gt;
&lt;li&gt;Major 업데이트는 해당 패키지의 Migration Guide나 CHANGELOG를 먼저 확인한다.&lt;/li&gt;
&lt;li&gt;업데이트 후 &lt;code&gt;pnpm install&lt;/code&gt;을 실행해서 peer dependency 경고가 있는지 확인한다.&lt;/li&gt;
&lt;li&gt;빌드를 실행해서 타입 에러나 import 에러가 있는지 확인한다.&lt;/li&gt;
&lt;li&gt;주요 기능을 수동으로 테스트해서 런타임 breaking change가 없는지 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;모든 패키지의 공식 사이트를 일일이 방문할 필요는 없다.&lt;/p&gt;
&lt;p&gt;다만 Major 업데이트인 경우에는 해당 패키지의 Release Notes나 Migration Guide를 확인하는 것이 좋다.&lt;/p&gt;</description>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/11</guid>
      <comments>https://harubanbo.tistory.com/11#entry11comment</comments>
      <pubDate>Sat, 8 Aug 2026 23:56:07 +0900</pubDate>
    </item>
    <item>
      <title>패키지 모듈 업데이트 시 breaking change와 peer dependency란?</title>
      <link>https://harubanbo.tistory.com/10</link>
      <description>&lt;p&gt;가끔씩 패키지 업데이트를 하는 경우 종종 문제가 있어서 정리차원에서 남긴다.&lt;/p&gt;
&lt;h2&gt;Breaking Change&lt;/h2&gt;
&lt;p&gt;브레이킹 체인지는 패키지나 모듈을 업데이트했을 때 기존 코드가 더 이상 정상 동작하지 않게 되는 변경사항을 의미한다.&lt;/p&gt;
&lt;p&gt;예를 들어 패키지 A가 v1에서 v2로 업데이트되면서 다음과 같은 변경사항이 생겼다고 해보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;함수 이름이 변경됨&lt;/li&gt;
&lt;li&gt;함수의 매개변수가 달라짐&lt;/li&gt;
&lt;li&gt;기존에 있던 기능이 deprecated되거나 사라짐&lt;/li&gt;
&lt;li&gt;반환값의 형태가 변경됨&lt;/li&gt;
&lt;li&gt;기존 사용 방식이 더 이상 지원되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;프로젝트 코드가 v1 기준으로 작성되어 있었다면, v2로 업데이트했을 때 기존 코드가 깨질 수 있다. &lt;strong&gt;Semantic Versioning(SemVer)&lt;/strong&gt; 기준으로는 버전을 &lt;code&gt;MAJOR.MINOR.PATCH&lt;/code&gt; 형태로 표현한다. 이 중 기존 버전과 호환되지 않는 변경사항이 있을 때 &lt;strong&gt;MAJOR 버전&lt;/strong&gt;을 올린다.&lt;/p&gt;
&lt;p&gt;예를 들어 아래처럼 &lt;code&gt;1.x.x&lt;/code&gt;에서 &lt;code&gt;2.0.0&lt;/code&gt;으로 올라간다면 breaking change가 포함되어 있을 가능성이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1.x.x → 2.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;다만 모든 패키지가 SemVer를 완벽하게 지키는 것은 아니기 때문에, 실제 업데이트 전에는 changelog나 migration guide를 확인하는 것이 좋다.&lt;/p&gt;
&lt;h2&gt;Peer Dependency&lt;/h2&gt;
&lt;p&gt;Peer Dependency는 간단히 말하면 “이 패키지는 특정 의존성이 필요하지만, 그 의존성을 직접 설치해서 포함하지는 않으니 프로젝트 쪽에서 설치해줘”라는 의미다. 예를 들어 어떤 React 컴포넌트 라이브러리의 &lt;code&gt;package.json&lt;/code&gt;에 아래처럼 적혀 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;peerDependencies&amp;quot;: {
    &amp;quot;react&amp;quot;: &amp;quot;^18.0.0&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;위 React 컴포넌트 라이브러리는 &lt;code&gt;react&lt;/code&gt;가 필요하지만, 패키지 자체적으로 React를 번들하거나 별도로 포함하지 않는다. 대신 이 라이브러리를 사용하는 프로젝트에 이미 설치된 React를 함께 사용하겠다는 의미다. 왜냐하면 React 같은 라이브러리는 프로젝트 전체에서 같은 인스턴스를 공유해야 하는 경우가 많기 때문이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문제&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;중복 방지&lt;/td&gt;
&lt;td&gt;라이브러리 A도 React를 설치하고, 프로젝트도 React를 설치하면 React가 여러 개 존재할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;충돌 방지&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;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Breaking Change와 Peer Dependency의 관계&lt;/h2&gt;
&lt;p&gt;Breaking Change와 Peer Dependency는 서로 다른 개념이지만, 실무에서는 자주 같이 엮인다. 예를 들어 &lt;code&gt;next-intl&lt;/code&gt;이 peer dependency로 &lt;code&gt;next: ^14.0.0&lt;/code&gt;을 요구한다고 하자. 그런데 프로젝트에서 Next.js를 15로 올리면 &lt;code&gt;next-intl&lt;/code&gt; 입장에서는 자신이 기대하는 Next.js 버전 범위를 벗어나게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;프로젝트
→ next@15 사용

next-intl
→ next@^14.0.0 기대&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 경우 peer dependency 버전 범위에 맞지 않아 경고가 발생하거나, 환경에 따라 설치가 실패할 수 있다. 그리고 Next.js 15에서 &lt;code&gt;next-intl&lt;/code&gt;이 의존하던 동작이 바뀌었거나 제거되었다면 breaking change의 영향을 받을 수도 있다. 그런데 패키지 간 의존성 문제로 peer dependency 경고가 발생하더라도, 실무에서는 상황을 감안하고 그대로 사용하는 경우도 있다. 예를 들어 React 19가 나왔지만 어떤 패키지는 peer dependency에 React 18까지만 명시하고 있을 수 있다. 이때 peer dependency에 명시된 버전 범위는 “이 범위 밖에서는 물리적으로 절대 동작하지 않는다”는 의미라기보다는 “이 범위 안에서 동작을 기대하거나 테스트했다”는 의미에 가깝다.&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;그래서 프로젝트 루트에 React 19가 있고, 어떤 패키지가 React 18까지만 peer dependency로 명시하고 있더라도 실제 런타임에서는 정상 동작할 수 있다. 다만 패키지 매니저 설정에 따라 경고만 나오고 설치될 수도 있고, 설치 단계에서 실패할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;React 19 릴리스
    ↓
Next.js가 React 19 지원 추가
    ↓
인기 라이브러리들이 React 19 지원 추가
    ↓
중소 라이브러리들이 React 19 지원 추가
    ↓
유지보수가 멈춘 라이브러리는 계속 미지원 상태로 남을 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;실무에서는 peer dependency 경고가 나와도 실제로 동작하면 경고를 감수하고 사용하기도 한다. 이러한 경고는 보통 “상위 버전 환경에서 테스트되었거나 보장된 범위를 벗어났다”는 의미이지, 반드시 동작하지 않는다는 의미는 아니다. 반대로 타입 에러나 런타임 문제가 발생한다면 임시로 우회하거나, 근본적으로는 대체 라이브러리로 교체해야 할 수도 있다. 이조차 애매하다면 다른 패키지들이 React 19 같은 새 버전을 충분히 따라올 때까지 업그레이드 시점을 늦추기도 한다. 모든 패키지의 peer dependency가 항상 완벽하게 맞물리지는 않기 때문이다.&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;결국 브레이킹 체인지는 패키지를 업데이트했을 때 기존 코드가 깨질 수 있는 변경사항을 의미한다. Peer Dependency는 필요한 의존성 패키지를 패키지 자체에서 직접 가지고 있는 것이 아니라, 해당 패키지를 사용하는 프로젝트에 설치하도록 요구하고 함께 사용하려는 목적의 의존성이다.&lt;/p&gt;</description>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/10</guid>
      <comments>https://harubanbo.tistory.com/10#entry10comment</comments>
      <pubDate>Sat, 25 Jul 2026 23:38:33 +0900</pubDate>
    </item>
    <item>
      <title>구조분해할당 &amp;amp; 전개(spread) &amp;amp; 나머지(rest) 연산자 간단정리</title>
      <link>https://harubanbo.tistory.com/9</link>
      <description>&lt;h1&gt;구조분해 할당, 전개 연산자, 나머지 연산자 정리&lt;/h1&gt;
&lt;p&gt;구조분해 할당, 전개 연산자, 나머지 연산자는 객체나 배열의 값을 더 쉽게 꺼내거나 복사할 때 자주 사용된다.&lt;/p&gt;
&lt;p&gt;이름이 비슷하고 &lt;code&gt;...&lt;/code&gt; 문법도 같이 사용되다 보니 처음에는 헷갈릴 수 있다.&lt;/p&gt;
&lt;p&gt;코드 보다가 가끔씩 헷갈려서 단순 정리용으로 남기는 글이다.&lt;/p&gt;
&lt;h2&gt;1. 예제 객체&lt;/h2&gt;
&lt;p&gt;아래는 예제에서 사용할 객체다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const a = { aa: 1, bb: 2, cc: 3, ss: { aaa: 1, bbb: 2 } };&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a&lt;/code&gt; 객체는 &lt;code&gt;aa&lt;/code&gt;, &lt;code&gt;bb&lt;/code&gt;, &lt;code&gt;cc&lt;/code&gt;, &lt;code&gt;ss&lt;/code&gt; 속성을 가지고 있다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;ss&lt;/code&gt; 속성의 값은 다시 객체다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;{
  aaa: 1,
  bbb: 2
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 구조분해 할당&lt;/h2&gt;
&lt;p&gt;구조분해 할당을 사용하면 객체나 배열의 값을 변수로 바로 꺼낼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { aa, bb, cc, ss } = a;

console.log(aa, bb, cc, ss);
// 1 2 3 { aaa: 1, bbb: 2 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드는 &lt;code&gt;a&lt;/code&gt; 객체에서 &lt;code&gt;aa&lt;/code&gt;, &lt;code&gt;bb&lt;/code&gt;, &lt;code&gt;cc&lt;/code&gt;, &lt;code&gt;ss&lt;/code&gt; 속성을 꺼내서 각각 같은 이름의 변수로 선언한다.&lt;/p&gt;
&lt;p&gt;즉, 아래처럼 직접 접근해서 변수에 담는 것과 비슷하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const aa = a.aa;
const bb = a.bb;
const cc = a.cc;
const ss = a.ss;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;구조분해 할당을 사용하면 이 과정을 한 줄로 줄일 수 있다.&lt;/p&gt;
&lt;h2&gt;3. 중첩 객체의 값 가져오기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ss&lt;/code&gt; 속성 안에 있는 &lt;code&gt;aaa&lt;/code&gt; 값을 가져오고 싶다면 여러 방식이 있다.&lt;/p&gt;
&lt;p&gt;먼저 &lt;code&gt;ss&lt;/code&gt; 자체를 구조분해 할당으로 꺼낸 뒤, &lt;code&gt;ss.aaa&lt;/code&gt;로 접근할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { aa, bb, cc, ss } = a;

console.log(ss.aaa);
// 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 &lt;code&gt;ss&lt;/code&gt;라는 변수가 실제로 생성된다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;ss.aaa&lt;/code&gt;, &lt;code&gt;ss.bbb&lt;/code&gt;처럼 &lt;code&gt;ss&lt;/code&gt; 내부 속성에 접근할 수 있다.&lt;/p&gt;
&lt;h2&gt;4. 중첩 구조분해 할당&lt;/h2&gt;
&lt;p&gt;중첩 구조분해 할당을 사용하면 객체 안쪽의 값까지 한 번에 꺼낼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const {
  aa,
  bb,
  cc,
  ss: { aaa },
} = a;

console.log(aaa);
// 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드는 &lt;code&gt;a.ss.aaa&lt;/code&gt; 값을 &lt;code&gt;aaa&lt;/code&gt;라는 변수로 꺼낸 것이다.&lt;/p&gt;
&lt;p&gt;주의할 점은 이 경우 &lt;code&gt;ss&lt;/code&gt; 변수 자체가 만들어지는 것은 아니라는 점이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;console.log(ss);
// ReferenceError: ss is not defined&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 아래 구조분해 할당은 &lt;code&gt;ss&lt;/code&gt;라는 변수를 만드는 것이 아니라 &lt;code&gt;ss&lt;/code&gt; 속성 안으로 들어가서 &lt;code&gt;aaa&lt;/code&gt;를 꺼내겠다는 의미다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const {
  ss: { aaa },
} = a;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { ss } = a;
// ss 변수 생성

const {
  ss: { aaa },
} = a;
// ss 변수는 생성되지 않고 aaa 변수만 생성&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. undefined와 ReferenceError&lt;/h2&gt;
&lt;p&gt;객체에서 존재하지 않는 속성에 접근하면 &lt;code&gt;undefined&lt;/code&gt;가 나온다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;console.log(a.notExist);
// undefined&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 선언되지 않은 변수에 접근하면 참조 오류가 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;console.log(notExist);
// ReferenceError: notExist is not defined&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 중첩 구조분해 할당에서 &lt;code&gt;ss&lt;/code&gt; 변수를 만들지 않았는데 &lt;code&gt;ss&lt;/code&gt;에 접근하면 &lt;code&gt;undefined&lt;/code&gt;가 아니라 &lt;code&gt;ReferenceError&lt;/code&gt;가 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const {
  ss: { aaa },
} = a;

console.log(aaa);
// 1

console.log(ss);
// ReferenceError: ss is not defined&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 콜론(:)의 의미&lt;/h2&gt;
&lt;p&gt;객체 구조분해 할당에서 콜론(&lt;code&gt;:&lt;/code&gt;)은 상황에 따라 두 가지처럼 보일 수 있다.&lt;/p&gt;
&lt;p&gt;첫 번째는 속성 이름을 다른 변수명으로 바꿔서 받는 경우다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { ss: wow } = a;

console.log(wow);
// { aaa: 1, bbb: 2 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드는 &lt;code&gt;a.ss&lt;/code&gt; 값을 &lt;code&gt;wow&lt;/code&gt;라는 이름의 변수로 받겠다는 의미다.&lt;/p&gt;
&lt;p&gt;이 경우 &lt;code&gt;ss&lt;/code&gt;라는 변수는 생성되지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;console.log(ss);
// ReferenceError: ss is not defined&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 코드에서는 아래처럼 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const {
  formState: { errors: fsErrors },
} = useForm&amp;lt;Inputs&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드는 &lt;code&gt;formState.errors&lt;/code&gt; 값을 꺼내서 &lt;code&gt;fsErrors&lt;/code&gt;라는 변수명으로 받겠다는 의미다.&lt;/p&gt;
&lt;p&gt;즉, &lt;code&gt;errors&lt;/code&gt;라는 이름 대신 &lt;code&gt;fsErrors&lt;/code&gt;라는 이름으로 사용하겠다는 뜻이다.&lt;/p&gt;
&lt;h2&gt;7. 콜론(:)과 중첩 구조분해 할당&lt;/h2&gt;
&lt;p&gt;콜론(&lt;code&gt;:&lt;/code&gt;)은 중첩 구조분해 할당에서도 사용된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const {
  aa,
  bb,
  cc,
  ss: { aaa },
} = a;

console.log(aaa);
// 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경우의 의미는 &lt;code&gt;ss&lt;/code&gt; 속성 안으로 들어가서 다시 구조분해 할당을 하겠다는 뜻이다.&lt;/p&gt;
&lt;p&gt;결과적으로 생성되는 변수는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;aa
bb
cc
aaa&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ss&lt;/code&gt; 변수는 생성되지 않는다.&lt;/p&gt;
&lt;p&gt;즉, 아래 코드는 &lt;code&gt;ss&lt;/code&gt;를 변수로 받는 코드가 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;ss: { aaa }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a.ss&lt;/code&gt; 안으로 들어가서 &lt;code&gt;aaa&lt;/code&gt;를 꺼내겠다는 의미다.&lt;/p&gt;
&lt;h2&gt;8. ... 연산자의 의미&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;...&lt;/code&gt; 문법은 위치에 따라 의미가 달라진다.&lt;/p&gt;
&lt;p&gt;크게 두 가지로 볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;값을 펼치는 위치에서 사용
→ 전개 연산자(spread)

구조분해 할당에서 나머지를 모으는 위치에서 사용
→ 나머지 연산자(rest)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 같은 &lt;code&gt;...&lt;/code&gt; 문법이지만 어디에서 사용되는지에 따라 의미가 달라진다.&lt;/p&gt;
&lt;h2&gt;9. 전개 연산자(spread)&lt;/h2&gt;
&lt;p&gt;전개 연산자는 객체나 배열의 값을 펼칠 때 사용한다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;ss&lt;/code&gt; 객체를 새로운 객체로 복사하고 싶다면 아래처럼 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { ss } = a;

const test = { ...ss };

console.log(test);
// { aaa: 1, bbb: 2 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드에서 &lt;code&gt;{ ...ss }&lt;/code&gt;는 &lt;code&gt;ss&lt;/code&gt; 객체의 속성들을 펼쳐서 새로운 객체를 만든다는 의미다.&lt;/p&gt;
&lt;p&gt;즉, 아래 객체를 새로 만드는 것과 비슷하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const test = {
  aaa: 1,
  bbb: 2,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 아래처럼 출력하면 둘 다 같은 형태로 나온다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;console.log(test, { ...ss });
// { aaa: 1, bbb: 2 } { aaa: 1, bbb: 2 }&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. 나머지 연산자(rest)&lt;/h2&gt;
&lt;p&gt;나머지 연산자는 구조분해 할당에서 아직 꺼내지 않은 나머지 값들을 모을 때 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { aa, ...test } = a;

console.log(test);
// { bb: 2, cc: 3, ss: { aaa: 1, bbb: 2 } }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드는 &lt;code&gt;a&lt;/code&gt; 객체에서 &lt;code&gt;aa&lt;/code&gt;는 따로 꺼내고, 나머지 속성인 &lt;code&gt;bb&lt;/code&gt;, &lt;code&gt;cc&lt;/code&gt;, &lt;code&gt;ss&lt;/code&gt;는 &lt;code&gt;test&lt;/code&gt;라는 객체에 모으겠다는 의미다.&lt;/p&gt;
&lt;p&gt;결과적으로 생성되는 변수는 아래 두 개다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;aa
test&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;test&lt;/code&gt;에는 &lt;code&gt;aa&lt;/code&gt;를 제외한 나머지 속성들이 들어간다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;{
  bb: 2,
  cc: 3,
  ss: { aaa: 1, bbb: 2 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 구조분해 할당 안에서 &lt;code&gt;...test&lt;/code&gt;처럼 사용되면 전개가 아니라 나머지 값을 모으는 역할을 한다.&lt;/p&gt;
&lt;h2&gt;11. spread와 rest 비교&lt;/h2&gt;
&lt;p&gt;같은 &lt;code&gt;...&lt;/code&gt; 문법이라도 위치에 따라 의미가 달라진다.&lt;/p&gt;
&lt;p&gt;전개 연산자는 값을 만들 때 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const test = { ...a };&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드는 &lt;code&gt;a&lt;/code&gt; 객체의 속성을 펼쳐서 새로운 객체를 만든다.&lt;/p&gt;
&lt;p&gt;반면 나머지 연산자는 구조분해 할당에서 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { aa, ...test } = a;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드는 &lt;code&gt;aa&lt;/code&gt;를 제외한 나머지 속성을 &lt;code&gt;test&lt;/code&gt; 객체에 모은다.&lt;/p&gt;
&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;const test = { ...a };
→ a를 펼쳐서 새 객체 생성
→ spread

const { aa, ...test } = a;
→ aa를 제외한 나머지를 test에 모음
→ rest&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;12. 정리&lt;/h2&gt;
&lt;p&gt;구조분해 할당은 객체나 배열의 값을 변수로 쉽게 꺼내는 문법이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { aa, bb } = a;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중첩 구조분해 할당을 사용하면 객체 안쪽의 값까지 바로 꺼낼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const {
  ss: { aaa },
} = a;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다만 이 경우 &lt;code&gt;ss&lt;/code&gt; 변수 자체가 생성되는 것은 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;console.log(aaa);
// 1

console.log(ss);
// ReferenceError: ss is not defined&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;콜론(&lt;code&gt;:&lt;/code&gt;)은 속성을 다른 변수명으로 받을 때 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { ss: wow } = a;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경우 &lt;code&gt;a.ss&lt;/code&gt; 값을 &lt;code&gt;wow&lt;/code&gt;라는 변수명으로 받는다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;...&lt;/code&gt; 문법은 위치에 따라 의미가 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const test = { ...a };&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;값을 만들 때 사용하면 전개 연산자다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const { aa, ...test } = a;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;구조분해 할당 안에서 사용하면 나머지 연산자다.&lt;/p&gt;
&lt;p&gt;결론:&lt;/p&gt;
&lt;p&gt;객체에서 필요한 값만 바로 꺼내고 싶다&lt;br&gt;→ 구조분해 할당&lt;/p&gt;
&lt;p&gt;객체 안쪽 값까지 한 번에 꺼내고 싶다&lt;br&gt;→ 중첩 구조분해 할당&lt;/p&gt;
&lt;p&gt;속성 이름과 다른 변수명으로 받고 싶다&lt;br&gt;→ &lt;code&gt;속성명: 변수명&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;객체나 배열을 펼쳐서 새 값을 만들고 싶다&lt;br&gt;→ spread&lt;/p&gt;
&lt;p&gt;구조분해 할당 후 남은 값을 모으고 싶다&lt;br&gt;→ rest&lt;/p&gt;</description>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/9</guid>
      <comments>https://harubanbo.tistory.com/9#entry9comment</comments>
      <pubDate>Sat, 11 Jul 2026 23:36:25 +0900</pubDate>
    </item>
    <item>
      <title>git worktree</title>
      <link>https://harubanbo.tistory.com/8</link>
      <description>&lt;h1&gt;git worktree로 브랜치별 작업 디렉토리 분리하기&lt;/h1&gt;
&lt;p&gt;보통 Git 저장소를 clone하면 하나의 &lt;code&gt;.git&lt;/code&gt; 디렉토리와 하나의 워킹 디렉토리를 가지고 작업한다. 이 디렉토리 안에서 브랜치를 바꾸고, 파일을 수정하고, 스테이징하고, 커밋하는데 일반적인 작업 흐름은 아래와 비슷하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout main
git checkout -b feature/a
# 작업
git checkout main
git checkout -b feature/b
# 작업&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;문제는 하나의 디렉토리 안에서 브랜치를 계속 바꿔가며 작업한다는 점이다. 예를 들어 &lt;code&gt;feature/a&lt;/code&gt; 브랜치에서 아직 커밋하지 않은 작업을 하고 있는데, 갑자기 &lt;code&gt;main&lt;/code&gt; 기준으로 급한 수정이 필요할 수 있다. 이때 보통은 stash 기능을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git stash
git checkout main
git checkout -b hotfix
# 급한 작업
git checkout feature/a
git stash pop&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;하지만 이 방식은 stash가 여러 개 쌓이면 관리가 복잡해지고, 잘못된 stash를 꺼내거나 충돌이 생기는 경우도 있다. 그래서 브랜치를 자주 오가야 하거나, 동시에 여러 작업을 진행해야 하는 상황이라면 &lt;code&gt;git worktree&lt;/code&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;h2&gt;1. git worktree란?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt;는 하나의 Git 저장소에 여러 개의 실제 작업 디렉토리를 연결해서 사용할 수 있게 해주는 기능이다. 일반적인 Git 저장소는 하나의 저장소에 하나의 워킹 디렉토리를 가진다. 반면 &lt;code&gt;git worktree&lt;/code&gt;를 사용하면 같은 저장소를 기준으로 여러 개의 워킹 디렉토리를 만들고 넘나들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;project/
  .git/
  src/
  package.json

project-feature-a/
  src/
  package.json

project-feature-b/
  src/
  package.json&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;여기서 &lt;code&gt;project&lt;/code&gt;가 기존 저장소이고, &lt;code&gt;project-feature-a&lt;/code&gt;, &lt;code&gt;project-feature-b&lt;/code&gt;가 worktree로 분리된 작업 디렉토리다. 각 worktree는 같은 Git 저장소에 연결되어 있지만, 서로 다른 브랜치를 checkout한 독립적인 작업 디렉토리처럼 사용할 수 있다. 즉, 하나의 디렉토리 안에서 브랜치를 계속 바꾸는 것이 아니라 브랜치별로 작업 폴더를 따로 두는 방식이다.&lt;/p&gt;
&lt;h2&gt;2. 왜 git worktree를 사용할까?&lt;/h2&gt;
&lt;p&gt;기존에는 하나의 저장소 디렉토리 안에서 브랜치를 바꿔가며 작업하는 경우가 많았다. 예를 들어 &lt;code&gt;feature/a&lt;/code&gt; 브랜치에서 작업 중인데 급하게 &lt;code&gt;main&lt;/code&gt; 기준으로 hotfix를 해야 한다면 보통 아래처럼 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git stash
git checkout main
git checkout -b hotfix&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;작업을 끝낸 뒤에는 다시 원래 브랜치로 돌아와서 stash를 복구한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout feature/a
git stash pop&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;작업이 단순할 때는 이 방식도 충분하지만 작업 중인 파일이 많거나, stash가 여러 개 쌓여 있거나, 여러 브랜치를 동시에 확인해야 하는 상황에서는 점점 번거로워진다. 특히 아래와 같은 문제가 생길 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;- stash 목록이 많아져서 어떤 stash가 어떤 작업이었는지 헷갈린다.
- 잘못된 stash를 pop할 수 있다.
- stash pop 과정에서 충돌이 발생할 수 있다.
- 브랜치를 바꿀 때마다 작업 상태를 정리해야 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt;를 사용하면 이런 상황에서 브랜치별로 작업 디렉토리를 분리할 수 있다. 예를 들어 &lt;code&gt;feature/a&lt;/code&gt;는 &lt;code&gt;project-feature-a&lt;/code&gt; 디렉토리에서 작업하고, &lt;code&gt;hotfix&lt;/code&gt;는 &lt;code&gt;project-hotfix&lt;/code&gt; 디렉토리에서 작업하는 식이다. 그러면 기존 작업 디렉토리를 건드리지 않고도 다른 브랜치 작업을 바로 진행할 수 있다.&lt;/p&gt;
&lt;h2&gt;3. AI 에이전트와 git worktree&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt; 자체는 예전부터 존재하던 Git 기능이다. 다만 최근에는 AI 에이전트를 활용한 병렬 작업이 많아지면서 다시 자주 언급되고 있다. 예를 들어 여러 AI 에이전트에게 각각 다른 작업을 맡긴다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;agent-a → feature/a 작업
agent-b → feature/b 작업
agent-c → refactor 작업&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이때 하나의 작업 디렉토리를 공유하면 서로 파일을 덮어쓰거나, 브랜치 상태가 꼬이거나, 아직 커밋하지 않은 변경사항이 충돌할 수 있다. 반면 worktree를 사용하면 각 에이전트에게 서로 다른 작업 디렉토리를 줄 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;project-agent-a/
project-agent-b/
project-agent-c/&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;각 디렉토리는 같은 저장소를 기준으로 연결되어 있지만, 작업 공간은 분리되어 있다. 그래서 병렬 작업을 시키거나, 여러 브랜치를 동시에 열어두고 비교해야 할 때 유용하다.&lt;/p&gt;
&lt;h2&gt;4. 기본 사용법&lt;/h2&gt;
&lt;p&gt;현재 브랜치가 &lt;code&gt;main&lt;/code&gt;이고, 이미 &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt; 브랜치가 있다고 가정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;main
A
B&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;현재 &lt;code&gt;main&lt;/code&gt; 브랜치에 checkout되어 있는 상태에서 &lt;code&gt;B&lt;/code&gt; 브랜치를 별도 디렉토리로 분리하고 싶다면 아래처럼 실행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../test-branch B&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 명령은 현재 저장소의 상위 디렉토리에 &lt;code&gt;test-branch&lt;/code&gt;라는 새 디렉토리를 만들고, 그 디렉토리에서 &lt;code&gt;B&lt;/code&gt; 브랜치를 checkout한다. 즉, 아래와 같은 의미다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;../test-branch 디렉토리를 만든다.
해당 디렉토리를 현재 Git 저장소의 worktree로 연결한다.
그 worktree에서 B 브랜치를 checkout한다.&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이제 &lt;code&gt;B&lt;/code&gt; 브랜치 작업을 하고 싶다면 기존 저장소에서 &lt;code&gt;git checkout B&lt;/code&gt;를 하는 것이 아니라, 그냥 &lt;code&gt;../test-branch&lt;/code&gt; 디렉토리로 이동하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;worktree로 분리된 브랜치는 이미 해당 디렉토리에서 checkout된 상태라고 보면 된다.&lt;/p&gt;
&lt;h2&gt;5. 브랜치명과 폴더명&lt;/h2&gt;
&lt;p&gt;worktree를 만들 때 폴더명과 브랜치명이 반드시 같을 필요는 없다. 예를 들어 아래 명령은 &lt;code&gt;B&lt;/code&gt; 브랜치를 &lt;code&gt;test-branch&lt;/code&gt;라는 폴더에 checkout한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../test-branch B&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;폴더명은 &lt;code&gt;test-branch&lt;/code&gt;이고, checkout된 브랜치는 &lt;code&gt;B&lt;/code&gt;다. 이미 존재하는 브랜치명을 경로로 그대로 사용하고 싶다면 아래처럼 쓸 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../B B&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;또는 브랜치명과 같은 이름의 디렉토리를 만들고 싶다면 상황에 따라 아래처럼 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../&amp;lt;브랜치명&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 경우 별도로 checkout할 브랜치나 커밋을 지정하지 않았기 때문에, Git은 경로의 마지막 이름을 기준으로 브랜치명을 잡는다. 예를 들어 현재 &lt;code&gt;main&lt;/code&gt; 브랜치에서 아래 명령을 실행한다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;그러면 &lt;code&gt;test-branch&lt;/code&gt;라는 이름의 브랜치가 없을 경우, 현재 &lt;code&gt;HEAD&lt;/code&gt;를 기준으로 &lt;code&gt;test-branch&lt;/code&gt; 브랜치를 새로 만들고 해당 worktree에서 checkout한다. 즉, 현재 &lt;code&gt;main&lt;/code&gt;에 있었다면 &lt;code&gt;main&lt;/code&gt;의 현재 커밋을 기준으로 &lt;code&gt;test-branch&lt;/code&gt; 브랜치가 만들어진다. 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 기존 B 브랜치를 ../test-branch 디렉토리에서 checkout
git worktree add ../test-branch B

# 경로 이름을 기준으로 브랜치를 만들거나 checkout
git worktree add ../test-branch

# 폴더명과 브랜치명을 다르게 지정
git worktree add ../&amp;lt;사용할폴더명&amp;gt; &amp;lt;브랜치명&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;6. 같은 브랜치는 동시에 두 worktree에서 checkout할 수 없다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt;를 사용할 때 가장 주의해야 할 점은 같은 브랜치를 동시에 두 개 이상의 worktree에서 checkout할 수 없다는 것이다. 예를 들어 현재 기존 저장소에서 &lt;code&gt;main&lt;/code&gt; 브랜치에 checkout되어 있다고 하자. 이 상태에서 아래 명령을 실행하면 실패한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../test-branch main&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;왜냐하면 &lt;code&gt;main&lt;/code&gt; 브랜치는 이미 기존 worktree에서 checkout되어 있기 때문이다. 이때는 아래와 비슷한 오류가 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fatal: &amp;#39;main&amp;#39; is already used by worktree at ...&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;즉, worktree는 브랜치별 작업 디렉토리를 분리해주는 기능이지만, 하나의 브랜치를 여러 작업 디렉토리에서 동시에 checkout하는 구조는 아니다. &lt;code&gt;main&lt;/code&gt; 브랜치가 이미 기존 디렉토리에서 checkout되어 있다면, 새 worktree에서 다시 &lt;code&gt;main&lt;/code&gt; 브랜치를 checkout할 수 없다.&lt;/p&gt;
&lt;h2&gt;7. 이미 checkout된 브랜치를 기준으로 새 브랜치 만들기&lt;/h2&gt;
&lt;p&gt;그렇다고 해서 &lt;code&gt;main&lt;/code&gt;의 내용을 기준으로 새 worktree를 만들 수 없는 것은 아니다. 이미 checkout되어 있는 &lt;code&gt;main&lt;/code&gt; 브랜치를 그대로 새 worktree에서 checkout할 수는 없지만, &lt;code&gt;main&lt;/code&gt;을 기준으로 새로운 브랜치를 만들면서 worktree를 생성할 수는 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add -b real-branch ../test-branch main&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 명령은 아래와 같은 의미다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;main 브랜치를 기준으로
real-branch라는 새 브랜치를 만들고
../test-branch 디렉토리에 worktree를 생성한 뒤
그 worktree에서 real-branch를 checkout한다.&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;즉, &lt;code&gt;main&lt;/code&gt;을 직접 checkout하는 것이 아니라 &lt;code&gt;main&lt;/code&gt;에서 새 브랜치를 따서 checkout하는 방식이다. &lt;code&gt;main&lt;/code&gt;뿐만 아니라 다른 브랜치를 기준으로도 동일하게 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add -b new-branch ../new-worktree base-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;8. 브랜치 작업 없이 내용만 필요할 때&lt;/h2&gt;
&lt;p&gt;어떤 브랜치의 내용만 확인하고 싶고, 새 브랜치 작업을 할 필요가 없다면 detached HEAD 상태로 worktree를 만들 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add --detach ../test-branch main&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 명령은 &lt;code&gt;main&lt;/code&gt; 브랜치의 현재 커밋을 기준으로 &lt;code&gt;../test-branch&lt;/code&gt; 디렉토리를 만들지만, 해당 worktree는 &lt;code&gt;main&lt;/code&gt; 브랜치를 checkout한 상태가 아니다. 대신 &lt;code&gt;main&lt;/code&gt;과 같은 커밋을 가리키는 detached HEAD 상태가 된다. 즉, 특정 브랜치의 파일 상태만 확인하거나 테스트용으로 잠깐 사용할 때 적합하다. 브랜치 작업을 이어갈 목적이라면 &lt;code&gt;--detach&lt;/code&gt;보다는 새 브랜치를 만들어 사용하는 편이 낫다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add -b real-branch ../test-branch main&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;9. worktree 목록 확인하기&lt;/h2&gt;
&lt;p&gt;현재 연결된 worktree 목록은 아래 명령으로 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree list&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;예를 들어 아래처럼 나올 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/path/project            abc1234 [main]
/path/test-branch        def5678 [B]&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 출력은 현재 저장소에 어떤 worktree가 연결되어 있고, 각 worktree가 어떤 브랜치 또는 커밋을 보고 있는지 보여준다. worktree를 사용하다 보면 어떤 브랜치가 어느 디렉토리에 연결되어 있는지 헷갈릴 수 있으므로 자주 확인하는 것이 좋다.&lt;/p&gt;
&lt;h2&gt;10. worktree 제거하기&lt;/h2&gt;
&lt;p&gt;worktree를 제거할 때는 단순히 폴더를 삭제하는 것보다 &lt;code&gt;git worktree remove&lt;/code&gt;를 사용하는 것이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree remove ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 명령은 worktree 연결 정보를 정리하고, 해당 worktree 디렉토리도 제거한다. 다만 기본적으로 &lt;code&gt;git worktree remove&lt;/code&gt;는 정리된 상태의 worktree를 제거할 때 사용한다. 해당 worktree 안에 아직 커밋하지 않은 변경사항이나 추적되지 않는 파일이 있으면 제거가 거부될 수 있다. 이 경우 정말 삭제해도 되는 상태인지 먼저 확인해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ../test-branch
git status&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;정말 강제로 제거해야 한다면 &lt;code&gt;--force&lt;/code&gt; 옵션을 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree remove --force ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;다만 &lt;code&gt;--force&lt;/code&gt;는 작업 중인 파일을 잃을 수 있으므로 신중하게 사용하는 것이 좋다. 주의할 점은 worktree를 제거한다고 해서 그 worktree에서 사용하던 브랜치가 삭제되는 것은 아니라는 점이다. 예를 들어 &lt;code&gt;real-branch&lt;/code&gt; 브랜치를 checkout한 &lt;code&gt;../test-branch&lt;/code&gt; worktree를 제거해도 &lt;code&gt;real-branch&lt;/code&gt; 브랜치 자체는 그대로 남아 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;worktree 제거
→ 작업 디렉토리와 worktree 연결 정보 제거

브랜치 삭제
→ 별도의 브랜치 삭제 명령 필요&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;즉, worktree는 브랜치 자체가 아니라 브랜치를 checkout해둔 작업 디렉토리 구조라고 보는 편이 이해하기 쉽다.&lt;/p&gt;
&lt;h2&gt;11. 폴더를 직접 삭제했다면 prune으로 정리하기&lt;/h2&gt;
&lt;p&gt;worktree 디렉토리를 수동으로 삭제하면 실제 파일은 사라질 수 있다. 하지만 Git 저장소 내부에는 해당 worktree에 대한 참조 정보가 남아 있을 수 있다. 이 경우 아래 명령으로 정리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree prune&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;&lt;code&gt;prune&lt;/code&gt;은 실제 작업 디렉토리는 사라졌지만 Git 내부에 남아 있는 오래된 worktree 관리 정보를 정리한다. 현재 상태를 먼저 보고 싶다면 아래 명령을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree list&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;목록에서 이미 사라진 worktree가 남아 있거나, 정리 가능한 상태로 표시된다면 &lt;code&gt;prune&lt;/code&gt;으로 정리하면 된다. 하지만 다음부터는 가능하면 폴더를 직접 삭제하기보다 아래 명령을 사용하는 것이 낫다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree remove ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;12. worktree 디렉토리는 저장소 바깥에 만드는 것이 좋다&lt;/h2&gt;
&lt;p&gt;worktree 디렉토리는 기존 저장소 디렉토리 안에 만들 수도 있다. 하지만 보통은 기존 저장소 바깥에 만드는 편이 낫다. 예를 들어 기존 저장소가 아래 위치에 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;~/projects/my-app&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;그러면 worktree는 아래처럼 상위 디렉토리에 나란히 만드는 식이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;~/projects/my-app
~/projects/my-app-feature-a
~/projects/my-app-hotfix&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;명령어로는 아래처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../my-app-hotfix -b hotfix main&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;반대로 기존 저장소 내부에 worktree를 만들면 아래처럼 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;~/projects/my-app
~/projects/my-app/worktrees/hotfix&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이렇게 만들 수도 있지만, 기존 저장소 안에 또 다른 작업 디렉토리가 생기므로 &lt;code&gt;.gitignore&lt;/code&gt;에 추가해야 하는 번거로움이 생길 수 있다. 그래서 특별한 이유가 없다면 기존 저장소 바깥에 worktree 디렉토리를 만드는 편이 관리하기 쉽다.&lt;/p&gt;
&lt;h2&gt;13. .gitignore에 포함된 파일은 자동으로 생기지 않는다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt;는 현재 폴더를 그대로 복사하는 기능이 아니다. 특정 브랜치나 커밋을 별도 작업 디렉토리에 checkout하는 기능이다. 따라서 Git이 추적하지 않는 로컬 파일은 새 worktree에 자동으로 생기지 않는다. 예를 들어 프로젝트에 아래와 같은 파일들이 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.env
node_modules/
dist/&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;그리고 이 파일들이 &lt;code&gt;.gitignore&lt;/code&gt;에 포함되어 있다면, 새 worktree를 만들었을 때 자동으로 복사되지 않는다. 또한 현재 작업 디렉토리에 아직 커밋하지 않은 변경사항도 새 worktree로 복사되지 않는다. worktree는 현재 디렉토리의 파일 상태를 그대로 복제하는 기능이 아니라, 특정 브랜치나 커밋을 별도 작업 디렉토리에 checkout하는 기능이기 때문이다. 그래서 worktree를 만든 뒤에도 필요한 로컬 파일은 별도로 준비해야 한다. 예를 들어 &lt;code&gt;.env&lt;/code&gt; 같은 파일이 필요하다면 직접 복사하거나, 프로젝트에 맞는 별도 스크립트를 만들어두는 편이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp .env ../my-app-hotfix/.env&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;또는 자주 필요한 파일이 있다면 간단한 스크립트로 처리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

TARGET=$1

cp .env &amp;quot;$TARGET/.env&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;사용 예시는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./copy-local-files.sh ../my-app-hotfix&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;프로젝트마다 필요한 로컬 파일이 다르기 때문에, worktree를 사용할 때는 이 부분을 미리 정리해두는 것이 좋다.&lt;/p&gt;
&lt;h2&gt;14. worktree로 분리한 브랜치에 접근하는 방식&lt;/h2&gt;
&lt;p&gt;worktree로 분리한 브랜치는 이미 해당 작업 디렉토리에서 checkout된 상태다. 그래서 다른 worktree에서 그 브랜치로 다시 checkout하려고 하면 안 된다. 예를 들어 &lt;code&gt;B&lt;/code&gt; 브랜치를 &lt;code&gt;../test-branch&lt;/code&gt; worktree에 checkout했다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../test-branch B&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이제 기존 저장소 디렉토리에서 아래 명령을 실행하면 실패할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout B&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;왜냐하면 &lt;code&gt;B&lt;/code&gt; 브랜치는 이미 &lt;code&gt;../test-branch&lt;/code&gt; worktree에서 사용 중이기 때문이다. 이 경우 &lt;code&gt;B&lt;/code&gt; 브랜치에 접근하는 방법은 &lt;code&gt;checkout&lt;/code&gt;이 아니라 해당 디렉토리로 이동하는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;즉, worktree를 사용하면 브랜치 접근 방식이 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;기존 방식
→ 같은 디렉토리에서 git checkout으로 브랜치 이동

worktree 방식
→ 브랜치별 디렉토리로 직접 이동&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 개념을 이해하지 못하면 “왜 checkout이 안 되지?”라고 헷갈릴 수 있다. worktree로 분리한 브랜치는 이미 다른 디렉토리에서 checkout되어 있는 상태라고 보면 된다.&lt;/p&gt;
&lt;h2&gt;15. worktree를 제거해야 다시 checkout할 수 있다&lt;/h2&gt;
&lt;p&gt;worktree에서 사용 중인 브랜치를 기존 저장소 디렉토리에서 다시 checkout하고 싶다면 먼저 해당 worktree를 제거해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree remove ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;그 후에는 기존 저장소에서 해당 브랜치를 checkout할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout B&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;단순히 &lt;code&gt;../test-branch&lt;/code&gt; 폴더만 삭제하는 것은 권장하지 않는다. 폴더를 직접 삭제해도 Git 내부의 worktree 연결 정보가 남아 있을 수 있기 때문이다. 그래서 worktree를 정리할 때는 가능하면 아래 명령을 사용하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree remove ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;만약 이미 폴더를 직접 삭제했다면 아래 명령으로 남은 정보를 정리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree prune&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;16. worktree의 단점&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt;는 편리하지만 단점도 있다. 가장 큰 단점은 관리할 디렉토리가 늘어난다는 점이다. 브랜치별로 디렉토리가 생기기 때문에 프로젝트가 많아 보일 수 있고, 어떤 디렉토리가 어떤 브랜치인지 헷갈릴 수 있다. 또한 같은 프로젝트 파일이 여러 디렉토리에 존재하게 되므로 디스크 용량도 더 사용한다. 예를 들어 의존성 설치가 필요한 프로젝트라면 각 worktree마다 &lt;code&gt;node_modules&lt;/code&gt;를 따로 설치해야 할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;my-app/
my-app-feature-a/
my-app-hotfix/&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이런 구조가 편해지는 경우도 있지만, 반대로 관리 포인트가 늘어나는 경우도 있다. 그래서 단순히 브랜치를 가끔 바꾸는 정도라면 기존 checkout 방식이나 stash만으로도 충분할 수 있다. 하지만 여러 작업을 동시에 진행하거나, 브랜치 전환 비용이 크거나, AI 에이전트처럼 독립된 작업 환경을 여러 개 만들어야 한다면 worktree가 꽤 유용하다.&lt;/p&gt;
&lt;h2&gt;17. 자주 쓰는 명령어 정리&lt;/h2&gt;
&lt;p&gt;기존 브랜치를 새 worktree로 checkout한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../test-branch B&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;경로 이름을 기준으로 브랜치를 만들거나 checkout한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;기준 브랜치에서 새 브랜치를 만들고 worktree를 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add -b real-branch ../test-branch main&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;특정 브랜치의 커밋을 detached HEAD 상태로 checkout한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree add --detach ../test-branch main&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;현재 worktree 목록을 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree list&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;worktree를 제거한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree remove ../test-branch&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;수동 삭제 등으로 남은 worktree 관리 정보를 정리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree prune&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;18. 정리&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt;는 하나의 Git 저장소에 여러 작업 디렉토리를 연결해서 사용할 수 있게 해주는 기능이다. 기존에는 하나의 디렉토리 안에서 브랜치를 바꿔가며 작업했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout main
git checkout feature/a
git checkout feature/b&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;하지만 worktree를 사용하면 브랜치별로 작업 디렉토리를 분리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;my-app/             → main
my-app-feature-a/   → feature/a
my-app-feature-b/   → feature/b&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;핵심은 worktree가 브랜치를 복사하는 기능이 아니라, 특정 브랜치나 커밋을 별도의 작업 디렉토리에 checkout해두는 기능이라는 점이다. 그래서 worktree로 분리한 브랜치는 이미 해당 디렉토리에서 checkout된 상태다. 다른 디렉토리에서 같은 브랜치를 다시 checkout하려고 하면 실패할 수 있다. 접근 방식도 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;기존 방식
→ git checkout으로 브랜치 이동

worktree 방식
→ 브랜치가 checkout된 폴더로 직접 이동&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;그리고 worktree를 제거한다고 해서 브랜치가 삭제되는 것은 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;git worktree remove
→ worktree 구조와 작업 디렉토리 제거

git branch -d
→ 브랜치 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;결론:&lt;/p&gt;
&lt;p&gt;전환할 브랜치가 많지 않다&lt;br&gt;→ 기존 checkout 방식으로 충분하다.&lt;/p&gt;
&lt;p&gt;작업 중인 상태를 잠깐 저장하고 다른 브랜치로 이동해야 한다&lt;br&gt;→ git stash를 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;여러 브랜치를 동시에 열어두고 독립적으로 작업하고 싶다&lt;br&gt;→ git worktree가 유용하다.&lt;/p&gt;
&lt;p&gt;특히 AI 에이전트처럼 여러 작업 주체에게 독립된 작업 공간을 나눠줘야 한다면 &lt;code&gt;git worktree&lt;/code&gt;를 고려해볼 만하다.&lt;/p&gt;</description>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/8</guid>
      <comments>https://harubanbo.tistory.com/8#entry8comment</comments>
      <pubDate>Sat, 27 Jun 2026 23:40:25 +0900</pubDate>
    </item>
    <item>
      <title>pnpm corepack?</title>
      <link>https://harubanbo.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm 버전을 업데이트했는데도 pnpm -v를 찍어보면 예전 버전이 나오는 경우가 있다. 터미널을 껐다 켜도 동일한 이슈가 발생하는데 pnpm의 경우에는 내부 corepack과 시스템 설정 우선순위가 달라서 살펴볼 필요가 있다. 시스템에 pnpm이 여러 개 설치되어 있을 수 있고, Corepack을 사용 중이라면 실제로 실행되는 pnpm이 내가 생각한 것과 다를 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리용으로 남기는 글이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 먼저 현재 잡히는 pnpm 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 할 일은 현재 터미널에서 어떤 pnpm이 실행되는지 확인하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;pnpm -v
&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;size16&quot;&gt;현재 실행되는 pnpm 버전을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;which pnpm
&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;size16&quot;&gt;현재 pnpm 명령어가 어느 경로의 실행 파일을 보고 있는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;type -a pnpm
&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;size16&quot;&gt;type -a pnpm은 현재 셸에서 찾을 수 있는 pnpm 후보들을 모두 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 아래처럼 여러 개가 나올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;pnpm is /Users/me/.nvm/versions/node/v22.11.0/bin/pnpm
pnpm is /opt/homebrew/bin/pnpm
pnpm is /usr/local/bin/pnpm
&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;size16&quot;&gt;여기서 중요한 점은 &lt;b&gt;가장 첫 줄에 나오는 pnpm이 실제로 실행되는 pnpm&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 어딘가에는 pnpm 11이 설치되어 있어도 현재 셸의 PATH 우선순위상 pnpm 10이 먼저 잡히고 있다면 pnpm -v는 계속 10으로 나올 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 pnpm이 여러 개 잡힐까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm을 설치하는 방법은 여러 가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로는 아래 방식들이 있다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install -g pnpm
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;corepack enable pnpm
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;brew install pnpm
&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;size16&quot;&gt;그리고 Node.js 버전 매니저를 쓰고 있다면 Node.js 버전마다 전역 패키지 위치가 달라질 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 시스템에는 아래와 같은 pnpm들이 동시에 존재할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Corepack이 제공하는 pnpm
npm으로 전역 설치한 pnpm
Homebrew로 설치한 pnpm
Node.js 버전별 global bin에 설치된 pnpm
&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;size16&quot;&gt;이 중 어떤 pnpm이 실행되는지는 결국 PATH 우선순위에 따라 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 pnpm을 업데이트했는데도 버전이 바뀌지 않는다면, 실제로는 내가 업데이트한 pnpm이 아니라 다른 위치의 pnpm을 계속 실행하고 있는 상황일 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Corepack이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Corepack은 pnpm이나 Yarn 같은 패키지 매니저를 프로젝트별로 관리할 수 있게 해주는 Node.js 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히는 pnpm 자체라기보다는, pnpm 명령어 앞에 있는 프록시 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;pnpm 명령 실행
&amp;rarr; Corepack 프록시가 먼저 동작
&amp;rarr; 현재 프로젝트의 package.json 확인
&amp;rarr; 어떤 pnpm 버전을 써야 하는지 판단
&amp;rarr; 필요한 pnpm 버전을 다운로드하거나 캐시에서 찾아 실행
&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;size16&quot;&gt;즉, Corepack의 pnpm 프록시는 pnpm 명령을 가로채서 &amp;ldquo;이 프로젝트에서는 어떤 pnpm 버전을 써야 하지?&amp;rdquo;를 판단한 뒤 알맞은 pnpm을 실행해주는 중간 실행기라고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 16.13 이후 버전부터는 Corepack이 같이 제공되는 경우가 많다. 다만 Corepack이 포함되어 있다고 해서 항상 바로 활성화되어 있지는 않아서 아래 명령어로 활성화한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;corepack enable pnpm
&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은 Node.js 25부터는 Corepack이 Node.js 기본 배포판에 포함되지 않는 방향으로 바뀐다고 하니 앞으로의 환경에서는 Corepack을 별도로 설치해야 할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install -g corepack@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Corepack 사용 여부 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Corepack을 쓰는 환경인지 확인하려면 아래 명령어를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;corepack --version
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;which corepack
&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;size16&quot;&gt;Corepack을 통해 실행되는 pnpm 버전은 아래처럼 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;corepack pnpm -v
&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;size16&quot;&gt;다만 이 명령어는 단순히 &amp;ldquo;Corepack의 전역 기본 pnpm 버전&amp;rdquo;만 보여준다고 생각하면 헷갈릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 내부에서 실행하면 Corepack은 현재 디렉터리 기준으로 가장 가까운 package.json을 찾고, 그 안의 packageManager 값을 먼저 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 package.json에 아래처럼 적혀 있다면,&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;packageManager&quot;: &quot;pnpm@11.1.3&quot;
}
&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;size16&quot;&gt;Corepack은 해당 프로젝트에서 pnpm 11.1.3을 쓰려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 명시된 packageManager가 없다면 Corepack은 프로젝트에서 원하는 pnpm 버전을 알 수 없다. 이 경우 Corepack이 알고 있는 기본 버전이나, 이미 활성화된 pnpm 버전이 실행될 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 프로젝트에서 pnpm 버전 고정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 프로젝트에서 pnpm 버전을 맞추고 싶다면 package.json의 packageManager 속성을 사용하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 현재 프로젝트에서 pnpm 11을 쓰고 싶다면 아래 명령어를 실행한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;corepack use pnpm@latest-11
&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;size16&quot;&gt;특정 버전을 정확히 지정할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;corepack use pnpm@11.1.3
&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;size16&quot;&gt;이 명령어를 실행하면 현재 프로젝트의 package.json에 packageManager 속성이 추가되거나 갱신된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;packageManager&quot;: &quot;pnpm@11.1.3&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해두면 Corepack을 사용하는 팀원들은 해당 프로젝트에서 같은 pnpm 버전을 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 pnpm 버전도 의존성처럼 프로젝트에 명시해두는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm-lock.yaml만 맞춘다고 해서 항상 같은 결과가 나온다고 보기 어렵다. pnpm 자체의 버전이 다르면 lockfile 처리 방식이나 지원 기능이 달라질 수 있기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 프로젝트 밖에서 사용할 기본 pnpm 버전 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 내부가 아니라, 프로젝트 밖에서 기본으로 사용할 pnpm 버전을 Corepack 기준으로 바꾸고 싶다면 아래 명령어를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;corepack install --global pnpm@latest-11
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 특정 버전을 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;corepack install --global pnpm@11.1.3
&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;size16&quot;&gt;이 명령은 시스템에 직접 pnpm을 전역 설치하는 것과는 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히는 Corepack이 프로젝트 밖에서 pnpm을 실행할 때 사용할 기본 버전을 설정하는 것에 가깝다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 프로젝트의 package.json에 pnpm 버전 명시
corepack use pnpm@latest-11
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;# Corepack이 프로젝트 밖에서 사용할 기본 pnpm 버전 설정
corepack install --global pnpm@latest-11
&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;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;프로젝트별 pnpm 버전을 고정하고 싶다
&amp;rarr; corepack use pnpm@버전

프로젝트 밖에서 기본으로 쓸 Corepack pnpm 버전을 바꾸고 싶다
&amp;rarr; corepack install --global pnpm@버전
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Corepack을 쓰지 않고 pnpm을 전역 설치하는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Corepack을 쓰지 않고 pnpm을 직접 전역 설치할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;npm install -g pnpm@latest-11
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 시스템 PATH 기준으로 pnpm 실행 파일이 잡힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이미 Corepack이 활성화되어 있다면, 내가 npm으로 전역 설치한 pnpm보다 Corepack의 pnpm shim이 먼저 잡힐 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 전역 설치를 했는데도 버전이 바뀌지 않는다면 다시 아래를 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;which pnpm
type -a pnpm
&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;size16&quot;&gt;Corepack을 아예 사용하지 않을 생각이라면 비활성화할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;corepack disable
&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;size16&quot;&gt;또는 pnpm만 대상으로 비활성화할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;corepack disable pnpm
&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;size16&quot;&gt;다만 팀 프로젝트에서 패키지 매니저 버전까지 맞추고 싶다면 Corepack을 끄는 것보다, Corepack을 활성화하고 프로젝트의 packageManager를 명시하는 쪽이 더 안정적이다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트라면 없어도 큰 문제가 없을 수 있지만 팀 프로젝트에서는 없는 것보다 있는 편이 좋다. packageManager가 없으면 각자 시스템에 설치된 pnpm 버전을 쓰게 될 수 있는데 어떤 사람은 pnpm 9, 어떤 사람은 pnpm 10, 어떤 사람은 pnpm 11을 쓰는 상황이 생긴다. 결과적으로 lockfile 변경이 불필요하게 생기거나, 특정 명령이 어떤 환경에서는 되고 어떤 환경에서는 안 되는 문제가 생길 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm을 업데이트했는데도 예전 버전이 나오는 이유는 보통 아래 중 하나다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. PATH 우선순위상 예전 pnpm이 먼저 잡히고 있다.
2. Corepack의 pnpm shim이 실행되고 있다.
3. 현재 프로젝트의 package.json에 packageManager가 예전 버전으로 고정되어 있다.
4. 프로젝트 밖에서 Corepack 기본 pnpm 버전이 예전 버전으로 설정되어 있다.
5. Node.js 버전이 pnpm 최신 버전을 지원하지 않는다.
&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;size16&quot;&gt;핵심은 단순히 &amp;ldquo;pnpm을 설치했다&amp;rdquo;가 아니라, &lt;b&gt;현재 터미널에서 어떤 pnpm이 실행되고 있는지&lt;/b&gt;를 확인하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 팀 프로젝트에서는 Corepack을 활성화하고, 프로젝트마다 packageManager를 명시하는 방식이 가장 낫다고 생각한다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;packageManager&quot;: &quot;pnpm@11.1.3&quot;
}
&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;size16&quot;&gt;이렇게 해두면 프로젝트를 받은 사람이 별도로 pnpm 버전을 맞추지 않아도 Corepack이 해당 버전을 찾아 실행해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Corepack을 쓰면 pnpm을 단순히 전역 설치해서 쓰는 것이 아니라, 프로젝트별로 pnpm 버전까지 관리하는 구조가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론:&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;전역 pnpm 하나로 모든 프로젝트를 처리하고 싶다
&amp;rarr; npm install -g pnpm

프로젝트별 pnpm 버전까지 재현성 있게 맞추고 싶다
&amp;rarr; corepack + packageManager
&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;size16&quot;&gt;팀 프로젝트라면 후자가 더 안전하다.&lt;/p&gt;</description>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/7</guid>
      <comments>https://harubanbo.tistory.com/7#entry7comment</comments>
      <pubDate>Sat, 13 Jun 2026 23:40:38 +0900</pubDate>
    </item>
    <item>
      <title>리액트 Context 정리</title>
      <link>https://harubanbo.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 Context 개념이 정리용으로 요약해서 남기는 글이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 컨텍스트란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Context는 &lt;b&gt;props drilling 문제를 해결&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;props로 전달받지 않고 직접 사용&lt;/b&gt; 가능&lt;/li&gt;
&lt;li&gt;핵심 3요소: &lt;code&gt;createContext&lt;/code&gt; &amp;rarr; &lt;code&gt;Provider&lt;/code&gt; &amp;rarr; &lt;code&gt;useContext&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context 사용 방식에는 useContext 훅을 사용하는 방법과 Consumer라는 것을 사용하는 방법이 있으나 함수형에서는 useContext가 사실상 표준으로 Consumer는 클래스 컴포넌트나 일부 조건부 렌더링 같은 특수 케이스에서만 쓴다고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본 사용 예제&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { createContext, useContext } from 'react';

// 1) 컨텍스트 객체 생성
const ThemeContext = createContext&amp;lt;'light' | 'dark'&amp;gt;('light');

// 2) Provider로 값 제공
function App() {
  return (
    &amp;lt;ThemeContext.Provider value=&quot;dark&quot;&amp;gt;
      &amp;lt;Page /&amp;gt;
    &amp;lt;/ThemeContext.Provider&amp;gt;
  );
}

// 3) 사용처에서 useContext로 소비
function Page() {
  const theme = useContext(ThemeContext);
  return &amp;lt;div&amp;gt;현재 테마: {theme}&amp;lt;/div&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 리렌더링 동작&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 코드&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/app/test2/page.tsx
'use client';

import ComponentA from &quot;@/component/test2/ComponentA&quot;;
import ComponentB from &quot;@/component/test2/ComponentB&quot;;
import CounterProvider from &quot;@/component/test2/CounterProvider&quot;;
import { useState } from &quot;react&quot;;

export default function ContextPage() {
    const parentCounter = useState(7);
    console.log(&quot;page&quot;);
    return (
        &amp;lt;CounterProvider parentCounter={parentCounter}&amp;gt;
            &amp;lt;h1&amp;gt;Context 리렌더링 테스트&amp;lt;/h1&amp;gt;
            &amp;lt;ComponentA /&amp;gt;
            &amp;lt;ComponentB /&amp;gt;
        &amp;lt;/CounterProvider&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/component/test2/CounterProvider.tsx
import React, { createContext, useContext, useState } from &quot;react&quot;;

const counterContext = createContext&amp;lt;[
    [number, React.Dispatch&amp;lt;React.SetStateAction&amp;lt;number&amp;gt;&amp;gt;],
    [number, React.Dispatch&amp;lt;React.SetStateAction&amp;lt;number&amp;gt;&amp;gt;]
]&amp;gt;([
    [0, () =&amp;gt; {}],
    [0, () =&amp;gt; {}],
]);

// 부모로부터 값을 받아오거나 프로바이더 자체도 값을 가지고 있다.
export default function CounterProvider({
    children,
    parentCounter,
}: {
    children: React.ReactNode;
    parentCounter: [number, React.Dispatch&amp;lt;React.SetStateAction&amp;lt;number&amp;gt;&amp;gt;];
}) {
    const providerCounter = useState(0);
    console.log(&quot;CounterProvider&quot;);
    return (
        &amp;lt;counterContext.Provider value={[providerCounter, parentCounter]}&amp;gt;
            {children}
        &amp;lt;/counterContext.Provider&amp;gt;
    );
}

export function useCounterContext() {
    return useContext(counterContext);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/component/test2/ComponentA.tsx
import ComponentA_1 from &quot;./ComponentA_1&quot;;

export default function ComponentA() {
    console.log(&quot;ComponentA&quot;);
    return (
        &amp;lt;&amp;gt;
            &amp;lt;div&amp;gt;ComponentA&amp;lt;/div&amp;gt;
            &amp;lt;ComponentA_1 /&amp;gt;
        &amp;lt;/&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/component/test2/ComponentA_1.tsx
import { useCounterContext } from &quot;./CounterProvider&quot;;

export default function ComponentA_1() {
    const [, parentCounter] = useCounterContext();
    const [counter, setCounter] = parentCounter;
    console.log(&quot;ComponentA_1&quot;);
    return (
        &amp;lt;div&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; setCounter(counter + 1)}&amp;gt;
                ComponentA_1 parent button: {counter}
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/component/test2/ComponentB.tsx
import ComponentB_1 from &quot;./ComponentB_1&quot;;

export default function ComponentB() {
    console.log(&quot;ComponentB&quot;);
    return (
        &amp;lt;&amp;gt;
            &amp;lt;div&amp;gt;ComponentB&amp;lt;/div&amp;gt;
            &amp;lt;ComponentB_1 /&amp;gt;
        &amp;lt;/&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/component/test2/ComponentB_1.tsx
import { useCounterContext } from &quot;./CounterProvider&quot;;

export default function ComponentB_1() {
    const [providerCounter] = useCounterContext();
    const [counter, setCounter] = providerCounter;
    console.log(&quot;ComponentB_1&quot;);
    return (
        &amp;lt;div&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; setCounter(counter + 1)}&amp;gt;
                ComponentB_1 provider button: {counter}
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트리 구조&lt;/h3&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;ContextPage (parentCounter state 소유)
└─ CounterProvider (providerCounter state 소유, context value 제공)
   ├─ ComponentA
   │  └─ ComponentA_1  &amp;larr; parentCounter 구독
   └─ ComponentB
      └─ ComponentB_1  &amp;larr; providerCounter 구독&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;각 버튼 클릭 시 console 출력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;ComponentB_1&lt;/code&gt; 버튼 클릭&lt;/b&gt; &amp;mdash; &lt;code&gt;providerCounter&lt;/code&gt; 변경 (state 소유자: &lt;code&gt;CounterProvider&lt;/code&gt;)&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;CounterProvider
ComponentA_1
ComponentB_1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;ComponentA_1&lt;/code&gt; 버튼 클릭&lt;/b&gt; &amp;mdash; &lt;code&gt;parentCounter&lt;/code&gt; 변경 (state 소유자: &lt;code&gt;ContextPage&lt;/code&gt;)&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;page
CounterProvider
ComponentA
ComponentA_1
ComponentB
ComponentB_1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리렌더링 규칙 (자주 하는 착각 정정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 ComponentB_1 버튼을 누르면 중간에 ComponentA와 ComponentB도 리렌더링 될 것이라고 생각할 수 있으나 그렇지 않다.&lt;/p&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;&lt;b&gt;정확한 규칙:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트는 &lt;b&gt;자기 상태(state)나 props가 변할 때&lt;/b&gt;만 리렌더된다.&lt;/li&gt;
&lt;li&gt;리렌더된 컴포넌트의 &lt;b&gt;JSX 본문 안에서 새로 생성된&lt;/b&gt; 자식 React Element만 리렌더 대상이 된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{children}&lt;/code&gt;처럼 &lt;b&gt;props로 받은 엘리먼트는 호출자가 만든 객체&lt;/b&gt;다. 호출자가 리렌더되지 않으면 객체 참조가 그대로라서 리액트는 그 서브트리 호출을 생략한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨텍스트를 사용하는 측은 트리 구조와 무관하게&lt;/b&gt;, Provider의 value가 바뀌면 별도로 리렌더가 예약된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;children as props&quot; 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;ComponentA/&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;ComponentB/&amp;gt;&lt;/code&gt; 엘리먼트는 &lt;b&gt;&lt;code&gt;ContextPage&lt;/code&gt;가 만든 객체&lt;/b&gt;다. &lt;code&gt;CounterProvider&lt;/code&gt;는 그것을 &lt;code&gt;children&lt;/code&gt;으로 받아서 그대로 꽂아둘 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;code&gt;CounterProvider&lt;/code&gt;가 자기 state 변경으로 리렌더돼도, &lt;b&gt;&lt;code&gt;children&lt;/code&gt; 참조가 그대로&lt;/b&gt;라서 &lt;code&gt;ComponentA&lt;/code&gt;, &lt;code&gt;ComponentB&lt;/code&gt;는 호출되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 단, &lt;code&gt;ComponentA_1&lt;/code&gt;, &lt;code&gt;ComponentB_1&lt;/code&gt;는 &lt;code&gt;useContext&lt;/code&gt; 구독자이므로 컨텍스트 값이 바뀌면 별도로 리렌더된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심:&lt;/b&gt; &quot;JSX 트리상의 부모-자식&quot;과 &quot;엘리먼트를 만든 책임자&quot;는 다르다. 리렌더 전파는 후자(엘리먼트를 만든 컴포넌트)를 기준으로 일어난다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 동일 컨텍스트를 중첩해서 쓰는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;규칙: 가장 가까운(closest) 상위 Provider의 값이 적용된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Parent
└─ &amp;lt;ThemeContext value=&quot;dark&quot;&amp;gt;          &amp;larr; (A)
   ├─ Child1   ─ useContext &amp;rarr; &quot;dark&quot;   (A가 가장 가까움)
   └─ &amp;lt;ThemeContext value=&quot;light&quot;&amp;gt;      &amp;larr; (B)
      └─ Child2 ─ useContext &amp;rarr; &quot;light&quot; (B가 가장 가까움)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활용:&lt;/b&gt; 전체는 dark 테마지만, 특정 모달/섹션만 light로 덮어쓰고 싶을 때 같은 부분 오버라이드가 가능하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 서로 다른 컨텍스트를 중첩해서 쓰는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심: 순서는 동작에 영향이 없다.&lt;/b&gt; 두 컨텍스트가 서로 독립적이면 누가 상위든 하위든 결과는 같다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;// 둘은 동작상 동일
&amp;lt;ThemeContext.Provider value=&quot;dark&quot;&amp;gt;
  &amp;lt;UserContext.Provider value={user}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/UserContext.Provider&amp;gt;
&amp;lt;/ThemeContext.Provider&amp;gt;

&amp;lt;UserContext.Provider value={user}&amp;gt;
  &amp;lt;ThemeContext.Provider value=&quot;dark&quot;&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/ThemeContext.Provider&amp;gt;
&amp;lt;/UserContext.Provider&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/6</guid>
      <comments>https://harubanbo.tistory.com/6#entry6comment</comments>
      <pubDate>Sat, 23 May 2026 23:31:54 +0900</pubDate>
    </item>
    <item>
      <title>FSD 아키텍처 정독하기 4편</title>
      <link>https://harubanbo.tistory.com/5</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;FSD 적용 중 주의점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UI의 큰 재사용 블록 widget&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모든 페이지에 공통적으로 쓰이는 헤더 UI가 있다고 가정해보자, 이 헤더 UI를 모든 페이지에 구현하는 것은 비효율적이므로 재사용 로직으로 분리하고 싶을 것이다. 이미 shared라는 코드 재사용에 용이한 레이어를 알고있지만 shared 레이어에 큰 UI 블록을 넣는 것은 주의할 점이 있다.&lt;br /&gt;&lt;br /&gt;UI 블록이 크더라도 단순 API 호출 등 내부 구현 로직이 굉장히 간단하면 문제가 없으나 서로 다른 도메인의 데이터를 가져와서 UI 구성요소를 구현해야하는 복잡한 경우에는 보통 features나 entities 레이어에 관련 요소들이 위치할 것이다. 레이어 참조 제약 조건에 따라서 shared 레이어는 상위 레이어인 features나 entities의 요소를 참조할 수 없다.&lt;br /&gt;&lt;br /&gt;그렇다면 이런 경우에는 헤더 UI를 shared 레이어에 구성하는 게 아니라 widgets 레이어에 구현하고 widgets에서 features, entities 레이어의 요소를 가져와서 구현하는 것이 올바른 방향이다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// shared/ui/Header.tsx 로고 + 네비게이션 링크만 있음, 상위 레이어(Features, Entities) 사용 안 함
export function Header() {
  return (
    &amp;lt;nav&amp;gt;
      &amp;lt;Logo /&amp;gt;
      &amp;lt;Link to=&quot;/&quot;&amp;gt;Home&amp;lt;/Link&amp;gt;
      &amp;lt;Link to=&quot;/login&quot;&amp;gt;Sign in&amp;lt;/Link&amp;gt;
    &amp;lt;/nav&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// widgets/header/ui/Header.tsx 올바른 방법 - Widgets 레이어 사용
import { LoginButton } from 'features/auth';      // 하위 레이어
import { CartPreview } from 'features/cart';      // 하위 레이어
import { UserAvatar } from 'entities/user';       // 하위 레이어
import { Button } from 'shared/ui';               // 하위 레이어

export function Header() {
  return (
    &amp;lt;nav&amp;gt;
      &amp;lt;Logo /&amp;gt;
      &amp;lt;CartPreview /&amp;gt;
      &amp;lt;UserAvatar /&amp;gt;
      &amp;lt;LoginButton /&amp;gt;
    &amp;lt;/nav&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;features와 entities에 들어가야 하는 것은 API 호출, 상태관리, 도메인 지식 등 비즈니스 로직이 포함된 요소가 들어가야 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// features/auth/ui/LoginButton.tsx
// shared에 넣으면 안 됨 - 로그인 로직을 알고 있음
import { useAuth } from '../model/authStore';
import { login } from '../api/login';

export function LoginButton() {
  const { isLoggedIn, logout } = useAuth();

  if (isLoggedIn) {
    return &amp;lt;button onClick={logout}&amp;gt;로그아웃&amp;lt;/button&amp;gt;;
  }
  return &amp;lt;button onClick={login}&amp;gt;로그인&amp;lt;/button&amp;gt;;
}

// entities/user/ui/UserAvatar.tsx
// shared에 넣으면 안 됨 - User라는 도메인 개념을 알고 있음
import { useCurrentUser } from '../model/userStore';
import { Avatar } from 'shared/ui';  // Shared의 순수 UI 사용

export function UserAvatar() {
  const user = useCurrentUser();  // 현재 유저 정보 가져오기

  return (
    &amp;lt;Link to={`/profile/${user.username}`}&amp;gt;
      &amp;lt;Avatar src={user.image} alt={user.username} /&amp;gt;
    &amp;lt;/Link&amp;gt;
  );
}

// features/cart/ui/CartPreview.tsx
// shared에 넣으면 안 됨 - 장바구니 비즈니스 로직 포함
import { useCart } from '../model/cartStore';

export function CartPreview() {
  const { items, totalPrice } = useCart();

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;span&amp;gt;  {items.length}개&amp;lt;/span&amp;gt;
      &amp;lt;span&amp;gt;{totalPrice}원&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스 파일 사용시 주의사항 - circular import(순환참조)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;circular import는 두 개 이상의 파일이 서로를 참조하는 구조를 말한다. 이런 구조는 번들러가 처리하기 어렵고 디버깅 하기도 힘든 런타임 오류를 만들 수 있다. 순환 참조는 인덱스 파일이 없더라도 발생할 수 있지만 인덱스 파일을 쓰면 이런 실수가 발생하기 쉬워진다. 예를 들어서 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;  pages/home/
    index.js # Public API 정의
    ui/
      HomePage.jsx
    api/
      loadUserStatistics.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index(public API)에서 HomePage를 가져온다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// pages/home/index.js (Public API)
export { HomePage } from &quot;./ui/HomePage&quot;; // HomePage 가져옴
export { loadUserStatistics } from &quot;./api/loadUserStatistics&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HomePage 안에서 index의 loadUserStatistics를 임포트&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// pages/home/ui/HomePage.jsx
import { loadUserStatistics } from &quot;../&quot;;  // index.js에서 가져옴

export function HomePage() {
  // loadUserStatistics 사용...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순환 구조 발생&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;index.js (public API) ─────────┐
    │                          │
    │ export { HomePage }      │
    ▼                          │
HomePage.jsx                   │
    │                          │
    │ import { loadUserStatistics } from &quot;../&quot;
    │                          │
    └──────────────────────────┘
         다시 index.js로!&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;size16&quot;&gt;그래서 이를 해결하려면 같은 슬라이스 내부에서 모듈을 import할 때는 상대경로를 사용해서 정확히 어느 파일을 참조하는지 명확히 작성해야 한다. 결국 public API(index)는 정말 외부 슬라이스를 위한 용도로만 참조해야 한다. index에서 export한 모듈이 다시 index를 참조하지 않도록 주의한다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// ❌ 잘못된 방법 - index.js(public API)를 통해 가져옴
import { loadUserStatistics } from &quot;../&quot;;

// ✅ 올바른 방법 - 파일을 직접 가져옴 (순환 없음)
import { loadUserStatistics } from &quot;../api/loadUserStatistics&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// pages/home/ui/HomePage.jsx

// 같은 슬라이스 내부 &amp;rarr; 직접 경로
import { loadUserStatistics } from &quot;../api/loadUserStatistics&quot;;

// 다른 슬라이스 public API (절대경로나 alias를 사용)
import { Button } from &quot;@/shared/ui&quot;;
import { UserCard } from &quot;@/entities/user&quot;;&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;FSD 적용 중 최적화 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;large bundle &amp;amp; tree shaking 문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 번들러는 index 파일에서 여러 모듈을 한 번에 export할 경우 실제로 사용하지 않는 코드를 제대로 제거하지 못할 수 있다. 대부분의 public API에서는 모듈간 연관성이 높아서 큰 문제는 없으나 shared/ui, shared/lib처럼 서로 관련성이 낮은 모듈 묶음에서는 문제가 커진다. 예를 들어서 아래와 같은 구조가 있다면&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;-   shared/ui/
    -   button
    -   text-field
    -   carousel
    -   accordion&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황에서 외부에서 단순히 button 하나만 쓰고 싶은데 shared/ui 전체를 통째로 하는 큰 index(public API)가 있다면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;carousel, accordian 등 무거운 의존성까지 함께 번들에 포함될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 각 코드 안에 무거운 라이브러리까지 포함되어 있다면 최종 번들 크기에 큰 영향을 준다. 이를 해결하려면 다음과 같이 각 컴포넌트, 라이브러리 별로 별도의 작은 index(pulbic API)를 만든다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;-   shared/ui/
    -   button
        - index.ts
    -   text-field
        - index.ts&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 사용하는 쪽에서는 shared/ui의 public이 아니라 shared/ui/button, shared/ui/text-field와 같이 컴포넌트 단위에 직접 import해서 public API(index)를 불러온다&lt;/p&gt;
&lt;pre class=&quot;capnproto&quot;&gt;&lt;code&gt;import { Button } from &quot;@/shared/ui/button&quot;;
import { TextField } from &quot;@/shared/ui/text-field&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 public API의 우회 방지의 한계점이 있다. 슬라이스에 index(public API)를 만들어도 개발자가 직접 내부 경로를 입력해서 import하거나 IDE 차원에서 auto import를 통해 내부 파일 경로를 자동으로 선택해버리면 public API 규칙이 깨질 수 있으므로 주의해야 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대규모 프로젝트에서의 bundler 성능 문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index(public API)가 너무 많아지면 불필요한 모듈이 import되므로 개발서버 실행 속도나 HMR 성능이 저하될 수 있다.&lt;/p&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;large bundle &amp;amp; tree shaking 문제에서 설명한 것처럼 shared/ui, shared/lib 등에 있는 큰 index를 없애고 컴포넌트/모듈 단위로 쪼갠 작은 index를 사용한다.&lt;/li&gt;
&lt;li&gt;세그먼트 단위로 불필요한 index(public API)를 만들지 않는다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;예를 들어서 features/comments/index.ts가 이미 슬라이스의 public API 역할을 하고 있다면 features/comments/ui/index.ts같이 중첩된 index는 굳이 만들 필요가 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;큰 프로젝트는 기능 단위 chunk 또는 패키지로 나눈다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Google Docs처럼 Document Editor와 File Browser를 서로 다른 chunk 패키지로 분리한다.&lt;/li&gt;
&lt;li&gt;모노레포에서는 각 패키지를 독립적인 FSD root로 구성할 수 있다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일부 패키지는 shared와 entities 레이어만 포함&lt;/li&gt;
&lt;li&gt;다른 패키지는 app, pages 레이어만 포함&lt;/li&gt;
&lt;li&gt;필요한 경우 작은 shared를 각 패키지에 두고 다른 패키지의 큰 shared를 참조하는 방식으로 설계&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 소리냐면 프로젝트가 커지면 모든 코드가 하나의 FSD 구조에 들어있게 된다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;  src/
    app/
    pages/
      document-editor/
      file-browser/
      settings/
      user-profile/
    ... (수십 개의 페이지)
    features/
    ... (수십 개의 기능)
    entities/
    ... (수십 개의 엔티티)
    shared/
    ... (수백 개의 공통 모듈)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이는 번들러가 모든 파일을 한꺼번에 처리해야 해서 느려진다. 그래서 기능 단위로 쪼개는 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Google Docs 예시&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;Document Editor&lt;/b&gt; (문서 편집기) - 독립된 앱처럼 동작&lt;/li&gt;
&lt;li&gt;&lt;b&gt;File Browser&lt;/b&gt; (파일 탐색기) - 독립된 앱처럼 동작&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Settings&lt;/b&gt; (설정) - 독립된 앱처럼 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들을 별도의 패키지로 분리하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;glsl&quot;&gt;&lt;code&gt;  packages/
    document-editor/        # 독립된 FSD 구조
      src/
        app/
        pages/
        widgets/
        features/
        entities/
        shared/

    file-browser/           # 독립된 FSD 구조
      src/
        app/
        pages/
        features/
        entities/
        shared/

    shared-core/            # 공통으로 쓰는 것들
      src/
        shared/
        entities/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;패키지간의 관계는 아래와 같다&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;┌─────────────────┐     ┌─────────────────┐
│ document-editor │     │  file-browser   │
│                 │     │                 │
│  자체 shared/   │     │  자체 shared/   │
│  자체 features/ │     │  자체 features/ │
└────────┬────────┘     └────────┬────────┘
         │                       │
         │    참조               │    참조
         ▼                       ▼
    ┌─────────────────────────────────┐
    │          shared-core            │
    │                                 │
    │  공통 UI 컴포넌트               │
    │  공통 Entity (User 등)          │
    │  공통 API 클라이언트            │
    └─────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실제 구조 예시는 아래와 같다&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;  packages/
    shared-core/                 # 전체 공통
      shared/
        ui/                      # Button, Modal 등
        api/                     # API 클라이언트
      entities/
        user/                    # User는 모든 곳에서 필요

    document-editor/             # 문서 편집기 전용
      shared/
        ui/                      # 에디터 전용 UI (Toolbar 등)
      entities/
        document/                # Document 엔티티
      features/
        text-formatting/         # 텍스트 서식
        insert-image/            # 이미지 삽입
      pages/
        editor/

    file-browser/                # 파일 탐색기 전용
      entities/
        folder/
        file/
      features/
        file-upload/
        file-search/
      pages/
        browser/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;즉, 각 패키지가 자체 FSD 구조를 가지고, 공통 부분만 `shared-core` 같은 패키지로 분리하는 방식이다. 결국 모노레포로 전환하라는 의미인데 다음 상황이면 프로젝트를 모노레포로의 전환을 고려한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드/HMR이 눈에 띄게 느려질 때&lt;/li&gt;
&lt;li&gt;서로 독립적인 큰 기능 영역이 생길 때&lt;/li&gt;
&lt;li&gt;여러 팀이 각 영역을 담당할 때&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/5</guid>
      <comments>https://harubanbo.tistory.com/5#entry5comment</comments>
      <pubDate>Sat, 9 May 2026 23:02:02 +0900</pubDate>
    </item>
    <item>
      <title>FSD 아키텍처 정독하기 3편</title>
      <link>https://harubanbo.tistory.com/4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;슬라이스와 세그먼트&lt;/h2&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;사진 갤러리: photo, effetcts, gallery-page&lt;/li&gt;
&lt;li&gt;소셜 네트워크: post, comments, news-feed&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 shared 레이어는 비즈니스 로직이 전혀 없으므로, 제품 관점에서 슬라이스로 나눌 의미가 없다. app 레이어 또한 애플리케이션 전체를 다루기 때문에, 여기서 다시 슬라이스로 나눌 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬라이스는 다른 슬라이스와 최대한 독립적(cross-import를 쓰지 않는 이상 같은 레이어 내에서 다른 슬라이스 간섭 불가)이어야 하고 또한 자신의 핵심 목적과 직접적으로 관련된 코드 대부분을 내부에 포함해야 한다. 슬라이스 내부 모듈(파일)은 자신보다 하위 레이어의 슬라이스의 모듈(파일)만 import 가능하다. (features 레이어의 슬라이스 안에서 entities 레이어의 요소 import)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;슬라이스 그룹&lt;/b&gt;&lt;/p&gt;
&lt;p 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-origin-width=&quot;365&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MwS5s/dJMcaiwjoj2/5CKHYoCKHCxbTTtssCAir0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MwS5s/dJMcaiwjoj2/5CKHYoCKHCxbTTtssCAir0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MwS5s/dJMcaiwjoj2/5CKHYoCKHCxbTTtssCAir0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMwS5s%2FdJMcaiwjoj2%2F5CKHYoCKHCxbTTtssCAir0%2Fimg.jpg&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;365&quot; height=&quot;236&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;세그먼트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세그먼트는 FSD 구조에서 마지막 계층으로 코드를 기술적인 역할과 성격에 따라 나누는 기준인데 슬라이스 하위나 app, shared 레이어 하위에서 세분화되는 구조로 코드의 역할(UI, 데이터처리, 상태관리 등)에 따라 코드를 그룹화한다. 일반적으로는 다음과 같은 표준 세그먼트로도 충분하며 세그먼트 또한 아래 5가지를 무조건 만들 필요 없이 필요한 것만 만들어서 도입하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ui&lt;/code&gt; - UI 관련 코드: Component, Date Formatter, Style 등 UI 표현과 직접 관련된 코드(렌더링과 외관을 다루는 코드)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api&lt;/code&gt; - Backend 통신: Request Function, Data Type, Mapper 등&lt;/li&gt;
&lt;li&gt;&lt;code&gt;model&lt;/code&gt; - Data Model: Schema, Interface, Store, Business Logic 등 애플리케이션 도메인 모델 등&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lib&lt;/code&gt; - Slice 내부에서 사용하는 공통 Library 코드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config&lt;/code&gt; - Configuration, Feature Flag 등 설정 관련 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 맞게 커스텀 세그먼트를 정의할 수도 있다. 특히 app, shared 레이어는 슬라이스가 없어서 이 두 레이어에서는 커스텀 세그먼트를 자주 사용하게 된다. (필요하다면 App 또는 Shared 레이어에서만 추가적인 segment를 정의해라) 세그먼트 이름을 정할 때는 폴더 안에 무슨 파일이 들어있는지가 아니라 무엇을 위해 존재하는지(목적)이 드러나도록 작성하는 게 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;public API - 엄격한 공개 API 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;public API는 슬라이스의 기능을 외부에서 사용할 수 있는 공식 경로다. 상위 레이어는 하위 레이어의 요소를 참조할 수 있다고 했는데 무작정 참고할 수 있는 게 아니다. 하위 레이어의 요소에서 공개 API 정의를 해줘야 상위 레이어가 참조할 수 있다. 외부 코드는 반드시 이 경로를 통해서만 슬라이스 내부의 특정 코드에 접근할 수 있다. 즉, 슬라이스와 외부 코드간에 계약이자 접근 게이트의 역할이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 public API는 re-export를 모아둔 index 파일로 구현한다. 예를 들어서 pages/auth/index.js 파일에서 LoginPage, RegisterPage등을 다시 내보내는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export { LoginPage } from &quot;./ui/LoginPage&quot;;
export { RegisterPage } from &quot;./ui/RegisterPage&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;  shared/
    ui/
      Button.tsx
      Avatar.tsx
      Modal.tsx
      index.ts # 공개 API 필수

  entities/
    user/
      ui/
        UserAvatar.tsx
        UserCard.tsx
      model/
        userStore.ts
      index.ts # 공개 API 필수

  features/
    auth/
      ui/
        LoginButton.tsx
      index.ts # 공개 API 필수&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// shared/ui/index.ts
export { Button } from './Button';
export { Avatar } from './Avatar';
export { Modal } from './Modal';

// entities/user/index.ts
export { UserAvatar } from './ui/UserAvatar';
export { UserCard } from './ui/UserCard';
export { useCurrentUser } from './model/userStore';
export type { User } from './model/types';

// features/auth/index.ts
export { LoginButton } from './ui/LoginButton';&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// widgets/header/ui/Header.tsx
import { Button, Avatar } from 'shared/ui'; // 공개 API 통해
import { UserAvatar } from 'entities/user'; // 공개 API 통해
import { LoginButton } from 'features/auth';// 공개 API 통해

// ❌ 이렇게 직접 접근 금지
import { Button } from 'shared/ui/Button';
import { UserAvatar } from 'entities/user/ui/UserAvatar';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하는 이유는 내부 구조 변경에 자유롭다는 장점이 있다. 예를 들어서 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// entities/user/ui/UserAvatar.tsx &amp;rarr; entities/user/ui/avatars/UserAvatar.tsx 로 내부 구조를 바꿔도

// entities/user/index.ts만 경로 수정하면 됨
export { UserAvatar } from './ui/avatars/UserAvatar'; // 경로만 변경

// 상위 레이어 코드는 변경 불필요
import { UserAvatar } from 'entities/user'; // 그대로 동작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개 API가 외부와의 계약 역할을 하므로 내부를 마음대로 리팩토링해도 상위 레이어 참조 코드에는 영향이 없다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;슬라이스의 public API 규칙&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬라이스 내부 구조는 팀이 원하는 방식으로 자유롭게 설계할 수 있다. 하지만 다른 슬라이스에서 사용할 수 있도록 명확한 public API를 반드시 제공해야 한다. 이 규칙을 slice public API rule이라고 한다. (자세한 내용은 아래 public API에서 설명)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 슬라이스(또는 슬라이스가 없는 레이어의 세그먼트 등)는 public API를 정의해야 한다.&lt;/li&gt;
&lt;li&gt;외부 모듈은 슬라이스, 세그먼트의 내부 구조에 직접 접근하지 않고 제공해주는 public API를 통해서만 접근해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;좋은 public API의 최소 세 가지 조건&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 public API는 슬라이스를 다른 코드와 통합하기 쉽고 안정적으로 유지보수할 수 있게 해준다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내부 구조 변경에 영향이 없어야 한다. - 슬라이스 내부 폴더 구조를 바꾸더라도 외부 코드는 그대로 동작해야 한다.&lt;/li&gt;
&lt;li&gt;주요 동작 변경 = API 변경 - 슬라이스 동작이 크게 바뀌어 기존 기대가 깨진다면 public API도 함께 변경되어야 한다.&lt;/li&gt;
&lt;li&gt;필요한 부분만 노출 - 슬라이스 전체 구현을 공개하지 않고 외부에서 꼭 필요한 기능만 선별해서 노출한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안 좋은 예시로는 무분별한 wildcard re-export가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 초기에는 편의상 한 줄로 모든 것을 export하려고 와일드카드 re-export를 할 수도 있으나 이런 방식은 슬라이스의 인터페이스를 흐리게 만들고 나중에 큰 부담이 된다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// ❌ 금지
export * from &quot;./ui/Comment&quot;; // 무분별한 UI export
export * from &quot;./model/comments&quot;; // 내부 모델 노출&lt;/code&gt;&lt;/pre&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;발견 가능성 저하 - public API에서 어떤 기능을 제공하는지 한눈에 파악하기 어렵다.&lt;/li&gt;
&lt;li&gt;내부구현 노출 - 외부에서 알 필요가 없는 내부 코드를 외부에서 직접 사용하게 되고 이 코드에 대한 의존성이 생기면 리팩토링이 어려워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cross-import를 위한 public API&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 정리한 내용이지만 크로스 임포트는 같은 레이어 안에서 한 슬라이스가 다른 슬라이스를 임포트하는 것을 말한다. 레이어 import rule에 따라 원칙적으로는 금지되지만 엔티티 간 참조처럼 현실적으로 불가피한 경우가 있다. 예를 들어서 도메인 모델에서 artist 엔티티와 song 엔티티가 서로 연관성을 가진다면 억지로 숨기기보다 코드적으로 그 관계를 드러내는 편이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 &lt;code&gt;@x&lt;/code&gt; 표기를 사용해서 교차 참조 전용 public API를 명시적으로 구현한다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;-   entities
    -   artist
        -   @x
            - song.ts &amp;mdash; entities/song 전용 Public API
        - index.ts &amp;mdash; 일반 Public API&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import type { Artist } from &quot;entities/artist/@x/song&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크로스 임포트는 반드시 최소화 해야하며 허용하더라도 엔티티 레이어에서만 사용할 것을 권장한다. 다른 레이어에서는 가능한 의존관계를 제거하고 설계를 다시 검토하는 것이 좋다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 정리된 내용만 보자면 FSD를 도입하면 다음과 같은 장점이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일관성 - 구조가 표준화되어 팀 간 협업과 신규 멤버 온보딩이 쉬워진다.&lt;/li&gt;
&lt;li&gt;격리성 - Layer와 Slice간 의존성을 제한해서 특정 모듈만 안전하게 수정 가능 -&amp;gt; 무슨 소리?&lt;/li&gt;
&lt;li&gt;재사용 범위 제어 - 재사용 가능한 코드를 필요한 범위에서만 활용할 수 있어서 DRY 원칙과 실용성을 균형있게 유지한다.&lt;/li&gt;
&lt;li&gt;도메인 중심 구조 - 비즈니스 용어 기반의 구조로 되어있어서 전체 코드를 몰라도 특정 기능을 독립적으로 구현 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용하다보면 규칙에 맞추기 어려운 지점이 있을 수 있으나 현재 시점에서는 가장 체계적인 아키텍처로 보인다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 FSD 활용 시 주의할 점에 대해서 포스팅할 예정이다.&lt;/p&gt;</description>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/4</guid>
      <comments>https://harubanbo.tistory.com/4#entry4comment</comments>
      <pubDate>Sat, 25 Apr 2026 21:58:47 +0900</pubDate>
    </item>
    <item>
      <title>FSD 아키텍처 정독하기 2편</title>
      <link>https://harubanbo.tistory.com/3</link>
      <description>&lt;h2&gt;1. 엔티티 간 관계와 상호작용&lt;/h2&gt;
&lt;p&gt;원칙적으로는 엔티티 슬라이스끼리는 서로를 모르는 상태가 이상적이다. 하지만 실제 애플리케이션에서는 한 엔티티가 다른 엔티티를 포함하거나 여러 엔티티가 서로 상호작용하는 일이 자주 발생한다. 이런 경우 두 엔티티 간의 구체적인 상호작용 로직은 &lt;strong&gt;상위 레이어(features, pages 등등)로 올려서 처리&lt;/strong&gt;하는 것이 좋다.&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;만약 한 엔티티의 데이터 안에 다른 엔티티가 포함되어야 한다면 &lt;code&gt;@x&lt;/code&gt; &lt;strong&gt;cross-import 표기법을 사용해서 교차 public API를 통해 연결되었음을 명시&lt;/strong&gt;해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import type { Song } from &amp;quot;entities/song/@x/artist&amp;quot;;

export interface Artist {
    name: string;
    songs: Array&amp;lt;Song&amp;gt;; // Artist 엔티티 안에 Song 엔티티를 포함
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
쇼핑몰 도메인과 관련해서 엔티티의 구조가 다음과 같다면

&lt;pre&gt;&lt;code&gt;  entities/
    user/
      ui/         # UserAvatar, UserCard
      model/      # User 타입, userStore
      api/        # fetchUser
    product/
      ui/         # ProductCard, ProductImage
      model/      # Product 타입
      api/        # fetchProduct
    cart/
      ui/         # CartItem
      model/      # Cart 타입, cartStore
      api/        # fetchCart&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;
장바구니에 상품 추가하기 기능을 만들어야 한다면 이것은 product와 cart라는 두 엔티티가 필요해진다.

&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// ❌ 잘못된 방법 - entities/cart에서 product를 직접 import
// entities/cart/ui/AddToCartButton.tsx
import { Product } from &amp;#39;entities/product&amp;#39;;  // ❌ 같은 레이어 참조 금지 규칙 위반

function AddToCartButton({ product }: { product: Product }) {
  const addToCart = useCartStore(state =&amp;gt; state.add);

  return (
    &amp;lt;button onClick={() =&amp;gt; addToCart(product)}&amp;gt;
      장바구니 담기
    &amp;lt;/button&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
위와 같은 이슈를 해결하는 첫 번째 방법은 상위 레이어에서 두 엔티티의 상호작용을 조합하는 방법이다.
첫 번째 해결 방법: 상위 레이어(Features)로 올리기 - 두 엔티티 간의 구체적인 상호작용 로직은 상위 레이어(Feature 또는 Page)로 올려서 처리를 권장

&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;  features/
    add-to-cart/           #   상호작용을 Feature로 분리
      ui/
        AddToCartButton.tsx
      index.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
✅ 올바른 방법 - features에서 두 entity를 조합

&lt;pre&gt;&lt;code&gt;// features/add-to-cart/ui/AddToCartButton.tsx
import { Product } from &amp;#39;entities/product&amp;#39;;  // ✅ 하위 레이어
import { useCartStore } from &amp;#39;entities/cart&amp;#39;; // ✅ 하위 레이어

interface AddToCartButtonProps {
  product: Product;
}

export function AddToCartButton({ product }: AddToCartButtonProps) {
  const addToCart = useCartStore(state =&amp;gt; state.add);

  const handleClick = () =&amp;gt; {
    addToCart({
      productId: product.id,
      name: product.name,
      price: product.price,
      quantity: 1,
    });
  };

  return (
    &amp;lt;button onClick={handleClick}&amp;gt;
      장바구니 담기
    &amp;lt;/button&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;
전체 구조로 보기

&lt;pre&gt;&lt;code&gt;  entities/
    user/
    # User 자체에 관한 것만
    product/
    # Product 자체에 관한 것만 (ProductCard, fetchProduct)
    cart/
    # Cart 자체에 관한 것만 (cartStore, CartItem)

  features/
    add-to-cart/
    # Product → Cart 상호작용 ✅
    checkout/
    # Cart + User → 결제 상호작용 ✅
    product-review/
    # User + Product → 리뷰 작성 상호작용 ✅&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;
페이지에서 조합하면 다음과 같다.

&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// pages/product-detail/ui/ProductDetailPage.tsx
import { ProductCard } from &amp;#39;entities/product&amp;#39;;
import { AddToCartButton } from &amp;#39;features/add-to-cart&amp;#39;;

export function ProductDetailPage() {
  const product = useLoaderData();

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;ProductCard product={product} /&amp;gt;
      &amp;lt;AddToCartButton product={product} /&amp;gt;  {/* Feature 사용 */}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br&gt;하지만 상황에 따라서 엔티티 데이터 안에 다른 엔티티가 포함되어야 하는 등 어쩔 수 없이 같은 레이어의 다른 슬라이스를 참조해야할 때 예외적으로 &lt;code&gt;@x&lt;/code&gt; cross-import를 통해 해결한다. (추후 나오는 public API의 일환으로 볼 수 있다)&lt;br&gt;&lt;br/&gt;&lt;br&gt;&lt;strong&gt;1단계:&lt;/strong&gt; 예를 들어서 artist 엔티티가 song 엔티티 목록을 가지고 있는 경우라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// entities/artist/model/artist.ts
interface Artist {
  name: string;
  songs: Array&amp;lt;Song&amp;gt;;  //   Song 타입이 필요함!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
이 경우 `entities/artist`에서 `entities/song`의 타입을 참조해야 하는데 FSD 규칙상 같은 레이어의 슬라이스끼리는 참조가 금지
@x 표기법 사용 방법
1단계: Song에서 교차 참조용 Public API 생성

&lt;pre&gt;&lt;code&gt;  entities/
    song/
      @x/                    #   교차 참조 전용 폴더
        artist.ts            # artist를 위한 export
      model/
        types.ts
      index.ts               # 일반 Public API&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;
song 엔티티에서는 artist가 사용할 것만 export하는데 이때 `@x` 폴더 자체가 cross-import용 public API 역할을 하므로 별도로 song 엔티티의 index.ts에서 export를 해줄 필요는 없다.

&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// entities/song/@x/artist.ts
export type { Song } from &amp;#39;../model/types&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;&lt;strong&gt;2단계:&lt;/strong&gt; artist 엔티티에서 &lt;code&gt;@x&lt;/code&gt; 경로로 song 엔티티를 import&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// entities/artist/model/artist.ts
import type { Song } from &amp;#39;entities/song/@x/artist&amp;#39;;  //   @x 경로 사용

export interface Artist {
  name: string;
  songs: Array&amp;lt;Song&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
왜 이렇게 하는가? - 의존성이 명확하게 보임

&lt;pre&gt;&lt;code&gt;  entities/song/@x/
    artist.ts    # &amp;quot;song이 artist에게 뭔가를 제공하고 있구나&amp;quot;
    playlist.ts  # &amp;quot;song이 playlist에게도 뭔가를 제공하고 있구나&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;이렇게 cross-import를 하는 이유는 &lt;strong&gt;함께 리팩토링할 코드를 알 수 있다는 장점&lt;/strong&gt;이 있다. 연결된 엔티티들은 함께 리팩토링이 필요하구나! 하는 연결관계를 놓치지 않게 명시하므로 &lt;code&gt;@x&lt;/code&gt; 폴더만 보면 &lt;strong&gt;이 엔티티가 누구와 연결되어있는지 바로 파악이 가능&lt;/strong&gt;하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// ❌ 일반 import - 금지됨
import { Song } from &amp;#39;entities/song&amp;#39;;

// ✅ @x import - 허용 (교차 참조 명시)
import type { Song } from &amp;#39;entities/song/@x/artist&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 엔티티에서 얘기하는 도메인이란? (별도로 찾아본 내용 정리)&lt;/h2&gt;
&lt;p&gt;FSD에서 엔티티는 프로젝트가 다루는 &lt;strong&gt;핵심 비즈니스 도메인 객체&lt;/strong&gt;를 표현하는 레이어로 DDD의 도메인과 FSD에서 다루는 도메인은 관련은 있으나 같지는 않다. DDD는 비즈니스 문제 영역 전체를 깊이 있게 모델링했다면 FSD의 도메인은 프론트에서 다루는(보여지는) 핵심 데이터 개념 위주로 이루어진다고 보인다. 결국 비즈니스 규칙을 정밀하게 모델링하는 것이 아닌 파일 구조를 비즈니스 의미 단위로 분류하기 위한 목적이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이 프로젝트에서 어떤 것들을 다루나&lt;/li&gt;
&lt;li&gt;주로 UI 표현과 API 데이터 타입에 집중&lt;/li&gt;
&lt;li&gt;도메인 간 얽혀있는 복잡한 비즈니스 규칙은 백엔드에 위임&lt;/li&gt;
&lt;li&gt;프론트에서 다루는 핵심인 것들에 대한 UI 표현과 데이터 타입을 의미&lt;br/&gt;
FSD Entity의 실제 역할 - &quot;이 앱에서 다루는 핵심 '것'들의 UI 표현과 데이터 타입&quot;

&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;  entities/
    user/          # &amp;quot;유저&amp;quot;라는 개념
      ui/          # UserAvatar, UserCard (어떻게 보여줄까)
      model/       # User 타입 (어떤 데이터인가)
      api/         # fetchUser (어떻게 가져올까)

    product/       # &amp;quot;상품&amp;quot;이라는 개념
    order/         # &amp;quot;주문&amp;quot;이라는 개념
    comment/       # &amp;quot;댓글&amp;quot;이라는 개념&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
핵심 차이는 DDD는 &quot;주문에 상품을 추가할 때 재고 확인, 할인 적용, 최소 금액 체크...&quot; 등 복잡한 비즈니스 규칙을 도메인 모델 내부에서 처리하고 FSD는 &quot;주문 데이터를 받아서 화면에 보여주자&quot; 등 세부적인 비즈니스 규칙은 백엔드가 처리하고 프론트는 결과를 표시
&lt;br/&gt;&lt;br/&gt;
정리하면 FSD 도메인은 DDD에서 영감을 받아오지만 프론트엔드에 맞게 단순화된 개념으로 볼 수 있을 것이다. DDD는 비즈니스를 코드로 모델링하고 FSD에서 말하는 엔티티 도메인은 프로젝트에서 다루는 핵심 개념들을 폴더로 구분하는 느낌이다.</description>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/3</guid>
      <comments>https://harubanbo.tistory.com/3#entry3comment</comments>
      <pubDate>Sat, 11 Apr 2026 18:24:51 +0900</pubDate>
    </item>
    <item>
      <title>FSD 아키텍처 정독하기 1편</title>
      <link>https://harubanbo.tistory.com/2</link>
      <description>&lt;p&gt;현재 프론트 아키텍처 중 얘기가 많이 나오는 FSD 아키텍처에 대해 숙지할 필요가 있어서 FSD 공식문서를 읽고 두 번에 걸쳐서 내용을 정리할 예정이며 본 포스팅은 FSD 아키텍처의 기본적인 개념에 대해 정리하고 두 번째에서 FSD를 쓰면 무엇이 좋은지 주의할 점은 없는지 좀 더 세부적인 내용을 기록할 예정이다.&lt;/p&gt;
&lt;h2&gt;1. 개요&lt;/h2&gt;
&lt;p&gt;FSD (Feature-Sliced Design) 는 프론트엔드 애플리케이션 코드를 구조화하기 위한 아키텍처 방법론이다. 이 방법론은  요구사항이 바뀌어도 코드 구조가 무너지지 않고, 새 기능을 쉽게 추가할 수 있는 프로젝트를 만드는 것으로 FSD가 얼마나 많은 책임을 가지는지, 다른 모듈에 얼마나 의존하는지에 따라 계층화한다.&lt;br&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;2. 당장 적용할 필요가 있는가?&lt;/h2&gt;
&lt;p&gt;단순 라이브러리보다 웹, 모바일, 데스크톱 등 프론트엔드 애플리케이션을 만드는 프로젝트에 어울리는 구조인데 어떤 언어나 프로젝트 환경에라도 적용이 가능하다. 다만 현재 프로젝트 구조에 특별한 문제가 없다면 굳이 바꿀 필요는 없으며 모든 프로젝트에 맞는 아키텍처도 아니다. 필요하다면 기존 프로젝트 구조에서 점진적으로 마이그레이션 하는 방법도있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프로젝트가 커지면서 구조가 복잡해지고 유지보수 속도가 느려지는 경우&lt;/li&gt;
&lt;li&gt;기존 폴더 구조를 이해하기 힘든 경우&lt;br/&gt;

&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. FSD의 기본 구조&lt;/h2&gt;
&lt;p&gt;FSD 구조는 크게 레이어(Layer), 슬라이스(Slice), 세그먼트(Segment) 레벨로 구분된다.&lt;/p&gt;
&lt;p&gt;레이어는 FSD 프로젝트의 표준 최상위 폴더로 아래와 같은 폴더 구조를 가진다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;App 폴더 - Routing, EntryPoint, Global Styles, Provider등 &lt;strong&gt;앱을 실행하는 모든 요소&lt;/strong&gt;가 여기에 모인다. 주로 앱 전체에 적용되는 설정과 관련된 항목들이 포함&lt;/li&gt;
&lt;li&gt;Pages 폴더 - 라우팅 기준으로 구성된 주요 화면단위의 페이지를 의미(어떤 widget, feature, entity를 조합할지 정의하는 레이어)&lt;/li&gt;
&lt;li&gt;Widgets 폴더 - 크고 독립적으로 동작하는 UI 구성 단위, 일반적으로 하나의 완결된 화면 기능을 제공&lt;/li&gt;
&lt;li&gt;Features 폴더 - 사용자에게 비즈니스 가치를 제공하는 액션을 구현한 재사용 가능한 제품 기능 단위&lt;/li&gt;
&lt;li&gt;Entities 폴더 - 프로젝트가 다루는 비즈니스 Entity&lt;/li&gt;
&lt;li&gt;Shared 폴더 - 모든 Layer에서 재사용되는 코드(라이브러리, 유틸리티 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3-1. 레이어&lt;/h3&gt;
&lt;p&gt;레이어는 FSD에서 코드를 나눌 때 사용하는 가장 큰 구분 단위다. &lt;strong&gt;코드를 나눌 때는 각 부분이 어떤 역할을 맡는지, 다른 코드에 얼마나 의존하는지를 기준&lt;/strong&gt;으로 한다.&lt;/p&gt;
&lt;p&gt;총 6개의 레이어가 있으며 아래로 내려갈수록 담당하는 기능과 의존성이 줄어드는 순서다 (담당 작업의 규모가 줄어든다는 의미) 모든 레이어를 프로젝트에 사용하는 것은 아니고 &lt;strong&gt;필요한 레이어만 골라서&lt;/strong&gt; 쓴다. 그리고 새로운 레이어를 직접 정의해서 사용하는 것은 권장하지 않는다. 각 레이어의 역할이 이미 표준으로 충분히 정의되어있기 때문이다. 추가로 상위 레이어는 하위 레이어를 참조할 수 있지만 하위 레이어가 상위 레이어를 참조하는 것은 불가능하다. (pages는 features나 entities의 모듈을 참조할 수 있지만 features가 pages를 참조 불가능)&lt;/p&gt;
&lt;p&gt;각 레이어마다 각자 여러 개의 슬라이스(서로 밀접하게 연관된 모듈 묶음)로 구성된다. 그리고 &lt;strong&gt;슬라이스들 사이의 연결은 레이어 단방향 import 규칙을 통해 제한&lt;/strong&gt;한다. 하나의 슬라이스 안에서 작성된 코드는 위에서 얘기한 것처럼 &lt;strong&gt;자신이 속한 레이어보다 아래 레이어에 있는 다른 슬라이스만 단방향으로 import 가능&lt;/strong&gt;하다.&lt;/p&gt;
&lt;p&gt;예를 들어서 &lt;code&gt;  ~/features/aaa/api/request.ts&lt;/code&gt; 는 다음의 경우에만 다른 slice를 import할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;같은 레이어의 &lt;code&gt;  ~/features/bbb&lt;/code&gt; 는 import 불가능 (같은 레이어의 다른 슬라이스를 의미)&lt;ul&gt;
&lt;li&gt;같은 레이어에서 상호 참조가 필요한 경우에는 상위 레이어에서 조합하거나 예외적으로 &lt;code&gt;@x&lt;/code&gt; 를 사용한 cross-import를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;하위 레이어의 &lt;code&gt;  ~/entities&lt;/code&gt;, &lt;code&gt;  ~/shared&lt;/code&gt; 등 내부 슬라이스 요소들은 import 가능&lt;/li&gt;
&lt;li&gt;같은 slice의 &lt;code&gt;  ~/features/aaa/lib/cache.ts&lt;/code&gt; 는 import 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;app과 shared는 레이어이면서 동시에 하나의 큰 슬라이스처럼 동작한다. 그래서 내부에는 바로 세그먼트가 위치한다. 그래서 &lt;strong&gt;app과 shared 레이어 내부에서 세그먼트끼리는 자유롭게 import 가능&lt;/strong&gt;하다. shared는 비즈니스 도메인이 없기 때문이고, app은 모든 도메인을 묶는 상위 조정자 역할을 하기 때문이다.&lt;br&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;shared 레이어&lt;/strong&gt;는 여러 곳에서 사용하는 UI 요소나 라이브러리 등 도구가 위치하는 곳이다. App과 동일하게 슬라이스 없이 세그먼트만 존재하는데 세그먼트의 예시는 보통 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;api 폴더 - API 클라이언트나 공통 백엔드 요청함수 등 API 호출에 대한 공통적으로 적용되어야 하는 래퍼나 공통 트리거 함수 등등&lt;/li&gt;
&lt;li&gt;ui 폴더 - 공통 UI 컴포넌트가 위치하며 비즈니스 로직이 없는 순수한 버튼, 모달, 폼 입력 등의 컴포넌트가 위치한다. 단, UI 자체 로직을 포함하는 컴포넌트는 허용된다.&lt;ul&gt;
&lt;li&gt;비즈니스 로직이란 앱이 무엇을 하는가에 대한 로직으로 API 호출이나 도메인 데이터 처리, 인증권한체크, 비즈니스 규칙 적용 등의 내용을 의미&lt;/li&gt;
&lt;li&gt;UI자체 로직이라는 것은 열림 닫힘 상태, 포커스 관리, 키보드 네비게이션, 애니메이션, 입력값 필터링 등의 단순 로직을 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;lib 폴더 - 단순히 utils나 helpers를 모아두는 폴더를 말하는 게 아니며 lib/datetime등 폴더명이 명확하게 하나의 주제에 대해 목적이 무엇인지 드러나야 한다.&lt;/li&gt;
&lt;li&gt;config 폴더 - 환경변수 관리 등&lt;/li&gt;
&lt;li&gt;routes 폴더 - 라우팅 기본요소 및 라우트 상수 등&lt;/li&gt;
&lt;li&gt;i18n 폴더 - 언어 지원에 대한 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;가끔가다 앱 전체에 걸쳐서 동일하게 사용되어야 하는 로직이 존재한다. FSD 구조에서 슬라이스는 격리되도록 설계되었기 때문에 코드 재사용은 더 낮은 계층인 shared에 의해 촉진된다. shared 계층은 계층과 슬라이스의 하이브리드 느낌이다. 일반적으로 shared 코드는 미리 계획되지 않고 개발 중에 추출될 여지가 크다. 실제로 어떤 코드 부분이 공유되는지는 개발 중에 명확해지기 때문인데 어떤 종류의 코드가 자연스럽게 shared에 속하는지 알아두는 것은 도움이 될 수 있다.&lt;br&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;세그먼트의 이름은 이 폴더가 무엇을 하는지 목적이 드러나는 이름&lt;/strong&gt;이어야 한다. 그냥 components, hooks, types처럼 모호한 이름은 지양한다. (상황에 따라 다르지만 이런 이름을 쓰는 경우도 있으나 쓰더라도 이 폴더 안에 무엇이 있는지 바로 인지가 가능한 것이 좋다) 가급적 세그먼트에는 README를 통해 역할과 범위를 문서화하는 것을 권장한다.&lt;br&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;entities 레이어&lt;/strong&gt;는 프로젝트에서 다루는 핵심 비즈니스 개념을 표현한다. 대부분의 경우에는 실제 도메인 용어와 일치한다. 각 엔티티 슬라이스의 세그먼트는 다음과 같이 구성될 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;model 폴더 - 데이터 상태, 도메인 로직, 검증 스키마&lt;/li&gt;
&lt;li&gt;api 폴더 - 해당 엔티티와 관련된 API 요청&lt;/li&gt;
&lt;li&gt;ui 폴더 - 엔티티의 시각적 표현으로 완성된 큰 UI 블록이 아니어도 됨, 여러 페이지에서 재사용 가능한 형태로 설계하고 비즈니스 로직은 가능하면 props/slot으로 외부에서 주입하는 방식 권장&lt;br/&gt;

&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;features 레이어&lt;/strong&gt;는 사용자가 애플리케이션에서 수행하는 주요 기능이 들어간다. 보통 하나 이상의 엔티티와 연관되어 동작하는데 다음을 고려해서 구성한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 동작을 무조건 feature로 만들 필요는 없다. feature가 많아지면 중요한 기능이 어디 있는지 찾기 어려워질 수 있다.&lt;/li&gt;
&lt;li&gt;여러 페이지에서 재사용되는 기능일 때 feature로 추출하는 걸 고려한다.&lt;ul&gt;
&lt;li&gt;예를 들어서 여러 종류의 에디터에서 동일한 댓글 기능을 사용한다면 comments라는 명칭으로 feature를 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;feature의 구성은 보통은 다음과 같다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ui 폴더 - 상호작용 UI(폼, 검색 바 등)&lt;/li&gt;
&lt;li&gt;api 폴더 - 해당 기능과 직접 관련된 API 요청&lt;/li&gt;
&lt;li&gt;model 폴더 - 검증 로직, 내부 상태 관리&lt;/li&gt;
&lt;li&gt;config 폴더 - feature flag 등 기능별 설정…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;pages와 features 레이어만 훑어봐도 이 앱이 어떤 기능을 제공하는지 대략 이해할 수 있도록 구성하는게 좋다.&lt;br&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;widgets 레이어&lt;/strong&gt;는 독립적으로 동작하는 비교적 큰 UI 블록을 두는 곳이다. 여러 페이지에서 재사용되거나 한 페이지에서 큰 섹션 단위로 나누어지는 UI 블록이 있을 때 유용하다. 만약 재사용되지 않고 특정 페이지의 핵심 콘텐츠에만 쓰인다면 굳이 위젯으로 분리하지 말고 페이지 레이어 내부에 두는 것이 좋다.&lt;/p&gt;
&lt;p&gt;nested routing 환경에서는 위젯이 페이지와 비슷한 역할을 할 수 있다. 예를 들어서 데이터 로딩, 로딩 상태 표시, 에러처리 등을 모두 포함하는 하나의 라우터 단위 UI 블록으로 동작할 수 있다.&lt;br&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pages 레이어&lt;/strong&gt;는 웹,앱에서는 보이는 화면(screen) 또는 액티비티(activity)에 해당한다.&lt;/p&gt;
&lt;p&gt;일반적으로  페이지1개 - 슬라이스1개 구조를 많이 사용하지만 구조가 유사한 페이지들은  하나의 슬라이스로 묶는 것도 가능하다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;slice group? - 슬라이스가 많아지면 평탄하게 나열하는 것만으로는 관리가 여려워지므로 관련된 슬라이스들을 폴더로 묶어서 정리할 수도 있다. pages 레이어에만 해당되는 게 아니라 다른 레이어의 슬라이스에도 동일하게 적용될 수 있는 개념이다. 단, 단순히 정리 목적일 뿐이라서 그룹 자체가 슬라이스처럼 index.ts같은 public API를 가지면 안되고 FSD의 import 규칙(단방향, 같은 레이어의 슬라이스간 import 금지)은 그대로 적용된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;    pages/
      home/           # 홈 화면
      profile/        # 프로필 화면
      settings/       # 설정 화면
      product-detail/ # 상품 상세 화면

    pages/
      auth/           # 로그인 + 회원가입 (구조가 비슷해서 그룹으로 묶음)
        ui/
          LoginPage.tsx
          RegisterPage.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;코드를 찾기만 쉽다면 페이지의 슬라이스의 갯수에 특별한 제한은 없으며 재사용되지 않는 UI는 그대로 페이지 내부에 두면 된다.(다른 페이지에서 재사용하지 않는 컴포넌트는 굳이 &lt;code&gt;widgets/&lt;/code&gt;나 &lt;code&gt;shared/&lt;/code&gt;로 분리할 필요 없이 페이지 내부에 두면 된다)&lt;/p&gt;
&lt;p&gt;페이지 레이어에는 보통 전용 모델이 없으며 필요한 경우 간단한 상태만 컴포넌트 내부에서 관리한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ui 폴더 - 페이지 UI, 로딩상태, 에러상태처리&lt;/li&gt;
&lt;li&gt;api 폴더 - 페이지에서 사용하는 데이터 페칭, 변경 요청&lt;br/&gt;

&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;app 레이어&lt;/strong&gt;는 앱 전역에서 동작하는 환경설정과 공용 로직을 관리하는 앱 전체를 실행시킬 때 영향을 주는 설정과 초기화를 담당하는 레이어다.&lt;/p&gt;
&lt;p&gt;예를 들어서 라우터 설정, 전역 상태관리(store설정) , 글로벌 스타일 앱 진입점 설정 등과 같이 앱 전체에 영향을 주는 코드가 위치한다. shared와 마찬가지로 슬라이스 없이 세그먼트로만 구성된다.&lt;/p&gt;
&lt;p&gt;대표적인 세그먼트 예시&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  app/
├──   providers/      # React Query, Redux, Theme Provider 등
├──   routes/         # 라우팅 설정
├──   styles/         # 전역 CSS, CSS 변수
├──   config/         # 환경 설정, 앱 설정
└──   types/          # 전역 타입 선언
└──   store/
└──   entrypoint/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;앱이 어떻게 구성이 되는지를 정의하므로 비즈니스 로직이 없고 순수한 인프라적인 코드가 들어간다.&lt;/p&gt;</description>
      <category>fsd</category>
      <category>프론트</category>
      <author>harubanbo</author>
      <guid isPermaLink="true">https://harubanbo.tistory.com/2</guid>
      <comments>https://harubanbo.tistory.com/2#entry2comment</comments>
      <pubDate>Sat, 28 Mar 2026 17:34:55 +0900</pubDate>
    </item>
  </channel>
</rss>